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] 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"); + } +}