This commit is contained in:
mahyargdz
2025-09-14 15:15:03 +03:30
commit be82059172
534 changed files with 51310 additions and 0 deletions
@@ -0,0 +1,40 @@
import { Expose } from "class-transformer";
import { IsNotEmpty, IsOptional } from "class-validator";
import { ApiProperty } from "../../../common/decorator/swggerDocs";
import { IsValidId } from "../../../common/decorator/validation.decorator";
import { CategoryModel } from "../models/category.model";
export class CreateCategoryDTO {
@Expose()
@IsNotEmpty()
@ApiProperty({ type: "string", description: "the title_fa of category", example: "موبایل" })
title_fa: string;
@Expose()
@IsNotEmpty()
@ApiProperty({ type: "string", description: "the title_en of category", example: "mobile" })
title_en: string;
@Expose()
@IsNotEmpty()
@ApiProperty({ type: "string", description: "the name of icon", example: "mobile" })
icon: string;
@Expose()
@IsNotEmpty()
@ApiProperty({ type: "string", description: "the image url of category ", example: "https://cdn.com" })
imageUrl: string;
@Expose()
@IsNotEmpty()
@ApiProperty({ type: "string", description: "the description of category", example: " موبایل و لوازم جانبی" })
description: string;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsValidId(CategoryModel)
@ApiProperty({ type: "string", description: "the id of parent category", example: "6103e3a9f9b1b5b27cd7c8e9" })
parent?: string;
}
@@ -0,0 +1,65 @@
import { Expose } from "class-transformer";
import { IsEnum, IsNotEmpty, IsOptional, IsString, Length } from "class-validator";
import { ApiProperty } from "../../../common/decorator/swggerDocs";
import { IsValidId } from "../../../common/decorator/validation.decorator";
import { CategoryThemeEnum } from "../../../common/enums/category.enum";
import { CategoryModel } from "../models/category.model";
export class UpdateCategoryDTO {
@Expose()
@IsNotEmpty()
@IsString()
@Length(24, 24)
@IsValidId(CategoryModel)
@ApiProperty({ type: "string", description: "cat id", example: "66f3bcaee566db722a044c62" })
catId: string;
@Expose()
@IsOptional()
@IsNotEmpty()
@ApiProperty({ type: "string", description: "the title_fa of category", example: "موبایل" })
title_fa: string;
@Expose()
@IsOptional()
@IsNotEmpty()
@ApiProperty({ type: "string", description: "the title_en of category", example: "mobile" })
title_en: string;
@Expose()
@IsOptional()
@IsNotEmpty()
@ApiProperty({ type: "string", description: "the name of icon", example: "mobile" })
icon: string;
@Expose()
@IsOptional()
@IsNotEmpty()
@ApiProperty({ type: "string", description: "the description of category", example: " موبایل و لوازم جانبی" })
description: string;
@Expose()
@IsOptional()
@IsNotEmpty()
@ApiProperty({ type: "string", description: "the image url of category ", example: "https://cdn.com" })
imageUrl: string;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsValidId(CategoryModel)
@ApiProperty({ type: "string", description: "the id of parent category", example: "6103e3a9f9b1b5b27cd7c8e9" })
parent?: string;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsEnum(CategoryThemeEnum)
@ApiProperty({
type: "string",
description: "the variation theme of a category ==> should be one of Color/Size/Meterage or noColor_noSize",
example: "Color",
})
theme: CategoryThemeEnum;
}
@@ -0,0 +1,78 @@
import { Expose } from "class-transformer";
import { IsBoolean, IsInt, IsOptional, IsString, Max, Min } from "class-validator";
import { PaginationDTO } from "../../../common/dto/pagination.dto";
export enum categoryFetchType {
Panel = "panel",
Front = "front",
}
export class CategorySearchQueryDTO {
@Expose()
@IsOptional()
@IsInt()
@Max(50)
limit?: number;
@Expose()
@IsOptional()
@IsInt()
@Min(1)
page?: number;
@Expose()
@IsOptional()
@IsBoolean()
stock?: boolean;
@Expose()
@IsOptional()
@IsInt()
maxPrice?: number;
@Expose()
@IsOptional()
@IsInt()
minPrice?: number;
@Expose()
@IsOptional()
@IsString()
brand?: string;
@Expose()
@IsOptional()
@IsBoolean()
wholeSale?: boolean;
@Expose()
@IsOptional()
@IsInt()
@Min(1)
@Max(7)
sort?: number;
}
export class SellerCategorySearchDTO {
@Expose()
@IsOptional()
@IsString()
// @MinLength(1)
q: string;
@Expose()
@IsOptional()
parentId: string;
}
export class CategoryQueryDto extends PaginationDTO {
@Expose()
@IsOptional()
@IsString()
// @MinLength(1)
q: string;
@Expose()
@IsOptional()
type: categoryFetchType;
}
+207
View File
@@ -0,0 +1,207 @@
import { Expose, Transform, Type, plainToInstance } from "class-transformer";
import { ICategory } from "../models/Abstraction/ICategory";
import { ICategoryAttribute } from "../models/Abstraction/ICategoryAttributes";
export class Size_MeterageDTO {
@Expose()
_id: number;
@Expose()
value: string;
}
export class ColorDTO {
@Expose()
_id: number;
@Expose()
name: string;
@Expose()
hexColor: string;
}
export class VariantDTO {
@Expose()
_id: string;
@Expose()
@Type(() => ColorDTO)
@Transform(({ value }) => (value && value.length > 0 ? value : undefined) /*, { toClassOnly: true }*/)
colors: ColorDTO[];
@Expose()
@Type(() => Size_MeterageDTO)
@Transform(({ value }) => (value && value.length > 0 ? value : undefined) /*, { toClassOnly: true }*/)
sizes: Size_MeterageDTO[];
@Expose()
@Type(() => Size_MeterageDTO)
@Transform(({ value }) => (value && value.length > 0 ? value : undefined) /*, { toClassOnly: true }*/)
meterages: Size_MeterageDTO[];
}
export class CategoryVariantDTO {
@Expose()
_id: string;
@Expose()
title_fa: string;
@Expose()
title_en: string;
@Expose()
theme: string;
@Expose()
icon: string;
@Expose()
imageUrl: string;
@Expose()
description: string;
@Expose()
leaf: boolean;
@Expose()
@Type(() => VariantDTO)
categoryVariants: VariantDTO;
public static transformCategory(data: ICategory): CategoryVariantDTO {
const CategoryDTO = plainToInstance(CategoryVariantDTO, data, {
excludeExtraneousValues: true,
enableImplicitConversion: true, //this is for transform the value to the exact type that defined for property
});
return CategoryDTO;
}
}
export class CategoryPath {
@Expose()
_id: string;
@Expose()
title_fa: string;
@Expose()
title_en: string;
}
export class CategoryTreeDTO {
@Expose()
// @Transform(({ obj }) => obj._id.toString())
_id: string;
@Expose()
title_fa: string;
@Expose()
title_en: string;
@Expose()
icon: string;
// @Expose()
// @Type(() => CategoryPath)
// hierarchy: CategoryPath[];
@Expose()
imageUrl: string;
@Expose()
theme: string;
@Expose()
description: string;
@Expose()
leaf: boolean;
@Expose()
@Type(() => CategoryPath)
path: CategoryPath[];
@Expose()
parent: string; // parent can be null too but class transformer convert it to null automatically
public static transformCategory(data: ICategory): CategoryTreeDTO {
const transformed = plainToInstance(CategoryTreeDTO, data, {
excludeExtraneousValues: true,
enableImplicitConversion: true,
});
// transformed._id = data._id instanceof ObjectId ? data._id.toString() : data._id;
// transformed.parent = data.parent instanceof ObjectId ? data.parent.toString() : data.parent;
return transformed;
}
}
class AttributeValuesDTO {
@Expose()
_id: number;
@Expose()
text: string;
}
class AttributeDTO {
@Expose()
_id: number;
@Expose()
title: string;
@Expose()
type: string;
@Expose()
multiple: boolean;
@Expose()
hint: string;
@Expose()
required: boolean;
@Expose()
@Type(() => AttributeValuesDTO)
values: AttributeValuesDTO[];
}
export class CategoryAttributeDTO {
@Expose()
_id: number;
@Expose()
category_title_fa: string;
@Expose()
@Type(() => AttributeDTO)
attributes: AttributeDTO[];
public static transformAttribute(data: ICategoryAttribute): CategoryAttributeDTO {
const transformed = plainToInstance(CategoryAttributeDTO, data, {
excludeExtraneousValues: true,
enableImplicitConversion: true,
});
// transformed._id = data._id instanceof ObjectId ? data._id.toString() : data._id;
// transformed.parent = data.parent instanceof ObjectId ? data.parent.toString() : data.parent;
return transformed;
}
}
// @Transform((value) => {
// console.log(value);
// if (!value.obj.parent) return value.obj.parent;
// return value.obj.parent.toString();
// })
// CategoryDTO._id = data._id instanceof ObjectId ? data._id.toString() : data._id;
// CategoryDTO.attributes._id = data.attributes._id instanceof ObjectId ? data.attributes._id.toString() : data.attributes._id;
// @Transform(({ obj }) => obj._id.toString())
// _id: string;
@@ -0,0 +1,58 @@
import { Expose, Type } from "class-transformer";
import { ArrayMinSize, IsArray, IsBoolean, IsEnum, IsNotEmpty, IsOptional, IsString, ValidateNested } from "class-validator";
import { ApiProperty } from "../../../common/decorator/swggerDocs";
import { categoryAttType } from "../../../common/enums/category.enum";
class AttributeValue {
@Expose()
@IsNotEmpty()
@IsString()
@ApiProperty({ type: "string", description: "Text of the attribute value", example: "ابریشم" })
text: string;
}
export class CreateCategoryAttDTO {
@Expose()
@IsNotEmpty()
@ApiProperty({ type: "string", description: "Title of the attribute", example: "جنس" })
title: string;
@Expose()
@IsNotEmpty()
@IsEnum(categoryAttType)
@ApiProperty({ type: "string", description: "Type of the attribute", example: "checkbox" })
type: categoryAttType;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsBoolean()
@ApiProperty({ type: "boolean", description: "if this be true then attribute can be selected many times", example: false })
multiple: boolean;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsString()
@ApiProperty({ type: "string", description: "hint of the attribute", example: "نوع پارچه" })
hint: string;
@Expose()
@IsNotEmpty()
@IsBoolean()
@ApiProperty({ type: "boolean", description: "Specifies if the attribute is required", example: true })
required: boolean;
@Expose()
@IsNotEmpty()
@IsArray()
@ArrayMinSize(1)
@ValidateNested({ each: true })
@Type(() => AttributeValue)
@ApiProperty({
type: "Array",
description: "If the attribute allows multiple values (e.g., checkbox or select), provide the values here",
example: [{ text: "ابریشم" }, { text: "اسفنجی" }],
})
values: AttributeValue[];
}
@@ -0,0 +1,83 @@
import { PickType } from "@nestjs/mapped-types";
import { Expose, Type } from "class-transformer";
import { ArrayMinSize, IsEnum, IsHexColor, IsNotEmpty, IsOptional, ValidateNested } from "class-validator";
import { ApiProperty } from "../../../common/decorator/swggerDocs";
import { CategoryThemeEnum } from "../../../common/enums/category.enum";
export class CreateColorDTO {
@Expose()
@IsNotEmpty()
name: string;
@Expose()
@IsNotEmpty()
@IsHexColor()
hexColor: string;
}
export class CreateSizeDTO {
@Expose()
@IsNotEmpty()
value: string;
}
export class CreateMeterageDTO {
@Expose()
@IsNotEmpty()
value: string;
}
export class CreateCategoryThemeDTO {
@Expose()
@IsNotEmpty()
@IsEnum(CategoryThemeEnum)
@ApiProperty({
type: "string",
description: "the variation theme of a category ==> should be one of Color/Size/Meterage or noColor_noSize",
example: "Color",
})
theme: CategoryThemeEnum;
@Expose()
@IsOptional()
@IsNotEmpty()
@ArrayMinSize(1)
@ValidateNested({ each: true })
@Type(() => CreateColorDTO)
@ApiProperty({
type: "Array",
required: true,
description: "the variation colors of a category theme",
example: [{ name: "مشکی", hexColor: "#000000" }],
})
colors?: CreateColorDTO[];
@Expose()
@IsOptional()
@IsNotEmpty()
@ArrayMinSize(1)
@ValidateNested({ each: true })
@Type(() => CreateSizeDTO)
@ApiProperty({
type: "Array",
description: "the variation sizes of a category theme",
example: [{ value: "M" }, { value: "S" }, { value: "L" }],
})
sizes?: CreateSizeDTO[];
@Expose()
@IsOptional()
@IsNotEmpty()
@ArrayMinSize(1)
@ValidateNested({ each: true })
@Type(() => CreateSizeDTO)
@ApiProperty({
type: "Array",
description: "the variation meterage of a category theme",
example: [{ value: "10" }, { value: "20" }, { value: "50" }],
})
meterages?: CreateMeterageDTO[];
}
export class UpdateCategoryVariantDTO extends PickType(CreateCategoryThemeDTO, ["colors", "sizes", "meterages"] as const) {}
+127
View File
@@ -0,0 +1,127 @@
import { Request } from "express";
import { inject } from "inversify";
import { controller, httpGet, httpPost, queryParam, request, requestParam } from "inversify-express-utils";
import { CategoryService } from "./category.service";
import { HttpStatus } from "../../common";
import { CategoryQueryDto, CategorySearchQueryDTO, SellerCategorySearchDTO, categoryFetchType } from "./DTO/category-sarch.dto";
import { BaseController } from "../../common/base/controller";
import { ApiAuth, ApiFile, ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags } from "../../common/decorator/swggerDocs";
import { Guard } from "../../core/middlewares/guard.middleware";
import { ValidationMiddleware } from "../../core/middlewares/validator.middleware";
import { IOCTYPES } from "../../IOC/ioc.types";
import { UploadService } from "../../utils/upload.service";
import { PermissionEnum } from "../admin/models/Abstraction/IPermission";
import { IUser } from "../user/models/Abstraction/IUser";
@controller("/category")
@ApiTags("Category")
class CategoryController extends BaseController {
@inject(IOCTYPES.CategoryService) categoryService: CategoryService;
//#######################################################
//#######################################################
@ApiOperation("Get all categories")
@ApiResponse("get all categories", HttpStatus.Ok)
@ApiQuery("limit", "the limit of return data")
@ApiQuery("page", "the page want to get")
@ApiQuery("q", "search based on category name")
@ApiQuery("type", "specify to add pagination for category if its panel or front ==> panel | front")
@httpGet("")
public async getAll(@queryParam() queryDto: CategoryQueryDto) {
if (queryDto.type === categoryFetchType.Panel) {
const { count, category } = await this.categoryService.getCategoryForPanel(queryDto);
const { pager } = this.paginate(count);
return this.response({ category, pager });
}
const data = await this.categoryService.getCategories();
return this.response({ data });
}
@ApiOperation("Get all categories (tree like)")
@ApiResponse("successful", HttpStatus.Ok)
@ApiQuery("parentId", "parent id of a category")
@httpGet("/tree", ValidationMiddleware.validateQuery(SellerCategorySearchDTO))
public async getAllCategoryTree(@queryParam() queryDto: SellerCategorySearchDTO) {
const data = await this.categoryService.getCategoriesTree(queryDto);
return this.response({ data });
}
@ApiOperation("Get a category with id")
@ApiResponse("get a category", HttpStatus.Ok)
@ApiParam("id", "id of the category", true)
@httpGet("/:id")
public async getById(@requestParam("id") id: string) {
const data = await this.categoryService.getCategoryById(id);
return this.response({ data });
}
@ApiOperation("get all variant of a category with its id ")
@ApiResponse("successful", HttpStatus.Ok)
@ApiParam("id", "id of category", true)
@httpGet("/:id/variants")
public async getCategoryVariants(@requestParam("id") catId: string) {
const data = await this.categoryService.getCategoryVariantsS(catId);
return this.response({ data });
}
@ApiOperation("get all attribute of a category with its id ")
@ApiResponse("successful", HttpStatus.Ok)
@ApiParam("id", "id of category", true)
@httpGet("/:id/attributes")
public async getCategoryAttribute(@requestParam("id") catId: string) {
const data = await this.categoryService.getCategoryAttributeS(catId);
return this.response({ data }, HttpStatus.Ok);
}
@ApiOperation("get products of a category")
@ApiResponse("successful", HttpStatus.Ok)
@ApiParam("name", "name of category", true)
@ApiQuery("limit", "the limit of return data")
@ApiQuery("page", "the page want to get")
@ApiQuery("stock", "search based on product stock")
@ApiQuery("maxPrice", "search based on product priceRange")
@ApiQuery("minPrice", "search based on product priceRange")
@ApiQuery("brand", "search based on product brand id")
@ApiQuery("wholeSale", "search based on product wholeSale if its enable")
@ApiQuery("sort", "sort option should be int from 1 to 7")
@httpGet("/:name/search", Guard.authOptional(), ValidationMiddleware.validateQuery(CategorySearchQueryDTO))
@ApiAuth()
public async getProductsOfCategory(
@requestParam("name") name: string,
@queryParam() searchQueryDto: CategorySearchQueryDTO,
@request() req: Request,
) {
const user = req.user as IUser;
const { count, ...data } = await this.categoryService.getProductsOfCategory(name, searchQueryDto, user?._id?.toString());
const { pager } = this.paginate(count);
return this.response({ ...data, pager });
}
//#########################################uploader
@ApiOperation("Upload a category image ==> need to login as admin")
@ApiResponse("File uploaded successfully", HttpStatus.Accepted)
@ApiFile("image")
@ApiAuth()
@httpPost(
"/image/upload",
Guard.authAdmin(),
Guard.checkAdminPermissions([PermissionEnum.ADMIN]),
UploadService.single("image", "category-images"),
)
public async uploadImage(@request() req: Request) {
// eslint-disable-next-line no-undef
const file = req.file as Express.MulterS3.File;
const data = {
message: "file uploaded!",
url: {
size: file?.size,
url: file?.location,
type: file?.mimetype,
},
};
return this.response({ data }, HttpStatus.Accepted);
}
}
export { CategoryController };
+541
View File
@@ -0,0 +1,541 @@
import { Types } from "mongoose";
import { SellerCategorySearchDTO } from "./DTO/category-sarch.dto";
import { IAttributeValue } from "./models/Abstraction/IAttributeValue";
import { ICategory } from "./models/Abstraction/ICategory";
import { ICategoryAttribute } from "./models/Abstraction/ICategoryAttributes";
import { IColor } from "./models/Abstraction/IColor";
import { IMeterage } from "./models/Abstraction/IMeterage";
import { ISize } from "./models/Abstraction/ISize";
import { AttributeValueModel } from "./models/attributeValue.model";
import { CategoryModel } from "./models/category.model";
import { CategoryAttributeModel } from "./models/CategoryAttribute.model";
import { ColorModel } from "./models/color.model";
import { MeterageModel } from "./models/meterage.model";
import { SizeModel } from "./models/size.model";
import { BaseRepository } from "../../common/base/repository";
import { BadRequestError } from "../../core/app/app.errors";
const { ObjectId } = Types;
class CategoryRepository extends BaseRepository<ICategory> {
constructor() {
super(CategoryModel);
}
async findByTitle(title_en: string, id?: string) {
const query: Record<string, any> = { title_en };
if (id) {
query._id = { $ne: id };
}
return this.model.findOne(query);
}
// async getCategoryTree(parentId: string) {
// const categories = await this.model.aggregate([
// {
// $match: {
// parent: parentId ? new ObjectId(parentId) : null,
// deleted: false,
// },
// },
// ]);
// for (const category of categories) {
// category.path = await this.getCategoryPath(category._id);
// }
// return categories;
// }
async getCategoryTree(queryDto: SellerCategorySearchDTO) {
const matchConditions: Record<string, any> = {
deleted: false,
};
if (queryDto.q) {
matchConditions.$or = [{ title_en: { $regex: queryDto.q, $options: "i" } }, { title_fa: { $regex: queryDto.q, $options: "i" } }];
} else {
matchConditions.parent = queryDto.parentId ? new ObjectId(queryDto.parentId) : null;
}
const categories = await this.model.aggregate([
{
$match: matchConditions,
},
{
$lookup: {
from: "categories",
foreignField: "_id",
localField: "hierarchy",
as: "path",
},
},
{
$project: {
path: {
icon: 0,
imageUrl: 0,
hierarchy: 0,
description: 0,
theme: 0,
variants: 0,
leaf: 0,
parent: 0,
url: 0,
children: 0,
deleted: 0,
__v: 0,
},
children: 0,
variants: 0,
__v: 0,
},
},
]);
console.dir(categories, { depth: null });
// for (const category of categories) {
// category.path = await this.getCategoryPath(category._id);
// }
return categories;
}
//#######################################################
async getCategoryTree2(): Promise<ICategory[]> {
return this.model.aggregate([
{
$match: { parent: null },
},
{
$graphLookup: {
from: "categories",
startWith: "$_id",
connectFromField: "_id",
connectToField: "parent",
as: "children",
depthField: "level",
},
},
{
$addFields: {
// url: "/category/$title_en",
},
},
{
$project: {
__v: 0,
updatedAt: 0,
"children.__v": 0,
},
},
{
$sort: {
level: 1,
},
},
]);
}
/** get category variant */
async getCategoryVariants(categoryIds: string[]) {
// Step 1: Fetch all categories by IDs
const categories = await this.model
.find({ _id: { $in: categoryIds.map((id) => new Types.ObjectId(id)) } }, { _id: 1, theme: 1, variants: 1, title_en: 1 })
.lean();
// Create a map to store themes and their corresponding unique variant IDs
const themeVariantsMap = new Map<string, Set<string>>();
// Step 2: Populate the map with themes and variant IDs
for (const category of categories) {
if (!category?.theme || !category?.variants.length) continue;
const theme = `${category?.theme}s`.toLowerCase(); // e.g., 'colors', 'sizes', 'meterages'
if (!themeVariantsMap.has(theme)) {
themeVariantsMap.set(theme, new Set());
}
// Add variant IDs to the set for the specific theme
category.variants.forEach((variantId) => {
themeVariantsMap.get(theme)?.add(variantId.toString());
});
}
// Step 3: Prepare a result object to store fetched variant data
const result: Record<string, any[]> = {};
// Step 4: Perform a single lookup per theme
for (const [theme, variantIdsSet] of themeVariantsMap.entries()) {
const variantIds = Array.from(variantIdsSet).map((id) => +id);
if (variantIds.length > 0) {
let variantsData;
// Fetch the variants from the corresponding theme collection
if (theme === "colors") variantsData = await ColorModel.find({ _id: { $in: variantIds } }, { __v: 0 }).lean();
if (theme === "sizes") variantsData = await SizeModel.find({ _id: { $in: variantIds } }, { __v: 0 }).lean();
if (theme === "meterages") variantsData = await MeterageModel.find({ _id: { $in: variantIds } }, { __v: 0 }).lean();
// throw new Error("theme not valid");
// Store the result for this theme
result[theme] = variantsData as any;
}
}
return result;
}
async getSingleCategoryVariant(catId: string) {
// const category = await this.model.findById(catId).populate("variants").lean();
// if (!category) {
// throw new Error("Category not found");
// }
// return category;
const category = await this.model.findById(catId).lean();
if (!category || !category.theme) throw new BadRequestError("category has no theme");
const theme = `${category?.theme}s`.toLowerCase();
// Ensure the theme is valid
const allowedThemes = ["colors", "sizes", "meterages"];
if (!allowedThemes.includes(theme)) {
throw new BadRequestError(`category has ${theme}`);
}
const result = await this.model.aggregate([
{
$match: {
_id: new ObjectId(catId),
},
},
{
$lookup: {
from: theme,
localField: "variants",
foreignField: "_id",
as: "variantData",
},
},
{
$addFields: {
categoryVariants: {
[theme]: "$variantData",
},
},
},
{
$project: {
variantData: 0,
variants: 0,
categoryVariants: {
[theme]: {
__v: 0,
},
},
},
},
]);
return result[0];
}
/** get category attributes */
async getCategoryAttributes(categoryIds: string[]): Promise<any | null> {
const attributes = await this.model.aggregate([
{
$match: {
_id: { $in: categoryIds.map((id) => new Types.ObjectId(id)) },
},
},
{
$lookup: {
from: "categoryattributes",
localField: "_id",
foreignField: "category",
as: "attributes",
},
},
{
$unwind: "$attributes",
},
{
$lookup: {
from: "attributevalues",
localField: "attributes._id",
foreignField: "attribute",
as: "attributes.values",
},
},
{
$group: {
_id: null, // Use `null` to group all categories together
category_title_fa: { $addToSet: "$title_fa" },
attributes: {
$push: {
_id: "$attributes._id",
title: "$attributes.title",
hint: "$attributes.hint",
type: "$attributes.type",
multiple: "$attributes.multiple",
required: "$attributes.required",
values: "$attributes.values",
},
},
},
},
{
$project: {
_id: 0, // Remove the grouping _id
category_title_fa: 1,
attributes: 1,
},
},
]);
return attributes.length ? attributes[0] : null;
}
/** find with name */
async findWithName(name: string) {
return this.model.findOne(
{ title_en: name },
{ _id: 1, title_fa: 1, title_en: 1, description: 1, icon: 1, theme: 1, children: 1, url: 1 },
);
}
async getCategoryPath(catId: string) {
const category = await this.model.findById(catId).lean();
if (!category) return null;
const path = [{ id: category._id, title_fa: category.title_fa, title_en: category.title_en }]; // Add the current category to the path
let parent = category.parent;
while (parent) {
const parentCategory = await this.model.findById(parent).lean();
if (!parentCategory) break;
path.unshift({ id: parentCategory._id, title_fa: parentCategory.title_fa, title_en: parentCategory.title_en });
parent = parentCategory.parent;
}
return path;
}
}
function createCategoryRepo(): CategoryRepository {
return new CategoryRepository();
}
//=====================>category att repo
class CategoryAttributeRepo extends BaseRepository<ICategoryAttribute> {
constructor() {
super(CategoryAttributeModel);
}
async findByCategoryId(catId: string) {
return this.model.findOne({ category: catId });
}
}
function createCategoryAttributeRepo(): CategoryAttributeRepo {
return new CategoryAttributeRepo();
}
//=============================> attribute value repo
class AttributeValueRepo extends BaseRepository<IAttributeValue> {
constructor() {
super(AttributeValueModel);
}
async findByText(text: string) {
return this.model.findOne({ text });
}
}
function createAttributeValueRepo(): AttributeValueRepo {
return new AttributeValueRepo();
}
//=====================>size model repository
class SizeRepository extends BaseRepository<ISize> {
constructor() {
super(SizeModel);
}
}
function createSizeRepo(): SizeRepository {
return new SizeRepository();
}
//=====================>color model repo
class ColorRepository extends BaseRepository<IColor> {
constructor() {
super(ColorModel);
}
}
function createColorRepo(): ColorRepository {
return new ColorRepository();
}
//=====================>meterage model repo
class MeterageRepository extends BaseRepository<IMeterage> {
constructor() {
super(MeterageModel);
}
}
function createMeterageRepo(): MeterageRepository {
return new MeterageRepository();
}
export {
createCategoryRepo,
createCategoryAttributeRepo,
createSizeRepo,
createMeterageRepo,
createColorRepo,
createAttributeValueRepo,
AttributeValueRepo,
CategoryRepository,
CategoryAttributeRepo,
SizeRepository,
ColorRepository,
MeterageRepository,
};
// async getCategoryVariant(catId: string) {
// return this.model.aggregate([
// {
// $match: {
// _id: new ObjectId(catId),
// },
// },
// {
// $lookup: {
// from: "colors",
// localField: "variants",
// foreignField: "_id",
// as: "colors",
// },
// },
// {
// $lookup: {
// from: "sizes",
// localField: "variants",
// foreignField: "_id",
// as: "sizes",
// },
// },
// {
// $lookup: {
// from: "meterages",
// localField: "variants",
// foreignField: "_id",
// as: "meterages",
// },
// },
// {
// $addFields: {
// variants: {
// $cond: {
// if: { $gt: [{ $size: "$colors" }, 0] },
// then: { colors: "$colors" },
// else: {},
// },
// },
// },
// },
// {
// $addFields: {
// "variants.sizes": {
// $cond: {
// if: { $gt: [{ $size: "$sizes" }, 0] },
// then: "$sizes",
// else: "$$REMOVE",
// },
// },
// "variants.meterages": {
// $cond: {
// if: { $gt: [{ $size: "$meterages" }, 0] },
// then: "$meterages",
// else: "$$REMOVE",
// },
// },
// },
// },
// {
// $project: {
// colors: 0,
// sizes: 0,
// meterages: 0,
// },
// },
// ]);
// return this.model.aggregate([
// {
// $match: {
// _id: new ObjectId(catId),
// },
// },
// {
// $lookup: {
// from: "colors",
// localField: "variants",
// foreignField: "_id",
// as: "colors",
// },
// },
// {
// $lookup: {
// from: "sizes",
// localField: "variants",
// foreignField: "_id",
// as: "sizes",
// },
// },
// {
// $lookup: {
// from: "meterages",
// localField: "variants",
// foreignField: "_id",
// as: "meterages",
// },
// },
// {
// $addFields: {
// variants: {},
// },
// },
// {
// $addFields: {
// "variants.colors": "$colors",
// "variants.sizes": "$sizes",
// "variants.meterages": "$meterages",
// },
// },
// // {
// // $unwind: "$variants",
// // },
// {
// $project: {
// colors: 0,
// sizes: 0,
// meterages: 0,
// // "variants.colors": { $cond: { if: { $ne: ["$variants.colors", null] }, then: "$variants.colors", else: "$$REMOVE" } },
// // "variants.sizes": { $cond: { if: { $ne: ["$variants.sizes", null] }, then: "$variants.sizes", else: "$$REMOVE" } },
// // "variants.meterages": { $cond: { if: { $ne: ["$variants.meterages", null] }, then: "$variants.meterages", else: "$$REMOVE" } },
// },
// },
// // {
// // $addFields: {
// // "variants.colors": {
// // $cond: { if: { $eq: [{ $size: "$variants.colors" }, 0] }, then: null, else: "$variants.colors" },
// // },
// // "variants.sizes": {
// // $cond: { if: { $eq: [{ $size: "$variants.sizes" }, 0] }, then: null, else: "$variants.sizes" },
// // },
// // "variants.meterages": {
// // $cond: { if: { $eq: [{ $size: "$variants.meterages" }, 0] }, then: null, else: "$variants.meterages" },
// // },
// // },
// // },
// ]);
// }
+481
View File
@@ -0,0 +1,481 @@
import { inject, injectable } from "inversify";
import { ClientSession, FilterQuery, isValidObjectId, startSession } from "mongoose";
import slugify from "slugify";
import {
AttributeValueRepo,
CategoryAttributeRepo,
CategoryRepository,
ColorRepository,
MeterageRepository,
SizeRepository,
} from "./category.repository";
import { BrandRepository } from "../brand/brand.repository";
import { CategoryQueryDto, CategorySearchQueryDTO, SellerCategorySearchDTO } from "./DTO/category-sarch.dto";
import { CategoryAttributeDTO, CategoryTreeDTO, CategoryVariantDTO } from "./DTO/category.dto";
import { CreateCategoryDTO } from "./DTO/CreateCategory.dto";
import { CreateCategoryAttDTO } from "./DTO/createCategoryAtt.dto";
import {
CreateCategoryThemeDTO,
CreateColorDTO,
CreateMeterageDTO,
CreateSizeDTO,
UpdateCategoryVariantDTO,
} from "./DTO/createCategoryTheme.dto";
import { UpdateCategoryDTO } from "./DTO/UpdateCategory.dto";
import { IColor } from "./models/Abstraction/IColor";
import { IMeterage } from "./models/Abstraction/IMeterage";
import { ISize } from "./models/Abstraction/ISize";
import { BaseRepository } from "../../common/base/repository";
import { CategoryThemeEnum } from "../../common/enums/category.enum";
import { CategoryMessage, CommonMessage } from "../../common/enums/message.enum";
import { BadRequestError, ForbiddenError } from "../../core/app/app.errors";
import { IOCTYPES } from "../../IOC/ioc.types";
import { ProductDTO } from "../product/DTO/product.dto";
import { IProduct } from "../product/models/Abstraction/IProduct";
import { ProductRepository } from "../product/Repository/product";
import { ICategory } from "./models/Abstraction/ICategory";
import { paginationUtils } from "../../utils/pagination.utils";
@injectable()
class CategoryService {
@inject(IOCTYPES.CategoryRepository) categoryRepository: CategoryRepository;
@inject(IOCTYPES.ProductRepository) productRepo: ProductRepository;
@inject(IOCTYPES.BrandRepository) brandRepo: BrandRepository;
@inject(IOCTYPES.CategoryAttributeRepository) categoryAttRepo: CategoryAttributeRepo;
@inject(IOCTYPES.AttributeValueRepository) attributeValueRepo: AttributeValueRepo;
@inject(IOCTYPES.ColorRepository) colorRepo: ColorRepository;
@inject(IOCTYPES.SizeRepository) sizeRepo: SizeRepository;
@inject(IOCTYPES.MeterageRepository) meterageRepo: MeterageRepository;
async getCategories() {
return this.categoryRepository.model.find({ parent: null });
}
//################################
async getCategoryForPanel(queryDto: CategoryQueryDto) {
const { limit, skip } = paginationUtils(queryDto);
const searchQuery: FilterQuery<ICategory> = {
parent: null,
deleted: false,
};
if (queryDto.q) {
searchQuery.$or = [{ title_en: { $regex: queryDto.q, $options: "i" } }, { title_fa: { $regex: queryDto.q, $options: "i" } }];
}
const count = await this.categoryRepository.model.countDocuments(searchQuery);
const category = await this.categoryRepository.model.find(searchQuery).skip(skip).limit(limit);
return { count, category };
}
//################################
async getCategoriesTree(queryDto: SellerCategorySearchDTO) {
return this.categoryRepository.getCategoryTree(queryDto);
// const categories = docs.map((doc) => CategoryTreeDTO.transformCategory(doc));
// return categories;
}
//################################
// async searchCategory(queryDto: SellerCategorySearchDTO) {
// const docs = await this.categoryRepository.searchCategory(queryDto.q);
// const categories = docs.map((doc) => CategoryTreeDTO.transformCategory(doc));
// return {
// categories,
// };
// }
async getCategoryById(id: string) {
if (!isValidObjectId(id)) throw new BadRequestError(CommonMessage.NotValidId);
return this.categoryRepository.model.findOne({ _id: id, deleted: false });
}
//################################
async updateCategory(updateDto: UpdateCategoryDTO) {
const category = await this.checkCategoryId(updateDto.catId);
if (updateDto.title_en) {
updateDto.title_en = slugify(updateDto.title_en);
}
const existTitle = await this.categoryRepository.findByTitle(updateDto.title_en, category._id.toString());
if (existTitle) throw new BadRequestError(CategoryMessage.DuplicateTitle);
//TODO: check if parent is valid for update theme
if (updateDto.theme) {
await this.categoryRepository.model.findOneAndUpdate({ _id: category._id.toString() }, { theme: updateDto.theme, variants: [] });
}
if (updateDto.parent) {
const updatedCategory = await this.categoryRepository.model.findByIdAndUpdate(
category._id.toString(),
{ ...updateDto, leaf: false },
{ new: true },
);
return {
message: CategoryMessage.Updated,
updatedCategory,
};
}
const updatedCategory = await this.categoryRepository.model.findByIdAndUpdate(category._id.toString(), updateDto, { new: true });
return {
message: CategoryMessage.Updated,
updatedCategory,
};
}
//################################
async createCategory(createDto: CreateCategoryDTO) {
createDto.title_en = slugify(createDto.title_en);
const existTitle = await this.categoryRepository.findByTitle(createDto.title_en);
if (existTitle) throw new BadRequestError(CategoryMessage.DuplicateTitle);
//if parent exist first set parent leaf false
if (createDto.parent) {
await this.categoryRepository.model.findByIdAndUpdate(createDto.parent, { leaf: false });
}
//we set the leaf to true
const category = await this.categoryRepository.model.create(createDto);
return {
message: CategoryMessage.Created,
category,
};
}
//################################
async deleteCategory(categoryId: string) {
const category = await this.checkCategoryId(categoryId);
const existChild = await this.categoryRepository.model.exists({ parent: category._id.toString() });
if (existChild) throw new BadRequestError(CategoryMessage.CategoryIsParent);
const productsWithCategory = await this.productRepo.model.find(
{
category: category._id.toString(),
},
{ _id: 1, title_fa: 1 },
);
if (productsWithCategory.length > 0) {
const productsName = productsWithCategory.map((product) => product.title_en);
throw new BadRequestError([`${CategoryMessage.CategoryHasProduct} ${productsName.join(" ")}`]);
}
await this.categoryRepository.model.findByIdAndUpdate(category._id.toString(), {
deleted: true,
});
return {
message: CategoryMessage.Deleted,
};
}
//################################
async updateCategoryVariantS(updateDto: UpdateCategoryVariantDTO, id: string) {
const session = await startSession();
session.startTransaction();
try {
const { colors, sizes, meterages } = updateDto;
const category = await this.checkCategoryId(id);
// Ensure the theme matches the provided attributes and create variants
const variantRefs = await this.createVariantsBasedOnTheme(session, category.theme, colors, sizes, meterages);
// Update the category with the theme and variants
category.variants = variantRefs;
await category.save({ session });
await session.commitTransaction();
return {
message: CommonMessage.Updated,
category,
};
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
await session.endSession();
}
}
//################################
async createCategoryThemeS(createCategoryThemeDto: CreateCategoryThemeDTO, catId: string) {
const session = await startSession();
session.startTransaction();
try {
const { theme, colors, sizes, meterages } = createCategoryThemeDto;
// Validate category ID
const category = await this.checkCategoryId(catId);
// Check if the theme already exists
if (category.theme) throw new BadRequestError(CategoryMessage.AlreadyExist);
// Ensure the theme matches the provided attributes and create variants
const variantRefs = await this.createVariantsBasedOnTheme(session, theme, colors, sizes, meterages);
// Update the category with the theme and variants
category.theme = theme;
category.variants = variantRefs;
await category.save({ session });
// Commit the transaction
await session.commitTransaction();
session.endSession();
return {
message: CategoryMessage.VariantCreated,
variant: category,
};
} catch (error) {
// Rollback any changes made in the transaction
await session.abortTransaction();
session.endSession();
throw error; // Re-throw the error to be handled by the calling errorHandler
}
}
async createCategoryAttributeS(createAttributeDto: CreateCategoryAttDTO, categoryId: string) {
const session = await startSession();
session.startTransaction();
try {
const { title, type, multiple, required, values, hint } = createAttributeDto;
// Validate the existence of the category
await this.checkCategoryId(categoryId);
// Check if attribute already exists for the category
const existAttribute = await this.categoryAttRepo.model.exists({ category: categoryId, title });
if (existAttribute) {
throw new BadRequestError(CategoryMessage.AttAlreadyExist);
}
// Create the category attribute
const categoryAttribute = await this.categoryAttRepo.model.create(
[
{
category: categoryId,
title,
type,
multiple,
hint,
required,
},
],
{ session },
);
for (const value of values) {
await this.attributeValueRepo.model.create([{ text: value.text, attribute: categoryAttribute[0]._id }], { session });
}
// Commit the transaction
await session.commitTransaction();
session.endSession();
return {
message: CategoryMessage.AttributeCreated,
attribute: categoryAttribute[0],
};
} catch (error) {
// Rollback any changes made in the transaction
await session.abortTransaction();
session.endSession();
throw error; // Re-throw the error to be handled by the calling errorHandler
}
}
async getCategoryAttributeS(catId: string) {
await this.checkCategoryId(catId);
const docs = await this.categoryRepository.getCategoryAttributes([catId]);
if (!docs) throw new ForbiddenError(CategoryMessage.CategoryNotValidToChose);
const attribute = CategoryAttributeDTO.transformAttribute(docs);
return {
category: attribute,
};
}
async deleteCategoryAttributeS(attId: string) {
const existAttribute = await this.categoryAttRepo.model.findById(attId);
if (!existAttribute) throw new BadRequestError(CategoryMessage.NotValidId);
await this.categoryAttRepo.model.findByIdAndDelete(attId);
return {
message: CategoryMessage.AttributeDeleted,
};
}
async deleteCategoryVariantS(catId: string, varId: string) {
const category = await this.checkCategoryId(catId);
category.variants = category.variants.filter((variant) => variant.toString() !== varId);
await category.save();
return {
message: CategoryMessage.AttributeDeleted,
};
}
async getCategoryVariantsS(catId: string) {
await this.checkCategoryId(catId);
const docs = await this.categoryRepository.getSingleCategoryVariant(catId);
// const category = await this.categoryRepository.model.findById(catId).populate({
// path: "attributes",
// populate: {
// path: "variants",
// },
// });
if (!docs) throw new ForbiddenError(CategoryMessage.CategoryNotValidToChose);
const category = CategoryVariantDTO.transformCategory(docs);
return {
category,
};
}
async getProductsOfCategory(name: string, queries: CategorySearchQueryDTO, userId?: string) {
const category = await this.categoryRepository.findWithName(name);
if (!category) throw new BadRequestError(CategoryMessage.NotFound);
const categoryIds = await this.getAllCategoryIds(category._id.toString());
// get related attributes, price range, and brands
const catAttributes = await this.categoryRepository.getCategoryAttributes(categoryIds);
const priceRange = await this.productRepo.getPriceRange(categoryIds);
const brands = await this.brandRepo.model.find({ category: { $in: categoryIds } });
// get category variants
const categoryVariants = await this.categoryRepository.getCategoryVariants(categoryIds);
const colors = categoryVariants?.colors;
const sizes = categoryVariants?.sizes;
const meterages = categoryVariants?.meterages;
// fetch products from the category and its children
const { docs, count } = await this.productRepo.getProductsWithCategory(categoryIds, queries, userId);
const products = docs.map((doc: IProduct) => ProductDTO.transformProduct(doc));
const filters = { attributes: catAttributes?.attributes, priceRange, colors, sizes, meterages };
// fetch category breadcrumb
const breadcrumb = await this.getCategoryBreadcrumb(category._id.toString());
return { category, brands, products, filters, count, breadcrumb };
}
//=========================================>helper method
// Helper method to get category breadcrumb
private async getCategoryBreadcrumb(categoryId: string): Promise<CategoryTreeDTO[]> {
const breadcrumb: CategoryTreeDTO[] = [];
let currentCategory = await this.categoryRepository.findById(categoryId);
while (currentCategory) {
breadcrumb.unshift(CategoryTreeDTO.transformCategory(currentCategory));
if (currentCategory.parent) {
currentCategory = await this.categoryRepository.findById(currentCategory.parent.toString());
} else {
currentCategory = null;
}
}
return breadcrumb;
}
// Helper function to get all category IDs, including children
private async getAllCategoryIds(rootCategoryId: string): Promise<string[]> {
const categoriesToProcess = [rootCategoryId];
const allCategoryIds = new Set<string>([rootCategoryId]);
while (categoriesToProcess.length > 0) {
const currentCategoryId = categoriesToProcess.pop();
const currentCategory = await this.categoryRepository.findById(currentCategoryId as string);
if (currentCategory && currentCategory.children && currentCategory.children.length > 0) {
for (const child of currentCategory.children) {
if (!allCategoryIds.has(child._id.toString())) {
allCategoryIds.add(child._id.toString());
categoriesToProcess.push(child._id.toString());
}
}
}
}
return Array.from(allCategoryIds);
}
// Helper method to check the category ID validity
private async checkCategoryId(catId: string) {
//check if id is valid
if (!isValidObjectId(catId)) throw new BadRequestError(CommonMessage.NotValidId);
//check if category exist in database
const existCategory = await this.categoryRepository.findById(catId);
if (!existCategory) throw new BadRequestError(CategoryMessage.NotValidId);
return existCategory;
}
//Helper method to Create variants based on the provided theme
public async createVariantsBasedOnTheme(
session: ClientSession,
theme: CategoryThemeEnum,
colors?: CreateColorDTO[],
sizes?: CreateSizeDTO[],
meterages?: CreateMeterageDTO[],
): Promise<number[]> {
const variantRefs: number[] = [];
if (theme === CategoryThemeEnum.Colored) {
if (colors && colors.length > 0) {
await this.processVariants(this.colorRepo, "name", colors, session, variantRefs);
} else {
throw new BadRequestError(CategoryMessage.MissingColors);
}
} else if (theme === CategoryThemeEnum.Sized) {
if (sizes && sizes.length > 0) {
await this.processVariants(this.sizeRepo, "value", sizes, session, variantRefs);
} else {
throw new BadRequestError(CategoryMessage.MissingSizes);
}
} else if (theme === CategoryThemeEnum.Meterage) {
if (meterages && meterages.length > 0) {
await this.processVariants(this.meterageRepo, "value", meterages, session, variantRefs);
} else {
throw new BadRequestError(CategoryMessage.MissingMeterages);
}
} else if (theme === CategoryThemeEnum.No_color_No_sized) {
return [];
} else {
throw new BadRequestError(CategoryMessage.InvalidTheme);
}
return variantRefs;
}
// Generic function to process create variants
private async processVariants<V extends IColor | ISize | IMeterage, T>(
repo: BaseRepository<V>,
field: keyof T,
items: T[],
session: ClientSession,
variantRefs: number[],
) {
for (const item of items) {
const query = { [field]: item[field] } as FilterQuery<V>;
console.log("query", query);
const all = await repo.model.find();
const existingItem = await repo.model.exists({ ...query });
console.log(existingItem);
console.log("all", all);
if (existingItem) {
variantRefs.push(existingItem._id);
} else {
console.log(item);
const newItem = await repo.model.create([{ ...item }], { session });
variantRefs.push(newItem[0]._id);
await session.commitTransaction();
}
}
}
}
export { CategoryService };
@@ -0,0 +1,5 @@
export interface IAttributeValue {
_id: number;
attribute: number;
text: string;
}
@@ -0,0 +1,21 @@
import { Types } from "mongoose";
import { CategoryThemeEnum } from "../../../../common/enums/category.enum";
export interface ICategory {
_id: Types.ObjectId;
title_fa: string;
title_en: string;
icon: string;
imageUrl: string;
description: string;
theme: CategoryThemeEnum;
variants: number[];
leaf: boolean;
parent: Types.ObjectId;
url: string;
children: ICategory[];
deleted: boolean;
hierarchy: Types.ObjectId[];
// attributes: Types.ObjectId;
}
@@ -0,0 +1,16 @@
import { Types } from "mongoose";
import { categoryAttType } from "../../../../common/enums/category.enum";
export interface ICategoryAttribute {
_id: number;
category: Types.ObjectId;
title: string;
hint: string;
type: categoryAttType;
multiple: boolean;
required: boolean;
// value: number;
// values: number[];
// divisions: string[];
}
@@ -0,0 +1,5 @@
export interface IColor {
_id: number;
name: string;
hexColor: string;
}
@@ -0,0 +1,4 @@
export interface IMeterage {
_id: number;
value: string;
}
@@ -0,0 +1,4 @@
export interface ISize {
_id: number;
value: string;
}
@@ -0,0 +1,33 @@
import mongoose, { Schema, model } from "mongoose";
import { ICategoryAttribute } from "./Abstraction/ICategoryAttributes";
import { AttributeValueModel } from "./attributeValue.model";
import { categoryAttType } from "../../../common/enums/category.enum";
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
const AutoIncrement = require("mongoose-sequence")(mongoose);
const CategoryAttributesSchema = new Schema<ICategoryAttribute>(
{
_id: Number,
category: { type: Schema.Types.ObjectId, ref: "Category", required: true },
title: { type: String, required: true },
hint: { type: String, default: null },
type: { type: String, enum: categoryAttType, required: true },
multiple: { type: Boolean, default: false },
required: { type: Boolean, required: true },
// value: { type: Number, ref: "AttributeValue", default: null }, // Single value
// values: [{ type: Number, ref: "AttributeValue" }], // Multiple values
},
{ timestamps: true, toJSON: { virtuals: true, versionKey: false }, id: false },
);
CategoryAttributesSchema.plugin(AutoIncrement, { id: "catAtt_id", inc_field: "_id" });
CategoryAttributesSchema.post(["deleteOne", "findOneAndDelete"], async function (doc, next) {
await AttributeValueModel.deleteMany({ attribute: doc._id });
next();
});
const CategoryAttributeModel = model<ICategoryAttribute>("CategoryAttribute", CategoryAttributesSchema);
export { CategoryAttributeModel };
@@ -0,0 +1,19 @@
import mongoose, { Schema, model } from "mongoose";
import { IAttributeValue } from "./Abstraction/IAttributeValue";
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
const AutoIncrement = require("mongoose-sequence")(mongoose);
const AttributeValueSchema = new Schema<IAttributeValue>(
{
_id: Number,
attribute: { type: Number, ref: "CategoryAttribute", required: true },
text: { type: String, required: true },
},
{ timestamps: true },
);
AttributeValueSchema.plugin(AutoIncrement, { id: "attValue_id", inc_field: "_id" });
const AttributeValueModel = model<IAttributeValue>("AttributeValue", AttributeValueSchema);
export { AttributeValueModel };
@@ -0,0 +1,64 @@
import { CallbackError, Schema, model } from "mongoose";
import { ICategory } from "./Abstraction/ICategory";
import { CategoryThemeEnum } from "../../../common/enums/category.enum";
const categorySchema = new Schema<ICategory>(
{
title_fa: { type: String, required: true },
title_en: { type: String, unique: true, required: true },
icon: { type: String, default: "/images/icons/default.png" },
imageUrl: { type: String, required: true },
description: { type: String, required: true },
theme: { type: String, enum: CategoryThemeEnum },
variants: [{ type: Number, refPath: "theme" }],
hierarchy: [{ type: Schema.Types.ObjectId, ref: "Category" }],
leaf: { type: Boolean, default: true },
parent: { type: Schema.Types.ObjectId, ref: "Category", default: null },
deleted: { type: Boolean, default: false },
// variants: { type: [Number], refPath: "theme" },
// attributes: { type: Schema.Types.ObjectId, ref: "CategoryAttribute" },
},
{ timestamps: true, toJSON: { virtuals: true, versionKey: false }, id: false },
);
categorySchema.virtual("url").get(function () {
return `/category/${this.title_en}/search`;
});
categorySchema.virtual("children", {
ref: "Category",
localField: "_id",
foreignField: "parent",
justOne: false,
});
categorySchema.pre("find", function (next) {
this.populate({ path: "children" }).where({ deleted: false });
next();
});
categorySchema.pre("findOne", function (next) {
this.populate({ path: "children" }).where({ deleted: false });
next();
});
categorySchema.pre("save", async function (next) {
try {
if (this.isNew || this.isModified("parent")) {
if (this.parent) {
const parentCategory = await CategoryModel.findById(this.parent).exec();
if (!parentCategory) return next(new Error("Parent category not found."));
this.hierarchy = parentCategory.hierarchy.concat(this._id);
} else {
this.hierarchy = [this._id];
}
}
next();
} catch (err) {
next(err as CallbackError);
}
});
const CategoryModel = model<ICategory>("Category", categorySchema);
export { CategoryModel };
@@ -0,0 +1,20 @@
import mongoose, { Schema, model } from "mongoose";
import { IColor } from "./Abstraction/IColor";
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
const AutoIncrement = require("mongoose-sequence")(mongoose);
const ColorSchema = new Schema<IColor>(
{
_id: Number,
name: { type: String, unique: true, required: true },
hexColor: { type: String, unique: true, required: true },
},
{ timestamps: false, _id: false, toJSON: { versionKey: false } },
);
ColorSchema.plugin(AutoIncrement, { id: "color_id", inc_field: "_id" });
const ColorModel = model<IColor>("Color", ColorSchema);
export { ColorModel };
@@ -0,0 +1,19 @@
import mongoose, { Schema, model } from "mongoose";
import { IMeterage } from "./Abstraction/IMeterage";
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
const AutoIncrement = require("mongoose-sequence")(mongoose);
const MeterageSchema = new Schema<IMeterage>(
{
_id: Number,
value: { type: String, unique: true, required: true },
},
{ timestamps: false, _id: false, toJSON: { versionKey: false } },
);
MeterageSchema.plugin(AutoIncrement, { id: "meterage_id", inc_field: "_id" });
const MeterageModel = model<IMeterage>("Meterage", MeterageSchema);
export { MeterageModel };
+19
View File
@@ -0,0 +1,19 @@
import mongoose, { Schema, model } from "mongoose";
import { ISize } from "./Abstraction/ISize";
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
const AutoIncrement = require("mongoose-sequence")(mongoose);
const SizeSchema = new Schema<ISize>(
{
_id: Number,
value: { type: String, unique: true, required: true },
},
{ timestamps: false, _id: false, toJSON: { versionKey: false } },
);
SizeSchema.plugin(AutoIncrement, { id: "size_id", inc_field: "_id" });
const SizeModel = model<ISize>("Size", SizeSchema);
export { SizeModel };