From 28d002ad65c67fcda9be2c07bfe1f4f25c2567f1 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 12 Jun 2024 14:16:06 +0700 Subject: [PATCH] feat: product endpoints --- src/controllers/product/product-controller.ts | 244 ++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100644 src/controllers/product/product-controller.ts diff --git a/src/controllers/product/product-controller.ts b/src/controllers/product/product-controller.ts new file mode 100644 index 0000000..8b8fbbe --- /dev/null +++ b/src/controllers/product/product-controller.ts @@ -0,0 +1,244 @@ +import { + Body, + Controller, + Delete, + Get, + Put, + Path, + Post, + Request, + Route, + Security, + Tags, + Queries, +} from "tsoa"; +import { Prisma, Status } from "@prisma/client"; + +import prisma from "../../db"; +import minio, { presignedGetObjectIfExist } 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 ProductCreate = { + code: "AC" | "DO" | "ac" | "do"; + name: string; + detail: string; + process: string; + price: number; + agentPrice: number; + serviceCharge: number; + attributes?: { + [key: string]: Date | string | number | null; + }; +}; + +type ProductUpdate = { + name: string; + detail: string; + process?: string; + price: number; + agentPrice: number; + serviceCharge: number; + attributes?: { + [key: string]: Date | string | number | null; + }; +}; + +function imageLocation(id: string) { + return `product/${id}/image`; +} + +@Route("api/v1/product") +@Tags("Product") +@Security("keycloak") +export class ProductController extends Controller { + @Get() + async getProduct( + @Queries() + qs: { + query?: string; + page?: number; + pageSize?: number; + filter?: string[]; + }, + ) { + qs.page = qs.page ?? 1; + qs.pageSize = qs.pageSize ?? 30; + qs.query = qs.query ?? ""; + + const { query, page, pageSize, filter } = qs; + + const where = { + OR: [{ name: { contains: query } }, { detail: { contains: query } }], + AND: filter + ?.map((v) => { + const match = /[\w\-]+:[\W\w]+$/.exec(v); + + if (!match) return []; + + const [key, ...rest] = v.split(":"); + const val = rest.join(); + + return { + OR: [ + { + attributes: { + path: [key], + string_contains: val, + }, + }, + { + attributes: { + path: [key], + equals: Number.isFinite(+val) + ? +val + : val === "true" + ? true + : val === "false" + ? false + : val, + }, + }, + ], + }; + }) + .flat(), + } satisfies Prisma.ProductWhereInput; + + const [result, total] = await prisma.$transaction([ + prisma.product.findMany({ + orderBy: { createdAt: "asc" }, + where, + take: pageSize, + skip: (page - 1) * pageSize, + }), + prisma.product.count({ where }), + ]); + + return { + result: await Promise.all( + result.map(async (v) => ({ + ...v, + imageUrl: await presignedGetObjectIfExist( + MINIO_BUCKET, + imageLocation(v.id), + 12 * 60 * 60, + ), + })), + ), + page, + pageSize, + total, + }; + } + + @Get("{productId}") + async getProductById(@Path() productId: string) { + const record = await prisma.product.findFirst({ + where: { id: productId }, + }); + + if (!record) { + throw new HttpError(HttpStatus.NOT_FOUND, "Product cannot be found.", "data_not_found"); + } + + return Object.assign(record, { + imageUrl: await presignedGetObjectIfExist(MINIO_BUCKET, imageLocation(record.id), 60 * 60), + }); + } + + @Post() + async createProduct(@Request() req: RequestWithUser, @Body() body: ProductCreate) { + const record = await prisma.$transaction( + async (tx) => { + const last = await tx.runningNo.upsert({ + where: { + key: `PRODUCT_${body.code.toLocaleUpperCase()}`, + }, + create: { + key: `PRODUCT_${body.code.toLocaleUpperCase()}`, + value: 1, + }, + update: { value: { increment: 1 } }, + }); + return await prisma.product.create({ + data: { + ...body, + code: `${body.code.toLocaleUpperCase()}${last.value.toString().padStart(3, "0")}`, + attributes: body.attributes, + createdBy: req.user.name, + updateBy: req.user.name, + }, + }); + }, + { + isolationLevel: Prisma.TransactionIsolationLevel.Serializable, + }, + ); + + this.setStatus(HttpStatus.CREATED); + + return Object.assign(record, { + imageUrl: await presignedGetObjectIfExist( + MINIO_BUCKET, + imageLocation(record.id), + 12 * 60 * 60, + ), + imageUploadUrl: await minio.presignedPutObject( + MINIO_BUCKET, + imageLocation(record.id), + 12 * 60 * 60, + ), + }); + } + + @Put("{productId}") + async editProduct( + @Request() req: RequestWithUser, + @Body() body: ProductUpdate, + @Path() productId: string, + ) { + if (!(await prisma.product.findUnique({ where: { id: productId } }))) { + throw new HttpError(HttpStatus.NOT_FOUND, "Product cannot be found.", "data_not_found"); + } + + const record = await prisma.product.update({ + data: { ...body, updateBy: req.user.name }, + where: { id: productId }, + }); + return Object.assign(record, { + profileImageUrl: await presignedGetObjectIfExist( + MINIO_BUCKET, + imageLocation(record.id), + 12 * 60 * 60, + ), + profileImageUploadUrl: await minio.presignedPutObject( + MINIO_BUCKET, + imageLocation(record.id), + 12 * 60 * 60, + ), + }); + } + + @Delete("{productId}") + async deleteProduct(@Path() productId: string) { + const record = await prisma.product.findFirst({ where: { id: productId } }); + + if (!record) { + throw new HttpError(HttpStatus.NOT_FOUND, "Product cannot be found.", "data_not_found"); + } + + if (record.status !== Status.CREATED) { + throw new HttpError(HttpStatus.FORBIDDEN, "Product is in used.", "data_in_used"); + } + + return await prisma.product.delete({ where: { id: productId } }); + } +}