import { Body, Controller, Delete, Get, Put, Path, Post, Query, Request, Route, Security, Tags, } 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 ServiceCreate = { code: "MOU" | "mou"; name: string; detail: string; attributes?: { [key: string]: any; }; status?: Status; work?: { name: string; productId: string[]; attributes?: { [key: string]: any }; }[]; }; type ServiceUpdate = { name?: string; detail?: string; attributes?: { [key: string]: any; }; status?: "ACTIVE" | "INACTIVE"; work?: { name: string; productId: string[]; attributes?: { [key: string]: any }; }[]; }; function imageLocation(id: string) { return `service/${id}/service-image`; } @Route("api/v1/service") @Tags("Service") export class ServiceController extends Controller { @Get("stats") @Security("keycloak") async getServiceStats() { return await prisma.service.count(); } @Get() @Security("keycloak") async getService( @Query() query: string = "", @Query() page: number = 1, @Query() pageSize: number = 30, ) { const where = { OR: [{ name: { contains: query } }, { detail: { contains: query } }], } satisfies Prisma.ServiceWhereInput; const [result, total] = await prisma.$transaction([ prisma.service.findMany({ include: { work: true, }, orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }], where, take: pageSize, skip: (page - 1) * pageSize, }), prisma.service.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("{serviceId}") @Security("keycloak") async getServiceById(@Path() serviceId: string) { const record = await prisma.service.findFirst({ include: { work: { orderBy: { order: "asc" }, include: { productOnWork: { include: { product: true }, orderBy: { order: "asc" }, }, }, }, }, where: { id: serviceId }, }); if (!record) { throw new HttpError(HttpStatus.NOT_FOUND, "Service cannot be found.", "serviceNotFound"); } return Object.assign(record, { imageUrl: await presignedGetObjectIfExist(MINIO_BUCKET, imageLocation(record.id), 60 * 60), }); } @Get("{serviceId}/work") @Security("keycloak") async getWorkOfService( @Path() serviceId: string, @Query() page: number = 1, @Query() pageSize: number = 30, ) { const where = { serviceId, } satisfies Prisma.WorkWhereInput; const [result, total] = await prisma.$transaction([ prisma.work.findMany({ include: { productOnWork: { include: { product: true, }, orderBy: { order: "asc" }, }, }, where, take: pageSize, skip: (page - 1) * pageSize, }), prisma.work.count({ where }), ]); return { result, page, pageSize, total }; } @Get("{serviceId}/image") async getServiceImageById(@Request() req: RequestWithUser, @Path() serviceId: string) { const url = await presignedGetObjectIfExist(MINIO_BUCKET, imageLocation(serviceId), 60 * 60); if (!url) { throw new HttpError(HttpStatus.NOT_FOUND, "Image cannot be found", "imageNotFound"); } return req.res?.redirect(url); } @Post() @Security("keycloak") async createService(@Request() req: RequestWithUser, @Body() body: ServiceCreate) { const { work, ...payload } = body; const record = await prisma.$transaction( async (tx) => { const last = await tx.runningNo.upsert({ where: { key: `SERVICE_${body.code.toLocaleUpperCase()}`, }, create: { key: `SERVICE_${body.code.toLocaleUpperCase()}`, value: 1, }, update: { value: { increment: 1 } }, }); const workList = await Promise.all( (work || []).map(async (w, wIdx) => tx.work.create({ data: { name: w.name, order: wIdx + 1, attributes: w.attributes, productOnWork: { createMany: { data: w.productId.map((p, pIdx) => ({ productId: p, order: pIdx + 1, })), }, }, }, }), ), ); return tx.service.create({ include: { work: { include: { productOnWork: { include: { product: true, }, orderBy: { order: "asc" }, }, }, }, }, data: { ...payload, statusOrder: +(body.status === "INACTIVE"), code: `${body.code.toLocaleUpperCase()}${last.value.toString().padStart(3, "0")}`, work: { connect: workList.map((v) => ({ id: v.id })) }, 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("{serviceId}") @Security("keycloak") async editService( @Request() req: RequestWithUser, @Body() body: ServiceUpdate, @Path() serviceId: string, ) { if (!(await prisma.service.findUnique({ where: { id: serviceId } }))) { throw new HttpError(HttpStatus.NOT_FOUND, "Service cannot be found.", "serviceNotFound"); } const { work, ...payload } = body; const record = await prisma.$transaction(async (tx) => { const workList = await Promise.all( (work || []).map(async (w, wIdx) => tx.work.create({ data: { name: w.name, order: wIdx + 1, attributes: w.attributes, productOnWork: { createMany: { data: w.productId.map((p, pIdx) => ({ productId: p, order: pIdx + 1, })), }, }, }, }), ), ); return await tx.service.update({ data: { ...payload, statusOrder: +(payload.status === "INACTIVE"), work: { deleteMany: {}, connect: workList.map((v) => ({ id: v.id })), }, updateBy: req.user.name, }, where: { id: serviceId }, }); }); 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, ), }); } @Delete("{serviceId}") @Security("keycloak") async deleteService(@Path() serviceId: string) { const record = await prisma.service.findFirst({ where: { id: serviceId } }); if (!record) { throw new HttpError(HttpStatus.NOT_FOUND, "Service cannot be found.", "serviceNotFound"); } if (record.status !== Status.CREATED) { throw new HttpError(HttpStatus.FORBIDDEN, "Service is in used.", "serviceInUsed"); } return await prisma.service.delete({ where: { id: serviceId } }); } }