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 } }); } }