From 7a291fb3bef88e831c30725e3db0f3c88ef9427a Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 2 Apr 2024 09:25:44 +0700 Subject: [PATCH 001/161] feat: keycloak user and role management --- src/controllers/keycloak/user-controller.ts | 51 +++++++++++++++++++++ src/services/keycloak.ts | 10 ++-- 2 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 src/controllers/keycloak/user-controller.ts diff --git a/src/controllers/keycloak/user-controller.ts b/src/controllers/keycloak/user-controller.ts new file mode 100644 index 0000000..086efe6 --- /dev/null +++ b/src/controllers/keycloak/user-controller.ts @@ -0,0 +1,51 @@ +import { Body, Controller, Delete, Get, Path, Post, Route, Security, Tags } from "tsoa"; +import { addUserRoles, createUser, getRoles, removeUserRoles } from "../../services/keycloak"; + +@Route("api/keycloak") +@Tags("Keycloak") +@Security("keycloak") +export class KeycloakController extends Controller { + @Post("user") + async createUser( + @Body() body: { username: string; password: string; firstName?: string; lastName?: string }, + ) { + return await createUser(body.username, body.password, { + firstName: body.firstName, + lastName: body.lastName, + }); + } + + @Get("role") + async getRole() { + const role = await getRoles(); + if (Array.isArray(role)) return role; + throw new Error("Failed. Cannot get role."); + } + + @Post("{userId}/role") + async addRole(@Path() userId: string, @Body() body: { role: string[] }) { + const list = await getRoles(); + + if (!Array.isArray(list)) throw new Error("Failed. Cannot get role(s) data from the server."); + + const result = await addUserRoles( + userId, + list.filter((v) => body.role.includes(v.id)), + ); + + if (!result) throw new Error("Failed. Cannot set user's role."); + } + + @Delete("{userId}/role/{roleId}") + async deleteRole(@Path() userId: string, @Path() roleId: string) { + const list = await getRoles(); + + if (!Array.isArray(list)) throw new Error("Failed. Cannot get role(s) data from the server."); + + const result = await removeUserRoles( + userId, + list.filter((v) => roleId === v.id), + ); + if (!result) throw new Error("Failed. Cannot remove user's role."); + } +} diff --git a/src/services/keycloak.ts b/src/services/keycloak.ts index 7a5ce4a..c12a3b4 100644 --- a/src/services/keycloak.ts +++ b/src/services/keycloak.ts @@ -121,7 +121,7 @@ export async function getRoles(name?: string) { const data = await res.json(); if (Array.isArray(data)) { - return data.map((v: Record) => ({ id: v.id, name: v.name })); + return data.map((v: Record) => ({ id: v.id, name: v.name })); } return { @@ -137,7 +137,7 @@ export async function getRoles(name?: string) { * * @returns true if success, false otherwise. */ -export async function addUserRoles(userId: string, roleId: string[]) { +export async function addUserRoles(userId: string, roles: { id: string; name: string }[]) { const res = await fetch( `${KC_URL}/admin/realms/${KC_REALM}/users/${userId}/role-mappings/realm`, { @@ -147,7 +147,7 @@ export async function addUserRoles(userId: string, roleId: string[]) { "content-type": `application/json`, }, method: "POST", - body: JSON.stringify(roleId.map((v) => ({ id: v }))), + body: JSON.stringify(roles), }, ).catch((e) => console.log(e)); @@ -165,7 +165,7 @@ export async function addUserRoles(userId: string, roleId: string[]) { * * @returns true if success, false otherwise. */ -export async function removeUserRoles(userId: string, roleId: string[]) { +export async function removeUserRoles(userId: string, roles: { id: string; name: string }[]) { const res = await fetch( `${KC_URL}/admin/realms/${KC_REALM}/users/${userId}/role-mappings/realm`, { @@ -175,7 +175,7 @@ export async function removeUserRoles(userId: string, roleId: string[]) { "content-type": `application/json`, }, method: "DELETE", - body: JSON.stringify(roleId.map((v) => ({ id: v }))), + body: JSON.stringify(roles), }, ).catch((e) => console.log(e)); From d58046502c0e6b083c954d86c8b3e033e72c4744 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 2 Apr 2024 09:26:29 +0700 Subject: [PATCH 002/161] feat: add province, district, sub-district endpoint --- src/controllers/address-controller.ts | 56 +++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/controllers/address-controller.ts diff --git a/src/controllers/address-controller.ts b/src/controllers/address-controller.ts new file mode 100644 index 0000000..c836829 --- /dev/null +++ b/src/controllers/address-controller.ts @@ -0,0 +1,56 @@ +import { Controller, Get, Path, Route, Tags } from "tsoa"; +import prisma from "../db"; + +@Route("api/address") +@Tags("Address") +export class AddressController extends Controller { + @Get("province") + async getProvince() { + return await prisma.province.findMany(); + } + + @Get("province/{provinceId}") + async getProvinceById(@Path() provinceId: string) { + return await prisma.province.findFirst({ + where: { id: provinceId }, + }); + } + + @Get("province/{provinceId}/district") + async getDistrictOfProvince(@Path() provinceId: string) { + return await prisma.district.findMany({ + where: { provinceId }, + }); + } + + @Get("district") + async getDistrict() { + return await prisma.district.findMany(); + } + + @Get("district/{districtId}") + async getDistrictOfId(@Path() districtId: string) { + return await prisma.province.findFirst({ + where: { id: districtId }, + }); + } + + @Get("district/{districtId}/sub-district") + async getSubDistrictOfDistrict(@Path() districtId: string) { + return await prisma.subDistrict.findMany({ + where: { districtId }, + }); + } + + @Get("sub-district") + async getSubDistrict() { + return await prisma.subDistrict.findMany(); + } + + @Get("sub-district/{subDistrictId}") + async getSubDistrictOfId(@Path() subDistrictId: string) { + return await prisma.subDistrict.findFirst({ + where: { id: subDistrictId }, + }); + } +} From 3a5339774549220cee8da0b6cb0e051fe4a00ef6 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 2 Apr 2024 09:28:36 +0700 Subject: [PATCH 003/161] feat: add branch controller --- src/controllers/branch/branch-controller.ts | 25 +++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/controllers/branch/branch-controller.ts diff --git a/src/controllers/branch/branch-controller.ts b/src/controllers/branch/branch-controller.ts new file mode 100644 index 0000000..51d03f3 --- /dev/null +++ b/src/controllers/branch/branch-controller.ts @@ -0,0 +1,25 @@ +import { Prisma } from "@prisma/client"; +import { + Body, + Controller, + Delete, + Get, + Patch, + Path, + Post, + Query, + Request, + Route, + Security, + Tags, +} from "tsoa"; + +import prisma from "../../db"; +import HttpError from "../../interfaces/http-error"; +import HttpStatus from "../../interfaces/http-status"; +import { RequestWithUser } from "../../interfaces/user"; +@Route("api/branch") +@Tags("Branch") +@Security("keycloak") +export class BranchController extends Controller { +} From 59a79a3374405580aa491899c9cc7782bc4af8a9 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 2 Apr 2024 09:29:20 +0700 Subject: [PATCH 004/161] feat: get branch endpoint --- src/controllers/branch/branch-controller.ts | 43 +++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/controllers/branch/branch-controller.ts b/src/controllers/branch/branch-controller.ts index 51d03f3..7f8c448 100644 --- a/src/controllers/branch/branch-controller.ts +++ b/src/controllers/branch/branch-controller.ts @@ -22,4 +22,47 @@ import { RequestWithUser } from "../../interfaces/user"; @Tags("Branch") @Security("keycloak") export class BranchController extends Controller { + @Get() + async getBranch( + @Query() zipCode?: string, + @Query() query: string = "", + @Query() page: number = 1, + @Query() pageSize: number = 30, + ) { + const where = { + OR: [ + { nameEN: { contains: query }, zipCode }, + { nameTH: { contains: query }, zipCode }, + { email: { contains: query }, zipCode }, + ], + } satisfies Prisma.BranchWhereInput; + + const [result, total] = await prisma.$transaction([ + prisma.branch.findMany({ + include: { + province: true, + district: true, + subDistrict: true, + }, + where, + take: pageSize, + skip: (page - 1) * pageSize, + }), + prisma.branch.count({ where }), + ]); + + return { result, page, pageSize, total }; + } + + @Get("{branchId}") + async getBranchById(@Path() branchId: string) { + return await prisma.branch.findFirst({ + include: { + province: true, + district: true, + subDistrict: true, + }, + where: { id: branchId }, + }); + } } From 9938dba0e1d7587bd8a8d7285f3fa7142571cb61 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 2 Apr 2024 10:45:37 +0700 Subject: [PATCH 005/161] feat: delete branch endpoint --- src/controllers/branch/branch-controller.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/controllers/branch/branch-controller.ts b/src/controllers/branch/branch-controller.ts index 7f8c448..99e78b2 100644 --- a/src/controllers/branch/branch-controller.ts +++ b/src/controllers/branch/branch-controller.ts @@ -65,4 +65,19 @@ export class BranchController extends Controller { where: { id: branchId }, }); } + + @Delete("{branchId}") + async deleteBranch(@Path() branchId: string) { + const record = await prisma.branch.findFirst({ where: { id: branchId } }); + + if (!record) { + throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found."); + } + + if (record.status === Status.USED) { + throw new HttpError(HttpStatus.FORBIDDEN, "Branch is in used."); + } + + return await prisma.branch.delete({ where: { id: branchId } }); + } } From 95463dfe7c6844d43b6a55b98d200fc60b708b6e Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 2 Apr 2024 10:47:36 +0700 Subject: [PATCH 006/161] feat: add type for create and update endpoint --- src/controllers/branch/branch-controller.ts | 41 ++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/controllers/branch/branch-controller.ts b/src/controllers/branch/branch-controller.ts index 99e78b2..ef032bd 100644 --- a/src/controllers/branch/branch-controller.ts +++ b/src/controllers/branch/branch-controller.ts @@ -1,4 +1,4 @@ -import { Prisma } from "@prisma/client"; +import { Prisma, Status } from "@prisma/client"; import { Body, Controller, @@ -18,6 +18,45 @@ import prisma from "../../db"; import HttpError from "../../interfaces/http-error"; import HttpStatus from "../../interfaces/http-status"; import { RequestWithUser } from "../../interfaces/user"; + +type BranchCreate = { + code: string; + taxNo: string; + nameEN: string; + nameTH: string; + addressEN: string; + addressTH: string; + zipCode: string; + email: string; + telephoneNo: string; + longitude: string; + latitude: string; + + subDistrictId?: string | null; + districtId?: string | null; + provinceId?: string | null; + headOfficeId?: string | null; +}; + +type BranchUpdate = { + code?: string; + taxNo?: string; + nameEN?: string; + nameTH?: string; + addressEN?: string; + addressTH?: string; + zipCode?: string; + email?: string; + telephoneNo?: string; + longitude?: string; + latitude?: string; + + subDistrictId?: string | null; + districtId?: string | null; + provinceId?: string | null; + headOfficeId?: string | null; +}; + @Route("api/branch") @Tags("Branch") @Security("keycloak") From a39b9de4232d6098183383dcefad7aaaeb9c8f3a Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 2 Apr 2024 10:48:35 +0700 Subject: [PATCH 007/161] feat: update database field --- prisma/schema.prisma | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index abd8bb9..dcc50ef 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -65,6 +65,11 @@ model SubDistrict { employee Employee[] } +enum Status { + CREATED + USED +} + model Branch { id String @id @default(uuid()) code String @@ -96,7 +101,7 @@ model Branch { headOffice Branch? @relation(name: "HeadOfficeRelation", fields: [headOfficeId], references: [id]) headOfficeId String? - status String? + status Status @default(CREATED) createdBy String? createdAt DateTime @default(now()) @@ -109,10 +114,9 @@ model Branch { } model BranchContact { - id String @id @default(uuid()) - telephoneNo String - lineId String - qrCodeImageUrl String? + id String @id @default(uuid()) + telephoneNo String + lineId String branch Branch @relation(fields: [branchId], references: [id], onDelete: Cascade) branchId String @@ -141,6 +145,8 @@ model BranchUser { model User { id String @id @default(uuid()) + keycloakId String + code String firstNameTH String firstNameEN String @@ -169,8 +175,6 @@ model User { startDate DateTime retireDate DateTime - profileImageUrl String? - userType String userRole String @@ -185,7 +189,7 @@ model User { trainingPlace String - status String? + status Status @default(CREATED) createdBy String? createdAt DateTime @default(now()) @@ -368,11 +372,10 @@ model EmployeeOtherInfo { model Service { id String @id @default(uuid()) - code String - name String - detail String - imageUrl String - status String? + code String + name String + detail String + status String? createdBy String? createdAt DateTime @default(now()) From c841fa84edb2e4d611dc350eb0900adc9bb55746 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 2 Apr 2024 11:19:12 +0700 Subject: [PATCH 008/161] feat: create branch endpoint --- src/controllers/branch/branch-controller.ts | 35 +++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/controllers/branch/branch-controller.ts b/src/controllers/branch/branch-controller.ts index ef032bd..6257321 100644 --- a/src/controllers/branch/branch-controller.ts +++ b/src/controllers/branch/branch-controller.ts @@ -105,6 +105,41 @@ export class BranchController extends Controller { }); } + @Post() + async createBranch(@Request() req: RequestWithUser, @Body() body: BranchCreate) { + if (body.provinceId || body.districtId || body.subDistrictId || body.headOfficeId) { + const [province, district, subDistrict, branch] = await prisma.$transaction([ + prisma.province.findFirst({ where: { id: body.provinceId || undefined } }), + prisma.district.findFirst({ where: { id: body.districtId || undefined } }), + prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }), + prisma.branch.findFirst({ where: { id: body.headOfficeId || undefined } }), + ]); + if (body.provinceId && !province) + throw new HttpError(HttpStatus.BAD_REQUEST, "Province cannot be found."); + if (body.districtId && !district) + throw new HttpError(HttpStatus.BAD_REQUEST, "District cannot be found."); + if (body.subDistrictId && !subDistrict) + throw new HttpError(HttpStatus.BAD_REQUEST, "Sub-district cannot be found."); + if (body.headOfficeId && !branch) + throw new HttpError(HttpStatus.BAD_REQUEST, "Head branch cannot be found."); + } + + const { provinceId, districtId, subDistrictId, headOfficeId, ...rest } = body; + + return await prisma.branch.create({ + data: { + ...rest, + isHeadOffice: headOfficeId === null, + province: { connect: provinceId ? { id: provinceId } : undefined }, + district: { connect: districtId ? { id: districtId } : undefined }, + subDistrict: { connect: subDistrictId ? { id: subDistrictId } : undefined }, + headOffice: { connect: headOfficeId ? { id: headOfficeId } : undefined }, + createdBy: req.user.name, + updateBy: req.user.name, + }, + }); + } + @Delete("{branchId}") async deleteBranch(@Path() branchId: string) { const record = await prisma.branch.findFirst({ where: { id: branchId } }); From 72f9f5fe84ed95901df838afe6ae179fdbdedd97 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 2 Apr 2024 11:52:55 +0700 Subject: [PATCH 009/161] feat: edit branch endpoint --- src/controllers/branch/branch-controller.ts | 56 +++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/controllers/branch/branch-controller.ts b/src/controllers/branch/branch-controller.ts index 6257321..197ec21 100644 --- a/src/controllers/branch/branch-controller.ts +++ b/src/controllers/branch/branch-controller.ts @@ -140,6 +140,62 @@ export class BranchController extends Controller { }); } + @Patch("{branchId}") + async editBranch( + @Request() req: RequestWithUser, + @Body() body: BranchUpdate, + @Path() branchId: string, + ) { + if (body.subDistrictId || body.districtId || body.provinceId || body.headOfficeId) { + const [province, district, subDistrict, branch] = await prisma.$transaction([ + prisma.province.findFirst({ where: { id: body.provinceId || undefined } }), + prisma.district.findFirst({ where: { id: body.districtId || undefined } }), + prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }), + prisma.branch.findFirst({ where: { id: body.headOfficeId || undefined } }), + ]); + if (body.provinceId && !province) + throw new HttpError(HttpStatus.BAD_REQUEST, "Province cannot be found."); + if (body.districtId && !district) + throw new HttpError(HttpStatus.BAD_REQUEST, "District cannot be found."); + if (body.subDistrictId && !subDistrict) + throw new HttpError(HttpStatus.BAD_REQUEST, "Sub-district cannot be found."); + if (body.headOfficeId && !branch) + throw new HttpError(HttpStatus.BAD_REQUEST, "Head branch cannot be found."); + } + + const { provinceId, districtId, subDistrictId, headOfficeId, ...rest } = body; + + const record = await prisma.branch.update({ + include: { province: true, district: true, subDistrict: true }, + data: { + ...rest, + isHeadOffice: headOfficeId === null, + province: { + connect: provinceId ? { id: provinceId } : undefined, + disconnect: provinceId === null || undefined, + }, + district: { + connect: districtId ? { id: districtId } : undefined, + disconnect: districtId === null || undefined, + }, + subDistrict: { + connect: subDistrictId ? { id: subDistrictId } : undefined, + disconnect: subDistrictId === null || undefined, + }, + headOffice: { + connect: headOfficeId ? { id: headOfficeId } : undefined, + disconnect: headOfficeId === null || undefined, + }, + updateBy: req.user.name, + }, + where: { id: branchId }, + }); + if (!record) { + throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found."); + } + return record; + } + @Delete("{branchId}") async deleteBranch(@Path() branchId: string) { const record = await prisma.branch.findFirst({ where: { id: branchId } }); From 9f95bbd7456bfeea1b0d8fec4425bc973c297270 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 2 Apr 2024 11:54:11 +0700 Subject: [PATCH 010/161] feat: change database field to enum with default --- prisma/schema.prisma | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index abd8bb9..5b72ed7 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -203,7 +203,7 @@ model Customer { customerNameEN String imageUrl String? - status String? + status Status @default(CREATED) createdBy String? createdAt DateTime @default(now()) @@ -290,7 +290,7 @@ model Employee { customerBranch CustomerBranch? @relation(fields: [customerBranchId], references: [id], onDelete: SetNull) customerBranchId String? - status String? + status Status @default(CREATED) createdBy String? createdAt DateTime @default(now()) @@ -368,11 +368,11 @@ model EmployeeOtherInfo { model Service { id String @id @default(uuid()) - code String - name String - detail String - imageUrl String - status String? + code String + name String + detail String + + status Status @default(CREATED) createdBy String? createdAt DateTime @default(now()) @@ -384,12 +384,14 @@ model Service { model Work { id String @id @default(uuid()) - order String + order Int name String service Service @relation(fields: [serviceId], references: [id], onDelete: Cascade) serviceId String + status Status @default(CREATED) + createdBy String? createdAt DateTime @default(now()) updateBy String? @@ -416,7 +418,8 @@ model ProductGroup { name String detail String remark String - status String? + + status Status @default(CREATED) createdBy String? createdAt DateTime @default(now()) @@ -432,7 +435,8 @@ model ProductType { name String detail String remark String - status String? + + status Status @default(CREATED) createdBy String? createdAt DateTime @default(now()) @@ -453,7 +457,8 @@ model Product { agentPrice Int serviceCharge Int imageUrl String - status String? + + status Status @default(CREATED) productType ProductType? @relation(fields: [productTypeId], references: [id], onDelete: SetNull) productTypeId String? From 44bf22808109fb13d8b3ef8faaa7bdeff85cfa56 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 2 Apr 2024 13:18:59 +0700 Subject: [PATCH 011/161] feat: include relation response when create and delete --- src/controllers/branch/branch-controller.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/controllers/branch/branch-controller.ts b/src/controllers/branch/branch-controller.ts index 197ec21..20828b1 100644 --- a/src/controllers/branch/branch-controller.ts +++ b/src/controllers/branch/branch-controller.ts @@ -127,6 +127,11 @@ export class BranchController extends Controller { const { provinceId, districtId, subDistrictId, headOfficeId, ...rest } = body; return await prisma.branch.create({ + include: { + province: true, + district: true, + subDistrict: true, + }, data: { ...rest, isHeadOffice: headOfficeId === null, @@ -198,7 +203,14 @@ export class BranchController extends Controller { @Delete("{branchId}") async deleteBranch(@Path() branchId: string) { - const record = await prisma.branch.findFirst({ where: { id: branchId } }); + const record = await prisma.branch.findFirst({ + include: { + province: true, + district: true, + subDistrict: true, + }, + where: { id: branchId }, + }); if (!record) { throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found."); From d6aa0385cdc3c34e9f7cc4c5d12c2c7f4aa9316a Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 2 Apr 2024 13:51:55 +0700 Subject: [PATCH 012/161] fix: empty user info when crud --- src/middlewares/auth-provider/keycloak.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middlewares/auth-provider/keycloak.ts b/src/middlewares/auth-provider/keycloak.ts index 2c7359f..de8ed42 100644 --- a/src/middlewares/auth-provider/keycloak.ts +++ b/src/middlewares/auth-provider/keycloak.ts @@ -39,7 +39,7 @@ export async function keycloakAuth( payload = await verifyOffline(token); break; default: - if (process.env.KC_REALM_URL) { + if (process.env.KC_URL) { payload = await verifyOnline(token); break; } From aa182a314eccb078d9f72fae66e51ead84a86591 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 2 Apr 2024 13:54:49 +0700 Subject: [PATCH 013/161] feat: branch contact controller --- src/controllers/branch/contact-controller.ts | 32 ++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/controllers/branch/contact-controller.ts diff --git a/src/controllers/branch/contact-controller.ts b/src/controllers/branch/contact-controller.ts new file mode 100644 index 0000000..f782271 --- /dev/null +++ b/src/controllers/branch/contact-controller.ts @@ -0,0 +1,32 @@ +import { + Body, + Controller, + Delete, + Get, + Patch, + Path, + Post, + Query, + Request, + Route, + Security, + Tags, +} from "tsoa"; + +import prisma from "../../db"; +import minio from "../../services/minio"; +import HttpError from "../../interfaces/http-error"; +import HttpStatus from "../../interfaces/http-status"; +import { RequestWithUser } from "../../interfaces/user"; + +if (!process.env.MINIO_BUCKET) { + throw Error("Require MinIO bucket."); +} + +const MINIO_BUCKET = process.env.MINIO_BUCKET; + +@Route("api/branch/{branchId}/contact") +@Tags("Branch Contact") +@Security("keycloak") +export class BranchContactController extends Controller { +} From 5aded1d21527309d801e435d4fc121e393c996ce Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 2 Apr 2024 13:55:54 +0700 Subject: [PATCH 014/161] feat: create branch contact endpoint --- src/controllers/branch/contact-controller.ts | 26 ++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/controllers/branch/contact-controller.ts b/src/controllers/branch/contact-controller.ts index f782271..479dc5b 100644 --- a/src/controllers/branch/contact-controller.ts +++ b/src/controllers/branch/contact-controller.ts @@ -29,4 +29,30 @@ const MINIO_BUCKET = process.env.MINIO_BUCKET; @Tags("Branch Contact") @Security("keycloak") export class BranchContactController extends Controller { + @Post() + async createBranchContact( + @Request() req: RequestWithUser, + @Path() branchId: string, + @Body() body: BranchContactCreate, + ) { + if (!(await prisma.branch.findFirst({ where: { id: branchId } }))) { + throw new HttpError(HttpStatus.BAD_REQUEST, "Branch not found."); + } + const record = await prisma.branchContact.create({ + include: { branch: true }, + data: { ...body, branchId, createdBy: req.user.name, updateBy: req.user.name }, + }); + return Object.assign(record, { + qrCodeImageUrl: await minio.presignedGetObject( + MINIO_BUCKET, + `branch/contact-${record.id}`, + 12 * 60 * 60, + ), + qrCodeImageUploadUrl: await minio.presignedPutObject( + MINIO_BUCKET, + `branch/contact-${record.id}`, + 12 * 60 * 60, + ), + }); + } } From 712a0952dde66c1c480db4b376aa83e52f8cdc02 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 2 Apr 2024 13:56:40 +0700 Subject: [PATCH 015/161] feat: delete branch contact endpoint --- src/controllers/branch/contact-controller.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/controllers/branch/contact-controller.ts b/src/controllers/branch/contact-controller.ts index 479dc5b..ecd4917 100644 --- a/src/controllers/branch/contact-controller.ts +++ b/src/controllers/branch/contact-controller.ts @@ -55,4 +55,10 @@ export class BranchContactController extends Controller { ), }); } + + @Delete("{contactId}") + async deleteBranchContact(@Path() branchId: string, @Path() contactId: string) { + const result = await prisma.branchContact.deleteMany({ where: { id: contactId, branchId } }); + if (result.count <= 0) throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found."); + } } From 1cea8830d600da6be99f548902ba60719f86269c Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 2 Apr 2024 14:01:59 +0700 Subject: [PATCH 016/161] refactor: use same function to get img location --- src/controllers/branch/contact-controller.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/controllers/branch/contact-controller.ts b/src/controllers/branch/contact-controller.ts index ecd4917..3f22d75 100644 --- a/src/controllers/branch/contact-controller.ts +++ b/src/controllers/branch/contact-controller.ts @@ -25,6 +25,10 @@ if (!process.env.MINIO_BUCKET) { const MINIO_BUCKET = process.env.MINIO_BUCKET; +function imageLocation(id: string) { + return `branch/contact-${id}`; +} + @Route("api/branch/{branchId}/contact") @Tags("Branch Contact") @Security("keycloak") @@ -45,12 +49,12 @@ export class BranchContactController extends Controller { return Object.assign(record, { qrCodeImageUrl: await minio.presignedGetObject( MINIO_BUCKET, - `branch/contact-${record.id}`, + imageLocation(record.id), 12 * 60 * 60, ), qrCodeImageUploadUrl: await minio.presignedPutObject( MINIO_BUCKET, - `branch/contact-${record.id}`, + imageLocation(record.id), 12 * 60 * 60, ), }); From e58141b864f2927b65b82129c9e994dbf8b7208b Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 2 Apr 2024 14:08:07 +0700 Subject: [PATCH 017/161] fix: missing type --- src/controllers/branch/contact-controller.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/controllers/branch/contact-controller.ts b/src/controllers/branch/contact-controller.ts index 3f22d75..a1bcb3d 100644 --- a/src/controllers/branch/contact-controller.ts +++ b/src/controllers/branch/contact-controller.ts @@ -25,6 +25,16 @@ if (!process.env.MINIO_BUCKET) { const MINIO_BUCKET = process.env.MINIO_BUCKET; +type BranchContactCreate = { + lineId: string; + telephoneNo: string; +}; + +type BranchContactUpdate = { + lineId?: string; + telephoneNo?: string; +}; + function imageLocation(id: string) { return `branch/contact-${id}`; } From 63de7b3e52582fedb3ed3a2fe624ff1ab9da5fcd Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 2 Apr 2024 15:21:19 +0700 Subject: [PATCH 018/161] feat: edit branch endpoint --- src/controllers/branch/contact-controller.ts | 29 ++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/controllers/branch/contact-controller.ts b/src/controllers/branch/contact-controller.ts index a1bcb3d..dcb7541 100644 --- a/src/controllers/branch/contact-controller.ts +++ b/src/controllers/branch/contact-controller.ts @@ -70,6 +70,35 @@ export class BranchContactController extends Controller { }); } + @Patch("{contactId}") + async editBranchContact( + @Request() req: RequestWithUser, + @Body() body: BranchContactUpdate, + @Path() branchId: string, + @Path() contactId: string, + ) { + const record = await prisma.branchContact.update({ + include: { branch: true }, + data: { ...body, updateBy: req.user.name }, + where: { id: contactId, branchId }, + }); + if (!record) { + throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found."); + } + return Object.assign(record, { + qrCodeImageUrl: await minio.presignedGetObject( + MINIO_BUCKET, + imageLocation(record.id), + 12 * 60 * 60, + ), + qrCodeImageUploadUrl: await minio.presignedPutObject( + MINIO_BUCKET, + imageLocation(record.id), + 12 * 60 * 60, + ), + }); + } + @Delete("{contactId}") async deleteBranchContact(@Path() branchId: string, @Path() contactId: string) { const result = await prisma.branchContact.deleteMany({ where: { id: contactId, branchId } }); From d7b7a6ebee6cf983ccb4669d92d17e886b82a920 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 2 Apr 2024 15:25:28 +0700 Subject: [PATCH 019/161] feat: get branch endpoint --- src/controllers/branch/contact-controller.ts | 29 ++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/controllers/branch/contact-controller.ts b/src/controllers/branch/contact-controller.ts index dcb7541..a673f2d 100644 --- a/src/controllers/branch/contact-controller.ts +++ b/src/controllers/branch/contact-controller.ts @@ -43,6 +43,35 @@ function imageLocation(id: string) { @Tags("Branch Contact") @Security("keycloak") export class BranchContactController extends Controller { + @Get() + async getBranchContact( + @Path() branchId: string, + @Query() page: number = 1, + @Query() pageSize: number = 30, + ) { + const [result, total] = await prisma.$transaction([ + prisma.branchContact.findMany({ + where: { branchId }, + take: pageSize, + skip: (page - 1) * pageSize, + }), + prisma.branchContact.count({ where: { branchId } }), + ]); + + return { result, page, pageSize, total }; + } + + @Get("{contactId}") + async getBranchContactById(@Path() branchId: string, @Path() contactId: string) { + const record = await prisma.branchContact.findFirst({ where: { id: contactId, branchId } }); + + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found."); + + return Object.assign(record, { + qrCodeImageUrl: await minio.presignedGetObject(MINIO_BUCKET, imageLocation(record.id)), + }); + } + @Post() async createBranchContact( @Request() req: RequestWithUser, From ba65c1fe83170e1dfa1ef03edd10eaa6288ea8d4 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 2 Apr 2024 15:26:37 +0700 Subject: [PATCH 020/161] refactor: add devMessage field (useful in front-end) --- src/app.ts | 8 ++++++++ src/interfaces/http-error.ts | 4 +++- src/middlewares/error.ts | 3 +++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/app.ts b/src/app.ts index e7b19d2..2d500f5 100644 --- a/src/app.ts +++ b/src/app.ts @@ -20,6 +20,14 @@ const APP_PORT = +(process.env.APP_PORT || 3000); RegisterRoutes(app); + app.get("*", (_, res) => + res.status(404).send({ + status: 404, + message: "Route not found.", + devMessage: "unknown_url", + }), + ); + app.use(error); app.listen(APP_PORT, APP_HOST, () => console.log(`Listening on: http://localhost:${APP_PORT}`)); diff --git a/src/interfaces/http-error.ts b/src/interfaces/http-error.ts index 5d396e2..8abe48b 100644 --- a/src/interfaces/http-error.ts +++ b/src/interfaces/http-error.ts @@ -6,13 +6,15 @@ class HttpError extends Error { */ status: HttpStatus; message: string; + devMessage?: string; - constructor(status: HttpStatus, message: string) { + constructor(status: HttpStatus, message: string, devMessage?: string) { super(message); this.name = "HttpError"; this.status = status; this.message = message; + this.devMessage = devMessage; } } diff --git a/src/middlewares/error.ts b/src/middlewares/error.ts index b010f0a..d80ceb3 100644 --- a/src/middlewares/error.ts +++ b/src/middlewares/error.ts @@ -8,6 +8,7 @@ function error(error: Error, _req: Request, res: Response, _next: NextFunction) return res.status(error.status).json({ status: error.status, message: error.message, + devMessage: error.devMessage, }); } @@ -16,6 +17,7 @@ function error(error: Error, _req: Request, res: Response, _next: NextFunction) status: HttpStatus.UNPROCESSABLE_ENTITY, message: "Validation error(s).", detail: error.fields, + devMessage: "missing_or_invalid_parameter", }); } @@ -24,6 +26,7 @@ function error(error: Error, _req: Request, res: Response, _next: NextFunction) return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ status: HttpStatus.INTERNAL_SERVER_ERROR, message: error.message, + devMessage: "system_error", }); } From 94d77e494cda80c58ab936e0e6a3cfb3736f8500 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 2 Apr 2024 15:32:17 +0700 Subject: [PATCH 021/161] refactor: add devMessage to error --- src/controllers/branch/branch-controller.ts | 62 ++++++++++++++++---- src/controllers/branch/contact-controller.ts | 20 +++++-- 2 files changed, 66 insertions(+), 16 deletions(-) diff --git a/src/controllers/branch/branch-controller.ts b/src/controllers/branch/branch-controller.ts index 20828b1..21c1bac 100644 --- a/src/controllers/branch/branch-controller.ts +++ b/src/controllers/branch/branch-controller.ts @@ -95,7 +95,7 @@ export class BranchController extends Controller { @Get("{branchId}") async getBranchById(@Path() branchId: string) { - return await prisma.branch.findFirst({ + const record = await prisma.branch.findFirst({ include: { province: true, district: true, @@ -103,6 +103,12 @@ export class BranchController extends Controller { }, where: { id: branchId }, }); + + if (!record) { + throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found"); + } + + return record; } @Post() @@ -115,13 +121,29 @@ export class BranchController extends Controller { prisma.branch.findFirst({ where: { id: body.headOfficeId || undefined } }), ]); if (body.provinceId && !province) - throw new HttpError(HttpStatus.BAD_REQUEST, "Province cannot be found."); + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Province cannot be found.", + "missing_or_invalid_parameter", + ); if (body.districtId && !district) - throw new HttpError(HttpStatus.BAD_REQUEST, "District cannot be found."); + throw new HttpError( + HttpStatus.BAD_REQUEST, + "District cannot be found.", + "missing_or_invalid_parameter", + ); if (body.subDistrictId && !subDistrict) - throw new HttpError(HttpStatus.BAD_REQUEST, "Sub-district cannot be found."); + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Sub-district cannot be found.", + "missing_or_invalid_parameter", + ); if (body.headOfficeId && !branch) - throw new HttpError(HttpStatus.BAD_REQUEST, "Head branch cannot be found."); + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Head branch cannot be found.", + "missing_or_invalid_parameter", + ); } const { provinceId, districtId, subDistrictId, headOfficeId, ...rest } = body; @@ -159,13 +181,29 @@ export class BranchController extends Controller { prisma.branch.findFirst({ where: { id: body.headOfficeId || undefined } }), ]); if (body.provinceId && !province) - throw new HttpError(HttpStatus.BAD_REQUEST, "Province cannot be found."); + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Province cannot be found.", + "missing_or_invalid_parameter", + ); if (body.districtId && !district) - throw new HttpError(HttpStatus.BAD_REQUEST, "District cannot be found."); + throw new HttpError( + HttpStatus.BAD_REQUEST, + "District cannot be found.", + "missing_or_invalid_parameter", + ); if (body.subDistrictId && !subDistrict) - throw new HttpError(HttpStatus.BAD_REQUEST, "Sub-district cannot be found."); + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Sub-district cannot be found.", + "missing_or_invalid_parameter", + ); if (body.headOfficeId && !branch) - throw new HttpError(HttpStatus.BAD_REQUEST, "Head branch cannot be found."); + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Head branch cannot be found.", + "missing_or_invalid_parameter", + ); } const { provinceId, districtId, subDistrictId, headOfficeId, ...rest } = body; @@ -196,7 +234,7 @@ export class BranchController extends Controller { where: { id: branchId }, }); if (!record) { - throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found."); + throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found"); } return record; } @@ -213,11 +251,11 @@ export class BranchController extends Controller { }); if (!record) { - throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found."); + throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found"); } if (record.status === Status.USED) { - throw new HttpError(HttpStatus.FORBIDDEN, "Branch is in used."); + throw new HttpError(HttpStatus.FORBIDDEN, "Branch is in used.", "data_exists"); } return await prisma.branch.delete({ where: { id: branchId } }); diff --git a/src/controllers/branch/contact-controller.ts b/src/controllers/branch/contact-controller.ts index a673f2d..70d10a5 100644 --- a/src/controllers/branch/contact-controller.ts +++ b/src/controllers/branch/contact-controller.ts @@ -65,7 +65,13 @@ export class BranchContactController extends Controller { async getBranchContactById(@Path() branchId: string, @Path() contactId: string) { const record = await prisma.branchContact.findFirst({ where: { id: contactId, branchId } }); - if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found."); + if (!record) { + throw new HttpError( + HttpStatus.NOT_FOUND, + "Branch contact cannot be found.", + "data_not_found", + ); + } return Object.assign(record, { qrCodeImageUrl: await minio.presignedGetObject(MINIO_BUCKET, imageLocation(record.id)), @@ -79,7 +85,11 @@ export class BranchContactController extends Controller { @Body() body: BranchContactCreate, ) { if (!(await prisma.branch.findFirst({ where: { id: branchId } }))) { - throw new HttpError(HttpStatus.BAD_REQUEST, "Branch not found."); + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Branch not found.", + "missing_or_invalid_parameter", + ); } const record = await prisma.branchContact.create({ include: { branch: true }, @@ -112,7 +122,7 @@ export class BranchContactController extends Controller { where: { id: contactId, branchId }, }); if (!record) { - throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found."); + throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found"); } return Object.assign(record, { qrCodeImageUrl: await minio.presignedGetObject( @@ -131,6 +141,8 @@ export class BranchContactController extends Controller { @Delete("{contactId}") async deleteBranchContact(@Path() branchId: string, @Path() contactId: string) { const result = await prisma.branchContact.deleteMany({ where: { id: contactId, branchId } }); - if (result.count <= 0) throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found."); + if (result.count <= 0) { + throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found"); + } } } From 2197ee4104eb8169d6f2a6d54a4a1d37aa8fa5d0 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 2 Apr 2024 15:42:53 +0700 Subject: [PATCH 022/161] refactor: set status to 201 on create data --- src/controllers/branch/branch-controller.ts | 6 +++++- src/controllers/branch/contact-controller.ts | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/controllers/branch/branch-controller.ts b/src/controllers/branch/branch-controller.ts index 21c1bac..5eab1e7 100644 --- a/src/controllers/branch/branch-controller.ts +++ b/src/controllers/branch/branch-controller.ts @@ -148,7 +148,7 @@ export class BranchController extends Controller { const { provinceId, districtId, subDistrictId, headOfficeId, ...rest } = body; - return await prisma.branch.create({ + const record = await prisma.branch.create({ include: { province: true, district: true, @@ -165,6 +165,10 @@ export class BranchController extends Controller { updateBy: req.user.name, }, }); + + this.setStatus(HttpStatus.CREATED); + + return record; } @Patch("{branchId}") diff --git a/src/controllers/branch/contact-controller.ts b/src/controllers/branch/contact-controller.ts index 70d10a5..57dc329 100644 --- a/src/controllers/branch/contact-controller.ts +++ b/src/controllers/branch/contact-controller.ts @@ -95,6 +95,9 @@ export class BranchContactController extends Controller { include: { branch: true }, data: { ...body, branchId, createdBy: req.user.name, updateBy: req.user.name }, }); + + this.setStatus(HttpStatus.CREATED); + return Object.assign(record, { qrCodeImageUrl: await minio.presignedGetObject( MINIO_BUCKET, From 302f129e91c88d654e05010305dd8e9d2a8d6270 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 2 Apr 2024 15:43:33 +0700 Subject: [PATCH 023/161] feat: add type to http error dev message --- src/interfaces/http-error.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/interfaces/http-error.ts b/src/interfaces/http-error.ts index 8abe48b..787b79c 100644 --- a/src/interfaces/http-error.ts +++ b/src/interfaces/http-error.ts @@ -1,14 +1,16 @@ import HttpStatus from "./http-status"; +type DevMessage = "missing_or_invalid_parameter" | "data_exists" | "unknown_url" | "data_not_found"; + class HttpError extends Error { /** * HTTP Status Code */ status: HttpStatus; message: string; - devMessage?: string; + devMessage?: DevMessage; - constructor(status: HttpStatus, message: string, devMessage?: string) { + constructor(status: HttpStatus, message: string, devMessage?: DevMessage) { super(message); this.name = "HttpError"; From bc77e8aab5f8ffb26cd42c264ac3ca1c7e2c5d2e Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 3 Apr 2024 09:17:09 +0700 Subject: [PATCH 024/161] fix: build script --- Dockerfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Dockerfile b/Dockerfile index b0d0c92..28c0a8d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,6 +4,7 @@ ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" RUN corepack enable +RUN apt-get update && apt-get install -y openssl WORKDIR /app @@ -11,6 +12,7 @@ COPY . . FROM base AS deps RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile +RUN pnpm prisma generate FROM base AS build RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile @@ -18,7 +20,9 @@ RUN pnpm prisma generate RUN pnpm run build FROM base as prod + ENV NODE_ENV="production" + COPY --from=deps /app/node_modules /app/node_modules COPY --from=build /app/dist /app/dist COPY --from=base /app/static /app/static From 58cc1ef8f78e930364c7e1bb06172ad2f535f28b Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 3 Apr 2024 09:18:57 +0700 Subject: [PATCH 025/161] fix: missing package --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 28c0a8d..39fe85e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,7 @@ ENV PATH="$PNPM_HOME:$PATH" RUN corepack enable RUN apt-get update && apt-get install -y openssl +RUN pnpm i -g prisma WORKDIR /app From 7f81331b3ebcbaf8ce8ccc1adc2e5870cb370cbe Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 3 Apr 2024 09:28:40 +0700 Subject: [PATCH 026/161] feat: runtime migration --- Dockerfile | 4 +++- entrypoint.sh | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 entrypoint.sh diff --git a/Dockerfile b/Dockerfile index 39fe85e..c8d8da5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,4 +28,6 @@ COPY --from=deps /app/node_modules /app/node_modules COPY --from=build /app/dist /app/dist COPY --from=base /app/static /app/static -CMD ["pnpm", "run", "start"] +RUN chmod u+x ./entrypoint.sh + +ENTRYPOINT ["./entrypoint.sh"] diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..4d9a5e6 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +pnpm prisma migrate deploy +pnpm run start From 653eef846df79a8f9b0439fa9ee9481e181bcaf9 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 3 Apr 2024 09:30:22 +0700 Subject: [PATCH 027/161] feat: add migration --- .../20240403022955_init/migration.sql | 450 ++++++++++++++++++ 1 file changed, 450 insertions(+) create mode 100644 prisma/migrations/20240403022955_init/migration.sql diff --git a/prisma/migrations/20240403022955_init/migration.sql b/prisma/migrations/20240403022955_init/migration.sql new file mode 100644 index 0000000..623aef0 --- /dev/null +++ b/prisma/migrations/20240403022955_init/migration.sql @@ -0,0 +1,450 @@ +-- CreateEnum +CREATE TYPE "Status" AS ENUM ('CREATED', 'USED'); + +-- CreateTable +CREATE TABLE "Province" ( + "id" TEXT NOT NULL, + "nameTH" TEXT NOT NULL, + "nameEN" TEXT NOT NULL, + "createdBy" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updateBy" TEXT, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Province_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "District" ( + "id" TEXT NOT NULL, + "nameTH" TEXT NOT NULL, + "nameEN" TEXT NOT NULL, + "provinceId" TEXT NOT NULL, + "createdBy" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updateBy" TEXT, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "District_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "SubDistrict" ( + "id" TEXT NOT NULL, + "nameTH" TEXT NOT NULL, + "nameEN" TEXT NOT NULL, + "zipCode" TEXT NOT NULL, + "districtId" TEXT NOT NULL, + "createdBy" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updateBy" TEXT, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "SubDistrict_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Branch" ( + "id" TEXT NOT NULL, + "code" TEXT NOT NULL, + "taxNo" TEXT NOT NULL, + "nameTH" TEXT NOT NULL, + "nameEN" TEXT NOT NULL, + "addressTH" TEXT NOT NULL, + "addressEN" TEXT NOT NULL, + "provinceId" TEXT, + "districtId" TEXT, + "subDistrictId" TEXT, + "zipCode" TEXT NOT NULL, + "email" TEXT NOT NULL, + "telephoneNo" TEXT NOT NULL, + "latitude" TEXT NOT NULL, + "longitude" TEXT NOT NULL, + "isHeadOffice" BOOLEAN NOT NULL DEFAULT false, + "headOfficeId" TEXT, + "status" "Status" NOT NULL DEFAULT 'CREATED', + "createdBy" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updateBy" TEXT, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Branch_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "BranchContact" ( + "id" TEXT NOT NULL, + "telephoneNo" TEXT NOT NULL, + "lineId" TEXT NOT NULL, + "branchId" TEXT NOT NULL, + "createdBy" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updateBy" TEXT, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "BranchContact_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "BranchUser" ( + "id" TEXT NOT NULL, + "branchId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "createdBy" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updateBy" TEXT, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "BranchUser_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL, + "keycloakId" TEXT NOT NULL, + "code" TEXT NOT NULL, + "firstNameTH" TEXT NOT NULL, + "firstNameEN" TEXT NOT NULL, + "lastNameTH" TEXT NOT NULL, + "lastNameEN" TEXT NOT NULL, + "addressTH" TEXT NOT NULL, + "addressEN" TEXT NOT NULL, + "provinceId" TEXT, + "districtId" TEXT, + "subDistrictId" TEXT, + "zipCode" TEXT NOT NULL, + "email" TEXT NOT NULL, + "telephoneNo" TEXT NOT NULL, + "registrationNo" TEXT NOT NULL, + "startDate" TIMESTAMP(3) NOT NULL, + "retireDate" TIMESTAMP(3) NOT NULL, + "userType" TEXT NOT NULL, + "userRole" TEXT NOT NULL, + "discountCondition" TEXT NOT NULL, + "licenseNo" TEXT NOT NULL, + "licenseIssueDate" TIMESTAMP(3) NOT NULL, + "licenseExpireDate" TIMESTAMP(3) NOT NULL, + "sourceNationality" TEXT NOT NULL, + "importNationality" TEXT NOT NULL, + "trainingPlace" TEXT NOT NULL, + "status" "Status" NOT NULL DEFAULT 'CREATED', + "createdBy" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updateBy" TEXT, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Customer" ( + "id" TEXT NOT NULL, + "code" TEXT NOT NULL, + "customerType" TEXT NOT NULL, + "customerNameTH" TEXT NOT NULL, + "customerNameEN" TEXT NOT NULL, + "imageUrl" TEXT, + "status" "Status" NOT NULL DEFAULT 'CREATED', + "createdBy" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updateBy" TEXT, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Customer_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "CustomerBranch" ( + "id" TEXT NOT NULL, + "branchNo" TEXT NOT NULL, + "legalPersonNo" TEXT NOT NULL, + "nameTH" TEXT NOT NULL, + "nameEN" TEXT NOT NULL, + "customerId" TEXT NOT NULL, + "taxNo" TEXT NOT NULL, + "registerName" TEXT NOT NULL, + "registerDate" TIMESTAMP(3) NOT NULL, + "authorizedCapital" TEXT NOT NULL, + "addressEN" TEXT NOT NULL, + "provinceId" TEXT, + "districtId" TEXT, + "subDistrictId" TEXT, + "zipCode" TEXT NOT NULL, + "email" TEXT NOT NULL, + "telephoneNo" TEXT NOT NULL, + "latitude" TEXT NOT NULL, + "longitude" TEXT NOT NULL, + "createdBy" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updateBy" TEXT, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "CustomerBranch_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Employee" ( + "id" TEXT NOT NULL, + "code" TEXT NOT NULL, + "fullNameTH" TEXT NOT NULL, + "fullNameEN" TEXT NOT NULL, + "dateOfBirth" TIMESTAMP(3) NOT NULL, + "gender" TEXT NOT NULL, + "nationality" TEXT NOT NULL, + "addressTH" TEXT NOT NULL, + "addressEN" TEXT NOT NULL, + "provinceId" TEXT, + "districtId" TEXT, + "subDistrictId" TEXT, + "zipCode" TEXT NOT NULL, + "email" TEXT NOT NULL, + "telephoneNo" TEXT NOT NULL, + "arrivalBarricade" TEXT NOT NULL, + "arrivalCardNo" TEXT NOT NULL, + "profileImageUrl" TEXT NOT NULL, + "customerBranchId" TEXT, + "status" "Status" NOT NULL DEFAULT 'CREATED', + "createdBy" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updateBy" TEXT, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Employee_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "EmployeeCheckup" ( + "id" TEXT NOT NULL, + "employeeId" TEXT NOT NULL, + "checkupResult" TEXT NOT NULL, + "checkupType" TEXT NOT NULL, + "provinceId" TEXT, + "hospitalName" TEXT NOT NULL, + "remark" TEXT NOT NULL, + "medicalBenefitScheme" TEXT NOT NULL, + "insuranceCompany" TEXT NOT NULL, + "coverageStartDate" TIMESTAMP(3) NOT NULL, + "coverageExpireDate" TIMESTAMP(3) NOT NULL, + "createdBy" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updateBy" TEXT, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "EmployeeCheckup_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "EmployeeWork" ( + "id" TEXT NOT NULL, + "employeeId" TEXT NOT NULL, + "ownerName" TEXT NOT NULL, + "positionName" TEXT NOT NULL, + "jobType" TEXT NOT NULL, + "workplace" TEXT NOT NULL, + "workPermitNo" TEXT NOT NULL, + "workPermitIssuDate" TIMESTAMP(3) NOT NULL, + "workPermitExpireDate" TIMESTAMP(3) NOT NULL, + "workEndDate" TIMESTAMP(3) NOT NULL, + "createdBy" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updateBy" TEXT, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "EmployeeWork_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "EmployeeOtherInfo" ( + "id" TEXT NOT NULL, + "employeeId" TEXT NOT NULL, + "citizenId" TEXT NOT NULL, + "fatherFullName" TEXT NOT NULL, + "motherFullName" TEXT NOT NULL, + "birthPlace" TEXT NOT NULL, + "createdBy" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updateBy" TEXT, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "EmployeeOtherInfo_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Service" ( + "id" TEXT NOT NULL, + "code" TEXT NOT NULL, + "name" TEXT NOT NULL, + "detail" TEXT NOT NULL, + "status" "Status" NOT NULL DEFAULT 'CREATED', + "createdBy" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updateBy" TEXT, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Service_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Work" ( + "id" TEXT NOT NULL, + "order" INTEGER NOT NULL, + "name" TEXT NOT NULL, + "serviceId" TEXT NOT NULL, + "status" "Status" NOT NULL DEFAULT 'CREATED', + "createdBy" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updateBy" TEXT, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Work_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "WorkProduct" ( + "id" TEXT NOT NULL, + "workId" TEXT NOT NULL, + "createdBy" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updateBy" TEXT, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "WorkProduct_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ProductGroup" ( + "id" TEXT NOT NULL, + "code" TEXT NOT NULL, + "name" TEXT NOT NULL, + "detail" TEXT NOT NULL, + "remark" TEXT NOT NULL, + "status" "Status" NOT NULL DEFAULT 'CREATED', + "createdBy" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updateBy" TEXT, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "ProductGroup_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ProductType" ( + "id" TEXT NOT NULL, + "code" TEXT NOT NULL, + "name" TEXT NOT NULL, + "detail" TEXT NOT NULL, + "remark" TEXT NOT NULL, + "status" "Status" NOT NULL DEFAULT 'CREATED', + "createdBy" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updateBy" TEXT, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "ProductType_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Product" ( + "id" TEXT NOT NULL, + "code" TEXT NOT NULL, + "name" TEXT NOT NULL, + "detail" TEXT NOT NULL, + "process" TEXT NOT NULL, + "price" INTEGER NOT NULL, + "agentPrice" INTEGER NOT NULL, + "serviceCharge" INTEGER NOT NULL, + "imageUrl" TEXT NOT NULL, + "status" "Status" NOT NULL DEFAULT 'CREATED', + "productTypeId" TEXT, + "productGroupId" TEXT, + "createdBy" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updateBy" TEXT, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Product_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "District" ADD CONSTRAINT "District_provinceId_fkey" FOREIGN KEY ("provinceId") REFERENCES "Province"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "SubDistrict" ADD CONSTRAINT "SubDistrict_districtId_fkey" FOREIGN KEY ("districtId") REFERENCES "District"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Branch" ADD CONSTRAINT "Branch_provinceId_fkey" FOREIGN KEY ("provinceId") REFERENCES "Province"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Branch" ADD CONSTRAINT "Branch_districtId_fkey" FOREIGN KEY ("districtId") REFERENCES "District"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Branch" ADD CONSTRAINT "Branch_subDistrictId_fkey" FOREIGN KEY ("subDistrictId") REFERENCES "SubDistrict"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Branch" ADD CONSTRAINT "Branch_headOfficeId_fkey" FOREIGN KEY ("headOfficeId") REFERENCES "Branch"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "BranchContact" ADD CONSTRAINT "BranchContact_branchId_fkey" FOREIGN KEY ("branchId") REFERENCES "Branch"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "BranchUser" ADD CONSTRAINT "BranchUser_branchId_fkey" FOREIGN KEY ("branchId") REFERENCES "Branch"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "BranchUser" ADD CONSTRAINT "BranchUser_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "User" ADD CONSTRAINT "User_provinceId_fkey" FOREIGN KEY ("provinceId") REFERENCES "Province"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "User" ADD CONSTRAINT "User_districtId_fkey" FOREIGN KEY ("districtId") REFERENCES "District"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "User" ADD CONSTRAINT "User_subDistrictId_fkey" FOREIGN KEY ("subDistrictId") REFERENCES "SubDistrict"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CustomerBranch" ADD CONSTRAINT "CustomerBranch_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CustomerBranch" ADD CONSTRAINT "CustomerBranch_provinceId_fkey" FOREIGN KEY ("provinceId") REFERENCES "Province"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CustomerBranch" ADD CONSTRAINT "CustomerBranch_districtId_fkey" FOREIGN KEY ("districtId") REFERENCES "District"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CustomerBranch" ADD CONSTRAINT "CustomerBranch_subDistrictId_fkey" FOREIGN KEY ("subDistrictId") REFERENCES "SubDistrict"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Employee" ADD CONSTRAINT "Employee_provinceId_fkey" FOREIGN KEY ("provinceId") REFERENCES "Province"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Employee" ADD CONSTRAINT "Employee_districtId_fkey" FOREIGN KEY ("districtId") REFERENCES "District"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Employee" ADD CONSTRAINT "Employee_subDistrictId_fkey" FOREIGN KEY ("subDistrictId") REFERENCES "SubDistrict"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Employee" ADD CONSTRAINT "Employee_customerBranchId_fkey" FOREIGN KEY ("customerBranchId") REFERENCES "CustomerBranch"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "EmployeeCheckup" ADD CONSTRAINT "EmployeeCheckup_employeeId_fkey" FOREIGN KEY ("employeeId") REFERENCES "Employee"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "EmployeeCheckup" ADD CONSTRAINT "EmployeeCheckup_provinceId_fkey" FOREIGN KEY ("provinceId") REFERENCES "Province"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "EmployeeWork" ADD CONSTRAINT "EmployeeWork_employeeId_fkey" FOREIGN KEY ("employeeId") REFERENCES "Employee"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "EmployeeOtherInfo" ADD CONSTRAINT "EmployeeOtherInfo_employeeId_fkey" FOREIGN KEY ("employeeId") REFERENCES "Employee"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Work" ADD CONSTRAINT "Work_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "WorkProduct" ADD CONSTRAINT "WorkProduct_workId_fkey" FOREIGN KEY ("workId") REFERENCES "Work"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Product" ADD CONSTRAINT "Product_productTypeId_fkey" FOREIGN KEY ("productTypeId") REFERENCES "ProductType"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Product" ADD CONSTRAINT "Product_productGroupId_fkey" FOREIGN KEY ("productGroupId") REFERENCES "ProductGroup"("id") ON DELETE SET NULL ON UPDATE CASCADE; From 075d40876e2801e01c7d8de4d0789732dbacd2cd Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 3 Apr 2024 10:51:40 +0700 Subject: [PATCH 028/161] fix: create branch head office condition when not send --- src/controllers/branch/branch-controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/branch/branch-controller.ts b/src/controllers/branch/branch-controller.ts index 5eab1e7..1e12a87 100644 --- a/src/controllers/branch/branch-controller.ts +++ b/src/controllers/branch/branch-controller.ts @@ -156,7 +156,7 @@ export class BranchController extends Controller { }, data: { ...rest, - isHeadOffice: headOfficeId === null, + isHeadOffice: !headOfficeId, province: { connect: provinceId ? { id: provinceId } : undefined }, district: { connect: districtId ? { id: districtId } : undefined }, subDistrict: { connect: subDistrictId ? { id: subDistrictId } : undefined }, From 7ba77907d3b3be309491f88092d0525aadfae86d Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 3 Apr 2024 10:54:46 +0700 Subject: [PATCH 029/161] refactor: change method --- src/controllers/branch/branch-controller.ts | 4 ++-- src/controllers/branch/contact-controller.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/controllers/branch/branch-controller.ts b/src/controllers/branch/branch-controller.ts index 1e12a87..a255e56 100644 --- a/src/controllers/branch/branch-controller.ts +++ b/src/controllers/branch/branch-controller.ts @@ -4,7 +4,7 @@ import { Controller, Delete, Get, - Patch, + Put, Path, Post, Query, @@ -171,7 +171,7 @@ export class BranchController extends Controller { return record; } - @Patch("{branchId}") + @Put("{branchId}") async editBranch( @Request() req: RequestWithUser, @Body() body: BranchUpdate, diff --git a/src/controllers/branch/contact-controller.ts b/src/controllers/branch/contact-controller.ts index 57dc329..9607afb 100644 --- a/src/controllers/branch/contact-controller.ts +++ b/src/controllers/branch/contact-controller.ts @@ -3,7 +3,7 @@ import { Controller, Delete, Get, - Patch, + Put, Path, Post, Query, @@ -112,7 +112,7 @@ export class BranchContactController extends Controller { }); } - @Patch("{contactId}") + @Put("{contactId}") async editBranchContact( @Request() req: RequestWithUser, @Body() body: BranchContactUpdate, From e7568fa06f7fb4ffd47fa5ad504a4ef938075a3f Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 3 Apr 2024 12:49:20 +0700 Subject: [PATCH 030/161] fix: update condition and response relation --- src/controllers/branch/branch-controller.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/controllers/branch/branch-controller.ts b/src/controllers/branch/branch-controller.ts index a255e56..3dac955 100644 --- a/src/controllers/branch/branch-controller.ts +++ b/src/controllers/branch/branch-controller.ts @@ -216,7 +216,7 @@ export class BranchController extends Controller { include: { province: true, district: true, subDistrict: true }, data: { ...rest, - isHeadOffice: headOfficeId === null, + isHeadOffice: headOfficeId !== undefined ? headOfficeId === null : undefined, province: { connect: provinceId ? { id: provinceId } : undefined, disconnect: provinceId === null || undefined, @@ -262,6 +262,13 @@ export class BranchController extends Controller { throw new HttpError(HttpStatus.FORBIDDEN, "Branch is in used.", "data_exists"); } - return await prisma.branch.delete({ where: { id: branchId } }); + return await prisma.branch.delete({ + include: { + province: true, + district: true, + subDistrict: true, + }, + where: { id: branchId }, + }); } } From b1b93f0e344c63e20163f9338a3ce4e584b54fc1 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 3 Apr 2024 12:49:42 +0700 Subject: [PATCH 031/161] fix: missing qr code image url on get --- src/controllers/branch/contact-controller.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/controllers/branch/contact-controller.ts b/src/controllers/branch/contact-controller.ts index 9607afb..9598bb8 100644 --- a/src/controllers/branch/contact-controller.ts +++ b/src/controllers/branch/contact-controller.ts @@ -58,7 +58,21 @@ export class BranchContactController extends Controller { prisma.branchContact.count({ where: { branchId } }), ]); - return { result, page, pageSize, total }; + return { + result: await Promise.all( + result.map(async (v) => ({ + ...v, + qrCodeImageUrl: await minio.presignedGetObject( + MINIO_BUCKET, + imageLocation(v.id), + 12 * 60 * 60, + ), + })), + ), + page, + pageSize, + total, + }; } @Get("{contactId}") From aca05c9650833a543adce3b5b42b07b92833ab17 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 3 Apr 2024 15:34:36 +0700 Subject: [PATCH 032/161] feat: user endpoints (crud, many-to-many branch) --- src/controllers/branch/user-controller.ts | 211 +++++++++++++++ src/controllers/user/user-controller.ts | 315 ++++++++++++++++++++++ 2 files changed, 526 insertions(+) create mode 100644 src/controllers/branch/user-controller.ts create mode 100644 src/controllers/user/user-controller.ts diff --git a/src/controllers/branch/user-controller.ts b/src/controllers/branch/user-controller.ts new file mode 100644 index 0000000..9dcf8c5 --- /dev/null +++ b/src/controllers/branch/user-controller.ts @@ -0,0 +1,211 @@ +import { Prisma, Status } from "@prisma/client"; +import { + Body, + Controller, + Delete, + Get, + Path, + Post, + Query, + Request, + Route, + Security, + Tags, +} from "tsoa"; + +import prisma from "../../db"; +import HttpError from "../../interfaces/http-error"; +import HttpStatus from "../../interfaces/http-status"; +import { RequestWithUser } from "../../interfaces/user"; + +type BranchUserBody = { user: string[] }; + +@Route("api/branch/{branchId}/user") +@Tags("Branch User") +@Security("keycloak") +export class BranchUserController extends Controller { + @Get() + async getBranchUser( + @Path() branchId: string, + @Query() zipCode?: string, + @Query() query: string = "", + @Query() page: number = 1, + @Query() pageSize: number = 30, + ) { + const where = { + OR: [ + { user: { firstNameTH: { contains: query }, zipCode }, branchId }, + { user: { firstNameEN: { contains: query }, zipCode }, branchId }, + { user: { lastNameTH: { contains: query }, zipCode }, branchId }, + { user: { lastNameEN: { contains: query }, zipCode }, branchId }, + { user: { email: { contains: query }, zipCode }, branchId }, + { user: { telephoneNo: { contains: query }, zipCode }, branchId }, + ], + } satisfies Prisma.BranchUserWhereInput; + + const [result, total] = await prisma.$transaction([ + prisma.branchUser.findMany({ + include: { + user: { + include: { + province: true, + district: true, + subDistrict: true, + }, + }, + }, + where, + take: pageSize, + skip: (page - 1) * pageSize, + }), + prisma.branchUser.count({ where }), + ]); + + return { result: result.map((v) => v.user), page, pageSize, total }; + } + + @Post() + async createBranchUser( + @Request() req: RequestWithUser, + @Path() branchId: string, + @Body() body: BranchUserBody, + ) { + const user = await prisma.user.findMany({ + include: { branch: true }, + where: { id: { in: body.user } }, + }); + + if (user.length !== body.user.length) { + throw new HttpError( + HttpStatus.BAD_REQUEST, + "One or more user cannot be found.", + "missing_or_invalid_parameter", + ); + } + + await prisma.user.updateMany({ + where: { id: { in: body.user } }, + data: { status: Status.USED }, + }); + + await prisma.branchUser.createMany({ + data: user + .filter((a) => !a.branch.some((b) => b.branchId === branchId)) + .map((v) => ({ + branchId, + userId: v.id, + createdBy: req.user.name, + updateBy: req.user.name, + })), + }); + } + + @Delete() + async deleteBranchUser(@Path() branchId: string, @Body() body: BranchUserBody) { + await prisma.$transaction( + body.user.map((v) => prisma.branchUser.deleteMany({ where: { branchId, userId: v } })), + ); + } + + @Delete("{userId}") + async deleteBranchUserById(@Path() branchId: string, @Path() userId: string) { + await prisma.branchUser.deleteMany({ + where: { branchId, userId }, + }); + } +} + +type UserBranchBody = { branch: string[] }; + +@Route("api/user/{userId}/branch") +@Tags("User Branch") +@Security("keycloak") +export class UserBranchController extends Controller { + @Get() + async getUserBranch( + @Path() userId: string, + @Query() zipCode?: string, + @Query() query: string = "", + @Query() page: number = 1, + @Query() pageSize: number = 30, + ) { + const where = { + OR: [ + { branch: { nameTH: { contains: query }, zipCode }, userId }, + { branch: { nameEN: { contains: query }, zipCode }, userId }, + ], + } satisfies Prisma.BranchUserWhereInput; + + const [result, total] = await prisma.$transaction([ + prisma.branchUser.findMany({ + include: { + branch: { + include: { + province: true, + district: true, + subDistrict: true, + }, + }, + }, + where, + take: pageSize, + skip: (page - 1) * pageSize, + }), + prisma.branchUser.count({ where }), + ]); + + return { result: result.map((v) => v.branch), page, pageSize, total }; + } + + @Post() + async createUserBranch( + @Request() req: RequestWithUser, + @Path() userId: string, + @Body() body: UserBranchBody, + ) { + const branch = await prisma.branch.findMany({ + include: { user: true }, + where: { id: { in: body.branch } }, + }); + + if (branch.length !== body.branch.length) { + throw new HttpError( + HttpStatus.BAD_REQUEST, + "One or more branch cannot be found.", + "missing_or_invalid_parameter", + ); + } + + await prisma.branch.updateMany({ + where: { id: { in: body.branch } }, + data: { status: Status.USED }, + }); + + await prisma.branchUser.createMany({ + data: branch + .filter((a) => !a.user.some((b) => b.userId === userId)) + .map((v) => ({ + branchId: v.id, + userId, + createdBy: req.user.name, + updateBy: req.user.name, + })), + }); + + this.setStatus(HttpStatus.CREATED); + } + + @Delete() + async deleteUserBranch(@Path() userId: string, @Body() body: BranchUserBody) { + await prisma.$transaction( + body.user.map((v) => prisma.branchUser.deleteMany({ where: { userId, branchId: v } })), + ); + } + + @Delete("{branchId}") + async deleteUserBranchById(@Path() branchId: string, @Path() userId: string) { + await prisma.branchUser.deleteMany({ + where: { branchId, userId }, + }); + } +} diff --git a/src/controllers/user/user-controller.ts b/src/controllers/user/user-controller.ts new file mode 100644 index 0000000..0874f20 --- /dev/null +++ b/src/controllers/user/user-controller.ts @@ -0,0 +1,315 @@ +import { + Body, + Controller, + Delete, + Get, + Put, + Path, + Post, + Query, + Request, + Route, + Security, + Tags, +} from "tsoa"; +import { Prisma } from "@prisma/client"; + +import prisma from "../../db"; +import minio from "../../services/minio"; +import { RequestWithUser } from "../../interfaces/user"; +import HttpError from "../../interfaces/http-error"; +import HttpStatus from "../../interfaces/http-status"; + +if (!process.env.MINIO_BUCKET) { + throw Error("Require MinIO bucket."); +} + +const MINIO_BUCKET = process.env.MINIO_BUCKET; + +type UserCreate = { + keycloakId: string; + + userType: string; + userRole: string; + + firstNameTH: string; + firstNameEN: string; + lastNameTH: string; + lastNameEN: string; + + code: string; + registrationNo: string; + startDate: Date; + retireDate: Date; + discountCondition: string; + licenseNo: string; + licenseIssueDate: Date; + licenseExpireDate: Date; + sourceNationality: string; + importNationality: string; + trainingPlace: string; + + addressTH: string; + addressEN: string; + zipCode: string; + email: string; + telephoneNo: string; + + subDistrictId?: string | null; + districtId?: string | null; + provinceId?: string | null; +}; + +type UserUpdate = { + userType?: string; + userRole?: string; + + firstNameTH?: string; + firstNameEN?: string; + lastNameTH?: string; + lastNameEN?: string; + + code?: string; + registrationNo?: string; + startDate?: Date; + retireDate?: Date; + discountCondition?: string; + licenseNo?: string; + licenseIssueDate?: Date; + licenseExpireDate?: Date; + sourceNationality?: string; + importNationality?: string; + trainingPlace?: string; + + addressTH?: string; + addressEN?: string; + zipCode?: string; + email?: string; + telephoneNo?: string; + + subDistrictId?: string | null; + districtId?: string | null; + provinceId?: string | null; +}; + +function imageLocation(id: string) { + return `user/profile-img-${id}`; +} + +@Route("api/user") +@Tags("User") +@Security("keycloak") +export class UserController extends Controller { + @Get() + async getUser( + @Query() userType?: string, + @Query() zipCode?: string, + @Query() query: string = "", + @Query() page: number = 1, + @Query() pageSize: number = 30, + ) { + const where = { + OR: [ + { firstNameTH: { contains: query }, zipCode, userType }, + { firstNameEN: { contains: query }, zipCode, userType }, + { lastNameTH: { contains: query }, zipCode, userType }, + { lastNameEN: { contains: query }, zipCode, userType }, + { email: { contains: query }, zipCode, userType }, + { telephoneNo: { contains: query }, zipCode, userType }, + ], + } satisfies Prisma.UserWhereInput; + + const [result, total] = await prisma.$transaction([ + prisma.user.findMany({ + include: { + province: true, + district: true, + subDistrict: true, + }, + where, + take: pageSize, + skip: (page - 1) * pageSize, + }), + prisma.user.count({ where }), + ]); + + return { + result: await Promise.all( + result.map(async (v) => ({ + ...v, + profileImageUrl: await minio.presignedGetObject( + MINIO_BUCKET, + imageLocation(v.id), + 12 * 60 * 60, + ), + })), + ), + page, + pageSize, + total, + }; + } + + @Get("{userId}") + async getUserById(@Path() userId: string) { + const record = await prisma.user.findFirst({ + include: { + province: true, + district: true, + subDistrict: true, + }, + where: { id: userId }, + }); + + if (!record) + throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found", "data_not_found"); + + return Object.assign(record, { + profileImageUrl: await minio.presignedGetObject( + MINIO_BUCKET, + imageLocation(record.id), + 60 * 60, + ), + }); + } + + @Post() + async createUser(@Request() req: RequestWithUser, @Body() body: UserCreate) { + if (body.provinceId || body.districtId || body.subDistrictId) { + const [province, district, subDistrict] = await prisma.$transaction([ + prisma.province.findFirst({ where: { id: body.provinceId ?? undefined } }), + prisma.district.findFirst({ where: { id: body.districtId ?? undefined } }), + prisma.subDistrict.findFirst({ where: { id: body.subDistrictId ?? undefined } }), + ]); + if (body.provinceId && !province) { + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Province cannot be found.", + "missing_or_invalid_parameter", + ); + } + if (body.districtId && !district) { + throw new HttpError( + HttpStatus.BAD_REQUEST, + "District cannot be found.", + "missing_or_invalid_parameter", + ); + } + if (body.subDistrictId && !subDistrict) { + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Sub-district cannot be found.", + "missing_or_invalid_parameter", + ); + } + } + + const { provinceId, districtId, subDistrictId, ...rest } = body; + + const record = await prisma.user.create({ + include: { province: true, district: true, subDistrict: true }, + data: { + ...rest, + province: { connect: provinceId ? { id: provinceId } : undefined }, + district: { connect: districtId ? { id: districtId } : undefined }, + subDistrict: { connect: subDistrictId ? { id: subDistrictId } : undefined }, + createdBy: req.user.name, + updateBy: req.user.name, + }, + }); + + this.setStatus(HttpStatus.CREATED); + + return Object.assign(record, { + profileImageUrl: await minio.presignedPutObject( + MINIO_BUCKET, + imageLocation(record.id), + 12 * 60 * 60, + ), + profileImageUploadUrl: await minio.presignedPutObject( + MINIO_BUCKET, + imageLocation(record.id), + 12 * 60 * 60, + ), + }); + } + + @Put("{userId}") + async editUser( + @Request() req: RequestWithUser, + @Body() body: UserUpdate, + @Path() userId: string, + ) { + if (body.subDistrictId || body.districtId || body.provinceId) { + const [province, district, subDistrict] = await prisma.$transaction([ + prisma.province.findFirst({ where: { id: body.provinceId || undefined } }), + prisma.district.findFirst({ where: { id: body.districtId || undefined } }), + prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }), + ]); + + if (body.provinceId && !province) + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Province cannot be found.", + "missing_or_invalid_parameter", + ); + if (body.districtId && !district) + throw new HttpError( + HttpStatus.BAD_REQUEST, + "District cannot be found.", + "missing_or_invalid_parameter", + ); + if (body.subDistrictId && !subDistrict) + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Sub-district cannot be found.", + "missing_or_invalid_parameter", + ); + } + + const { provinceId, districtId, subDistrictId, ...rest } = body; + + const record = await prisma.user.update({ + include: { province: true, district: true, subDistrict: true }, + data: { + ...rest, + province: { + connect: provinceId ? { id: provinceId } : undefined, + disconnect: provinceId === null || undefined, + }, + district: { + connect: districtId ? { id: districtId } : undefined, + disconnect: districtId === null || undefined, + }, + subDistrict: { + connect: subDistrictId ? { id: subDistrictId } : undefined, + disconnect: subDistrictId === null || undefined, + }, + updateBy: req.user.name, + }, + where: { id: userId }, + }); + if (!record) { + throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found."); + } + return Object.assign(record, { + profileImageUrl: await minio.presignedGetObject( + MINIO_BUCKET, + imageLocation(record.id), + 12 * 60 * 60, + ), + profileImageUploadUrl: await minio.presignedPutObject( + MINIO_BUCKET, + imageLocation(record.id), + 12 * 60 * 60, + ), + }); + } + + @Delete("{userId}") + async deleteUser(@Path() userId: string) { + const result = await prisma.user.deleteMany({ where: { id: userId } }); + if (result.count <= 0) + throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found.", "data_not_found"); + } +} From 1f7b4a24e6d3300ea9bfb71ff824112afc27e82f Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 3 Apr 2024 15:37:05 +0700 Subject: [PATCH 033/161] feat: add elasticsearch --- src/services/elasticsearch.ts | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/services/elasticsearch.ts diff --git a/src/services/elasticsearch.ts b/src/services/elasticsearch.ts new file mode 100644 index 0000000..529bc4f --- /dev/null +++ b/src/services/elasticsearch.ts @@ -0,0 +1,7 @@ +import { Client } from "@elastic/elasticsearch"; + +const elasticsearch = new Client({ + node: `${process.env.ELASTICSEARCH_PROTOCOL}://${process.env.ELASTICSEARCH_HOST}:${process.env.ELASTICSEARCH_PORT}`, +}); + +export default elasticsearch; From 41c4d5e8f230287baa095e88ad126a678b7de217 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 3 Apr 2024 15:37:19 +0700 Subject: [PATCH 034/161] feat: add log middleware --- src/middlewares/log.ts | 62 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 src/middlewares/log.ts diff --git a/src/middlewares/log.ts b/src/middlewares/log.ts new file mode 100644 index 0000000..26c85bd --- /dev/null +++ b/src/middlewares/log.ts @@ -0,0 +1,62 @@ +import { NextFunction, Request, Response } from "express"; +import elasticsearch from "../services/elasticsearch"; + +if (!process.env.ELASTICSEARCH_INDEX) { + throw new Error("Require ELASTICSEARCH_INDEX to store log."); +} + +const ELASTICSEARCH_INDEX = process.env.ELASTICSEARCH_INDEX; + +async function logMiddleware(req: Request, res: Response, next: NextFunction) { + if (!req.url.startsWith("/api")) return next(); + + let data: any; + + const originalJson = res.json; + + res.json = function (v: any) { + data = v; + return originalJson.call(this, v); + }; + + const timestamp = new Date().toString(); + const start = performance.now(); + + req.app.locals.logData = {}; + + res.on("finish", () => { + const obj = { + systemName: "JWS-SOS", + startTimeStamp: timestamp, + endTimeStamp: new Date().toString(), + processTime: performance.now() - start, + host: req.hostname, + sessionId: req.headers["x-session-id"], + rtId: req.headers["x-rtid"], + tId: req.headers["x-tid"], + method: req.method, + endpoint: req.url, + responseCode: res.statusCode, + responseDescription: + data.devMessage !== undefined + ? data.devMessage + : { 200: "success", 201: "created_success", 204: "no_content", 304: "success" }[ + res.statusCode + ], + input: req.body, + output: data, + ...req.app.locals.logData, + }; + + console.log(obj); + + elasticsearch.index({ + index: ELASTICSEARCH_INDEX, + document: obj, + }); + }); + + return next(); +} + +export default logMiddleware; From 9c176b9878838ffc565fed39a8a43878bbffdd29 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:12:33 +0700 Subject: [PATCH 035/161] feat: logging data structure and conditioan level --- src/middlewares/log.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/middlewares/log.ts b/src/middlewares/log.ts index 26c85bd..9db083d 100644 --- a/src/middlewares/log.ts +++ b/src/middlewares/log.ts @@ -7,6 +7,14 @@ if (!process.env.ELASTICSEARCH_INDEX) { const ELASTICSEARCH_INDEX = process.env.ELASTICSEARCH_INDEX; +const LOG_LEVEL_MAP: Record = { + debug: 4, + info: 3, + warning: 2, + error: 1, + none: 0, +}; + async function logMiddleware(req: Request, res: Response, next: NextFunction) { if (!req.url.startsWith("/api")) return next(); @@ -25,7 +33,14 @@ async function logMiddleware(req: Request, res: Response, next: NextFunction) { req.app.locals.logData = {}; res.on("finish", () => { + const level = LOG_LEVEL_MAP[process.env.LOG_LEVEL ?? "info"] || 1; + + if (level === 1 && res.statusCode < 500) return; + if (level === 2 && res.statusCode < 400) return; + if (level === 3 && res.statusCode < 200) return; + const obj = { + logType: res.statusCode >= 500 ? "error" : res.statusCode >= 400 ? "warning" : "info", systemName: "JWS-SOS", startTimeStamp: timestamp, endTimeStamp: new Date().toString(), @@ -43,8 +58,8 @@ async function logMiddleware(req: Request, res: Response, next: NextFunction) { : { 200: "success", 201: "created_success", 204: "no_content", 304: "success" }[ res.statusCode ], - input: req.body, - output: data, + // input: (level < 1 && req.body) || undefined, + // output: (level < 1 && data) || undefined, ...req.app.locals.logData, }; From d01d4ef40a7aaf7d1002b5214ab68c94810ec637 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:12:41 +0700 Subject: [PATCH 036/161] feat: use middleware --- src/app.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/app.ts b/src/app.ts index 2d500f5..d9c0a8f 100644 --- a/src/app.ts +++ b/src/app.ts @@ -5,6 +5,7 @@ import swaggerUi from "swagger-ui-express"; import swaggerDocument from "./swagger.json"; import error from "./middlewares/error"; import { RegisterRoutes } from "./routes"; +import logMiddleware from "./middlewares/log"; const APP_HOST = process.env.APP_HOST || "0.0.0.0"; const APP_PORT = +(process.env.APP_PORT || 3000); @@ -15,6 +16,9 @@ const APP_PORT = +(process.env.APP_PORT || 3000); app.use(cors()); app.use(json()); app.use(urlencoded({ extended: true })); + + app.use(logMiddleware); + app.use("/", express.static("static")); app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument)); From 89e7767fae0e2cfb2cac03c91b6644c18c292e42 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:13:13 +0700 Subject: [PATCH 037/161] chore: .env example add elasticsearch and minio --- .env.example | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.env.example b/.env.example index 9445fac..511dbe2 100644 --- a/.env.example +++ b/.env.example @@ -7,4 +7,15 @@ KC_SERVICE_ACCOUNT_SECRET= APP_HOST=0.0.0.0 APP_PORT=3000 +MINIO_HOST=192.168.1.20 +MINIO_PORT=9000 +MINIO_ACCESS_KEY= +MINIO_SECRET_KEY= +MINIO_BUCKET=jws-dev + +ELASTICSEARCH_PROTOCOL=http +ELASTICSEARCH_HOST=192.168.1.20 +ELASTICSEARCH_PORT=9200 +ELASTICSEARCH_INDEX=jws-log-index + DATABASE_URL=postgresql://postgres:1234@192.168.1.20:5432/dev_1?schema=public From 3ed9d56f00be2810e1fa61abf043bcf51945d0e9 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:13:29 +0700 Subject: [PATCH 038/161] feat: add more dev message --- src/interfaces/http-error.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/interfaces/http-error.ts b/src/interfaces/http-error.ts index 787b79c..e9f34fd 100644 --- a/src/interfaces/http-error.ts +++ b/src/interfaces/http-error.ts @@ -1,6 +1,13 @@ import HttpStatus from "./http-status"; -type DevMessage = "missing_or_invalid_parameter" | "data_exists" | "unknown_url" | "data_not_found"; +type DevMessage = + | "missing_or_invalid_parameter" + | "data_exists" + | "data_in_used" + | "no_permission" + | "unknown_url" + | "data_not_found" + | "unauthorized"; class HttpError extends Error { /** From 7f934454a9bba85ee1c644eafc0ae99c5e2b077a Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:26:39 +0700 Subject: [PATCH 039/161] chore: change devMessage on error --- src/controllers/branch/branch-controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/branch/branch-controller.ts b/src/controllers/branch/branch-controller.ts index 3dac955..1bdd334 100644 --- a/src/controllers/branch/branch-controller.ts +++ b/src/controllers/branch/branch-controller.ts @@ -259,7 +259,7 @@ export class BranchController extends Controller { } if (record.status === Status.USED) { - throw new HttpError(HttpStatus.FORBIDDEN, "Branch is in used.", "data_exists"); + throw new HttpError(HttpStatus.FORBIDDEN, "Branch is in used.", "data_in_used"); } return await prisma.branch.delete({ From 8336310e69487d7c0432a7e79f00b3b6a200bb04 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:26:52 +0700 Subject: [PATCH 040/161] feat: check if user in in used --- src/controllers/user/user-controller.ts | 28 ++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/controllers/user/user-controller.ts b/src/controllers/user/user-controller.ts index 0874f20..8674ca1 100644 --- a/src/controllers/user/user-controller.ts +++ b/src/controllers/user/user-controller.ts @@ -12,7 +12,7 @@ import { Security, Tags, } from "tsoa"; -import { Prisma } from "@prisma/client"; +import { Prisma, Status } from "@prisma/client"; import prisma from "../../db"; import minio from "../../services/minio"; @@ -308,8 +308,30 @@ export class UserController extends Controller { @Delete("{userId}") async deleteUser(@Path() userId: string) { - const result = await prisma.user.deleteMany({ where: { id: userId } }); - if (result.count <= 0) + const record = await prisma.user.findFirst({ + include: { + province: true, + district: true, + subDistrict: true, + }, + where: { id: userId }, + }); + + if (!record) { throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found.", "data_not_found"); + } + + if (record.status === Status.USED) { + throw new HttpError(HttpStatus.FORBIDDEN, "User is in used.", "data_in_used"); + } + + return await prisma.user.delete({ + include: { + province: true, + district: true, + subDistrict: true, + }, + where: { id: userId }, + }); } } From 91e2f835297601d0a6b3780d7b2b3416c29f9af0 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:37:52 +0700 Subject: [PATCH 041/161] chore: deps elasticsearch --- package.json | 1 + pnpm-lock.yaml | 49 +++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 96f769a..0fc68a0 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "typescript": "^5.4.3" }, "dependencies": { + "@elastic/elasticsearch": "^8.13.0", "@prisma/client": "5.11.0", "@tsoa/runtime": "^6.2.0", "cors": "^2.8.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3577b65..035cc0e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,9 @@ settings: excludeLinksFromLockfile: false dependencies: + '@elastic/elasticsearch': + specifier: ^8.13.0 + version: 8.13.0 '@prisma/client': specifier: 5.11.0 version: 5.11.0(prisma@5.11.0) @@ -74,6 +77,30 @@ packages: '@jridgewell/trace-mapping': 0.3.9 dev: true + /@elastic/elasticsearch@8.13.0: + resolution: {integrity: sha512-OAYgzqArPqgDaIJ1yT0RX31YCgr1lleo53zL+36i23PFjHu08CA6Uq+BmBzEV05yEidl+ILPdeSfF3G8hPG/JQ==} + engines: {node: '>=18'} + dependencies: + '@elastic/transport': 8.5.0 + tslib: 2.6.2 + transitivePeerDependencies: + - supports-color + dev: false + + /@elastic/transport@8.5.0: + resolution: {integrity: sha512-T+zSUHXBfrqlj/E9pJiaEgKoTdGykBCohzNBt6omDfI6EQtaNT240oMO03oXo35T8rwrCVonSMSoedbmToncVA==} + engines: {node: '>=18'} + dependencies: + debug: 4.3.4(supports-color@5.5.0) + hpagent: 1.2.0 + ms: 2.1.3 + secure-json-parse: 2.7.0 + tslib: 2.6.2 + undici: 6.11.1 + transitivePeerDependencies: + - supports-color + dev: false + /@hapi/accept@6.0.3: resolution: {integrity: sha512-p72f9k56EuF0n3MwlBNThyVE5PXX40g+aQh+C/xbKrfzahM2Oispv3AXmOIU51t3j77zay1qrX7IIziZXspMlw==} dependencies: @@ -892,7 +919,6 @@ packages: dependencies: ms: 2.1.2 supports-color: 5.5.0 - dev: true /decode-uri-component@0.2.2: resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} @@ -1322,7 +1348,6 @@ packages: /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} - dev: true /has-property-descriptors@1.0.2: resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} @@ -1354,6 +1379,11 @@ packages: function-bind: 1.1.2 dev: false + /hpagent@1.2.0: + resolution: {integrity: sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==} + engines: {node: '>=14'} + dev: false + /http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} @@ -1715,7 +1745,6 @@ packages: /ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - dev: true /ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1963,6 +1992,10 @@ packages: resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==} dev: false + /secure-json-parse@2.7.0: + resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + dev: false + /semver@7.6.0: resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==} engines: {node: '>=10'} @@ -2165,7 +2198,6 @@ packages: engines: {node: '>=4'} dependencies: has-flag: 3.0.0 - dev: true /swagger-ui-dist@5.13.0: resolution: {integrity: sha512-uaWhh6j18IIs5tOX0arvIBnVINAzpTXaQXkr7qAk8zoupegJVg0UU/5+S/FgsgVCnzVsJ9d7QLjIxkswEeTg0Q==} @@ -2242,6 +2274,10 @@ packages: yn: 3.1.1 dev: true + /tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + dev: false + /tsoa@6.2.0: resolution: {integrity: sha512-EX/RyoU+4hD1rLM5NjYG+I7lEhqx1yuLgcHs/gyWQpkX/RL9cVR9hFA9LKQrK6PE+WTg1SEahn1MK3l/+6pVKw==} engines: {node: '>=18.0.0', yarn: '>=1.9.4'} @@ -2334,6 +2370,11 @@ packages: /undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + /undici@6.11.1: + resolution: {integrity: sha512-KyhzaLJnV1qa3BSHdj4AZ2ndqI0QWPxYzaIOio0WzcEJB9gvuysprJSLtpvc2D9mhR9jPDUk7xlJlZbH2KR5iw==} + engines: {node: '>=18.0'} + dev: false + /universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} From ad4861b04cf6776c0d10a42fb8c9bfad725911bc Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:33:19 +0700 Subject: [PATCH 042/161] feat: add user stat for each branch --- src/controllers/branch/branch-controller.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/controllers/branch/branch-controller.ts b/src/controllers/branch/branch-controller.ts index 1bdd334..cb9b096 100644 --- a/src/controllers/branch/branch-controller.ts +++ b/src/controllers/branch/branch-controller.ts @@ -61,6 +61,27 @@ type BranchUpdate = { @Tags("Branch") @Security("keycloak") export class BranchController extends Controller { + @Get("stats") + async getStat() { + const list = await prisma.branchUser.groupBy({ + by: ["branchId"], + _count: true, + }); + + const record = await prisma.branch.findMany({ + select: { + id: true, + nameEN: true, + nameTH: true, + }, + }); + + return record.map((a) => ({ + ...a, + userCount: list.find((b) => b.branchId === a.id)?._count ?? 0, + })); + } + @Get() async getBranch( @Query() zipCode?: string, From 8c898c4faa7186cf60e3f15d73797cd937508497 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 4 Apr 2024 09:10:31 +0700 Subject: [PATCH 043/161] fix: error reading variable --- src/middlewares/log.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middlewares/log.ts b/src/middlewares/log.ts index 9db083d..ded81a4 100644 --- a/src/middlewares/log.ts +++ b/src/middlewares/log.ts @@ -53,7 +53,7 @@ async function logMiddleware(req: Request, res: Response, next: NextFunction) { endpoint: req.url, responseCode: res.statusCode, responseDescription: - data.devMessage !== undefined + data?.devMessage !== undefined ? data.devMessage : { 200: "success", 201: "created_success", 204: "no_content", 304: "success" }[ res.statusCode From cd6e2040aff85cbac86131a70821ac716085a851 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 4 Apr 2024 09:14:12 +0700 Subject: [PATCH 044/161] fix: filter out non-api --- src/middlewares/log.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middlewares/log.ts b/src/middlewares/log.ts index ded81a4..dcb73a0 100644 --- a/src/middlewares/log.ts +++ b/src/middlewares/log.ts @@ -16,7 +16,7 @@ const LOG_LEVEL_MAP: Record = { }; async function logMiddleware(req: Request, res: Response, next: NextFunction) { - if (!req.url.startsWith("/api")) return next(); + if (!req.url.startsWith("/api/")) return next(); let data: any; From 48c9fe1cfde99a707187708517fa1e5e1dedf56f Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 4 Apr 2024 09:15:53 +0700 Subject: [PATCH 045/161] fix: filter out after finish response --- src/middlewares/log.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/middlewares/log.ts b/src/middlewares/log.ts index dcb73a0..f57f4e8 100644 --- a/src/middlewares/log.ts +++ b/src/middlewares/log.ts @@ -33,6 +33,8 @@ async function logMiddleware(req: Request, res: Response, next: NextFunction) { req.app.locals.logData = {}; res.on("finish", () => { + if (!req.url.startsWith("/api/")) return; + const level = LOG_LEVEL_MAP[process.env.LOG_LEVEL ?? "info"] || 1; if (level === 1 && res.statusCode < 500) return; From 88f84256cf51684f97b41fb622090fe4e2d26e1f Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 4 Apr 2024 11:02:01 +0700 Subject: [PATCH 046/161] fix: image not on delete data --- src/controllers/branch/contact-controller.ts | 3 +++ src/controllers/user/user-controller.ts | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/src/controllers/branch/contact-controller.ts b/src/controllers/branch/contact-controller.ts index 9598bb8..dde1d4f 100644 --- a/src/controllers/branch/contact-controller.ts +++ b/src/controllers/branch/contact-controller.ts @@ -161,5 +161,8 @@ export class BranchContactController extends Controller { if (result.count <= 0) { throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found"); } + await minio.removeObject(MINIO_BUCKET, imageLocation(contactId), { + forceDelete: true, + }); } } diff --git a/src/controllers/user/user-controller.ts b/src/controllers/user/user-controller.ts index 8674ca1..19cfc3f 100644 --- a/src/controllers/user/user-controller.ts +++ b/src/controllers/user/user-controller.ts @@ -325,6 +325,10 @@ export class UserController extends Controller { throw new HttpError(HttpStatus.FORBIDDEN, "User is in used.", "data_in_used"); } + await minio.removeObject(MINIO_BUCKET, imageLocation(userId), { + forceDelete: true, + }); + return await prisma.user.delete({ include: { province: true, From 7830d1ee63040db383a854cceec85f7b70cc4df4 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 4 Apr 2024 11:05:58 +0700 Subject: [PATCH 047/161] feat: temporary generate code for branch and user This field should be auto generated by system but currently don't know the pattern. --- prisma/schema.prisma | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 2e26fda..ad90c97 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -72,7 +72,7 @@ enum Status { model Branch { id String @id @default(uuid()) - code String + code String @default(uuid()) taxNo String nameTH String nameEN String @@ -147,7 +147,7 @@ model User { keycloakId String - code String + code String @default(uuid()) firstNameTH String firstNameEN String lastNameTH String From a86b01bc3e893ecd240efc399d0cbe4262b186c8 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 4 Apr 2024 11:07:09 +0700 Subject: [PATCH 048/161] feat: update status when delete user or branch This must be handle accordingly when new model has relation when these model. --- src/controllers/branch/branch-controller.ts | 11 +++++++++++ src/controllers/user/user-controller.ts | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/controllers/branch/branch-controller.ts b/src/controllers/branch/branch-controller.ts index cb9b096..05e5d01 100644 --- a/src/controllers/branch/branch-controller.ts +++ b/src/controllers/branch/branch-controller.ts @@ -283,6 +283,17 @@ export class BranchController extends Controller { throw new HttpError(HttpStatus.FORBIDDEN, "Branch is in used.", "data_in_used"); } + await prisma.user.updateMany({ + where: { + NOT: { + branch: { some: {} }, + }, + }, + data: { + status: Status.CREATED, + }, + }); + return await prisma.branch.delete({ include: { province: true, diff --git a/src/controllers/user/user-controller.ts b/src/controllers/user/user-controller.ts index 19cfc3f..09c78c1 100644 --- a/src/controllers/user/user-controller.ts +++ b/src/controllers/user/user-controller.ts @@ -329,6 +329,17 @@ export class UserController extends Controller { forceDelete: true, }); + await prisma.branch.updateMany({ + where: { + NOT: { + user: { some: {} }, + }, + }, + data: { + status: Status.CREATED, + }, + }); + return await prisma.user.delete({ include: { province: true, From d3e535374cb448e585bebb212e204b3364cb0e2a Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 4 Apr 2024 11:09:59 +0700 Subject: [PATCH 049/161] fix: remove parent relation Looks unnecessary. Doesn't seems to get used. --- src/controllers/branch/contact-controller.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/controllers/branch/contact-controller.ts b/src/controllers/branch/contact-controller.ts index dde1d4f..65b4398 100644 --- a/src/controllers/branch/contact-controller.ts +++ b/src/controllers/branch/contact-controller.ts @@ -106,7 +106,6 @@ export class BranchContactController extends Controller { ); } const record = await prisma.branchContact.create({ - include: { branch: true }, data: { ...body, branchId, createdBy: req.user.name, updateBy: req.user.name }, }); @@ -134,7 +133,6 @@ export class BranchContactController extends Controller { @Path() contactId: string, ) { const record = await prisma.branchContact.update({ - include: { branch: true }, data: { ...body, updateBy: req.user.name }, where: { id: contactId, branchId }, }); From 79f844adee6f5af80d698a8d241154f4f47af255 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 4 Apr 2024 15:50:30 +0700 Subject: [PATCH 050/161] fix: delete branch from user wrong body --- compose.yaml | 25 +++++++++++++++++++++++ src/controllers/branch/user-controller.ts | 4 ++-- 2 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 compose.yaml diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..61a8b42 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,25 @@ +version: "3.8" + +services: + app: + build: . + restart: unless-stopped + ports: + - 3000:3000 + environment: + - KC_URL=http://192.168.1.20:8080 + - KC_REALM=dev + - KC_SERVICE_ACCOUNT_CLIENT_ID= + - KC_SERVICE_ACCOUNT_SECRET= + - APP_HOST=0.0.0.0 + - APP_PORT=3000 + - MINIO_HOST=192.168.1.20 + - MINIO_PORT=9000 + - MINIO_ACCESS_KEY= + - MINIO_SECRET_KEY= + - MINIO_BUCKET=jws-dev + - ELASTICSEARCH_PROTOCOL=http + - ELASTICSEARCH_HOST=192.168.1.20 + - ELASTICSEARCH_PORT=9200 + - ELASTICSEARCH_INDEX=jws-log-index + - DATABASE_URL=postgresql://postgres:1234@192.168.1.20:5432/dev_1?schema=public diff --git a/src/controllers/branch/user-controller.ts b/src/controllers/branch/user-controller.ts index 9dcf8c5..1ae434b 100644 --- a/src/controllers/branch/user-controller.ts +++ b/src/controllers/branch/user-controller.ts @@ -196,9 +196,9 @@ export class UserBranchController extends Controller { } @Delete() - async deleteUserBranch(@Path() userId: string, @Body() body: BranchUserBody) { + async deleteUserBranch(@Path() userId: string, @Body() body: UserBranchBody) { await prisma.$transaction( - body.user.map((v) => prisma.branchUser.deleteMany({ where: { userId, branchId: v } })), + body.branch.map((v) => prisma.branchUser.deleteMany({ where: { userId, branchId: v } })), ); } From cd768129977b2c1d8ff0ee49efa6986bc509c580 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 4 Apr 2024 13:42:46 +0700 Subject: [PATCH 051/161] refactor: remove suffix from field This field does not indicate that it is specific to TH but any locale --- prisma/schema.prisma | 20 ++++++++++---------- src/controllers/branch/branch-controller.ts | 12 ++++++------ src/controllers/branch/user-controller.ts | 6 +++--- src/controllers/user/user-controller.ts | 16 ++++++++-------- 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index ad90c97..b0f82be 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -9,7 +9,7 @@ datasource db { model Province { id String @id @default(uuid()) - nameTH String + name String nameEN String createdBy String? @@ -27,7 +27,7 @@ model Province { model District { id String @id @default(uuid()) - nameTH String + name String nameEN String provinceId String @@ -47,7 +47,7 @@ model District { model SubDistrict { id String @id @default(uuid()) - nameTH String + name String nameEN String zipCode String @@ -74,9 +74,9 @@ model Branch { id String @id @default(uuid()) code String @default(uuid()) taxNo String - nameTH String + name String nameEN String - addressTH String + address String addressEN String province Province? @relation(fields: [provinceId], references: [id], onDelete: SetNull) @@ -148,12 +148,12 @@ model User { keycloakId String code String @default(uuid()) - firstNameTH String + firstName String firstNameEN String - lastNameTH String + lastName String lastNameEN String - addressTH String + address String addressEN String province Province? @relation(fields: [provinceId], references: [id], onDelete: SetNull) @@ -222,7 +222,7 @@ model CustomerBranch { branchNo String legalPersonNo String - nameTH String + name String nameEN String customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade) @@ -270,7 +270,7 @@ model Employee { gender String nationality String - addressTH String + address String addressEN String province Province? @relation(fields: [provinceId], references: [id], onDelete: SetNull) diff --git a/src/controllers/branch/branch-controller.ts b/src/controllers/branch/branch-controller.ts index 05e5d01..b4ae109 100644 --- a/src/controllers/branch/branch-controller.ts +++ b/src/controllers/branch/branch-controller.ts @@ -23,9 +23,9 @@ type BranchCreate = { code: string; taxNo: string; nameEN: string; - nameTH: string; + name: string; addressEN: string; - addressTH: string; + address: string; zipCode: string; email: string; telephoneNo: string; @@ -42,9 +42,9 @@ type BranchUpdate = { code?: string; taxNo?: string; nameEN?: string; - nameTH?: string; + name?: string; addressEN?: string; - addressTH?: string; + address?: string; zipCode?: string; email?: string; telephoneNo?: string; @@ -72,7 +72,7 @@ export class BranchController extends Controller { select: { id: true, nameEN: true, - nameTH: true, + name: true, }, }); @@ -92,7 +92,7 @@ export class BranchController extends Controller { const where = { OR: [ { nameEN: { contains: query }, zipCode }, - { nameTH: { contains: query }, zipCode }, + { name: { contains: query }, zipCode }, { email: { contains: query }, zipCode }, ], } satisfies Prisma.BranchWhereInput; diff --git a/src/controllers/branch/user-controller.ts b/src/controllers/branch/user-controller.ts index 1ae434b..483f1c4 100644 --- a/src/controllers/branch/user-controller.ts +++ b/src/controllers/branch/user-controller.ts @@ -34,9 +34,9 @@ export class BranchUserController extends Controller { ) { const where = { OR: [ - { user: { firstNameTH: { contains: query }, zipCode }, branchId }, + { user: { firstName: { contains: query }, zipCode }, branchId }, { user: { firstNameEN: { contains: query }, zipCode }, branchId }, - { user: { lastNameTH: { contains: query }, zipCode }, branchId }, + { user: { lastName: { contains: query }, zipCode }, branchId }, { user: { lastNameEN: { contains: query }, zipCode }, branchId }, { user: { email: { contains: query }, zipCode }, branchId }, { user: { telephoneNo: { contains: query }, zipCode }, branchId }, @@ -131,7 +131,7 @@ export class UserBranchController extends Controller { ) { const where = { OR: [ - { branch: { nameTH: { contains: query }, zipCode }, userId }, + { branch: { name: { contains: query }, zipCode }, userId }, { branch: { nameEN: { contains: query }, zipCode }, userId }, ], } satisfies Prisma.BranchUserWhereInput; diff --git a/src/controllers/user/user-controller.ts b/src/controllers/user/user-controller.ts index 09c78c1..30bfa19 100644 --- a/src/controllers/user/user-controller.ts +++ b/src/controllers/user/user-controller.ts @@ -32,9 +32,9 @@ type UserCreate = { userType: string; userRole: string; - firstNameTH: string; + firstName: string; firstNameEN: string; - lastNameTH: string; + lastName: string; lastNameEN: string; code: string; @@ -49,7 +49,7 @@ type UserCreate = { importNationality: string; trainingPlace: string; - addressTH: string; + address: string; addressEN: string; zipCode: string; email: string; @@ -64,9 +64,9 @@ type UserUpdate = { userType?: string; userRole?: string; - firstNameTH?: string; + firstName?: string; firstNameEN?: string; - lastNameTH?: string; + lastName?: string; lastNameEN?: string; code?: string; @@ -81,7 +81,7 @@ type UserUpdate = { importNationality?: string; trainingPlace?: string; - addressTH?: string; + address?: string; addressEN?: string; zipCode?: string; email?: string; @@ -110,9 +110,9 @@ export class UserController extends Controller { ) { const where = { OR: [ - { firstNameTH: { contains: query }, zipCode, userType }, + { firstName: { contains: query }, zipCode, userType }, { firstNameEN: { contains: query }, zipCode, userType }, - { lastNameTH: { contains: query }, zipCode, userType }, + { lastName: { contains: query }, zipCode, userType }, { lastNameEN: { contains: query }, zipCode, userType }, { email: { contains: query }, zipCode, userType }, { telephoneNo: { contains: query }, zipCode, userType }, From 86efb84ceae9abe50d515f3a6640ea00640acd22 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 4 Apr 2024 14:05:07 +0700 Subject: [PATCH 052/161] refactor: recreate migration file --- .../migration.sql | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) rename prisma/migrations/{20240403022955_init => 20240404070242_init}/migration.sql (98%) diff --git a/prisma/migrations/20240403022955_init/migration.sql b/prisma/migrations/20240404070242_init/migration.sql similarity index 98% rename from prisma/migrations/20240403022955_init/migration.sql rename to prisma/migrations/20240404070242_init/migration.sql index 623aef0..7851f6c 100644 --- a/prisma/migrations/20240403022955_init/migration.sql +++ b/prisma/migrations/20240404070242_init/migration.sql @@ -4,7 +4,7 @@ CREATE TYPE "Status" AS ENUM ('CREATED', 'USED'); -- CreateTable CREATE TABLE "Province" ( "id" TEXT NOT NULL, - "nameTH" TEXT NOT NULL, + "name" TEXT NOT NULL, "nameEN" TEXT NOT NULL, "createdBy" TEXT, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, @@ -17,7 +17,7 @@ CREATE TABLE "Province" ( -- CreateTable CREATE TABLE "District" ( "id" TEXT NOT NULL, - "nameTH" TEXT NOT NULL, + "name" TEXT NOT NULL, "nameEN" TEXT NOT NULL, "provinceId" TEXT NOT NULL, "createdBy" TEXT, @@ -31,7 +31,7 @@ CREATE TABLE "District" ( -- CreateTable CREATE TABLE "SubDistrict" ( "id" TEXT NOT NULL, - "nameTH" TEXT NOT NULL, + "name" TEXT NOT NULL, "nameEN" TEXT NOT NULL, "zipCode" TEXT NOT NULL, "districtId" TEXT NOT NULL, @@ -48,9 +48,9 @@ CREATE TABLE "Branch" ( "id" TEXT NOT NULL, "code" TEXT NOT NULL, "taxNo" TEXT NOT NULL, - "nameTH" TEXT NOT NULL, + "name" TEXT NOT NULL, "nameEN" TEXT NOT NULL, - "addressTH" TEXT NOT NULL, + "address" TEXT NOT NULL, "addressEN" TEXT NOT NULL, "provinceId" TEXT, "districtId" TEXT, @@ -103,11 +103,11 @@ CREATE TABLE "User" ( "id" TEXT NOT NULL, "keycloakId" TEXT NOT NULL, "code" TEXT NOT NULL, - "firstNameTH" TEXT NOT NULL, + "firstName" TEXT NOT NULL, "firstNameEN" TEXT NOT NULL, - "lastNameTH" TEXT NOT NULL, + "lastName" TEXT NOT NULL, "lastNameEN" TEXT NOT NULL, - "addressTH" TEXT NOT NULL, + "address" TEXT NOT NULL, "addressEN" TEXT NOT NULL, "provinceId" TEXT, "districtId" TEXT, @@ -158,7 +158,7 @@ CREATE TABLE "CustomerBranch" ( "id" TEXT NOT NULL, "branchNo" TEXT NOT NULL, "legalPersonNo" TEXT NOT NULL, - "nameTH" TEXT NOT NULL, + "name" TEXT NOT NULL, "nameEN" TEXT NOT NULL, "customerId" TEXT NOT NULL, "taxNo" TEXT NOT NULL, @@ -191,7 +191,7 @@ CREATE TABLE "Employee" ( "dateOfBirth" TIMESTAMP(3) NOT NULL, "gender" TEXT NOT NULL, "nationality" TEXT NOT NULL, - "addressTH" TEXT NOT NULL, + "address" TEXT NOT NULL, "addressEN" TEXT NOT NULL, "provinceId" TEXT, "districtId" TEXT, From fff5558701a54d28017250da8690ae2400283f94 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 4 Apr 2024 15:27:57 +0700 Subject: [PATCH 053/161] refactor: update schema and migration --- .../migration.sql | 4 ++-- prisma/schema.prisma | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) rename prisma/migrations/{20240404070242_init => 20240404082734_init}/migration.sql (99%) diff --git a/prisma/migrations/20240404070242_init/migration.sql b/prisma/migrations/20240404082734_init/migration.sql similarity index 99% rename from prisma/migrations/20240404070242_init/migration.sql rename to prisma/migrations/20240404082734_init/migration.sql index 7851f6c..6991bd0 100644 --- a/prisma/migrations/20240404070242_init/migration.sql +++ b/prisma/migrations/20240404082734_init/migration.sql @@ -141,7 +141,7 @@ CREATE TABLE "Customer" ( "id" TEXT NOT NULL, "code" TEXT NOT NULL, "customerType" TEXT NOT NULL, - "customerNameTH" TEXT NOT NULL, + "customerName" TEXT NOT NULL, "customerNameEN" TEXT NOT NULL, "imageUrl" TEXT, "status" "Status" NOT NULL DEFAULT 'CREATED', @@ -186,7 +186,7 @@ CREATE TABLE "CustomerBranch" ( CREATE TABLE "Employee" ( "id" TEXT NOT NULL, "code" TEXT NOT NULL, - "fullNameTH" TEXT NOT NULL, + "fullName" TEXT NOT NULL, "fullNameEN" TEXT NOT NULL, "dateOfBirth" TIMESTAMP(3) NOT NULL, "gender" TEXT NOT NULL, diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b0f82be..51fd626 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -76,7 +76,7 @@ model Branch { taxNo String name String nameEN String - address String + address String addressEN String province Province? @relation(fields: [provinceId], references: [id], onDelete: SetNull) @@ -153,7 +153,7 @@ model User { lastName String lastNameEN String - address String + address String addressEN String province Province? @relation(fields: [provinceId], references: [id], onDelete: SetNull) @@ -203,7 +203,7 @@ model Customer { id String @id @default(uuid()) code String customerType String - customerNameTH String + customerName String customerNameEN String imageUrl String? @@ -264,13 +264,13 @@ model Employee { id String @id @default(uuid()) code String - fullNameTH String + fullName String fullNameEN String dateOfBirth DateTime gender String nationality String - address String + address String addressEN String province Province? @relation(fields: [provinceId], references: [id], onDelete: SetNull) From 57d2c575bc9316a78e7e197966f60d91305a4a00 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 4 Apr 2024 16:08:22 +0700 Subject: [PATCH 054/161] refactor: change from required to not required --- .../20240404090554_update/migration.sql | 11 ++++++++++ prisma/schema.prisma | 20 ++++++++--------- src/controllers/user/user-controller.ts | 22 +++++++++---------- 3 files changed, 32 insertions(+), 21 deletions(-) create mode 100644 prisma/migrations/20240404090554_update/migration.sql diff --git a/prisma/migrations/20240404090554_update/migration.sql b/prisma/migrations/20240404090554_update/migration.sql new file mode 100644 index 0000000..09b8f7b --- /dev/null +++ b/prisma/migrations/20240404090554_update/migration.sql @@ -0,0 +1,11 @@ +-- AlterTable +ALTER TABLE "User" ALTER COLUMN "registrationNo" DROP NOT NULL, +ALTER COLUMN "startDate" DROP NOT NULL, +ALTER COLUMN "retireDate" DROP NOT NULL, +ALTER COLUMN "discountCondition" DROP NOT NULL, +ALTER COLUMN "licenseNo" DROP NOT NULL, +ALTER COLUMN "licenseIssueDate" DROP NOT NULL, +ALTER COLUMN "licenseExpireDate" DROP NOT NULL, +ALTER COLUMN "sourceNationality" DROP NOT NULL, +ALTER COLUMN "importNationality" DROP NOT NULL, +ALTER COLUMN "trainingPlace" DROP NOT NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 51fd626..8b98e78 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -170,24 +170,24 @@ model User { email String telephoneNo String - registrationNo String + registrationNo String? - startDate DateTime - retireDate DateTime + startDate DateTime? + retireDate DateTime? userType String userRole String - discountCondition String + discountCondition String? - licenseNo String - licenseIssueDate DateTime - licenseExpireDate DateTime + licenseNo String? + licenseIssueDate DateTime? + licenseExpireDate DateTime? - sourceNationality String - importNationality String + sourceNationality String? + importNationality String? - trainingPlace String + trainingPlace String? status Status @default(CREATED) diff --git a/src/controllers/user/user-controller.ts b/src/controllers/user/user-controller.ts index 30bfa19..f507caf 100644 --- a/src/controllers/user/user-controller.ts +++ b/src/controllers/user/user-controller.ts @@ -37,17 +37,17 @@ type UserCreate = { lastName: string; lastNameEN: string; - code: string; - registrationNo: string; - startDate: Date; - retireDate: Date; - discountCondition: string; - licenseNo: string; - licenseIssueDate: Date; - licenseExpireDate: Date; - sourceNationality: string; - importNationality: string; - trainingPlace: string; + code?: string; + registrationNo?: string; + startDate?: Date; + retireDate?: Date; + discountCondition?: string; + licenseNo?: string; + licenseIssueDate?: Date; + licenseExpireDate?: Date; + sourceNationality?: string; + importNationality?: string; + trainingPlace?: string; address: string; addressEN: string; From 203fd88e66496f21d2fb10e1056a06195404602e Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 4 Apr 2024 17:42:52 +0700 Subject: [PATCH 055/161] refactor: database fields --- prisma/schema.prisma | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8b98e78..0a43c92 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -233,6 +233,7 @@ model CustomerBranch { registerDate DateTime authorizedCapital String + address String addressEN String province Province? @relation(fields: [provinceId], references: [id], onDelete: SetNull) @@ -252,6 +253,8 @@ model CustomerBranch { latitude String longitude String + status Status @default(CREATED) + createdBy String? createdAt DateTime @default(now()) updateBy String? @@ -264,8 +267,12 @@ model Employee { id String @id @default(uuid()) code String - fullName String - fullNameEN String + nrcNo String + firstName String + firstNameEN String + lastName String + lastNameEN String + dateOfBirth DateTime gender String nationality String @@ -289,7 +296,6 @@ model Employee { arrivalBarricade String arrivalCardNo String - profileImageUrl String customerBranch CustomerBranch? @relation(fields: [customerBranchId], references: [id], onDelete: SetNull) customerBranchId String? @@ -303,7 +309,7 @@ model Employee { employeeCheckup EmployeeCheckup[] employeeWork EmployeeWork[] - EmployeeOtherInfo EmployeeOtherInfo[] + employeeOtherInfo EmployeeOtherInfo[] } model EmployeeCheckup { From 818112fff4914d1444aab893816f0d97a1334799 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 5 Apr 2024 09:03:20 +0700 Subject: [PATCH 056/161] chore: reset migration --- .../20240404090554_update/migration.sql | 11 ------- .../migration.sql | 30 +++++++++++-------- 2 files changed, 17 insertions(+), 24 deletions(-) delete mode 100644 prisma/migrations/20240404090554_update/migration.sql rename prisma/migrations/{20240404082734_init => 20240405020304_init}/migration.sql (96%) diff --git a/prisma/migrations/20240404090554_update/migration.sql b/prisma/migrations/20240404090554_update/migration.sql deleted file mode 100644 index 09b8f7b..0000000 --- a/prisma/migrations/20240404090554_update/migration.sql +++ /dev/null @@ -1,11 +0,0 @@ --- AlterTable -ALTER TABLE "User" ALTER COLUMN "registrationNo" DROP NOT NULL, -ALTER COLUMN "startDate" DROP NOT NULL, -ALTER COLUMN "retireDate" DROP NOT NULL, -ALTER COLUMN "discountCondition" DROP NOT NULL, -ALTER COLUMN "licenseNo" DROP NOT NULL, -ALTER COLUMN "licenseIssueDate" DROP NOT NULL, -ALTER COLUMN "licenseExpireDate" DROP NOT NULL, -ALTER COLUMN "sourceNationality" DROP NOT NULL, -ALTER COLUMN "importNationality" DROP NOT NULL, -ALTER COLUMN "trainingPlace" DROP NOT NULL; diff --git a/prisma/migrations/20240404082734_init/migration.sql b/prisma/migrations/20240405020304_init/migration.sql similarity index 96% rename from prisma/migrations/20240404082734_init/migration.sql rename to prisma/migrations/20240405020304_init/migration.sql index 6991bd0..6693d29 100644 --- a/prisma/migrations/20240404082734_init/migration.sql +++ b/prisma/migrations/20240405020304_init/migration.sql @@ -115,18 +115,18 @@ CREATE TABLE "User" ( "zipCode" TEXT NOT NULL, "email" TEXT NOT NULL, "telephoneNo" TEXT NOT NULL, - "registrationNo" TEXT NOT NULL, - "startDate" TIMESTAMP(3) NOT NULL, - "retireDate" TIMESTAMP(3) NOT NULL, + "registrationNo" TEXT, + "startDate" TIMESTAMP(3), + "retireDate" TIMESTAMP(3), "userType" TEXT NOT NULL, "userRole" TEXT NOT NULL, - "discountCondition" TEXT NOT NULL, - "licenseNo" TEXT NOT NULL, - "licenseIssueDate" TIMESTAMP(3) NOT NULL, - "licenseExpireDate" TIMESTAMP(3) NOT NULL, - "sourceNationality" TEXT NOT NULL, - "importNationality" TEXT NOT NULL, - "trainingPlace" TEXT NOT NULL, + "discountCondition" TEXT, + "licenseNo" TEXT, + "licenseIssueDate" TIMESTAMP(3), + "licenseExpireDate" TIMESTAMP(3), + "sourceNationality" TEXT, + "importNationality" TEXT, + "trainingPlace" TEXT, "status" "Status" NOT NULL DEFAULT 'CREATED', "createdBy" TEXT, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, @@ -165,6 +165,7 @@ CREATE TABLE "CustomerBranch" ( "registerName" TEXT NOT NULL, "registerDate" TIMESTAMP(3) NOT NULL, "authorizedCapital" TEXT NOT NULL, + "address" TEXT NOT NULL, "addressEN" TEXT NOT NULL, "provinceId" TEXT, "districtId" TEXT, @@ -174,6 +175,7 @@ CREATE TABLE "CustomerBranch" ( "telephoneNo" TEXT NOT NULL, "latitude" TEXT NOT NULL, "longitude" TEXT NOT NULL, + "status" "Status" NOT NULL DEFAULT 'CREATED', "createdBy" TEXT, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updateBy" TEXT, @@ -186,8 +188,11 @@ CREATE TABLE "CustomerBranch" ( CREATE TABLE "Employee" ( "id" TEXT NOT NULL, "code" TEXT NOT NULL, - "fullName" TEXT NOT NULL, - "fullNameEN" TEXT NOT NULL, + "nrcNo" TEXT NOT NULL, + "firstName" TEXT NOT NULL, + "firstNameEN" TEXT NOT NULL, + "lastName" TEXT NOT NULL, + "lastNameEN" TEXT NOT NULL, "dateOfBirth" TIMESTAMP(3) NOT NULL, "gender" TEXT NOT NULL, "nationality" TEXT NOT NULL, @@ -201,7 +206,6 @@ CREATE TABLE "Employee" ( "telephoneNo" TEXT NOT NULL, "arrivalBarricade" TEXT NOT NULL, "arrivalCardNo" TEXT NOT NULL, - "profileImageUrl" TEXT NOT NULL, "customerBranchId" TEXT, "status" "Status" NOT NULL DEFAULT 'CREATED', "createdBy" TEXT, From 24c85b728c41f622565dce37752fa14576752958 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 5 Apr 2024 09:39:44 +0700 Subject: [PATCH 057/161] feat: filter by branch type (head/sub) and relation --- src/controllers/branch/branch-controller.ts | 24 +++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/controllers/branch/branch-controller.ts b/src/controllers/branch/branch-controller.ts index b4ae109..65b95fb 100644 --- a/src/controllers/branch/branch-controller.ts +++ b/src/controllers/branch/branch-controller.ts @@ -20,7 +20,7 @@ import HttpStatus from "../../interfaces/http-status"; import { RequestWithUser } from "../../interfaces/user"; type BranchCreate = { - code: string; + code?: string; taxNo: string; nameEN: string; name: string; @@ -85,11 +85,17 @@ export class BranchController extends Controller { @Get() async getBranch( @Query() zipCode?: string, + @Query() filter?: "head" | "sub", + @Query() tree?: boolean, @Query() query: string = "", @Query() page: number = 1, @Query() pageSize: number = 30, ) { const where = { + AND: { + headOfficeId: filter === "head" || tree ? null : undefined, + NOT: { headOfficeId: filter === "sub" ? null : undefined }, + }, OR: [ { nameEN: { contains: query }, zipCode }, { name: { contains: query }, zipCode }, @@ -103,6 +109,13 @@ export class BranchController extends Controller { province: true, district: true, subDistrict: true, + branch: tree && { + include: { + province: true, + district: true, + subDistrict: true, + }, + }, }, where, take: pageSize, @@ -115,12 +128,19 @@ export class BranchController extends Controller { } @Get("{branchId}") - async getBranchById(@Path() branchId: string) { + async getBranchById(@Path() branchId: string, @Query() includeSubBranch: boolean) { const record = await prisma.branch.findFirst({ include: { province: true, district: true, subDistrict: true, + branch: includeSubBranch && { + include: { + province: true, + district: true, + subDistrict: true, + }, + }, }, where: { id: branchId }, }); From 24c39d0cf0cb74a1b68db5624ac737d0efd3edf9 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 5 Apr 2024 09:42:54 +0700 Subject: [PATCH 058/161] refactor: auto generate code (may change in the future) --- prisma/schema.prisma | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0a43c92..2093453 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -201,7 +201,7 @@ model User { model Customer { id String @id @default(uuid()) - code String + code String @default(uuid()) customerType String customerName String customerNameEN String From 767127cec9a71a660fabaa788b1b25b8f96b0f97 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 5 Apr 2024 10:38:29 +0700 Subject: [PATCH 059/161] fix: update status once used and must not delete --- src/controllers/branch/branch-controller.ts | 17 +++++------------ src/controllers/branch/user-controller.ts | 8 ++++---- src/controllers/user/user-controller.ts | 13 +------------ 3 files changed, 10 insertions(+), 28 deletions(-) diff --git a/src/controllers/branch/branch-controller.ts b/src/controllers/branch/branch-controller.ts index 65b95fb..79af679 100644 --- a/src/controllers/branch/branch-controller.ts +++ b/src/controllers/branch/branch-controller.ts @@ -207,6 +207,10 @@ export class BranchController extends Controller { }, }); + if (headOfficeId) { + await prisma.branch.update({ where: { id: headOfficeId }, data: { status: Status.ACTIVE } }); + } + this.setStatus(HttpStatus.CREATED); return record; @@ -299,21 +303,10 @@ export class BranchController extends Controller { throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found"); } - if (record.status === Status.USED) { + if (record.status !== Status.CREATED) { throw new HttpError(HttpStatus.FORBIDDEN, "Branch is in used.", "data_in_used"); } - await prisma.user.updateMany({ - where: { - NOT: { - branch: { some: {} }, - }, - }, - data: { - status: Status.CREATED, - }, - }); - return await prisma.branch.delete({ include: { province: true, diff --git a/src/controllers/branch/user-controller.ts b/src/controllers/branch/user-controller.ts index 483f1c4..c326805 100644 --- a/src/controllers/branch/user-controller.ts +++ b/src/controllers/branch/user-controller.ts @@ -84,8 +84,8 @@ export class BranchUserController extends Controller { } await prisma.user.updateMany({ - where: { id: { in: body.user } }, - data: { status: Status.USED }, + where: { id: { in: body.user }, status: Status.CREATED }, + data: { status: Status.ACTIVE }, }); await prisma.branchUser.createMany({ @@ -177,8 +177,8 @@ export class UserBranchController extends Controller { } await prisma.branch.updateMany({ - where: { id: { in: body.branch } }, - data: { status: Status.USED }, + where: { id: { in: body.branch }, status: Status.CREATED }, + data: { status: Status.ACTIVE }, }); await prisma.branchUser.createMany({ diff --git a/src/controllers/user/user-controller.ts b/src/controllers/user/user-controller.ts index f507caf..6be8828 100644 --- a/src/controllers/user/user-controller.ts +++ b/src/controllers/user/user-controller.ts @@ -321,7 +321,7 @@ export class UserController extends Controller { throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found.", "data_not_found"); } - if (record.status === Status.USED) { + if (record.status !== Status.CREATED) { throw new HttpError(HttpStatus.FORBIDDEN, "User is in used.", "data_in_used"); } @@ -329,17 +329,6 @@ export class UserController extends Controller { forceDelete: true, }); - await prisma.branch.updateMany({ - where: { - NOT: { - user: { some: {} }, - }, - }, - data: { - status: Status.CREATED, - }, - }); - return await prisma.user.delete({ include: { province: true, From 1e722fc2db257181bd43a51d30baefa18956c9fb Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 5 Apr 2024 10:41:03 +0700 Subject: [PATCH 060/161] chore: migration --- .../20240405033140_update/migration.sql | 43 +++++++++++++++++++ prisma/schema.prisma | 3 +- 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 prisma/migrations/20240405033140_update/migration.sql diff --git a/prisma/migrations/20240405033140_update/migration.sql b/prisma/migrations/20240405033140_update/migration.sql new file mode 100644 index 0000000..0745fdf --- /dev/null +++ b/prisma/migrations/20240405033140_update/migration.sql @@ -0,0 +1,43 @@ +/* + Warnings: + + - The values [USED] on the enum `Status` will be removed. If these variants are still used in the database, this will fail. + +*/ +-- AlterEnum +BEGIN; +CREATE TYPE "Status_new" AS ENUM ('CREATED', 'ACTIVE', 'INACTIVE'); +ALTER TABLE "Product" ALTER COLUMN "status" DROP DEFAULT; +ALTER TABLE "Customer" ALTER COLUMN "status" DROP DEFAULT; +ALTER TABLE "Service" ALTER COLUMN "status" DROP DEFAULT; +ALTER TABLE "Work" ALTER COLUMN "status" DROP DEFAULT; +ALTER TABLE "CustomerBranch" ALTER COLUMN "status" DROP DEFAULT; +ALTER TABLE "Employee" ALTER COLUMN "status" DROP DEFAULT; +ALTER TABLE "Branch" ALTER COLUMN "status" DROP DEFAULT; +ALTER TABLE "ProductGroup" ALTER COLUMN "status" DROP DEFAULT; +ALTER TABLE "ProductType" ALTER COLUMN "status" DROP DEFAULT; +ALTER TABLE "User" ALTER COLUMN "status" DROP DEFAULT; +ALTER TABLE "Branch" ALTER COLUMN "status" TYPE "Status_new" USING ("status"::text::"Status_new"); +ALTER TABLE "User" ALTER COLUMN "status" TYPE "Status_new" USING ("status"::text::"Status_new"); +ALTER TABLE "Customer" ALTER COLUMN "status" TYPE "Status_new" USING ("status"::text::"Status_new"); +ALTER TABLE "CustomerBranch" ALTER COLUMN "status" TYPE "Status_new" USING ("status"::text::"Status_new"); +ALTER TABLE "Employee" ALTER COLUMN "status" TYPE "Status_new" USING ("status"::text::"Status_new"); +ALTER TABLE "Service" ALTER COLUMN "status" TYPE "Status_new" USING ("status"::text::"Status_new"); +ALTER TABLE "Work" ALTER COLUMN "status" TYPE "Status_new" USING ("status"::text::"Status_new"); +ALTER TABLE "ProductGroup" ALTER COLUMN "status" TYPE "Status_new" USING ("status"::text::"Status_new"); +ALTER TABLE "ProductType" ALTER COLUMN "status" TYPE "Status_new" USING ("status"::text::"Status_new"); +ALTER TABLE "Product" ALTER COLUMN "status" TYPE "Status_new" USING ("status"::text::"Status_new"); +ALTER TYPE "Status" RENAME TO "Status_old"; +ALTER TYPE "Status_new" RENAME TO "Status"; +DROP TYPE "Status_old"; +ALTER TABLE "Product" ALTER COLUMN "status" SET DEFAULT 'CREATED'; +ALTER TABLE "Customer" ALTER COLUMN "status" SET DEFAULT 'CREATED'; +ALTER TABLE "Service" ALTER COLUMN "status" SET DEFAULT 'CREATED'; +ALTER TABLE "Work" ALTER COLUMN "status" SET DEFAULT 'CREATED'; +ALTER TABLE "CustomerBranch" ALTER COLUMN "status" SET DEFAULT 'CREATED'; +ALTER TABLE "Employee" ALTER COLUMN "status" SET DEFAULT 'CREATED'; +ALTER TABLE "Branch" ALTER COLUMN "status" SET DEFAULT 'CREATED'; +ALTER TABLE "ProductGroup" ALTER COLUMN "status" SET DEFAULT 'CREATED'; +ALTER TABLE "ProductType" ALTER COLUMN "status" SET DEFAULT 'CREATED'; +ALTER TABLE "User" ALTER COLUMN "status" SET DEFAULT 'CREATED'; +COMMIT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 2093453..d1f1ef4 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -67,7 +67,8 @@ model SubDistrict { enum Status { CREATED - USED + ACTIVE + INACTIVE } model Branch { From 36e00ce9353ce620faab0aa2e6bde71d69f29f9a Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 5 Apr 2024 10:42:16 +0700 Subject: [PATCH 061/161] chore: move file --- src/controllers/{user => }/user-controller.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) rename src/controllers/{user => }/user-controller.ts (97%) diff --git a/src/controllers/user/user-controller.ts b/src/controllers/user-controller.ts similarity index 97% rename from src/controllers/user/user-controller.ts rename to src/controllers/user-controller.ts index 6be8828..c0fe6be 100644 --- a/src/controllers/user/user-controller.ts +++ b/src/controllers/user-controller.ts @@ -14,11 +14,11 @@ import { } from "tsoa"; import { Prisma, Status } from "@prisma/client"; -import prisma from "../../db"; -import minio from "../../services/minio"; -import { RequestWithUser } from "../../interfaces/user"; -import HttpError from "../../interfaces/http-error"; -import HttpStatus from "../../interfaces/http-status"; +import prisma from "../db"; +import minio from "../services/minio"; +import { RequestWithUser } from "../interfaces/user"; +import HttpError from "../interfaces/http-error"; +import HttpStatus from "../interfaces/http-status"; if (!process.env.MINIO_BUCKET) { throw Error("Require MinIO bucket."); From 4d8ea943a90e9f547fe17379f10ab2ec60d5392e Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 5 Apr 2024 10:44:13 +0700 Subject: [PATCH 062/161] feat: status input on created and update --- src/controllers/branch/branch-controller.ts | 2 ++ src/controllers/user-controller.ts | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/src/controllers/branch/branch-controller.ts b/src/controllers/branch/branch-controller.ts index 79af679..8d30d10 100644 --- a/src/controllers/branch/branch-controller.ts +++ b/src/controllers/branch/branch-controller.ts @@ -21,6 +21,7 @@ import { RequestWithUser } from "../../interfaces/user"; type BranchCreate = { code?: string; + status?: Status; taxNo: string; nameEN: string; name: string; @@ -40,6 +41,7 @@ type BranchCreate = { type BranchUpdate = { code?: string; + status?: "ACTIVE" | "INACTIVE"; taxNo?: string; nameEN?: string; name?: string; diff --git a/src/controllers/user-controller.ts b/src/controllers/user-controller.ts index c0fe6be..1863c21 100644 --- a/src/controllers/user-controller.ts +++ b/src/controllers/user-controller.ts @@ -27,6 +27,8 @@ if (!process.env.MINIO_BUCKET) { const MINIO_BUCKET = process.env.MINIO_BUCKET; type UserCreate = { + status?: Status; + keycloakId: string; userType: string; @@ -61,6 +63,8 @@ type UserCreate = { }; type UserUpdate = { + status?: "ACTIVE" | "INACTIVE"; + userType?: string; userRole?: string; From 41c27dced5f7dd83bf480976c46764670488edeb Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 5 Apr 2024 10:53:52 +0700 Subject: [PATCH 063/161] feat: customer create endpoint --- src/controllers/customer-controller.ts | 63 ++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 src/controllers/customer-controller.ts diff --git a/src/controllers/customer-controller.ts b/src/controllers/customer-controller.ts new file mode 100644 index 0000000..97f3625 --- /dev/null +++ b/src/controllers/customer-controller.ts @@ -0,0 +1,63 @@ +import { Prisma, Status } from "@prisma/client"; +import { Body, Controller, Delete, Get, Path, Post, Put, Query, Request, Route, Tags } from "tsoa"; +import { RequestWithUser } from "../interfaces/user"; +import prisma from "../db"; +import minio from "../services/minio"; +import HttpStatus from "../interfaces/http-status"; +import HttpError from "../interfaces/http-error"; + +if (!process.env.MINIO_BUCKET) { + throw Error("Require MinIO bucket."); +} + +const MINIO_BUCKET = process.env.MINIO_BUCKET; + +export type CustomerCreate = { + code?: string; + status?: Status; + customerType: string; + customerName: string; + customerNameEN: string; +}; + +export type CustomerUpdate = { + code?: string; + status?: "ACTIVE" | "INACTIVE"; + customerType?: string; + customerName?: string; + customerNameEN?: string; +}; + +function imageLocation(id: string) { + return `customer/img-${id}`; +} + +@Route("api/customer") +@Tags("Customer") +export class CustomerController extends Controller { + @Post() + async create(@Request() req: RequestWithUser, @Body() body: CustomerCreate) { + const record = await prisma.customer.create({ + data: { + ...body, + createdBy: req.user.name, + updateBy: req.user.name, + }, + }); + + this.setStatus(HttpStatus.CREATED); + + return Object.assign(record, { + imageUrl: await minio.presignedGetObject( + MINIO_BUCKET, + imageLocation(record.id), + 12 * 60 * 60, + ), + imageUploadUrl: await minio.presignedPutObject( + MINIO_BUCKET, + imageLocation(record.id), + 12 * 60 * 60, + ), + }); + } +} From 62bbc0a0712328e366e8426d3a8d9c650740a22f Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 5 Apr 2024 10:57:14 +0700 Subject: [PATCH 064/161] fix: missing auth --- src/controllers/customer-controller.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/controllers/customer-controller.ts b/src/controllers/customer-controller.ts index 97f3625..d993eb5 100644 --- a/src/controllers/customer-controller.ts +++ b/src/controllers/customer-controller.ts @@ -34,6 +34,7 @@ function imageLocation(id: string) { @Route("api/customer") @Tags("Customer") +@Security("keycloak") export class CustomerController extends Controller { @Post() async create(@Request() req: RequestWithUser, @Body() body: CustomerCreate) { From 578fc225d964423d5c0fe764242eeb2ddd62a971 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 5 Apr 2024 10:57:43 +0700 Subject: [PATCH 065/161] chore: format --- src/controllers/customer-controller.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/controllers/customer-controller.ts b/src/controllers/customer-controller.ts index d993eb5..68ce96c 100644 --- a/src/controllers/customer-controller.ts +++ b/src/controllers/customer-controller.ts @@ -1,5 +1,19 @@ import { Prisma, Status } from "@prisma/client"; import { Body, Controller, Delete, Get, Path, Post, Put, Query, Request, Route, Tags } from "tsoa"; +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 minio from "../services/minio"; From e76650bfce5ec89dab6fe287aa3ffed725aedb72 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 5 Apr 2024 11:00:31 +0700 Subject: [PATCH 066/161] feat: list customer --- src/controllers/customer-controller.ts | 32 ++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/controllers/customer-controller.ts b/src/controllers/customer-controller.ts index 68ce96c..a02da59 100644 --- a/src/controllers/customer-controller.ts +++ b/src/controllers/customer-controller.ts @@ -50,6 +50,38 @@ function imageLocation(id: string) { @Tags("Customer") @Security("keycloak") export class CustomerController extends Controller { + @Get() + async list( + @Query() query: string = "", + @Query() page: number = 1, + @Query() pageSize: number = 30, + ) { + const where = { + OR: [{ customerName: { contains: query } }, { customerNameEN: { contains: query } }], + } satisfies Prisma.CustomerWhereInput; + + const [result, total] = await prisma.$transaction([ + prisma.customer.findMany({ + where, + take: pageSize, + skip: (page - 1) * pageSize, + }), + prisma.customer.count({ where }), + ]); + + return { + result: await Promise.all( + result.map(async (v) => ({ + ...v, + imageUrl: await minio.presignedGetObject(MINIO_BUCKET, imageLocation(v.id), 12 * 60 * 60), + })), + ), + page, + pageSize, + total, + }; + } + @Post() async create(@Request() req: RequestWithUser, @Body() body: CustomerCreate) { const record = await prisma.customer.create({ From 239582b4722ea6b9a2c48f1915c1f59d60ec5564 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 5 Apr 2024 11:00:44 +0700 Subject: [PATCH 067/161] feat: list customer by id --- src/controllers/customer-controller.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/controllers/customer-controller.ts b/src/controllers/customer-controller.ts index a02da59..74748bf 100644 --- a/src/controllers/customer-controller.ts +++ b/src/controllers/customer-controller.ts @@ -82,6 +82,14 @@ export class CustomerController extends Controller { }; } + @Get("{customerId}") + async getById(@Path() customerId: string) { + const record = await prisma.customer.findFirst({ where: { id: customerId } }); + if (!record) + throw new HttpError(HttpStatus.NOT_FOUND, "Customer cannot be found.", "data_not_found"); + return record; + } + @Post() async create(@Request() req: RequestWithUser, @Body() body: CustomerCreate) { const record = await prisma.customer.create({ From 6e07e0658eb6d0309d3fb4d419214ab188e69109 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 5 Apr 2024 11:02:23 +0700 Subject: [PATCH 068/161] feat: delete customer --- src/controllers/customer-controller.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/controllers/customer-controller.ts b/src/controllers/customer-controller.ts index 74748bf..e11957c 100644 --- a/src/controllers/customer-controller.ts +++ b/src/controllers/customer-controller.ts @@ -115,4 +115,19 @@ export class CustomerController extends Controller { ), }); } + + @Delete("{customerId}") + async deleteById(@Path() customerId: string) { + const record = await prisma.customer.findFirst({ where: { id: customerId } }); + + if (!record) { + throw new HttpError(HttpStatus.NOT_FOUND, "Customer cannot be found.", "data_not_found"); + } + + if (record.status !== Status.CREATED) { + throw new HttpError(HttpStatus.FORBIDDEN, "Customer is in used.", "data_in_used"); + } + + return await prisma.customer.delete({ where: { id: customerId } }); + } } From f9fdfce9bacc7449251a0ffd31b143d5eb75ef02 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 5 Apr 2024 11:02:40 +0700 Subject: [PATCH 069/161] fix: bad commit 578fc225 --- src/controllers/customer-controller.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/controllers/customer-controller.ts b/src/controllers/customer-controller.ts index e11957c..e93c2ae 100644 --- a/src/controllers/customer-controller.ts +++ b/src/controllers/customer-controller.ts @@ -1,5 +1,4 @@ import { Prisma, Status } from "@prisma/client"; -import { Body, Controller, Delete, Get, Path, Post, Put, Query, Request, Route, Tags } from "tsoa"; import { Body, Controller, From 7bd3c2ebd35180e8cfbfe79c3e430ba431467134 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 5 Apr 2024 11:06:01 +0700 Subject: [PATCH 070/161] fix: missing image url --- src/controllers/customer-controller.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/controllers/customer-controller.ts b/src/controllers/customer-controller.ts index e93c2ae..acc12a2 100644 --- a/src/controllers/customer-controller.ts +++ b/src/controllers/customer-controller.ts @@ -86,7 +86,13 @@ export class CustomerController extends Controller { const record = await prisma.customer.findFirst({ where: { id: customerId } }); if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "Customer cannot be found.", "data_not_found"); - return record; + return Object.assign(record, { + imageUrl: await minio.presignedGetObject( + MINIO_BUCKET, + imageLocation(record.id), + 12 * 60 * 60, + ), + }); } @Post() From ef88a48d0cf3cd335e8df1fdd7807650fec784ae Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 5 Apr 2024 11:18:01 +0700 Subject: [PATCH 071/161] feat: edit customer endpoint --- src/controllers/customer-controller.ts | 33 ++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/controllers/customer-controller.ts b/src/controllers/customer-controller.ts index acc12a2..fe7d347 100644 --- a/src/controllers/customer-controller.ts +++ b/src/controllers/customer-controller.ts @@ -121,6 +121,39 @@ export class CustomerController extends Controller { }); } + @Put("{customerId}") + async editById( + @Path() customerId: string, + @Request() req: RequestWithUser, + @Body() body: CustomerUpdate, + ) { + const record = await prisma.customer.update({ + where: { id: customerId }, + data: { + ...body, + createdBy: req.user.name, + updateBy: req.user.name, + }, + }); + + if (!record) { + throw new HttpError(HttpStatus.NOT_FOUND, "Customer cannot be found."); + } + + return Object.assign(record, { + imageUrl: await minio.presignedGetObject( + MINIO_BUCKET, + imageLocation(record.id), + 12 * 60 * 60, + ), + imageUploadUrl: await minio.presignedPutObject( + MINIO_BUCKET, + imageLocation(record.id), + 12 * 60 * 60, + ), + }); + } + @Delete("{customerId}") async deleteById(@Path() customerId: string) { const record = await prisma.customer.findFirst({ where: { id: customerId } }); From d5ce9a4bb8b15aa2b4c9e68de7d65dead8acdbf8 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 5 Apr 2024 11:18:36 +0700 Subject: [PATCH 072/161] chore: add tags to open api spec (for sorting purpose) --- tsoa.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tsoa.json b/tsoa.json index 2f059de..92b82d2 100644 --- a/tsoa.json +++ b/tsoa.json @@ -12,6 +12,19 @@ "description": "Keycloak Bearer Token", "in": "header" } + }, + "spec": { + "tags": [ + { "name": "OpenAPI" }, + { "name": "Address" }, + { "name": "Branch" }, + { "name": "User" }, + { "name": "Branch User" }, + { "name": "User Branch" }, + { "name": "Customer" }, + { "name": "Customer Branch" }, + { "name": "Employee" } + ] } }, "routes": { From 88a7a4431ac1b1a4e7a1b6aed4f509f78220d147 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 5 Apr 2024 11:25:30 +0700 Subject: [PATCH 073/161] chore: group two controller together --- src/controllers/branch/user-controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/branch/user-controller.ts b/src/controllers/branch/user-controller.ts index c326805..5c766fc 100644 --- a/src/controllers/branch/user-controller.ts +++ b/src/controllers/branch/user-controller.ts @@ -118,7 +118,7 @@ export class BranchUserController extends Controller { type UserBranchBody = { branch: string[] }; @Route("api/user/{userId}/branch") -@Tags("User Branch") +@Tags("Branch User") @Security("keycloak") export class UserBranchController extends Controller { @Get() From b5fb9d3408a8cd59ce33ec5f8ed57724f0750cdd Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 5 Apr 2024 11:30:18 +0700 Subject: [PATCH 074/161] chore: restructure --- ...tact-controller.ts => branch-contact-controller.ts} | 10 +++++----- src/controllers/{branch => }/branch-controller.ts | 8 ++++---- .../user-controller.ts => branch-user-controller.ts} | 8 ++++---- .../user-controller.ts => keycloak-controller.ts} | 2 +- 4 files changed, 14 insertions(+), 14 deletions(-) rename src/controllers/{branch/contact-controller.ts => branch-contact-controller.ts} (94%) rename src/controllers/{branch => }/branch-controller.ts (97%) rename src/controllers/{branch/user-controller.ts => branch-user-controller.ts} (96%) rename src/controllers/{keycloak/user-controller.ts => keycloak-controller.ts} (98%) diff --git a/src/controllers/branch/contact-controller.ts b/src/controllers/branch-contact-controller.ts similarity index 94% rename from src/controllers/branch/contact-controller.ts rename to src/controllers/branch-contact-controller.ts index 65b4398..7a9b3b7 100644 --- a/src/controllers/branch/contact-controller.ts +++ b/src/controllers/branch-contact-controller.ts @@ -13,11 +13,11 @@ import { Tags, } from "tsoa"; -import prisma from "../../db"; -import minio from "../../services/minio"; -import HttpError from "../../interfaces/http-error"; -import HttpStatus from "../../interfaces/http-status"; -import { RequestWithUser } from "../../interfaces/user"; +import prisma from "../db"; +import minio from "../services/minio"; +import HttpError from "../interfaces/http-error"; +import HttpStatus from "../interfaces/http-status"; +import { RequestWithUser } from "../interfaces/user"; if (!process.env.MINIO_BUCKET) { throw Error("Require MinIO bucket."); diff --git a/src/controllers/branch/branch-controller.ts b/src/controllers/branch-controller.ts similarity index 97% rename from src/controllers/branch/branch-controller.ts rename to src/controllers/branch-controller.ts index 8d30d10..0c2c777 100644 --- a/src/controllers/branch/branch-controller.ts +++ b/src/controllers/branch-controller.ts @@ -14,10 +14,10 @@ import { Tags, } from "tsoa"; -import prisma from "../../db"; -import HttpError from "../../interfaces/http-error"; -import HttpStatus from "../../interfaces/http-status"; -import { RequestWithUser } from "../../interfaces/user"; +import prisma from "../db"; +import HttpError from "../interfaces/http-error"; +import HttpStatus from "../interfaces/http-status"; +import { RequestWithUser } from "../interfaces/user"; type BranchCreate = { code?: string; diff --git a/src/controllers/branch/user-controller.ts b/src/controllers/branch-user-controller.ts similarity index 96% rename from src/controllers/branch/user-controller.ts rename to src/controllers/branch-user-controller.ts index 5c766fc..5987661 100644 --- a/src/controllers/branch/user-controller.ts +++ b/src/controllers/branch-user-controller.ts @@ -13,10 +13,10 @@ import { Tags, } from "tsoa"; -import prisma from "../../db"; -import HttpError from "../../interfaces/http-error"; -import HttpStatus from "../../interfaces/http-status"; -import { RequestWithUser } from "../../interfaces/user"; +import prisma from "../db"; +import HttpError from "../interfaces/http-error"; +import HttpStatus from "../interfaces/http-status"; +import { RequestWithUser } from "../interfaces/user"; type BranchUserBody = { user: string[] }; diff --git a/src/controllers/keycloak/user-controller.ts b/src/controllers/keycloak-controller.ts similarity index 98% rename from src/controllers/keycloak/user-controller.ts rename to src/controllers/keycloak-controller.ts index 086efe6..209b943 100644 --- a/src/controllers/keycloak/user-controller.ts +++ b/src/controllers/keycloak-controller.ts @@ -1,5 +1,5 @@ import { Body, Controller, Delete, Get, Path, Post, Route, Security, Tags } from "tsoa"; -import { addUserRoles, createUser, getRoles, removeUserRoles } from "../../services/keycloak"; +import { addUserRoles, createUser, getRoles, removeUserRoles } from "../services/keycloak"; @Route("api/keycloak") @Tags("Keycloak") From 7d26d6a9ea8d38f30271e55e9c0312f05897e1df Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 5 Apr 2024 11:32:02 +0700 Subject: [PATCH 075/161] chore: remove user branch tag (merged with branch user) --- tsoa.json | 1 - 1 file changed, 1 deletion(-) diff --git a/tsoa.json b/tsoa.json index 92b82d2..3db71af 100644 --- a/tsoa.json +++ b/tsoa.json @@ -20,7 +20,6 @@ { "name": "Branch" }, { "name": "User" }, { "name": "Branch User" }, - { "name": "User Branch" }, { "name": "Customer" }, { "name": "Customer Branch" }, { "name": "Employee" } From 20d9598a4eb2c711db34fe24c6f442f29dd978fb Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 5 Apr 2024 11:37:40 +0700 Subject: [PATCH 076/161] feat: add customer branch controller --- src/controllers/customer-branch-controller.ts | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/controllers/customer-branch-controller.ts diff --git a/src/controllers/customer-branch-controller.ts b/src/controllers/customer-branch-controller.ts new file mode 100644 index 0000000..f83a434 --- /dev/null +++ b/src/controllers/customer-branch-controller.ts @@ -0,0 +1,35 @@ +import { Prisma, Status } from "@prisma/client"; +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 HttpStatus from "../interfaces/http-status"; +import HttpError from "../interfaces/http-error"; +import minio from "../services/minio"; + +if (!process.env.MINIO_BUCKET) { + throw Error("Require MinIO bucket."); +} + +const MINIO_BUCKET = process.env.MINIO_BUCKET; + +function imageLocation(id: string) { + return `employee/profile-img-${id}`; +} +@Route("api/customer-branch") +@Tags("Customer Branch") +@Security("keycloak") +export class CustomerBranchController extends Controller { +} From a90400b34864df87f0ef53058af9974a678f381e Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 5 Apr 2024 11:42:03 +0700 Subject: [PATCH 077/161] feat: add type for create and edit --- src/controllers/customer-branch-controller.ts | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/src/controllers/customer-branch-controller.ts b/src/controllers/customer-branch-controller.ts index f83a434..4c1cca9 100644 --- a/src/controllers/customer-branch-controller.ts +++ b/src/controllers/customer-branch-controller.ts @@ -28,6 +28,67 @@ const MINIO_BUCKET = process.env.MINIO_BUCKET; function imageLocation(id: string) { return `employee/profile-img-${id}`; } + +type CustomerBranchCreate = { + customerId: string; + + code: string; + status?: Status; + + branchNo: string; + legalPersonNo: string; + + taxNo: string; + name: string; + nameEN: string; + addressEN: string; + address: string; + zipCode: string; + email: string; + telephoneNo: string; + longitude: string; + latitude: string; + + registerName: string; + registerDate: Date; + authorizedCapital: string; + + subDistrictId?: string | null; + districtId?: string | null; + provinceId?: string | null; + headOfficeId?: string | null; +}; + +type CustomerBranchUpdate = { + customerId?: string; + + code?: string; + status?: "ACTIVE" | "INACTIVE"; + + branchNo?: string; + legalPersonNo?: string; + + taxNo?: string; + name?: string; + nameEN?: string; + addressEN?: string; + address?: string; + zipCode?: string; + email?: string; + telephoneNo?: string; + longitude?: string; + latitude?: string; + + registerName?: string; + registerDate?: Date; + authorizedCapital?: string; + + subDistrictId?: string | null; + districtId?: string | null; + provinceId?: string | null; + headOfficeId?: string | null; +}; + @Route("api/customer-branch") @Tags("Customer Branch") @Security("keycloak") From fd37eb65203157c18c85ecd25aa9c68252976a3a Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 5 Apr 2024 13:51:36 +0700 Subject: [PATCH 078/161] feat: create customer branch endpoint --- src/controllers/customer-branch-controller.ts | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/src/controllers/customer-branch-controller.ts b/src/controllers/customer-branch-controller.ts index 4c1cca9..2a8b48d 100644 --- a/src/controllers/customer-branch-controller.ts +++ b/src/controllers/customer-branch-controller.ts @@ -93,4 +93,67 @@ type CustomerBranchUpdate = { @Tags("Customer Branch") @Security("keycloak") export class CustomerBranchController extends Controller { + @Post() + async create(@Request() req: RequestWithUser, @Body() body: CustomerBranchCreate) { + if (body.provinceId || body.districtId || body.subDistrictId || body.customerId) { + const [province, district, subDistrict, customer] = await prisma.$transaction([ + prisma.province.findFirst({ where: { id: body.provinceId || undefined } }), + prisma.district.findFirst({ where: { id: body.districtId || undefined } }), + prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }), + prisma.customer.findFirst({ where: { id: body.customerId || undefined } }), + ]); + if (body.provinceId && !province) + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Province cannot be found.", + "missing_or_invalid_parameter", + ); + if (body.districtId && !district) + throw new HttpError( + HttpStatus.BAD_REQUEST, + "District cannot be found.", + "missing_or_invalid_parameter", + ); + if (body.subDistrictId && !subDistrict) + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Sub-district cannot be found.", + "missing_or_invalid_parameter", + ); + if (body.customerId && !customer) + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Customer cannot be found.", + "missing_or_invalid_parameter", + ); + } + + const { provinceId, districtId, subDistrictId, customerId, ...rest } = body; + + const record = await prisma.customerBranch.create({ + include: { + province: true, + district: true, + subDistrict: true, + }, + data: { + ...rest, + customer: { connect: { id: customerId } }, + province: { connect: provinceId ? { id: provinceId } : undefined }, + district: { connect: districtId ? { id: districtId } : undefined }, + subDistrict: { connect: subDistrictId ? { id: subDistrictId } : undefined }, + createdBy: req.user.name, + updateBy: req.user.name, + }, + }); + + await prisma.customer.update({ + where: { id: customerId, status: Status.CREATED }, + data: { status: Status.ACTIVE }, + }); + + this.setStatus(HttpStatus.CREATED); + + return record; + } } From 6adc96135254702c1bd175ddf5ccd4406a963f8e Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 5 Apr 2024 14:15:38 +0700 Subject: [PATCH 079/161] feat: edit customer branch endpoint --- src/controllers/customer-branch-controller.ts | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/controllers/customer-branch-controller.ts b/src/controllers/customer-branch-controller.ts index 2a8b48d..06682e0 100644 --- a/src/controllers/customer-branch-controller.ts +++ b/src/controllers/customer-branch-controller.ts @@ -156,4 +156,77 @@ export class CustomerBranchController extends Controller { return record; } + + @Put("{branchId}") + async editById( + @Request() req: RequestWithUser, + @Body() body: CustomerBranchUpdate, + @Path() branchId: string, + ) { + if (body.provinceId || body.districtId || body.subDistrictId || body.customerId) { + const [province, district, subDistrict, customer] = await prisma.$transaction([ + prisma.province.findFirst({ where: { id: body.provinceId || undefined } }), + prisma.district.findFirst({ where: { id: body.districtId || undefined } }), + prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }), + prisma.customer.findFirst({ where: { id: body.customerId || undefined } }), + ]); + if (body.provinceId && !province) + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Province cannot be found.", + "missing_or_invalid_parameter", + ); + if (body.districtId && !district) + throw new HttpError( + HttpStatus.BAD_REQUEST, + "District cannot be found.", + "missing_or_invalid_parameter", + ); + if (body.subDistrictId && !subDistrict) + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Sub-district cannot be found.", + "missing_or_invalid_parameter", + ); + if (body.customerId && !customer) + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Customer cannot be found.", + "missing_or_invalid_parameter", + ); + } + + const { provinceId, districtId, subDistrictId, customerId, ...rest } = body; + + const record = await prisma.customerBranch.update({ + where: { id: branchId }, + include: { + province: true, + district: true, + subDistrict: true, + }, + data: { + ...rest, + customer: { connect: customerId ? { id: customerId } : undefined }, + province: { + connect: provinceId ? { id: provinceId } : undefined, + disconnect: provinceId === null || undefined, + }, + district: { + connect: districtId ? { id: districtId } : undefined, + disconnect: districtId === null || undefined, + }, + subDistrict: { + connect: subDistrictId ? { id: subDistrictId } : undefined, + disconnect: subDistrictId === null || undefined, + }, + createdBy: req.user.name, + updateBy: req.user.name, + }, + }); + + this.setStatus(HttpStatus.CREATED); + + return record; + } } From d43ac9f54e75f2af6eb1530951c9a1d1739dec20 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 5 Apr 2024 14:27:38 +0700 Subject: [PATCH 080/161] fix: input type not matched with db --- src/controllers/customer-branch-controller.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/controllers/customer-branch-controller.ts b/src/controllers/customer-branch-controller.ts index 06682e0..ce26649 100644 --- a/src/controllers/customer-branch-controller.ts +++ b/src/controllers/customer-branch-controller.ts @@ -32,7 +32,6 @@ function imageLocation(id: string) { type CustomerBranchCreate = { customerId: string; - code: string; status?: Status; branchNo: string; @@ -56,13 +55,11 @@ type CustomerBranchCreate = { subDistrictId?: string | null; districtId?: string | null; provinceId?: string | null; - headOfficeId?: string | null; }; type CustomerBranchUpdate = { customerId?: string; - code?: string; status?: "ACTIVE" | "INACTIVE"; branchNo?: string; @@ -86,7 +83,6 @@ type CustomerBranchUpdate = { subDistrictId?: string | null; districtId?: string | null; provinceId?: string | null; - headOfficeId?: string | null; }; @Route("api/customer-branch") From 58611f8589b9f394aeb252ef6fa69df63e510dd3 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 5 Apr 2024 14:28:25 +0700 Subject: [PATCH 081/161] fix: error when trying to update --- src/controllers/customer-branch-controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/customer-branch-controller.ts b/src/controllers/customer-branch-controller.ts index ce26649..642d63d 100644 --- a/src/controllers/customer-branch-controller.ts +++ b/src/controllers/customer-branch-controller.ts @@ -143,7 +143,7 @@ export class CustomerBranchController extends Controller { }, }); - await prisma.customer.update({ + await prisma.customer.updateMany({ where: { id: customerId, status: Status.CREATED }, data: { status: Status.ACTIVE }, }); From 011d53d65572cfcc7ef205efe0cfd743aecab698 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 5 Apr 2024 14:43:21 +0700 Subject: [PATCH 082/161] fix: error on not exist --- src/controllers/customer-branch-controller.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/controllers/customer-branch-controller.ts b/src/controllers/customer-branch-controller.ts index 642d63d..4312eb6 100644 --- a/src/controllers/customer-branch-controller.ts +++ b/src/controllers/customer-branch-controller.ts @@ -194,6 +194,10 @@ export class CustomerBranchController extends Controller { const { provinceId, districtId, subDistrictId, customerId, ...rest } = body; + if (!(await prisma.customerBranch.findUnique({ where: { id: branchId } }))) { + throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found"); + } + const record = await prisma.customerBranch.update({ where: { id: branchId }, include: { From 910c8b50e44df26a7b334990e6d66fec31b39df8 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 5 Apr 2024 14:55:24 +0700 Subject: [PATCH 083/161] feat: list customer branch --- src/controllers/customer-branch-controller.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/controllers/customer-branch-controller.ts b/src/controllers/customer-branch-controller.ts index 4312eb6..e13984c 100644 --- a/src/controllers/customer-branch-controller.ts +++ b/src/controllers/customer-branch-controller.ts @@ -89,6 +89,38 @@ type CustomerBranchUpdate = { @Tags("Customer Branch") @Security("keycloak") export class CustomerBranchController extends Controller { + @Get() + async list( + @Query() zipCode?: string, + @Query() query: string = "", + @Query() page: number = 1, + @Query() pageSize: number = 30, + ) { + const where = { + OR: [ + { nameEN: { contains: query }, zipCode }, + { name: { contains: query }, zipCode }, + { email: { contains: query }, zipCode }, + ], + } satisfies Prisma.BranchWhereInput; + + const [result, total] = await prisma.$transaction([ + prisma.customerBranch.findMany({ + include: { + province: true, + district: true, + subDistrict: true, + }, + where, + take: pageSize, + skip: (page - 1) * pageSize, + }), + prisma.customerBranch.count({ where }), + ]); + + return { result, page, pageSize, total }; + } + @Post() async create(@Request() req: RequestWithUser, @Body() body: CustomerBranchCreate) { if (body.provinceId || body.districtId || body.subDistrictId || body.customerId) { From 620a57c6c635519f2f9ff913fa022fef5afe27fa Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 5 Apr 2024 15:07:29 +0700 Subject: [PATCH 084/161] feat: get employee of customer branch --- src/controllers/customer-branch-controller.ts | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/controllers/customer-branch-controller.ts b/src/controllers/customer-branch-controller.ts index e13984c..8ece315 100644 --- a/src/controllers/customer-branch-controller.ts +++ b/src/controllers/customer-branch-controller.ts @@ -121,6 +121,56 @@ export class CustomerBranchController extends Controller { return { result, page, pageSize, total }; } + @Get("{branchId}/employee") + async listEmployee( + @Path() branchId: string, + @Query() zipCode?: string, + @Query() query: string = "", + @Query() page: number = 1, + @Query() pageSize: number = 30, + ) { + const where = { + AND: { customerBranchId: branchId }, + OR: [ + { firstName: { contains: query }, zipCode }, + { firstNameEN: { contains: query }, zipCode }, + { lastName: { contains: query }, zipCode }, + { lastNameEN: { contains: query }, zipCode }, + { email: { contains: query }, zipCode }, + ], + } satisfies Prisma.EmployeeWhereInput; + + const [result, total] = await prisma.$transaction([ + prisma.employee.findMany({ + include: { + province: true, + district: true, + subDistrict: true, + }, + where, + take: pageSize, + skip: (page - 1) * pageSize, + }), + prisma.employee.count({ where }), + ]); + + return { + result: await Promise.all( + result.map(async (v) => ({ + ...v, + profileImageUrl: await minio.presignedGetObject( + MINIO_BUCKET, + imageLocation(v.id), + 12 * 60 * 60, + ), + })), + ), + page, + pageSize, + total, + }; + } + @Post() async create(@Request() req: RequestWithUser, @Body() body: CustomerBranchCreate) { if (body.provinceId || body.districtId || body.subDistrictId || body.customerId) { From 3f43ad1b9bbf7e3fd865b1761c0f4f19736100cc Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 5 Apr 2024 15:09:44 +0700 Subject: [PATCH 085/161] feat: get customer branch by id --- src/controllers/customer-branch-controller.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/controllers/customer-branch-controller.ts b/src/controllers/customer-branch-controller.ts index 8ece315..5b6b7b6 100644 --- a/src/controllers/customer-branch-controller.ts +++ b/src/controllers/customer-branch-controller.ts @@ -121,6 +121,24 @@ export class CustomerBranchController extends Controller { return { result, page, pageSize, total }; } + @Get("{branchId}") + async getById(@Path() branchId: string) { + const record = await prisma.customerBranch.findFirst({ + include: { + province: true, + district: true, + subDistrict: true, + }, + where: { id: branchId }, + }); + + if (!record) { + throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found"); + } + + return record; + } + @Get("{branchId}/employee") async listEmployee( @Path() branchId: string, From c0e59ccb703781bff5f2773de11ed212fbb90bbf Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 5 Apr 2024 15:13:33 +0700 Subject: [PATCH 086/161] feat: delete customer branch --- src/controllers/customer-branch-controller.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/controllers/customer-branch-controller.ts b/src/controllers/customer-branch-controller.ts index 5b6b7b6..303d9f8 100644 --- a/src/controllers/customer-branch-controller.ts +++ b/src/controllers/customer-branch-controller.ts @@ -329,4 +329,23 @@ export class CustomerBranchController extends Controller { return record; } + + @Delete("{branchId}") + async delete(@Path() branchId: string) { + const record = await prisma.customerBranch.findFirst({ where: { id: branchId } }); + + if (!record) { + throw new HttpError( + HttpStatus.NOT_FOUND, + "Customer branch cannot be found.", + "data_not_found", + ); + } + + if (record.status !== Status.CREATED) { + throw new HttpError(HttpStatus.FORBIDDEN, "Customer branch is in used.", "data_in_used"); + } + + return await prisma.customerBranch.delete({ where: { id: branchId } }); + } } From 6899c9823a2aedb00e372d55fd83445b3cae0c4c Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 5 Apr 2024 15:23:59 +0700 Subject: [PATCH 087/161] fix: error when record not found on update --- src/controllers/branch-contact-controller.ts | 22 ++++++++++++++++---- src/controllers/branch-controller.ts | 8 ++++--- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/controllers/branch-contact-controller.ts b/src/controllers/branch-contact-controller.ts index 7a9b3b7..90ccbe7 100644 --- a/src/controllers/branch-contact-controller.ts +++ b/src/controllers/branch-contact-controller.ts @@ -132,13 +132,23 @@ export class BranchContactController extends Controller { @Path() branchId: string, @Path() contactId: string, ) { + if ( + !(await prisma.branchContact.findUnique({ + where: { id: contactId, branchId }, + })) + ) { + throw new HttpError( + HttpStatus.NOT_FOUND, + "Branch contact cannot be found.", + "data_not_found", + ); + } + const record = await prisma.branchContact.update({ data: { ...body, updateBy: req.user.name }, where: { id: contactId, branchId }, }); - if (!record) { - throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found"); - } + return Object.assign(record, { qrCodeImageUrl: await minio.presignedGetObject( MINIO_BUCKET, @@ -157,7 +167,11 @@ export class BranchContactController extends Controller { async deleteBranchContact(@Path() branchId: string, @Path() contactId: string) { const result = await prisma.branchContact.deleteMany({ where: { id: contactId, branchId } }); if (result.count <= 0) { - throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found"); + throw new HttpError( + HttpStatus.NOT_FOUND, + "Branch contact cannot be found.", + "data_not_found", + ); } await minio.removeObject(MINIO_BUCKET, imageLocation(contactId), { forceDelete: true, diff --git a/src/controllers/branch-controller.ts b/src/controllers/branch-controller.ts index 0c2c777..bb0ba51 100644 --- a/src/controllers/branch-controller.ts +++ b/src/controllers/branch-controller.ts @@ -259,6 +259,10 @@ export class BranchController extends Controller { const { provinceId, districtId, subDistrictId, headOfficeId, ...rest } = body; + if (!(await prisma.branch.findUnique({ where: { id: branchId } }))) { + throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found"); + } + const record = await prisma.branch.update({ include: { province: true, district: true, subDistrict: true }, data: { @@ -284,9 +288,7 @@ export class BranchController extends Controller { }, where: { id: branchId }, }); - if (!record) { - throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found"); - } + return record; } From addd4caa63a04739ac2cedcbc2e19ce8422a9dca Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 5 Apr 2024 15:24:24 +0700 Subject: [PATCH 088/161] feat: add update user data endpoint --- src/controllers/keycloak-controller.ts | 20 ++++++++++++--- src/services/keycloak.ts | 34 ++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/controllers/keycloak-controller.ts b/src/controllers/keycloak-controller.ts index 209b943..f2aa82d 100644 --- a/src/controllers/keycloak-controller.ts +++ b/src/controllers/keycloak-controller.ts @@ -1,8 +1,14 @@ -import { Body, Controller, Delete, Get, Path, Post, Route, Security, Tags } from "tsoa"; -import { addUserRoles, createUser, getRoles, removeUserRoles } from "../services/keycloak"; +import { Body, Controller, Delete, Get, Path, Post, Put, Route, Security, Tags } from "tsoa"; +import { + addUserRoles, + createUser, + editUser, + getRoles, + removeUserRoles, +} from "../services/keycloak"; @Route("api/keycloak") -@Tags("Keycloak") +@Tags("Single-Sign On") @Security("keycloak") export class KeycloakController extends Controller { @Post("user") @@ -15,6 +21,14 @@ export class KeycloakController extends Controller { }); } + @Put("user/{userId}") + async editUser( + @Path() userId: string, + @Body() body: { username?: string; password?: string; firstName?: string; lastName?: string }, + ) { + return await editUser(userId, body); + } + @Get("role") async getRole() { const role = await getRoles(); diff --git a/src/services/keycloak.ts b/src/services/keycloak.ts index c12a3b4..15089e1 100644 --- a/src/services/keycloak.ts +++ b/src/services/keycloak.ts @@ -91,6 +91,40 @@ export async function createUser(username: string, password: string, opts?: Reco return id || true; } +/** + * Update keycloak user by given username and password with roles + * + * Client must have permission to manage realm's user + * + * @returns user uuid or true if success, false otherwise. + */ +export async function editUser(userId: string, opts: Record) { + const { password, ...rest } = opts; + + const res = await fetch(`${KC_URL}/admin/realms/${KC_REALM}/users/${userId}`, { + // prettier-ignore + headers: { + "authorization": `Bearer ${await getToken()}`, + "content-type": `application/json`, + }, + method: "PUT", + body: JSON.stringify({ + enabled: true, + credentials: (password && [{ type: "password", value: opts?.password }]) || undefined, + ...rest, + }), + }).catch((e) => console.log("Keycloak Error: ", e)); + + if (!res) return false; + if (!res.ok) { + return Boolean(console.error("Keycloak Error Response: ", await res.json())); + } + + const path = res.headers.get("Location"); + const id = path?.split("/").at(-1); + return id || true; +} + /** * Get roles list or specific role data * From f7738f45916be8a74f5e5c908b60a77ba9fae211 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 5 Apr 2024 15:24:44 +0700 Subject: [PATCH 089/161] chore: sort tag --- tsoa.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tsoa.json b/tsoa.json index 3db71af..2b4f809 100644 --- a/tsoa.json +++ b/tsoa.json @@ -16,13 +16,20 @@ "spec": { "tags": [ { "name": "OpenAPI" }, + { "name": "Single-Sign On" }, { "name": "Address" }, { "name": "Branch" }, + { "name": "Branch Contact" }, { "name": "User" }, { "name": "Branch User" }, { "name": "Customer" }, { "name": "Customer Branch" }, - { "name": "Employee" } + { "name": "Employee" }, + { "name": "Employee Checkup" }, + { "name": "Service" }, + { "name": "Work" }, + { "name": "Product Type" }, + { "name": "Product Group" } ] } }, From 372bd546393fe593ec62a3950ee978ddc65668b7 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 5 Apr 2024 15:24:59 +0700 Subject: [PATCH 090/161] chore: update package name --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0fc68a0..390a7e9 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "template", + "name": "jws-backend", "version": "1.0.0", "description": "", "main": "./dist/app.js", From 97cc610171be0b6396a0b8c6a7132d83ce1f4699 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 5 Apr 2024 16:43:44 +0700 Subject: [PATCH 091/161] refactor: do not update status if not created --- src/controllers/branch-controller.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/controllers/branch-controller.ts b/src/controllers/branch-controller.ts index bb0ba51..f988da4 100644 --- a/src/controllers/branch-controller.ts +++ b/src/controllers/branch-controller.ts @@ -210,7 +210,10 @@ export class BranchController extends Controller { }); if (headOfficeId) { - await prisma.branch.update({ where: { id: headOfficeId }, data: { status: Status.ACTIVE } }); + await prisma.branch.updateMany({ + where: { id: headOfficeId, status: Status.CREATED }, + data: { status: Status.ACTIVE }, + }); } this.setStatus(HttpStatus.CREATED); From f9a737faffc232f2ae634310da350ce1294d4241 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 5 Apr 2024 16:43:59 +0700 Subject: [PATCH 092/161] feat: add gender field to database --- src/controllers/user-controller.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/controllers/user-controller.ts b/src/controllers/user-controller.ts index 1863c21..97c9b42 100644 --- a/src/controllers/user-controller.ts +++ b/src/controllers/user-controller.ts @@ -38,6 +38,7 @@ type UserCreate = { firstNameEN: string; lastName: string; lastNameEN: string; + gender: string; code?: string; registrationNo?: string; @@ -72,6 +73,7 @@ type UserUpdate = { firstNameEN?: string; lastName?: string; lastNameEN?: string; + gender?: string; code?: string; registrationNo?: string; From 5bafff7482d27fef6a3b97c6af98044b4b5228e9 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 5 Apr 2024 16:46:47 +0700 Subject: [PATCH 093/161] chore: add migration --- prisma/migrations/20240405094638_update/migration.sql | 8 ++++++++ prisma/schema.prisma | 1 + 2 files changed, 9 insertions(+) create mode 100644 prisma/migrations/20240405094638_update/migration.sql diff --git a/prisma/migrations/20240405094638_update/migration.sql b/prisma/migrations/20240405094638_update/migration.sql new file mode 100644 index 0000000..f5bdc4b --- /dev/null +++ b/prisma/migrations/20240405094638_update/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `gender` to the `User` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "gender" TEXT NOT NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d1f1ef4..9896ee5 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -153,6 +153,7 @@ model User { firstNameEN String lastName String lastNameEN String + gender String address String addressEN String From a69738549be08c3da413880f51e7f07a6812b03d Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 5 Apr 2024 18:10:11 +0700 Subject: [PATCH 094/161] fix: optional query --- src/controllers/branch-controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/branch-controller.ts b/src/controllers/branch-controller.ts index f988da4..31a0dd2 100644 --- a/src/controllers/branch-controller.ts +++ b/src/controllers/branch-controller.ts @@ -130,7 +130,7 @@ export class BranchController extends Controller { } @Get("{branchId}") - async getBranchById(@Path() branchId: string, @Query() includeSubBranch: boolean) { + async getBranchById(@Path() branchId: string, @Query() includeSubBranch?: boolean) { const record = await prisma.branch.findFirst({ include: { province: true, From 112bd0eb2344a36fdd8e9b22ee4611e818ddbf9f Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 8 Apr 2024 20:55:52 +0700 Subject: [PATCH 095/161] chore: sort tag --- tsoa.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tsoa.json b/tsoa.json index 2b4f809..f503e5e 100644 --- a/tsoa.json +++ b/tsoa.json @@ -26,6 +26,8 @@ { "name": "Customer Branch" }, { "name": "Employee" }, { "name": "Employee Checkup" }, + { "name": "Employee Work" }, + { "name": "Employee Other Info" }, { "name": "Service" }, { "name": "Work" }, { "name": "Product Type" }, From 120e2942b87f28ce0482d81f30bf64bb1f1a7325 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 9 Apr 2024 08:51:22 +0700 Subject: [PATCH 096/161] fix: data ordering --- src/controllers/branch-contact-controller.ts | 1 + src/controllers/branch-controller.ts | 1 + src/controllers/branch-user-controller.ts | 2 ++ src/controllers/customer-branch-controller.ts | 2 ++ src/controllers/customer-controller.ts | 1 + src/controllers/user-controller.ts | 1 + 6 files changed, 8 insertions(+) diff --git a/src/controllers/branch-contact-controller.ts b/src/controllers/branch-contact-controller.ts index 90ccbe7..1f8a473 100644 --- a/src/controllers/branch-contact-controller.ts +++ b/src/controllers/branch-contact-controller.ts @@ -51,6 +51,7 @@ export class BranchContactController extends Controller { ) { const [result, total] = await prisma.$transaction([ prisma.branchContact.findMany({ + orderBy: { createdAt: "asc" }, where: { branchId }, take: pageSize, skip: (page - 1) * pageSize, diff --git a/src/controllers/branch-controller.ts b/src/controllers/branch-controller.ts index 31a0dd2..adb3fef 100644 --- a/src/controllers/branch-controller.ts +++ b/src/controllers/branch-controller.ts @@ -107,6 +107,7 @@ export class BranchController extends Controller { const [result, total] = await prisma.$transaction([ prisma.branch.findMany({ + orderBy: { createdAt: "asc" }, include: { province: true, district: true, diff --git a/src/controllers/branch-user-controller.ts b/src/controllers/branch-user-controller.ts index 5987661..503d215 100644 --- a/src/controllers/branch-user-controller.ts +++ b/src/controllers/branch-user-controller.ts @@ -45,6 +45,7 @@ export class BranchUserController extends Controller { const [result, total] = await prisma.$transaction([ prisma.branchUser.findMany({ + orderBy: { user: { createdAt: "asc" } }, include: { user: { include: { @@ -138,6 +139,7 @@ export class UserBranchController extends Controller { const [result, total] = await prisma.$transaction([ prisma.branchUser.findMany({ + orderBy: { branch: { createdAt: "asc" } }, include: { branch: { include: { diff --git a/src/controllers/customer-branch-controller.ts b/src/controllers/customer-branch-controller.ts index 303d9f8..329d512 100644 --- a/src/controllers/customer-branch-controller.ts +++ b/src/controllers/customer-branch-controller.ts @@ -106,6 +106,7 @@ export class CustomerBranchController extends Controller { const [result, total] = await prisma.$transaction([ prisma.customerBranch.findMany({ + orderBy: { createdAt: "asc" }, include: { province: true, district: true, @@ -160,6 +161,7 @@ export class CustomerBranchController extends Controller { const [result, total] = await prisma.$transaction([ prisma.employee.findMany({ + orderBy: { createdAt: "asc" }, include: { province: true, district: true, diff --git a/src/controllers/customer-controller.ts b/src/controllers/customer-controller.ts index fe7d347..b6686f2 100644 --- a/src/controllers/customer-controller.ts +++ b/src/controllers/customer-controller.ts @@ -61,6 +61,7 @@ export class CustomerController extends Controller { const [result, total] = await prisma.$transaction([ prisma.customer.findMany({ + orderBy: { createdAt: "asc" }, where, take: pageSize, skip: (page - 1) * pageSize, diff --git a/src/controllers/user-controller.ts b/src/controllers/user-controller.ts index 97c9b42..672f7cb 100644 --- a/src/controllers/user-controller.ts +++ b/src/controllers/user-controller.ts @@ -127,6 +127,7 @@ export class UserController extends Controller { const [result, total] = await prisma.$transaction([ prisma.user.findMany({ + orderBy: { createdAt: "asc" }, include: { province: true, district: true, From 3abff0594a0bcdfad4afe0fdb186b757893cb91b Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 9 Apr 2024 11:45:29 +0700 Subject: [PATCH 097/161] feat: auto gen branch code --- src/controllers/branch-controller.ts | 76 +++++++++++++++------------- 1 file changed, 42 insertions(+), 34 deletions(-) diff --git a/src/controllers/branch-controller.ts b/src/controllers/branch-controller.ts index adb3fef..ea98c88 100644 --- a/src/controllers/branch-controller.ts +++ b/src/controllers/branch-controller.ts @@ -20,7 +20,6 @@ import HttpStatus from "../interfaces/http-status"; import { RequestWithUser } from "../interfaces/user"; type BranchCreate = { - code?: string; status?: Status; taxNo: string; nameEN: string; @@ -40,7 +39,6 @@ type BranchCreate = { }; type BranchUpdate = { - code?: string; status?: "ACTIVE" | "INACTIVE"; taxNo?: string; nameEN?: string; @@ -157,41 +155,50 @@ export class BranchController extends Controller { @Post() async createBranch(@Request() req: RequestWithUser, @Body() body: BranchCreate) { - if (body.provinceId || body.districtId || body.subDistrictId || body.headOfficeId) { - const [province, district, subDistrict, branch] = await prisma.$transaction([ - prisma.province.findFirst({ where: { id: body.provinceId || undefined } }), - prisma.district.findFirst({ where: { id: body.districtId || undefined } }), - prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }), - prisma.branch.findFirst({ where: { id: body.headOfficeId || undefined } }), - ]); - if (body.provinceId && !province) - throw new HttpError( - HttpStatus.BAD_REQUEST, - "Province cannot be found.", - "missing_or_invalid_parameter", - ); - if (body.districtId && !district) - throw new HttpError( - HttpStatus.BAD_REQUEST, - "District cannot be found.", - "missing_or_invalid_parameter", - ); - if (body.subDistrictId && !subDistrict) - throw new HttpError( - HttpStatus.BAD_REQUEST, - "Sub-district cannot be found.", - "missing_or_invalid_parameter", - ); - if (body.headOfficeId && !branch) - throw new HttpError( - HttpStatus.BAD_REQUEST, - "Head branch cannot be found.", - "missing_or_invalid_parameter", - ); - } + const [province, district, subDistrict, head] = await prisma.$transaction([ + prisma.province.findFirst({ where: { id: body.provinceId || undefined } }), + prisma.district.findFirst({ where: { id: body.districtId || undefined } }), + prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }), + prisma.branch.findFirst({ where: { id: body.headOfficeId || undefined } }), + ]); + if (body.provinceId && !province) + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Province cannot be found.", + "missing_or_invalid_parameter", + ); + if (body.districtId && !district) + throw new HttpError( + HttpStatus.BAD_REQUEST, + "District cannot be found.", + "missing_or_invalid_parameter", + ); + if (body.subDistrictId && !subDistrict) + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Sub-district cannot be found.", + "missing_or_invalid_parameter", + ); + if (body.headOfficeId && !head) + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Head branch cannot be found.", + "missing_or_invalid_parameter", + ); const { provinceId, districtId, subDistrictId, headOfficeId, ...rest } = body; + const year = new Date().getFullYear(); + + const last = await prisma.branch.findFirst({ + orderBy: { createdAt: "desc" }, + where: { headOfficeId: headOfficeId ?? null }, + }); + + const code = !headOfficeId + ? `HQ${year.toString().slice(2)}${+(last?.code.slice(-1) || 0) + 1}` + : `BR${head?.code.slice(2, 5)}${(+(last?.code.slice(-2) || 0) + 1).toString().padStart(2, "0")}`; + const record = await prisma.branch.create({ include: { province: true, @@ -200,6 +207,7 @@ export class BranchController extends Controller { }, data: { ...rest, + code, isHeadOffice: !headOfficeId, province: { connect: provinceId ? { id: provinceId } : undefined }, district: { connect: districtId ? { id: districtId } : undefined }, From 3f0ed2c8d67e9b163b2f52ec7b9d564f7d58a348 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 9 Apr 2024 13:05:49 +0700 Subject: [PATCH 098/161] feat: auto gen user code --- .../20240409040419_update/migration.sql | 5 ++ prisma/schema.prisma | 15 +++- src/controllers/branch-user-controller.ts | 85 ++++++++++++++----- src/controllers/user-controller.ts | 40 +++++++-- 4 files changed, 113 insertions(+), 32 deletions(-) create mode 100644 prisma/migrations/20240409040419_update/migration.sql diff --git a/prisma/migrations/20240409040419_update/migration.sql b/prisma/migrations/20240409040419_update/migration.sql new file mode 100644 index 0000000..f43823a --- /dev/null +++ b/prisma/migrations/20240409040419_update/migration.sql @@ -0,0 +1,5 @@ +-- CreateEnum +CREATE TYPE "UserType" AS ENUM ('USER', 'MESSENGER', 'DELEGATE', 'AGENCY'); + +-- AlterTable +ALTER TABLE "User" ALTER COLUMN "code" DROP NOT NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9896ee5..cc45b79 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -73,7 +73,7 @@ enum Status { model Branch { id String @id @default(uuid()) - code String @default(uuid()) + code String taxNo String name String nameEN String @@ -143,12 +143,19 @@ model BranchUser { updatedAt DateTime @updatedAt } +enum UserType { + USER + MESSENGER + DELEGATE + AGENCY +} + model User { id String @id @default(uuid()) keycloakId String - code String @default(uuid()) + code String? firstName String firstNameEN String lastName String @@ -177,7 +184,7 @@ model User { startDate DateTime? retireDate DateTime? - userType String + userType UserType userRole String discountCondition String? @@ -203,7 +210,7 @@ model User { model Customer { id String @id @default(uuid()) - code String @default(uuid()) + code String customerType String customerName String customerNameEN String diff --git a/src/controllers/branch-user-controller.ts b/src/controllers/branch-user-controller.ts index 503d215..c96e5a2 100644 --- a/src/controllers/branch-user-controller.ts +++ b/src/controllers/branch-user-controller.ts @@ -1,4 +1,4 @@ -import { Prisma, Status } from "@prisma/client"; +import { Prisma, Status, UserType } from "@prisma/client"; import { Body, Controller, @@ -71,10 +71,23 @@ export class BranchUserController extends Controller { @Path() branchId: string, @Body() body: BranchUserBody, ) { - const user = await prisma.user.findMany({ - include: { branch: true }, - where: { id: { in: body.user } }, - }); + const [branch, user] = await prisma.$transaction([ + prisma.branch.findUnique({ + where: { id: branchId }, + }), + prisma.user.findMany({ + include: { branch: true }, + where: { id: { in: body.user } }, + }), + ]); + + if (!branch) { + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Branch cannot be found.", + "missing_or_invalid_parameter", + ); + } if (user.length !== body.user.length) { throw new HttpError( @@ -84,21 +97,55 @@ export class BranchUserController extends Controller { ); } - await prisma.user.updateMany({ - where: { id: { in: body.user }, status: Status.CREATED }, - data: { status: Status.ACTIVE }, - }); + await prisma.$transaction([ + prisma.user.updateMany({ + where: { id: { in: body.user }, status: Status.CREATED }, + data: { status: Status.ACTIVE }, + }), + prisma.branchUser.createMany({ + data: user + .filter((a) => !a.branch.some((b) => b.branchId === branchId)) + .map((v) => ({ + branchId, + userId: v.id, + createdBy: req.user.name, + updateBy: req.user.name, + })), + }), + ]); - await prisma.branchUser.createMany({ - data: user - .filter((a) => !a.branch.some((b) => b.branchId === branchId)) - .map((v) => ({ - branchId, - userId: v.id, - createdBy: req.user.name, - updateBy: req.user.name, - })), - }); + const group: Record = { + USER: [], + AGENCY: [], + DELEGATE: [], + MESSENGER: [], + }; + + for (const u of user) group[u.userType].push(u.id); + + for (const g of Object.values(UserType)) { + if (group[g].length === 0) continue; + + const last = await prisma.user.findFirst({ + orderBy: { createdAt: "desc" }, + where: { + userType: g, + code: { startsWith: `${branch.code.slice(2, 5).padEnd(3, "0")}` }, + }, + }); + + const code = (idx: number) => + `${branch.code.slice(4).padEnd(3, "0")}${g !== "USER" ? g.charAt(0) : ""}${(+(last?.code?.slice(-4) || 0) + idx + 1).toString().padStart(4, "0")}`; + + await prisma.$transaction( + group[g].map((v, i) => + prisma.user.updateMany({ + where: { id: v, code: null }, + data: { code: code(i) }, + }), + ), + ); + } } @Delete() diff --git a/src/controllers/user-controller.ts b/src/controllers/user-controller.ts index 672f7cb..5c6d1d1 100644 --- a/src/controllers/user-controller.ts +++ b/src/controllers/user-controller.ts @@ -12,7 +12,7 @@ import { Security, Tags, } from "tsoa"; -import { Prisma, Status } from "@prisma/client"; +import { Prisma, Status, UserType } from "@prisma/client"; import prisma from "../db"; import minio from "../services/minio"; @@ -31,7 +31,7 @@ type UserCreate = { keycloakId: string; - userType: string; + userType: UserType; userRole: string; firstName: string; @@ -40,7 +40,6 @@ type UserCreate = { lastNameEN: string; gender: string; - code?: string; registrationNo?: string; startDate?: Date; retireDate?: Date; @@ -66,7 +65,7 @@ type UserCreate = { type UserUpdate = { status?: "ACTIVE" | "INACTIVE"; - userType?: string; + userType?: UserType; userRole?: string; firstName?: string; @@ -75,7 +74,6 @@ type UserUpdate = { lastNameEN?: string; gender?: string; - code?: string; registrationNo?: string; startDate?: Date; retireDate?: Date; @@ -108,7 +106,7 @@ function imageLocation(id: string) { export class UserController extends Controller { @Get() async getUser( - @Query() userType?: string, + @Query() userType?: UserType, @Query() zipCode?: string, @Query() query: string = "", @Query() page: number = 1, @@ -276,10 +274,36 @@ export class UserController extends Controller { const { provinceId, districtId, subDistrictId, ...rest } = body; + const user = await prisma.user.findFirst({ + where: { id: userId }, + }); + + if (!user) { + throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found"); + } + + const lastUserOfType = + body.userType && + body.userType !== user.userType && + user.code && + (await prisma.user.findFirst({ + orderBy: { createdAt: "desc" }, + where: { + userType: body.userType, + code: { startsWith: `${user.code?.slice(0, 3)}` }, + }, + })); + + console.log(lastUserOfType); + const record = await prisma.user.update({ include: { province: true, district: true, subDistrict: true }, data: { ...rest, + code: + (lastUserOfType && + `${user.code?.slice(0, 3)}${body.userType !== "USER" ? body.userType?.charAt(0) : ""}${(+(lastUserOfType?.code?.slice(-4) || 0) + 1).toString().padStart(4, "0")}`) || + undefined, province: { connect: provinceId ? { id: provinceId } : undefined, disconnect: provinceId === null || undefined, @@ -296,9 +320,7 @@ export class UserController extends Controller { }, where: { id: userId }, }); - if (!record) { - throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found."); - } + return Object.assign(record, { profileImageUrl: await minio.presignedGetObject( MINIO_BUCKET, From dd7e5aa2f21a3210c1494862c400260690a459a7 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 9 Apr 2024 13:08:08 +0700 Subject: [PATCH 099/161] fix: type error (temporary) --- src/controllers/customer-controller.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/controllers/customer-controller.ts b/src/controllers/customer-controller.ts index b6686f2..03ce278 100644 --- a/src/controllers/customer-controller.ts +++ b/src/controllers/customer-controller.ts @@ -26,7 +26,6 @@ if (!process.env.MINIO_BUCKET) { const MINIO_BUCKET = process.env.MINIO_BUCKET; export type CustomerCreate = { - code?: string; status?: Status; customerType: string; customerName: string; @@ -34,7 +33,6 @@ export type CustomerCreate = { }; export type CustomerUpdate = { - code?: string; status?: "ACTIVE" | "INACTIVE"; customerType?: string; customerName?: string; @@ -101,6 +99,7 @@ export class CustomerController extends Controller { const record = await prisma.customer.create({ data: { ...body, + code: "CUSTOMER001", createdBy: req.user.name, updateBy: req.user.name, }, From 808bec7fc84504fafb716a328869a7c32f7d7bd3 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 9 Apr 2024 13:13:18 +0700 Subject: [PATCH 100/161] fix: migration reset --- .../20240405033140_update/migration.sql | 43 ------------------- .../20240405094638_update/migration.sql | 8 ---- .../20240409040419_update/migration.sql | 5 --- .../migration.sql | 10 +++-- 4 files changed, 7 insertions(+), 59 deletions(-) delete mode 100644 prisma/migrations/20240405033140_update/migration.sql delete mode 100644 prisma/migrations/20240405094638_update/migration.sql delete mode 100644 prisma/migrations/20240409040419_update/migration.sql rename prisma/migrations/{20240405020304_init => 20240409061231_init}/migration.sql (98%) diff --git a/prisma/migrations/20240405033140_update/migration.sql b/prisma/migrations/20240405033140_update/migration.sql deleted file mode 100644 index 0745fdf..0000000 --- a/prisma/migrations/20240405033140_update/migration.sql +++ /dev/null @@ -1,43 +0,0 @@ -/* - Warnings: - - - The values [USED] on the enum `Status` will be removed. If these variants are still used in the database, this will fail. - -*/ --- AlterEnum -BEGIN; -CREATE TYPE "Status_new" AS ENUM ('CREATED', 'ACTIVE', 'INACTIVE'); -ALTER TABLE "Product" ALTER COLUMN "status" DROP DEFAULT; -ALTER TABLE "Customer" ALTER COLUMN "status" DROP DEFAULT; -ALTER TABLE "Service" ALTER COLUMN "status" DROP DEFAULT; -ALTER TABLE "Work" ALTER COLUMN "status" DROP DEFAULT; -ALTER TABLE "CustomerBranch" ALTER COLUMN "status" DROP DEFAULT; -ALTER TABLE "Employee" ALTER COLUMN "status" DROP DEFAULT; -ALTER TABLE "Branch" ALTER COLUMN "status" DROP DEFAULT; -ALTER TABLE "ProductGroup" ALTER COLUMN "status" DROP DEFAULT; -ALTER TABLE "ProductType" ALTER COLUMN "status" DROP DEFAULT; -ALTER TABLE "User" ALTER COLUMN "status" DROP DEFAULT; -ALTER TABLE "Branch" ALTER COLUMN "status" TYPE "Status_new" USING ("status"::text::"Status_new"); -ALTER TABLE "User" ALTER COLUMN "status" TYPE "Status_new" USING ("status"::text::"Status_new"); -ALTER TABLE "Customer" ALTER COLUMN "status" TYPE "Status_new" USING ("status"::text::"Status_new"); -ALTER TABLE "CustomerBranch" ALTER COLUMN "status" TYPE "Status_new" USING ("status"::text::"Status_new"); -ALTER TABLE "Employee" ALTER COLUMN "status" TYPE "Status_new" USING ("status"::text::"Status_new"); -ALTER TABLE "Service" ALTER COLUMN "status" TYPE "Status_new" USING ("status"::text::"Status_new"); -ALTER TABLE "Work" ALTER COLUMN "status" TYPE "Status_new" USING ("status"::text::"Status_new"); -ALTER TABLE "ProductGroup" ALTER COLUMN "status" TYPE "Status_new" USING ("status"::text::"Status_new"); -ALTER TABLE "ProductType" ALTER COLUMN "status" TYPE "Status_new" USING ("status"::text::"Status_new"); -ALTER TABLE "Product" ALTER COLUMN "status" TYPE "Status_new" USING ("status"::text::"Status_new"); -ALTER TYPE "Status" RENAME TO "Status_old"; -ALTER TYPE "Status_new" RENAME TO "Status"; -DROP TYPE "Status_old"; -ALTER TABLE "Product" ALTER COLUMN "status" SET DEFAULT 'CREATED'; -ALTER TABLE "Customer" ALTER COLUMN "status" SET DEFAULT 'CREATED'; -ALTER TABLE "Service" ALTER COLUMN "status" SET DEFAULT 'CREATED'; -ALTER TABLE "Work" ALTER COLUMN "status" SET DEFAULT 'CREATED'; -ALTER TABLE "CustomerBranch" ALTER COLUMN "status" SET DEFAULT 'CREATED'; -ALTER TABLE "Employee" ALTER COLUMN "status" SET DEFAULT 'CREATED'; -ALTER TABLE "Branch" ALTER COLUMN "status" SET DEFAULT 'CREATED'; -ALTER TABLE "ProductGroup" ALTER COLUMN "status" SET DEFAULT 'CREATED'; -ALTER TABLE "ProductType" ALTER COLUMN "status" SET DEFAULT 'CREATED'; -ALTER TABLE "User" ALTER COLUMN "status" SET DEFAULT 'CREATED'; -COMMIT; diff --git a/prisma/migrations/20240405094638_update/migration.sql b/prisma/migrations/20240405094638_update/migration.sql deleted file mode 100644 index f5bdc4b..0000000 --- a/prisma/migrations/20240405094638_update/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ -/* - Warnings: - - - Added the required column `gender` to the `User` table without a default value. This is not possible if the table is not empty. - -*/ --- AlterTable -ALTER TABLE "User" ADD COLUMN "gender" TEXT NOT NULL; diff --git a/prisma/migrations/20240409040419_update/migration.sql b/prisma/migrations/20240409040419_update/migration.sql deleted file mode 100644 index f43823a..0000000 --- a/prisma/migrations/20240409040419_update/migration.sql +++ /dev/null @@ -1,5 +0,0 @@ --- CreateEnum -CREATE TYPE "UserType" AS ENUM ('USER', 'MESSENGER', 'DELEGATE', 'AGENCY'); - --- AlterTable -ALTER TABLE "User" ALTER COLUMN "code" DROP NOT NULL; diff --git a/prisma/migrations/20240405020304_init/migration.sql b/prisma/migrations/20240409061231_init/migration.sql similarity index 98% rename from prisma/migrations/20240405020304_init/migration.sql rename to prisma/migrations/20240409061231_init/migration.sql index 6693d29..9c0ad32 100644 --- a/prisma/migrations/20240405020304_init/migration.sql +++ b/prisma/migrations/20240409061231_init/migration.sql @@ -1,5 +1,8 @@ -- CreateEnum -CREATE TYPE "Status" AS ENUM ('CREATED', 'USED'); +CREATE TYPE "Status" AS ENUM ('CREATED', 'ACTIVE', 'INACTIVE'); + +-- CreateEnum +CREATE TYPE "UserType" AS ENUM ('USER', 'MESSENGER', 'DELEGATE', 'AGENCY'); -- CreateTable CREATE TABLE "Province" ( @@ -102,11 +105,12 @@ CREATE TABLE "BranchUser" ( CREATE TABLE "User" ( "id" TEXT NOT NULL, "keycloakId" TEXT NOT NULL, - "code" TEXT NOT NULL, + "code" TEXT, "firstName" TEXT NOT NULL, "firstNameEN" TEXT NOT NULL, "lastName" TEXT NOT NULL, "lastNameEN" TEXT NOT NULL, + "gender" TEXT NOT NULL, "address" TEXT NOT NULL, "addressEN" TEXT NOT NULL, "provinceId" TEXT, @@ -118,7 +122,7 @@ CREATE TABLE "User" ( "registrationNo" TEXT, "startDate" TIMESTAMP(3), "retireDate" TIMESTAMP(3), - "userType" TEXT NOT NULL, + "userType" "UserType" NOT NULL, "userRole" TEXT NOT NULL, "discountCondition" TEXT, "licenseNo" TEXT, From b804fafc42595deafc639e4639356746bae03850 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 9 Apr 2024 13:56:15 +0700 Subject: [PATCH 101/161] refactor: customer db --- .../20240409065557_update_customer/migration.sql | 14 ++++++++++++++ prisma/schema.prisma | 10 +++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 prisma/migrations/20240409065557_update_customer/migration.sql diff --git a/prisma/migrations/20240409065557_update_customer/migration.sql b/prisma/migrations/20240409065557_update_customer/migration.sql new file mode 100644 index 0000000..f5291b9 --- /dev/null +++ b/prisma/migrations/20240409065557_update_customer/migration.sql @@ -0,0 +1,14 @@ +/* + Warnings: + + - You are about to drop the column `imageUrl` on the `Customer` table. All the data in the column will be lost. + - Changed the type of `customerType` on the `Customer` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required. + +*/ +-- CreateEnum +CREATE TYPE "CustomerType" AS ENUM ('CORP', 'PERS'); + +-- AlterTable +ALTER TABLE "Customer" DROP COLUMN "imageUrl", +DROP COLUMN "customerType", +ADD COLUMN "customerType" "CustomerType" NOT NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index cc45b79..c30c61e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -208,13 +208,17 @@ model User { branch BranchUser[] } +enum CustomerType { + CORP + PERS +} + model Customer { - id String @id @default(uuid()) + id String @id @default(uuid()) code String - customerType String + customerType CustomerType customerName String customerNameEN String - imageUrl String? status Status @default(CREATED) From 032674f5795813f19e5701d9d500b7e099b07a3e Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 9 Apr 2024 13:58:04 +0700 Subject: [PATCH 102/161] refactor: update type --- src/controllers/customer-controller.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/controllers/customer-controller.ts b/src/controllers/customer-controller.ts index 03ce278..028e812 100644 --- a/src/controllers/customer-controller.ts +++ b/src/controllers/customer-controller.ts @@ -1,4 +1,4 @@ -import { Prisma, Status } from "@prisma/client"; +import { CustomerType, Prisma, Status } from "@prisma/client"; import { Body, Controller, @@ -27,14 +27,14 @@ const MINIO_BUCKET = process.env.MINIO_BUCKET; export type CustomerCreate = { status?: Status; - customerType: string; + customerType: CustomerType; customerName: string; customerNameEN: string; }; export type CustomerUpdate = { status?: "ACTIVE" | "INACTIVE"; - customerType?: string; + customerType: CustomerType; customerName?: string; customerNameEN?: string; }; From eacb225ffee3758d612e79623405a57e8cf7e2c1 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 9 Apr 2024 14:40:55 +0700 Subject: [PATCH 103/161] feat: employee endpoints --- src/controllers/employee-controller.ts | 330 +++++++++++++++++++++++++ 1 file changed, 330 insertions(+) create mode 100644 src/controllers/employee-controller.ts diff --git a/src/controllers/employee-controller.ts b/src/controllers/employee-controller.ts new file mode 100644 index 0000000..aeff13e --- /dev/null +++ b/src/controllers/employee-controller.ts @@ -0,0 +1,330 @@ +import { Prisma, Status } from "@prisma/client"; +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 HttpStatus from "../interfaces/http-status"; +import HttpError from "../interfaces/http-error"; +import minio from "../services/minio"; + +if (!process.env.MINIO_BUCKET) { + throw Error("Require MinIO bucket."); +} + +const MINIO_BUCKET = process.env.MINIO_BUCKET; + +function imageLocation(id: string) { + return `employee/profile-img-${id}`; +} + +type EmployeeCreate = { + customerBranchId: string; + + status?: Status; + + code: string; + nrcNo: string; + + dateOfBirth: Date; + gender: string; + nationality: string; + + firstName: string; + firstNameEN: string; + lastName: string; + lastNameEN: string; + + addressEN: string; + address: string; + zipCode: string; + email: string; + telephoneNo: string; + + arrivalBarricade: string; + arrivalCardNo: string; + + subDistrictId?: string | null; + districtId?: string | null; + provinceId?: string | null; +}; + +type EmployeeUpdate = { + customerBranchId?: string; + status?: "ACTIVE" | "INACTIVE"; + + code?: string; + nrcNo?: string; + + dateOfBirth?: Date; + gender?: string; + nationality?: string; + + firstName?: string; + firstNameEN?: string; + lastName?: string; + lastNameEN?: string; + + addressEN?: string; + address?: string; + zipCode?: string; + email?: string; + telephoneNo?: string; + + arrivalBarricade?: string; + arrivalCardNo?: string; + + subDistrictId?: string | null; + districtId?: string | null; + provinceId?: string | null; +}; + +@Route("api/employee") +@Tags("Employee") +@Security("keycloak") +export class EmployeeController extends Controller { + @Get() + async list( + @Query() zipCode?: string, + @Query() query: string = "", + @Query() page: number = 1, + @Query() pageSize: number = 30, + ) { + const where = { + OR: [ + { firstName: { contains: query }, zipCode }, + { firstNameEN: { contains: query }, zipCode }, + { lastName: { contains: query }, zipCode }, + { lastNameEN: { contains: query }, zipCode }, + { email: { contains: query }, zipCode }, + ], + } satisfies Prisma.EmployeeWhereInput; + + const [result, total] = await prisma.$transaction([ + prisma.employee.findMany({ + orderBy: { createdAt: "asc" }, + include: { + province: true, + district: true, + subDistrict: true, + }, + where, + take: pageSize, + skip: (page - 1) * pageSize, + }), + prisma.employee.count({ where }), + ]); + + return { + result: await Promise.all( + result.map(async (v) => ({ + ...v, + profileImageUrl: await minio.presignedGetObject( + MINIO_BUCKET, + imageLocation(v.id), + 12 * 60 * 60, + ), + })), + ), + page, + pageSize, + total, + }; + } + + @Get("{employeeId}") + async getById(@Path() employeeId: string) { + const record = await prisma.employee.findFirst({ + include: { + province: true, + district: true, + subDistrict: true, + }, + where: { id: employeeId }, + }); + + if (!record) { + throw new HttpError(HttpStatus.NOT_FOUND, "Employee cannot be found.", "data_not_found"); + } + + return record; + } + + @Post() + async create(@Request() req: RequestWithUser, @Body() body: EmployeeCreate) { + if (body.provinceId || body.districtId || body.subDistrictId || body.customerBranchId) { + const [province, district, subDistrict, customerBranch] = await prisma.$transaction([ + prisma.province.findFirst({ where: { id: body.provinceId || undefined } }), + prisma.district.findFirst({ where: { id: body.districtId || undefined } }), + prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }), + prisma.customerBranch.findFirst({ where: { id: body.customerBranchId || undefined } }), + ]); + if (body.provinceId && !province) + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Province cannot be found.", + "missing_or_invalid_parameter", + ); + if (body.districtId && !district) + throw new HttpError( + HttpStatus.BAD_REQUEST, + "District cannot be found.", + "missing_or_invalid_parameter", + ); + if (body.subDistrictId && !subDistrict) + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Sub-district cannot be found.", + "missing_or_invalid_parameter", + ); + if (body.customerBranchId && !customerBranch) + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Customer Branch cannot be found.", + "missing_or_invalid_parameter", + ); + } + + const { provinceId, districtId, subDistrictId, customerBranchId, ...rest } = body; + + const record = await prisma.employee.create({ + include: { + province: true, + district: true, + subDistrict: true, + }, + data: { + ...rest, + province: { connect: provinceId ? { id: provinceId } : undefined }, + district: { connect: districtId ? { id: districtId } : undefined }, + subDistrict: { connect: subDistrictId ? { id: subDistrictId } : undefined }, + customerBranch: { connect: { id: customerBranchId } }, + createdBy: req.user.name, + updateBy: req.user.name, + }, + }); + + await prisma.customerBranch.updateMany({ + where: { id: customerBranchId, status: Status.CREATED }, + data: { status: Status.ACTIVE }, + }); + + this.setStatus(HttpStatus.CREATED); + + return Object.assign(record, { + profileImageUrl: await minio.presignedPutObject( + MINIO_BUCKET, + imageLocation(record.id), + 12 * 60 * 60, + ), + profileImageUploadUrl: await minio.presignedPutObject( + MINIO_BUCKET, + imageLocation(record.id), + 12 * 60 * 60, + ), + }); + } + + @Put("{employeeId}") + async editById( + @Request() req: RequestWithUser, + @Body() body: EmployeeUpdate, + @Path() employeeId: string, + ) { + if (body.provinceId || body.districtId || body.subDistrictId || body.customerBranchId) { + const [province, district, subDistrict, customerBranch] = await prisma.$transaction([ + prisma.province.findFirst({ where: { id: body.provinceId || undefined } }), + prisma.district.findFirst({ where: { id: body.districtId || undefined } }), + prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }), + prisma.customerBranch.findFirst({ where: { id: body.customerBranchId || undefined } }), + ]); + if (body.provinceId && !province) + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Province cannot be found.", + "missing_or_invalid_parameter", + ); + if (body.districtId && !district) + throw new HttpError( + HttpStatus.BAD_REQUEST, + "District cannot be found.", + "missing_or_invalid_parameter", + ); + if (body.subDistrictId && !subDistrict) + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Sub-district cannot be found.", + "missing_or_invalid_parameter", + ); + if (body.customerBranchId && !customerBranch) + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Customer cannot be found.", + "missing_or_invalid_parameter", + ); + } + + const { provinceId, districtId, subDistrictId, customerBranchId, ...rest } = body; + + const record = await prisma.employee.update({ + where: { id: employeeId }, + include: { + province: true, + district: true, + subDistrict: true, + }, + data: { + ...rest, + customerBranch: { connect: customerBranchId ? { id: customerBranchId } : undefined }, + province: { + connect: provinceId ? { id: provinceId } : undefined, + disconnect: provinceId === null || undefined, + }, + district: { + connect: districtId ? { id: districtId } : undefined, + disconnect: districtId === null || undefined, + }, + subDistrict: { + connect: subDistrictId ? { id: subDistrictId } : undefined, + disconnect: subDistrictId === null || undefined, + }, + createdBy: req.user.name, + updateBy: req.user.name, + }, + }); + + this.setStatus(HttpStatus.CREATED); + + return record; + } + + @Delete("{employeeId}") + async delete(@Path() employeeId: string) { + const record = await prisma.employee.findFirst({ where: { id: employeeId } }); + + if (!record) { + throw new HttpError(HttpStatus.NOT_FOUND, "Employee cannot be found.", "data_not_found"); + } + + if (record.status !== Status.CREATED) { + throw new HttpError( + HttpStatus.FORBIDDEN, + "Emplyee is in used.", + "missing_or_invalid_parameter", + ); + } + + return await prisma.employee.delete({ where: { id: employeeId } }); + } +} From 875f7ae0238b317aea89329f46e6e8950a504a8c Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 9 Apr 2024 15:13:44 +0700 Subject: [PATCH 104/161] feat: stat by userType --- src/controllers/branch-controller.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/controllers/branch-controller.ts b/src/controllers/branch-controller.ts index ea98c88..23841ef 100644 --- a/src/controllers/branch-controller.ts +++ b/src/controllers/branch-controller.ts @@ -1,4 +1,4 @@ -import { Prisma, Status } from "@prisma/client"; +import { Prisma, Status, UserType } from "@prisma/client"; import { Body, Controller, @@ -61,11 +61,12 @@ type BranchUpdate = { @Tags("Branch") @Security("keycloak") export class BranchController extends Controller { - @Get("stats") - async getStat() { + @Get("user-stats") + async getUserStat(@Query() userType?: UserType) { const list = await prisma.branchUser.groupBy({ - by: ["branchId"], _count: true, + where: { user: { userType } }, + by: "branchId", }); const record = await prisma.branch.findMany({ From 356e307470495e4649aeea399fc0f102d8daadf5 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 9 Apr 2024 15:17:16 +0700 Subject: [PATCH 105/161] refactor: field name --- src/controllers/branch-controller.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/controllers/branch-controller.ts b/src/controllers/branch-controller.ts index 23841ef..df5f785 100644 --- a/src/controllers/branch-controller.ts +++ b/src/controllers/branch-controller.ts @@ -77,10 +77,11 @@ export class BranchController extends Controller { }, }); - return record.map((a) => ({ - ...a, - userCount: list.find((b) => b.branchId === a.id)?._count ?? 0, - })); + return record.map((a) => + Object.assign(a, { + count: list.find((b) => b.branchId === a.id)?._count ?? 0, + }), + ); } @Get() From 748451240ac1e09a70b564e540b61ab7576fb426 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 9 Apr 2024 15:43:21 +0700 Subject: [PATCH 106/161] fix: update type should not require customerType --- src/controllers/customer-controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/customer-controller.ts b/src/controllers/customer-controller.ts index 028e812..51eb4e4 100644 --- a/src/controllers/customer-controller.ts +++ b/src/controllers/customer-controller.ts @@ -34,7 +34,7 @@ export type CustomerCreate = { export type CustomerUpdate = { status?: "ACTIVE" | "INACTIVE"; - customerType: CustomerType; + customerType?: CustomerType; customerName?: string; customerNameEN?: string; }; From 1f96418de0d5235fb552ea301bb65438cde7e65d Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 9 Apr 2024 17:34:53 +0700 Subject: [PATCH 107/161] refactor: nullable on create --- src/controllers/user-controller.ts | 40 +++++++++++++++--------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/controllers/user-controller.ts b/src/controllers/user-controller.ts index 5c6d1d1..6dd2c93 100644 --- a/src/controllers/user-controller.ts +++ b/src/controllers/user-controller.ts @@ -40,16 +40,16 @@ type UserCreate = { lastNameEN: string; gender: string; - registrationNo?: string; - startDate?: Date; - retireDate?: Date; - discountCondition?: string; - licenseNo?: string; - licenseIssueDate?: Date; - licenseExpireDate?: Date; - sourceNationality?: string; - importNationality?: string; - trainingPlace?: string; + registrationNo?: string | null; + startDate?: Date | null; + retireDate?: Date | null; + discountCondition?: string | null; + licenseNo?: string | null; + licenseIssueDate?: Date | null; + licenseExpireDate?: Date | null; + sourceNationality?: string | null; + importNationality?: string | null; + trainingPlace?: string | null; address: string; addressEN: string; @@ -74,16 +74,16 @@ type UserUpdate = { lastNameEN?: string; gender?: string; - registrationNo?: string; - startDate?: Date; - retireDate?: Date; - discountCondition?: string; - licenseNo?: string; - licenseIssueDate?: Date; - licenseExpireDate?: Date; - sourceNationality?: string; - importNationality?: string; - trainingPlace?: string; + registrationNo?: string | null; + startDate?: Date | null; + retireDate?: Date | null; + discountCondition?: string | null; + licenseNo?: string | null; + licenseIssueDate?: Date | null; + licenseExpireDate?: Date | null; + sourceNationality?: string | null; + importNationality?: string | null; + trainingPlace?: string | null; address?: string; addressEN?: string; From 1e06b3356fc5345b43f55be6ccd2bc6fa8f48715 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 9 Apr 2024 17:51:46 +0700 Subject: [PATCH 108/161] feat: stats user by user type --- src/controllers/user-controller.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/controllers/user-controller.ts b/src/controllers/user-controller.ts index 6dd2c93..86e4afd 100644 --- a/src/controllers/user-controller.ts +++ b/src/controllers/user-controller.ts @@ -104,6 +104,27 @@ function imageLocation(id: string) { @Tags("User") @Security("keycloak") export class UserController extends Controller { + @Get("type-stats") + async getUserTypeStats() { + const list = await prisma.user.groupBy({ + by: "userType", + _count: true, + }); + + return list.reduce>( + (a, c) => { + a[c.userType] = c._count; + return a; + }, + { + USER: 0, + MESSENGER: 0, + DELEGATE: 0, + AGENCY: 0, + }, + ); + } + @Get() async getUser( @Query() userType?: UserType, From eda469779b79cd8ba321701494305115a0f06b11 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 9 Apr 2024 17:54:04 +0700 Subject: [PATCH 109/161] refacotr: change type from object to string instead --- src/middlewares/log.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/middlewares/log.ts b/src/middlewares/log.ts index f57f4e8..3cd45f5 100644 --- a/src/middlewares/log.ts +++ b/src/middlewares/log.ts @@ -60,8 +60,8 @@ async function logMiddleware(req: Request, res: Response, next: NextFunction) { : { 200: "success", 201: "created_success", 204: "no_content", 304: "success" }[ res.statusCode ], - // input: (level < 1 && req.body) || undefined, - // output: (level < 1 && data) || undefined, + input: (level === 4 && JSON.stringify(req.body, null, 2)) || undefined, + output: (level === 4 && JSON.stringify(data, null, 2)) || undefined, ...req.app.locals.logData, }; From 8cc442df18a89d5cf4d946560c1a41d843ecadbe Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 9 Apr 2024 18:04:26 +0700 Subject: [PATCH 110/161] fix: missing full stop --- src/controllers/user-controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/user-controller.ts b/src/controllers/user-controller.ts index 86e4afd..2fefb3a 100644 --- a/src/controllers/user-controller.ts +++ b/src/controllers/user-controller.ts @@ -188,7 +188,7 @@ export class UserController extends Controller { }); if (!record) - throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found", "data_not_found"); + throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found.", "data_not_found"); return Object.assign(record, { profileImageUrl: await minio.presignedGetObject( From 27e545adcdd19e6ea46a08f9e38d9fc6773cd96b Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 10 Apr 2024 10:21:14 +0700 Subject: [PATCH 111/161] feat: require user to change password on first login --- src/controllers/keycloak-controller.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/controllers/keycloak-controller.ts b/src/controllers/keycloak-controller.ts index f2aa82d..90f2400 100644 --- a/src/controllers/keycloak-controller.ts +++ b/src/controllers/keycloak-controller.ts @@ -18,6 +18,7 @@ export class KeycloakController extends Controller { return await createUser(body.username, body.password, { firstName: body.firstName, lastName: body.lastName, + requiredActions: ["UPDATE_PASSWORD"], }); } From 8ab2ab156f577e68301625451c3de386952bc3b0 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 10 Apr 2024 10:32:28 +0700 Subject: [PATCH 112/161] feat: filter out internal roles (3 for now) --- src/controllers/keycloak-controller.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/controllers/keycloak-controller.ts b/src/controllers/keycloak-controller.ts index 90f2400..ffc8ec9 100644 --- a/src/controllers/keycloak-controller.ts +++ b/src/controllers/keycloak-controller.ts @@ -33,7 +33,11 @@ export class KeycloakController extends Controller { @Get("role") async getRole() { const role = await getRoles(); - if (Array.isArray(role)) return role; + if (Array.isArray(role)) + return role.filter( + (a) => + !["uma_authorization", "offline_access", "default-roles"].some((b) => a.name.includes(b)), + ); throw new Error("Failed. Cannot get role."); } From d44c2599996accb77199ead8ad62524a0e6a98fe Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 10 Apr 2024 10:50:45 +0700 Subject: [PATCH 113/161] feat: add attachment to user controller --- src/controllers/user-controller.ts | 97 ++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/src/controllers/user-controller.ts b/src/controllers/user-controller.ts index 2fefb3a..5816ba2 100644 --- a/src/controllers/user-controller.ts +++ b/src/controllers/user-controller.ts @@ -379,6 +379,22 @@ export class UserController extends Controller { forceDelete: true, }); + new Promise((resolve, reject) => { + const item: string[] = []; + + const stream = minio.listObjectsV2(MINIO_BUCKET, `${attachmentLocation(userId)}/`); + + stream.on("data", (v) => v && v.name && item.push(v.name)); + stream.on("end", () => resolve(item)); + stream.on("error", () => reject(new Error("MinIO error."))); + }).then((list) => { + list.map(async (v) => { + await minio.removeObject(MINIO_BUCKET, `${attachmentLocation(userId)}/${v}`, { + forceDelete: true, + }); + }); + }); + return await prisma.user.delete({ include: { province: true, @@ -389,3 +405,84 @@ export class UserController extends Controller { }); } } + +function attachmentLocation(uid: string) { + return `user-attachment/${uid}`; +} + +@Route("api/user/{userId}/attachment") +@Tags("User") +@Security("keycloak") +export class UserAttachmentController extends Controller { + @Get() + async listAttachment(@Path() userId: string) { + const record = await prisma.user.findFirst({ + include: { + province: true, + district: true, + subDistrict: true, + }, + where: { id: userId }, + }); + + if (!record) { + throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found.", "data_not_found"); + } + + const list = await new Promise((resolve, reject) => { + const item: string[] = []; + + const stream = minio.listObjectsV2(MINIO_BUCKET, `${attachmentLocation(userId)}/`); + + stream.on("data", (v) => v && v.name && item.push(v.name)); + stream.on("end", () => resolve(item)); + stream.on("error", () => reject(new Error("MinIO error."))); + }); + + return await Promise.all( + list.map(async (v) => ({ + name: v.split("/").at(-1) as string, + url: await minio.presignedGetObject(MINIO_BUCKET, v, 12 * 60 * 60), + })), + ); + } + + @Post() + async addAttachment(@Path() userId: string, @Body() payload: { file: string[] }) { + const record = await prisma.user.findFirst({ + include: { + province: true, + district: true, + subDistrict: true, + }, + where: { id: userId }, + }); + + if (!record) { + throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found.", "data_not_found"); + } + + return await Promise.all( + payload.file.map(async (v) => ({ + name: v, + url: await minio.presignedGetObject(MINIO_BUCKET, `${attachmentLocation(userId)}/${v}`), + uploadUrl: await minio.presignedPutObject( + MINIO_BUCKET, + `${attachmentLocation(userId)}/${v}`, + 12 * 60 * 60, + ), + })), + ); + } + + @Delete() + async deleteAttachment(@Path() userId: string, @Body() payload: { file: string[] }) { + await Promise.all( + payload.file.map(async (v) => { + await minio.removeObject(MINIO_BUCKET, `${attachmentLocation(userId)}/${v}`, { + forceDelete: true, + }); + }), + ); + } +} From de01db361acd900e474f7efc62567612ddcef80c Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 10 Apr 2024 11:37:12 +0700 Subject: [PATCH 114/161] feat: add more user field --- .../20240410043453_add_missing_user_field/migration.sql | 3 +++ prisma/schema.prisma | 5 ++++- src/controllers/user-controller.ts | 4 ++++ 3 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 prisma/migrations/20240410043453_add_missing_user_field/migration.sql diff --git a/prisma/migrations/20240410043453_add_missing_user_field/migration.sql b/prisma/migrations/20240410043453_add_missing_user_field/migration.sql new file mode 100644 index 0000000..a32f678 --- /dev/null +++ b/prisma/migrations/20240410043453_add_missing_user_field/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "birtDate" TIMESTAMP(3), +ADD COLUMN "responsibleArea" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c30c61e..48e8eb7 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -196,7 +196,10 @@ model User { sourceNationality String? importNationality String? - trainingPlace String? + trainingPlace String? + responsibleArea String? + + birtDate DateTime? status Status @default(CREATED) diff --git a/src/controllers/user-controller.ts b/src/controllers/user-controller.ts index 5816ba2..f1b5f85 100644 --- a/src/controllers/user-controller.ts +++ b/src/controllers/user-controller.ts @@ -50,6 +50,8 @@ type UserCreate = { sourceNationality?: string | null; importNationality?: string | null; trainingPlace?: string | null; + responsibleArea?: string | null; + birthDate?: Date | null; address: string; addressEN: string; @@ -84,6 +86,8 @@ type UserUpdate = { sourceNationality?: string | null; importNationality?: string | null; trainingPlace?: string | null; + responsibleArea?: string | null; + birthDate?: Date | null; address?: string; addressEN?: string; From ac58fe47e942e949410aa9265bd898d40985b429 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 10 Apr 2024 12:23:59 +0700 Subject: [PATCH 115/161] feat: auto generate customer code --- src/controllers/customer-controller.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/controllers/customer-controller.ts b/src/controllers/customer-controller.ts index 51eb4e4..c2a8b52 100644 --- a/src/controllers/customer-controller.ts +++ b/src/controllers/customer-controller.ts @@ -96,10 +96,17 @@ export class CustomerController extends Controller { @Post() async create(@Request() req: RequestWithUser, @Body() body: CustomerCreate) { + const last = await prisma.customer.findFirst({ + orderBy: { createdAt: "desc" }, + where: { customerType: body.customerType }, + }); + + const code = `${body.customerType}${(+(last?.code.slice(-6) || 0) + 1).toString().padStart(6, "0")}`; + const record = await prisma.customer.create({ data: { ...body, - code: "CUSTOMER001", + code, createdBy: req.user.name, updateBy: req.user.name, }, From f7eea342cc367c1707b7dfdbb488257a1b03976e Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 10 Apr 2024 12:32:40 +0700 Subject: [PATCH 116/161] fix: typo --- prisma/migrations/20240410053228_fix_typo/migration.sql | 9 +++++++++ prisma/schema.prisma | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 prisma/migrations/20240410053228_fix_typo/migration.sql diff --git a/prisma/migrations/20240410053228_fix_typo/migration.sql b/prisma/migrations/20240410053228_fix_typo/migration.sql new file mode 100644 index 0000000..59c29dd --- /dev/null +++ b/prisma/migrations/20240410053228_fix_typo/migration.sql @@ -0,0 +1,9 @@ +/* + Warnings: + + - You are about to drop the column `birtDate` on the `User` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "User" DROP COLUMN "birtDate", +ADD COLUMN "birthDate" TIMESTAMP(3); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 48e8eb7..1829f9b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -199,7 +199,7 @@ model User { trainingPlace String? responsibleArea String? - birtDate DateTime? + birthDate DateTime? status Status @default(CREATED) From f89dfcad37d99b679afb7827a2b18e6cbee77f42 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 10 Apr 2024 12:46:23 +0700 Subject: [PATCH 117/161] feat: check for permission using roles --- src/middlewares/auth-provider/keycloak.ts | 12 +++++++----- src/middlewares/auth.ts | 4 ++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/middlewares/auth-provider/keycloak.ts b/src/middlewares/auth-provider/keycloak.ts index de8ed42..05d856f 100644 --- a/src/middlewares/auth-provider/keycloak.ts +++ b/src/middlewares/auth-provider/keycloak.ts @@ -16,11 +16,7 @@ const jwtVerify = createVerifier({ const jwtDecode = createDecoder(); -export async function keycloakAuth( - request: Express.Request, - _securityName?: string, - _scopes?: string[], -) { +export async function keycloakAuth(request: Express.Request, roles?: string[]) { const token = request.headers["authorization"]?.includes("Bearer ") ? request.headers["authorization"].split(" ")[1] : request.headers["authorization"]; @@ -49,6 +45,12 @@ export async function keycloakAuth( } } + if (Array.isArray(roles) && roles.length > 0 && Array.isArray(payload.roles)) { + if (!roles.some((a: string) => payload.roles.includes(a))) { + throw new HttpError(HttpStatus.FORBIDDEN, "คุณไม่มีสิทธิในการเข้าถึงข้อมูลดังกล่าว"); + } + } + return payload; } diff --git a/src/middlewares/auth.ts b/src/middlewares/auth.ts index 75f3773..ad0bb1e 100644 --- a/src/middlewares/auth.ts +++ b/src/middlewares/auth.ts @@ -6,11 +6,11 @@ import { keycloakAuth } from "./auth-provider/keycloak"; export async function expressAuthentication( request: Express.Request, securityName: string, - _scopes?: string[], + scopes?: string[], ) { switch (securityName) { case "keycloak": - return keycloakAuth(request); + return keycloakAuth(request, scopes); default: throw new HttpError(HttpStatus.NOT_IMPLEMENTED, "ไม่ทราบวิธียืนยันตัวตน"); } From 32127ae804eee1b1423a4ede8ea279b9588c47b0 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 10 Apr 2024 12:49:41 +0700 Subject: [PATCH 118/161] refactor: change condition order --- src/middlewares/auth-provider/keycloak.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middlewares/auth-provider/keycloak.ts b/src/middlewares/auth-provider/keycloak.ts index 05d856f..5201c36 100644 --- a/src/middlewares/auth-provider/keycloak.ts +++ b/src/middlewares/auth-provider/keycloak.ts @@ -45,7 +45,7 @@ export async function keycloakAuth(request: Express.Request, roles?: string[]) { } } - if (Array.isArray(roles) && roles.length > 0 && Array.isArray(payload.roles)) { + if (Array.isArray(payload.roles) && Array.isArray(roles) && roles.length > 0) { if (!roles.some((a: string) => payload.roles.includes(a))) { throw new HttpError(HttpStatus.FORBIDDEN, "คุณไม่มีสิทธิในการเข้าถึงข้อมูลดังกล่าว"); } From 7e6325d3598f00d7fd8808c906c8fdaaec7c2be0 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 10 Apr 2024 13:37:33 +0700 Subject: [PATCH 119/161] feat: delete user --- src/controllers/keycloak-controller.ts | 6 ++++++ src/services/keycloak.ts | 25 ++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/controllers/keycloak-controller.ts b/src/controllers/keycloak-controller.ts index ffc8ec9..5ebb09f 100644 --- a/src/controllers/keycloak-controller.ts +++ b/src/controllers/keycloak-controller.ts @@ -2,6 +2,7 @@ import { Body, Controller, Delete, Get, Path, Post, Put, Route, Security, Tags } import { addUserRoles, createUser, + deleteUser, editUser, getRoles, removeUserRoles, @@ -30,6 +31,11 @@ export class KeycloakController extends Controller { return await editUser(userId, body); } + @Delete("user/{userId}") + async deleteUser(@Path() userId: string) { + return await deleteUser(userId); + } + @Get("role") async getRole() { const role = await getRoles(); diff --git a/src/services/keycloak.ts b/src/services/keycloak.ts index 15089e1..a9c52bc 100644 --- a/src/services/keycloak.ts +++ b/src/services/keycloak.ts @@ -92,7 +92,7 @@ export async function createUser(username: string, password: string, opts?: Reco } /** - * Update keycloak user by given username and password with roles + * Update keycloak user by uuid * * Client must have permission to manage realm's user * @@ -125,6 +125,29 @@ export async function editUser(userId: string, opts: Record) { return id || true; } +/** + * Delete keycloak user by uuid + * + * Client must have permission to manage realm's user + * + * @returns user uuid or true if success, false otherwise. + */ +export async function deleteUser(userId: string) { + const res = await fetch(`${KC_URL}/admin/realms/${KC_REALM}/users/${userId}`, { + // prettier-ignore + headers: { + "authorization": `Bearer ${await getToken()}`, + "content-type": `application/json`, + }, + method: "DELETE", + }).catch((e) => console.log("Keycloak Error: ", e)); + + if (!res) return false; + if (!res.ok) { + return Boolean(console.error("Keycloak Error Response: ", await res.json())); + } +} + /** * Get roles list or specific role data * From ae42cb2f761e8bc19b9f1cafb40ed9d5ce170f73 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 10 Apr 2024 15:11:13 +0700 Subject: [PATCH 120/161] feat: add query param to include user branch --- src/controllers/user-controller.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/controllers/user-controller.ts b/src/controllers/user-controller.ts index f1b5f85..e0d3956 100644 --- a/src/controllers/user-controller.ts +++ b/src/controllers/user-controller.ts @@ -133,6 +133,7 @@ export class UserController extends Controller { async getUser( @Query() userType?: UserType, @Query() zipCode?: string, + @Query() includeBranch: boolean = false, @Query() query: string = "", @Query() page: number = 1, @Query() pageSize: number = 30, @@ -155,6 +156,7 @@ export class UserController extends Controller { province: true, district: true, subDistrict: true, + branch: { include: { branch: includeBranch } }, }, where, take: pageSize, @@ -167,6 +169,7 @@ export class UserController extends Controller { result: await Promise.all( result.map(async (v) => ({ ...v, + branch: includeBranch ? v.branch.map((a) => a.branch) : undefined, profileImageUrl: await minio.presignedGetObject( MINIO_BUCKET, imageLocation(v.id), From c59e0f537a69f627368135504399b8ea89463769 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 10 Apr 2024 16:21:56 +0700 Subject: [PATCH 121/161] feat: detect self reference --- src/controllers/branch-controller.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/controllers/branch-controller.ts b/src/controllers/branch-controller.ts index df5f785..49edaa3 100644 --- a/src/controllers/branch-controller.ts +++ b/src/controllers/branch-controller.ts @@ -238,6 +238,13 @@ export class BranchController extends Controller { @Body() body: BranchUpdate, @Path() branchId: string, ) { + if (body.headOfficeId === branchId) + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Cannot make this as head office and branch at the same time.", + "missing_or_invalid_parameter", + ); + if (body.subDistrictId || body.districtId || body.provinceId || body.headOfficeId) { const [province, district, subDistrict, branch] = await prisma.$transaction([ prisma.province.findFirst({ where: { id: body.provinceId || undefined } }), From ff5b3ee6403f5c69ab75ae3e4cf05197aba439a4 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 10 Apr 2024 16:37:14 +0700 Subject: [PATCH 122/161] feat: emplyee checkup & other info endpoints --- .../employee-checkup-controller.ts | 183 ++++++++++++++++++ .../employee-other-info-controller.ts | 127 ++++++++++++ 2 files changed, 310 insertions(+) create mode 100644 src/controllers/employee-checkup-controller.ts create mode 100644 src/controllers/employee-other-info-controller.ts diff --git a/src/controllers/employee-checkup-controller.ts b/src/controllers/employee-checkup-controller.ts new file mode 100644 index 0000000..de683d8 --- /dev/null +++ b/src/controllers/employee-checkup-controller.ts @@ -0,0 +1,183 @@ +import { + Body, + Controller, + Delete, + Get, + Path, + Post, + Put, + Request, + Route, + Security, + Tags, +} from "tsoa"; +import { RequestWithUser } from "../interfaces/user"; +import prisma from "../db"; +import HttpStatus from "../interfaces/http-status"; +import HttpError from "../interfaces/http-error"; + +type EmployeeCheckupCreate = { + checkupType: string; + checkupResult: string; + + provinceId?: string | null; + + hospitalName: string; + remark: string; + medicalBenefitScheme: string; + insuranceCompany: string; + coverageStartDate: Date; + coverageExpireDate: Date; +}; + +type EmployeeCheckupEdit = { + checkupType?: string; + checkupResult?: string; + + provinceId?: string | null; + + hospitalName?: string; + remark?: string; + medicalBenefitScheme?: string; + insuranceCompany?: string; + coverageStartDate?: Date; + coverageExpireDate?: Date; +}; + +@Route("api/employee/{employeeId}/checkup") +@Tags("Employee Checkup") +@Security("keycloak") +export class EmployeeCheckupController extends Controller { + @Get() + async list(@Path() employeeId: string) { + return prisma.employeeCheckup.findMany({ + orderBy: { createdAt: "asc" }, + where: { employeeId }, + }); + } + + @Get("{checkupId}") + async getById(@Path() employeeId: string, @Path() checkupId: string) { + const record = await prisma.employeeCheckup.findFirst({ + where: { id: checkupId, employeeId }, + }); + if (!record) { + throw new HttpError( + HttpStatus.NOT_FOUND, + "Employee checkup cannot be found.", + "data_not_found", + ); + } + return record; + } + + @Post() + async create( + @Request() req: RequestWithUser, + @Path() employeeId: string, + @Body() body: EmployeeCheckupCreate, + ) { + if (body.provinceId || employeeId) { + const [province, employee] = await prisma.$transaction([ + prisma.province.findFirst({ where: { id: body.provinceId || undefined } }), + prisma.employee.findFirst({ where: { id: employeeId } }), + ]); + if (body.provinceId && !province) + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Province cannot be found.", + "missing_or_invalid_parameter", + ); + if (!employee) + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Employee cannot be found.", + "missing_or_invalid_parameter", + ); + } + + const { provinceId, ...rest } = body; + + const record = await prisma.employeeCheckup.create({ + include: { province: true }, + data: { + ...rest, + province: { connect: provinceId ? { id: provinceId } : undefined }, + employee: { connect: { id: employeeId } }, + createdBy: req.user.name, + updateBy: req.user.name, + }, + }); + + this.setStatus(HttpStatus.CREATED); + + return record; + } + + @Put("{checkupId}") + async editById( + @Request() req: RequestWithUser, + @Path() employeeId: string, + @Path() checkupId: string, + @Body() body: EmployeeCheckupEdit, + ) { + if (body.provinceId || employeeId) { + const [province, employee] = await prisma.$transaction([ + prisma.province.findFirst({ where: { id: body.provinceId || undefined } }), + prisma.employee.findFirst({ where: { id: employeeId } }), + ]); + if (body.provinceId && !province) + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Province cannot be found.", + "missing_or_invalid_parameter", + ); + if (!employee) + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Employee cannot be found.", + "missing_or_invalid_parameter", + ); + } + + const { provinceId, ...rest } = body; + + if (!(await prisma.employeeCheckup.findUnique({ where: { id: checkupId, employeeId } }))) { + throw new HttpError( + HttpStatus.NOT_FOUND, + "Employee checkup cannot be found.", + "data_not_found", + ); + } + + const record = await prisma.employeeCheckup.update({ + include: { province: true }, + where: { id: checkupId, employeeId }, + data: { + ...rest, + province: { connect: provinceId ? { id: provinceId } : undefined }, + createdBy: req.user.name, + updateBy: req.user.name, + }, + }); + + this.setStatus(HttpStatus.CREATED); + + return record; + } + + @Delete("{checkupId}") + async deleteById(@Path() employeeId: string, @Path() checkupId: string) { + const record = await prisma.employeeCheckup.findFirst({ where: { id: checkupId, employeeId } }); + + if (!record) { + throw new HttpError( + HttpStatus.NOT_FOUND, + "Employee checkup cannot be found.", + "data_not_found", + ); + } + + return await prisma.employeeCheckup.delete({ where: { id: checkupId, employeeId } }); + } +} diff --git a/src/controllers/employee-other-info-controller.ts b/src/controllers/employee-other-info-controller.ts new file mode 100644 index 0000000..4445c17 --- /dev/null +++ b/src/controllers/employee-other-info-controller.ts @@ -0,0 +1,127 @@ +import { Prisma, Status } from "@prisma/client"; +import { + Body, + Controller, + Delete, + Get, + Put, + Path, + Post, + Query, + Request, + Route, + Security, + Tags, +} from "tsoa"; + +import prisma from "../db"; +import HttpError from "../interfaces/http-error"; +import HttpStatus from "../interfaces/http-status"; +import { RequestWithUser } from "../interfaces/user"; + +type EmployeeOtherInfoCreate = { + citizenId: string; + fatherFullName: string; + motherFullName: string; + birthPlace: string; +}; + +type EmployeeOtherInfoUpdate = { + citizenId: string; + fatherFullName: string; + motherFullName: string; + birthPlace: string; +}; + +@Route("api/employee/{employeeId}/other-info") +@Tags("Employee Other Info") +@Security("keycloak") +export class EmployeeOtherInfo extends Controller { + @Get() + async list(@Path() employeeId: string) { + return prisma.employeeOtherInfo.findMany({ + orderBy: { createdAt: "asc" }, + where: { employeeId }, + }); + } + + @Get("{otherInfoId}") + async getById(@Path() employeeId: string, @Path() otherInfoId: string) { + const record = await prisma.employeeOtherInfo.findFirst({ + where: { id: otherInfoId, employeeId }, + }); + if (!record) { + throw new HttpError(HttpStatus.NOT_FOUND, "Employee info cannot be found.", "data_not_found"); + } + return record; + } + + @Post() + async create( + @Request() req: RequestWithUser, + @Path() employeeId: string, + @Body() body: EmployeeOtherInfoCreate, + ) { + if (!(await prisma.employee.findUnique({ where: { id: employeeId } }))) + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Employee cannot be found.", + "missing_or_invalid_parameter", + ); + + const record = await prisma.employeeOtherInfo.create({ + data: { + ...body, + employee: { connect: { id: employeeId } }, + createdBy: req.user.name, + updateBy: req.user.name, + }, + }); + + this.setStatus(HttpStatus.CREATED); + + return record; + } + + @Put("{otherInfoId}") + async editById( + @Request() req: RequestWithUser, + @Path() employeeId: string, + @Path() otherInfoId: string, + @Body() body: EmployeeOtherInfoUpdate, + ) { + if (!(await prisma.employeeOtherInfo.findUnique({ where: { id: otherInfoId, employeeId } }))) { + throw new HttpError( + HttpStatus.NOT_FOUND, + "Employee other info cannot be found.", + "data_not_found", + ); + } + + const record = await prisma.employeeOtherInfo.update({ + where: { id: otherInfoId, employeeId }, + data: { ...body, createdBy: req.user.name, updateBy: req.user.name }, + }); + + this.setStatus(HttpStatus.CREATED); + + return record; + } + + @Delete("{otherInfoId}") + async deleteById(@Path() employeeId: string, @Path() otherInfoId: string) { + const record = await prisma.employeeOtherInfo.findFirst({ + where: { id: otherInfoId, employeeId }, + }); + + if (!record) { + throw new HttpError( + HttpStatus.NOT_FOUND, + "Employee other info cannot be found.", + "data_not_found", + ); + } + + return await prisma.employeeOtherInfo.delete({ where: { id: otherInfoId, employeeId } }); + } +} From ed2c7daabbd687de0fe612ceb7cf6fdcc728af99 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 10 Apr 2024 17:27:00 +0700 Subject: [PATCH 123/161] feat: get branch stats by hq or br --- src/controllers/branch-controller.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/controllers/branch-controller.ts b/src/controllers/branch-controller.ts index 49edaa3..a1463cc 100644 --- a/src/controllers/branch-controller.ts +++ b/src/controllers/branch-controller.ts @@ -61,6 +61,22 @@ type BranchUpdate = { @Tags("Branch") @Security("keycloak") export class BranchController extends Controller { + @Get("stats") + async getStats() { + const list = await prisma.branch.groupBy({ + _count: true, + by: "isHeadOffice", + }); + + return list.reduce>( + (a, c) => { + a[c.isHeadOffice ? "hq" : "br"] = c._count; + return a; + }, + { hq: 0, br: 0 }, + ); + } + @Get("user-stats") async getUserStat(@Query() userType?: UserType) { const list = await prisma.branchUser.groupBy({ From 30acdfdd32bf17559d364f40430c45457e40e22c Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 11 Apr 2024 14:28:44 +0700 Subject: [PATCH 124/161] feat: list by headOfficeId & get with contact --- src/controllers/branch-controller.ts | 35 ++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/src/controllers/branch-controller.ts b/src/controllers/branch-controller.ts index a1463cc..ba0743b 100644 --- a/src/controllers/branch-controller.ts +++ b/src/controllers/branch-controller.ts @@ -18,6 +18,13 @@ import prisma from "../db"; import HttpError from "../interfaces/http-error"; import HttpStatus from "../interfaces/http-status"; import { RequestWithUser } from "../interfaces/user"; +import minio from "../services/minio"; + +if (!process.env.MINIO_BUCKET) { + throw Error("Require MinIO bucket."); +} + +const MINIO_BUCKET = process.env.MINIO_BUCKET; type BranchCreate = { status?: Status; @@ -104,6 +111,7 @@ export class BranchController extends Controller { async getBranch( @Query() zipCode?: string, @Query() filter?: "head" | "sub", + @Query() headOfficeId?: string, @Query() tree?: boolean, @Query() query: string = "", @Query() page: number = 1, @@ -111,8 +119,8 @@ export class BranchController extends Controller { ) { const where = { AND: { - headOfficeId: filter === "head" || tree ? null : undefined, - NOT: { headOfficeId: filter === "sub" ? null : undefined }, + headOfficeId: headOfficeId ?? (filter === "head" || tree ? null : undefined), + NOT: { headOfficeId: filter === "sub" && !headOfficeId ? null : undefined }, }, OR: [ { nameEN: { contains: query }, zipCode }, @@ -147,7 +155,11 @@ export class BranchController extends Controller { } @Get("{branchId}") - async getBranchById(@Path() branchId: string, @Query() includeSubBranch?: boolean) { + async getBranchById( + @Path() branchId: string, + @Query() includeSubBranch?: boolean, + @Query() includeContact?: boolean, + ) { const record = await prisma.branch.findFirst({ include: { province: true, @@ -160,6 +172,7 @@ export class BranchController extends Controller { subDistrict: true, }, }, + contact: includeContact, }, where: { id: branchId }, }); @@ -168,7 +181,21 @@ export class BranchController extends Controller { throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found"); } - return record; + return { + ...record, + contact: record.contact + ? await Promise.all( + record.contact.map(async (v) => + Object.assign(v, { + qrCodeImageUrl: await minio.presignedGetObject( + MINIO_BUCKET, + `branch/contact-${record.id}`, + ), + }), + ), + ) + : undefined, + }; } @Post() From 849a7d0078306be7ff42244780f5369eff3c97b5 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 11 Apr 2024 17:08:22 +0700 Subject: [PATCH 125/161] fix: auto gen user code --- src/controllers/branch-user-controller.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/controllers/branch-user-controller.ts b/src/controllers/branch-user-controller.ts index c96e5a2..19ffcd9 100644 --- a/src/controllers/branch-user-controller.ts +++ b/src/controllers/branch-user-controller.ts @@ -126,16 +126,20 @@ export class BranchUserController extends Controller { for (const g of Object.values(UserType)) { if (group[g].length === 0) continue; - const last = await prisma.user.findFirst({ + const last = await prisma.branchUser.findFirst({ orderBy: { createdAt: "desc" }, + include: { user: true }, where: { - userType: g, - code: { startsWith: `${branch.code.slice(2, 5).padEnd(3, "0")}` }, + branchId, + user: { + userType: g, + code: { startsWith: `${branch.code.slice(4).padEnd(3, "0")}` }, + }, }, }); const code = (idx: number) => - `${branch.code.slice(4).padEnd(3, "0")}${g !== "USER" ? g.charAt(0) : ""}${(+(last?.code?.slice(-4) || 0) + idx + 1).toString().padStart(4, "0")}`; + `${branch.code.slice(4).padEnd(3, "0")}${g !== "USER" ? g.charAt(0) : ""}${(+(last?.user.code?.slice(-4) || 0) + idx + 1).toString().padStart(4, "0")}`; await prisma.$transaction( group[g].map((v, i) => From 6c54796d9dc516bc1e799695709c50ee756f08d0 Mon Sep 17 00:00:00 2001 From: Methapon-Frappet Date: Sat, 13 Apr 2024 20:27:43 +0700 Subject: [PATCH 126/161] refactor: add head office indicator to stats --- src/controllers/branch-controller.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/controllers/branch-controller.ts b/src/controllers/branch-controller.ts index ba0743b..d755ea8 100644 --- a/src/controllers/branch-controller.ts +++ b/src/controllers/branch-controller.ts @@ -97,6 +97,7 @@ export class BranchController extends Controller { id: true, nameEN: true, name: true, + isHeadOffice: true, }, }); From ee1569e5294f8d81b254260a22cea0310cc9505a Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 17 Apr 2024 11:21:57 +0700 Subject: [PATCH 127/161] refactor: add user to keycloak on create user --- src/controllers/user-controller.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/controllers/user-controller.ts b/src/controllers/user-controller.ts index e0d3956..99e5e96 100644 --- a/src/controllers/user-controller.ts +++ b/src/controllers/user-controller.ts @@ -19,6 +19,7 @@ import minio from "../services/minio"; import { RequestWithUser } from "../interfaces/user"; import HttpError from "../interfaces/http-error"; import HttpStatus from "../interfaces/http-status"; +import { createUser } from "../services/keycloak"; if (!process.env.MINIO_BUCKET) { throw Error("Require MinIO bucket."); @@ -29,11 +30,11 @@ const MINIO_BUCKET = process.env.MINIO_BUCKET; type UserCreate = { status?: Status; - keycloakId: string; - userType: UserType; userRole: string; + username: string; + firstName: string; firstNameEN: string; lastName: string; @@ -237,11 +238,22 @@ export class UserController extends Controller { } } - const { provinceId, districtId, subDistrictId, ...rest } = body; + const { provinceId, districtId, subDistrictId, username, ...rest } = body; + + const result = await createUser(username, username, { + firstName: body.firstName, + lastName: body.lastName, + requiredActions: ["UPDATE_PASSWORD"], + }); + + if (!result || typeof result !== "string") { + throw new Error("Cannot create user with keycloak service."); + } const record = await prisma.user.create({ include: { province: true, district: true, subDistrict: true }, data: { + id: result, ...rest, province: { connect: provinceId ? { id: provinceId } : undefined }, district: { connect: districtId ? { id: districtId } : undefined }, From ddb2155c44c22968bb3c3f3d7eadf0e548d50579 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 17 Apr 2024 11:22:12 +0700 Subject: [PATCH 128/161] chore: add migration --- .../20240417020614_remove_field/migration.sql | 8 ++++++++ .../20240417041541_move_field/migration.sql | 11 +++++++++++ prisma/schema.prisma | 4 +--- 3 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 prisma/migrations/20240417020614_remove_field/migration.sql create mode 100644 prisma/migrations/20240417041541_move_field/migration.sql diff --git a/prisma/migrations/20240417020614_remove_field/migration.sql b/prisma/migrations/20240417020614_remove_field/migration.sql new file mode 100644 index 0000000..3cb801f --- /dev/null +++ b/prisma/migrations/20240417020614_remove_field/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - You are about to drop the column `keycloakId` on the `User` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "User" DROP COLUMN "keycloakId"; diff --git a/prisma/migrations/20240417041541_move_field/migration.sql b/prisma/migrations/20240417041541_move_field/migration.sql new file mode 100644 index 0000000..3b720dc --- /dev/null +++ b/prisma/migrations/20240417041541_move_field/migration.sql @@ -0,0 +1,11 @@ +/* + Warnings: + + - You are about to drop the column `lineId` on the `BranchContact` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Branch" ADD COLUMN "lineId" TEXT; + +-- AlterTable +ALTER TABLE "BranchContact" DROP COLUMN "lineId"; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 1829f9b..98cbe89 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -93,6 +93,7 @@ model Branch { email String telephoneNo String + lineId String? latitude String longitude String @@ -117,7 +118,6 @@ model Branch { model BranchContact { id String @id @default(uuid()) telephoneNo String - lineId String branch Branch @relation(fields: [branchId], references: [id], onDelete: Cascade) branchId String @@ -153,8 +153,6 @@ enum UserType { model User { id String @id @default(uuid()) - keycloakId String - code String? firstName String firstNameEN String From 155bb19b9be6637d552065f5e00067169ac578d0 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 17 Apr 2024 11:23:20 +0700 Subject: [PATCH 129/161] refactor: move field lineId contact field --- src/controllers/branch-controller.ts | 36 ++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/src/controllers/branch-controller.ts b/src/controllers/branch-controller.ts index d755ea8..97196d1 100644 --- a/src/controllers/branch-controller.ts +++ b/src/controllers/branch-controller.ts @@ -36,6 +36,7 @@ type BranchCreate = { zipCode: string; email: string; telephoneNo: string; + lineId: string; longitude: string; latitude: string; @@ -55,6 +56,7 @@ type BranchUpdate = { zipCode?: string; email?: string; telephoneNo?: string; + lineId?: string; longitude?: string; latitude?: string; @@ -64,6 +66,10 @@ type BranchUpdate = { headOfficeId?: string | null; }; +function lineImageLoc(id: string) { + return `branch/line-qr-${id}`; +} + @Route("api/branch") @Tags("Branch") @Security("keycloak") @@ -273,7 +279,18 @@ export class BranchController extends Controller { this.setStatus(HttpStatus.CREATED); - return record; + return Object.assign(record, { + qrCodeImageUrl: await minio.presignedGetObject( + MINIO_BUCKET, + lineImageLoc(record.id), + 12 * 60 * 60, + ), + qrCodeImageUploadUrl: await minio.presignedPutObject( + MINIO_BUCKET, + lineImageLoc(record.id), + 12 * 60 * 60, + ), + }); } @Put("{branchId}") @@ -354,7 +371,18 @@ export class BranchController extends Controller { where: { id: branchId }, }); - return record; + return Object.assign(record, { + qrCodeImageUrl: await minio.presignedGetObject( + MINIO_BUCKET, + lineImageLoc(record.id), + 12 * 60 * 60, + ), + qrCodeImageUploadUrl: await minio.presignedPutObject( + MINIO_BUCKET, + lineImageLoc(record.id), + 12 * 60 * 60, + ), + }); } @Delete("{branchId}") @@ -376,6 +404,10 @@ export class BranchController extends Controller { throw new HttpError(HttpStatus.FORBIDDEN, "Branch is in used.", "data_in_used"); } + await minio.removeObject(MINIO_BUCKET, lineImageLoc(branchId), { + forceDelete: true, + }); + return await prisma.branch.delete({ include: { province: true, From 6b6f9e9de077a0e051add4fa55e140f6501bc9a8 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 17 Apr 2024 11:40:23 +0700 Subject: [PATCH 130/161] refactor: move field --- src/controllers/branch-contact-controller.ts | 50 ++------------------ 1 file changed, 4 insertions(+), 46 deletions(-) diff --git a/src/controllers/branch-contact-controller.ts b/src/controllers/branch-contact-controller.ts index 1f8a473..450ca55 100644 --- a/src/controllers/branch-contact-controller.ts +++ b/src/controllers/branch-contact-controller.ts @@ -26,19 +26,13 @@ if (!process.env.MINIO_BUCKET) { const MINIO_BUCKET = process.env.MINIO_BUCKET; type BranchContactCreate = { - lineId: string; telephoneNo: string; }; type BranchContactUpdate = { - lineId?: string; telephoneNo?: string; }; -function imageLocation(id: string) { - return `branch/contact-${id}`; -} - @Route("api/branch/{branchId}/contact") @Tags("Branch Contact") @Security("keycloak") @@ -60,16 +54,7 @@ export class BranchContactController extends Controller { ]); return { - result: await Promise.all( - result.map(async (v) => ({ - ...v, - qrCodeImageUrl: await minio.presignedGetObject( - MINIO_BUCKET, - imageLocation(v.id), - 12 * 60 * 60, - ), - })), - ), + result, page, pageSize, total, @@ -88,9 +73,7 @@ export class BranchContactController extends Controller { ); } - return Object.assign(record, { - qrCodeImageUrl: await minio.presignedGetObject(MINIO_BUCKET, imageLocation(record.id)), - }); + return record; } @Post() @@ -112,18 +95,7 @@ export class BranchContactController extends Controller { this.setStatus(HttpStatus.CREATED); - return Object.assign(record, { - qrCodeImageUrl: await minio.presignedGetObject( - MINIO_BUCKET, - imageLocation(record.id), - 12 * 60 * 60, - ), - qrCodeImageUploadUrl: await minio.presignedPutObject( - MINIO_BUCKET, - imageLocation(record.id), - 12 * 60 * 60, - ), - }); + return record; } @Put("{contactId}") @@ -150,18 +122,7 @@ export class BranchContactController extends Controller { where: { id: contactId, branchId }, }); - return Object.assign(record, { - qrCodeImageUrl: await minio.presignedGetObject( - MINIO_BUCKET, - imageLocation(record.id), - 12 * 60 * 60, - ), - qrCodeImageUploadUrl: await minio.presignedPutObject( - MINIO_BUCKET, - imageLocation(record.id), - 12 * 60 * 60, - ), - }); + return record; } @Delete("{contactId}") @@ -174,8 +135,5 @@ export class BranchContactController extends Controller { "data_not_found", ); } - await minio.removeObject(MINIO_BUCKET, imageLocation(contactId), { - forceDelete: true, - }); } } From 194054422c990076e60992624bb9464e03cd1a0d Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 17 Apr 2024 11:55:47 +0700 Subject: [PATCH 131/161] chore: cleanup --- src/controllers/user-controller.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/controllers/user-controller.ts b/src/controllers/user-controller.ts index 99e5e96..e51a566 100644 --- a/src/controllers/user-controller.ts +++ b/src/controllers/user-controller.ts @@ -334,8 +334,6 @@ export class UserController extends Controller { }, })); - console.log(lastUserOfType); - const record = await prisma.user.update({ include: { province: true, district: true, subDistrict: true }, data: { From 326f7692c08aadf3e6f2a2c6987b5220eb342cef Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 17 Apr 2024 13:05:36 +0700 Subject: [PATCH 132/161] fix: also delete user in keycloak when delete user --- src/controllers/user-controller.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/controllers/user-controller.ts b/src/controllers/user-controller.ts index e51a566..52b275a 100644 --- a/src/controllers/user-controller.ts +++ b/src/controllers/user-controller.ts @@ -19,7 +19,7 @@ import minio from "../services/minio"; import { RequestWithUser } from "../interfaces/user"; import HttpError from "../interfaces/http-error"; import HttpStatus from "../interfaces/http-status"; -import { createUser } from "../services/keycloak"; +import { createUser, deleteUser } from "../services/keycloak"; if (!process.env.MINIO_BUCKET) { throw Error("Require MinIO bucket."); @@ -412,6 +412,8 @@ export class UserController extends Controller { }); }); + await deleteUser(userId); + return await prisma.user.delete({ include: { province: true, From eaa41f49be672f34fcc9400a091ca9e612ad378d Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 17 Apr 2024 13:42:01 +0700 Subject: [PATCH 133/161] chore: update db --- .../20240417063829_update_table/migration.sql | 119 ++++++++++++++++++ .../migration.sql | 2 + prisma/schema.prisma | 111 +++++++++++++++- 3 files changed, 230 insertions(+), 2 deletions(-) create mode 100644 prisma/migrations/20240417063829_update_table/migration.sql create mode 100644 prisma/migrations/20240417064127_add_missing_user_field/migration.sql diff --git a/prisma/migrations/20240417063829_update_table/migration.sql b/prisma/migrations/20240417063829_update_table/migration.sql new file mode 100644 index 0000000..8b7bd17 --- /dev/null +++ b/prisma/migrations/20240417063829_update_table/migration.sql @@ -0,0 +1,119 @@ +/* + Warnings: + + - You are about to drop the column `telephoneNo` on the `Branch` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Branch" DROP COLUMN "telephoneNo"; + +-- CreateTable +CREATE TABLE "Menu" ( + "id" TEXT NOT NULL, + "caption" TEXT NOT NULL, + "captionEN" TEXT NOT NULL, + "menuType" TEXT NOT NULL, + "url" TEXT NOT NULL, + "createdBy" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updateBy" TEXT, + "updatedAt" TIMESTAMP(3) NOT NULL, + "parentId" TEXT, + + CONSTRAINT "Menu_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "RoleMenuPermission" ( + "id" TEXT NOT NULL, + "userRole" TEXT NOT NULL, + "permission" TEXT NOT NULL, + "menuId" TEXT NOT NULL, + "createdBy" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updateBy" TEXT, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "RoleMenuPermission_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "UserMenuPermission" ( + "id" TEXT NOT NULL, + "permission" TEXT NOT NULL, + "menuId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "createdBy" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updateBy" TEXT, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "UserMenuPermission_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "MenuComponent" ( + "id" TEXT NOT NULL, + "componentId" TEXT NOT NULL, + "componentTag" TEXT NOT NULL, + "menuId" TEXT NOT NULL, + "createdBy" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updateBy" TEXT, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "MenuComponent_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "RoleMenuComponentPermission" ( + "id" TEXT NOT NULL, + "componentId" TEXT NOT NULL, + "componentTag" TEXT NOT NULL, + "menuComponentId" TEXT NOT NULL, + "permission" TEXT NOT NULL, + "createdBy" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updateBy" TEXT, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "RoleMenuComponentPermission_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "UserMenuComponentPermission" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "menuComponentId" TEXT NOT NULL, + "permission" TEXT NOT NULL, + "createdBy" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updateBy" TEXT, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "UserMenuComponentPermission_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "Menu" ADD CONSTRAINT "Menu_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "Menu"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "RoleMenuPermission" ADD CONSTRAINT "RoleMenuPermission_menuId_fkey" FOREIGN KEY ("menuId") REFERENCES "Menu"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserMenuPermission" ADD CONSTRAINT "UserMenuPermission_menuId_fkey" FOREIGN KEY ("menuId") REFERENCES "Menu"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserMenuPermission" ADD CONSTRAINT "UserMenuPermission_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MenuComponent" ADD CONSTRAINT "MenuComponent_menuId_fkey" FOREIGN KEY ("menuId") REFERENCES "Menu"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "RoleMenuComponentPermission" ADD CONSTRAINT "RoleMenuComponentPermission_menuComponentId_fkey" FOREIGN KEY ("menuComponentId") REFERENCES "MenuComponent"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserMenuComponentPermission" ADD CONSTRAINT "UserMenuComponentPermission_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserMenuComponentPermission" ADD CONSTRAINT "UserMenuComponentPermission_menuComponentId_fkey" FOREIGN KEY ("menuComponentId") REFERENCES "MenuComponent"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20240417064127_add_missing_user_field/migration.sql b/prisma/migrations/20240417064127_add_missing_user_field/migration.sql new file mode 100644 index 0000000..b0b8442 --- /dev/null +++ b/prisma/migrations/20240417064127_add_missing_user_field/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Branch" ADD COLUMN "contactName" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 98cbe89..164c6fd 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -7,6 +7,111 @@ datasource db { url = env("DATABASE_URL") } +model Menu { + id String @id @default(uuid()) + + caption String + captionEN String + menuType String + url String + + createdBy String? + createdAt DateTime @default(now()) + updateBy String? + updatedAt DateTime @updatedAt + + parent Menu? @relation(name: "MenuRelation", fields: [parentId], references: [id]) + parentId String? + + children Menu[] @relation(name: "MenuRelation") + roleMenuPermission RoleMenuPermission[] + userMenuPermission UserMenuPermission[] + userComponent MenuComponent[] +} + +model RoleMenuPermission { + id String @id @default(uuid()) + + userRole String + permission String + + menu Menu @relation(fields: [menuId], references: [id]) + menuId String + + createdBy String? + createdAt DateTime @default(now()) + updateBy String? + updatedAt DateTime @updatedAt +} + +model UserMenuPermission { + id String @id @default(uuid()) + + permission String + + menu Menu @relation(fields: [menuId], references: [id]) + menuId String + + user User @relation(fields: [userId], references: [id]) + userId String + + createdBy String? + createdAt DateTime @default(now()) + updateBy String? + updatedAt DateTime @updatedAt +} + +model MenuComponent { + id String @id @default(uuid()) + + componentId String + componentTag String + + menu Menu @relation(fields: [menuId], references: [id]) + menuId String + + createdBy String? + createdAt DateTime @default(now()) + updateBy String? + updatedAt DateTime @updatedAt + roleMenuComponentPermission RoleMenuComponentPermission[] + userMennuComponentPermission UserMenuComponentPermission[] +} + +model RoleMenuComponentPermission { + id String @id @default(uuid()) + + componentId String + componentTag String + + menuComponent MenuComponent @relation(fields: [menuComponentId], references: [id]) + menuComponentId String + + permission String + + createdBy String? + createdAt DateTime @default(now()) + updateBy String? + updatedAt DateTime @updatedAt +} + +model UserMenuComponentPermission { + id String @id @default(uuid()) + + userId String + user User @relation(fields: [userId], references: [id]) + + menuComponent MenuComponent @relation(fields: [menuComponentId], references: [id]) + menuComponentId String + + permission String + + createdBy String? + createdAt DateTime @default(now()) + updateBy String? + updatedAt DateTime @updatedAt +} + model Province { id String @id @default(uuid()) name String @@ -92,7 +197,7 @@ model Branch { zipCode String email String - telephoneNo String + contactName String? lineId String? latitude String @@ -206,7 +311,9 @@ model User { updateBy String? updatedAt DateTime @updatedAt - branch BranchUser[] + branch BranchUser[] + userMenuPermission UserMenuPermission[] + userMenuComponentPermission UserMenuComponentPermission[] } enum CustomerType { From 63293b671b3d8dd2c52e6386b7f60a2e3366efb0 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 17 Apr 2024 13:42:11 +0700 Subject: [PATCH 134/161] refactor: update branch endpoint --- src/controllers/branch-contact-controller.ts | 7 ------- src/controllers/branch-controller.ts | 18 +++++++++++++++--- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/controllers/branch-contact-controller.ts b/src/controllers/branch-contact-controller.ts index 450ca55..b71aea5 100644 --- a/src/controllers/branch-contact-controller.ts +++ b/src/controllers/branch-contact-controller.ts @@ -14,17 +14,10 @@ import { } from "tsoa"; import prisma from "../db"; -import minio from "../services/minio"; import HttpError from "../interfaces/http-error"; import HttpStatus from "../interfaces/http-status"; import { RequestWithUser } from "../interfaces/user"; -if (!process.env.MINIO_BUCKET) { - throw Error("Require MinIO bucket."); -} - -const MINIO_BUCKET = process.env.MINIO_BUCKET; - type BranchContactCreate = { telephoneNo: string; }; diff --git a/src/controllers/branch-controller.ts b/src/controllers/branch-controller.ts index 97196d1..552e5e4 100644 --- a/src/controllers/branch-controller.ts +++ b/src/controllers/branch-controller.ts @@ -35,7 +35,8 @@ type BranchCreate = { address: string; zipCode: string; email: string; - telephoneNo: string; + contactName: string; + telephoneNo: string | string[]; lineId: string; longitude: string; latitude: string; @@ -55,7 +56,8 @@ type BranchUpdate = { address?: string; zipCode?: string; email?: string; - telephoneNo?: string; + contactName?: string; + telephoneNo?: string | string[]; lineId?: string; longitude?: string; latitude?: string; @@ -238,7 +240,7 @@ export class BranchController extends Controller { "missing_or_invalid_parameter", ); - const { provinceId, districtId, subDistrictId, headOfficeId, ...rest } = body; + const { provinceId, districtId, subDistrictId, headOfficeId, telephoneNo, ...rest } = body; const year = new Date().getFullYear(); @@ -279,7 +281,17 @@ export class BranchController extends Controller { this.setStatus(HttpStatus.CREATED); + if (record && telephoneNo) { + await prisma.branchContact.createMany({ + data: + typeof telephoneNo === "string" + ? [{ telephoneNo, branchId: record.id }] + : telephoneNo.map((v) => ({ telephoneNo: v, branchId: record.id })), + }); + } + return Object.assign(record, { + contact: await prisma.branchContact.findMany({ where: { branchId: record.id } }), qrCodeImageUrl: await minio.presignedGetObject( MINIO_BUCKET, lineImageLoc(record.id), From f9eb48d74191aef98425e64c7d89273f638a06ab Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 17 Apr 2024 13:46:16 +0700 Subject: [PATCH 135/161] fix: update contact endpoint --- src/controllers/branch-controller.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/controllers/branch-controller.ts b/src/controllers/branch-controller.ts index 552e5e4..7febf1a 100644 --- a/src/controllers/branch-controller.ts +++ b/src/controllers/branch-controller.ts @@ -351,7 +351,7 @@ export class BranchController extends Controller { ); } - const { provinceId, districtId, subDistrictId, headOfficeId, ...rest } = body; + const { provinceId, districtId, subDistrictId, headOfficeId, telephoneNo, ...rest } = body; if (!(await prisma.branch.findUnique({ where: { id: branchId } }))) { throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found"); @@ -383,6 +383,16 @@ export class BranchController extends Controller { where: { id: branchId }, }); + if (record && telephoneNo) { + await prisma.branchContact.deleteMany({ where: { branchId } }); + await prisma.branchContact.createMany({ + data: + typeof telephoneNo === "string" + ? [{ telephoneNo, branchId }] + : telephoneNo.map((v) => ({ telephoneNo: v, branchId })), + }); + } + return Object.assign(record, { qrCodeImageUrl: await minio.presignedGetObject( MINIO_BUCKET, From 23f29ce0527563b4af70b04bdd941eda4932b216 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 17 Apr 2024 13:59:30 +0700 Subject: [PATCH 136/161] feat: also return with contact --- src/controllers/branch-controller.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/controllers/branch-controller.ts b/src/controllers/branch-controller.ts index 7febf1a..34257bd 100644 --- a/src/controllers/branch-controller.ts +++ b/src/controllers/branch-controller.ts @@ -145,6 +145,7 @@ export class BranchController extends Controller { province: true, district: true, subDistrict: true, + contact: true, branch: tree && { include: { province: true, From ec3203e1a8e0e65b3326d12cdd0ac24805c57afc Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 17 Apr 2024 16:21:53 +0700 Subject: [PATCH 137/161] refactor: add more field --- .../migration.sql | 3 +++ prisma/schema.prisma | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 prisma/migrations/20240417091605_add_missing_user_field_checkpoint/migration.sql diff --git a/prisma/migrations/20240417091605_add_missing_user_field_checkpoint/migration.sql b/prisma/migrations/20240417091605_add_missing_user_field_checkpoint/migration.sql new file mode 100644 index 0000000..30b641e --- /dev/null +++ b/prisma/migrations/20240417091605_add_missing_user_field_checkpoint/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "checkpoint" TEXT, +ADD COLUMN "checkpointEN" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 164c6fd..083924c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -287,6 +287,9 @@ model User { startDate DateTime? retireDate DateTime? + checkpoint String? + checkpointEN String? + userType UserType userRole String From 57f56671b57122f3cf6325da730c396b064b84a5 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 17 Apr 2024 16:22:07 +0700 Subject: [PATCH 138/161] feat: add field --- src/controllers/user-controller.ts | 32 ++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/controllers/user-controller.ts b/src/controllers/user-controller.ts index 52b275a..9640367 100644 --- a/src/controllers/user-controller.ts +++ b/src/controllers/user-controller.ts @@ -19,7 +19,7 @@ import minio from "../services/minio"; import { RequestWithUser } from "../interfaces/user"; import HttpError from "../interfaces/http-error"; import HttpStatus from "../interfaces/http-status"; -import { createUser, deleteUser } from "../services/keycloak"; +import { addUserRoles, createUser, deleteUser, getRoles } from "../services/keycloak"; if (!process.env.MINIO_BUCKET) { throw Error("Require MinIO bucket."); @@ -41,6 +41,8 @@ type UserCreate = { lastNameEN: string; gender: string; + checkpoint?: string | null; + checkpointEN?: string | null; registrationNo?: string | null; startDate?: Date | null; retireDate?: Date | null; @@ -77,6 +79,8 @@ type UserUpdate = { lastNameEN?: string; gender?: string; + checkpoint?: string | null; + checkpointEN?: string | null; registrationNo?: string | null; startDate?: Date | null; retireDate?: Date | null; @@ -240,21 +244,41 @@ export class UserController extends Controller { const { provinceId, districtId, subDistrictId, username, ...rest } = body; - const result = await createUser(username, username, { + let list = await getRoles(); + + if (!Array.isArray(list)) throw new Error("Failed. Cannot get role(s) data from the server."); + if (Array.isArray(list)) { + list = list.filter( + (a) => + !["uma_authorization", "offline_access", "default-roles"].some((b) => a.name.includes(b)), + ); + } + + const userId = await createUser(username, username, { firstName: body.firstName, lastName: body.lastName, requiredActions: ["UPDATE_PASSWORD"], }); - if (!result || typeof result !== "string") { + if (!userId || typeof userId !== "string") { throw new Error("Cannot create user with keycloak service."); } + const role = list.find((v) => v.id === body.userRole); + + const resultAddRole = role && (await addUserRoles(userId, [role])); + + if (!resultAddRole) { + await deleteUser(userId); + throw new Error("Failed. Cannot set user's role."); + } + const record = await prisma.user.create({ include: { province: true, district: true, subDistrict: true }, data: { - id: result, + id: userId, ...rest, + userRole: role.name, province: { connect: provinceId ? { id: provinceId } : undefined }, district: { connect: districtId ? { id: districtId } : undefined }, subDistrict: { connect: subDistrictId ? { id: subDistrictId } : undefined }, From 4592192327fa46421852d7d938cf918ee989504f Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 17 Apr 2024 16:22:14 +0700 Subject: [PATCH 139/161] feat: get role list of user --- src/services/keycloak.ts | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/services/keycloak.ts b/src/services/keycloak.ts index a9c52bc..db7b4b5 100644 --- a/src/services/keycloak.ts +++ b/src/services/keycloak.ts @@ -187,6 +187,45 @@ export async function getRoles(name?: string) { }; } +/** + * Get roles list of user + * + * Client must have permission to get realms roles + * + * @returns role's info (array if not specify name) if success, null if not found, false otherwise. + */ +export async function getUserRoles(userId: string) { + const res = await fetch( + `${KC_URL}/admin/realms/${KC_REALM}/users/${userId}/role-mappings/realm`, + { + // prettier-ignore + headers: { + "authorization": `Bearer ${await getToken()}`, + }, + }, + ).catch((e) => console.log(e)); + + if (!res) return false; + if (!res.ok && res.status !== 404) { + return Boolean(console.error("Keycloak Error Response: ", await res.json())); + } + + if (res.status === 404) { + return null; + } + + const data = await res.json(); + + if (Array.isArray(data)) { + return data.map((v: Record) => ({ id: v.id, name: v.name })); + } + + return { + id: data.id, + name: data.name, + }; +} + /** * Assign role to user * From f91c9c2f145027f0ef73a05d73380c7d1afb1d4d Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 17 Apr 2024 16:35:35 +0700 Subject: [PATCH 140/161] feat: edit user role --- src/controllers/user-controller.ts | 31 +++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/controllers/user-controller.ts b/src/controllers/user-controller.ts index 9640367..2af6d33 100644 --- a/src/controllers/user-controller.ts +++ b/src/controllers/user-controller.ts @@ -19,7 +19,14 @@ import minio from "../services/minio"; import { RequestWithUser } from "../interfaces/user"; import HttpError from "../interfaces/http-error"; import HttpStatus from "../interfaces/http-status"; -import { addUserRoles, createUser, deleteUser, getRoles } from "../services/keycloak"; +import { + addUserRoles, + createUser, + deleteUser, + getRoles, + getUserRoles, + removeUserRoles, +} from "../services/keycloak"; if (!process.env.MINIO_BUCKET) { throw Error("Require MinIO bucket."); @@ -336,6 +343,27 @@ export class UserController extends Controller { ); } + let list = await getRoles(); + + if (!Array.isArray(list)) throw new Error("Failed. Cannot get role(s) data from the server."); + if (Array.isArray(list)) { + list = list.filter( + (a) => + !["uma_authorization", "offline_access", "default-roles"].some((b) => a.name.includes(b)), + ); + } + const currentRole = await getUserRoles(userId); + + const role = list.find((v) => v.id === body.userRole); + + const resultAddRole = role && (await addUserRoles(userId, [role])); + + if (!resultAddRole) { + throw new Error("Failed. Cannot set user's role."); + } else { + if (Array.isArray(currentRole)) await removeUserRoles(userId, currentRole); + } + const { provinceId, districtId, subDistrictId, ...rest } = body; const user = await prisma.user.findFirst({ @@ -362,6 +390,7 @@ export class UserController extends Controller { include: { province: true, district: true, subDistrict: true }, data: { ...rest, + userRole: role.name, code: (lastUserOfType && `${user.code?.slice(0, 3)}${body.userType !== "USER" ? body.userType?.charAt(0) : ""}${(+(lastUserOfType?.code?.slice(-4) || 0) + 1).toString().padStart(4, "0")}`) || From 4c6ebb339d721fcf649ed293b7bd27c48e44988f Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 17 Apr 2024 16:48:41 +0700 Subject: [PATCH 141/161] chore: db migrate --- .../20240417094807_add_username_field/migration.sql | 8 ++++++++ prisma/schema.prisma | 1 + 2 files changed, 9 insertions(+) create mode 100644 prisma/migrations/20240417094807_add_username_field/migration.sql diff --git a/prisma/migrations/20240417094807_add_username_field/migration.sql b/prisma/migrations/20240417094807_add_username_field/migration.sql new file mode 100644 index 0000000..2d730b3 --- /dev/null +++ b/prisma/migrations/20240417094807_add_username_field/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `username` to the `User` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "username" TEXT NOT NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 083924c..4b648c5 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -263,6 +263,7 @@ model User { firstNameEN String lastName String lastNameEN String + username String gender String address String From 3cc439d3d447e46e701b1e6eb79c7741535d2047 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 17 Apr 2024 16:48:49 +0700 Subject: [PATCH 142/161] feat: store username --- src/controllers/user-controller.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/controllers/user-controller.ts b/src/controllers/user-controller.ts index 2af6d33..2f76cb9 100644 --- a/src/controllers/user-controller.ts +++ b/src/controllers/user-controller.ts @@ -285,6 +285,7 @@ export class UserController extends Controller { data: { id: userId, ...rest, + username, userRole: role.name, province: { connect: provinceId ? { id: provinceId } : undefined }, district: { connect: districtId ? { id: districtId } : undefined }, From a352b874703a60e48301b1f3e45cd295848d15b0 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 17 Apr 2024 17:50:27 +0700 Subject: [PATCH 143/161] feat: add branch image --- src/controllers/branch-controller.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/controllers/branch-controller.ts b/src/controllers/branch-controller.ts index 34257bd..39e05c2 100644 --- a/src/controllers/branch-controller.ts +++ b/src/controllers/branch-controller.ts @@ -72,6 +72,10 @@ function lineImageLoc(id: string) { return `branch/line-qr-${id}`; } +function branchImageLoc(id: string) { + return `branch/branch-img-${id}`; +} + @Route("api/branch") @Tags("Branch") @Security("keycloak") @@ -197,10 +201,8 @@ export class BranchController extends Controller { ? await Promise.all( record.contact.map(async (v) => Object.assign(v, { - qrCodeImageUrl: await minio.presignedGetObject( - MINIO_BUCKET, - `branch/contact-${record.id}`, - ), + imageUrl: await minio.presignedGetObject(MINIO_BUCKET, branchImageLoc(v.id)), + qrCodeImageUrl: await minio.presignedGetObject(MINIO_BUCKET, lineImageLoc(v.id)), }), ), ) @@ -293,6 +295,8 @@ export class BranchController extends Controller { return Object.assign(record, { contact: await prisma.branchContact.findMany({ where: { branchId: record.id } }), + imageUrl: await minio.presignedGetObject(MINIO_BUCKET, branchImageLoc(record.id)), + imageUploadUrl: await minio.presignedPutObject(MINIO_BUCKET, branchImageLoc(record.id)), qrCodeImageUrl: await minio.presignedGetObject( MINIO_BUCKET, lineImageLoc(record.id), @@ -395,6 +399,8 @@ export class BranchController extends Controller { } return Object.assign(record, { + imageUrl: await minio.presignedGetObject(MINIO_BUCKET, branchImageLoc(record.id)), + imageUploadUrl: await minio.presignedPutObject(MINIO_BUCKET, branchImageLoc(record.id)), qrCodeImageUrl: await minio.presignedGetObject( MINIO_BUCKET, lineImageLoc(record.id), @@ -430,6 +436,9 @@ export class BranchController extends Controller { await minio.removeObject(MINIO_BUCKET, lineImageLoc(branchId), { forceDelete: true, }); + await minio.removeObject(MINIO_BUCKET, branchImageLoc(branchId), { + forceDelete: true, + }); return await prisma.branch.delete({ include: { From d4cfd138d9645803177b08e25f75a316a02898e9 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 18 Apr 2024 09:22:53 +0700 Subject: [PATCH 144/161] feat: auto gen branch no --- src/controllers/customer-branch-controller.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/controllers/customer-branch-controller.ts b/src/controllers/customer-branch-controller.ts index 329d512..c2220cc 100644 --- a/src/controllers/customer-branch-controller.ts +++ b/src/controllers/customer-branch-controller.ts @@ -34,7 +34,6 @@ type CustomerBranchCreate = { status?: Status; - branchNo: string; legalPersonNo: string; taxNo: string; @@ -62,7 +61,6 @@ type CustomerBranchUpdate = { status?: "ACTIVE" | "INACTIVE"; - branchNo?: string; legalPersonNo?: string; taxNo?: string; @@ -228,6 +226,10 @@ export class CustomerBranchController extends Controller { const { provinceId, districtId, subDistrictId, customerId, ...rest } = body; + const count = await prisma.customerBranch.count({ + where: { customerId }, + }); + const record = await prisma.customerBranch.create({ include: { province: true, @@ -236,6 +238,7 @@ export class CustomerBranchController extends Controller { }, data: { ...rest, + branchNo: `${count + 1}`, customer: { connect: { id: customerId } }, province: { connect: provinceId ? { id: provinceId } : undefined }, district: { connect: districtId ? { id: districtId } : undefined }, From b3277c85606de7d5cd54a9d4259dbe60f73a9692 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 18 Apr 2024 13:09:14 +0700 Subject: [PATCH 145/161] chore: db migration --- .../migration.sql | 12 +++++++++++ .../migration.sql | 2 ++ .../migration.sql | 8 +++++++ prisma/schema.prisma | 21 +++++++++---------- 4 files changed, 32 insertions(+), 11 deletions(-) create mode 100644 prisma/migrations/20240418042147_fix_missing_field/migration.sql create mode 100644 prisma/migrations/20240418060611_add_hq_tel_field/migration.sql create mode 100644 prisma/migrations/20240418060739_remove_optional_hq_tel/migration.sql diff --git a/prisma/migrations/20240418042147_fix_missing_field/migration.sql b/prisma/migrations/20240418042147_fix_missing_field/migration.sql new file mode 100644 index 0000000..c22f622 --- /dev/null +++ b/prisma/migrations/20240418042147_fix_missing_field/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - You are about to drop the column `componentId` on the `RoleMenuComponentPermission` table. All the data in the column will be lost. + - You are about to drop the column `componentTag` on the `RoleMenuComponentPermission` table. All the data in the column will be lost. + - Added the required column `userRole` to the `RoleMenuComponentPermission` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "RoleMenuComponentPermission" DROP COLUMN "componentId", +DROP COLUMN "componentTag", +ADD COLUMN "userRole" TEXT NOT NULL; diff --git a/prisma/migrations/20240418060611_add_hq_tel_field/migration.sql b/prisma/migrations/20240418060611_add_hq_tel_field/migration.sql new file mode 100644 index 0000000..ae17c14 --- /dev/null +++ b/prisma/migrations/20240418060611_add_hq_tel_field/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Branch" ADD COLUMN "telephoneHq" TEXT; diff --git a/prisma/migrations/20240418060739_remove_optional_hq_tel/migration.sql b/prisma/migrations/20240418060739_remove_optional_hq_tel/migration.sql new file mode 100644 index 0000000..922dfa6 --- /dev/null +++ b/prisma/migrations/20240418060739_remove_optional_hq_tel/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Made the column `telephoneHq` on table `Branch` required. This step will fail if there are existing NULL values in that column. + +*/ +-- AlterTable +ALTER TABLE "Branch" ALTER COLUMN "telephoneHq" SET NOT NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4b648c5..47d0d8b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -81,14 +81,12 @@ model MenuComponent { model RoleMenuComponentPermission { id String @id @default(uuid()) - componentId String - componentTag String + userRole String + permission String menuComponent MenuComponent @relation(fields: [menuComponentId], references: [id]) menuComponentId String - permission String - createdBy String? createdAt DateTime @default(now()) updateBy String? @@ -177,13 +175,14 @@ enum Status { } model Branch { - id String @id @default(uuid()) - code String - taxNo String - name String - nameEN String - address String - addressEN String + id String @id @default(uuid()) + code String + taxNo String + name String + nameEN String + address String + addressEN String + telephoneHq String province Province? @relation(fields: [provinceId], references: [id], onDelete: SetNull) provinceId String? From 208aa77efdc47453763c44adbe36b147e497da74 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 18 Apr 2024 13:09:29 +0700 Subject: [PATCH 146/161] feat: add hq tel field --- src/controllers/branch-controller.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/controllers/branch-controller.ts b/src/controllers/branch-controller.ts index 39e05c2..741521d 100644 --- a/src/controllers/branch-controller.ts +++ b/src/controllers/branch-controller.ts @@ -36,6 +36,7 @@ type BranchCreate = { zipCode: string; email: string; contactName: string; + telephoneHq: string; telephoneNo: string | string[]; lineId: string; longitude: string; @@ -57,6 +58,7 @@ type BranchUpdate = { zipCode?: string; email?: string; contactName?: string; + telephoneHq: string; telephoneNo?: string | string[]; lineId?: string; longitude?: string; From cb5d7ae208cecfc4fca68edea4c71bd0e2cc77e5 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 18 Apr 2024 13:09:53 +0700 Subject: [PATCH 147/161] fix: error when update record not found --- src/controllers/customer-controller.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/controllers/customer-controller.ts b/src/controllers/customer-controller.ts index c2a8b52..4be81cf 100644 --- a/src/controllers/customer-controller.ts +++ b/src/controllers/customer-controller.ts @@ -134,6 +134,10 @@ export class CustomerController extends Controller { @Request() req: RequestWithUser, @Body() body: CustomerUpdate, ) { + if (!(await prisma.customer.findUnique({ where: { id: customerId } }))) { + throw new HttpError(HttpStatus.NOT_FOUND, "Customer cannot be found.", "data_not_found"); + } + const record = await prisma.customer.update({ where: { id: customerId }, data: { @@ -143,10 +147,6 @@ export class CustomerController extends Controller { }, }); - if (!record) { - throw new HttpError(HttpStatus.NOT_FOUND, "Customer cannot be found."); - } - return Object.assign(record, { imageUrl: await minio.presignedGetObject( MINIO_BUCKET, From d565a29a75e2e6191b9d82e8aeaed0dd2f81a2d6 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 18 Apr 2024 13:12:18 +0700 Subject: [PATCH 148/161] chore: bump deps ver --- package.json | 4 ++-- pnpm-lock.yaml | 56 +++++++++++++++++++++++++------------------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index 390a7e9..b2e63ea 100644 --- a/package.json +++ b/package.json @@ -24,13 +24,13 @@ "@types/swagger-ui-express": "^4.1.6", "nodemon": "^3.1.0", "prettier": "^3.2.5", - "prisma": "^5.11.0", + "prisma": "^5.12.1", "ts-node": "^10.9.2", "typescript": "^5.4.3" }, "dependencies": { "@elastic/elasticsearch": "^8.13.0", - "@prisma/client": "5.11.0", + "@prisma/client": "5.12.1", "@tsoa/runtime": "^6.2.0", "cors": "^2.8.5", "dotenv": "^16.4.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 035cc0e..244f837 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ dependencies: specifier: ^8.13.0 version: 8.13.0 '@prisma/client': - specifier: 5.11.0 - version: 5.11.0(prisma@5.11.0) + specifier: 5.12.1 + version: 5.12.1(prisma@5.12.1) '@tsoa/runtime': specifier: ^6.2.0 version: 6.2.0 @@ -59,8 +59,8 @@ devDependencies: specifier: ^3.2.5 version: 3.2.5 prisma: - specifier: ^5.11.0 - version: 5.11.0 + specifier: ^5.12.1 + version: 5.12.1 ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@20.12.2)(typescript@5.4.3) @@ -366,8 +366,8 @@ packages: dev: false optional: true - /@prisma/client@5.11.0(prisma@5.11.0): - resolution: {integrity: sha512-SWshvS5FDXvgJKM/a0y9nDC1rqd7KG0Q6ZVzd+U7ZXK5soe73DJxJJgbNBt2GNXOa+ysWB4suTpdK5zfFPhwiw==} + /@prisma/client@5.12.1(prisma@5.12.1): + resolution: {integrity: sha512-6/JnizEdlSBxDIdiLbrBdMW5NqDxOmhXAJaNXiPpgzAPr/nLZResT6MMpbOHLo5yAbQ1Vv5UU8PTPRzb0WIxdA==} engines: {node: '>=16.13'} requiresBuild: true peerDependencies: @@ -376,35 +376,35 @@ packages: prisma: optional: true dependencies: - prisma: 5.11.0 + prisma: 5.12.1 dev: false - /@prisma/debug@5.11.0: - resolution: {integrity: sha512-N6yYr3AbQqaiUg+OgjkdPp3KPW1vMTAgtKX6+BiB/qB2i1TjLYCrweKcUjzOoRM5BriA4idrkTej9A9QqTfl3A==} + /@prisma/debug@5.12.1: + resolution: {integrity: sha512-kd/wNsR0klrv79o1ITsbWxYyh4QWuBidvxsXSParPsYSu0ircUmNk3q4ojsgNc3/81b0ozg76iastOG43tbf8A==} - /@prisma/engines-version@5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102: - resolution: {integrity: sha512-WXCuyoymvrS4zLz4wQagSsc3/nE6CHy8znyiMv8RKazKymOMd5o9FP5RGwGHAtgoxd+aB/BWqxuP/Ckfu7/3MA==} + /@prisma/engines-version@5.12.0-21.473ed3124229e22d881cb7addf559799debae1ab: + resolution: {integrity: sha512-6yvO8s80Tym61aB4QNtYZfWVmE3pwqe807jEtzm8C5VDe7nw8O1FGX3TXUaXmWV0fQTIAfRbeL2Gwrndabp/0g==} - /@prisma/engines@5.11.0: - resolution: {integrity: sha512-gbrpQoBTYWXDRqD+iTYMirDlF9MMlQdxskQXbhARhG6A/uFQjB7DZMYocMQLoiZXO/IskfDOZpPoZE8TBQKtEw==} + /@prisma/engines@5.12.1: + resolution: {integrity: sha512-HQDdglLw2bZR/TXD2Y+YfDMvi5Q8H+acbswqOsWyq9pPjBLYJ6gzM+ptlTU/AV6tl0XSZLU1/7F4qaWa8bqpJA==} requiresBuild: true dependencies: - '@prisma/debug': 5.11.0 - '@prisma/engines-version': 5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102 - '@prisma/fetch-engine': 5.11.0 - '@prisma/get-platform': 5.11.0 + '@prisma/debug': 5.12.1 + '@prisma/engines-version': 5.12.0-21.473ed3124229e22d881cb7addf559799debae1ab + '@prisma/fetch-engine': 5.12.1 + '@prisma/get-platform': 5.12.1 - /@prisma/fetch-engine@5.11.0: - resolution: {integrity: sha512-994viazmHTJ1ymzvWugXod7dZ42T2ROeFuH6zHPcUfp/69+6cl5r9u3NFb6bW8lLdNjwLYEVPeu3hWzxpZeC0w==} + /@prisma/fetch-engine@5.12.1: + resolution: {integrity: sha512-qSs3KcX1HKcea1A+hlJVK/ljj0PNIUHDxAayGMvgJBqmaN32P9tCidlKz1EGv6WoRFICYnk3Dd/YFLBwnFIozA==} dependencies: - '@prisma/debug': 5.11.0 - '@prisma/engines-version': 5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102 - '@prisma/get-platform': 5.11.0 + '@prisma/debug': 5.12.1 + '@prisma/engines-version': 5.12.0-21.473ed3124229e22d881cb7addf559799debae1ab + '@prisma/get-platform': 5.12.1 - /@prisma/get-platform@5.11.0: - resolution: {integrity: sha512-rxtHpMLxNTHxqWuGOLzR2QOyQi79rK1u1XYAVLZxDGTLz/A+uoDnjz9veBFlicrpWjwuieM4N6jcnjj/DDoidw==} + /@prisma/get-platform@5.12.1: + resolution: {integrity: sha512-pgIR+pSvhYHiUcqXVEZS31NrFOTENC9yFUdEAcx7cdQBoZPmHVjtjN4Ss6NzVDMYPrKJJ51U14EhEoeuBlMioQ==} dependencies: - '@prisma/debug': 5.11.0 + '@prisma/debug': 5.12.1 /@tsconfig/node10@1.0.11: resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} @@ -1861,13 +1861,13 @@ packages: hasBin: true dev: true - /prisma@5.11.0: - resolution: {integrity: sha512-KCLiug2cs0Je7kGkQBN9jDWoZ90ogE/kvZTUTgz2h94FEo8pczCkPH7fPNXkD1sGU7Yh65risGGD1HQ5DF3r3g==} + /prisma@5.12.1: + resolution: {integrity: sha512-SkMnb6wyIxTv9ACqiHBI2u9gD6y98qXRoCoLEnZsF6yee5Qg828G+ARrESN+lQHdw4maSZFFSBPPDpvSiVTo0Q==} engines: {node: '>=16.13'} hasBin: true requiresBuild: true dependencies: - '@prisma/engines': 5.11.0 + '@prisma/engines': 5.12.1 /promise.any@2.0.6: resolution: {integrity: sha512-Ew/MrPtTjiHnnki0AA2hS2o65JaZ5n+5pp08JSyWWUdeOGF4F41P+Dn+rdqnaOV/FTxhR6eBDX412luwn3th9g==} From 624f4f2fd7cd4a4e2803e595a97fd5920902d263 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 18 Apr 2024 13:21:37 +0700 Subject: [PATCH 149/161] refactor: change field name --- .../20240418061959_rename_field/migration.sql | 9 ++++++ .../migration.sql | 8 ++++++ prisma/schema.prisma | 2 +- src/controllers/branch-controller.ts | 28 +++++++++---------- 4 files changed, 32 insertions(+), 15 deletions(-) create mode 100644 prisma/migrations/20240418061959_rename_field/migration.sql create mode 100644 prisma/migrations/20240418062042_remove_optional/migration.sql diff --git a/prisma/migrations/20240418061959_rename_field/migration.sql b/prisma/migrations/20240418061959_rename_field/migration.sql new file mode 100644 index 0000000..6233aa1 --- /dev/null +++ b/prisma/migrations/20240418061959_rename_field/migration.sql @@ -0,0 +1,9 @@ +/* + Warnings: + + - You are about to drop the column `telephoneHq` on the `Branch` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Branch" DROP COLUMN "telephoneHq", +ADD COLUMN "telephoneNo" TEXT; diff --git a/prisma/migrations/20240418062042_remove_optional/migration.sql b/prisma/migrations/20240418062042_remove_optional/migration.sql new file mode 100644 index 0000000..ca8ae37 --- /dev/null +++ b/prisma/migrations/20240418062042_remove_optional/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Made the column `telephoneNo` on table `Branch` required. This step will fail if there are existing NULL values in that column. + +*/ +-- AlterTable +ALTER TABLE "Branch" ALTER COLUMN "telephoneNo" SET NOT NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 47d0d8b..1652ee2 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -182,7 +182,7 @@ model Branch { nameEN String address String addressEN String - telephoneHq String + telephoneNo String province Province? @relation(fields: [provinceId], references: [id], onDelete: SetNull) provinceId String? diff --git a/src/controllers/branch-controller.ts b/src/controllers/branch-controller.ts index 741521d..b4dc67c 100644 --- a/src/controllers/branch-controller.ts +++ b/src/controllers/branch-controller.ts @@ -36,8 +36,8 @@ type BranchCreate = { zipCode: string; email: string; contactName: string; - telephoneHq: string; - telephoneNo: string | string[]; + contact: string | string[]; + telephoneNo: string; lineId: string; longitude: string; latitude: string; @@ -57,9 +57,9 @@ type BranchUpdate = { address?: string; zipCode?: string; email?: string; + telephoneNo: string; contactName?: string; - telephoneHq: string; - telephoneNo?: string | string[]; + contact?: string | string[]; lineId?: string; longitude?: string; latitude?: string; @@ -245,7 +245,7 @@ export class BranchController extends Controller { "missing_or_invalid_parameter", ); - const { provinceId, districtId, subDistrictId, headOfficeId, telephoneNo, ...rest } = body; + const { provinceId, districtId, subDistrictId, headOfficeId, contact, ...rest } = body; const year = new Date().getFullYear(); @@ -286,12 +286,12 @@ export class BranchController extends Controller { this.setStatus(HttpStatus.CREATED); - if (record && telephoneNo) { + if (record && contact) { await prisma.branchContact.createMany({ data: - typeof telephoneNo === "string" - ? [{ telephoneNo, branchId: record.id }] - : telephoneNo.map((v) => ({ telephoneNo: v, branchId: record.id })), + typeof contact === "string" + ? [{ telephoneNo: contact, branchId: record.id }] + : contact.map((v) => ({ telephoneNo: v, branchId: record.id })), }); } @@ -358,7 +358,7 @@ export class BranchController extends Controller { ); } - const { provinceId, districtId, subDistrictId, headOfficeId, telephoneNo, ...rest } = body; + const { provinceId, districtId, subDistrictId, headOfficeId, contact, ...rest } = body; if (!(await prisma.branch.findUnique({ where: { id: branchId } }))) { throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found"); @@ -390,13 +390,13 @@ export class BranchController extends Controller { where: { id: branchId }, }); - if (record && telephoneNo) { + if (record && contact) { await prisma.branchContact.deleteMany({ where: { branchId } }); await prisma.branchContact.createMany({ data: - typeof telephoneNo === "string" - ? [{ telephoneNo, branchId }] - : telephoneNo.map((v) => ({ telephoneNo: v, branchId })), + typeof contact === "string" + ? [{ telephoneNo: contact, branchId }] + : contact.map((v) => ({ telephoneNo: v, branchId })), }); } From 17135be8cf53167bdffe6a4b507551687ef96010 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 18 Apr 2024 14:37:01 +0700 Subject: [PATCH 150/161] fix: wrong structure return --- src/controllers/branch-controller.ts | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/controllers/branch-controller.ts b/src/controllers/branch-controller.ts index b4dc67c..4e3f4ac 100644 --- a/src/controllers/branch-controller.ts +++ b/src/controllers/branch-controller.ts @@ -197,19 +197,10 @@ export class BranchController extends Controller { throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found"); } - return { - ...record, - contact: record.contact - ? await Promise.all( - record.contact.map(async (v) => - Object.assign(v, { - imageUrl: await minio.presignedGetObject(MINIO_BUCKET, branchImageLoc(v.id)), - qrCodeImageUrl: await minio.presignedGetObject(MINIO_BUCKET, lineImageLoc(v.id)), - }), - ), - ) - : undefined, - }; + return Object.assign(record, { + imageUrl: await minio.presignedGetObject(MINIO_BUCKET, branchImageLoc(record.id)), + qrCodeImageUrl: await minio.presignedGetObject(MINIO_BUCKET, lineImageLoc(record.id)), + }); } @Post() From d7b0cb63ab49c13759a8a7102a2c91568241be96 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 18 Apr 2024 16:14:25 +0700 Subject: [PATCH 151/161] feat: add more searching field --- src/controllers/branch-controller.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/controllers/branch-controller.ts b/src/controllers/branch-controller.ts index 4e3f4ac..0c6f994 100644 --- a/src/controllers/branch-controller.ts +++ b/src/controllers/branch-controller.ts @@ -141,6 +141,7 @@ export class BranchController extends Controller { { nameEN: { contains: query }, zipCode }, { name: { contains: query }, zipCode }, { email: { contains: query }, zipCode }, + { telephoneNo: { contains: query }, zipCode }, ], } satisfies Prisma.BranchWhereInput; From 186f13bd6e83c8a89ab9935fba1d762bc8a209d3 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 18 Apr 2024 16:15:21 +0700 Subject: [PATCH 152/161] feat: menu and menu component for permission endpoint --- src/controllers/permission-controller.ts | 178 +++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 src/controllers/permission-controller.ts diff --git a/src/controllers/permission-controller.ts b/src/controllers/permission-controller.ts new file mode 100644 index 0000000..8d9b17c --- /dev/null +++ b/src/controllers/permission-controller.ts @@ -0,0 +1,178 @@ +import { Body, Controller, Delete, Get, Path, Post, Put, Route, Security, Tags } from "tsoa"; +import prisma from "../db"; +import HttpError from "../interfaces/http-error"; +import HttpStatus from "../interfaces/http-status"; +import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"; + +type MenuCreate = { + caption: string; + captionEN: string; + menuType: string; + url: string; + parentId?: string; +}; + +type MenuEdit = { + caption: string; + captionEN: string; + menuType: string; + url: string; +}; + +@Route("v1/permission/menu") +@Tags("Permission") +@Security("keycloak") +export class MenuController extends Controller { + @Get() + async listMenu() { + const record = await prisma.menu.findMany({ + include: { children: true, roleMenuPermission: true }, + orderBy: { createdAt: "asc" }, + }); + return record; + } + + @Post() + async createMenu(@Body() body: MenuCreate) { + if (body.parentId) { + const parent = await prisma.menu.findFirst({ where: { id: body.parentId } }); + + if (!parent) { + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Parent menu not found.", + "missing_or_invalid_parameter", + ); + } + } + + const record = await prisma.menu.create({ + include: { parent: true }, + data: body, + }); + + this.setStatus(HttpStatus.CREATED); + + return record; + } + + @Put("{menuId}") + async editMenu(@Path("menuId") id: string, @Body() body: MenuEdit) { + const record = await prisma.menu + .update({ + include: { parent: true }, + where: { id }, + data: body, + }) + .catch((e) => { + if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { + throw new HttpError(HttpStatus.NOT_FOUND, "Menu cannot be found.", "data_not_found"); + } + throw new Error(e); + }); + + return record; + } + + @Delete("{menuId}") + async deleteMenu(@Path("menuId") id: string) { + const record = await prisma.menu.deleteMany({ where: { id } }); + if (record.count <= 0) { + throw new HttpError(HttpStatus.NOT_FOUND, "Menu cannot be found.", "data_not_found"); + } + } +} + +type MenuComponentCreate = { + componentId: string; + componentTag: string; + menuId: string; +}; + +type MenuComponentEdit = { + componentId?: string; + componentTag?: string; + menuId?: string; +}; + +@Route("v1/permission/menu-component") +@Tags("Permission") +@Security("keycloak") +export class MenuComponentController extends Controller { + @Get() + async listMenuComponent() { + const record = await prisma.menuComponent.findMany({ + include: { roleMenuComponentPermission: true }, + orderBy: { createdAt: "asc" }, + }); + + return record; + } + + @Post() + async createMenuComponent(@Body() body: MenuComponentCreate) { + const menu = await prisma.menu.findFirst({ where: { id: body.menuId } }); + + if (!menu) { + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Menu not found.", + "missing_or_invalid_parameter", + ); + } + + const record = await prisma.menuComponent.create({ + data: body, + }); + + this.setStatus(HttpStatus.CREATED); + + return record; + } + + @Put("{menuComponentId}") + async editMenuComponent(@Path("menuComponentId") id: string, @Body() body: MenuComponentEdit) { + if (body.menuId) { + const menu = await prisma.menu.findFirst({ where: { id: body.menuId } }); + + if (!menu) { + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Menu not found.", + "missing_or_invalid_parameter", + ); + } + } + + const record = await prisma.menuComponent + .update({ + include: { roleMenuComponentPermission: true }, + where: { id }, + data: body, + }) + .catch((e) => { + if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { + throw new HttpError( + HttpStatus.NOT_FOUND, + "Menu component cannot be found.", + "data_not_found", + ); + } + throw new Error(e); + }); + + return record; + } + + @Delete("{menuComponentId}") + async deleteMenuComponent(@Path("menuComponentId") id: string) { + const record = await prisma.menuComponent.deleteMany({ where: { id } }); + if (record.count <= 0) { + throw new HttpError( + HttpStatus.NOT_FOUND, + "Menu component cannot be found.", + "data_not_found", + ); + } + } +} From 4750f1945d08f5c5dfd59f3b705a0a260b5139a3 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 18 Apr 2024 16:32:38 +0700 Subject: [PATCH 153/161] fix: error when not sent role --- src/controllers/user-controller.ts | 40 ++++++++++++++++++------------ 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/src/controllers/user-controller.ts b/src/controllers/user-controller.ts index 2f76cb9..51092fe 100644 --- a/src/controllers/user-controller.ts +++ b/src/controllers/user-controller.ts @@ -344,25 +344,33 @@ export class UserController extends Controller { ); } - let list = await getRoles(); + let userRole: string | undefined; - if (!Array.isArray(list)) throw new Error("Failed. Cannot get role(s) data from the server."); - if (Array.isArray(list)) { - list = list.filter( - (a) => - !["uma_authorization", "offline_access", "default-roles"].some((b) => a.name.includes(b)), - ); - } - const currentRole = await getUserRoles(userId); + if (body.userRole) { + let list = await getRoles(); - const role = list.find((v) => v.id === body.userRole); + if (!Array.isArray(list)) throw new Error("Failed. Cannot get role(s) data from the server."); + if (Array.isArray(list)) { + list = list.filter( + (a) => + !["uma_authorization", "offline_access", "default-roles"].some((b) => + a.name.includes(b), + ), + ); + } + const currentRole = await getUserRoles(userId); - const resultAddRole = role && (await addUserRoles(userId, [role])); + const role = list.find((v) => v.id === body.userRole); - if (!resultAddRole) { - throw new Error("Failed. Cannot set user's role."); - } else { - if (Array.isArray(currentRole)) await removeUserRoles(userId, currentRole); + const resultAddRole = role && (await addUserRoles(userId, [role])); + + if (!resultAddRole) { + throw new Error("Failed. Cannot set user's role."); + } else { + if (Array.isArray(currentRole)) await removeUserRoles(userId, currentRole); + } + + userRole = role.name; } const { provinceId, districtId, subDistrictId, ...rest } = body; @@ -391,7 +399,7 @@ export class UserController extends Controller { include: { province: true, district: true, subDistrict: true }, data: { ...rest, - userRole: role.name, + userRole, code: (lastUserOfType && `${user.code?.slice(0, 3)}${body.userType !== "USER" ? body.userType?.charAt(0) : ""}${(+(lastUserOfType?.code?.slice(-4) || 0) + 1).toString().padStart(4, "0")}`) || From fb158b24577b1cc6668d7f36633f2971e5e171fd Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 18 Apr 2024 16:39:49 +0700 Subject: [PATCH 154/161] fix: missing parameter username on edit --- src/controllers/user-controller.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/controllers/user-controller.ts b/src/controllers/user-controller.ts index 51092fe..b0f0780 100644 --- a/src/controllers/user-controller.ts +++ b/src/controllers/user-controller.ts @@ -23,6 +23,7 @@ import { addUserRoles, createUser, deleteUser, + editUser, getRoles, getUserRoles, removeUserRoles, @@ -77,6 +78,8 @@ type UserCreate = { type UserUpdate = { status?: "ACTIVE" | "INACTIVE"; + username?: string; + userType?: UserType; userRole?: string; @@ -373,6 +376,10 @@ export class UserController extends Controller { userRole = role.name; } + if (body.username) { + await editUser(userId, { username: body.username }); + } + const { provinceId, districtId, subDistrictId, ...rest } = body; const user = await prisma.user.findFirst({ From d49c20e9fa0cc2ab92a3022786a74eb16d098a0c Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 18 Apr 2024 16:47:50 +0700 Subject: [PATCH 155/161] feat: use role name instead --- src/controllers/user-controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/user-controller.ts b/src/controllers/user-controller.ts index b0f0780..ffa0f66 100644 --- a/src/controllers/user-controller.ts +++ b/src/controllers/user-controller.ts @@ -274,7 +274,7 @@ export class UserController extends Controller { throw new Error("Cannot create user with keycloak service."); } - const role = list.find((v) => v.id === body.userRole); + const role = list.find((v) => v.name === body.userRole); const resultAddRole = role && (await addUserRoles(userId, [role])); From 721f3bce6ef5c638401725ca832c72caebe18f2b Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 18 Apr 2024 16:50:12 +0700 Subject: [PATCH 156/161] fix: use role name instead (edit endpoint) --- src/controllers/user-controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/user-controller.ts b/src/controllers/user-controller.ts index ffa0f66..a31b0a8 100644 --- a/src/controllers/user-controller.ts +++ b/src/controllers/user-controller.ts @@ -363,7 +363,7 @@ export class UserController extends Controller { } const currentRole = await getUserRoles(userId); - const role = list.find((v) => v.id === body.userRole); + const role = list.find((v) => v.name === body.userRole); const resultAddRole = role && (await addUserRoles(userId, [role])); From d8034e221c6ffa4aa02c48085272964f54126bf8 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 18 Apr 2024 18:00:19 +0700 Subject: [PATCH 157/161] feat: update user code that has no associated branch --- src/controllers/branch-user-controller.ts | 26 +++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/controllers/branch-user-controller.ts b/src/controllers/branch-user-controller.ts index 19ffcd9..d33b307 100644 --- a/src/controllers/branch-user-controller.ts +++ b/src/controllers/branch-user-controller.ts @@ -157,6 +157,12 @@ export class BranchUserController extends Controller { await prisma.$transaction( body.user.map((v) => prisma.branchUser.deleteMany({ where: { branchId, userId: v } })), ); + await prisma.user.updateMany({ + where: { + branch: { none: {} }, + }, + data: { code: null }, + }); } @Delete("{userId}") @@ -164,6 +170,13 @@ export class BranchUserController extends Controller { await prisma.branchUser.deleteMany({ where: { branchId, userId }, }); + await prisma.user.updateMany({ + where: { + id: userId, + branch: { none: {} }, + }, + data: { code: null }, + }); } } @@ -253,6 +266,12 @@ export class UserBranchController extends Controller { await prisma.$transaction( body.branch.map((v) => prisma.branchUser.deleteMany({ where: { userId, branchId: v } })), ); + await prisma.user.updateMany({ + where: { + branch: { none: {} }, + }, + data: { code: null }, + }); } @Delete("{branchId}") @@ -260,5 +279,12 @@ export class UserBranchController extends Controller { await prisma.branchUser.deleteMany({ where: { branchId, userId }, }); + await prisma.user.updateMany({ + where: { + id: userId, + branch: { none: {} }, + }, + data: { code: null }, + }); } } From f7cc53e6ab988ac503404f96a7564a187d435347 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 18 Apr 2024 18:00:35 +0700 Subject: [PATCH 158/161] chore: sort tag permission --- tsoa.json | 1 + 1 file changed, 1 insertion(+) diff --git a/tsoa.json b/tsoa.json index f503e5e..694176c 100644 --- a/tsoa.json +++ b/tsoa.json @@ -17,6 +17,7 @@ "tags": [ { "name": "OpenAPI" }, { "name": "Single-Sign On" }, + { "name": "Permission" }, { "name": "Address" }, { "name": "Branch" }, { "name": "Branch Contact" }, From 030aa687e1f3f48fcea9b40089b7ff5a8983a746 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 19 Apr 2024 09:28:17 +0700 Subject: [PATCH 159/161] feat: change to some field to optional --- src/controllers/branch-controller.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/branch-controller.ts b/src/controllers/branch-controller.ts index 0c6f994..d9dcda9 100644 --- a/src/controllers/branch-controller.ts +++ b/src/controllers/branch-controller.ts @@ -35,10 +35,10 @@ type BranchCreate = { address: string; zipCode: string; email: string; - contactName: string; + contactName?: string | null; contact: string | string[]; telephoneNo: string; - lineId: string; + lineId?: string | null; longitude: string; latitude: string; From b81453708334fd3978777fd194d2031a43f662a6 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 19 Apr 2024 09:33:05 +0700 Subject: [PATCH 160/161] feat: update contact to be nullable when request --- src/controllers/branch-controller.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/controllers/branch-controller.ts b/src/controllers/branch-controller.ts index d9dcda9..9aa12a3 100644 --- a/src/controllers/branch-controller.ts +++ b/src/controllers/branch-controller.ts @@ -36,7 +36,7 @@ type BranchCreate = { zipCode: string; email: string; contactName?: string | null; - contact: string | string[]; + contact?: string | string[] | null; telephoneNo: string; lineId?: string | null; longitude: string; @@ -59,7 +59,7 @@ type BranchUpdate = { email?: string; telephoneNo: string; contactName?: string; - contact?: string | string[]; + contact?: string | string[] | null; lineId?: string; longitude?: string; latitude?: string; @@ -382,14 +382,15 @@ export class BranchController extends Controller { where: { id: branchId }, }); - if (record && contact) { + if (record && contact !== undefined) { await prisma.branchContact.deleteMany({ where: { branchId } }); - await prisma.branchContact.createMany({ - data: - typeof contact === "string" - ? [{ telephoneNo: contact, branchId }] - : contact.map((v) => ({ telephoneNo: v, branchId })), - }); + contact && + (await prisma.branchContact.createMany({ + data: + typeof contact === "string" + ? [{ telephoneNo: contact, branchId }] + : contact.map((v) => ({ telephoneNo: v, branchId })), + })); } return Object.assign(record, { From 27bbcd93204e9432388b686ab5bb7778ad74cc73 Mon Sep 17 00:00:00 2001 From: Suphonchai Phoonsawat <114205689+suphonchaip@users.noreply.github.com> Date: Fri, 19 Apr 2024 09:37:10 +0700 Subject: [PATCH 161/161] Add Github Action --- .github/workflows/release.yml | 74 +++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..d518c84 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,74 @@ +name: release-test +run-name: release-test ${{ github.actor }} +on: + push: + tags: + - "version-[0-9]+.[0-9]+.[0-9]+" + workflow_dispatch: +env: + REGISTRY: docker.frappet.com + IMAGE_NAME: jws/jws-backend + DEPLOY_HOST: 49.0.91.80 + COMPOSE_PATH: /home/frappet/docker/jws +jobs: + # act workflow_dispatch -W .github/workflows/release.yaml --input IMAGE_VER=test-v1 -s DOCKER_USER=sorawit -s DOCKER_PASS=P@ssword -s SSH_PASSWORD=P@ssw0rd + release-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + # skip Set up QEMU because it fail on act and container + # Gen Version try to get version from tag or inut + - name: Set output tags + id: vars + run: echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT + - name: Gen Version + id: gen_ver + run: | + if [[ $GITHUB_REF == 'refs/tags/'* ]]; then + IMAGE_VER=${{ steps.vars.outputs.tag }} + else + IMAGE_VER=${{ github.event.inputs.IMAGE_VER }} + fi + if [[ $IMAGE_VER == '' ]]; then + IMAGE_VER='test-vBeta' + fi + echo '::set-output name=image_ver::'$IMAGE_VER + - name: Check Version + run: | + echo $GITHUB_REF + echo ${{ steps.gen_ver.outputs.image_ver }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Login in to registry + uses: docker/login-action@v2 + with: + registry: ${{env.REGISTRY}} + username: ${{secrets.DOCKER_USER}} + password: ${{secrets.DOCKER_PASS}} + - name: Build and push docker image + uses: docker/build-push-action@v3 + with: + context: . + platforms: linux/amd64 + push: true + tags: ${{env.REGISTRY}}/${{env.IMAGE_NAME}}:${{ steps.gen_ver.outputs.image_ver }},${{env.REGISTRY}}/${{env.IMAGE_NAME}}:latest + + - uses: snow-actions/line-notify@v1.1.0 + if: success() + with: + access_token: ${{ secrets.TOKEN_LINE }} + message: | + -Success✅✅✅ + Image: ${{env.IMAGE_NAME}} + Version: ${{ steps.gen_ver.outputs.IMAGE_VER }} + By: ${{ github.actor }} + - uses: snow-actions/line-notify@v1.1.0 + if: failure() + with: + access_token: ${{ secrets.TOKEN_LINE }} + message: | + -Failure❌❌❌ + Image: ${{env.IMAGE_NAME}} + Version: ${{ steps.gen_ver.outputs.IMAGE_VER }} + By: ${{ github.actor }}