From e21abda0ceb2a846719eb22e6b44c4a3414dd979 Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Tue, 25 Nov 2025 11:26:33 +0330 Subject: [PATCH] category --- .../foods/controllers/food.controller.ts | 4 +- src/modules/foods/dto/create-food.dto.ts | 14 +++-- src/modules/foods/entities/category.entity.ts | 4 +- src/modules/foods/entities/food.entity.ts | 6 +- src/modules/foods/providers/food.service.ts | 50 ++++++++--------- .../foods/repositories/food.repository.ts | 2 +- src/seeders/DatabaseSeeder.ts | 56 +++++++++---------- 7 files changed, 67 insertions(+), 69 deletions(-) diff --git a/src/modules/foods/controllers/food.controller.ts b/src/modules/foods/controllers/food.controller.ts index da6c8b6..1af823c 100644 --- a/src/modules/foods/controllers/food.controller.ts +++ b/src/modules/foods/controllers/food.controller.ts @@ -64,8 +64,8 @@ export class FoodController { @ApiParam({ name: 'id', required: true }) @ApiOkResponse({ description: 'The food', type: CreateFoodDto }) @ApiNotFoundResponse({ description: 'Food not found' }) - findOne(@Param('id') id: string) { - return this.foodsService.findOne(id); + findById(@Param('id') id: string) { + return this.foodsService.findById(id); } @UseGuards(AdminAuthGuard) diff --git a/src/modules/foods/dto/create-food.dto.ts b/src/modules/foods/dto/create-food.dto.ts index 018d1ca..603346f 100644 --- a/src/modules/foods/dto/create-food.dto.ts +++ b/src/modules/foods/dto/create-food.dto.ts @@ -1,8 +1,12 @@ -import { IsBoolean, IsOptional, IsString, IsNumber, IsArray } from 'class-validator'; -import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsBoolean, IsOptional, IsString, IsNumber, IsArray, IsNotEmpty } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Type } from 'class-transformer'; export class CreateFoodDto { + @IsNotEmpty() + @IsString() + @ApiProperty({ example: '123e4567-e89b-12d3-a456-426614174000' }) + categoryId: string; @IsOptional() @IsBoolean() @ApiPropertyOptional({ example: false }) @@ -92,7 +96,7 @@ export class CreateFoodDto { @ApiPropertyOptional({ example: true }) @Type(() => Boolean) dinner?: boolean; - + @IsOptional() @IsNumber() @Type(() => Number) @@ -128,13 +132,13 @@ export class CreateFoodDto { @ApiPropertyOptional({ example: false }) @Type(() => Boolean) pickupServe?: boolean; - + @IsOptional() @IsNumber() @Type(() => Number) @ApiPropertyOptional({ example: 0 }) discount?: number; - + @IsOptional() @IsArray() @IsString({ each: true }) diff --git a/src/modules/foods/entities/category.entity.ts b/src/modules/foods/entities/category.entity.ts index 7b9f598..6999320 100644 --- a/src/modules/foods/entities/category.entity.ts +++ b/src/modules/foods/entities/category.entity.ts @@ -1,4 +1,4 @@ -import { Entity, Property, ManyToMany, Collection } from '@mikro-orm/core'; +import { Entity, Property, Collection, OneToMany } from '@mikro-orm/core'; import { Food } from './food.entity'; import { BaseEntity } from '../../../common/entities/base.entity'; @@ -7,7 +7,7 @@ export class Category extends BaseEntity { @Property() title!: string; - @ManyToMany(() => Food, food => food.categories) + @OneToMany(() => Food, food => food.category) foods = new Collection(this); @Property({ default: true }) diff --git a/src/modules/foods/entities/food.entity.ts b/src/modules/foods/entities/food.entity.ts index 64f3e8f..24cd9f6 100644 --- a/src/modules/foods/entities/food.entity.ts +++ b/src/modules/foods/entities/food.entity.ts @@ -1,4 +1,4 @@ -import { Collection, Entity, ManyToMany, ManyToOne, Property } from '@mikro-orm/core'; +import { Entity, ManyToOne, Property } from '@mikro-orm/core'; import { Category } from './category.entity'; import { BaseEntity } from '../../../common/entities/base.entity'; import { Restaurant } from '../../../modules/restaurants/entities/restaurant.entity'; @@ -8,8 +8,8 @@ export class Food extends BaseEntity { @ManyToOne(() => Restaurant) restaurant: Restaurant; - @ManyToMany(() => Category) - categories = new Collection(this); + @ManyToOne(() => Category) + category!: Category; @Property({ nullable: true }) title?: string; diff --git a/src/modules/foods/providers/food.service.ts b/src/modules/foods/providers/food.service.ts index ab76b7f..0f3fdab 100644 --- a/src/modules/foods/providers/food.service.ts +++ b/src/modules/foods/providers/food.service.ts @@ -6,7 +6,7 @@ import { CategoryRepository } from '../repositories/category.repository'; import { EntityManager } from '@mikro-orm/postgresql'; import { RequiredEntityData } from '@mikro-orm/core'; import { Food } from '../entities/food.entity'; -import { FoodMessage, RestMessage } from 'src/common/enums/message.enum'; +import { CategoryMessage, FoodMessage, RestMessage } from 'src/common/enums/message.enum'; import { RestRepository } from 'src/modules/restaurants/repositories/rest.repository'; @Injectable() @@ -19,11 +19,16 @@ export class FoodService { ) {} async create(restId: string, createFoodDto: CreateFoodDto): Promise { - const { categoryIds, ...rest } = createFoodDto; + const { categoryId, ...rest } = createFoodDto; const restaurant = await this.restRepository.findOne({ id: restId }); if (!restaurant) { throw new NotFoundException(RestMessage.NOT_FOUND); } + const category = await this.categoryRepository.findOne({ id: categoryId }); + if (!category) { + throw new NotFoundException(CategoryMessage.NOT_FOUND); + } + // prepare data with defaults for required fields so repository.create typing is satisfied const data: RequiredEntityData = { // boolean/day flags @@ -49,6 +54,7 @@ export class FoodService { prepareTime: rest.prepareTime ?? 0, images: rest.images ?? undefined, restaurant: restaurant, + category: category }; const food = this.foodRepository.create(data); @@ -56,12 +62,6 @@ export class FoodService { throw new Error('Failed to create food entity'); } - // attach categories if provided - if (categoryIds && categoryIds.length) { - const categories = await this.categoryRepository.find({ id: { $in: categoryIds } }); - categories.forEach(cat => food.categories.add(cat)); - } - await this.em.persistAndFlush(food); return food; } @@ -70,40 +70,36 @@ export class FoodService { return this.foodRepository.findAllPaginated(restId, dto); } + async findById(id: string): Promise { + const food = await this.foodRepository.findOne({ id }, { populate: ['category'] }); + if (!food) { + throw new NotFoundException(FoodMessage.NOT_FOUND); + } + return food; + } + async findAllByRestaurant(slug: string): Promise { const restaurant = await this.restRepository.findOne({ slug }); if (!restaurant) { throw new NotFoundException(RestMessage.NOT_FOUND); } - return this.foodRepository.find({ restaurant: { slug } }); - } - - findOne(id: string) { - return this.foodRepository.findOne({ id }, { populate: ['categories'] }); + return this.foodRepository.find({ restaurant: { slug }, isActive: true }, { populate: ['category'] }); } async update(id: string, dto: Partial): Promise { const food = await this.foodRepository.findOne({ id }); - if (!food) { throw new NotFoundException(FoodMessage.NOT_FOUND); } - - const { categoryIds, ...rest } = dto; - - // assign scalar fields - this.em.assign(food, rest as Partial); - // attach new categories if provided (adds, does not attempt to preserve/remove existing ones) - if (Array.isArray(categoryIds) && categoryIds.length) { - const categories = await this.categoryRepository.find({ id: { $in: categoryIds } }); + if (dto.categoryId) { + const category = await this.categoryRepository.findOne({ id: dto.categoryId }); - food.categories.removeAll(); + if (!category) { + throw new NotFoundException(CategoryMessage.NOT_FOUND); + } - categories.forEach(cat => { - // Collection.add will ignore duplicates - food.categories.add(cat); - }); + this.em.assign(food, { category: category }); } await this.em.persistAndFlush(food); diff --git a/src/modules/foods/repositories/food.repository.ts b/src/modules/foods/repositories/food.repository.ts index 57dcaeb..e8fe3e4 100644 --- a/src/modules/foods/repositories/food.repository.ts +++ b/src/modules/foods/repositories/food.repository.ts @@ -48,7 +48,7 @@ export class FoodRepository extends EntityRepository { limit, offset, orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' }, - populate: ['categories'], + populate: ['category'], }); const totalPages = Math.ceil(total / limit); diff --git a/src/seeders/DatabaseSeeder.ts b/src/seeders/DatabaseSeeder.ts index df6d091..d245c5a 100644 --- a/src/seeders/DatabaseSeeder.ts +++ b/src/seeders/DatabaseSeeder.ts @@ -271,7 +271,7 @@ export class DatabaseSeeder extends Seeder { desc: 'کباب کوبیده خوشمزه با برنج و کره', price: 150000, restaurant: zhivan, - categories: [createdCategories[0]], // غذای اصلی + category: createdCategories[0], // غذای اصلی isActive: true, stock: 50, stockDefault: 50, @@ -281,7 +281,7 @@ export class DatabaseSeeder extends Seeder { desc: 'جوجه کباب تازه با برنج', price: 120000, restaurant: zhivan, - categories: [createdCategories[0]], // غذای اصلی + category: createdCategories[0], // غذای اصلی isActive: true, stock: 40, stockDefault: 40, @@ -291,7 +291,7 @@ export class DatabaseSeeder extends Seeder { desc: 'کباب بختیاری با برنج و کره', price: 180000, restaurant: zhivan, - categories: [createdCategories[0]], // غذای اصلی + category: createdCategories[0], // غذای اصلی isActive: true, stock: 35, stockDefault: 35, @@ -301,7 +301,7 @@ export class DatabaseSeeder extends Seeder { desc: 'کباب برگ مرغوب با برنج', price: 160000, restaurant: zhivan, - categories: [createdCategories[0]], // غذای اصلی + category: createdCategories[0], // غذای اصلی isActive: true, stock: 30, stockDefault: 30, @@ -311,7 +311,7 @@ export class DatabaseSeeder extends Seeder { desc: 'سالاد تازه با سس مخصوص', price: 35000, restaurant: zhivan, - categories: [createdCategories[1]], // پیش غذا + category: createdCategories[1], // پیش غذا isActive: true, stock: 30, stockDefault: 30, @@ -321,7 +321,7 @@ export class DatabaseSeeder extends Seeder { desc: 'ماست و خیار تازه', price: 25000, restaurant: zhivan, - categories: [createdCategories[1]], // پیش غذا + category: createdCategories[1], // پیش غذا isActive: true, stock: 25, stockDefault: 25, @@ -331,7 +331,7 @@ export class DatabaseSeeder extends Seeder { desc: 'ترشی مخلوط خانگی', price: 20000, restaurant: zhivan, - categories: [createdCategories[1]], // پیش غذا + category: createdCategories[1], // پیش غذا isActive: true, stock: 20, stockDefault: 20, @@ -341,7 +341,7 @@ export class DatabaseSeeder extends Seeder { desc: 'بستنی وانیلی خوشمزه', price: 25000, restaurant: zhivan, - categories: [createdCategories[2]], // دسر + category: createdCategories[2], // دسر isActive: true, stock: 20, stockDefault: 20, @@ -351,7 +351,7 @@ export class DatabaseSeeder extends Seeder { desc: 'شیرینی محلی', price: 30000, restaurant: zhivan, - categories: [createdCategories[2]], // دسر + category: createdCategories[2], // دسر isActive: true, stock: 15, stockDefault: 15, @@ -361,7 +361,7 @@ export class DatabaseSeeder extends Seeder { desc: 'نوشابه گازدار', price: 15000, restaurant: zhivan, - categories: [createdCategories[3]], // نوشیدنی + category: createdCategories[3], // نوشیدنی isActive: true, stock: 100, stockDefault: 100, @@ -371,7 +371,7 @@ export class DatabaseSeeder extends Seeder { desc: 'دوغ محلی', price: 20000, restaurant: zhivan, - categories: [createdCategories[3]], // نوشیدنی + category: createdCategories[3], // نوشیدنی isActive: true, stock: 50, stockDefault: 50, @@ -381,7 +381,7 @@ export class DatabaseSeeder extends Seeder { desc: 'آب معدنی', price: 10000, restaurant: zhivan, - categories: [createdCategories[3]], // نوشیدنی + category: createdCategories[3], // نوشیدنی isActive: true, stock: 80, stockDefault: 80, @@ -392,7 +392,7 @@ export class DatabaseSeeder extends Seeder { desc: 'پیتزا با پنیر و قارچ', price: 180000, restaurant: boote, - categories: [createdCategories[4]], // غذای اصلی + category: createdCategories[4], // غذای اصلی isActive: true, stock: 30, stockDefault: 30, @@ -402,7 +402,7 @@ export class DatabaseSeeder extends Seeder { desc: 'پیتزا پپرونی تند', price: 200000, restaurant: boote, - categories: [createdCategories[4]], // غذای اصلی + category: createdCategories[4], // غذای اصلی isActive: true, stock: 25, stockDefault: 25, @@ -412,7 +412,7 @@ export class DatabaseSeeder extends Seeder { desc: 'برگر با سیب زمینی سرخ کرده', price: 140000, restaurant: boote, - categories: [createdCategories[4]], // غذای اصلی + category: createdCategories[4], // غذای اصلی isActive: true, stock: 25, stockDefault: 25, @@ -422,7 +422,7 @@ export class DatabaseSeeder extends Seeder { desc: 'برگر دوبل با پنیر', price: 180000, restaurant: boote, - categories: [createdCategories[4]], // غذای اصلی + category: createdCategories[4], // غذای اصلی isActive: true, stock: 20, stockDefault: 20, @@ -432,7 +432,7 @@ export class DatabaseSeeder extends Seeder { desc: 'سوپ گرم و خوشمزه', price: 45000, restaurant: boote, - categories: [createdCategories[5]], // پیش غذا + category: createdCategories[5], // پیش غذا isActive: true, stock: 15, stockDefault: 15, @@ -442,7 +442,7 @@ export class DatabaseSeeder extends Seeder { desc: 'سالاد سزار با سس مخصوص', price: 55000, restaurant: boote, - categories: [createdCategories[5]], // پیش غذا + category: createdCategories[5], // پیش غذا isActive: true, stock: 20, stockDefault: 20, @@ -452,7 +452,7 @@ export class DatabaseSeeder extends Seeder { desc: 'سیب زمینی سرخ کرده ترد', price: 40000, restaurant: boote, - categories: [createdCategories[5]], // پیش غذا + category: createdCategories[5], // پیش غذا isActive: true, stock: 30, stockDefault: 30, @@ -462,7 +462,7 @@ export class DatabaseSeeder extends Seeder { desc: 'کیک شکلاتی خامه‌ای', price: 60000, restaurant: boote, - categories: [createdCategories[6]], // دسر + category: createdCategories[6], // دسر isActive: true, stock: 10, stockDefault: 10, @@ -472,7 +472,7 @@ export class DatabaseSeeder extends Seeder { desc: 'چیزکیک وانیلی', price: 65000, restaurant: boote, - categories: [createdCategories[6]], // دسر + category: createdCategories[6], // دسر isActive: true, stock: 12, stockDefault: 12, @@ -482,7 +482,7 @@ export class DatabaseSeeder extends Seeder { desc: 'نوشابه گازدار', price: 15000, restaurant: boote, - categories: [createdCategories[7]], // نوشیدنی + category: createdCategories[7], // نوشیدنی isActive: true, stock: 100, stockDefault: 100, @@ -492,7 +492,7 @@ export class DatabaseSeeder extends Seeder { desc: 'آبمیوه تازه', price: 35000, restaurant: boote, - categories: [createdCategories[7]], // نوشیدنی + category: createdCategories[7], // نوشیدنی isActive: true, stock: 40, stockDefault: 40, @@ -502,7 +502,7 @@ export class DatabaseSeeder extends Seeder { desc: 'قهوه اسپرسو', price: 45000, restaurant: boote, - categories: [createdCategories[7]], // نوشیدنی + category: createdCategories[7], // نوشیدنی isActive: true, stock: 50, stockDefault: 50, @@ -510,7 +510,7 @@ export class DatabaseSeeder extends Seeder { ]; for (const foodData of foods) { - if (foodData.restaurant) { + if (foodData.restaurant && foodData.category) { const existing = await em.findOne(Food, { title: foodData.title, restaurant: foodData.restaurant, @@ -521,6 +521,7 @@ export class DatabaseSeeder extends Seeder { desc: foodData.desc, price: foodData.price, restaurant: foodData.restaurant, + category: foodData.category, isActive: foodData.isActive, stock: foodData.stock, stockDefault: foodData.stockDefault, @@ -535,10 +536,7 @@ export class DatabaseSeeder extends Seeder { inPlaceServe: false, pickupServe: false, }); - // Add categories - if (foodData.categories) { - foodData.categories.forEach(cat => food.categories.add(cat)); - } + em.persist(food); } }