From 38c684a78b3e3d39ce9aec8a790aeafe83381d62 Mon Sep 17 00:00:00 2001 From: somni Date: Tue, 26 Nov 2024 23:51:00 +0900 Subject: [PATCH] backend: Separate util functions into several files and refactor code --- projects/Backend/src/lib/common-functions.ts | 143 ------------- projects/Backend/src/lib/types.ts | 26 --- projects/Backend/src/lib/utils/cache-map.ts | 33 +++ projects/Backend/src/lib/utils/db.ts | 188 ++++++++++++++++++ projects/Backend/src/lib/utils/object.ts | 10 + projects/Backend/src/lib/utils/security.ts | 19 ++ .../src/modules/common/util/util.service.ts | 7 +- .../src/modules/v2/account/account.service.ts | 6 +- .../v2/booth-member/booth-member.service.ts | 12 +- .../v2/booth-order/booth-order.service.ts | 34 ++-- .../src/modules/v2/booth/booth.service.ts | 34 ++-- .../src/modules/v2/fair/fair.service.ts | 10 +- .../goods-category/goods-category.service.ts | 12 +- .../goods-combination.service.ts | 12 +- .../src/modules/v2/goods/goods.service.ts | 16 +- 15 files changed, 326 insertions(+), 236 deletions(-) delete mode 100644 projects/Backend/src/lib/common-functions.ts create mode 100644 projects/Backend/src/lib/utils/cache-map.ts create mode 100644 projects/Backend/src/lib/utils/db.ts create mode 100644 projects/Backend/src/lib/utils/object.ts create mode 100644 projects/Backend/src/lib/utils/security.ts diff --git a/projects/Backend/src/lib/common-functions.ts b/projects/Backend/src/lib/common-functions.ts deleted file mode 100644 index 4c9d26de..00000000 --- a/projects/Backend/src/lib/common-functions.ts +++ /dev/null @@ -1,143 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import { createHash, randomBytes } from "crypto"; -import { ISuccessResponse, SEQUELIZE_INTERNAL_KEYS, SUCCESS_RESPONSE } from "@myboothmanager/common"; -import { InternalServerErrorException, BadRequestException } from "@nestjs/common"; -import { BaseError, Includeable, Model, ModelDefined, Transaction, WhereOptions, col, fn, where } from "sequelize"; -import { Fn, Where } from "sequelize/types/utils"; -import { EntityNotFoundException } from "./exceptions"; - -/* === Sequelize model util functions === */ -export function jsonContains>(column: keyof T, containTarget: string): Fn { - return fn("JSON_CONTAINS", col(column as string), containTarget); -} - -export function stringCompareCaseSensitive>(column: keyof T, search: string): Where { - return where(fn("BINARY", col(column as string)), search); -} - -export async function findOneByPk>(model: { new (): T }, pk: number, includeModels?: Includeable[], transaction?: Transaction, excludeSequelizeInternalKeys: boolean = true): Promise { - let target: Model | null = null; - - try { - target = await (model as unknown as ModelDefined).findByPk(pk, { - include: includeModels, - attributes: { - exclude: excludeSequelizeInternalKeys ? SEQUELIZE_INTERNAL_KEYS : [], - }, - transaction, - }); - } catch(error) { - console.error(error); - - if(error instanceof BaseError) { - // DB error - throw new InternalServerErrorException("DB 오류"); - } else { - // Unknown error - throw new BadRequestException(); - } - } - - if(!target) throw new EntityNotFoundException(); - return target as T; -} - -export async function findAll>(model: { new (): T }, where: WhereOptions, includeModels?: Includeable[], transaction?: Transaction, excludeSequelizeInternalKeys: boolean = true): Promise { - let target: Model[] | null = null; - - try { - target = await (model as unknown as ModelDefined).findAll({ - where, - include: includeModels, - attributes: { - exclude: excludeSequelizeInternalKeys ? SEQUELIZE_INTERNAL_KEYS : [], - }, - transaction, - }); - } catch(error) { - console.error(error); - - if(error instanceof BaseError) { - // DB error - throw new InternalServerErrorException("DB 오류"); - } else { - // Unknown error - throw new BadRequestException(); - } - } - - if(!target) throw new EntityNotFoundException(); - return target as T[]; -} - -export async function create>(model: { new (): T }, dto: object, transaction?: Transaction, additionalParams?: Record): Promise { - try { - return await (model as unknown as ModelDefined).create({ - ...dto, - ...additionalParams, - }, { transaction }) as unknown as T; - } catch(error) { - console.error(error); - - if(error instanceof BaseError) { - // DB error - throw new InternalServerErrorException("DB 오류"); - } else { - // Unknown error - throw new BadRequestException(); - } - } -} - -export async function removeTarget>(model: T, transaction?: Transaction, ignoreParanoid: boolean = false): Promise { - try { - await model.destroy({ force: ignoreParanoid, transaction }); - await model.save({ transaction }); - } catch(error) { - console.error(error); - - throw new BadRequestException("삭제할 수 없습니다."); - } - - return SUCCESS_RESPONSE; -} - -export async function removeOne>(model: { new (): T }, where: WhereOptions, transaction?: Transaction, ignoreParanoid: boolean = false): Promise { - try { - const target = await (model as unknown as ModelDefined).findAll({ where }); - if(target.length !== 1) throw new BadRequestException("삭제 대상이 하나가 아닙니다."); - - return await removeTarget(target[0], transaction, ignoreParanoid); - } catch(error) { - console.error(error); - - if(error instanceof BaseError) { - // DB error - throw new InternalServerErrorException("DB 오류"); - } else { - // Unknown error - throw new BadRequestException(); - } - } -} - -/* === Common util functions === */ -export function generateRandomDigestFileName() { - const digest = `${createHash("sha1").update(randomBytes(64)).digest("base64url")}`; - - return { - fileName: digest, - withExt(ext: string) { - return `${this.fileName}.${ext}`; - }, - }; -} - -export function deleteKeys(obj: T, keys: readonly (keyof T)[]): void { - // Delete keys from object in-place - - for(const key of keys) { - delete obj[key]; - } -} diff --git a/projects/Backend/src/lib/types.ts b/projects/Backend/src/lib/types.ts index 61943833..3c618633 100644 --- a/projects/Backend/src/lib/types.ts +++ b/projects/Backend/src/lib/types.ts @@ -15,29 +15,3 @@ export interface IUploadStorage { extensions?: Array; imageThumbnailBase64?: string; } - -export abstract class CacheMap { - protected cache: Map = new Map(); - - abstract fetch(key: K): Promise; - - async get(key: K): Promise { - if(!this.has(key)) { - this.cache.set(key, await this.fetch(key)); - } - - return this.cache.get(key) as V; - } - - has(key: K): boolean { - return this.cache.has(key); - } - - async testValue(key: K, valueToTest: V): Promise { - return await this.get(key) === valueToTest; - } - - invalidate(key: K): boolean { - return this.cache.delete(key); - } -} diff --git a/projects/Backend/src/lib/utils/cache-map.ts b/projects/Backend/src/lib/utils/cache-map.ts new file mode 100644 index 00000000..1b11d2ec --- /dev/null +++ b/projects/Backend/src/lib/utils/cache-map.ts @@ -0,0 +1,33 @@ +/** + * Works like a Map, but if a key is not found, it will fetch the value from the fetch method. + * + * This is an abstract class that should be extended to implement the fetch method and use. + * + * @template K - Key type + * @template V - Value type + */ +export abstract class CacheMap { + protected cache: Map = new Map(); + + abstract fetch(key: K): Promise; + + async get(key: K): Promise { + if(!this.has(key)) { + this.cache.set(key, await this.fetch(key)); + } + + return this.cache.get(key) as V; + } + + has(key: K): boolean { + return this.cache.has(key); + } + + async testValue(key: K, valueToTest: V): Promise { + return await this.get(key) === valueToTest; + } + + invalidate(key: K): boolean { + return this.cache.delete(key); + } +} diff --git a/projects/Backend/src/lib/utils/db.ts b/projects/Backend/src/lib/utils/db.ts new file mode 100644 index 00000000..05ed8718 --- /dev/null +++ b/projects/Backend/src/lib/utils/db.ts @@ -0,0 +1,188 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { SEQUELIZE_INTERNAL_KEYS } from "@myboothmanager/common"; +import { InternalServerErrorException, BadRequestException } from "@nestjs/common"; +import { type Model, fn, col, where, type ModelDefined, BaseError, type FindOptions, type CreateOptions, type InstanceDestroyOptions, type ModelAttributes } from "sequelize"; +import type { Fn, Where } from "sequelize/types/utils"; +import { EntityNotFoundException } from "../exceptions"; + +type FindOptionsAsParam = Partial> & { includeSequelizeInternalKeys?: boolean } & { attributes?: { exclude?: string[] } }; +type CreateOptionsAsParam = Partial> & { includeSequelizeInternalKeys?: boolean }; + +/* === Query builder functions === */ +/** + * Build a `JSON_CONTAINS` query + * + * @template TModel - Model type + * @param column - Name of column that contains JSON + * @param searchValue - Value to search + * @returns {Fn} - Sequelize function + */ +export function jsonContains(column: keyof TModel, searchValue: string | number): Fn { + return fn("JSON_CONTAINS", col(column as string), searchValue.toString()); +} + +/** + * Build a `BINARY` query for case-sensitive string comparison + * + * @template TModel - Model type + * @param column - Name of column that contains string + * @param searchValue - Value to search + * @returns {Where} - Sequelize where clause + */ +export function stringCompareCaseSensitive(column: keyof TModel, searchValue: string): Where { + return where(fn("BINARY", col(column as string)), searchValue); +} + +/* === Execution functions === */ +/** + * Wrapper function for handling Sequelize actions + * + * This is for convenient error handling and to reduce code duplication. This should not be exported and used directly. + */ +async function actionWrapper(action: () => Promise): Promise { + let target: TResult | null = null; + + try { + target = await action(); + } catch(error) { + console.error(error); + + if(error instanceof BaseError) { + // DB error + throw new InternalServerErrorException(`DB error: ${error.name}`); + } else { + // Unknown error + throw new BadRequestException(`Unknown error: ${error instanceof Error ? error.name : "Unknwon"}`); + } + } + + if(!target) throw new EntityNotFoundException(); + return target; +} + +/** + * Find one record by primary key + * + * @template TModel - Model type + * @param model - Model class + * @param pk - Primary key to search + * @param options - Additional options to be passed to Sequelize + * @returns {Promise} - Found record + */ +export async function findOneByPk( + model: { new (): TModel }, + pk: number, + options: Omit, "where"> = { includeSequelizeInternalKeys: false }): Promise { + return await actionWrapper(async () => await (model as unknown as ModelDefined).findByPk(pk, { + ...options, + + attributes: { + ...options.attributes, + exclude: [ + ...(options.attributes?.exclude ?? []), + ...(options.includeSequelizeInternalKeys ? [] : SEQUELIZE_INTERNAL_KEYS), + ], + }, + }) as TModel); +} + +/** + * Find all records matching the condition + * + * @template TModel - Model type + * @param model - Model class + * @param options - Additional options (including `where` clause) to be passed to Sequelize + * @returns {Promise} - Found records + */ +export async function findAll( + model: { new (): TModel }, + options: FindOptionsAsParam = { includeSequelizeInternalKeys: false }): Promise { + return await actionWrapper(async () => await (model as unknown as ModelDefined).findAll({ + ...options, + + attributes: { + ...options.attributes, + exclude: [ + ...(options.attributes?.exclude ?? []), + ...(options.includeSequelizeInternalKeys ? [] : SEQUELIZE_INTERNAL_KEYS), + ], + }, + }) as TModel[]); +} + +/** + * Create a new record + * + * @template TModel - Model type + * @param model - Model class + * @param values - Data to be inserted + * @param options - Additional options to be passed to Sequelize + * @returns {Promise} - Created record + */ +export async function create( + model: { new (): TModel }, + values: Partial>, + options: CreateOptionsAsParam = { includeSequelizeInternalKeys: false }): Promise { + return await actionWrapper(async () => await (model as unknown as ModelDefined).create({ + ...Object.fromEntries( + Object.entries(values).filter(([key]) => { + if(!options.includeSequelizeInternalKeys) { + return !SEQUELIZE_INTERNAL_KEYS.some(internalKey => internalKey === key); + } + + return true; + }), + ), + }, { ...options }) as TModel); +} + +/** + * Remove specified single record(model instance) + * + * @template TModel - Model type + * @param model - Model instance + * @param options - Additional options to be passed to Sequelize + * @returns {Promise} - `true` if the record is successfully removed + */ +export async function removeInstance( + model: TModel, + options?: InstanceDestroyOptions): Promise { + try { + await model.destroy(); + await model.save({ transaction: options?.transaction }); + } catch(error) { + console.error(error); + + throw new BadRequestException("Can't be deleted"); + } + + return true; +} + +/** + * Remove specified single record by primary key + * + * @template TModel - Model type + * @param model - Model class + * @param pk - Primary key to search + * @param options - Additional options to be passed to Sequelize + * @returns {Promise} - `true` if the record is successfully removed + */ +export async function removeByPk( + model: { new (): TModel }, + pk: number, + options?: InstanceDestroyOptions): Promise { + try { + await (model as unknown as ModelDefined).destroy({ + where: { [(model as unknown as ModelDefined).primaryKeyAttribute]: pk }, + ...options, + }); + } catch(error) { + console.error(error); + + throw new BadRequestException("Can't be deleted"); + } + + return true; +} diff --git a/projects/Backend/src/lib/utils/object.ts b/projects/Backend/src/lib/utils/object.ts new file mode 100644 index 00000000..e6409b6d --- /dev/null +++ b/projects/Backend/src/lib/utils/object.ts @@ -0,0 +1,10 @@ +/** + * Delete keys from object in-place + * @param obj Target object + * @param keys Keys to delete + */ +export function deleteKeys(obj: T, keys: readonly (keyof T)[]): void { + for(const key of keys) { + delete obj[key]; + } +} diff --git a/projects/Backend/src/lib/utils/security.ts b/projects/Backend/src/lib/utils/security.ts new file mode 100644 index 00000000..9ac7c174 --- /dev/null +++ b/projects/Backend/src/lib/utils/security.ts @@ -0,0 +1,19 @@ +import { createHash, randomBytes } from "crypto"; + +/** + * Generates a random digest file name using random 32 bytes hashed with SHAKE256 (32 bytes output length). + * + * while SHAKE256 output length is 32 bytes, base64url encoded text with a length of 43 characters will be returned. + * + * @returns `fileName` is the bas64url-encoded generated file name, `withExt` is a function that appends an extension to the file name. + */ +export function generateRandomDigestFileName() { + const digest = `${createHash("shake256", { outputLength: 32 }).update(randomBytes(32)).digest("base64url")}`; + + return { + fileName: digest, + withExt(ext: string) { + return `${this.fileName}.${ext}`; + }, + }; +} diff --git a/projects/Backend/src/modules/common/util/util.service.ts b/projects/Backend/src/modules/common/util/util.service.ts index 58e94e16..c894787d 100644 --- a/projects/Backend/src/modules/common/util/util.service.ts +++ b/projects/Backend/src/modules/common/util/util.service.ts @@ -1,7 +1,6 @@ import type { Model } from "sequelize-typescript"; import type { MultipartFile } from "@fastify/multipart"; import type { FastifyRequest } from "fastify"; -import type { IUploadStorage } from "@/lib/types"; import path from "path"; import { createWriteStream } from "fs"; import * as fs from "fs/promises"; @@ -10,9 +9,9 @@ import AppRootPath from "app-root-path"; import { ISuccessResponse, ImageSizeConstraintKey, MAX_UPLOAD_FILE_BYTES, SUCCESS_RESPONSE, IImageUploadInfo } from "@myboothmanager/common"; import { InvalidRequestBodyException, RequestMaxSizeExceededException } from "@/lib/exceptions"; import UploadStorage from "@/db/models/uploadstorage"; -import { create, generateRandomDigestFileName } from "@/lib/common-functions"; -import { InternalKeysWithId } from "@/lib/types"; import ImageManipulator from "@/lib/image-manipulation"; +import { generateRandomDigestFileName } from "@/lib/utils/security"; +import { create } from "@/lib/utils/db"; @Injectable() export class UtilService { @@ -184,7 +183,7 @@ export class UtilService { fileName: digest.withExt("webp"), extensions: ["webp", "jpg"], imageThumbnailBase64, - } as Omit)).save(); + })).save(); await targetModelInstance.update({ [targetModelImageIdColumnKey]: upload.id }); diff --git a/projects/Backend/src/modules/v2/account/account.service.ts b/projects/Backend/src/modules/v2/account/account.service.ts index eafedef0..e95d8742 100644 --- a/projects/Backend/src/modules/v2/account/account.service.ts +++ b/projects/Backend/src/modules/v2/account/account.service.ts @@ -1,7 +1,6 @@ import { Injectable } from "@nestjs/common"; import * as argon2 from "argon2"; import Account from "@/db/models/account"; -import { create, removeOne, stringCompareCaseSensitive } from "@/lib/common-functions"; import { EntityNotFoundException, InvalidRequestBodyException } from "@/lib/exceptions"; import { ISuccessResponse, SEQUELIZE_INTERNAL_KEYS, SUCCESS_RESPONSE } from "@myboothmanager/common"; import { CreateAccountRequestDto } from "./dto/create.dto"; @@ -10,6 +9,7 @@ import { AccountInfoUpdateFailedException, AccountPasswordUpdateFailedException import { UpdateAccountPasswordRequestDto } from "./dto/update-password.dto"; import { WhereOptions } from "sequelize"; import { SUPER_ADMIN_AUTH_DATA } from "../auth/auth.service"; +import { create as dbCreate, findOneByPk, removeByPk, removeInstance, stringCompareCaseSensitive } from "@/lib/utils/db"; @Injectable() export default class AccountService { @@ -74,7 +74,7 @@ export default class AccountService { loginPassHash: await argon2.hash(createDto.loginPass), }; - return await create(Account, request); + return await dbCreate(Account, request); } /** @@ -129,6 +129,6 @@ export default class AccountService { async remove(id: number): Promise { // TODO: Remove all booths associated with the account - return await removeOne(Account, { id }); + return await removeByPk(Account, id) ? SUCCESS_RESPONSE : SUCCESS_RESPONSE; } } diff --git a/projects/Backend/src/modules/v2/booth-member/booth-member.service.ts b/projects/Backend/src/modules/v2/booth-member/booth-member.service.ts index fd75d720..f582f705 100644 --- a/projects/Backend/src/modules/v2/booth-member/booth-member.service.ts +++ b/projects/Backend/src/modules/v2/booth-member/booth-member.service.ts @@ -1,16 +1,16 @@ import type Booth from "@/db/models/booth"; import BoothMember from "@/db/models/booth-member"; import Goods from "@/db/models/goods"; -import { create as commonCreate, findAll as commonFindAll, findOneByPk, jsonContains, removeTarget } from "@/lib/common-functions"; import { EntityNotFoundException, NoAccessException } from "@/lib/exceptions"; -import type { ISuccessResponse } from "@myboothmanager/common"; +import { SUCCESS_RESPONSE, type ISuccessResponse } from "@myboothmanager/common"; import { forwardRef, Inject, Injectable } from "@nestjs/common"; -import { CacheMap } from "../../../lib/types"; import { BoothService } from "../booth/booth.service"; import { BoothMemberParentBoothNotFoundException } from "./booth-member.exception"; import type { CreateBoothMemberRequestDto } from "./dto/create.dto"; import { BoothMemberImageService } from "./booth-member.image.service"; import type { UpdateBoothMemberRequestDto } from "./dto/update.dto"; +import { CacheMap } from "@/lib/utils/cache-map"; +import { create as dbCreate, findAll as dbFindAll, findOneByPk, jsonContains, removeInstance } from "@/lib/utils/db"; @Injectable() export class BoothMemberService { @@ -94,7 +94,7 @@ export class BoothMemberService { await this.booth.findOne(boothId, !force); } - return await commonFindAll(BoothMember, { boothId }); + return await dbFindAll(BoothMember, { where: { boothId } }); } /** @@ -110,7 +110,7 @@ export class BoothMemberService { } // Create - return await commonCreate(BoothMember, createDto); + return await dbCreate(BoothMember, createDto); } /** @@ -149,7 +149,7 @@ export class BoothMemberService { } // Remove the entity - return await removeTarget(member); + return await removeInstance(member) ? SUCCESS_RESPONSE : SUCCESS_RESPONSE; } } diff --git a/projects/Backend/src/modules/v2/booth-order/booth-order.service.ts b/projects/Backend/src/modules/v2/booth-order/booth-order.service.ts index 007cc796..9a3b64a6 100644 --- a/projects/Backend/src/modules/v2/booth-order/booth-order.service.ts +++ b/projects/Backend/src/modules/v2/booth-order/booth-order.service.ts @@ -1,11 +1,9 @@ -import { Injectable } from "@nestjs/common"; +import { forwardRef, Inject, Injectable } from "@nestjs/common"; import { GoodsService } from "../goods/goods.service"; import { GoodsCombinationService } from "../goods-combination/goods-combination.service"; import BoothOrder from "@/db/models/goods-order"; import Booth from "@/db/models/booth"; -import { findOneByPk, create as commonCreate } from "@/lib/common-functions"; import { EntityNotFoundException, NoAccessException } from "@/lib/exceptions"; -import { CacheMap } from "@/lib/types"; import { BoothService } from "../booth/booth.service"; import { BoothOrderCreateGoodsCombinationNotFoundException, BoothOrderCreateGoodsNotFoundException, BoothOrderCreateInvalidGoodsAmountException, BoothOrderCreateInvalidGoodsCombinationException, BoothOrderCreateOrderEmptyException, BoothOrderParentBoothNotFoundException, BoothOrderStatusUpdateFailedException, BoothOrderStatusUpdateProhibitedException } from "./booth-order.exception"; import { CreateBoothOrderRequestDto } from "./dto/create.dto"; @@ -13,13 +11,18 @@ import { UpdateBoothOrderStatusRequestDto } from "./dto/update-status.dto"; import Goods from "@/db/models/goods"; import GoodsCombination from "@/db/models/goods-combination"; import MBMSequelize from "@/db/sequelize"; -import { IGoodsOrderItem, ISuccessResponse, SUCCESS_RESPONSE, GoodsOrderStatus, SEQUELIZE_INTERNAL_KEYS } from "@myboothmanager/common"; +import { IGoodsOrderItem, ISuccessResponse, SUCCESS_RESPONSE, GoodsOrderStatus } from "@myboothmanager/common"; +import { CacheMap } from "@/lib/utils/cache-map"; +import { findOneByPk, create as dbCreate, findAll as dbFindAll } from "@/lib/utils/db"; @Injectable() export class BoothOrderService { constructor( + @Inject(forwardRef(() => BoothService)) private readonly booth: BoothService, + @Inject(forwardRef(() => GoodsService)) private readonly goods: GoodsService, + @Inject(forwardRef(() => GoodsCombinationService)) private readonly combination: GoodsCombinationService, ) { } @@ -55,12 +58,12 @@ export class BoothOrderService { return { booth, - order: await findOneByPk(BoothOrder, orderId), + order: await findOneByPk(BoothOrder, orderId, { includeSequelizeInternalKeys: true }), }; } async create(createDto: CreateBoothOrderRequestDto, boothId: number, accountId: number): Promise { - if(!(await Booth.findOne({ where: { ownerId: accountId } }))) { + if(!(await Booth.findOne({ where: { id: boothId, ownerId: accountId } }))) { throw new BoothOrderParentBoothNotFoundException(); } @@ -92,7 +95,7 @@ export class BoothOrderService { // Not using GoodsService function for transaction // const goods = await this.goodsService.findGoodsBelongsToBooth(order.gId, createBoothOrderDto.boothId, callerAccountId); - const goods = await findOneByPk(Goods, order.gId, [], transaction); + const goods = await findOneByPk(Goods, order.gId, { transaction }); if(!goods) throw new BoothOrderCreateGoodsNotFoundException(); if(goods.boothId !== createDto.boothId) throw new NoAccessException(); @@ -104,7 +107,7 @@ export class BoothOrderService { // Not using GoodsCombinationService function for transaction // const combination = await this.goodsCombinationService.findGoodsCombinationBelongsToBooth(order.cId, createBoothOrderDto.boothId, callerAccountId); - const combination = await findOneByPk(GoodsCombination, order.cId, [], transaction); + const combination = await findOneByPk(GoodsCombination, order.cId, { transaction }); if(!combination) throw new BoothOrderCreateGoodsCombinationNotFoundException(); if(combination.boothId !== createDto.boothId) throw new NoAccessException(); @@ -127,7 +130,7 @@ export class BoothOrderService { } } - const created = await commonCreate(BoothOrder, createDto, transaction); + const created = await dbCreate(BoothOrder, createDto, { transaction }); await transaction.commit(); return created; } catch(err) { @@ -193,14 +196,15 @@ export class BoothOrderService { } async findAll(boothId: number, accountId: number): Promise { - const where = boothId ? { boothId } : undefined; + // Check if the booth belongs to the account + // `BoothService.findOne()` will throw errors on its own + await this.booth.findOne(boothId, false, accountId); - return await BoothOrder.findAll({ - where, - attributes: { - include: ["createdAt"], - exclude: SEQUELIZE_INTERNAL_KEYS, + return await dbFindAll(BoothOrder, { + where: { + boothId: boothId ?? undefined, }, + includeSequelizeInternalKeys: true, }); } } diff --git a/projects/Backend/src/modules/v2/booth/booth.service.ts b/projects/Backend/src/modules/v2/booth/booth.service.ts index 472fe80a..1e8bd0c2 100644 --- a/projects/Backend/src/modules/v2/booth/booth.service.ts +++ b/projects/Backend/src/modules/v2/booth/booth.service.ts @@ -1,8 +1,6 @@ import Account from "@/db/models/account"; import Booth from "@/db/models/booth"; -import { findOneByPk, findAll as commonFindAll, create as commonCreate, removeTarget } from "@/lib/common-functions"; import { NoAccessException } from "@/lib/exceptions"; -import { CacheMap } from "@/lib/types"; import { BoothStatus, ISuccessResponse, SUCCESS_RESPONSE } from "@myboothmanager/common"; import { forwardRef, Inject, Injectable } from "@nestjs/common"; import { Op } from "sequelize"; @@ -12,6 +10,8 @@ import { UpdateBoothRequestDto } from "./dto/update.dto"; import { UpdateBoothStatusRequestDto } from "./dto/update-status.dto"; import { SUPER_ADMIN_AUTH_DATA } from "../auth/auth.service"; import { BoothInfoUpdateFailedException, BoothNotPublishedException, BoothStatusUpdateFailedException } from "./booth.exception"; +import { CacheMap } from "@/lib/utils/cache-map"; +import { create as dbCreate, findAll as dbFindAll, findOneByPk, removeInstance } from "@/lib/utils/db"; @Injectable() export class BoothService { @@ -94,20 +94,22 @@ export class BoothService { * @returns Array of found `Booth` entities */ async findAll(onlyAvailable = false, accountId?: number): Promise { - const booths = await commonFindAll(Booth, { - ...(accountId ? { ownerId: accountId } : { }), - ...(onlyAvailable ? { - // status != CLOSE && !(status == PREPARE && statusContentPublished == false) - [Op.and]: { - status: { [Op.not]: BoothStatus.CLOSE }, - [Op.not]: { - [Op.and]: { - status: BoothStatus.PREPARE, - statusContentPublished: false, + const booths = await dbFindAll(Booth, { + where: { + ...(accountId ? { ownerId: accountId } : { }), + ...(onlyAvailable ? { + // status != CLOSE && !(status == PREPARE && statusContentPublished == false) + [Op.and]: { + status: { [Op.not]: BoothStatus.CLOSE }, + [Op.not]: { + [Op.and]: { + status: BoothStatus.PREPARE, + statusContentPublished: false, + }, }, }, - }, - } : { }), + } : { }), + }, }); // associatedFair.isPassed === false @@ -123,7 +125,7 @@ export class BoothService { * @returns Newly created `Booth` entity */ async create(createDto: CreateBoothRequestDto, accountId: number): Promise { - return await commonCreate(Booth, createDto, undefined, { ownerId: accountId }); + return await dbCreate(Booth, { ...createDto, ownerId: accountId }); } /** @@ -183,7 +185,7 @@ export class BoothService { // TODO: Remove all related entities like goods, orders, etc. - return await removeTarget(booth); + return await removeInstance(booth) ? SUCCESS_RESPONSE : SUCCESS_RESPONSE; } } diff --git a/projects/Backend/src/modules/v2/fair/fair.service.ts b/projects/Backend/src/modules/v2/fair/fair.service.ts index be42e788..e3636616 100644 --- a/projects/Backend/src/modules/v2/fair/fair.service.ts +++ b/projects/Backend/src/modules/v2/fair/fair.service.ts @@ -1,10 +1,10 @@ import { Injectable } from "@nestjs/common"; -import { ISuccessResponse } from "@myboothmanager/common"; +import { ISuccessResponse, SUCCESS_RESPONSE } from "@myboothmanager/common"; import Fair from "@/db/models/fair"; -import { create as commonCreate, removeOne, findAll as commonFindAll, findOneByPk } from "@/lib/common-functions"; import { CreateFairRequestDto } from "./dto/create.dto"; import { FairInfoUpdateFailedException, FairPassedException } from "./fair.exception"; import { UpdateFairRequestDto } from "./dto/update.dto"; +import { findOneByPk, findAll as dbFindAll, create as dbCreate, removeByPk } from "@/lib/utils/db"; @Injectable() export class FairService { @@ -16,7 +16,7 @@ export class FairService { * @returns Array of found `Fair` entities */ async findAll(includePassed = false): Promise { - const fairs = await commonFindAll(Fair, { }); + const fairs = await dbFindAll(Fair, { }); return fairs.filter(fair => includePassed || (!includePassed && !fair.isPassed)); } @@ -43,7 +43,7 @@ export class FairService { * @param dto DTO for creating a new fair */ async create(dto: CreateFairRequestDto): Promise { - return await commonCreate(Fair, dto); + return await dbCreate(Fair, dto); } /** @@ -72,6 +72,6 @@ export class FairService { async remove(id: number): Promise { // TODO: Unassociate booths with the fair - return await removeOne(Fair, { id }); + return await removeByPk(Fair, id) ? SUCCESS_RESPONSE : SUCCESS_RESPONSE; } } diff --git a/projects/Backend/src/modules/v2/goods-category/goods-category.service.ts b/projects/Backend/src/modules/v2/goods-category/goods-category.service.ts index 6f03e9f2..cdf16407 100644 --- a/projects/Backend/src/modules/v2/goods-category/goods-category.service.ts +++ b/projects/Backend/src/modules/v2/goods-category/goods-category.service.ts @@ -1,14 +1,14 @@ import { forwardRef, Inject, Injectable } from "@nestjs/common"; import { BoothService } from "../booth/booth.service"; -import { CacheMap } from "@/lib/types"; -import { findOneByPk, findAll as commonFindAll, create as commonCreate, removeTarget } from "@/lib/common-functions"; +import { CacheMap } from "@/lib/utils/cache-map"; +import { findOneByPk, findAll as dbFindAll, create as dbCreate, removeInstance } from "@/lib/utils/db"; import GoodsCategory from "@/db/models/goods-category"; import { DuplicatedEntityException, EntityNotFoundException, NoAccessException } from "@/lib/exceptions"; import Booth from "@/db/models/booth"; import { GoodsCategoryInfoUpdateFailedException, GoodsCategoryParentBoothNotFoundException } from "./goods-category.exception"; import { CreateGoodsCategoryRequestDto } from "./dto/create.dto"; import { UpdateGoodsCategoryRequestDto } from "./dto/update.dto"; -import { ISuccessResponse } from "@myboothmanager/common"; +import { ISuccessResponse, SUCCESS_RESPONSE } from "@myboothmanager/common"; import Goods from "@/db/models/goods"; import GoodsCombination from "@/db/models/goods-combination"; @@ -92,7 +92,7 @@ export class GoodsCategoryService { await this.booth.findOne(boothId, !force); } - return await commonFindAll(GoodsCategory, { boothId }); + return await dbFindAll(GoodsCategory, { where: { boothId } }); } /** @@ -118,7 +118,7 @@ export class GoodsCategoryService { } // Create - return await commonCreate(GoodsCategory, createDto); + return await dbCreate(GoodsCategory, createDto); } /** @@ -160,7 +160,7 @@ export class GoodsCategoryService { } // Remove - return await removeTarget(category, undefined, true); + return await removeInstance(category, { force: true }) ? SUCCESS_RESPONSE : SUCCESS_RESPONSE; } } diff --git a/projects/Backend/src/modules/v2/goods-combination/goods-combination.service.ts b/projects/Backend/src/modules/v2/goods-combination/goods-combination.service.ts index 0ad7e1c7..9145ee9e 100644 --- a/projects/Backend/src/modules/v2/goods-combination/goods-combination.service.ts +++ b/projects/Backend/src/modules/v2/goods-combination/goods-combination.service.ts @@ -4,10 +4,10 @@ import { GoodsCombinationImageService } from "./goods-combination.image.service" import GoodsCombination from "@/db/models/goods-combination"; import Booth from "@/db/models/booth"; import Goods from "@/db/models/goods"; -import { findOneByPk, removeTarget, findAll as commonFindAll, create as commonCreate } from "@/lib/common-functions"; +import { findOneByPk, removeInstance, findAll as dbFindAll, create as dbCreate } from "@/lib/utils/db"; import { NoAccessException, EntityNotFoundException } from "@/lib/exceptions"; -import { CacheMap } from "@/lib/types"; -import { GoodsStockVisibility, ISuccessResponse } from "@myboothmanager/common"; +import { CacheMap } from "@/lib/utils/cache-map"; +import { GoodsStockVisibility, ISuccessResponse, SUCCESS_RESPONSE } from "@myboothmanager/common"; import { CreateGoodsCombinationRequestDto } from "./dto/create.dto"; import { UpdateGoodsCombinationRequestDto } from "./dto/update.dto"; import { GoodsCombinationParentBoothNotFoundException, GoodsCombinationInfoUpdateFailedException } from "./goods-combination.exception"; @@ -94,7 +94,7 @@ export class GoodsCombinationService { await this.booth.findOne(boothId, !force); } - return await commonFindAll(GoodsCombination, { boothId }); + return await dbFindAll(GoodsCombination, { where: { boothId } }); } /** @@ -135,7 +135,7 @@ export class GoodsCombinationService { } // Create combination - const combination = await commonCreate(GoodsCombination, createDto); + const combination = await dbCreate(GoodsCombination, createDto); // Update existing goods to associate with the combination const goodsToBeUpdated = await Goods.findAll({ @@ -253,7 +253,7 @@ export class GoodsCombinationService { // Delete all images of the goods combination await this.image.deleteAllImages(id, boothId, accountId); - return await removeTarget(combination); + return await removeInstance(combination) ? SUCCESS_RESPONSE : SUCCESS_RESPONSE; } } diff --git a/projects/Backend/src/modules/v2/goods/goods.service.ts b/projects/Backend/src/modules/v2/goods/goods.service.ts index d9f076dd..ae7404a0 100644 --- a/projects/Backend/src/modules/v2/goods/goods.service.ts +++ b/projects/Backend/src/modules/v2/goods/goods.service.ts @@ -1,15 +1,15 @@ import { forwardRef, Inject, Injectable } from "@nestjs/common"; import { GoodsImageService } from "./goods.image.service"; import Goods from "@/db/models/goods"; -import { CacheMap } from "@/lib/types"; -import { findOneByPk, findAll as commonFindAll, create as commonCreate, removeTarget } from "@/lib/common-functions"; import { EntityNotFoundException, NoAccessException } from "@/lib/exceptions"; import { BoothService } from "../booth/booth.service"; import Booth from "@/db/models/booth"; import { GoodsInfoUpdateFailedException, GoodsParentBoothNotFoundException } from "./goods.exception"; import { CreateGoodsRequestDto } from "./dto/create.dto"; import { UpdateGoodsRequestDto } from "./dto/update.dto"; -import { ISuccessResponse } from "@myboothmanager/common"; +import { ISuccessResponse, SUCCESS_RESPONSE } from "@myboothmanager/common"; +import { CacheMap } from "@/lib/utils/cache-map"; +import { findOneByPk, removeInstance, findAll as dbFindAll, create as dbCreate } from "@/lib/utils/db"; @Injectable() export class GoodsService { @@ -93,7 +93,11 @@ export class GoodsService { await this.booth.findOne(boothId, !force); } - return await commonFindAll(Goods, boothId ? { boothId } : { }); + return await dbFindAll(Goods, { + where: { + boothId: boothId ?? undefined, + }, + }); } /** @@ -120,7 +124,7 @@ export class GoodsService { } // Create - return await commonCreate(Goods, createDto); + return await dbCreate(Goods, createDto); } /** @@ -163,7 +167,7 @@ export class GoodsService { // Delete all images of the goods await this.image.deleteAllImages(id, boothId, accountId); - return await removeTarget(goods); + return await removeInstance(goods) ? SUCCESS_RESPONSE : SUCCESS_RESPONSE; } }