This commit is contained in:
2025-11-15 08:41:35 +03:30
parent 15c0c508b9
commit 0f1407c748
23 changed files with 714 additions and 177 deletions
@@ -0,0 +1,70 @@
import { Controller, Get, Post, Body, Patch, Param, Delete, Query } from '@nestjs/common';
import { FoodService } from '../providers/food.service';
import { CreateFoodDto } from '../dto/create-food.dto';
import { UpdateFoodDto } from '../dto/update-food.dto';
import { FindFoodsDto } from '../dto/find-foods.dto';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiCreatedResponse,
ApiOkResponse,
ApiNotFoundResponse,
ApiQuery,
ApiBody,
ApiParam,
} from '@nestjs/swagger';
@ApiTags('foods')
@Controller('foods')
export class CategoryController {
constructor(private readonly foodService: FoodService) {}
@Post()
@ApiOperation({ summary: 'Create a new food' })
@ApiCreatedResponse({ description: 'The food has been successfully created.', type: CreateFoodDto })
@ApiBody({ type: CreateFoodDto })
create(@Body() createFoodDto: CreateFoodDto) {
return this.foodService.create(createFoodDto);
}
@Get()
@ApiOperation({ summary: 'Get a paginated list of foods' })
@ApiOkResponse({ description: 'Paginated list of foods' })
@ApiQuery({ name: 'page', required: false, type: Number })
@ApiQuery({ name: 'limit', required: false, type: Number })
@ApiQuery({ name: 'search', required: false, type: String })
@ApiQuery({ name: 'orderBy', required: false, type: String })
@ApiQuery({ name: 'order', required: false, enum: ['asc', 'desc'] })
@ApiQuery({ name: 'categoryId', required: false, type: String })
@ApiQuery({ name: 'isActive', required: false, type: Boolean })
findAll(@Query() dto: FindFoodsDto) {
return this.foodService.findAll(dto);
}
@Get(':id')
@ApiOperation({ summary: 'Get a food by id' })
@ApiParam({ name: 'id', required: true })
@ApiOkResponse({ description: 'The food', type: CreateFoodDto })
@ApiNotFoundResponse({ description: 'Food not found' })
findOne(@Param('id') id: string) {
return this.foodService.findOne(id);
}
@Patch(':id')
@ApiOperation({ summary: 'Update a food' })
@ApiParam({ name: 'id', required: true })
@ApiBody({ type: UpdateFoodDto })
@ApiOkResponse({ description: 'The updated food', type: UpdateFoodDto })
update(@Param('id') id: string, @Body() updateFoodDto: UpdateFoodDto) {
return this.foodService.update(id, updateFoodDto);
}
@Delete(':id')
@ApiOperation({ summary: 'Delete (soft) a food' })
@ApiParam({ name: 'id', required: true })
@ApiResponse({ status: 200, description: 'Food deleted' })
remove(@Param('id') id: string) {
return this.foodService.remove(id);
}
}
@@ -0,0 +1,65 @@
import { Controller, Get, Post, Body, Patch, Param, Delete, Query } from '@nestjs/common';
import { CategoryService } from '../providers/category.service';
import { CreateCategoryDto } from '../dto/create-category.dto';
import { UpdateCategoryDto } from '../dto/update-category.dto';
import {
ApiTags,
ApiOperation,
ApiCreatedResponse,
ApiOkResponse,
ApiNotFoundResponse,
ApiParam,
ApiBody,
ApiQuery,
} from '@nestjs/swagger';
@ApiTags('categories')
@Controller('categories')
export class CategorysController {
constructor(private readonly categoryService: CategoryService) {}
@Post()
@ApiOperation({ summary: 'Create category' })
@ApiCreatedResponse({ description: 'Category created', type: CreateCategoryDto })
@ApiBody({ type: CreateCategoryDto })
create(@Body() dto: CreateCategoryDto) {
return this.categoryService.create(dto);
}
@Get()
@ApiOperation({ summary: 'List categories' })
@ApiOkResponse({ description: 'List of categories' })
@ApiQuery({ name: 'restId', required: false, type: String })
@ApiQuery({ name: 'isActive', required: false, type: Boolean })
findAll(@Query('restId') restId?: string, @Query('isActive') isActive?: string) {
// convert isActive from query string to boolean when provided
const isActiveBool: boolean | undefined = typeof isActive !== 'undefined' ? isActive === 'true' : undefined;
return this.categoryService.findAll({ restId, isActive: isActiveBool });
}
@Get(':id')
@ApiOperation({ summary: 'Get category' })
@ApiParam({ name: 'id' })
@ApiOkResponse({ description: 'Category detail', type: CreateCategoryDto })
@ApiNotFoundResponse({ description: 'Category not found' })
findOne(@Param('id') id: string) {
return this.categoryService.findOne(id);
}
@Patch(':id')
@ApiOperation({ summary: 'Update category' })
@ApiParam({ name: 'id' })
@ApiBody({ type: UpdateCategoryDto })
@ApiOkResponse({ description: 'Updated category', type: UpdateCategoryDto })
update(@Param('id') id: string, @Body() dto: UpdateCategoryDto) {
return this.categoryService.update(id, dto);
}
@Delete(':id')
@ApiOperation({ summary: 'Delete category' })
@ApiParam({ name: 'id' })
@ApiOkResponse({ description: 'Deleted' })
remove(@Param('id') id: string) {
return this.categoryService.remove(id);
}
}
@@ -0,0 +1,70 @@
import { Controller, Get, Post, Body, Patch, Param, Delete, Query } from '@nestjs/common';
import { FoodService } from '../providers/food.service';
import { CreateFoodDto } from '../dto/create-food.dto';
import { UpdateFoodDto } from '../dto/update-food.dto';
import { FindFoodsDto } from '../dto/find-foods.dto';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiCreatedResponse,
ApiOkResponse,
ApiNotFoundResponse,
ApiQuery,
ApiBody,
ApiParam,
} from '@nestjs/swagger';
@ApiTags('foods')
@Controller('foods')
export class FoodController {
constructor(private readonly foodsService: FoodService) {}
@Post()
@ApiOperation({ summary: 'Create a new food' })
@ApiCreatedResponse({ description: 'The food has been successfully created.', type: CreateFoodDto })
@ApiBody({ type: CreateFoodDto })
create(@Body() createFoodDto: CreateFoodDto) {
return this.foodsService.create(createFoodDto);
}
@Get()
@ApiOperation({ summary: 'Get a paginated list of foods' })
@ApiOkResponse({ description: 'Paginated list of foods' })
@ApiQuery({ name: 'page', required: false, type: Number })
@ApiQuery({ name: 'limit', required: false, type: Number })
@ApiQuery({ name: 'search', required: false, type: String })
@ApiQuery({ name: 'orderBy', required: false, type: String })
@ApiQuery({ name: 'order', required: false, enum: ['asc', 'desc'] })
@ApiQuery({ name: 'categoryId', required: false, type: String })
@ApiQuery({ name: 'isActive', required: false, type: Boolean })
findAll(@Query() dto: FindFoodsDto) {
return this.foodsService.findAll(dto);
}
@Get(':id')
@ApiOperation({ summary: 'Get a food by id' })
@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);
}
@Patch(':id')
@ApiOperation({ summary: 'Update a food' })
@ApiParam({ name: 'id', required: true })
@ApiBody({ type: UpdateFoodDto })
@ApiOkResponse({ description: 'The updated food', type: UpdateFoodDto })
update(@Param('id') id: string, @Body() updateFoodDto: UpdateFoodDto) {
return this.foodsService.update(id, updateFoodDto);
}
@Delete(':id')
@ApiOperation({ summary: 'Delete (soft) a food' })
@ApiParam({ name: 'id', required: true })
@ApiResponse({ status: 200, description: 'Food deleted' })
remove(@Param('id') id: string) {
return this.foodsService.remove(id);
}
}
@@ -0,0 +1,26 @@
import { IsString, IsOptional, IsBoolean } from 'class-validator';
import { ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
export class CreateCategoryDto {
@IsOptional()
@IsString()
@ApiPropertyOptional({ example: 'Beverages' })
title?: string;
@IsOptional()
@IsBoolean()
@Type(() => Boolean)
@ApiPropertyOptional({ example: true })
isActive?: boolean;
@IsOptional()
@IsString()
@ApiPropertyOptional({ example: 'restaurant-ulid' })
restId?: string;
@IsOptional()
@IsString()
@ApiPropertyOptional({ example: 'https://cdn.example.com/avatar.png' })
avatarUrl?: string;
}
+53 -8
View File
@@ -1,87 +1,132 @@
import { IsBoolean, IsOptional, IsString, IsNumber, IsArray } from 'class-validator';
import { ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
export class CreateFoodDto {
@IsOptional()
@IsBoolean()
@ApiPropertyOptional({ example: false })
@Type(() => Boolean)
breakfast?: boolean;
@IsOptional()
@IsBoolean()
@ApiPropertyOptional({ example: false })
@Type(() => Boolean)
tue?: boolean;
@IsOptional()
@IsBoolean()
@ApiPropertyOptional({ example: false })
@Type(() => Boolean)
wed?: boolean;
@IsOptional()
@IsBoolean()
@ApiPropertyOptional({ example: false })
@Type(() => Boolean)
thu?: boolean;
@IsOptional()
@IsBoolean()
@ApiPropertyOptional({ example: false })
@Type(() => Boolean)
fri?: boolean;
@IsOptional()
@IsString()
titleFa?: string;
@ApiPropertyOptional({ example: 'قرمه سبزی' })
title?: string;
@IsOptional()
@IsString()
titleEn?: string;
@IsOptional()
@IsString()
contentFa?: string;
@IsOptional()
@IsString()
contentEn?: string;
@ApiPropertyOptional({ example: 'توضیحات غذا' })
content?: string;
@IsOptional()
@IsNumber()
@Type(() => Number)
@ApiPropertyOptional({ example: 120000 })
price?: number;
@IsOptional()
@IsNumber()
@Type(() => Number)
@ApiPropertyOptional({ example: 10 })
points?: number;
@IsOptional()
@IsNumber()
@Type(() => Number)
@ApiPropertyOptional({ example: 15 })
prepareTime?: number;
@IsOptional()
@IsBoolean()
@ApiPropertyOptional({ example: true })
@Type(() => Boolean)
sat?: boolean;
@IsOptional()
@IsBoolean()
@ApiPropertyOptional({ example: true })
@Type(() => Boolean)
sun?: boolean;
@IsOptional()
@IsBoolean()
@ApiPropertyOptional({ example: true })
@Type(() => Boolean)
mon?: boolean;
@IsOptional()
@IsBoolean()
@ApiPropertyOptional({ example: false })
@Type(() => Boolean)
noon?: boolean;
@IsOptional()
@IsBoolean()
@ApiPropertyOptional({ example: true })
@Type(() => Boolean)
dinner?: boolean;
@IsOptional()
@IsBoolean()
@ApiPropertyOptional({ example: false })
@Type(() => Boolean)
isPickup?: boolean;
@IsOptional()
@IsNumber()
@Type(() => Number)
@ApiPropertyOptional({ example: 10 })
stock?: number;
@IsOptional()
@IsNumber()
@Type(() => Number)
@ApiPropertyOptional({ example: 10 })
stockDefault?: number;
@IsOptional()
@IsBoolean()
@ApiPropertyOptional({ example: true })
@Type(() => Boolean)
isActive?: boolean;
@IsOptional()
@IsArray()
@IsString({ each: true })
@ApiPropertyOptional({ type: [String] })
images?: string[];
@IsOptional()
@IsBoolean()
@ApiPropertyOptional({ example: false })
@Type(() => Boolean)
inPlaceServe?: boolean;
@IsOptional()
@IsBoolean()
@ApiPropertyOptional({ example: false })
@Type(() => Boolean)
pickupServe?: boolean;
@IsOptional()
@IsNumber()
@Type(() => Number)
@ApiPropertyOptional({ example: 4.5 })
rate?: number;
@IsOptional()
@IsNumber()
@Type(() => Number)
@ApiPropertyOptional({ example: 0 })
discount?: number;
@IsOptional()
@IsArray()
@IsString({ each: true })
@ApiPropertyOptional({ type: [String] })
categoryIds?: string[];
}
+43
View File
@@ -0,0 +1,43 @@
import { IsOptional, IsNumber, IsString, IsIn, IsBoolean } from 'class-validator';
import { ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
export class FindFoodsDto {
@IsOptional()
@IsNumber()
@Type(() => Number)
@ApiPropertyOptional({ example: 1 })
page?: number;
@IsOptional()
@IsNumber()
@Type(() => Number)
@ApiPropertyOptional({ example: 10 })
limit?: number;
@IsOptional()
@IsString()
@ApiPropertyOptional({ example: 'cheese' })
search?: string;
@IsOptional()
@IsString()
@ApiPropertyOptional({ example: 'createdAt' })
orderBy?: string;
@IsOptional()
@IsIn(['asc', 'desc'])
@ApiPropertyOptional({ example: 'desc' })
order?: 'asc' | 'desc';
@IsOptional()
@IsString()
@ApiPropertyOptional({ example: 'category-ulid' })
categoryId?: string;
@IsOptional()
@IsBoolean()
@Type(() => Boolean)
@ApiPropertyOptional({ example: true })
isActive?: boolean;
}
@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateCategoryDto } from './create-category.dto';
export class UpdateCategoryDto extends PartialType(CreateCategoryDto) {}
@@ -9,4 +9,13 @@ export class Category extends BaseEntity {
@ManyToMany(() => Foods, food => food.categories)
foods = new Collection<Foods>(this);
@Property({ default: true })
isActive: boolean = true;
@Property({ nullable: true })
restId?: string;
@Property({ nullable: true })
avatarUrl?: string;
}
+6 -9
View File
@@ -1,5 +1,5 @@
import { Collection, Entity, ManyToMany, Property } from '@mikro-orm/core';
import { Category } from './category';
import { Category } from './category.entity';
import { BaseEntity } from 'src/common/entities/base.entity';
@Entity({ tableName: 'foods' })
@@ -8,16 +8,10 @@ export class Foods extends BaseEntity {
categories = new Collection<Category>(this);
@Property({ nullable: true })
titleFa?: string;
@Property({ nullable: true })
titleEn?: string;
title?: string;
@Property({ type: 'text', nullable: true })
contentFa?: string;
@Property({ type: 'text', nullable: true })
contentEn?: string;
content?: string;
@Property({ type: 'decimal', precision: 10, scale: 2, nullable: true })
price?: number;
@@ -25,6 +19,9 @@ export class Foods extends BaseEntity {
@Property({ type: 'int', nullable: true })
points?: number;
@Property({ type: 'int', nullable: true })
order?: number;
@Property({ type: 'int', nullable: true })
prepareTime?: number; // in minutes
+11
View File
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { FoodService } from './providers/food.service';
import { FoodController } from './controllers/food.controller';
import { CategorysController } from './controllers/categorys.controller';
import { CategoryService } from './providers/category.service';
@Module({
controllers: [FoodController, CategorysController],
providers: [FoodService, CategoryService],
})
export class FoodModule {}
-34
View File
@@ -1,34 +0,0 @@
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
import { FoodsService } from './providers/foods.service';
import { CreateFoodDto } from './dto/create-food.dto';
import { UpdateFoodDto } from './dto/update-food.dto';
@Controller('foods')
export class FoodsController {
constructor(private readonly foodsService: FoodsService) {}
@Post()
create(@Body() createFoodDto: CreateFoodDto) {
return this.foodsService.create(createFoodDto);
}
@Get()
findAll() {
return this.foodsService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.foodsService.findOne(+id);
}
@Patch(':id')
update(@Param('id') id: string, @Body() updateFoodDto: UpdateFoodDto) {
return this.foodsService.update(+id, updateFoodDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.foodsService.remove(+id);
}
}
-9
View File
@@ -1,9 +0,0 @@
import { Module } from '@nestjs/common';
import { FoodsService } from './providers/foods.service';
import { FoodsController } from './foods.controller';
@Module({
controllers: [FoodsController],
providers: [FoodsService],
})
export class FoodsModule {}
@@ -0,0 +1,57 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { CreateCategoryDto } from '../dto/create-category.dto';
import { UpdateCategoryDto } from '../dto/update-category.dto';
import { CategoryRepository } from '../repositories/category.repository';
import { EntityManager } from '@mikro-orm/postgresql';
import { RequiredEntityData, FilterQuery } from '@mikro-orm/core';
import { Category } from '../entities/category.entity';
import { CategoryMessage } from 'src/common/enums/message.enum';
@Injectable()
export class CategoryService {
constructor(
private readonly categoryRepository: CategoryRepository,
private readonly em: EntityManager,
) {}
async create(dto: CreateCategoryDto): Promise<Category> {
const data = {
title: dto.title,
isActive: dto.isActive ?? true,
restId: dto.restId,
avatarUrl: dto.avatarUrl,
} as RequiredEntityData<Category>;
const category = this.categoryRepository.create(data);
await this.em.persistAndFlush(category);
return category;
}
async findAll(opts?: { restId?: string; isActive?: boolean }): Promise<Category[]> {
const where: FilterQuery<Category> = {};
if (opts?.restId) where.restId = opts.restId;
if (opts && typeof opts.isActive === 'boolean') where.isActive = opts.isActive;
return this.categoryRepository.find(where, { populate: ['foods'] });
}
async findOne(id: string): Promise<Category> {
const cat = await this.categoryRepository.findOne({ id }, { populate: ['foods'] });
if (!cat) throw new NotFoundException(CategoryMessage.NOT_FOUND);
return cat;
}
async update(id: string, dto: UpdateCategoryDto): Promise<Category> {
const cat = await this.categoryRepository.findOne({ id });
if (!cat) throw new NotFoundException(CategoryMessage.NOT_FOUND);
this.em.assign(cat, dto as Partial<Category>);
await this.em.persistAndFlush(cat);
return cat;
}
async remove(id: string): Promise<void> {
const cat = await this.categoryRepository.findOne({ id });
if (!cat) throw new NotFoundException(CategoryMessage.NOT_FOUND);
cat.deletedAt = new Date();
await this.em.persistAndFlush(cat);
}
}
+103
View File
@@ -0,0 +1,103 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { CreateFoodDto } from '../dto/create-food.dto';
import { FindFoodsDto } from '../dto/find-foods.dto';
import { FoodRepository } from '../repositories/food.repository';
import { CategoryRepository } from '../repositories/category.repository';
import { EntityManager } from '@mikro-orm/postgresql';
import { RequiredEntityData } from '@mikro-orm/core';
import { Foods } from '../entities/food.entity';
import { FoodMessage } from 'src/common/enums/message.enum';
@Injectable()
export class FoodService {
constructor(
private readonly foodRepository: FoodRepository,
private readonly categoryRepository: CategoryRepository,
private readonly em: EntityManager,
) {}
async create(createFoodDto: CreateFoodDto): Promise<Foods> {
const { categoryIds, ...rest } = createFoodDto;
// prepare data with defaults for required fields so repository.create typing is satisfied
const data: RequiredEntityData<Foods> = {
// boolean/day flags
sat: rest.sat ?? false,
sun: rest.sun ?? false,
mon: rest.mon ?? false,
breakfast: rest.breakfast ?? false,
noon: rest.noon ?? false,
dinner: rest.dinner ?? false,
// pickup/stock
isPickup: rest.isPickup ?? false,
stock: rest.stock ?? 0,
stockDefault: rest.stockDefault ?? 0,
isActive: rest.isActive ?? true,
inPlaceServe: rest.inPlaceServe ?? false,
pickupServe: rest.pickupServe ?? false,
rate: rest.rate ?? 0,
discount: rest.discount ?? 0,
// map single-title/content DTO to localized fields
titleFa: rest.title,
contentFa: rest.content,
// numeric/array fields
price: rest.price ?? 0,
points: rest.points ?? 0,
prepareTime: rest.prepareTime ?? 0,
images: rest.images ?? undefined,
} as RequiredEntityData<Foods>;
const food = this.foodRepository.create(data);
// 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;
}
findAll(dto: FindFoodsDto) {
return this.foodRepository.findAllPaginated(dto);
}
findOne(id: string) {
return this.foodRepository.findOne({ id }, { populate: ['categories'] });
}
async update(id: string, dto: Partial<CreateFoodDto>): Promise<Foods> {
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<Foods>);
// 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 } });
categories.forEach(cat => {
// Collection.add will ignore duplicates
food.categories.add(cat);
});
}
await this.em.persistAndFlush(food);
return food;
}
async remove(id: string) {
const food = await this.foodRepository.findOne({ id });
if (!food) {
throw new NotFoundException(FoodMessage.NOT_FOUND);
}
food.deletedAt = new Date();
await this.em.persistAndFlush(food);
}
}
@@ -1,48 +0,0 @@
import { Injectable } from '@nestjs/common';
import { CreateFoodDto } from '../dto/create-food.dto';
import { FoodRepository } from '../repositories/food.repository';
import { CategoryRepository } from '../repositories/category.repository';
import { EntityManager } from '@mikro-orm/postgresql';
import { Foods } from '../entities/food.entity';
@Injectable()
export class FoodsService {
constructor(
private readonly foodRepository: FoodRepository,
private readonly categoryRepository: CategoryRepository,
private readonly em: EntityManager,
) {}
async create(createFoodDto: CreateFoodDto): Promise<Foods> {
const { categoryIds, ...rest } = createFoodDto;
// create an empty food entity and assign dto properties to avoid strict typing issues
const food = new Foods();
this.em.assign(food, rest as Partial<Foods>);
// 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;
}
findAll() {
return `This action returns all foods`;
}
findOne(id: number) {
return `This action returns a #${id} food`;
}
update(id: number) {
return `This action updates a #${id} food`;
}
remove(id: number) {
return `This action removes a #${id} food`;
}
}
@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
import { Category } from '../entities/category';
import { Category } from '../entities/category.entity';
@Injectable()
export class CategoryRepository extends EntityRepository<Category> {
@@ -1,10 +1,66 @@
import { Injectable } from '@nestjs/common';
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
import { FilterQuery } from '@mikro-orm/core';
import { Foods } from '../entities/food.entity';
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
type FindFoodsOpts = {
page?: number;
limit?: number;
search?: string;
orderBy?: string;
order?: 'asc' | 'desc';
categoryId?: string;
isActive?: boolean;
};
@Injectable()
export class FoodRepository extends EntityRepository<Foods> {
constructor(readonly em: EntityManager) {
super(em, Foods);
}
/**
* Find foods with pagination and optional filters.
* Supports: search (title/content), categoryId, isActive, ordering.
*/
async findAllPaginated(opts: FindFoodsOpts = {}): Promise<PaginatedResult<Foods>> {
const { page = 1, limit = 10, search, orderBy = 'createdAt', order = 'desc', categoryId, isActive } = opts;
const offset = (page - 1) * limit;
const where: FilterQuery<Foods> = {};
if (typeof isActive === 'boolean') {
where.isActive = isActive;
}
if (search) {
const pattern = `%${search}%`;
where.$or = [{ title: { $ilike: pattern } }, { content: { $ilike: pattern } }];
}
if (categoryId) {
// filter by related categories (typed via FilterQuery)
Object.assign(where, { categories: { id: categoryId } } as unknown as FilterQuery<Foods>);
}
const [data, total] = await this.findAndCount(where, {
limit,
offset,
orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' },
populate: ['categories'],
});
const totalPages = Math.ceil(total / limit);
return {
data,
meta: {
total,
page,
limit,
totalPages,
},
};
}
}