From 17b4b30a3f2829c6402735c583cfd990ce4e2cd9 Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Thu, 15 Jan 2026 20:37:28 +0330 Subject: [PATCH] attribute --- .../product/controllers/product.controller.ts | 57 ++++++++++ .../product/dto/create-attribute.dto.ts | 32 ++++++ .../product/dto/update-attribute.dto.ts | 5 + .../product/entities/attribute.entity.ts | 11 +- .../product/entities/product.entity.ts | 4 +- src/modules/product/product.module.ts | 10 +- .../product/providers/attribute.service.ts | 101 ++++++++++++++++++ .../repositories/attribute.repository.ts | 11 ++ 8 files changed, 226 insertions(+), 5 deletions(-) create mode 100644 src/modules/product/dto/create-attribute.dto.ts create mode 100644 src/modules/product/dto/update-attribute.dto.ts create mode 100644 src/modules/product/providers/attribute.service.ts create mode 100644 src/modules/product/repositories/attribute.repository.ts diff --git a/src/modules/product/controllers/product.controller.ts b/src/modules/product/controllers/product.controller.ts index 08bf72e..701ca4f 100644 --- a/src/modules/product/controllers/product.controller.ts +++ b/src/modules/product/controllers/product.controller.ts @@ -21,6 +21,9 @@ import { CreateCategoryDto } from '../dto/create-category.dto'; import { CategoryService } from '../providers/category.service'; import { FindCategoriesDto } from '../dto/find-categories.dto'; import { UpdateCategoryDto } from '../dto/update-category.dto'; +import { AttributeService } from '../providers/attribute.service'; +import { CreateAttributeDto } from '../dto/create-attribute.dto'; +import { UpdateAttributeDto } from '../dto/update-attribute.dto'; @ApiTags('products') @Controller() @@ -28,6 +31,7 @@ export class productController { constructor( private readonly productService: ProductService, private readonly categoryService: CategoryService, + private readonly attributeService: AttributeService, ) { } @@ -159,4 +163,57 @@ export class productController { removeCategory(@Param('id') id: string,) { return this.categoryService.remove(+id); } + + /*------------------------------ Attibute ------------------------*/ + @Post('admin/product/:id/attribute') + // @UseGuards(AdminAuthGuard) + @ApiBearerAuth() + @Permissions(Permission.MANAGE_productS) + @ApiOperation({ summary: 'Create a new category' }) + @ApiParam({ name: 'id', required: true }) + @ApiBody({ type: CreateAttributeDto }) + createAttribute(@Param('id') id: string, @Body() dto: CreateAttributeDto) { + return this.attributeService.create(+id, dto); + } + + @Get('admin/products/:id/attributes') + // @UseGuards(AdminAuthGuard) + @ApiBearerAuth() + @Permissions(Permission.MANAGE_productS) + @ApiParam({ name: 'id', required: true }) + @ApiOperation({ summary: 'Get list of product attributes' }) + async findProductAttributes(@Param('id') id: string) { + return this.attributeService.findAll(+id); + } + + @Get('admin/products/attributes/:id') + // @UseGuards(AdminAuthGuard) + @ApiBearerAuth() + @Permissions(Permission.MANAGE_productS) + @ApiOperation({ summary: 'Get a category by id' }) + @ApiParam({ name: 'id', required: true }) + findAttributeById(@Param('id') id: string) { + return this.attributeService.findById(+id); + } + + @Patch('admin/products/attributes/:id') + // @UseGuards(AdminAuthGuard) + @ApiBearerAuth() + @Permissions(Permission.MANAGE_productS) + @ApiOperation({ summary: 'Update a attribute' }) + @ApiParam({ name: 'id', required: true }) + @ApiBody({ type: UpdateAttributeDto }) + updateProductAttribute(@Param('id') id: string, @Body() dto: UpdateAttributeDto) { + return this.attributeService.update(+id, dto); + } + + @Delete('admin/products/attributes/:id') + // @UseGuards(AdminAuthGuard) + @ApiBearerAuth() + @Permissions(Permission.MANAGE_productS) + @ApiOperation({ summary: 'Delete (soft) a attribute' }) + @ApiParam({ name: 'id', required: true }) + removeAttribute(@Param('id') id: string,) { + return this.attributeService.remove(+id); + } } diff --git a/src/modules/product/dto/create-attribute.dto.ts b/src/modules/product/dto/create-attribute.dto.ts new file mode 100644 index 0000000..79b199d --- /dev/null +++ b/src/modules/product/dto/create-attribute.dto.ts @@ -0,0 +1,32 @@ +import { IsString, IsOptional, IsBoolean, IsInt, Min, IsEnum } from 'class-validator'; +import { ApiPropertyOptional, ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { AttributeType } from '../interface/product.interface'; + +export class CreateAttributeDto { + @IsString() + @ApiProperty({ example: 'نوع روکش' }) + name: string; + + @IsOptional() + @IsInt() + @ApiPropertyOptional() + parentId?: number; + + + @IsBoolean() + @Type(() => Boolean) + @ApiPropertyOptional({ example: true }) + isRequired: boolean; + + @IsEnum(() => AttributeType) + @ApiProperty() + type: AttributeType; + + @IsOptional() + @IsInt() + @Min(0) + @Type(() => Number) + @ApiPropertyOptional({ example: 1 }) + order?: number; +} diff --git a/src/modules/product/dto/update-attribute.dto.ts b/src/modules/product/dto/update-attribute.dto.ts new file mode 100644 index 0000000..8b892b5 --- /dev/null +++ b/src/modules/product/dto/update-attribute.dto.ts @@ -0,0 +1,5 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateAttributeDto } from './create-attribute.dto'; + +export class UpdateAttributeDto extends PartialType(CreateAttributeDto) { +} diff --git a/src/modules/product/entities/attribute.entity.ts b/src/modules/product/entities/attribute.entity.ts index 19b5de3..daacd08 100644 --- a/src/modules/product/entities/attribute.entity.ts +++ b/src/modules/product/entities/attribute.entity.ts @@ -1,4 +1,4 @@ -import { Entity, Enum, ManyToOne, Property } from '@mikro-orm/core'; +import { Entity, Enum, ManyToOne, OneToMany, Property } from '@mikro-orm/core'; import { BaseEntity } from '../../../common/entities/base.entity'; import { AttributeType } from '../interface/product.interface'; import { Product } from './product.entity'; @@ -8,6 +8,12 @@ export class Attribute extends BaseEntity { @ManyToOne(()=>Product) product: Product + @ManyToOne(() => Attribute, { nullable: true }) + parent?: Attribute | null + + @OneToMany(() => Attribute, (c) => c.parent) + children?: Attribute[] + @Property({ primary: true }) id: bigint; @@ -20,4 +26,7 @@ export class Attribute extends BaseEntity { @Property({ type: 'boolean', default: false }) isRequired: boolean = false; + @Property({ type: 'int', nullable: true }) + order?: number; + } diff --git a/src/modules/product/entities/product.entity.ts b/src/modules/product/entities/product.entity.ts index ac5d839..d4bc84b 100644 --- a/src/modules/product/entities/product.entity.ts +++ b/src/modules/product/entities/product.entity.ts @@ -1,4 +1,4 @@ -import { Collection, Entity, ManyToOne, OneToMany, Property } from '@mikro-orm/core'; +import { Collection, Entity, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; import { BaseEntity } from '../../../common/entities/base.entity'; import { Category } from './category.entity'; import { Attribute } from './attribute.entity'; @@ -6,7 +6,7 @@ import { Attribute } from './attribute.entity'; @Entity({ tableName: 'products' }) export class Product extends BaseEntity { - @Property({ primary: true }) + @PrimaryKey({ type: 'bigint', columnType: 'char(26)', autoincrement: true }) id: bigint @ManyToOne(() => Category) diff --git a/src/modules/product/product.module.ts b/src/modules/product/product.module.ts index b3eff0c..1ce43a0 100644 --- a/src/modules/product/product.module.ts +++ b/src/modules/product/product.module.ts @@ -12,6 +12,8 @@ import { JwtModule } from '@nestjs/jwt'; import { UtilsModule } from '../util/utils.module'; import { CategoryRepository } from './repositories/category.repository'; import { CategoryService } from './providers/category.service'; +import { AttributeRepository } from './repositories/attribute.repository'; +import { AttributeService } from './providers/attribute.service'; @Module({ imports: [ @@ -21,7 +23,11 @@ import { CategoryService } from './providers/category.service'; UtilsModule, ], controllers: [productController,], - providers: [ProductService, ProductRepository, CategoryRepository, CategoryService], - exports: [ProductRepository, CategoryRepository, CategoryService], + providers: [ProductService, ProductRepository, + CategoryRepository, CategoryService, + AttributeRepository, AttributeService], + exports: [ProductRepository, CategoryRepository, + CategoryService, AttributeRepository, + AttributeService], }) export class productModule { } diff --git a/src/modules/product/providers/attribute.service.ts b/src/modules/product/providers/attribute.service.ts new file mode 100644 index 0000000..5efcf31 --- /dev/null +++ b/src/modules/product/providers/attribute.service.ts @@ -0,0 +1,101 @@ +import { BadGatewayException, Injectable, NotFoundException } from '@nestjs/common'; +import { EntityManager } from '@mikro-orm/postgresql'; +import { RequiredEntityData } from '@mikro-orm/core'; +import { AttributeRepository } from '../repositories/attribute.repository'; +import { CreateAttributeDto } from '../dto/create-attribute.dto'; +import { Attribute } from '../entities/attribute.entity'; +import { ProductRepository } from '../repositories/product.repository'; +import { UpdateAttributeDto } from '../dto/update-attribute.dto'; + +@Injectable() +export class AttributeService { + + constructor( + private readonly attributeRepository: AttributeRepository, + private readonly productRepository: ProductRepository, + private readonly em: EntityManager, + ) { } + + async create(productId: number, dto: CreateAttributeDto) { + let parent: null | Attribute = null + + const product = await this.productRepository.findOne({ id: productId }) + if (!product) { + throw new BadGatewayException('Product not found') + } + + if (dto.parentId) { + parent = await this.attributeRepository.findOne({ id: dto.parentId }) + if (!parent) { + throw new BadGatewayException('Parent attribute not found') + } + } + + const data: RequiredEntityData = { + product, + name: dto.name, + isRequired: dto.isRequired, + order: dto.order, + type: dto.type, + parent + }; + + const category = this.attributeRepository.create(data) + + await this.em.persistAndFlush(category) + + return category + + } + + async update(attributeId: number, dto: UpdateAttributeDto) { + + const attribute = await this.attributeRepository.findOne({ id: attributeId }) + + if (!attribute) { + throw new NotFoundException('Attribute not found'); + } + + if (dto.parentId) { + const parent = await this.attributeRepository.findOne({ id: dto.parentId }) + if (!parent) { + throw new NotFoundException('parent attribute not found'); + } + + this.attributeRepository.assign(attribute, { parent }) + } + + this.attributeRepository.assign(attribute, dto) + + this.em.persistAndFlush(attribute) + + return attribute + + } + + findAll(productId: number) { + return this.attributeRepository.find({ product: { id: productId } }); + } + + async findById(attributeId: number): Promise { + const attribute = await this.attributeRepository.findOne({ id: attributeId }, + { populate: ['children', 'product'] }); + + if (!attribute) throw new NotFoundException('Attribute not found'); + return attribute; + } + + async remove(id: number): Promise { + const attribute = await this.findById(id); + if (!attribute) { + throw new NotFoundException('attribute not found'); + } + if (attribute.children && attribute.children.length > 0) { + throw new NotFoundException('attribute has children'); + } + attribute.deletedAt = new Date(); + return await this.em.persistAndFlush(attribute); + + } + +} diff --git a/src/modules/product/repositories/attribute.repository.ts b/src/modules/product/repositories/attribute.repository.ts new file mode 100644 index 0000000..7f304ab --- /dev/null +++ b/src/modules/product/repositories/attribute.repository.ts @@ -0,0 +1,11 @@ +import { Injectable } from '@nestjs/common'; +import { EntityManager, EntityRepository } from '@mikro-orm/postgresql'; +import { Attribute } from '../entities/attribute.entity'; + + +@Injectable() +export class AttributeRepository extends EntityRepository { + constructor(readonly em: EntityManager) { + super(em, Attribute); + } +}