diff --git a/libs/common/src/common.module.ts b/libs/common/src/common.module.ts index de0780a..582510d 100644 --- a/libs/common/src/common.module.ts +++ b/libs/common/src/common.module.ts @@ -9,6 +9,15 @@ import { EmailService } from './util/email.service'; import { ErrorMessageService } from 'src/error-message/error-message.service'; import { TuyaService } from './integrations/tuya/services/tuya.service'; import { SceneDeviceRepository } from './modules/scene-device/repositories'; +import { SpaceProductItemRepository, SpaceRepository } from './modules/space'; +import { + SpaceModelRepository, + SpaceProductModelRepository, + SubspaceModelRepository, + SubspaceProductItemModelRepository, + SubspaceProductModelRepository, +} from './modules/space-model'; +import { SubspaceRepository } from './modules/space/repositories/subspace.repository'; @Module({ providers: [ CommonService, @@ -16,6 +25,14 @@ import { SceneDeviceRepository } from './modules/scene-device/repositories'; ErrorMessageService, TuyaService, SceneDeviceRepository, + SpaceRepository, + SubspaceRepository, + SubspaceModelRepository, + SubspaceProductModelRepository, + SubspaceProductItemModelRepository, + SpaceModelRepository, + SpaceProductModelRepository, + SpaceProductItemRepository, ], exports: [ CommonService, @@ -25,6 +42,13 @@ import { SceneDeviceRepository } from './modules/scene-device/repositories'; EmailService, ErrorMessageService, SceneDeviceRepository, + SpaceRepository, + SubspaceRepository, + SubspaceModelRepository, + SubspaceProductModelRepository, + SubspaceProductItemModelRepository, + SpaceModelRepository, + SpaceProductModelRepository, ], imports: [ ConfigModule.forRoot({ diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index eeabcd4..5357904 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -11,7 +11,10 @@ import { PermissionTypeEntity } from '../modules/permission/entities'; import { SpaceEntity, SpaceLinkEntity, + SpaceProductItemEntity, SubspaceEntity, + SubspaceProductEntity, + SubspaceProductItemEntity, } from '../modules/space/entities'; import { UserSpaceEntity } from '../modules/user/entities'; import { DeviceUserPermissionEntity } from '../modules/device/entities'; @@ -32,6 +35,8 @@ import { SpaceProductItemModelEntity, SpaceProductModelEntity, SubspaceModelEntity, + SubspaceProductItemModelEntity, + SubspaceProductModelEntity, } from '../modules/space-model/entities'; import { InviteUserEntity, @@ -80,6 +85,12 @@ import { SpaceProductModelEntity, SpaceProductItemModelEntity, SubspaceModelEntity, + SpaceProductEntity, + SpaceProductItemEntity, + SubspaceProductModelEntity, + SubspaceProductItemModelEntity, + SubspaceProductEntity, + SubspaceProductItemEntity, InviteUserEntity, InviteUserSpaceEntity, ], diff --git a/libs/common/src/modules/product/entities/product.entity.ts b/libs/common/src/modules/product/entities/product.entity.ts index 7b25470..926d179 100644 --- a/libs/common/src/modules/product/entities/product.entity.ts +++ b/libs/common/src/modules/product/entities/product.entity.ts @@ -4,6 +4,7 @@ import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { DeviceEntity } from '../../device/entities'; import { SpaceProductEntity } from '../../space/entities/space-product.entity'; import { SpaceProductModelEntity } from '../../space-model/entities'; +import { SubspaceProductModelEntity } from '../../space-model/entities/subspace-model/subspace-product-model.entity'; @Entity({ name: 'product' }) export class ProductEntity extends AbstractEntity { @@ -37,6 +38,12 @@ export class ProductEntity extends AbstractEntity { ) spaceProductModels: SpaceProductModelEntity[]; + @OneToMany( + () => SubspaceProductModelEntity, + (subspaceProductModel) => subspaceProductModel.product, + ) + subpaceProductModels: SubspaceProductModelEntity[]; + @OneToMany( () => DeviceEntity, (devicesProductEntity) => devicesProductEntity.productDevice, diff --git a/libs/common/src/modules/space-model/dtos/index.ts b/libs/common/src/modules/space-model/dtos/index.ts index f775b4a..9ba8130 100644 --- a/libs/common/src/modules/space-model/dtos/index.ts +++ b/libs/common/src/modules/space-model/dtos/index.ts @@ -1,4 +1,4 @@ -export * from './subspace-model.dto'; +export * from './subspace-model'; export * from './space-model.dto'; -export * from './space-product-item.dto'; +export * from './space-product-item-model.dto'; export * from './space-product-model.dto'; diff --git a/libs/common/src/modules/space-model/dtos/space-product-item-model.dto.ts b/libs/common/src/modules/space-model/dtos/space-product-item-model.dto.ts new file mode 100644 index 0000000..5eb9bca --- /dev/null +++ b/libs/common/src/modules/space-model/dtos/space-product-item-model.dto.ts @@ -0,0 +1,15 @@ +import { IsString, IsNotEmpty } from 'class-validator'; + +export class SpaceProductItemModelDto { + @IsString() + @IsNotEmpty() + uuid: string; + + @IsString() + @IsNotEmpty() + tag: string; + + @IsString() + @IsNotEmpty() + spaceProductModelUuid: string; +} diff --git a/libs/common/src/modules/space-model/dtos/space-product-model.dto.ts b/libs/common/src/modules/space-model/dtos/space-product-model.dto.ts index eae8088..7952a23 100644 --- a/libs/common/src/modules/space-model/dtos/space-product-model.dto.ts +++ b/libs/common/src/modules/space-model/dtos/space-product-model.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsNotEmpty, IsNumber, IsString } from 'class-validator'; -import { SpaceProductItemDto } from './space-product-item.dto'; +import { SpaceProductItemModelDto } from './space-product-item-model.dto'; export class SpaceProductModelDto { @IsString() @@ -17,7 +17,7 @@ export class SpaceProductModelDto { @ApiProperty({ description: 'List of individual items with specific names for the product', - type: [SpaceProductItemDto], + type: [SpaceProductItemModelDto], }) - items: SpaceProductItemDto[]; + items: SpaceProductItemModelDto[]; } diff --git a/libs/common/src/modules/space-model/dtos/subspace-model/index.ts b/libs/common/src/modules/space-model/dtos/subspace-model/index.ts new file mode 100644 index 0000000..70337b6 --- /dev/null +++ b/libs/common/src/modules/space-model/dtos/subspace-model/index.ts @@ -0,0 +1,3 @@ +export * from './subspace-model.dto'; +export * from './subspace-product-item-model.dto'; +export * from './subspace-product-model.dto'; diff --git a/libs/common/src/modules/space-model/dtos/subspace-model.dto.ts b/libs/common/src/modules/space-model/dtos/subspace-model/subspace-model.dto.ts similarity index 100% rename from libs/common/src/modules/space-model/dtos/subspace-model.dto.ts rename to libs/common/src/modules/space-model/dtos/subspace-model/subspace-model.dto.ts diff --git a/libs/common/src/modules/space-model/dtos/subspace-model/subspace-product-item-model.dto.ts b/libs/common/src/modules/space-model/dtos/subspace-model/subspace-product-item-model.dto.ts new file mode 100644 index 0000000..479642b --- /dev/null +++ b/libs/common/src/modules/space-model/dtos/subspace-model/subspace-product-item-model.dto.ts @@ -0,0 +1,15 @@ +import { IsString, IsNotEmpty } from 'class-validator'; + +export class SubspaceProductItemModelDto { + @IsString() + @IsNotEmpty() + uuid: string; + + @IsString() + @IsNotEmpty() + tag: string; + + @IsString() + @IsNotEmpty() + subspaceProductModelUuid: string; +} diff --git a/libs/common/src/modules/space-model/dtos/subspace-model/subspace-product-model.dto.ts b/libs/common/src/modules/space-model/dtos/subspace-model/subspace-product-model.dto.ts new file mode 100644 index 0000000..4eebaae --- /dev/null +++ b/libs/common/src/modules/space-model/dtos/subspace-model/subspace-product-model.dto.ts @@ -0,0 +1,23 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsNumber, IsString } from 'class-validator'; +import { SubspaceProductItemModelDto } from './subspace-product-item-model.dto'; + +export class SubpaceProductModelDto { + @IsString() + @IsNotEmpty() + uuid: string; + + @IsNumber() + @IsNotEmpty() + productCount: number; + + @IsString() + @IsNotEmpty() + productUuid: string; + + @ApiProperty({ + description: 'List of individual items with specific names for the product', + type: [SubspaceProductItemModelDto], + }) + items: SubspaceProductItemModelDto[]; +} diff --git a/libs/common/src/modules/space-model/entities/index.ts b/libs/common/src/modules/space-model/entities/index.ts index 3bf3f69..ec19308 100644 --- a/libs/common/src/modules/space-model/entities/index.ts +++ b/libs/common/src/modules/space-model/entities/index.ts @@ -1,4 +1,4 @@ export * from './space-model.entity'; -export * from './space-product-item.entity'; +export * from './space-product-item-model.entity'; export * from './space-product-model.entity'; -export * from './subspace-model.entity'; \ No newline at end of file +export * from './subspace-model'; diff --git a/libs/common/src/modules/space-model/entities/space-model.entity.ts b/libs/common/src/modules/space-model/entities/space-model.entity.ts index 9937a01..591d04f 100644 --- a/libs/common/src/modules/space-model/entities/space-model.entity.ts +++ b/libs/common/src/modules/space-model/entities/space-model.entity.ts @@ -8,9 +8,10 @@ import { } from 'typeorm'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { SpaceModelDto } from '../dtos'; -import { SubspaceModelEntity } from './subspace-model.entity'; +import { SubspaceModelEntity } from './subspace-model'; import { SpaceProductModelEntity } from './space-product-model.entity'; import { ProjectEntity } from '../../project/entities'; +import { SpaceEntity } from '../../space/entities'; @Entity({ name: 'space-model' }) @Unique(['modelName', 'project']) @@ -48,9 +49,13 @@ export class SpaceModelEntity extends AbstractEntity { () => SpaceProductModelEntity, (productModel) => productModel.spaceModel, { - cascade: true, nullable: true, }, ) public spaceProductModels: SpaceProductModelEntity[]; + + @OneToMany(() => SpaceEntity, (space) => space.spaceModel, { + cascade: true, + }) + public spaces: SpaceEntity[]; } diff --git a/libs/common/src/modules/space-model/entities/space-product-item.entity.ts b/libs/common/src/modules/space-model/entities/space-product-item-model.entity.ts similarity index 55% rename from libs/common/src/modules/space-model/entities/space-product-item.entity.ts rename to libs/common/src/modules/space-model/entities/space-product-item-model.entity.ts index 3695831..062418f 100644 --- a/libs/common/src/modules/space-model/entities/space-product-item.entity.ts +++ b/libs/common/src/modules/space-model/entities/space-product-item-model.entity.ts @@ -1,12 +1,11 @@ -import { Entity, Column, ManyToOne, Unique } from 'typeorm'; +import { Entity, Column, ManyToOne, OneToMany } from 'typeorm'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; -import { SpaceProductItemDto } from '../dtos'; +import { SpaceProductItemModelDto } from '../dtos'; import { SpaceProductModelEntity } from './space-product-model.entity'; -import { SpaceModelEntity } from './space-model.entity'; +import { SpaceProductItemEntity } from '../../space/entities'; @Entity({ name: 'space-product-item-model' }) -@Unique(['tag', 'spaceProductModel', 'spaceModel']) -export class SpaceProductItemModelEntity extends AbstractEntity { +export class SpaceProductItemModelEntity extends AbstractEntity { @Column({ nullable: false, }) @@ -21,12 +20,10 @@ export class SpaceProductItemModelEntity extends AbstractEntity SpaceModelEntity, - (spaceModel) => spaceModel.spaceProductModels, - { - nullable: false, - }, + @OneToMany( + () => SpaceProductItemEntity, + (spaceProductItem) => spaceProductItem.spaceProductItemModel, + { cascade: true }, ) - public spaceModel: SpaceModelEntity; + public items: SpaceProductItemEntity[]; } diff --git a/libs/common/src/modules/space-model/entities/space-product-model.entity.ts b/libs/common/src/modules/space-model/entities/space-product-model.entity.ts index d843d07..13b2b33 100644 --- a/libs/common/src/modules/space-model/entities/space-product-model.entity.ts +++ b/libs/common/src/modules/space-model/entities/space-product-model.entity.ts @@ -2,8 +2,9 @@ import { Entity, Column, ManyToOne, OneToMany } from 'typeorm'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { ProductEntity } from '../../product/entities'; import { SpaceModelEntity } from './space-model.entity'; -import { SpaceProductItemModelEntity } from './space-product-item.entity'; +import { SpaceProductItemModelEntity } from './space-product-item-model.entity'; import { SpaceProductModelDto } from '../dtos'; +import { SpaceProductEntity } from '../../space/entities'; @Entity({ name: 'space-product-model' }) export class SpaceProductModelEntity extends AbstractEntity { @@ -37,4 +38,13 @@ export class SpaceProductModelEntity extends AbstractEntity SpaceProductEntity, + (spaceProduct) => spaceProduct.spaceProductModel, + { + cascade: true, + }, + ) + public spaceProducts: SpaceProductEntity[]; } diff --git a/libs/common/src/modules/space-model/entities/subspace-model.entity.ts b/libs/common/src/modules/space-model/entities/subspace-model.entity.ts deleted file mode 100644 index 776f831..0000000 --- a/libs/common/src/modules/space-model/entities/subspace-model.entity.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Column, Entity, ManyToOne, Unique } from 'typeorm'; -import { AbstractEntity } from '../../abstract/entities/abstract.entity'; -import { SpaceModelEntity } from './space-model.entity'; -import { SubSpaceModelDto } from '../dtos'; - -@Entity({ name: 'subspace-model' }) -@Unique(['subspaceName', 'spaceModel']) -export class SubspaceModelEntity extends AbstractEntity { - @Column({ - type: 'uuid', - default: () => 'gen_random_uuid()', - nullable: false, - }) - public uuid: string; - - @Column({ - nullable: false, - }) - public subspaceName: string; - - @ManyToOne( - () => SpaceModelEntity, - (spaceModel) => spaceModel.subspaceModels, - { - nullable: false, - onDelete: 'CASCADE', - }, - ) - public spaceModel: SpaceModelEntity; -} diff --git a/libs/common/src/modules/space-model/entities/subspace-model/index.ts b/libs/common/src/modules/space-model/entities/subspace-model/index.ts new file mode 100644 index 0000000..e39403f --- /dev/null +++ b/libs/common/src/modules/space-model/entities/subspace-model/index.ts @@ -0,0 +1,3 @@ +export * from './subspace-model.entity'; +export * from './subspace-product-item-model.entity'; +export * from './subspace-product-model.entity'; diff --git a/libs/common/src/modules/space-model/entities/subspace-model/subspace-model.entity.ts b/libs/common/src/modules/space-model/entities/subspace-model/subspace-model.entity.ts new file mode 100644 index 0000000..89fcf63 --- /dev/null +++ b/libs/common/src/modules/space-model/entities/subspace-model/subspace-model.entity.ts @@ -0,0 +1,46 @@ +import { AbstractEntity } from '@app/common/modules/abstract/entities/abstract.entity'; +import { Column, Entity, ManyToOne, OneToMany, Unique } from 'typeorm'; +import { SubSpaceModelDto } from '../../dtos'; +import { SpaceModelEntity } from '../space-model.entity'; +import { SubspaceEntity } from '@app/common/modules/space/entities'; +import { SubspaceProductModelEntity } from './subspace-product-model.entity'; + +@Entity({ name: 'subspace-model' }) +@Unique(['subspaceName', 'spaceModel']) +export class SubspaceModelEntity extends AbstractEntity { + @Column({ + type: 'uuid', + default: () => 'gen_random_uuid()', + nullable: false, + }) + public uuid: string; + + @Column({ + nullable: false, + }) + public subspaceName: string; + + @ManyToOne( + () => SpaceModelEntity, + (spaceModel) => spaceModel.subspaceModels, + { + nullable: false, + onDelete: 'CASCADE', + }, + ) + public spaceModel: SpaceModelEntity; + + @OneToMany(() => SubspaceEntity, (space) => space.subSpaceModel, { + cascade: true, + }) + public spaces: SubspaceEntity[]; + + @OneToMany( + () => SubspaceProductModelEntity, + (productModel) => productModel.subspaceModel, + { + nullable: true, + }, + ) + public productModels: SubspaceProductModelEntity[]; +} diff --git a/libs/common/src/modules/space-model/entities/subspace-model/subspace-product-item-model.entity.ts b/libs/common/src/modules/space-model/entities/subspace-model/subspace-product-item-model.entity.ts new file mode 100644 index 0000000..2b6d305 --- /dev/null +++ b/libs/common/src/modules/space-model/entities/subspace-model/subspace-product-item-model.entity.ts @@ -0,0 +1,27 @@ +import { AbstractEntity } from '@app/common/modules/abstract/entities/abstract.entity'; +import { Entity, Column, ManyToOne, OneToMany } from 'typeorm'; +import { SubspaceProductItemModelDto } from '../../dtos'; +import { SubspaceProductModelEntity } from './subspace-product-model.entity'; +import { SubspaceProductItemEntity } from '@app/common/modules/space/entities'; + +@Entity({ name: 'subspace-product-item-model' }) +export class SubspaceProductItemModelEntity extends AbstractEntity { + @Column({ + nullable: false, + }) + public tag: string; + + @ManyToOne( + () => SubspaceProductModelEntity, + (productModel) => productModel.itemModels, + { + nullable: false, + }, + ) + public subspaceProductModel: SubspaceProductModelEntity; + + @OneToMany(() => SubspaceProductItemEntity, (item) => item.subspaceProduct, { + nullable: true, + }) + items: SubspaceProductItemEntity[]; +} diff --git a/libs/common/src/modules/space-model/entities/subspace-model/subspace-product-model.entity.ts b/libs/common/src/modules/space-model/entities/subspace-model/subspace-product-model.entity.ts new file mode 100644 index 0000000..843c70d --- /dev/null +++ b/libs/common/src/modules/space-model/entities/subspace-model/subspace-product-model.entity.ts @@ -0,0 +1,44 @@ +import { Entity, Column, ManyToOne, OneToMany } from 'typeorm'; +import { SubpaceProductModelDto } from '../../dtos'; +import { AbstractEntity } from '@app/common/modules/abstract/entities/abstract.entity'; +import { SubspaceModelEntity } from './subspace-model.entity'; +import { ProductEntity } from '@app/common/modules/product/entities'; +import { SubspaceProductEntity } from '@app/common/modules/space/entities'; +import { SubspaceProductItemModelEntity } from './subspace-product-item-model.entity'; + +@Entity({ name: 'subspace-product-model' }) +export class SubspaceProductModelEntity extends AbstractEntity { + @Column({ + nullable: false, + type: 'int', + }) + productCount: number; + + @ManyToOne( + () => SubspaceModelEntity, + (spaceModel) => spaceModel.productModels, + { + nullable: false, + }, + ) + public subspaceModel: SubspaceModelEntity; + + @ManyToOne(() => ProductEntity, (product) => product.subpaceProductModels, { + nullable: false, + }) + public product: ProductEntity; + + @OneToMany(() => SubspaceProductEntity, (product) => product.model, { + nullable: true, + }) + public subspaceProducts: SubspaceProductEntity[]; + + @OneToMany( + () => SubspaceProductItemModelEntity, + (product) => product.subspaceProductModel, + { + nullable: true, + }, + ) + public itemModels: SubspaceProductItemModelEntity[]; +} diff --git a/libs/common/src/modules/space-model/repositories/space-model.repository.ts b/libs/common/src/modules/space-model/repositories/space-model.repository.ts index 0e37479..fc92f14 100644 --- a/libs/common/src/modules/space-model/repositories/space-model.repository.ts +++ b/libs/common/src/modules/space-model/repositories/space-model.repository.ts @@ -5,6 +5,8 @@ import { SpaceProductItemModelEntity, SpaceProductModelEntity, SubspaceModelEntity, + SubspaceProductItemModelEntity, + SubspaceProductModelEntity, } from '../entities'; @Injectable() @@ -20,6 +22,20 @@ export class SubspaceModelRepository extends Repository { } } +@Injectable() +export class SubspaceProductModelRepository extends Repository { + constructor(private dataSource: DataSource) { + super(SubspaceProductModelEntity, dataSource.createEntityManager()); + } +} + +@Injectable() +export class SubspaceProductItemModelRepository extends Repository { + constructor(private dataSource: DataSource) { + super(SubspaceProductItemModelEntity, dataSource.createEntityManager()); + } +} + @Injectable() export class SpaceProductModelRepository extends Repository { constructor(private dataSource: DataSource) { diff --git a/libs/common/src/modules/space/dtos/index.ts b/libs/common/src/modules/space/dtos/index.ts index fcc0fdd..b470336 100644 --- a/libs/common/src/modules/space/dtos/index.ts +++ b/libs/common/src/modules/space/dtos/index.ts @@ -1,2 +1,4 @@ export * from './space.dto'; export * from './subspace.dto'; +export * from './space-product-item.dto'; +export * from './space-product.dto'; diff --git a/libs/common/src/modules/space-model/dtos/space-product-item.dto.ts b/libs/common/src/modules/space/dtos/space-product-item.dto.ts similarity index 86% rename from libs/common/src/modules/space-model/dtos/space-product-item.dto.ts rename to libs/common/src/modules/space/dtos/space-product-item.dto.ts index fa68825..8973c1a 100644 --- a/libs/common/src/modules/space-model/dtos/space-product-item.dto.ts +++ b/libs/common/src/modules/space/dtos/space-product-item.dto.ts @@ -11,5 +11,5 @@ export class SpaceProductItemDto { @IsString() @IsNotEmpty() - spaceProductModelUuid: string; + spaceProductUuid: string; } diff --git a/libs/common/src/modules/space/dtos/space-product.dto.ts b/libs/common/src/modules/space/dtos/space-product.dto.ts new file mode 100644 index 0000000..a57d29e --- /dev/null +++ b/libs/common/src/modules/space/dtos/space-product.dto.ts @@ -0,0 +1,23 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, IsNotEmpty, IsNumber } from 'class-validator'; +import { SpaceProductItemDto } from './space-product-item.dto'; + +export class SpaceProductModelDto { + @IsString() + @IsNotEmpty() + uuid: string; + + @IsNumber() + @IsNotEmpty() + productCount: number; + + @IsString() + @IsNotEmpty() + productUuid: string; + + @ApiProperty({ + description: 'List of individual items with specific names for the product', + type: [SpaceProductItemDto], + }) + items: SpaceProductItemDto[]; +} diff --git a/libs/common/src/modules/space/entities/index.ts b/libs/common/src/modules/space/entities/index.ts index f720cf4..f07ec93 100644 --- a/libs/common/src/modules/space/entities/index.ts +++ b/libs/common/src/modules/space/entities/index.ts @@ -1,3 +1,5 @@ export * from './space.entity'; -export * from './subspace.entity'; +export * from './subspace'; +export * from './space-product.entity'; +export * from './space-product-item.entity'; export * from './space-link.entity'; diff --git a/libs/common/src/modules/space/entities/space-product-item.entity.ts b/libs/common/src/modules/space/entities/space-product-item.entity.ts new file mode 100644 index 0000000..c53dfd4 --- /dev/null +++ b/libs/common/src/modules/space/entities/space-product-item.entity.ts @@ -0,0 +1,27 @@ +import { Column, Entity, ManyToOne } from 'typeorm'; +import { SpaceProductEntity } from './space-product.entity'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { SpaceProductItemDto } from '../dtos'; +import { SpaceProductItemModelEntity } from '../../space-model'; + +@Entity({ name: 'space-product-item' }) +export class SpaceProductItemEntity extends AbstractEntity { + @Column({ + nullable: false, + }) + public tag: string; + + @ManyToOne(() => SpaceProductEntity, (spaceProduct) => spaceProduct.items, { + nullable: false, + }) + public spaceProduct: SpaceProductEntity; + + @ManyToOne( + () => SpaceProductItemModelEntity, + (spaceProductItemModel) => spaceProductItemModel.items, + { + nullable: true, + }, + ) + public spaceProductItemModel?: SpaceProductItemModelEntity; +} diff --git a/libs/common/src/modules/space/entities/space-product.entity.ts b/libs/common/src/modules/space/entities/space-product.entity.ts index 92ad411..5f8a062 100644 --- a/libs/common/src/modules/space/entities/space-product.entity.ts +++ b/libs/common/src/modules/space/entities/space-product.entity.ts @@ -1,7 +1,9 @@ -import { Column, Entity, ManyToOne, JoinColumn } from 'typeorm'; +import { Column, Entity, ManyToOne, JoinColumn, OneToMany } from 'typeorm'; import { SpaceEntity } from './space.entity'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { ProductEntity } from '../../product/entities'; +import { SpaceProductItemEntity } from './space-product-item.entity'; +import { SpaceProductModelEntity } from '../../space-model'; @Entity({ name: 'space-product' }) export class SpaceProductEntity extends AbstractEntity { @@ -25,6 +27,21 @@ export class SpaceProductEntity extends AbstractEntity { }) productCount: number; + @OneToMany(() => SpaceProductItemEntity, (item) => item.spaceProduct, { + cascade: true, + }) + public items: SpaceProductItemEntity[]; + + @ManyToOne( + () => SpaceProductModelEntity, + (spaceProductModel) => spaceProductModel.spaceProducts, + { + nullable: true, + }, + ) + @JoinColumn({ name: 'space_product_model_uuid' }) + public spaceProductModel?: SpaceProductModelEntity; + constructor(partial: Partial) { super(); Object.assign(this, partial); diff --git a/libs/common/src/modules/space/entities/space.entity.ts b/libs/common/src/modules/space/entities/space.entity.ts index cb8367c..63beed5 100644 --- a/libs/common/src/modules/space/entities/space.entity.ts +++ b/libs/common/src/modules/space/entities/space.entity.ts @@ -11,10 +11,11 @@ import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { UserSpaceEntity } from '../../user/entities'; import { DeviceEntity } from '../../device/entities'; import { CommunityEntity } from '../../community/entities'; -import { SubspaceEntity } from './subspace.entity'; +import { SubspaceEntity } from './subspace'; import { SpaceLinkEntity } from './space-link.entity'; import { SpaceProductEntity } from './space-product.entity'; import { SceneEntity } from '../../scene/entities'; +import { SpaceModelEntity } from '../../space-model'; import { InviteUserSpaceEntity } from '../../Invite-user/entities'; @Entity({ name: 'space' }) @@ -98,6 +99,11 @@ export class SpaceEntity extends AbstractEntity { @OneToMany(() => SceneEntity, (scene) => scene.space) scenes: SceneEntity[]; + + @ManyToOne(() => SpaceModelEntity, { nullable: true }) + @JoinColumn({ name: 'space_model_uuid' }) + spaceModel?: SpaceModelEntity; + @OneToMany( () => InviteUserSpaceEntity, (inviteUserSpace) => inviteUserSpace.space, diff --git a/libs/common/src/modules/space/entities/subspace.entity.ts b/libs/common/src/modules/space/entities/subspace.entity.ts deleted file mode 100644 index ab38b00..0000000 --- a/libs/common/src/modules/space/entities/subspace.entity.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm'; -import { AbstractEntity } from '../../abstract/entities/abstract.entity'; -import { DeviceEntity } from '../../device/entities'; -import { SpaceEntity } from './space.entity'; -import { SubspaceDto } from '../dtos'; - -@Entity({ name: 'subspace' }) -export class SubspaceEntity extends AbstractEntity { - @Column({ - type: 'uuid', - default: () => 'gen_random_uuid()', - nullable: false, - }) - public uuid: string; - - @Column({ - nullable: false, - }) - public subspaceName: string; - - @ManyToOne(() => SpaceEntity, (space) => space.subspaces, { - nullable: false, - onDelete: 'CASCADE', - }) - @JoinColumn({ name: 'space_id' }) - space: SpaceEntity; - - @OneToMany(() => DeviceEntity, (device) => device.subspace, { - nullable: true, - }) - devices: DeviceEntity[]; - - constructor(partial: Partial) { - super(); - Object.assign(this, partial); - } -} diff --git a/libs/common/src/modules/space/entities/subspace/index.ts b/libs/common/src/modules/space/entities/subspace/index.ts new file mode 100644 index 0000000..471b7b1 --- /dev/null +++ b/libs/common/src/modules/space/entities/subspace/index.ts @@ -0,0 +1,3 @@ +export * from './subspace.entity'; +export * from './subspace-product.entity'; +export * from './subspace-product-item.entity'; diff --git a/libs/common/src/modules/space/entities/subspace/subspace-product-item.entity.ts b/libs/common/src/modules/space/entities/subspace/subspace-product-item.entity.ts new file mode 100644 index 0000000..1ec7958 --- /dev/null +++ b/libs/common/src/modules/space/entities/subspace/subspace-product-item.entity.ts @@ -0,0 +1,32 @@ +import { AbstractEntity } from '@app/common/modules/abstract/entities/abstract.entity'; +import { SpaceProductItemDto } from '../../dtos'; +import { Column, Entity, ManyToOne } from 'typeorm'; +import { SubspaceProductEntity } from './subspace-product.entity'; +import { SubspaceProductItemModelEntity } from '@app/common/modules/space-model'; + +@Entity({ name: 'subspace-product-item' }) +export class SubspaceProductItemEntity extends AbstractEntity { + @Column({ + nullable: false, + }) + public tag: string; + + @ManyToOne( + () => SubspaceProductEntity, + (subspaceProduct) => subspaceProduct.items, + { + nullable: false, + }, + ) + public subspaceProduct: SubspaceProductEntity; + + @ManyToOne(() => SubspaceProductItemModelEntity, (model) => model.items, { + nullable: true, + }) + model: SubspaceProductItemModelEntity; + + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} diff --git a/libs/common/src/modules/space/entities/subspace/subspace-product.entity.ts b/libs/common/src/modules/space/entities/subspace/subspace-product.entity.ts new file mode 100644 index 0000000..87ed807 --- /dev/null +++ b/libs/common/src/modules/space/entities/subspace/subspace-product.entity.ts @@ -0,0 +1,50 @@ +import { ProductEntity } from '@app/common/modules/product/entities'; +import { Column, Entity, ManyToOne, OneToMany } from 'typeorm'; +import { SubspaceEntity } from './subspace.entity'; +import { AbstractEntity } from '@app/common/modules/abstract/entities/abstract.entity'; +import { SubspaceProductItemEntity } from './subspace-product-item.entity'; +import { + SubspaceProductItemModelEntity, + SubspaceProductModelEntity, +} from '@app/common/modules/space-model'; +import { SpaceProductModelDto } from '../../dtos'; + +@Entity({ name: 'subspace-product' }) +export class SubspaceProductEntity extends AbstractEntity { + @Column({ + type: 'uuid', + default: () => 'gen_random_uuid()', + nullable: false, + }) + public uuid: string; + + @Column({ + nullable: false, + type: 'int', + }) + productCount: number; + + @ManyToOne(() => SubspaceEntity, (subspace) => subspace.subspaceProducts, { + nullable: false, + }) + public subspace: SubspaceEntity; + + @ManyToOne(() => ProductEntity, (product) => product.subpaceProductModels, { + nullable: false, + }) + public product: ProductEntity; + + @OneToMany(() => SubspaceProductItemEntity, (item) => item.subspaceProduct, { + nullable: true, + }) + public items: SubspaceProductItemEntity[]; + + @ManyToOne( + () => SubspaceProductModelEntity, + (model) => model.subspaceProducts, + { + nullable: true, + }, + ) + model: SubspaceProductModelEntity; +} diff --git a/libs/common/src/modules/space/entities/subspace/subspace.entity.ts b/libs/common/src/modules/space/entities/subspace/subspace.entity.ts new file mode 100644 index 0000000..bc6ff06 --- /dev/null +++ b/libs/common/src/modules/space/entities/subspace/subspace.entity.ts @@ -0,0 +1,52 @@ +import { AbstractEntity } from '@app/common/modules/abstract/entities/abstract.entity'; +import { DeviceEntity } from '@app/common/modules/device/entities'; +import { SubspaceModelEntity } from '@app/common/modules/space-model'; +import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm'; +import { SubspaceDto } from '../../dtos'; +import { SpaceEntity } from '../space.entity'; +import { SubspaceProductEntity } from './subspace-product.entity'; + +@Entity({ name: 'subspace' }) +export class SubspaceEntity extends AbstractEntity { + @Column({ + type: 'uuid', + default: () => 'gen_random_uuid()', + nullable: false, + }) + public uuid: string; + + @Column({ + nullable: false, + }) + public subspaceName: string; + + @ManyToOne(() => SpaceEntity, (space) => space.subspaces, { + nullable: false, + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'space_uuid' }) + space: SpaceEntity; + + @OneToMany(() => DeviceEntity, (device) => device.subspace, { + nullable: true, + }) + devices: DeviceEntity[]; + + @ManyToOne(() => SubspaceModelEntity, { nullable: true }) + @JoinColumn({ name: 'subspace_model_uuid' }) + subSpaceModel?: SubspaceModelEntity; + + @OneToMany( + () => SubspaceProductEntity, + (subspaceProduct) => subspaceProduct.subspace, + { + nullable: true, + }, + ) + public subspaceProducts: SubspaceProductEntity[]; + + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} diff --git a/libs/common/src/modules/space/index.ts b/libs/common/src/modules/space/index.ts new file mode 100644 index 0000000..b797801 --- /dev/null +++ b/libs/common/src/modules/space/index.ts @@ -0,0 +1,4 @@ +export * from './dtos'; +export * from './entities'; +export * from './repositories'; +export * from './space.repository.module'; diff --git a/libs/common/src/modules/space/repositories/space.repository.ts b/libs/common/src/modules/space/repositories/space.repository.ts index 43ce45e..66f96a4 100644 --- a/libs/common/src/modules/space/repositories/space.repository.ts +++ b/libs/common/src/modules/space/repositories/space.repository.ts @@ -1,7 +1,11 @@ import { DataSource, Repository } from 'typeorm'; import { Injectable } from '@nestjs/common'; import { SpaceProductEntity } from '../entities/space-product.entity'; -import { SpaceEntity, SpaceLinkEntity, SubspaceEntity } from '../entities'; +import { + SpaceEntity, + SpaceLinkEntity, + SpaceProductItemEntity, +} from '../entities'; @Injectable() export class SpaceRepository extends Repository { @@ -9,12 +13,6 @@ export class SpaceRepository extends Repository { super(SpaceEntity, dataSource.createEntityManager()); } } -@Injectable() -export class SubspaceRepository extends Repository { - constructor(private dataSource: DataSource) { - super(SubspaceEntity, dataSource.createEntityManager()); - } -} @Injectable() export class SpaceLinkRepository extends Repository { @@ -28,3 +26,10 @@ export class SpaceProductRepository extends Repository { super(SpaceProductEntity, dataSource.createEntityManager()); } } + +@Injectable() +export class SpaceProductItemRepository extends Repository { + constructor(private dataSource: DataSource) { + super(SpaceProductItemEntity, dataSource.createEntityManager()); + } +} diff --git a/libs/common/src/modules/space/repositories/subspace.repository.ts b/libs/common/src/modules/space/repositories/subspace.repository.ts new file mode 100644 index 0000000..3682c05 --- /dev/null +++ b/libs/common/src/modules/space/repositories/subspace.repository.ts @@ -0,0 +1,28 @@ +import { DataSource, Repository } from 'typeorm'; +import { + SubspaceEntity, + SubspaceProductEntity, + SubspaceProductItemEntity, +} from '../entities'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class SubspaceRepository extends Repository { + constructor(private dataSource: DataSource) { + super(SubspaceEntity, dataSource.createEntityManager()); + } +} + +@Injectable() +export class SubspaceProductRepository extends Repository { + constructor(private dataSource: DataSource) { + super(SubspaceProductEntity, dataSource.createEntityManager()); + } +} + +@Injectable() +export class SubspaceProductItemRepository extends Repository { + constructor(private dataSource: DataSource) { + super(SubspaceProductItemEntity, dataSource.createEntityManager()); + } +} diff --git a/libs/common/src/modules/space/space.repository.module.ts b/libs/common/src/modules/space/space.repository.module.ts index 90916c2..b39f98d 100644 --- a/libs/common/src/modules/space/space.repository.module.ts +++ b/libs/common/src/modules/space/space.repository.module.ts @@ -1,11 +1,17 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { SpaceEntity, SubspaceEntity } from './entities'; +import { SpaceEntity, SubspaceEntity, SubspaceProductEntity } from './entities'; @Module({ providers: [], exports: [], controllers: [], - imports: [TypeOrmModule.forFeature([SpaceEntity, SubspaceEntity])], + imports: [ + TypeOrmModule.forFeature([ + SpaceEntity, + SubspaceEntity, + SubspaceProductEntity, + ]), + ], }) export class SpaceRepositoryModule {} diff --git a/src/product/product.module.ts b/src/product/product.module.ts index e5ffd25..aa13d20 100644 --- a/src/product/product.module.ts +++ b/src/product/product.module.ts @@ -1,10 +1,12 @@ -import { Module } from '@nestjs/common'; +import { Global, Module } from '@nestjs/common'; import { ProductService } from './services'; import { ProductRepository } from '@app/common/modules/product/repositories'; import { ProductController } from './controllers'; +@Global() @Module({ controllers: [ProductController], providers: [ProductService, ProductRepository], + exports: [ProductService], }) export class ProductModule {} diff --git a/src/product/services/product.service.ts b/src/product/services/product.service.ts index cebf9ba..4b9377c 100644 --- a/src/product/services/product.service.ts +++ b/src/product/services/product.service.ts @@ -21,4 +21,23 @@ export class ProductService { message: 'List of products retrieved successfully', }); } + + async findOne(productUuid: string): Promise { + const product = await this.productRepository.findOne({ + where: { + uuid: productUuid, + }, + }); + if (!product) { + throw new HttpException( + `No product with ${productUuid} found in the system`, + HttpStatus.NOT_FOUND, + ); + } + + return new SuccessResponseDto({ + data: product, + message: 'Succefully retrieved product', + }); + } } diff --git a/src/project/project.module.ts b/src/project/project.module.ts index 9d82ba9..3cd906e 100644 --- a/src/project/project.module.ts +++ b/src/project/project.module.ts @@ -1,8 +1,9 @@ -import { Module } from '@nestjs/common'; +import { Global, Module } from '@nestjs/common'; import { ProjectController } from './controllers'; import { ProjectService } from './services'; import { ProjectRepository } from '@app/common/modules/project/repositiories'; +@Global() @Module({ imports: [], controllers: [ProjectController], diff --git a/src/space-model/common/index.ts b/src/space-model/common/index.ts new file mode 100644 index 0000000..e371345 --- /dev/null +++ b/src/space-model/common/index.ts @@ -0,0 +1 @@ +export * from './services'; diff --git a/src/space-model/common/services/base-product-item-model.service.ts b/src/space-model/common/services/base-product-item-model.service.ts new file mode 100644 index 0000000..80c8119 --- /dev/null +++ b/src/space-model/common/services/base-product-item-model.service.ts @@ -0,0 +1,60 @@ +import { HttpException, HttpStatus } from '@nestjs/common'; +import { QueryRunner } from 'typeorm'; +import { SpaceModelEntity } from '@app/common/modules/space-model'; +import { CreateProductItemModelDto } from 'src/space-model/dtos'; + +export abstract class BaseProductItemService { + async validateTags( + itemModelDtos: CreateProductItemModelDto[], + queryRunner: QueryRunner, + spaceModel: SpaceModelEntity, + ): Promise { + const incomingTags = new Set( + itemModelDtos.map((item) => item.tag).filter(Boolean), + ); + + const duplicateTags = itemModelDtos + .map((item) => item.tag) + .filter((tag, index, array) => array.indexOf(tag) !== index); + + if (duplicateTags.length > 0) { + throw new HttpException( + `Duplicate tags found in the request: ${[...new Set(duplicateTags)].join(', ')}`, + HttpStatus.BAD_REQUEST, + ); + } + + const existingTagsQuery = ` + SELECT DISTINCT tag + FROM ( + SELECT spi.tag + FROM "subspace-product-item-model" spi + INNER JOIN "subspace-product-model" spm ON spi.subspace_product_model_uuid = spm.uuid + INNER JOIN "subspace-model" sm ON spm.subspace_model_uuid = sm.uuid + WHERE sm.space_model_uuid = $1 + UNION + SELECT spi.tag + FROM "space-product-item-model" spi + INNER JOIN "space-product-model" spm ON spi.space_product_model_uuid = spm.uuid + WHERE spm.space_model_uuid = $1 + ) AS combined_tags; + `; + + const existingTags = await queryRunner.manager.query(existingTagsQuery, [ + spaceModel.uuid, + ]); + const existingTagsSet = new Set( + existingTags.map((row: { tag: string }) => row.tag), + ); + + const conflictingTags = [...incomingTags].filter((tag) => + existingTagsSet.has(tag), + ); + if (conflictingTags.length > 0) { + throw new HttpException( + `Tags already exist in the model: ${conflictingTags.join(', ')}`, + HttpStatus.CONFLICT, + ); + } + } +} diff --git a/src/space-model/common/services/base-product-model.service.ts b/src/space-model/common/services/base-product-model.service.ts new file mode 100644 index 0000000..b83511a --- /dev/null +++ b/src/space-model/common/services/base-product-model.service.ts @@ -0,0 +1,24 @@ +import { HttpException, HttpStatus } from '@nestjs/common'; +import { CreateSpaceProductModelDto } from 'src/space-model/dtos'; +import { ProductService } from '../../../product/services'; + +export abstract class BaseProductModelService { + constructor(private readonly productService: ProductService) {} + + protected async validateProductCount( + dto: CreateSpaceProductModelDto, + ): Promise { + const productItemCount = dto.items.length; + if (dto.productCount !== productItemCount) { + throw new HttpException( + `Product count (${dto.productCount}) does not match the number of items (${productItemCount}) for product ID ${dto.productUuid}.`, + HttpStatus.BAD_REQUEST, + ); + } + } + + protected async getProduct(productId: string) { + const product = await this.productService.findOne(productId); + return product.data; + } +} diff --git a/src/space-model/common/services/index.ts b/src/space-model/common/services/index.ts new file mode 100644 index 0000000..d1cc61e --- /dev/null +++ b/src/space-model/common/services/index.ts @@ -0,0 +1,2 @@ +export * from './base-product-item-model.service'; +export * from './base-product-model.service'; diff --git a/src/space-model/dtos/create-space-product-item-model.dto.ts b/src/space-model/dtos/create-space-product-item-model.dto.ts index 3825219..e6a7e76 100644 --- a/src/space-model/dtos/create-space-product-item-model.dto.ts +++ b/src/space-model/dtos/create-space-product-item-model.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsNotEmpty, IsString } from 'class-validator'; -export class CreateSpaceProductItemModelDto { +export class CreateProductItemModelDto { @ApiProperty({ description: 'Specific name for the product item', example: 'Light 1', diff --git a/src/space-model/dtos/create-space-product-model.dto.ts b/src/space-model/dtos/create-space-product-model.dto.ts index 11d8bb5..a4379a1 100644 --- a/src/space-model/dtos/create-space-product-model.dto.ts +++ b/src/space-model/dtos/create-space-product-model.dto.ts @@ -8,7 +8,7 @@ import { ArrayNotEmpty, } from 'class-validator'; import { Type } from 'class-transformer'; -import { CreateSpaceProductItemModelDto } from './create-space-product-item-model.dto'; +import { CreateProductItemModelDto } from './create-space-product-item-model.dto'; export class CreateSpaceProductModelDto { @ApiProperty({ @@ -17,7 +17,7 @@ export class CreateSpaceProductModelDto { }) @IsNotEmpty() @IsString() - productId: string; + productUuid: string; @ApiProperty({ description: 'Number of products in the model', @@ -29,11 +29,11 @@ export class CreateSpaceProductModelDto { @ApiProperty({ description: 'Specific names for each product item', - type: [CreateSpaceProductItemModelDto], + type: [CreateProductItemModelDto], }) @IsArray() @ArrayNotEmpty() @ValidateNested({ each: true }) - @Type(() => CreateSpaceProductItemModelDto) - items: CreateSpaceProductItemModelDto[]; + @Type(() => CreateProductItemModelDto) + items: CreateProductItemModelDto[]; } diff --git a/src/space-model/dtos/create-subspace-model.dto.ts b/src/space-model/dtos/create-subspace-model.dto.ts index a27ad3b..f8dbcdd 100644 --- a/src/space-model/dtos/create-subspace-model.dto.ts +++ b/src/space-model/dtos/create-subspace-model.dto.ts @@ -1,5 +1,13 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; +import { + IsArray, + IsNotEmpty, + IsOptional, + IsString, + ValidateNested, +} from 'class-validator'; +import { CreateSpaceProductModelDto } from './create-space-product-model.dto'; +import { Type } from 'class-transformer'; export class CreateSubspaceModelDto { @ApiProperty({ @@ -9,4 +17,14 @@ export class CreateSubspaceModelDto { @IsNotEmpty() @IsString() subspaceName: string; + + @ApiProperty({ + description: 'List of products included in the model', + type: [CreateSpaceProductModelDto], + }) + @IsArray() + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => CreateSpaceProductModelDto) + spaceProductModels?: CreateSpaceProductModelDto[]; } diff --git a/src/space-model/services/index.ts b/src/space-model/services/index.ts index 88e2d41..5c39727 100644 --- a/src/space-model/services/index.ts +++ b/src/space-model/services/index.ts @@ -1,4 +1,4 @@ export * from './space-model.service'; export * from './space-product-item-model.service'; export * from './space-product-model.service'; -export * from './subspace-model.service'; +export * from './subspace'; diff --git a/src/space-model/services/space-model.service.ts b/src/space-model/services/space-model.service.ts index 7359e25..0423c42 100644 --- a/src/space-model/services/space-model.service.ts +++ b/src/space-model/services/space-model.service.ts @@ -4,7 +4,7 @@ import { CreateSpaceModelDto } from '../dtos'; import { ProjectRepository } from '@app/common/modules/project/repositiories'; import { ProjectParam } from 'src/community/dtos'; import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; -import { SubSpaceModelService } from './subspace-model.service'; +import { SubSpaceModelService } from './subspace/subspace-model.service'; import { SpaceProductModelService } from './space-product-model.service'; import { DataSource } from 'typeorm'; diff --git a/src/space-model/services/space-product-item-model.service.ts b/src/space-model/services/space-product-item-model.service.ts index b2f856c..c69cae8 100644 --- a/src/space-model/services/space-product-item-model.service.ts +++ b/src/space-model/services/space-product-item-model.service.ts @@ -4,29 +4,30 @@ import { SpaceProductModelEntity, } from '@app/common/modules/space-model'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; -import { CreateSpaceProductItemModelDto } from '../dtos'; +import { CreateProductItemModelDto } from '../dtos'; import { QueryRunner } from 'typeorm'; +import { BaseProductItemService } from '../common'; @Injectable() -export class SpaceProductItemModelService { +export class SpaceProductItemModelService extends BaseProductItemService { constructor( private readonly spaceProductItemRepository: SpaceProductItemModelRepository, - ) {} + ) { + super(); + } async createProdutItemModel( - itemModelDtos: CreateSpaceProductItemModelDto[], + itemModelDtos: CreateProductItemModelDto[], spaceProductModel: SpaceProductModelEntity, spaceModel: SpaceModelEntity, queryRunner: QueryRunner, ) { - await this.validateTags(itemModelDtos, spaceModel, queryRunner); - + await this.validateTags(itemModelDtos, queryRunner, spaceModel); try { const productItems = itemModelDtos.map((dto) => queryRunner.manager.create(this.spaceProductItemRepository.target, { tag: dto.tag, spaceProductModel, - spaceModel, }), ); @@ -43,41 +44,4 @@ export class SpaceProductItemModelService { ); } } - - private async validateTags( - itemModelDtos: CreateSpaceProductItemModelDto[], - spaceModel: SpaceModelEntity, - queryRunner: QueryRunner, - ) { - const incomingTags = itemModelDtos.map((item) => item.tag); - - const duplicateTags = incomingTags.filter( - (tag, index) => incomingTags.indexOf(tag) !== index, - ); - if (duplicateTags.length > 0) { - throw new HttpException( - `Duplicate tags found in the request: ${[...new Set(duplicateTags)].join(', ')}`, - HttpStatus.BAD_REQUEST, - ); - } - - const existingTags = await queryRunner.manager.find( - this.spaceProductItemRepository.target, - { - where: { spaceModel }, - select: ['tag'], - }, - ); - const existingTagSet = new Set(existingTags.map((item) => item.tag)); - - const conflictingTags = incomingTags.filter((tag) => - existingTagSet.has(tag), - ); - if (conflictingTags.length > 0) { - throw new HttpException( - `Tags already exist in the model: ${conflictingTags.join(', ')}`, - HttpStatus.CONFLICT, - ); - } - } } diff --git a/src/space-model/services/space-product-model.service.ts b/src/space-model/services/space-product-model.service.ts index aa08a16..0f1e93e 100644 --- a/src/space-model/services/space-product-model.service.ts +++ b/src/space-model/services/space-product-model.service.ts @@ -1,20 +1,23 @@ +import { QueryRunner } from 'typeorm'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { CreateSpaceProductModelDto } from '../dtos'; +import { SpaceProductItemModelService } from './space-product-item-model.service'; +import { BaseProductModelService } from '../common'; +import { ProductService } from 'src/product/services'; import { SpaceModelEntity, SpaceProductModelRepository, } from '@app/common/modules/space-model'; -import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; -import { CreateSpaceProductModelDto } from '../dtos'; -import { ProductRepository } from '@app/common/modules/product/repositories'; -import { SpaceProductItemModelService } from './space-product-item-model.service'; -import { QueryRunner } from 'typeorm'; @Injectable() -export class SpaceProductModelService { +export class SpaceProductModelService extends BaseProductModelService { constructor( private readonly spaceProductModelRepository: SpaceProductModelRepository, - private readonly productRepository: ProductRepository, private readonly spaceProductItemModelService: SpaceProductItemModelService, - ) {} + productService: ProductService, + ) { + super(productService); + } async createSpaceProductModels( spaceProductModelDtos: CreateSpaceProductModelDto[], @@ -25,7 +28,7 @@ export class SpaceProductModelService { const productModels = await Promise.all( spaceProductModelDtos.map(async (dto) => { this.validateProductCount(dto); - const product = await this.getProduct(dto.productId); + const product = await this.getProduct(dto.productUuid); return queryRunner.manager.create( this.spaceProductModelRepository.target, { @@ -40,14 +43,15 @@ export class SpaceProductModelService { const savedProductModels = await queryRunner.manager.save(productModels); await Promise.all( - spaceProductModelDtos.map((dto, index) => - this.spaceProductItemModelService.createProdutItemModel( + spaceProductModelDtos.map((dto, index) => { + const savedModel = savedProductModels[index]; + return this.spaceProductItemModelService.createProdutItemModel( dto.items, - savedProductModels[index], + savedModel, // Pass the saved model spaceModel, queryRunner, - ), - ), + ); + }), ); } catch (error) { if (error instanceof HttpException) { @@ -60,25 +64,4 @@ export class SpaceProductModelService { ); } } - - private validateProductCount(dto: CreateSpaceProductModelDto) { - const productItemCount = dto.items.length; - if (dto.productCount !== productItemCount) { - throw new HttpException( - `Product count (${dto.productCount}) does not match the number of items (${productItemCount}) for product ID ${dto.productId}.`, - HttpStatus.BAD_REQUEST, - ); - } - } - - private async getProduct(productId: string) { - const product = await this.productRepository.findOneBy({ uuid: productId }); - if (!product) { - throw new HttpException( - `Product with ID ${productId} not found.`, - HttpStatus.NOT_FOUND, - ); - } - return product; - } } diff --git a/src/space-model/services/subspace/index.ts b/src/space-model/services/subspace/index.ts new file mode 100644 index 0000000..78d7cd3 --- /dev/null +++ b/src/space-model/services/subspace/index.ts @@ -0,0 +1,3 @@ +export * from './subspace-model.service'; +export * from './subspace-product-item-model.service'; +export * from './subspace-product-model.service'; diff --git a/src/space-model/services/subspace-model.service.ts b/src/space-model/services/subspace/subspace-model.service.ts similarity index 77% rename from src/space-model/services/subspace-model.service.ts rename to src/space-model/services/subspace/subspace-model.service.ts index a6a75aa..222dee4 100644 --- a/src/space-model/services/subspace-model.service.ts +++ b/src/space-model/services/subspace/subspace-model.service.ts @@ -3,13 +3,15 @@ import { SubspaceModelRepository, } from '@app/common/modules/space-model'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; -import { CreateSubspaceModelDto } from '../dtos'; +import { CreateSubspaceModelDto } from '../../dtos'; import { QueryRunner } from 'typeorm'; +import { SubspaceProductModelService } from './subspace-product-model.service'; @Injectable() export class SubSpaceModelService { constructor( private readonly subspaceModelRepository: SubspaceModelRepository, + private readonly subSpaceProducetModelService: SubspaceProductModelService, ) {} async createSubSpaceModels( @@ -28,6 +30,18 @@ export class SubSpaceModelService { ); await queryRunner.manager.save(subspaces); + + await Promise.all( + subSpaceModelDtos.map((dto, index) => { + const subspaceModel = subspaces[index]; + return this.subSpaceProducetModelService.createSubspaceProductModels( + dto.spaceProductModels, + spaceModel, + subspaceModel, + queryRunner, + ); + }), + ); } catch (error) { if (error instanceof HttpException) { throw error; diff --git a/src/space-model/services/subspace/subspace-product-item-model.service.ts b/src/space-model/services/subspace/subspace-product-item-model.service.ts new file mode 100644 index 0000000..393a5f3 --- /dev/null +++ b/src/space-model/services/subspace/subspace-product-item-model.service.ts @@ -0,0 +1,53 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { QueryRunner } from 'typeorm'; +import { + SpaceModelEntity, + SubspaceProductItemModelRepository, + SubspaceProductModelEntity, +} from '@app/common/modules/space-model'; +import { BaseProductItemService } from '../../common'; +import { CreateProductItemModelDto } from '../../dtos'; + +@Injectable() +export class SubspaceProductItemModelService extends BaseProductItemService { + constructor( + private readonly subspaceProductItemRepository: SubspaceProductItemModelRepository, + ) { + super(); + } + + async createProdutItemModel( + itemModelDtos: CreateProductItemModelDto[], + subspaceProductModel: SubspaceProductModelEntity, + spaceModel: SpaceModelEntity, + queryRunner: QueryRunner, + ) { + if (!subspaceProductModel) { + throw new HttpException( + 'The spaceProductModel parameter is required but was not provided.', + HttpStatus.BAD_REQUEST, + ); + } + await this.validateTags(itemModelDtos, queryRunner, spaceModel); + try { + const productItems = itemModelDtos.map((dto) => + queryRunner.manager.create(this.subspaceProductItemRepository.target, { + tag: dto.tag, + subspaceProductModel, + }), + ); + + await queryRunner.manager.save(productItems); + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + + throw new HttpException( + error.message || + 'An unexpected error occurred while creating product items.', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/space-model/services/subspace/subspace-product-model.service.ts b/src/space-model/services/subspace/subspace-product-model.service.ts new file mode 100644 index 0000000..30f9062 --- /dev/null +++ b/src/space-model/services/subspace/subspace-product-model.service.ts @@ -0,0 +1,69 @@ +import { + SpaceModelEntity, + SubspaceModelEntity, + SubspaceProductModelRepository, +} from '@app/common/modules/space-model'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { SubspaceProductItemModelService } from './subspace-product-item-model.service'; +import { CreateSpaceProductModelDto } from '../../dtos'; +import { QueryRunner } from 'typeorm'; +import { BaseProductModelService } from '../../common'; +import { ProductService } from 'src/product/services'; + +@Injectable() +export class SubspaceProductModelService extends BaseProductModelService { + constructor( + private readonly subpaceProductModelRepository: SubspaceProductModelRepository, + productService: ProductService, + private readonly subspaceProductItemModelService: SubspaceProductItemModelService, + ) { + super(productService); + } + + async createSubspaceProductModels( + spaceProductModelDtos: CreateSpaceProductModelDto[], + spaceModel: SpaceModelEntity, + subspaceModel: SubspaceModelEntity, + queryRunner: QueryRunner, + ) { + try { + const productModels = await Promise.all( + spaceProductModelDtos.map(async (dto) => { + this.validateProductCount(dto); + const product = await this.getProduct(dto.productUuid); + return queryRunner.manager.create( + this.subpaceProductModelRepository.target, + { + product, + productCount: dto.productCount, + subspaceModel, + }, + ); + }), + ); + + const savedProductModels = await queryRunner.manager.save(productModels); + + await Promise.all( + spaceProductModelDtos.map((dto, index) => { + const savedModel = savedProductModels[index]; + return this.subspaceProductItemModelService.createProdutItemModel( + dto.items, + savedModel, // Pass the saved model + spaceModel, + queryRunner, + ); + }), + ); + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + throw new HttpException( + error.message || + 'An unexpected error occurred while creating product models.', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/space-model/space-model.module.ts b/src/space-model/space-model.module.ts index bbc6245..ae275e2 100644 --- a/src/space-model/space-model.module.ts +++ b/src/space-model/space-model.module.ts @@ -7,15 +7,19 @@ import { SpaceProductItemModelService, SpaceProductModelService, SubSpaceModelService, + SubspaceProductItemModelService, } from './services'; import { SpaceModelRepository, SpaceProductItemModelRepository, SpaceProductModelRepository, SubspaceModelRepository, + SubspaceProductItemModelRepository, + SubspaceProductModelRepository, } from '@app/common/modules/space-model'; import { ProjectRepository } from '@app/common/modules/project/repositiories'; import { ProductRepository } from '@app/common/modules/product/repositories'; +import { SubspaceProductModelService } from './services/subspace/subspace-product-model.service'; @Module({ imports: [ConfigModule, SpaceRepositoryModule], @@ -31,6 +35,10 @@ import { ProductRepository } from '@app/common/modules/product/repositories'; ProductRepository, SpaceProductItemModelService, SpaceProductItemModelRepository, + SubspaceProductItemModelService, + SubspaceProductItemModelRepository, + SubspaceProductModelService, + SubspaceProductModelRepository, ], exports: [], }) diff --git a/src/space/common/base-product-item.service.ts b/src/space/common/base-product-item.service.ts new file mode 100644 index 0000000..dd62050 --- /dev/null +++ b/src/space/common/base-product-item.service.ts @@ -0,0 +1,69 @@ +import { HttpException, HttpStatus } from '@nestjs/common'; +import { QueryRunner } from 'typeorm'; + +export abstract class BaseProductItemService { + protected async validateTags( + incomingTags: string[], + queryRunner: QueryRunner, + spaceUuid: string, + ): Promise { + const duplicateTags = incomingTags.filter( + (tag, index) => incomingTags.indexOf(tag) !== index, + ); + + if (duplicateTags.length > 0) { + throw new HttpException( + `Duplicate tags found in the request: ${[...new Set(duplicateTags)].join(', ')}`, + HttpStatus.BAD_REQUEST, + ); + } + + const existingTagsQuery = ` + SELECT tag FROM ( + SELECT spi.tag + FROM "space-product-item" spi + INNER JOIN "space-product" spm ON spi.space_product_uuid = spm.uuid + WHERE spm.space_uuid = $1 + UNION + SELECT spi.tag + FROM "subspace-product-item" spi + INNER JOIN "subspace-product" spm ON spi.subspace_product_uuid = spm.uuid + INNER JOIN "subspace" sm ON spm.subspace_uuid = sm.uuid + WHERE sm.space_uuid = $1 + ) AS combined_tags; + `; + + const existingTags = await queryRunner.manager.query(existingTagsQuery, [ + spaceUuid, + ]); + + const existingTagSet = new Set( + existingTags.map((row: { tag: string }) => row.tag), + ); + const conflictingTags = incomingTags.filter((tag) => + existingTagSet.has(tag), + ); + + if (conflictingTags.length > 0) { + throw new HttpException( + `Tags already exist in the model: ${conflictingTags.join(', ')}`, + HttpStatus.CONFLICT, + ); + } + } + + protected async saveProductItems( + productItems: T[], + targetRepository: any, + queryRunner: QueryRunner, + ): Promise { + try { + await queryRunner.manager.save(targetRepository, productItems); + } catch (error) { + throw new HttpException( + error.message || 'An error occurred while creating product items.', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/space/common/index.ts b/src/space/common/index.ts new file mode 100644 index 0000000..6e76f39 --- /dev/null +++ b/src/space/common/index.ts @@ -0,0 +1 @@ +export * from './base-product-item.service'; diff --git a/src/space/dtos/add.space.dto.ts b/src/space/dtos/add.space.dto.ts index 554f17b..dba8e92 100644 --- a/src/space/dtos/add.space.dto.ts +++ b/src/space/dtos/add.space.dto.ts @@ -10,6 +10,42 @@ import { IsUUID, ValidateNested, } from 'class-validator'; +import { AddSubspaceDto } from './subspace'; + +export class CreateSpaceProductItemDto { + @ApiProperty({ + description: 'Specific name for the product item', + example: 'Light 1', + }) + @IsNotEmpty() + @IsString() + tag: string; +} + +export class ProductAssignmentDto { + @ApiProperty({ + description: 'UUID of the product to be assigned', + example: 'prod-uuid-1234', + }) + @IsNotEmpty() + productId: string; + + @ApiProperty({ + description: 'Number of items to assign for the product', + example: 3, + }) + count: number; + + @ApiProperty({ + description: 'Specific names for each product item', + type: [CreateSpaceProductItemDto], + example: [{ tag: 'Light 1' }, { tag: 'Light 2' }, { tag: 'Light 3' }], + }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => CreateSpaceProductItemDto) + items: CreateSpaceProductItemDto[]; +} export class AddSpaceDto { @ApiProperty({ @@ -29,9 +65,14 @@ export class AddSpaceDto { @IsOptional() parentUuid?: string; + @ApiProperty({ + description: 'Icon identifier for the space', + example: 'assets/location', + required: false, + }) @IsString() @IsOptional() - public icon: string; + public icon?: string; @ApiProperty({ description: 'Indicates whether the space is private or public', @@ -49,16 +90,39 @@ export class AddSpaceDto { @IsNumber() y: number; + @ApiProperty({ + description: 'UUID of the Space', + example: 'd290f1ee-6c54-4b01-90e6-d701748f0851', + }) + @IsString() + @IsOptional() + spaceModelUuid?: string; + @ApiProperty({ description: 'Y position on canvas', example: 200 }) @IsString() @IsOptional() - direction: string; + direction?: string; + @ApiProperty({ + description: 'List of products assigned to this space', + type: [ProductAssignmentDto], + required: false, + }) @IsArray() @ValidateNested({ each: true }) @IsOptional() @Type(() => ProductAssignmentDto) - products: ProductAssignmentDto[]; + products?: ProductAssignmentDto[]; + + @ApiProperty({ + description: 'List of subspaces included in the model', + type: [AddSubspaceDto], + }) + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => AddSubspaceDto) + subspaces?: AddSubspaceDto[]; } export class AddUserSpaceDto { @@ -101,11 +165,3 @@ export class AddUserSpaceUsingCodeDto { Object.assign(this, dto); } } - -class ProductAssignmentDto { - @IsNotEmpty() - productId: string; - - @IsNotEmpty() - count: number; -} diff --git a/src/space/dtos/subspace/add.subspace.dto.ts b/src/space/dtos/subspace/add.subspace.dto.ts index a2b12e2..6b5078b 100644 --- a/src/space/dtos/subspace/add.subspace.dto.ts +++ b/src/space/dtos/subspace/add.subspace.dto.ts @@ -1,5 +1,13 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; +import { Type } from 'class-transformer'; +import { + IsArray, + IsNotEmpty, + IsOptional, + IsString, + ValidateNested, +} from 'class-validator'; +import { ProductAssignmentDto } from '../add.space.dto'; export class AddSubspaceDto { @ApiProperty({ @@ -9,4 +17,15 @@ export class AddSubspaceDto { @IsNotEmpty() @IsString() subspaceName: string; + + @ApiProperty({ + description: 'List of products assigned to this space', + type: [ProductAssignmentDto], + required: false, + }) + @IsArray() + @ValidateNested({ each: true }) + @IsOptional() + @Type(() => ProductAssignmentDto) + products?: ProductAssignmentDto[]; } diff --git a/src/space/services/index.ts b/src/space/services/index.ts index 79eb32f..c67ccae 100644 --- a/src/space/services/index.ts +++ b/src/space/services/index.ts @@ -5,3 +5,5 @@ export * from './subspace'; export * from './space-link'; export * from './space-scene.service'; export * from './space-products'; +export * from './space-product-items'; +export * from './space-validation.service'; diff --git a/src/space/services/space-device.service.ts b/src/space/services/space-device.service.ts index 7dff75a..b856be0 100644 --- a/src/space/services/space-device.service.ts +++ b/src/space/services/space-device.service.ts @@ -6,23 +6,25 @@ import { GetSpaceParam } from '../dtos'; import { BaseResponseDto } from '@app/common/dto/base.response.dto'; import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; -import { SpaceService } from './space.service'; + +import { ValidationService } from './space-validation.service'; @Injectable() export class SpaceDeviceService { constructor( private readonly tuyaService: TuyaService, - private readonly spaceService: SpaceService, + private readonly validationService: ValidationService, ) {} async listDevicesInSpace(params: GetSpaceParam): Promise { const { spaceUuid, communityUuid, projectUuid } = params; try { - const space = await this.spaceService.validateCommunityAndSpace( - communityUuid, - spaceUuid, - projectUuid, - ); + const space = + await this.validationService.validateSpaceWithinCommunityAndProject( + communityUuid, + projectUuid, + spaceUuid, + ); if (!Array.isArray(space.devices)) { throw new HttpException( diff --git a/src/space/services/space-product-items/index.ts b/src/space/services/space-product-items/index.ts new file mode 100644 index 0000000..fff8634 --- /dev/null +++ b/src/space/services/space-product-items/index.ts @@ -0,0 +1 @@ +export * from './space-product-items.service'; diff --git a/src/space/services/space-product-items/space-product-items.service.ts b/src/space/services/space-product-items/space-product-items.service.ts new file mode 100644 index 0000000..d4883b9 --- /dev/null +++ b/src/space/services/space-product-items/space-product-items.service.ts @@ -0,0 +1,87 @@ +import { + SpaceEntity, + SpaceProductEntity, + SpaceProductItemRepository, +} from '@app/common/modules/space'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { CreateSpaceProductItemDto } from '../../dtos'; +import { QueryRunner } from 'typeorm'; +import { SpaceProductModelEntity } from '@app/common/modules/space-model'; +import { BaseProductItemService } from '../../common'; + +@Injectable() +export class SpaceProductItemService extends BaseProductItemService { + constructor( + private readonly spaceProductItemRepository: SpaceProductItemRepository, + ) { + super(); + } + + async createProductItem( + itemModelDtos: CreateSpaceProductItemDto[], + spaceProduct: SpaceProductEntity, + space: SpaceEntity, + queryRunner: QueryRunner, + ) { + if (!itemModelDtos?.length) return; + + const incomingTags = itemModelDtos.map((item) => item.tag); + + await this.validateTags(incomingTags, queryRunner, space.uuid); + + try { + const productItems = itemModelDtos.map((dto) => + queryRunner.manager.create(this.spaceProductItemRepository.target, { + tag: dto.tag, + spaceProduct, + }), + ); + + await this.saveProductItems( + productItems, + this.spaceProductItemRepository.target, + queryRunner, + ); + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + + throw new HttpException( + error.message || + 'An unexpected error occurred while creating product items.', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async createSpaceProductItemFromModel( + spaceProduct: SpaceProductEntity, + spaceProductModel: SpaceProductModelEntity, + queryRunner: QueryRunner, + ) { + const spaceProductItemModels = spaceProductModel.items; + if (!spaceProductItemModels?.length) return; + + try { + const productItems = spaceProductItemModels.map((model) => + queryRunner.manager.create(this.spaceProductItemRepository.target, { + tag: model.tag, + spaceProduct, + }), + ); + + await queryRunner.manager.save(productItems); + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + + throw new HttpException( + error.message || + 'An unexpected error occurred while creating product items.', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/space/services/space-products/space-products.service.ts b/src/space/services/space-products/space-products.service.ts index 5b218e4..2fe75cb 100644 --- a/src/space/services/space-products/space-products.service.ts +++ b/src/space/services/space-products/space-products.service.ts @@ -2,96 +2,101 @@ import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; import { ProductRepository } from '@app/common/modules/product/repositories'; import { SpaceEntity } from '@app/common/modules/space/entities'; import { SpaceProductEntity } from '@app/common/modules/space/entities/space-product.entity'; -import { SpaceProductRepository } from '@app/common/modules/space/repositories'; -import { In } from 'typeorm'; +import { In, QueryRunner } from 'typeorm'; +import { ProductAssignmentDto } from '../../dtos'; +import { SpaceProductItemService } from '../space-product-items'; +import { SpaceModelEntity } from '@app/common/modules/space-model'; +import { ProductEntity } from '@app/common/modules/product/entities'; +import { ProductService } from 'src/product/services'; @Injectable() export class SpaceProductService { constructor( private readonly productRepository: ProductRepository, - private readonly spaceProductRepository: SpaceProductRepository, + private readonly spaceProductItemService: SpaceProductItemService, + private readonly productService: ProductService, ) {} + async createFromModel( + spaceModel: SpaceModelEntity, + space: SpaceEntity, + queryRunner: QueryRunner, + ) { + const spaceProductModels = spaceModel.spaceProductModels; + if (!spaceProductModels?.length) return; + const newSpaceProducts = []; + + spaceProductModels.map((spaceProductModel) => { + newSpaceProducts.push( + queryRunner.manager.create(SpaceProductEntity, { + space: space, + product: spaceProductModel.product, + productCount: spaceProductModel.productCount, + spaceProductModel: spaceProductModel, + }), + ); + }); + if (newSpaceProducts.length > 0) { + await queryRunner.manager.save(SpaceProductEntity, newSpaceProducts); + await Promise.all( + newSpaceProducts.map((spaceProduct, index) => { + const spaceProductModel = spaceProductModels[index]; + return this.spaceProductItemService.createSpaceProductItemFromModel( + spaceProduct, + spaceProductModel, + queryRunner, + ); + }), + ); + } + } + async assignProductsToSpace( space: SpaceEntity, - products: { productId: string; count: number }[], + products: ProductAssignmentDto[], + queryRunner: QueryRunner, ): Promise { + let updatedProducts: SpaceProductEntity[] = []; + try { const uniqueProducts = this.validateUniqueProducts(products); const productEntities = await this.getProductEntities(uniqueProducts); + const existingSpaceProducts = await this.getExistingSpaceProducts( + space, + queryRunner, + ); - // Fetch existing space products - const existingSpaceProducts = await this.spaceProductRepository.find({ - where: { - space: { - uuid: space.uuid, - }, - }, - relations: ['product'], - }); - - const updatedProducts = []; - const newProducts = []; - - for (const { productId, count } of uniqueProducts) { - const product = productEntities.get(productId); - if (!product) { - throw new HttpException( - `Product with ID ${productId} not found`, - HttpStatus.NOT_FOUND, - ); - } - - // Check if product already exists in the space - const existingProduct = existingSpaceProducts.find( - (spaceProduct) => spaceProduct.product.uuid === productId, + if (existingSpaceProducts) { + updatedProducts = await this.updateExistingProducts( + existingSpaceProducts, + uniqueProducts, + productEntities, + queryRunner, ); - - if (existingProduct) { - // If count is different, update the existing record - if (existingProduct.productCount !== count) { - existingProduct.productCount = count; - updatedProducts.push(existingProduct); - } - } else { - // Add new product if it doesn't exist - newProducts.push( - this.spaceProductRepository.create({ - space, - product, - productCount: count, - }), - ); - } } - // Save updates and new records - if (updatedProducts.length > 0) { - await this.spaceProductRepository.save(updatedProducts); - } - - if (newProducts.length > 0) { - await this.spaceProductRepository.save(newProducts); - } + const newProducts = await this.createNewProducts( + uniqueProducts, + productEntities, + space, + queryRunner, + ); return [...updatedProducts, ...newProducts]; } catch (error) { - console.error('Error assigning products to space:', error); - if (!(error instanceof HttpException)) { throw new HttpException( - 'An error occurred while assigning products to the space', + `An error occurred while assigning products to the space ${error}`, HttpStatus.INTERNAL_SERVER_ERROR, ); } - throw error; } } private validateUniqueProducts( - products: { productId: string; count: number }[], - ): { productId: string; count: number }[] { + products: ProductAssignmentDto[], + ): ProductAssignmentDto[] { const productIds = new Set(); const uniqueProducts = []; @@ -110,15 +115,13 @@ export class SpaceProductService { } private async getProductEntities( - products: { productId: string; count: number }[], + products: ProductAssignmentDto[], ): Promise> { try { const productIds = products.map((p) => p.productId); - const productEntities = await this.productRepository.find({ where: { uuid: In(productIds) }, }); - return new Map(productEntities.map((p) => [p.uuid, p])); } catch (error) { console.error('Error fetching product entities:', error); @@ -128,4 +131,96 @@ export class SpaceProductService { ); } } + + private async getExistingSpaceProducts( + space: SpaceEntity, + queryRunner: QueryRunner, + ): Promise { + return queryRunner.manager.find(SpaceProductEntity, { + where: { space: { uuid: space.uuid } }, + relations: ['product'], + }); + } + + private async updateExistingProducts( + existingSpaceProducts: SpaceProductEntity[], + uniqueProducts: ProductAssignmentDto[], + productEntities: Map, + queryRunner: QueryRunner, + ): Promise { + const updatedProducts = []; + + for (const { productId, count } of uniqueProducts) { + productEntities.get(productId); + const existingProduct = existingSpaceProducts.find( + (spaceProduct) => spaceProduct.product.uuid === productId, + ); + + if (existingProduct && existingProduct.productCount !== count) { + existingProduct.productCount = count; + updatedProducts.push(existingProduct); + } + } + + if (updatedProducts.length > 0) { + await queryRunner.manager.save(SpaceProductEntity, updatedProducts); + } + + return updatedProducts; + } + + private async createNewProducts( + uniqueSpaceProducts: ProductAssignmentDto[], + productEntities: Map, + space: SpaceEntity, + queryRunner: QueryRunner, + ): Promise { + const newProducts = []; + + for (const uniqueSpaceProduct of uniqueSpaceProducts) { + const product = productEntities.get(uniqueSpaceProduct.productId); + await this.getProduct(uniqueSpaceProduct.productId); + this.validateProductCount(uniqueSpaceProduct); + + newProducts.push( + queryRunner.manager.create(SpaceProductEntity, { + space, + product, + productCount: uniqueSpaceProduct.count, + }), + ); + } + if (newProducts.length > 0) { + await queryRunner.manager.save(SpaceProductEntity, newProducts); + + await Promise.all( + uniqueSpaceProducts.map((dto, index) => { + const spaceProduct = newProducts[index]; + return this.spaceProductItemService.createProductItem( + dto.items, + spaceProduct, + space, + queryRunner, + ); + }), + ); + } + + return newProducts; + } + + private validateProductCount(dto: ProductAssignmentDto) { + const productItemCount = dto.items.length; + if (dto.count !== productItemCount) { + throw new HttpException( + `Product count (${dto.count}) does not match the number of items (${productItemCount}) for product ID ${dto.productId}.`, + HttpStatus.BAD_REQUEST, + ); + } + } + + async getProduct(productId: string): Promise { + const product = await this.productService.findOne(productId); + return product.data; + } } diff --git a/src/space/services/space-scene.service.ts b/src/space/services/space-scene.service.ts index 57d63a5..4e77158 100644 --- a/src/space/services/space-scene.service.ts +++ b/src/space/services/space-scene.service.ts @@ -1,16 +1,16 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { GetSpaceParam } from '../dtos'; import { BaseResponseDto } from '@app/common/dto/base.response.dto'; -import { SpaceService } from './space.service'; import { SceneService } from '../../scene/services'; import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; import { GetSceneDto } from '../../scene/dtos'; +import { ValidationService } from './space-validation.service'; @Injectable() export class SpaceSceneService { constructor( - private readonly spaceSevice: SpaceService, private readonly sceneSevice: SceneService, + private readonly validationService: ValidationService, ) {} async getScenes( @@ -20,10 +20,10 @@ export class SpaceSceneService { try { const { spaceUuid, communityUuid, projectUuid } = params; - await this.spaceSevice.validateCommunityAndSpace( + await this.validationService.validateSpaceWithinCommunityAndProject( communityUuid, - spaceUuid, projectUuid, + spaceUuid, ); const scenes = await this.sceneSevice.findScenesBySpace( diff --git a/src/space/services/space-user.service.ts b/src/space/services/space-user.service.ts index 0e10e1a..0912132 100644 --- a/src/space/services/space-user.service.ts +++ b/src/space/services/space-user.service.ts @@ -1,21 +1,19 @@ import { BaseResponseDto } from '@app/common/dto/base.response.dto'; import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; -import { SpaceRepository } from '@app/common/modules/space/repositories'; import { UserRepository, UserSpaceRepository, } from '@app/common/modules/user/repositories'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; -import { SpaceService } from './space.service'; import { UserSpaceParam } from '../dtos'; +import { ValidationService } from './space-validation.service'; @Injectable() export class SpaceUserService { constructor( - private readonly spaceRepository: SpaceRepository, + private readonly validationService: ValidationService, private readonly userRepository: UserRepository, private readonly userSpaceRepository: UserSpaceRepository, - private readonly spaceService: SpaceService, ) {} async associateUserToSpace(params: UserSpaceParam): Promise { const { communityUuid, spaceUuid, userUuid, projectUuid } = params; @@ -31,11 +29,12 @@ export class SpaceUserService { } // Find the space by ID - const space = await this.spaceService.validateCommunityAndSpace( - communityUuid, - spaceUuid, - projectUuid, - ); + const space = + await this.validationService.validateSpaceWithinCommunityAndProject( + communityUuid, + projectUuid, + spaceUuid, + ); // Check if the association already exists const existingAssociation = await this.userSpaceRepository.findOne({ @@ -73,10 +72,10 @@ export class SpaceUserService { } // Find the space by ID - await this.spaceService.validateCommunityAndSpace( + await this.validationService.validateSpaceWithinCommunityAndProject( communityUuid, - spaceUuid, projectUuid, + spaceUuid, ); // Find the existing association diff --git a/src/space/services/space-validation.service.ts b/src/space/services/space-validation.service.ts new file mode 100644 index 0000000..fa4a5f7 --- /dev/null +++ b/src/space/services/space-validation.service.ts @@ -0,0 +1,81 @@ +import { CommunityEntity } from '@app/common/modules/community/entities'; +import { SpaceEntity } from '@app/common/modules/space/entities'; +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { CommunityService } from '../../community/services'; +import { ProjectService } from '../../project/services'; +import { + SpaceModelEntity, + SpaceModelRepository, +} from '@app/common/modules/space-model'; + +@Injectable() +export class ValidationService { + constructor( + private readonly projectService: ProjectService, + private readonly communityService: CommunityService, + private readonly spaceRepository: SpaceRepository, + private readonly spaceModelRepository: SpaceModelRepository, + ) {} + + async validateCommunityAndProject( + communityUuid: string, + projectUuid: string, + ): Promise { + await this.projectService.findOne(projectUuid); + const community = await this.communityService.getCommunityById({ + communityUuid, + projectUuid, + }); + + return community.data; + } + + async validateSpaceWithinCommunityAndProject( + communityUuid: string, + projectUuid: string, + spaceUuid?: string, + ): Promise { + await this.validateCommunityAndProject(communityUuid, projectUuid); + const space = await this.validateSpace(spaceUuid); + return space; + } + + async validateSpace(spaceUuid: string): Promise { + const space = await this.spaceRepository.findOne({ + where: { uuid: spaceUuid }, + }); + + if (!space) { + throw new HttpException( + `Space with UUID ${spaceUuid} not found`, + HttpStatus.NOT_FOUND, + ); + } + + return space; + } + + async validateSpaceModel(spaceModelUuid: string): Promise { + const spaceModel = await this.spaceModelRepository.findOne({ + where: { uuid: spaceModelUuid }, + relations: [ + 'subspaceModels', + 'subspaceModels.productModels.product', + 'subspaceModels.productModels', + 'spaceProductModels', + 'spaceProductModels.product', + 'spaceProductModels.items', + ], + }); + + if (!spaceModel) { + throw new HttpException( + `Space model with UUID ${spaceModelUuid} not found`, + HttpStatus.NOT_FOUND, + ); + } + + return spaceModel; + } +} diff --git a/src/space/services/space.service.ts b/src/space/services/space.service.ts index 0061226..59aceb5 100644 --- a/src/space/services/space.service.ts +++ b/src/space/services/space.service.ts @@ -9,47 +9,68 @@ import { AddSpaceDto, CommunitySpaceParam, GetSpaceParam, + ProductAssignmentDto, UpdateSpaceDto, } from '../dtos'; import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; import { BaseResponseDto } from '@app/common/dto/base.response.dto'; -import { CommunityRepository } from '@app/common/modules/community/repositories'; import { SpaceEntity } from '@app/common/modules/space/entities'; import { generateRandomString } from '@app/common/helper/randomString'; import { SpaceLinkService } from './space-link'; import { SpaceProductService } from './space-products'; -import { ProjectRepository } from '@app/common/modules/project/repositiories'; +import { CreateSubspaceModelDto } from 'src/space-model/dtos'; +import { SubSpaceService } from './subspace'; +import { DataSource } from 'typeorm'; +import { ValidationService } from './space-validation.service'; @Injectable() export class SpaceService { constructor( + private readonly dataSource: DataSource, private readonly spaceRepository: SpaceRepository, - private readonly communityRepository: CommunityRepository, private readonly spaceLinkService: SpaceLinkService, private readonly spaceProductService: SpaceProductService, - private readonly projectRepository: ProjectRepository, + private readonly subSpaceService: SubSpaceService, + private readonly validationService: ValidationService, ) {} async createSpace( addSpaceDto: AddSpaceDto, params: CommunitySpaceParam, ): Promise { - const { parentUuid, direction, products } = addSpaceDto; + const { parentUuid, direction, products, spaceModelUuid, subspaces } = + addSpaceDto; const { communityUuid, projectUuid } = params; - await this.validateProject(projectUuid); + const queryRunner = this.dataSource.createQueryRunner(); - const community = await this.validateCommunity(communityUuid); + await queryRunner.connect(); + await queryRunner.startTransaction(); + + const community = await this.validationService.validateCommunityAndProject( + communityUuid, + projectUuid, + ); + + this.validateSpaceCreation(spaceModelUuid, products, subspaces); + + const parent = parentUuid + ? await this.validationService.validateSpace(parentUuid) + : null; + + const spaceModel = spaceModelUuid + ? await this.validationService.validateSpaceModel(spaceModelUuid) + : null; - const parent = parentUuid ? await this.validateSpace(parentUuid) : null; try { - const newSpace = this.spaceRepository.create({ + const newSpace = queryRunner.manager.create(SpaceEntity, { ...addSpaceDto, - community, + spaceModel, parent: parentUuid ? parent : null, + community, }); - await this.spaceRepository.save(newSpace); + await queryRunner.manager.save(newSpace); if (direction && parent) { await this.spaceLinkService.saveSpaceLink( @@ -59,12 +80,34 @@ export class SpaceService { ); } + if (subspaces?.length) { + await this.subSpaceService.createSubspacesFromDto( + subspaces, + newSpace, + queryRunner, + ); + } else if (spaceModel && spaceModel.subspaceModels.length) { + await this.subSpaceService.createSubSpaceFromModel( + spaceModel, + newSpace, + queryRunner, + ); + } + if (products && products.length > 0) { await this.spaceProductService.assignProductsToSpace( newSpace, products, + queryRunner, + ); + } else if (spaceModel && spaceModel.spaceProductModels.length) { + await this.spaceProductService.createFromModel( + spaceModel, + newSpace, + queryRunner, ); } + await queryRunner.commitTransaction(); return new SuccessResponseDto({ statusCode: HttpStatus.CREATED, @@ -72,7 +115,14 @@ export class SpaceService { message: 'Space created successfully', }); } catch (error) { + await queryRunner.rollbackTransaction(); + + if (error instanceof HttpException) { + throw error; + } throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR); + } finally { + await queryRunner.release(); } } @@ -80,8 +130,10 @@ export class SpaceService { params: CommunitySpaceParam, ): Promise { const { communityUuid, projectUuid } = params; - await this.validateCommunity(communityUuid); - await this.validateProject(projectUuid); + await this.validationService.validateCommunityAndProject( + communityUuid, + projectUuid, + ); try { // Get all spaces related to the community, including the parent-child relations const spaces = await this.spaceRepository.find({ @@ -114,11 +166,12 @@ export class SpaceService { async findOne(params: GetSpaceParam): Promise { const { communityUuid, spaceUuid, projectUuid } = params; try { - const space = await this.validateCommunityAndSpace( - communityUuid, - spaceUuid, - projectUuid, - ); + const space = + await this.validationService.validateSpaceWithinCommunityAndProject( + communityUuid, + projectUuid, + spaceUuid, + ); return new SuccessResponseDto({ message: `Space with ID ${spaceUuid} successfully fetched`, @@ -139,12 +192,13 @@ export class SpaceService { async delete(params: GetSpaceParam): Promise { try { const { communityUuid, spaceUuid, projectUuid } = params; - // First, check if the community exists - const space = await this.validateCommunityAndSpace( - communityUuid, - spaceUuid, - projectUuid, - ); + + const space = + await this.validationService.validateSpaceWithinCommunityAndProject( + communityUuid, + projectUuid, + spaceUuid, + ); // Delete the space await this.spaceRepository.remove(space); @@ -169,29 +223,39 @@ export class SpaceService { updateSpaceDto: UpdateSpaceDto, ): Promise { const { communityUuid, spaceUuid, projectUuid } = params; + const queryRunner = this.dataSource.createQueryRunner(); + try { - const space = await this.validateCommunityAndSpace( - communityUuid, - spaceUuid, - projectUuid, - ); + await queryRunner.connect(); + await queryRunner.startTransaction(); + + const space = + await this.validationService.validateSpaceWithinCommunityAndProject( + communityUuid, + projectUuid, + spaceUuid, + ); // If a parentId is provided, check if the parent exists const { parentUuid, products } = updateSpaceDto; - const parent = parentUuid ? await this.validateSpace(parentUuid) : null; + const parent = parentUuid + ? await this.validationService.validateSpace(parentUuid) + : null; // Update other space properties from updateSpaceDto Object.assign(space, updateSpaceDto, { parent }); // Save the updated space - const updatedSpace = await this.spaceRepository.save(space); + const updatedSpace = await queryRunner.manager.save(space); if (products && products.length > 0) { await this.spaceProductService.assignProductsToSpace( updatedSpace, products, + queryRunner, ); } + await queryRunner.commitTransaction(); return new SuccessResponseDto({ message: `Space with ID ${spaceUuid} successfully updated`, @@ -199,6 +263,8 @@ export class SpaceService { statusCode: HttpStatus.OK, }); } catch (error) { + await queryRunner.rollbackTransaction(); + if (error instanceof HttpException) { throw error; } @@ -206,6 +272,8 @@ export class SpaceService { 'An error occurred while updating the space', HttpStatus.INTERNAL_SERVER_ERROR, ); + } finally { + await queryRunner.release(); } } @@ -213,7 +281,11 @@ export class SpaceService { params: GetSpaceParam, ): Promise { const { spaceUuid, communityUuid, projectUuid } = params; - await this.validateCommunityAndSpace(communityUuid, spaceUuid, projectUuid); + await this.validationService.validateSpaceWithinCommunityAndProject( + communityUuid, + projectUuid, + spaceUuid, + ); try { // Get all spaces that are children of the provided space, including the parent-child relations @@ -243,11 +315,12 @@ export class SpaceService { try { const invitationCode = generateRandomString(6); - const space = await this.validateCommunityAndSpace( - communityUuid, - spaceUuid, - projectUuid, - ); + const space = + await this.validationService.validateSpaceWithinCommunityAndProject( + communityUuid, + projectUuid, + spaceUuid, + ); space.invitationCode = invitationCode; await this.spaceRepository.save(space); @@ -293,56 +366,16 @@ export class SpaceService { return rootSpaces; } - private async validateCommunity(communityId: string) { - const community = await this.communityRepository.findOne({ - where: { uuid: communityId }, - }); - if (!community) { + private validateSpaceCreation( + spaceModelUuid?: string, + products?: ProductAssignmentDto[], + subSpaces?: CreateSubspaceModelDto[], + ) { + if (spaceModelUuid && (products?.length || subSpaces?.length)) { throw new HttpException( - `Community with ID ${communityId} not found`, - HttpStatus.NOT_FOUND, + 'For space creation choose either space model or products and subspace', + HttpStatus.CONFLICT, ); } - return community; - } - - async validateCommunityAndSpace( - communityUuid: string, - spaceUuid: string, - projectUuid: string, - ) { - await this.validateProject(projectUuid); - - const community = await this.validateCommunity(communityUuid); - if (!community) { - this.throwNotFound('Community', communityUuid); - } - - const space = await this.validateSpace(spaceUuid); - return space; - } - - private async validateSpace(spaceUuid: string) { - const space = await this.spaceRepository.findOne({ - where: { uuid: spaceUuid }, - relations: ['devices', 'devices.productDevice'], - }); - if (!space) this.throwNotFound('Space', spaceUuid); - return space; - } - - private async validateProject(uuid: string) { - const project = await this.projectRepository.findOne({ - where: { uuid }, - }); - - if (!project) this.throwNotFound('Project', uuid); - } - - private throwNotFound(entity: string, uuid: string) { - throw new HttpException( - `${entity} with ID ${uuid} not found`, - HttpStatus.NOT_FOUND, - ); } } diff --git a/src/space/services/subspace/index.ts b/src/space/services/subspace/index.ts index 973d199..b51a84a 100644 --- a/src/space/services/subspace/index.ts +++ b/src/space/services/subspace/index.ts @@ -1,2 +1,4 @@ export * from './subspace.service'; export * from './subspace-device.service'; +export * from './subspace-product-item.service'; +export * from './subspace-product.service'; diff --git a/src/space/services/subspace/subspace-device.service.ts b/src/space/services/subspace/subspace-device.service.ts index e773ea4..d6f6320 100644 --- a/src/space/services/subspace/subspace-device.service.ts +++ b/src/space/services/subspace/subspace-device.service.ts @@ -1,29 +1,23 @@ import { BaseResponseDto } from '@app/common/dto/base.response.dto'; import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; import { DeviceRepository } from '@app/common/modules/device/repositories'; -import { - SpaceRepository, - SubspaceRepository, -} from '@app/common/modules/space/repositories'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { DeviceSubSpaceParam, GetSubSpaceParam } from '../../dtos'; -import { CommunityRepository } from '@app/common/modules/community/repositories'; import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; import { ProductRepository } from '@app/common/modules/product/repositories'; import { GetDeviceDetailsInterface } from '../../../device/interfaces/get.device.interface'; -import { SpaceService } from '../space.service'; +import { ValidationService } from '../space-validation.service'; +import { SubspaceRepository } from '@app/common/modules/space/repositories/subspace.repository'; @Injectable() export class SubspaceDeviceService { constructor( - private readonly spaceRepository: SpaceRepository, - private readonly communityRepository: CommunityRepository, private readonly subspaceRepository: SubspaceRepository, private readonly deviceRepository: DeviceRepository, private readonly tuyaService: TuyaService, private readonly productRepository: ProductRepository, - private readonly spaceService: SpaceService, + private readonly validationService: ValidationService, ) {} async listDevicesInSubspace( @@ -31,10 +25,10 @@ export class SubspaceDeviceService { ): Promise { const { subSpaceUuid, spaceUuid, communityUuid, projectUuid } = params; - await this.spaceService.validateCommunityAndSpace( + await this.validationService.validateSpaceWithinCommunityAndProject( communityUuid, - spaceUuid, projectUuid, + spaceUuid, ); const subspace = await this.findSubspaceWithDevices(subSpaceUuid); @@ -76,10 +70,10 @@ export class SubspaceDeviceService { const { subSpaceUuid, deviceUuid, spaceUuid, communityUuid, projectUuid } = params; try { - await this.spaceService.validateCommunityAndSpace( + await this.validationService.validateSpaceWithinCommunityAndProject( communityUuid, - spaceUuid, projectUuid, + spaceUuid, ); const subspace = await this.findSubspace(subSpaceUuid); @@ -111,10 +105,10 @@ export class SubspaceDeviceService { const { subSpaceUuid, deviceUuid, spaceUuid, communityUuid, projectUuid } = params; try { - await this.spaceService.validateCommunityAndSpace( + await this.validationService.validateSpaceWithinCommunityAndProject( communityUuid, - spaceUuid, projectUuid, + spaceUuid, ); const subspace = await this.findSubspace(subSpaceUuid); diff --git a/src/space/services/subspace/subspace-product-item.service.ts b/src/space/services/subspace/subspace-product-item.service.ts new file mode 100644 index 0000000..647a597 --- /dev/null +++ b/src/space/services/subspace/subspace-product-item.service.ts @@ -0,0 +1,92 @@ +import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; +import { QueryRunner } from 'typeorm'; + +import { + SpaceEntity, + SubspaceProductEntity, + SubspaceProductItemEntity, +} from '@app/common/modules/space'; +import { + SubspaceProductItemModelEntity, + SubspaceProductModelEntity, +} from '@app/common/modules/space-model'; +import { SubspaceProductItemRepository } from '@app/common/modules/space/repositories/subspace.repository'; +import { CreateSpaceProductItemDto } from '../../dtos'; +import { BaseProductItemService } from '../../common'; + +@Injectable() +export class SubspaceProductItemService extends BaseProductItemService { + constructor( + private readonly productItemRepository: SubspaceProductItemRepository, + ) { + super(); + } + + async createItemFromModel( + product: SubspaceProductEntity, + productModel: SubspaceProductModelEntity, + queryRunner: QueryRunner, + ): Promise { + const itemModels = productModel.itemModels; + + if (!itemModels?.length) return; + + try { + const productItems = itemModels.map((model) => + this.createProductItem(product, model, queryRunner), + ); + + await queryRunner.manager.save( + this.productItemRepository.target, + productItems, + ); + } catch (error) { + throw new HttpException( + error.message || 'An error occurred while creating product items.', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + private createProductItem( + product: SubspaceProductEntity, + model: SubspaceProductItemModelEntity, + queryRunner: QueryRunner, + ): Partial { + return queryRunner.manager.create(this.productItemRepository.target, { + tag: model.tag, + product, + model, + }); + } + + async createItemFromDtos( + product: SubspaceProductEntity, + itemDto: CreateSpaceProductItemDto[], + queryRunner: QueryRunner, + space: SpaceEntity, + ) { + if (!itemDto?.length) return; + const incomingTags = itemDto.map((item) => item.tag); + await this.validateTags(incomingTags, queryRunner, space.uuid); + + try { + const productItems = itemDto.map((dto) => + queryRunner.manager.create(SubspaceProductItemEntity, { + tag: dto.tag, + subspaceProduct: product, + }), + ); + + await queryRunner.manager.save( + this.productItemRepository.target, + productItems, + ); + } catch (error) { + throw new HttpException( + error.message || 'An error occurred while creating product items.', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/space/services/subspace/subspace-product.service.ts b/src/space/services/subspace/subspace-product.service.ts new file mode 100644 index 0000000..e789311 --- /dev/null +++ b/src/space/services/subspace/subspace-product.service.ts @@ -0,0 +1,128 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { QueryRunner } from 'typeorm'; + +import { + SpaceEntity, + SubspaceEntity, + SubspaceProductEntity, +} from '@app/common/modules/space'; +import { + SubspaceModelEntity, + SubspaceProductModelEntity, +} from '@app/common/modules/space-model'; +import { SubspaceProductItemService } from './subspace-product-item.service'; +import { ProductAssignmentDto } from 'src/space/dtos'; +import { ProductService } from 'src/product/services'; +import { ProductEntity } from '@app/common/modules/product/entities'; + +@Injectable() +export class SubspaceProductService { + constructor( + private readonly subspaceProductItemService: SubspaceProductItemService, + private readonly productService: ProductService, + ) {} + + async createFromModel( + subspaceModel: SubspaceModelEntity, + subspace: SubspaceEntity, + queryRunner: QueryRunner, + ): Promise { + const productModels = subspaceModel.productModels; + if (!productModels?.length) return; + + try { + const newSpaceProducts = productModels.map((productModel) => + this.createSubspaceProductEntity(subspace, productModel), + ); + + const subspaceProducts = await queryRunner.manager.save( + SubspaceProductEntity, + newSpaceProducts, + ); + + await Promise.all( + subspaceProducts.map((subspaceProduct, index) => + this.subspaceProductItemService.createItemFromModel( + subspaceProduct, + productModels[index], + queryRunner, + ), + ), + ); + } catch (error) { + throw new HttpException( + `Transaction failed: Unable to create subspace products ${error}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + private createSubspaceProductEntity( + subspace: SubspaceEntity, + productModel: SubspaceProductModelEntity, + ): Partial { + return { + subspace, + product: productModel.product, + productCount: productModel.productCount, + model: productModel, + }; + } + + async createFromDto( + productDtos: ProductAssignmentDto[], + subspace: SubspaceEntity, + queryRunner: QueryRunner, + space: SpaceEntity, + ): Promise { + try { + const newSpaceProducts = await Promise.all( + productDtos.map(async (dto) => { + this.validateProductCount(dto); + + const product = await this.getProduct(dto.productId); + return queryRunner.manager.create(SubspaceProductEntity, { + subspace, + product, + productCount: dto.count, + }); + }), + ); + + const subspaceProducts = await queryRunner.manager.save( + SubspaceProductEntity, + newSpaceProducts, + ); + + await Promise.all( + productDtos.map((dto, index) => + this.subspaceProductItemService.createItemFromDtos( + subspaceProducts[index], + dto.items, + queryRunner, + space, + ), + ), + ); + } catch (error) { + throw new HttpException( + `Failed to create subspace products from DTOs. Error: ${error.message}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async getProduct(productId: string): Promise { + const product = await this.productService.findOne(productId); + return product.data; + } + + async validateProductCount(dto: ProductAssignmentDto) { + if (dto.count !== dto.items.length) { + throw new HttpException( + 'Producy item and count doesnot match', + HttpStatus.BAD_REQUEST, + ); + } + } +} diff --git a/src/space/services/subspace/subspace.service.ts b/src/space/services/subspace/subspace.service.ts index c343512..6b9a1a3 100644 --- a/src/space/services/subspace/subspace.service.ts +++ b/src/space/services/subspace/subspace.service.ts @@ -1,5 +1,4 @@ import { BaseResponseDto } from '@app/common/dto/base.response.dto'; -import { SubspaceRepository } from '@app/common/modules/space/repositories'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { AddSubspaceDto, GetSpaceParam, GetSubSpaceParam } from '../../dtos'; import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; @@ -9,25 +8,119 @@ import { } from '@app/common/models/typeOrmCustom.model'; import { PageResponse } from '@app/common/dto/pagination.response.dto'; import { SubspaceDto } from '@app/common/modules/space/dtos'; -import { SpaceService } from '../space.service'; +import { QueryRunner } from 'typeorm'; +import { + SpaceEntity, + SubspaceEntity, +} from '@app/common/modules/space/entities'; +import { + SpaceModelEntity, + SubspaceModelEntity, +} from '@app/common/modules/space-model'; +import { ValidationService } from '../space-validation.service'; +import { SubspaceRepository } from '@app/common/modules/space/repositories/subspace.repository'; +import { SubspaceProductService } from './subspace-product.service'; @Injectable() export class SubSpaceService { constructor( private readonly subspaceRepository: SubspaceRepository, - private readonly spaceService: SpaceService, + private readonly validationService: ValidationService, + private readonly productService: SubspaceProductService, ) {} + async createSubspaces( + subspaceData: Array<{ + subspaceName: string; + space: SpaceEntity; + subSpaceModel?: SubspaceModelEntity; + }>, + queryRunner: QueryRunner, + ): Promise { + try { + const subspaces = subspaceData.map((data) => + queryRunner.manager.create(this.subspaceRepository.target, data), + ); + + return await queryRunner.manager.save(subspaces); + } catch (error) { + throw new HttpException( + 'An unexpected error occurred while creating subspaces.', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async createSubSpaceFromModel( + spaceModel: SpaceModelEntity, + space: SpaceEntity, + queryRunner: QueryRunner, + ): Promise { + const subSpaceModels = spaceModel.subspaceModels; + + if (!subSpaceModels?.length) return; + + const subspaceData = subSpaceModels.map((subSpaceModel) => ({ + subspaceName: subSpaceModel.subspaceName, + space, + subSpaceModel, + })); + + const subspaces = await this.createSubspaces(subspaceData, queryRunner); + + await Promise.all( + subSpaceModels.map((model, index) => { + this.productService.createFromModel( + model, + subspaces[index], + queryRunner, + ); + }), + ); + } + + async createSubspacesFromDto( + addSubspaceDtos: AddSubspaceDto[], + space: SpaceEntity, + queryRunner: QueryRunner, + ): Promise { + try { + const subspaceData = addSubspaceDtos.map((dto) => ({ + subspaceName: dto.subspaceName, + space, + })); + + const subspaces = await this.createSubspaces(subspaceData, queryRunner); + + await Promise.all( + addSubspaceDtos.map((dto, index) => + this.productService.createFromDto( + dto.products, + subspaces[index], + queryRunner, + space, + ), + ), + ); + + return subspaces; + } catch (error) { + throw new Error( + `Transaction failed: Unable to create subspaces and products. ${error.message}`, + ); + } + } + async createSubspace( addSubspaceDto: AddSubspaceDto, params: GetSpaceParam, ): Promise { - const { communityUuid, spaceUuid, projectUuid } = params; - const space = await this.spaceService.validateCommunityAndSpace( - communityUuid, - spaceUuid, - projectUuid, - ); + const space = + await this.validationService.validateSpaceWithinCommunityAndProject( + params.projectUuid, + params.projectUuid, + params.spaceUuid, + ); try { const newSubspace = this.subspaceRepository.create({ @@ -52,10 +145,10 @@ export class SubSpaceService { pageable: Partial, ): Promise { const { communityUuid, spaceUuid, projectUuid } = params; - await this.spaceService.validateCommunityAndSpace( + await this.validationService.validateSpaceWithinCommunityAndProject( communityUuid, - spaceUuid, projectUuid, + spaceUuid, ); try { @@ -76,10 +169,10 @@ export class SubSpaceService { async findOne(params: GetSubSpaceParam): Promise { const { communityUuid, subSpaceUuid, spaceUuid, projectUuid } = params; - await this.spaceService.validateCommunityAndSpace( + await this.validationService.validateSpaceWithinCommunityAndProject( communityUuid, - spaceUuid, projectUuid, + spaceUuid, ); try { const subSpace = await this.subspaceRepository.findOne({ @@ -116,12 +209,11 @@ export class SubSpaceService { updateSubSpaceDto: AddSubspaceDto, ): Promise { const { spaceUuid, communityUuid, subSpaceUuid, projectUuid } = params; - await this.spaceService.validateCommunityAndSpace( + await this.validationService.validateSpaceWithinCommunityAndProject( communityUuid, - spaceUuid, projectUuid, + spaceUuid, ); - const subSpace = await this.subspaceRepository.findOne({ where: { uuid: subSpaceUuid }, }); @@ -156,10 +248,10 @@ export class SubSpaceService { async delete(params: GetSubSpaceParam): Promise { const { spaceUuid, communityUuid, subSpaceUuid, projectUuid } = params; - await this.spaceService.validateCommunityAndSpace( + await this.validationService.validateSpaceWithinCommunityAndProject( communityUuid, - spaceUuid, projectUuid, + spaceUuid, ); const subspace = await this.subspaceRepository.findOne({ diff --git a/src/space/space.module.ts b/src/space/space.module.ts index 002c2df..81d99e2 100644 --- a/src/space/space.module.ts +++ b/src/space/space.module.ts @@ -12,18 +12,20 @@ import { import { SpaceDeviceService, SpaceLinkService, + SpaceProductItemService, SpaceProductService, SpaceSceneService, SpaceService, SpaceUserService, SubspaceDeviceService, + SubspaceProductItemService, SubSpaceService, } from './services'; import { SpaceProductRepository, SpaceRepository, - SubspaceRepository, SpaceLinkRepository, + SpaceProductItemRepository, } from '@app/common/modules/space/repositories'; import { CommunityRepository } from '@app/common/modules/community/repositories'; import { @@ -43,9 +45,18 @@ import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories'; import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories'; import { ProjectRepository } from '@app/common/modules/project/repositiories'; +import { SpaceModelRepository } from '@app/common/modules/space-model'; +import { CommunityModule } from 'src/community/community.module'; +import { ValidationService } from './services'; +import { + SubspaceProductItemRepository, + SubspaceProductRepository, + SubspaceRepository, +} from '@app/common/modules/space/repositories/subspace.repository'; +import { SubspaceProductService } from './services'; @Module({ - imports: [ConfigModule, SpaceRepositoryModule], + imports: [ConfigModule, SpaceRepositoryModule, CommunityModule], controllers: [ SpaceController, SpaceUserController, @@ -55,6 +66,7 @@ import { ProjectRepository } from '@app/common/modules/project/repositiories'; SpaceSceneController, ], providers: [ + ValidationService, SpaceService, TuyaService, ProductRepository, @@ -76,11 +88,19 @@ import { ProjectRepository } from '@app/common/modules/project/repositiories'; SceneRepository, DeviceService, DeviceStatusFirebaseService, + SubspaceProductItemRepository, DeviceStatusLogRepository, SceneDeviceRepository, SpaceProductService, SpaceProductRepository, ProjectRepository, + SpaceModelRepository, + SubspaceRepository, + SpaceProductItemService, + SpaceProductItemRepository, + SubspaceProductService, + SubspaceProductItemService, + SubspaceProductRepository, ], exports: [SpaceService], })