diff --git a/libs/common/src/common.module.ts b/libs/common/src/common.module.ts index 582510d..91e5507 100644 --- a/libs/common/src/common.module.ts +++ b/libs/common/src/common.module.ts @@ -9,13 +9,10 @@ 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 { SpaceRepository } from './modules/space'; import { SpaceModelRepository, - SpaceProductModelRepository, SubspaceModelRepository, - SubspaceProductItemModelRepository, - SubspaceProductModelRepository, } from './modules/space-model'; import { SubspaceRepository } from './modules/space/repositories/subspace.repository'; @Module({ @@ -28,11 +25,7 @@ import { SubspaceRepository } from './modules/space/repositories/subspace.reposi SpaceRepository, SubspaceRepository, SubspaceModelRepository, - SubspaceProductModelRepository, - SubspaceProductItemModelRepository, SpaceModelRepository, - SpaceProductModelRepository, - SpaceProductItemRepository, ], exports: [ CommonService, @@ -45,10 +38,7 @@ import { SubspaceRepository } from './modules/space/repositories/subspace.reposi SpaceRepository, SubspaceRepository, SubspaceModelRepository, - SubspaceProductModelRepository, - SubspaceProductItemModelRepository, SpaceModelRepository, - SpaceProductModelRepository, ], imports: [ ConfigModule.forRoot({ diff --git a/libs/common/src/constants/controller-route.ts b/libs/common/src/constants/controller-route.ts index 7ef8808..572bb9c 100644 --- a/libs/common/src/constants/controller-route.ts +++ b/libs/common/src/constants/controller-route.ts @@ -21,6 +21,11 @@ export class ControllerRoute { public static readonly DELETE_PROJECT_SUMMARY = 'Delete a project'; public static readonly DELETE_PROJECT_DESCRIPTION = 'This endpoint deletes an existing project by its unique identifier (UUID).'; + + public static readonly GET_USERS_BY_PROJECT_SUMMARY = + 'Get users by project'; + public static readonly GET_USERS_BY_PROJECT_DESCRIPTION = + 'This endpoint retrieves all users associated with a specific project.'; }; }; @@ -277,6 +282,14 @@ export class ControllerRoute { public static readonly LIST_SPACE_MODEL_SUMMARY = 'List Space Models'; public static readonly LIST_SPACE_MODEL_DESCRIPTION = 'This endpoint allows you to retrieve a list of space models within a specified project. Each space model includes its structure, associated subspaces, products, and product items.'; + + public static readonly UPDATE_SPACE_MODEL_SUMMARY = 'Update Space Model'; + public static readonly UPDATE_SPACE_MODEL_DESCRIPTION = + 'This endpoint allows you to update a Space Model attributesas well as manage its associated Subspaces and Device'; + + public static readonly DELETE_SPACE_MODEL_SUMMARY = 'Delete Space Model'; + public static readonly DELETE_SPACE_MODEL_DESCRIPTION = + 'This endpoint allows you to delete a specified Space Model within a project. Deleting a Space Model disables the model and all its associated subspaces and tags, ensuring they are no longer active but remain in the system for auditing.'; }; }; @@ -733,6 +746,7 @@ export class ControllerRoute { public static readonly CREATE_USER_INVITATION_DESCRIPTION = 'This endpoint creates an invitation for a user to assign to role and spaces.'; + public static readonly CHECK_EMAIL_SUMMARY = 'Check email'; public static readonly CHECK_EMAIL_DESCRIPTION = diff --git a/libs/common/src/constants/modify-action.enum.ts b/libs/common/src/constants/modify-action.enum.ts new file mode 100644 index 0000000..28d6f77 --- /dev/null +++ b/libs/common/src/constants/modify-action.enum.ts @@ -0,0 +1,5 @@ +export enum ModifyAction { + ADD = 'add', + UPDATE = 'update', + DELETE = 'delete', +} diff --git a/libs/common/src/constants/permissions-mapping.ts b/libs/common/src/constants/permissions-mapping.ts index e940933..09a4914 100644 --- a/libs/common/src/constants/permissions-mapping.ts +++ b/libs/common/src/constants/permissions-mapping.ts @@ -12,7 +12,8 @@ export const PermissionMapping = { 'ADD', 'UPDATE', 'DELETE', - 'MODULE_ADD', + 'MODEL_ADD', + 'MODEL_DELETE', 'MODEL_VIEW', 'ASSIGN_USER_TO_SPACE', 'DELETE_USER_FROM_SPACE', diff --git a/libs/common/src/constants/role-permissions.ts b/libs/common/src/constants/role-permissions.ts index aae658a..c457c3c 100644 --- a/libs/common/src/constants/role-permissions.ts +++ b/libs/common/src/constants/role-permissions.ts @@ -16,8 +16,10 @@ export const RolePermissions = { 'SPACE_ADD', 'SPACE_UPDATE', 'SPACE_DELETE', - 'SPACE_MODULE_ADD', + 'SPACE_MODEL_ADD', 'SPACE_MODEL_VIEW', + 'SPACE_MODEL_UPDATE', + 'SPACE_MODEL_DELETE', 'ASSIGN_USER_TO_SPACE', 'DELETE_USER_FROM_SPACE', 'SUBSPACE_VIEW', @@ -60,8 +62,10 @@ export const RolePermissions = { 'SPACE_ADD', 'SPACE_UPDATE', 'SPACE_DELETE', - 'SPACE_MODULE_ADD', + 'SPACE_MODEL_ADD', 'SPACE_MODEL_VIEW', + 'SPACE_MODEL_UPDATE', + 'SPACE_MODEL_DELETE', 'ASSIGN_USER_TO_SPACE', 'DELETE_USER_FROM_SPACE', 'SUBSPACE_VIEW', diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index 5357904..472772b 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -11,10 +11,8 @@ import { PermissionTypeEntity } from '../modules/permission/entities'; import { SpaceEntity, SpaceLinkEntity, - SpaceProductItemEntity, SubspaceEntity, - SubspaceProductEntity, - SubspaceProductItemEntity, + TagEntity, } from '../modules/space/entities'; import { UserSpaceEntity } from '../modules/user/entities'; import { DeviceUserPermissionEntity } from '../modules/device/entities'; @@ -28,15 +26,11 @@ import { CommunityEntity } from '../modules/community/entities'; import { DeviceStatusLogEntity } from '../modules/device-status-log/entities'; import { SceneEntity, SceneIconEntity } from '../modules/scene/entities'; import { SceneDeviceEntity } from '../modules/scene-device/entities'; -import { SpaceProductEntity } from '../modules/space/entities/space-product.entity'; import { ProjectEntity } from '../modules/project/entities'; import { SpaceModelEntity, - SpaceProductItemModelEntity, - SpaceProductModelEntity, SubspaceModelEntity, - SubspaceProductItemModelEntity, - SubspaceProductModelEntity, + TagModel, } from '../modules/space-model/entities'; import { InviteUserEntity, @@ -68,7 +62,7 @@ import { SpaceEntity, SpaceLinkEntity, SubspaceEntity, - SpaceProductEntity, + TagEntity, UserSpaceEntity, DeviceUserPermissionEntity, RoleTypeEntity, @@ -82,15 +76,8 @@ import { SceneIconEntity, SceneDeviceEntity, SpaceModelEntity, - SpaceProductModelEntity, - SpaceProductItemModelEntity, SubspaceModelEntity, - SpaceProductEntity, - SpaceProductItemEntity, - SubspaceProductModelEntity, - SubspaceProductItemModelEntity, - SubspaceProductEntity, - SubspaceProductItemEntity, + TagModel, InviteUserEntity, InviteUserSpaceEntity, ], diff --git a/libs/common/src/modules/Invite-user/entities/Invite-user.entity.ts b/libs/common/src/modules/Invite-user/entities/Invite-user.entity.ts index c93f94b..3eac7d6 100644 --- a/libs/common/src/modules/Invite-user/entities/Invite-user.entity.ts +++ b/libs/common/src/modules/Invite-user/entities/Invite-user.entity.ts @@ -34,7 +34,7 @@ export class InviteUserEntity extends AbstractEntity { email: string; @Column({ - nullable: false, + nullable: true, }) jobTitle: string; @@ -52,7 +52,7 @@ export class InviteUserEntity extends AbstractEntity { }) public lastName: string; @Column({ - nullable: false, + nullable: true, }) public phoneNumber: string; diff --git a/libs/common/src/modules/device/entities/device.entity.ts b/libs/common/src/modules/device/entities/device.entity.ts index 9a75950..bdd1ce5 100644 --- a/libs/common/src/modules/device/entities/device.entity.ts +++ b/libs/common/src/modules/device/entities/device.entity.ts @@ -6,10 +6,11 @@ import { Unique, Index, JoinColumn, + OneToOne, } from 'typeorm'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { DeviceDto, DeviceUserPermissionDto } from '../dtos/device.dto'; -import { SpaceEntity, SubspaceEntity } from '../../space/entities'; +import { SpaceEntity, SubspaceEntity, TagEntity } from '../../space/entities'; import { ProductEntity } from '../../product/entities'; import { UserEntity } from '../../user/entities'; import { DeviceNotificationDto } from '../dtos'; @@ -74,6 +75,11 @@ export class DeviceEntity extends AbstractEntity { @OneToMany(() => SceneDeviceEntity, (sceneDevice) => sceneDevice.device, {}) sceneDevices: SceneDeviceEntity[]; + @OneToOne(() => TagEntity, (tag) => tag.device, { + nullable: true, + }) + tag: TagEntity; + constructor(partial: Partial) { super(); Object.assign(this, partial); @@ -102,6 +108,7 @@ export class DeviceNotificationEntity extends AbstractEntity) { super(); Object.assign(this, partial); diff --git a/libs/common/src/modules/product/entities/product.entity.ts b/libs/common/src/modules/product/entities/product.entity.ts index 926d179..dd7a1e5 100644 --- a/libs/common/src/modules/product/entities/product.entity.ts +++ b/libs/common/src/modules/product/entities/product.entity.ts @@ -2,10 +2,8 @@ import { Column, Entity, OneToMany } from 'typeorm'; import { ProductDto } from '../dtos'; 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'; - +import { TagModel } from '../../space-model'; +import { TagEntity } from '../../space/entities/tag.entity'; @Entity({ name: 'product' }) export class ProductEntity extends AbstractEntity { @Column({ @@ -29,20 +27,11 @@ export class ProductEntity extends AbstractEntity { }) public prodType: string; - @OneToMany(() => SpaceProductEntity, (spaceProduct) => spaceProduct.product) - spaceProducts: SpaceProductEntity[]; + @OneToMany(() => TagModel, (tag) => tag.product) + tagModels: TagModel[]; - @OneToMany( - () => SpaceProductModelEntity, - (spaceProductModel) => spaceProductModel.product, - ) - spaceProductModels: SpaceProductModelEntity[]; - - @OneToMany( - () => SubspaceProductModelEntity, - (subspaceProductModel) => subspaceProductModel.product, - ) - subpaceProductModels: SubspaceProductModelEntity[]; + @OneToMany(() => TagEntity, (tag) => tag.product) + tags: TagEntity[]; @OneToMany( () => DeviceEntity, diff --git a/libs/common/src/modules/scene-device/entities/scene-device.entity.ts b/libs/common/src/modules/scene-device/entities/scene-device.entity.ts index 9814fdf..a5f1fb5 100644 --- a/libs/common/src/modules/scene-device/entities/scene-device.entity.ts +++ b/libs/common/src/modules/scene-device/entities/scene-device.entity.ts @@ -44,6 +44,12 @@ export class SceneDeviceEntity extends AbstractEntity { @JoinColumn({ name: 'scene_uuid' }) scene: SceneEntity; + @Column({ + nullable: false, + default: false, + }) + public disabled: boolean; + constructor(partial: Partial) { super(); Object.assign(this, partial); diff --git a/libs/common/src/modules/scene/entities/scene.entity.ts b/libs/common/src/modules/scene/entities/scene.entity.ts index 5daa690..86b1beb 100644 --- a/libs/common/src/modules/scene/entities/scene.entity.ts +++ b/libs/common/src/modules/scene/entities/scene.entity.ts @@ -59,6 +59,12 @@ export class SceneEntity extends AbstractEntity { @JoinColumn({ name: 'space_uuid' }) space: SpaceEntity; + @Column({ + nullable: false, + default: false, + }) + public disabled: boolean; + @ManyToOne(() => SceneIconEntity, (icon) => icon.scenesIconEntity, { nullable: false, }) diff --git a/libs/common/src/modules/space-model/dtos/index.ts b/libs/common/src/modules/space-model/dtos/index.ts index 9ba8130..242564d 100644 --- a/libs/common/src/modules/space-model/dtos/index.ts +++ b/libs/common/src/modules/space-model/dtos/index.ts @@ -1,4 +1,2 @@ export * from './subspace-model'; export * from './space-model.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 deleted file mode 100644 index 5eb9bca..0000000 --- a/libs/common/src/modules/space-model/dtos/space-product-item-model.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -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 deleted file mode 100644 index 7952a23..0000000 --- a/libs/common/src/modules/space-model/dtos/space-product-model.dto.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsNumber, IsString } from 'class-validator'; -import { SpaceProductItemModelDto } from './space-product-item-model.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: [SpaceProductItemModelDto], - }) - 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 index 70337b6..fb0ac5a 100644 --- a/libs/common/src/modules/space-model/dtos/subspace-model/index.ts +++ b/libs/common/src/modules/space-model/dtos/subspace-model/index.ts @@ -1,3 +1 @@ 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/subspace-product-item-model.dto.ts b/libs/common/src/modules/space-model/dtos/subspace-model/subspace-product-item-model.dto.ts deleted file mode 100644 index 479642b..0000000 --- a/libs/common/src/modules/space-model/dtos/subspace-model/subspace-product-item-model.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -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 deleted file mode 100644 index 4eebaae..0000000 --- a/libs/common/src/modules/space-model/dtos/subspace-model/subspace-product-model.dto.ts +++ /dev/null @@ -1,23 +0,0 @@ -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/dtos/tag-model.dto.ts b/libs/common/src/modules/space-model/dtos/tag-model.dto.ts new file mode 100644 index 0000000..f98f160 --- /dev/null +++ b/libs/common/src/modules/space-model/dtos/tag-model.dto.ts @@ -0,0 +1,21 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class TagModelDto { + @IsString() + @IsNotEmpty() + public uuid: string; + + @IsString() + @IsNotEmpty() + public name: string; + + @IsString() + @IsNotEmpty() + public productUuid: string; + + @IsString() + spaceModelUuid: string; + + @IsString() + subspaceModelUuid: string; +} diff --git a/libs/common/src/modules/space-model/entities/index.ts b/libs/common/src/modules/space-model/entities/index.ts index ec19308..f4fffbd 100644 --- a/libs/common/src/modules/space-model/entities/index.ts +++ b/libs/common/src/modules/space-model/entities/index.ts @@ -1,4 +1,3 @@ export * from './space-model.entity'; -export * from './space-product-item-model.entity'; -export * from './space-product-model.entity'; export * from './subspace-model'; +export * from './tag-model.entity'; 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 591d04f..18d0688 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 @@ -9,9 +9,9 @@ import { import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { SpaceModelDto } from '../dtos'; import { SubspaceModelEntity } from './subspace-model'; -import { SpaceProductModelEntity } from './space-product-model.entity'; import { ProjectEntity } from '../../project/entities'; import { SpaceEntity } from '../../space/entities'; +import { TagModel } from './tag-model.entity'; @Entity({ name: 'space-model' }) @Unique(['modelName', 'project']) @@ -28,6 +28,12 @@ export class SpaceModelEntity extends AbstractEntity { }) public modelName: string; + @Column({ + nullable: false, + default: false, + }) + public disabled: boolean; + @ManyToOne(() => ProjectEntity, (project) => project.spaceModels, { nullable: false, onDelete: 'CASCADE', @@ -45,17 +51,16 @@ export class SpaceModelEntity extends AbstractEntity { ) public subspaceModels: SubspaceModelEntity[]; - @OneToMany( - () => SpaceProductModelEntity, - (productModel) => productModel.spaceModel, - { - nullable: true, - }, - ) - public spaceProductModels: SpaceProductModelEntity[]; - @OneToMany(() => SpaceEntity, (space) => space.spaceModel, { cascade: true, }) public spaces: SpaceEntity[]; + + @OneToMany(() => TagModel, (tag) => tag.spaceModel) + tags: TagModel[]; + + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } } diff --git a/libs/common/src/modules/space-model/entities/space-product-item-model.entity.ts b/libs/common/src/modules/space-model/entities/space-product-item-model.entity.ts deleted file mode 100644 index 062418f..0000000 --- a/libs/common/src/modules/space-model/entities/space-product-item-model.entity.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Entity, Column, ManyToOne, OneToMany } from 'typeorm'; -import { AbstractEntity } from '../../abstract/entities/abstract.entity'; -import { SpaceProductItemModelDto } from '../dtos'; -import { SpaceProductModelEntity } from './space-product-model.entity'; -import { SpaceProductItemEntity } from '../../space/entities'; - -@Entity({ name: 'space-product-item-model' }) -export class SpaceProductItemModelEntity extends AbstractEntity { - @Column({ - nullable: false, - }) - public tag: string; - - @ManyToOne( - () => SpaceProductModelEntity, - (spaceProductModel) => spaceProductModel.items, - { - nullable: false, - }, - ) - public spaceProductModel: SpaceProductModelEntity; - - @OneToMany( - () => SpaceProductItemEntity, - (spaceProductItem) => spaceProductItem.spaceProductItemModel, - { cascade: true }, - ) - 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 deleted file mode 100644 index 13b2b33..0000000 --- a/libs/common/src/modules/space-model/entities/space-product-model.entity.ts +++ /dev/null @@ -1,50 +0,0 @@ -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-model.entity'; -import { SpaceProductModelDto } from '../dtos'; -import { SpaceProductEntity } from '../../space/entities'; - -@Entity({ name: 'space-product-model' }) -export class SpaceProductModelEntity extends AbstractEntity { - @Column({ - nullable: false, - type: 'int', - }) - productCount: number; - - @ManyToOne( - () => SpaceModelEntity, - (spaceModel) => spaceModel.spaceProductModels, - { - nullable: false, - onDelete: 'CASCADE', - }, - ) - public spaceModel: SpaceModelEntity; - - @ManyToOne(() => ProductEntity, (product) => product.spaceProductModels, { - nullable: false, - onDelete: 'CASCADE', - }) - public product: ProductEntity; - - @OneToMany( - () => SpaceProductItemModelEntity, - (item) => item.spaceProductModel, - { - cascade: true, - }, - ) - public items: SpaceProductItemModelEntity[]; - - @OneToMany( - () => SpaceProductEntity, - (spaceProduct) => spaceProduct.spaceProductModel, - { - cascade: true, - }, - ) - public spaceProducts: SpaceProductEntity[]; -} 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 index e39403f..262490e 100644 --- a/libs/common/src/modules/space-model/entities/subspace-model/index.ts +++ b/libs/common/src/modules/space-model/entities/subspace-model/index.ts @@ -1,3 +1,2 @@ export * from './subspace-model.entity'; -export * from './subspace-product-item-model.entity'; -export * from './subspace-product-model.entity'; + \ No newline at end of file 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 index 89fcf63..e1d0be0 100644 --- 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 @@ -3,7 +3,7 @@ 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'; +import { TagModel } from '../tag-model.entity'; @Entity({ name: 'subspace-model' }) @Unique(['subspaceName', 'spaceModel']) @@ -30,17 +30,17 @@ export class SubspaceModelEntity extends AbstractEntity { ) public spaceModel: SpaceModelEntity; - @OneToMany(() => SubspaceEntity, (space) => space.subSpaceModel, { + @OneToMany(() => SubspaceEntity, (subspace) => subspace.subSpaceModel, { cascade: true, }) - public spaces: SubspaceEntity[]; + public subspaceModel: SubspaceEntity[]; - @OneToMany( - () => SubspaceProductModelEntity, - (productModel) => productModel.subspaceModel, - { - nullable: true, - }, - ) - public productModels: SubspaceProductModelEntity[]; + @Column({ + nullable: false, + default: false, + }) + public disabled: boolean; + + @OneToMany(() => TagModel, (tag) => tag.subspaceModel) + tags: TagModel[]; } 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 deleted file mode 100644 index 2b6d305..0000000 --- a/libs/common/src/modules/space-model/entities/subspace-model/subspace-product-item-model.entity.ts +++ /dev/null @@ -1,27 +0,0 @@ -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 deleted file mode 100644 index 843c70d..0000000 --- a/libs/common/src/modules/space-model/entities/subspace-model/subspace-product-model.entity.ts +++ /dev/null @@ -1,44 +0,0 @@ -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/entities/tag-model.entity.ts b/libs/common/src/modules/space-model/entities/tag-model.entity.ts new file mode 100644 index 0000000..3f7805c --- /dev/null +++ b/libs/common/src/modules/space-model/entities/tag-model.entity.ts @@ -0,0 +1,46 @@ +import { + Column, + Entity, + JoinColumn, + ManyToOne, + OneToMany, + Unique, +} from 'typeorm'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { TagModelDto } from '../dtos/tag-model.dto'; +import { SpaceModelEntity } from './space-model.entity'; +import { SubspaceModelEntity } from './subspace-model'; +import { ProductEntity } from '../../product/entities'; +import { TagEntity } from '../../space/entities/tag.entity'; + +@Entity({ name: 'tag_model' }) +@Unique(['tag', 'product', 'spaceModel', 'subspaceModel']) +export class TagModel extends AbstractEntity { + @Column({ type: 'varchar', length: 255 }) + tag: string; + + @ManyToOne(() => ProductEntity, (product) => product.tagModels, { + nullable: false, + }) + @JoinColumn({ name: 'product_id' }) + product: ProductEntity; + + @ManyToOne(() => SpaceModelEntity, (space) => space.tags, { nullable: true }) + @JoinColumn({ name: 'space_model_id' }) + spaceModel: SpaceModelEntity; + + @ManyToOne(() => SubspaceModelEntity, (subspace) => subspace.tags, { + nullable: true, + }) + @JoinColumn({ name: 'subspace_model_id' }) + subspaceModel: SubspaceModelEntity; + + @Column({ + nullable: false, + default: false, + }) + public disabled: boolean; + + @OneToMany(() => TagEntity, (tag) => tag.model) + tags: TagEntity[]; +} 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 fc92f14..4af0d57 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 @@ -1,13 +1,6 @@ import { DataSource, Repository } from 'typeorm'; import { Injectable } from '@nestjs/common'; -import { - SpaceModelEntity, - SpaceProductItemModelEntity, - SpaceProductModelEntity, - SubspaceModelEntity, - SubspaceProductItemModelEntity, - SubspaceProductModelEntity, -} from '../entities'; +import { SpaceModelEntity, SubspaceModelEntity, TagModel } from '../entities'; @Injectable() export class SpaceModelRepository extends Repository { @@ -23,28 +16,8 @@ export class SubspaceModelRepository extends Repository { } @Injectable() -export class SubspaceProductModelRepository extends Repository { +export class TagModelRepository 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) { - super(SpaceProductModelEntity, dataSource.createEntityManager()); - } -} -@Injectable() -export class SpaceProductItemModelRepository extends Repository { - constructor(private dataSource: DataSource) { - super(SpaceProductItemModelEntity, dataSource.createEntityManager()); + super(TagModel, dataSource.createEntityManager()); } } diff --git a/libs/common/src/modules/space-model/space-model.repository.module.ts b/libs/common/src/modules/space-model/space-model.repository.module.ts index 573bba0..9a35d88 100644 --- a/libs/common/src/modules/space-model/space-model.repository.module.ts +++ b/libs/common/src/modules/space-model/space-model.repository.module.ts @@ -1,10 +1,5 @@ import { TypeOrmModule } from '@nestjs/typeorm'; -import { - SpaceModelEntity, - SpaceProductItemModelEntity, - SpaceProductModelEntity, - SubspaceModelEntity, -} from './entities'; +import { SpaceModelEntity, SubspaceModelEntity, TagModel } from './entities'; import { Module } from '@nestjs/common'; @Module({ @@ -12,12 +7,7 @@ import { Module } from '@nestjs/common'; exports: [], controllers: [], imports: [ - TypeOrmModule.forFeature([ - SpaceModelEntity, - SubspaceModelEntity, - SpaceProductModelEntity, - SpaceProductItemModelEntity, - ]), + TypeOrmModule.forFeature([SpaceModelEntity, SubspaceModelEntity, TagModel]), ], }) export class SpaceModelRepositoryModule {} diff --git a/libs/common/src/modules/space/dtos/index.ts b/libs/common/src/modules/space/dtos/index.ts index b470336..c511f8c 100644 --- a/libs/common/src/modules/space/dtos/index.ts +++ b/libs/common/src/modules/space/dtos/index.ts @@ -1,4 +1,3 @@ export * from './space.dto'; export * from './subspace.dto'; -export * from './space-product-item.dto'; -export * from './space-product.dto'; +export * from './tag.dto'; diff --git a/libs/common/src/modules/space/dtos/space-product-item.dto.ts b/libs/common/src/modules/space/dtos/space-product-item.dto.ts deleted file mode 100644 index 8973c1a..0000000 --- a/libs/common/src/modules/space/dtos/space-product-item.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { IsString, IsNotEmpty } from 'class-validator'; - -export class SpaceProductItemDto { - @IsString() - @IsNotEmpty() - uuid: string; - - @IsString() - @IsNotEmpty() - tag: string; - - @IsString() - @IsNotEmpty() - 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 deleted file mode 100644 index a57d29e..0000000 --- a/libs/common/src/modules/space/dtos/space-product.dto.ts +++ /dev/null @@ -1,23 +0,0 @@ -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/dtos/tag.dto.ts b/libs/common/src/modules/space/dtos/tag.dto.ts new file mode 100644 index 0000000..d988c62 --- /dev/null +++ b/libs/common/src/modules/space/dtos/tag.dto.ts @@ -0,0 +1,21 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class TagDto { + @IsString() + @IsNotEmpty() + public uuid: string; + + @IsString() + @IsNotEmpty() + public name: string; + + @IsString() + @IsNotEmpty() + public productUuid: string; + + @IsString() + spaceUuid: string; + + @IsString() + subspaceUuid: string; +} diff --git a/libs/common/src/modules/space/entities/index.ts b/libs/common/src/modules/space/entities/index.ts index f07ec93..5a514e6 100644 --- a/libs/common/src/modules/space/entities/index.ts +++ b/libs/common/src/modules/space/entities/index.ts @@ -1,5 +1,4 @@ export * from './space.entity'; export * from './subspace'; -export * from './space-product.entity'; -export * from './space-product-item.entity'; export * from './space-link.entity'; +export * from './tag.entity'; diff --git a/libs/common/src/modules/space/entities/space-link.entity.ts b/libs/common/src/modules/space/entities/space-link.entity.ts index a62ce4f..da11eb7 100644 --- a/libs/common/src/modules/space/entities/space-link.entity.ts +++ b/libs/common/src/modules/space/entities/space-link.entity.ts @@ -13,6 +13,12 @@ export class SpaceLinkEntity extends AbstractEntity { @JoinColumn({ name: 'end_space_id' }) public endSpace: SpaceEntity; + @Column({ + nullable: false, + default: false, + }) + public disabled: boolean; + @Column({ nullable: false, enum: Object.values(Direction), 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 deleted file mode 100644 index c53dfd4..0000000 --- a/libs/common/src/modules/space/entities/space-product-item.entity.ts +++ /dev/null @@ -1,27 +0,0 @@ -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 deleted file mode 100644 index a1d27ef..0000000 --- a/libs/common/src/modules/space/entities/space-product.entity.ts +++ /dev/null @@ -1,49 +0,0 @@ -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 { - @ManyToOne(() => SpaceEntity, (space) => space.spaceProducts, { - nullable: false, - onDelete: 'CASCADE', - }) - @JoinColumn({ name: 'space_uuid' }) - space: SpaceEntity; - - @ManyToOne(() => ProductEntity, (product) => product.spaceProducts, { - nullable: false, - onDelete: 'CASCADE', - }) - @JoinColumn({ name: 'product_uuid' }) - product: ProductEntity; - - @Column({ - nullable: true, - type: 'int', - }) - 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 63beed5..8ecbc70 100644 --- a/libs/common/src/modules/space/entities/space.entity.ts +++ b/libs/common/src/modules/space/entities/space.entity.ts @@ -13,10 +13,10 @@ import { DeviceEntity } from '../../device/entities'; import { CommunityEntity } from '../../community/entities'; 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'; +import { TagEntity } from './tag.entity'; @Entity({ name: 'space' }) @Unique(['invitationCode']) @@ -60,6 +60,12 @@ export class SpaceEntity extends AbstractEntity { @OneToMany(() => UserSpaceEntity, (userSpace) => userSpace.space) userSpaces: UserSpaceEntity[]; + @Column({ + nullable: false, + default: false, + }) + public disabled: boolean; + @OneToMany(() => SubspaceEntity, (subspace) => subspace.space, { nullable: true, }) @@ -94,14 +100,12 @@ export class SpaceEntity extends AbstractEntity { }) public icon: string; - @OneToMany(() => SpaceProductEntity, (spaceProduct) => spaceProduct.space) - spaceProducts: SpaceProductEntity[]; - @OneToMany(() => SceneEntity, (scene) => scene.space) scenes: SceneEntity[]; - @ManyToOne(() => SpaceModelEntity, { nullable: true }) - @JoinColumn({ name: 'space_model_uuid' }) + @ManyToOne(() => SpaceModelEntity, (spaceModel) => spaceModel.spaces, { + nullable: true, + }) spaceModel?: SpaceModelEntity; @OneToMany( @@ -109,6 +113,10 @@ export class SpaceEntity extends AbstractEntity { (inviteUserSpace) => inviteUserSpace.space, ) invitedUsers: InviteUserSpaceEntity[]; + + @OneToMany(() => TagEntity, (tag) => tag.space) + tags: TagEntity[]; + 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 index 471b7b1..be13961 100644 --- a/libs/common/src/modules/space/entities/subspace/index.ts +++ b/libs/common/src/modules/space/entities/subspace/index.ts @@ -1,3 +1 @@ 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 deleted file mode 100644 index 1ec7958..0000000 --- a/libs/common/src/modules/space/entities/subspace/subspace-product-item.entity.ts +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index ea98520..0000000 --- a/libs/common/src/modules/space/entities/subspace/subspace-product.entity.ts +++ /dev/null @@ -1,47 +0,0 @@ -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 { 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 index bc6ff06..c7247cc 100644 --- a/libs/common/src/modules/space/entities/subspace/subspace.entity.ts +++ b/libs/common/src/modules/space/entities/subspace/subspace.entity.ts @@ -4,7 +4,7 @@ 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'; +import { TagEntity } from '../tag.entity'; @Entity({ name: 'subspace' }) export class SubspaceEntity extends AbstractEntity { @@ -22,28 +22,28 @@ export class SubspaceEntity extends AbstractEntity { @ManyToOne(() => SpaceEntity, (space) => space.subspaces, { nullable: false, - onDelete: 'CASCADE', }) @JoinColumn({ name: 'space_uuid' }) space: SpaceEntity; + @Column({ + nullable: false, + default: false, + }) + public disabled: boolean; + @OneToMany(() => DeviceEntity, (device) => device.subspace, { nullable: true, }) devices: DeviceEntity[]; - @ManyToOne(() => SubspaceModelEntity, { nullable: true }) - @JoinColumn({ name: 'subspace_model_uuid' }) + @ManyToOne(() => SubspaceModelEntity, (subspace) => subspace.subspaceModel, { + nullable: true, + }) subSpaceModel?: SubspaceModelEntity; - @OneToMany( - () => SubspaceProductEntity, - (subspaceProduct) => subspaceProduct.subspace, - { - nullable: true, - }, - ) - public subspaceProducts: SubspaceProductEntity[]; + @OneToMany(() => TagEntity, (tag) => tag.subspace) + tags: TagEntity[]; constructor(partial: Partial) { super(); diff --git a/libs/common/src/modules/space/entities/tag.entity.ts b/libs/common/src/modules/space/entities/tag.entity.ts new file mode 100644 index 0000000..e7f8599 --- /dev/null +++ b/libs/common/src/modules/space/entities/tag.entity.ts @@ -0,0 +1,52 @@ +import { + Entity, + Column, + ManyToOne, + JoinColumn, + Unique, + OneToOne, +} from 'typeorm'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { ProductEntity } from '../../product/entities'; +import { TagDto } from '../dtos'; +import { TagModel } from '../../space-model/entities/tag-model.entity'; +import { SpaceEntity } from './space.entity'; +import { SubspaceEntity } from './subspace'; +import { DeviceEntity } from '../../device/entities'; + +@Entity({ name: 'tag' }) +@Unique(['tag', 'product', 'space', 'subspace']) +export class TagEntity extends AbstractEntity { + @Column({ type: 'varchar', length: 255 }) + tag: string; + + @ManyToOne(() => TagModel, (model) => model.tags, { + nullable: true, + }) + model: TagModel; + + @ManyToOne(() => ProductEntity, (product) => product.tags, { + nullable: false, + }) + product: ProductEntity; + + @ManyToOne(() => SpaceEntity, (space) => space.tags, { nullable: true }) + space: SpaceEntity; + + @ManyToOne(() => SubspaceEntity, (subspace) => subspace.tags, { + nullable: true, + }) + @JoinColumn({ name: 'subspace_id' }) + subspace: SubspaceEntity; + + @Column({ + nullable: false, + default: false, + }) + public disabled: boolean; + + @OneToOne(() => DeviceEntity, (device) => device.tag, { + nullable: true, + }) + device: DeviceEntity; +} diff --git a/libs/common/src/modules/space/repositories/space.repository.ts b/libs/common/src/modules/space/repositories/space.repository.ts index 66f96a4..b2aacc0 100644 --- a/libs/common/src/modules/space/repositories/space.repository.ts +++ b/libs/common/src/modules/space/repositories/space.repository.ts @@ -1,11 +1,6 @@ import { DataSource, Repository } from 'typeorm'; import { Injectable } from '@nestjs/common'; -import { SpaceProductEntity } from '../entities/space-product.entity'; -import { - SpaceEntity, - SpaceLinkEntity, - SpaceProductItemEntity, -} from '../entities'; +import { SpaceEntity, SpaceLinkEntity, TagEntity } from '../entities'; @Injectable() export class SpaceRepository extends Repository { @@ -20,16 +15,10 @@ export class SpaceLinkRepository extends Repository { super(SpaceLinkEntity, dataSource.createEntityManager()); } } -@Injectable() -export class SpaceProductRepository extends Repository { - constructor(private dataSource: DataSource) { - super(SpaceProductEntity, dataSource.createEntityManager()); - } -} @Injectable() -export class SpaceProductItemRepository extends Repository { +export class TagRepository extends Repository { constructor(private dataSource: DataSource) { - super(SpaceProductItemEntity, dataSource.createEntityManager()); + super(TagEntity, dataSource.createEntityManager()); } } diff --git a/libs/common/src/modules/space/repositories/subspace.repository.ts b/libs/common/src/modules/space/repositories/subspace.repository.ts index 3682c05..5897510 100644 --- a/libs/common/src/modules/space/repositories/subspace.repository.ts +++ b/libs/common/src/modules/space/repositories/subspace.repository.ts @@ -1,9 +1,5 @@ import { DataSource, Repository } from 'typeorm'; -import { - SubspaceEntity, - SubspaceProductEntity, - SubspaceProductItemEntity, -} from '../entities'; +import { SubspaceEntity } from '../entities'; import { Injectable } from '@nestjs/common'; @Injectable() @@ -12,17 +8,3 @@ export class SubspaceRepository extends Repository { 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 b39f98d..030c684 100644 --- a/libs/common/src/modules/space/space.repository.module.ts +++ b/libs/common/src/modules/space/space.repository.module.ts @@ -1,17 +1,11 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { SpaceEntity, SubspaceEntity, SubspaceProductEntity } from './entities'; +import { SpaceEntity, SubspaceEntity, TagEntity } from './entities'; @Module({ providers: [], exports: [], controllers: [], - imports: [ - TypeOrmModule.forFeature([ - SpaceEntity, - SubspaceEntity, - SubspaceProductEntity, - ]), - ], + imports: [TypeOrmModule.forFeature([SpaceEntity, SubspaceEntity, TagEntity])], }) export class SpaceRepositoryModule {} diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index add62c9..4846cd3 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -41,7 +41,7 @@ import { import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; import { DeviceRepository } from '@app/common/modules/device/repositories'; import { PermissionType } from '@app/common/constants/permission-type.enum'; -import { In } from 'typeorm'; +import { In, QueryRunner } from 'typeorm'; import { ProductType } from '@app/common/constants/product-type.enum'; import { SpaceRepository } from '@app/common/modules/space/repositories'; import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service'; @@ -59,6 +59,7 @@ import { DeviceSceneParamDto } from '../dtos/device.param.dto'; import { BaseResponseDto } from '@app/common/dto/base.response.dto'; import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; import { DeleteSceneFromSceneDeviceDto } from '../dtos/delete.device.dto'; +import { DeviceEntity } from '@app/common/modules/device/entities'; @Injectable() export class DeviceService { @@ -83,6 +84,7 @@ export class DeviceService { secretKey, }); } + async getDeviceByDeviceUuid( deviceUuid: string, withProductDevice: boolean = true, @@ -98,6 +100,29 @@ export class DeviceService { relations, }); } + async deleteDevice( + devices: DeviceEntity[], + orphanSpace: SpaceEntity, + queryRunner: QueryRunner, + ): Promise { + try { + const deviceIds = devices.map((device) => device.uuid); + + if (deviceIds.length > 0) { + await queryRunner.manager + .createQueryBuilder() + .update(DeviceEntity) + .set({ spaceDevice: orphanSpace }) + .whereInIds(deviceIds) + .execute(); + } + } catch (error) { + throw new HttpException( + `Failed to update devices to orphan space: ${error.message}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } async getDeviceByDeviceTuyaUuid(deviceTuyaUuid: string) { return await this.deviceRepository.findOne({ diff --git a/src/invite-user/dtos/add.invite-user.dto.ts b/src/invite-user/dtos/add.invite-user.dto.ts index 94f2e8a..0d9acbc 100644 --- a/src/invite-user/dtos/add.invite-user.dto.ts +++ b/src/invite-user/dtos/add.invite-user.dto.ts @@ -38,16 +38,16 @@ export class AddUserInvitationDto { @ApiProperty({ description: 'The job title of the user', example: 'Software Engineer', - required: true, + required: false, }) @IsString() - @IsNotEmpty() - public jobTitle: string; + @IsOptional() + public jobTitle?: string; @ApiProperty({ description: 'The phone number of the user', example: '+1234567890', - required: true, + required: false, }) @IsString() @IsOptional() diff --git a/src/invite-user/services/invite-user.service.ts b/src/invite-user/services/invite-user.service.ts index b49ed0b..aaeabd7 100644 --- a/src/invite-user/services/invite-user.service.ts +++ b/src/invite-user/services/invite-user.service.ts @@ -49,6 +49,7 @@ export class InviteUserService { try { const userRepo = queryRunner.manager.getRepository(UserEntity); + await this.checkEmailAndProject({ email }); const user = await userRepo.findOne({ where: { diff --git a/src/project/controllers/project.controller.ts b/src/project/controllers/project.controller.ts index a39ced1..c888acb 100644 --- a/src/project/controllers/project.controller.ts +++ b/src/project/controllers/project.controller.ts @@ -86,4 +86,18 @@ export class ProjectController { async findOne(@Param() params: GetProjectParam): Promise { return this.projectService.getProject(params.projectUuid); } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @ApiOperation({ + summary: ControllerRoute.PROJECT.ACTIONS.GET_USERS_BY_PROJECT_SUMMARY, + description: + ControllerRoute.PROJECT.ACTIONS.GET_USERS_BY_PROJECT_DESCRIPTION, + }) + @Get(':projectUuid/users') + async findUsersByProject( + @Param() params: GetProjectParam, + ): Promise { + return this.projectService.getUsersByProject(params.projectUuid); + } } diff --git a/src/project/project.module.ts b/src/project/project.module.ts index 8711499..0afbd1c 100644 --- a/src/project/project.module.ts +++ b/src/project/project.module.ts @@ -6,6 +6,8 @@ import { ProjectRepository } from '@app/common/modules/project/repositiories'; import { CreateOrphanSpaceHandler } from './handler'; import { SpaceRepository } from '@app/common/modules/space'; import { CommunityRepository } from '@app/common/modules/community/repositories'; +import { InviteUserRepository } from '@app/common/modules/Invite-user/repositiories'; +import { UserRepository } from '@app/common/modules/user/repositories'; const CommandHandlers = [CreateOrphanSpaceHandler]; @@ -19,6 +21,8 @@ const CommandHandlers = [CreateOrphanSpaceHandler]; CommunityRepository, ProjectService, ProjectRepository, + InviteUserRepository, + UserRepository, ], exports: [ProjectService, CqrsModule], }) diff --git a/src/project/services/project.service.ts b/src/project/services/project.service.ts index 75aa67a..1d5e609 100644 --- a/src/project/services/project.service.ts +++ b/src/project/services/project.service.ts @@ -12,11 +12,17 @@ import { ProjectDto } from '@app/common/modules/project/dtos'; import { PageResponse } from '@app/common/dto/pagination.response.dto'; import { CommandBus } from '@nestjs/cqrs'; import { CreateOrphanSpaceCommand } from '../command/create-orphan-space-command'; +import { InviteUserRepository } from '@app/common/modules/Invite-user/repositiories'; +import { UserRepository } from '@app/common/modules/user/repositories'; +import { UserStatusEnum } from '@app/common/constants/user-status.enum'; +import { RoleType } from '@app/common/constants/role.type.enum'; @Injectable() export class ProjectService { constructor( private readonly projectRepository: ProjectRepository, + private readonly inviteUserRepository: InviteUserRepository, + private readonly userRepository: UserRepository, private commandBus: CommandBus, ) {} @@ -181,6 +187,78 @@ export class ProjectService { } } + async getUsersByProject(uuid: string): Promise { + try { + // Fetch invited users + const invitedUsers = await this.inviteUserRepository.find({ + where: { project: { uuid }, isActive: true }, + select: [ + 'firstName', + 'lastName', + 'email', + 'createdAt', + 'status', + 'phoneNumber', + 'jobTitle', + 'invitedBy', + 'isEnabled', + ], + relations: ['roleType'], + }); + + // Fetch project users + const users = await this.userRepository.find({ + where: { project: { uuid }, isActive: true }, + select: ['firstName', 'lastName', 'email', 'createdAt'], + relations: ['roleType'], + }); + + // Combine both arrays + const allUsers = [...users, ...invitedUsers]; + + const normalizedUsers = allUsers.map((user) => { + const createdAt = new Date(user.createdAt); + const createdDate = createdAt.toLocaleDateString(); + const createdTime = createdAt.toLocaleTimeString(); + + // Normalize user properties + const normalizedProps = this.normalizeUserProperties(user); + + // Return the normalized user object + return { + ...user, + createdDate, + createdTime, + ...normalizedProps, + }; + }); + + return new SuccessResponseDto({ + message: `Users in project with ID ${uuid} retrieved successfully`, + data: normalizedUsers, + statusCode: HttpStatus.OK, + }); + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + + throw new HttpException( + `An error occurred while retrieving users in the project with id ${uuid}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + normalizeUserProperties(user: any) { + return { + status: user.status ?? UserStatusEnum.ACTIVE, + invitedBy: user.invitedBy ?? RoleType.SPACE_MEMBER, + isEnabled: user.isEnabled ?? true, + phoneNumber: user.phoneNumber ?? null, + jobTitle: user.jobTitle ?? null, + roleType: user.roleType?.type ?? null, + }; + } async findOne(uuid: string): Promise { const project = await this.projectRepository.findOne({ where: { uuid } }); return project; diff --git a/src/scene/services/scene.service.ts b/src/scene/services/scene.service.ts index 691fbe1..498d157 100644 --- a/src/scene/services/scene.service.ts +++ b/src/scene/services/scene.service.ts @@ -495,12 +495,16 @@ export class SceneService { const space = await this.getSpaceByUuid(scene.space.uuid); await this.delete(scene.sceneTuyaUuid, space.spaceTuyaUuid); - await this.sceneDeviceRepository.delete({ - scene: { uuid: sceneUuid }, - }); - await this.sceneRepository.delete({ - uuid: sceneUuid, - }); + await this.sceneDeviceRepository.update( + { uuid: sceneUuid }, + { disabled: true }, + ); + await this.sceneRepository.update( + { + uuid: sceneUuid, + }, + { disabled: true }, + ); return new SuccessResponseDto({ message: `Scene with ID ${sceneUuid} deleted successfully`, }); diff --git a/src/space-model/commands/index.ts b/src/space-model/commands/index.ts new file mode 100644 index 0000000..da760ea --- /dev/null +++ b/src/space-model/commands/index.ts @@ -0,0 +1,2 @@ +export * from './propogate-subspace-update-command'; +export * from './propagate-space-model-deletion.command'; diff --git a/src/space-model/commands/propagate-space-model-deletion.command.ts b/src/space-model/commands/propagate-space-model-deletion.command.ts new file mode 100644 index 0000000..0cacd5b --- /dev/null +++ b/src/space-model/commands/propagate-space-model-deletion.command.ts @@ -0,0 +1,9 @@ +import { SpaceModelEntity } from '@app/common/modules/space-model'; + +export class PropogateDeleteSpaceModelCommand { + constructor( + public readonly param: { + spaceModel: SpaceModelEntity; + }, + ) {} +} diff --git a/src/space-model/commands/propogate-subspace-update-command.ts b/src/space-model/commands/propogate-subspace-update-command.ts new file mode 100644 index 0000000..3b8ccdb --- /dev/null +++ b/src/space-model/commands/propogate-subspace-update-command.ts @@ -0,0 +1,12 @@ +import { ICommand } from '@nestjs/cqrs'; +import { SpaceModelEntity } from '@app/common/modules/space-model'; +import { ModifyspaceModelPayload } from '../interfaces'; + +export class PropogateUpdateSpaceModelCommand implements ICommand { + constructor( + public readonly param: { + spaceModel: SpaceModelEntity; + modifiedSpaceModels: ModifyspaceModelPayload; + }, + ) {} +} diff --git a/src/space-model/common/index.ts b/src/space-model/common/index.ts deleted file mode 100644 index e371345..0000000 --- a/src/space-model/common/index.ts +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index 80c8119..0000000 --- a/src/space-model/common/services/base-product-item-model.service.ts +++ /dev/null @@ -1,60 +0,0 @@ -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 deleted file mode 100644 index b83511a..0000000 --- a/src/space-model/common/services/base-product-model.service.ts +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index d1cc61e..0000000 --- a/src/space-model/common/services/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './base-product-item-model.service'; -export * from './base-product-model.service'; diff --git a/src/space-model/controllers/space-model.controller.ts b/src/space-model/controllers/space-model.controller.ts index d994ba6..5708e3f 100644 --- a/src/space-model/controllers/space-model.controller.ts +++ b/src/space-model/controllers/space-model.controller.ts @@ -2,15 +2,21 @@ import { ControllerRoute } from '@app/common/constants/controller-route'; import { Body, Controller, + Delete, Get, Param, Post, + Put, Query, UseGuards, } from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; import { SpaceModelService } from '../services'; -import { CreateSpaceModelDto } from '../dtos'; +import { + CreateSpaceModelDto, + SpaceModelParam, + UpdateSpaceModelDto, +} from '../dtos'; import { ProjectParam } from 'src/community/dtos'; import { BaseResponseDto } from '@app/common/dto/base.response.dto'; import { PermissionsGuard } from 'src/guards/permissions.guard'; @@ -27,7 +33,7 @@ export class SpaceModelController { @ApiBearerAuth() @UseGuards(PermissionsGuard) - @Permissions('SPACE_MODULE_ADD') + @Permissions('SPACE_MODEL_ADD') @ApiOperation({ summary: ControllerRoute.SPACE_MODEL.ACTIONS.CREATE_SPACE_MODEL_SUMMARY, description: @@ -59,4 +65,33 @@ export class SpaceModelController { ): Promise { return await this.spaceModelService.list(projectParam, query); } + + @ApiBearerAuth() + @UseGuards(PermissionsGuard) + @Permissions('SPACE_MODEL_UPDATE') + @ApiOperation({ + summary: ControllerRoute.SPACE_MODEL.ACTIONS.UPDATE_SPACE_MODEL_SUMMARY, + description: + ControllerRoute.SPACE_MODEL.ACTIONS.UPDATE_SPACE_MODEL_DESCRIPTION, + }) + @Put(':spaceModelUuid') + async update( + @Body() dto: UpdateSpaceModelDto, + @Param() param: SpaceModelParam, + ): Promise { + return await this.spaceModelService.update(dto, param); + } + + @ApiBearerAuth() + @UseGuards(PermissionsGuard) + @Permissions('SPACE_MODEL_DELETE') + @ApiOperation({ + summary: ControllerRoute.SPACE_MODEL.ACTIONS.DELETE_SPACE_MODEL_SUMMARY, + description: + ControllerRoute.SPACE_MODEL.ACTIONS.DELETE_SPACE_MODEL_DESCRIPTION, + }) + @Delete(':spaceModelUuid') + async delete(@Param() param: SpaceModelParam): Promise { + return await this.spaceModelService.deleteSpaceModel(param); + } } diff --git a/src/space-model/dtos/create-space-model.dto.ts b/src/space-model/dtos/create-space-model.dto.ts index 9506728..0c37779 100644 --- a/src/space-model/dtos/create-space-model.dto.ts +++ b/src/space-model/dtos/create-space-model.dto.ts @@ -1,8 +1,8 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsNotEmpty, IsString, IsArray, ValidateNested } from 'class-validator'; import { Type } from 'class-transformer'; -import { CreateSubspaceModelDto } from './create-subspace-model.dto'; -import { CreateSpaceProductModelDto } from './create-space-product-model.dto'; +import { CreateSubspaceModelDto } from './subspaces-model-dtos/create-subspace-model.dto'; +import { CreateTagModelDto } from './tag-model-dtos/create-tag-model.dto'; export class CreateSpaceModelDto { @ApiProperty({ @@ -23,11 +23,11 @@ export class CreateSpaceModelDto { subspaceModels?: CreateSubspaceModelDto[]; @ApiProperty({ - description: 'List of products included in the model', - type: [CreateSpaceProductModelDto], + description: 'List of tags associated with the space model', + type: [CreateTagModelDto], }) @IsArray() @ValidateNested({ each: true }) - @Type(() => CreateSpaceProductModelDto) - spaceProductModels?: CreateSpaceProductModelDto[]; + @Type(() => CreateTagModelDto) + tags?: CreateTagModelDto[]; } 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 deleted file mode 100644 index e6a7e76..0000000 --- a/src/space-model/dtos/create-space-product-item-model.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; - -export class CreateProductItemModelDto { - @ApiProperty({ - description: 'Specific name for the product item', - example: 'Light 1', - }) - @IsNotEmpty() - @IsString() - tag: string; -} diff --git a/src/space-model/dtos/create-space-product-model.dto.ts b/src/space-model/dtos/create-space-product-model.dto.ts deleted file mode 100644 index a4379a1..0000000 --- a/src/space-model/dtos/create-space-product-model.dto.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { - IsNotEmpty, - IsString, - IsArray, - ValidateNested, - IsInt, - ArrayNotEmpty, -} from 'class-validator'; -import { Type } from 'class-transformer'; -import { CreateProductItemModelDto } from './create-space-product-item-model.dto'; - -export class CreateSpaceProductModelDto { - @ApiProperty({ - description: 'ID of the product associated with the model', - example: 'product-uuid', - }) - @IsNotEmpty() - @IsString() - productUuid: string; - - @ApiProperty({ - description: 'Number of products in the model', - example: 3, - }) - @IsNotEmpty() - @IsInt() - productCount: number; - - @ApiProperty({ - description: 'Specific names for each product item', - type: [CreateProductItemModelDto], - }) - @IsArray() - @ArrayNotEmpty() - @ValidateNested({ each: true }) - @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 deleted file mode 100644 index f8dbcdd..0000000 --- a/src/space-model/dtos/create-subspace-model.dto.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -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({ - description: 'Name of the subspace', - example: 'Living Room', - }) - @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/dtos/index.ts b/src/space-model/dtos/index.ts index b4b1e07..3a04fe1 100644 --- a/src/space-model/dtos/index.ts +++ b/src/space-model/dtos/index.ts @@ -1,5 +1,6 @@ export * from './create-space-model.dto'; -export * from './create-space-product-item-model.dto'; -export * from './create-space-product-model.dto'; -export * from './create-subspace-model.dto'; export * from './project-param.dto'; +export * from './update-space-model.dto'; +export * from './space-model-param'; +export * from './subspaces-model-dtos'; +export * from './tag-model-dtos'; diff --git a/src/space-model/dtos/project-param.dto.ts b/src/space-model/dtos/project-param.dto.ts index e7d9e97..69e09b5 100644 --- a/src/space-model/dtos/project-param.dto.ts +++ b/src/space-model/dtos/project-param.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsUUID } from 'class-validator'; -export class projectParam { +export class ProjectParam { @ApiProperty({ description: 'UUID of the Project', example: 'd290f1ee-6c54-4b01-90e6-d701748f0851', diff --git a/src/space-model/dtos/space-model-param.ts b/src/space-model/dtos/space-model-param.ts new file mode 100644 index 0000000..2111546 --- /dev/null +++ b/src/space-model/dtos/space-model-param.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsUUID } from 'class-validator'; +import { ProjectParam } from './project-param.dto'; +export class SpaceModelParam extends ProjectParam { + @ApiProperty({ + description: 'UUID of the Space', + example: 'd290f1ee-6c54-4b01-90e6-d701748f0851', + }) + @IsUUID() + spaceModelUuid: string; +} diff --git a/src/space-model/dtos/subspaces-model-dtos/create-subspace-model.dto.ts b/src/space-model/dtos/subspaces-model-dtos/create-subspace-model.dto.ts new file mode 100644 index 0000000..1397edc --- /dev/null +++ b/src/space-model/dtos/subspaces-model-dtos/create-subspace-model.dto.ts @@ -0,0 +1,23 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator'; +import { CreateTagModelDto } from '../tag-model-dtos/create-tag-model.dto'; +import { Type } from 'class-transformer'; + +export class CreateSubspaceModelDto { + @ApiProperty({ + description: 'Name of the subspace', + example: 'Living Room', + }) + @IsNotEmpty() + @IsString() + subspaceName: string; + + @ApiProperty({ + description: 'List of tag models associated with the subspace', + type: [CreateTagModelDto], + }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => CreateTagModelDto) + tags?: CreateTagModelDto[]; +} diff --git a/src/space-model/dtos/subspaces-model-dtos/delete-subspace-model.dto.ts b/src/space-model/dtos/subspaces-model-dtos/delete-subspace-model.dto.ts new file mode 100644 index 0000000..62fe84e --- /dev/null +++ b/src/space-model/dtos/subspaces-model-dtos/delete-subspace-model.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class DeleteSubspaceModelDto { + @ApiProperty({ + description: 'Uuid of the subspace model need to be deleted', + example: '982fc3a3-64dc-4afb-a5b5-65ee8fef0424', + }) + @IsNotEmpty() + @IsString() + subspaceUuid: string; +} diff --git a/src/space-model/dtos/subspaces-model-dtos/index.ts b/src/space-model/dtos/subspaces-model-dtos/index.ts new file mode 100644 index 0000000..28b01ef --- /dev/null +++ b/src/space-model/dtos/subspaces-model-dtos/index.ts @@ -0,0 +1,4 @@ +export * from './delete-subspace-model.dto'; +export * from './create-subspace-model.dto'; +export * from './update-subspace-model.dto'; +export * from './modify-subspace-model.dto'; diff --git a/src/space-model/dtos/subspaces-model-dtos/modify-subspace-model.dto.ts b/src/space-model/dtos/subspaces-model-dtos/modify-subspace-model.dto.ts new file mode 100644 index 0000000..cbf021c --- /dev/null +++ b/src/space-model/dtos/subspaces-model-dtos/modify-subspace-model.dto.ts @@ -0,0 +1,47 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { + IsString, + IsOptional, + IsArray, + ValidateNested, + IsEnum, +} from 'class-validator'; +import { ModifyTagModelDto } from '../tag-model-dtos'; +import { ModifyAction } from '@app/common/constants/modify-action.enum'; + +export class ModifySubspaceModelDto { + @ApiProperty({ + description: 'Action to perform: add, update, or delete', + example: ModifyAction.ADD, + }) + @IsEnum(ModifyAction) + action: ModifyAction; + + @ApiPropertyOptional({ + description: 'UUID of the subspace (required for update/delete)', + example: '123e4567-e89b-12d3-a456-426614174000', + }) + @IsOptional() + @IsString() + uuid?: string; + + @ApiPropertyOptional({ + description: 'Name of the subspace (required for add/update)', + example: 'Living Room', + }) + @IsOptional() + @IsString() + subspaceName?: string; + + @ApiPropertyOptional({ + description: + 'List of tag modifications (add/update/delete) for the subspace', + type: [ModifyTagModelDto], + }) + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => ModifyTagModelDto) + tags?: ModifyTagModelDto[]; +} diff --git a/src/space-model/dtos/subspaces-model-dtos/update-subspace-model.dto.ts b/src/space-model/dtos/subspaces-model-dtos/update-subspace-model.dto.ts new file mode 100644 index 0000000..a386f4e --- /dev/null +++ b/src/space-model/dtos/subspaces-model-dtos/update-subspace-model.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class UpdateSubspaceModelDto { + @ApiProperty({ + description: 'Name of the subspace', + example: 'Living Room', + }) + @IsNotEmpty() + @IsString() + subspaceName?: string; + + @IsNotEmpty() + @IsString() + subspaceUuid: string; +} diff --git a/src/space-model/dtos/tag-model-dtos/create-tag-model.dto.ts b/src/space-model/dtos/tag-model-dtos/create-tag-model.dto.ts new file mode 100644 index 0000000..5f4ec66 --- /dev/null +++ b/src/space-model/dtos/tag-model-dtos/create-tag-model.dto.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class CreateTagModelDto { + @ApiProperty({ + description: 'Tag models associated with the space or subspace models', + example: 'Temperature Control', + }) + @IsNotEmpty() + @IsString() + tag: string; + + @ApiProperty({ + description: 'ID of the product associated with the tag', + example: '123e4567-e89b-12d3-a456-426614174000', + }) + @IsNotEmpty() + @IsString() + productUuid: string; +} diff --git a/src/space-model/dtos/tag-model-dtos/index.ts b/src/space-model/dtos/tag-model-dtos/index.ts new file mode 100644 index 0000000..a0f136d --- /dev/null +++ b/src/space-model/dtos/tag-model-dtos/index.ts @@ -0,0 +1,3 @@ +export * from './create-tag-model.dto'; +export * from './update-tag-model.dto'; +export * from './modify-tag-model.dto'; diff --git a/src/space-model/dtos/tag-model-dtos/modify-tag-model.dto.ts b/src/space-model/dtos/tag-model-dtos/modify-tag-model.dto.ts new file mode 100644 index 0000000..2b64fe3 --- /dev/null +++ b/src/space-model/dtos/tag-model-dtos/modify-tag-model.dto.ts @@ -0,0 +1,37 @@ +import { ModifyAction } from '@app/common/constants/modify-action.enum'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsString, IsOptional, IsEnum } from 'class-validator'; + +export class ModifyTagModelDto { + @ApiProperty({ + description: 'Action to perform: add, update, or delete', + example: ModifyAction.ADD, + }) + @IsEnum(ModifyAction) + action: ModifyAction; + + @ApiPropertyOptional({ + description: 'UUID of the tag model (required for update/delete)', + example: '123e4567-e89b-12d3-a456-426614174000', + }) + @IsOptional() + @IsString() + uuid?: string; + + @ApiPropertyOptional({ + description: 'Name of the tag model (required for add/update)', + example: 'Temperature Sensor', + }) + @IsOptional() + @IsString() + tag?: string; + + @ApiPropertyOptional({ + description: + 'UUID of the product associated with the tag (required for add)', + example: 'c789a91e-549a-4753-9006-02f89e8170e0', + }) + @IsOptional() + @IsString() + productUuid?: string; +} diff --git a/src/space-model/dtos/tag-model-dtos/update-tag-model.dto.ts b/src/space-model/dtos/tag-model-dtos/update-tag-model.dto.ts new file mode 100644 index 0000000..ca5612f --- /dev/null +++ b/src/space-model/dtos/tag-model-dtos/update-tag-model.dto.ts @@ -0,0 +1,21 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator'; + +export class UpdateTagModelDto { + @ApiProperty({ + description: 'UUID of the tag to be updated', + example: '123e4567-e89b-12d3-a456-426614174000', + }) + @IsNotEmpty() + @IsUUID() + uuid: string; + + @ApiProperty({ + description: 'Updated name of the tag', + example: 'Updated Tag Name', + required: false, + }) + @IsOptional() + @IsString() + tag?: string; +} diff --git a/src/space-model/dtos/update-space-model.dto.ts b/src/space-model/dtos/update-space-model.dto.ts new file mode 100644 index 0000000..d1110ea --- /dev/null +++ b/src/space-model/dtos/update-space-model.dto.ts @@ -0,0 +1,76 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsArray, IsOptional, IsString, ValidateNested } from 'class-validator'; +import { CreateSubspaceModelDto } from './subspaces-model-dtos/create-subspace-model.dto'; +import { Type } from 'class-transformer'; +import { + DeleteSubspaceModelDto, + ModifySubspaceModelDto, + UpdateSubspaceModelDto, +} from './subspaces-model-dtos'; +import { ModifyTagModelDto } from './tag-model-dtos'; + +export class ModifySubspacesModelDto { + @ApiProperty({ + description: 'List of subspaces to add', + type: [CreateSubspaceModelDto], + required: false, + }) + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => CreateSubspaceModelDto) + add?: CreateSubspaceModelDto[]; + + @ApiProperty({ + description: 'List of subspaces to add', + type: [CreateSubspaceModelDto], + required: false, + }) + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => UpdateSubspaceModelDto) + update?: UpdateSubspaceModelDto[]; + + @ApiProperty({ + description: 'List of subspaces to delete', + type: [DeleteSubspaceModelDto], + required: false, + }) + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => DeleteSubspaceModelDto) + delete?: DeleteSubspaceModelDto[]; +} + +export class UpdateSpaceModelDto { + @ApiProperty({ + description: 'Updated name of the space model', + example: 'New Space Model Name', + }) + @IsOptional() + @IsString() + modelName?: string; + + @ApiPropertyOptional({ + description: 'List of subspace modifications (add/update/delete)', + type: [ModifySubspaceModelDto], + }) + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => ModifySubspaceModelDto) + subspaceModels?: ModifySubspaceModelDto[]; + + @ApiPropertyOptional({ + description: + 'List of tag modifications (add/update/delete) for the space model', + type: [ModifyTagModelDto], + }) + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => ModifyTagModelDto) + tags?: ModifyTagModelDto[]; +} diff --git a/src/space-model/handlers/index.ts b/src/space-model/handlers/index.ts new file mode 100644 index 0000000..d7bb550 --- /dev/null +++ b/src/space-model/handlers/index.ts @@ -0,0 +1,2 @@ +export * from './propate-subspace-handler'; +export * from './propogate-space-model-deletion.handler'; diff --git a/src/space-model/handlers/propate-subspace-handler.ts b/src/space-model/handlers/propate-subspace-handler.ts new file mode 100644 index 0000000..f59aeb2 --- /dev/null +++ b/src/space-model/handlers/propate-subspace-handler.ts @@ -0,0 +1,250 @@ +import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; +import { PropogateUpdateSpaceModelCommand } from '../commands'; +import { SpaceEntity, SpaceRepository } from '@app/common/modules/space'; +import { SubspaceRepository } from '@app/common/modules/space/repositories/subspace.repository'; +import { + SpaceModelEntity, + SubspaceModelEntity, + TagModel, +} from '@app/common/modules/space-model'; +import { DataSource, QueryRunner } from 'typeorm'; +import { SubSpaceService } from 'src/space/services'; +import { TagService } from 'src/space/services/tag'; +import { TagModelService } from '../services'; +import { UpdatedSubspaceModelPayload } from '../interfaces'; +import { ModifyAction } from '@app/common/constants/modify-action.enum'; +import { ModifySubspaceDto } from 'src/space/dtos'; + +@CommandHandler(PropogateUpdateSpaceModelCommand) +export class PropogateUpdateSpaceModelHandler + implements ICommandHandler +{ + constructor( + private readonly spaceRepository: SpaceRepository, + private readonly subspaceRepository: SubspaceRepository, + private readonly dataSource: DataSource, + private readonly subSpaceService: SubSpaceService, + private readonly tagService: TagService, + private readonly tagModelService: TagModelService, + ) {} + + async execute(command: PropogateUpdateSpaceModelCommand): Promise { + const { spaceModel, modifiedSpaceModels } = command.param; + const queryRunner = this.dataSource.createQueryRunner(); + + try { + await queryRunner.connect(); + await queryRunner.startTransaction(); + const spaces = await this.spaceRepository.find({ + where: { spaceModel }, + }); + if ( + modifiedSpaceModels.modifiedSubspaceModels.addedSubspaceModels.length > + 0 + ) { + await this.addSubspaceModels( + modifiedSpaceModels.modifiedSubspaceModels.addedSubspaceModels, + spaces, + queryRunner, + ); + } else if ( + modifiedSpaceModels.modifiedSubspaceModels.updatedSubspaceModels + .length > 0 + ) { + await this.updateSubspaceModels( + modifiedSpaceModels.modifiedSubspaceModels.updatedSubspaceModels, + queryRunner, + ); + } + if ( + modifiedSpaceModels.modifiedSubspaceModels.deletedSubspaceModels?.length + ) { + const dtos: ModifySubspaceDto[] = + modifiedSpaceModels.modifiedSubspaceModels.deletedSubspaceModels.map( + (model) => ({ + action: ModifyAction.DELETE, + uuid: model, + }), + ); + await this.subSpaceService.modifySubSpace(dtos, queryRunner); + } + + if (modifiedSpaceModels.modifiedTags.added.length > 0) { + await this.createTags( + modifiedSpaceModels.modifiedTags.added, + queryRunner, + null, + spaceModel, + ); + } + + if (modifiedSpaceModels.modifiedTags.updated.length > 0) { + await this.updateTags( + modifiedSpaceModels.modifiedTags.updated, + queryRunner, + ); + } + + if (modifiedSpaceModels.modifiedTags.deleted.length > 0) { + await this.deleteTags( + modifiedSpaceModels.modifiedTags.deleted, + queryRunner, + ); + } + + await queryRunner.commitTransaction(); + } catch (error) { + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } + } + + async addSubspaceModels( + subspaceModels: SubspaceModelEntity[], + spaces: SpaceEntity[], + queryRunner: QueryRunner, + ) { + for (const space of spaces) { + await this.subSpaceService.createSubSpaceFromModel( + subspaceModels, + space, + queryRunner, + ); + } + } + + async updateSubspaceModels( + subspaceModels: UpdatedSubspaceModelPayload[], + queryRunner: QueryRunner, + ): Promise { + const subspaceUpdatePromises = subspaceModels.map(async (model) => { + const { + updated: tagsToUpdate, + deleted: tagsToDelete, + added: tagsToAdd, + } = model.modifiedTags; + + // Perform tag operations concurrently + await Promise.all([ + tagsToUpdate?.length && this.updateTags(tagsToUpdate, queryRunner), + tagsToDelete?.length && this.deleteTags(tagsToDelete, queryRunner), + tagsToAdd?.length && + this.createTags( + tagsToAdd, + queryRunner, + model.subspaceModelUuid, + null, + ), + ]); + + // Update subspace names + const subspaces = await queryRunner.manager.find( + this.subspaceRepository.target, + { + where: { + subSpaceModel: { + uuid: model.subspaceModelUuid, + }, + }, + }, + ); + + if (subspaces.length > 0) { + const updateSubspacePromises = subspaces.map((subspace) => + queryRunner.manager.update( + this.subspaceRepository.target, + { uuid: subspace.uuid }, + { subspaceName: model.subspaceName }, + ), + ); + await Promise.all(updateSubspacePromises); + } + }); + + // Wait for all subspace model updates to complete + await Promise.all(subspaceUpdatePromises); + } + async updateTags(models: TagModel[], queryRunner: QueryRunner) { + if (!models?.length) return; + + const updatePromises = models.map((model) => + this.tagService.updateTagsFromModel(model, queryRunner), + ); + await Promise.all(updatePromises); + } + + async deleteTags(uuids: string[], queryRunner: QueryRunner) { + const deletePromises = uuids.map((uuid) => + this.tagService.deleteTagFromModel(uuid, queryRunner), + ); + await Promise.all(deletePromises); + } + + async createTags( + models: TagModel[], + queryRunner: QueryRunner, + subspaceModelUuid?: string, + spaceModel?: SpaceModelEntity, + ): Promise { + if (!models.length) { + return; + } + + if (subspaceModelUuid) { + await this.processSubspaces(subspaceModelUuid, models, queryRunner); + } + + if (spaceModel) { + await this.processSpaces(spaceModel.uuid, models, queryRunner); + } + } + + private async processSubspaces( + subspaceModelUuid: string, + models: TagModel[], + queryRunner: QueryRunner, + ): Promise { + const subspaces = await this.subspaceRepository.find({ + where: { + subSpaceModel: { + uuid: subspaceModelUuid, + }, + }, + }); + + if (subspaces.length > 0) { + const subspacePromises = subspaces.map((subspace) => + this.tagService.createTagsFromModel( + queryRunner, + models, + null, + subspace, + ), + ); + await Promise.all(subspacePromises); + } + } + + private async processSpaces( + spaceModelUuid: string, + models: TagModel[], + queryRunner: QueryRunner, + ): Promise { + const spaces = await this.spaceRepository.find({ + where: { + spaceModel: { + uuid: spaceModelUuid, + }, + }, + }); + + if (spaces.length > 0) { + const spacePromises = spaces.map((space) => + this.tagService.createTagsFromModel(queryRunner, models, space, null), + ); + await Promise.all(spacePromises); + } + } +} diff --git a/src/space-model/handlers/propogate-space-model-deletion.handler.ts b/src/space-model/handlers/propogate-space-model-deletion.handler.ts new file mode 100644 index 0000000..acd690e --- /dev/null +++ b/src/space-model/handlers/propogate-space-model-deletion.handler.ts @@ -0,0 +1,57 @@ +import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; +import { Logger } from '@nestjs/common'; + +import { PropogateDeleteSpaceModelCommand } from '../commands'; +import { SpaceRepository } from '@app/common/modules/space'; +import { SpaceService } from '../../space/services/space.service'; +import { DataSource } from 'typeorm'; + +@CommandHandler(PropogateDeleteSpaceModelCommand) +export class PropogateDeleteSpaceModelHandler + implements ICommandHandler +{ + private readonly logger = new Logger(PropogateDeleteSpaceModelHandler.name); + + constructor( + private readonly spaceRepository: SpaceRepository, + private readonly spaceService: SpaceService, + private readonly dataSource: DataSource, + ) {} + + async execute(command: PropogateDeleteSpaceModelCommand): Promise { + const { spaceModel } = command.param; + const queryRunner = this.dataSource.createQueryRunner(); + + try { + await queryRunner.connect(); + await queryRunner.startTransaction(); + const spaces = await this.spaceRepository.find({ + where: { + spaceModel: { + uuid: spaceModel.uuid, + }, + }, + relations: ['subspaces', 'tags', 'subspaces.tags'], + }); + + for (const space of spaces) { + try { + await this.spaceService.unlinkSpaceFromModel(space, queryRunner); + } catch (innerError) { + this.logger.error( + `Error unlinking space model for space with UUID ${space.uuid}:`, + innerError.stack || innerError, + ); + } + } + } catch (error) { + await queryRunner.rollbackTransaction(); + this.logger.error( + 'Error propagating delete space model:', + error.stack || error, + ); + } finally { + await queryRunner.release(); + } + } +} diff --git a/src/space-model/interfaces/index.ts b/src/space-model/interfaces/index.ts new file mode 100644 index 0000000..0bacfcd --- /dev/null +++ b/src/space-model/interfaces/index.ts @@ -0,0 +1,2 @@ +export * from './update-subspace.interface'; +export * from './modify-subspace.interface'; diff --git a/src/space-model/interfaces/modify-subspace.interface.ts b/src/space-model/interfaces/modify-subspace.interface.ts new file mode 100644 index 0000000..8969baf --- /dev/null +++ b/src/space-model/interfaces/modify-subspace.interface.ts @@ -0,0 +1,24 @@ +import { SubspaceModelEntity, TagModel } from '@app/common/modules/space-model'; + +export interface ModifyspaceModelPayload { + modifiedSubspaceModels?: ModifySubspaceModelPayload; + modifiedTags?: ModifiedTagsModelPayload; +} + +export interface ModifySubspaceModelPayload { + addedSubspaceModels?: SubspaceModelEntity[]; + updatedSubspaceModels?: UpdatedSubspaceModelPayload[]; + deletedSubspaceModels?: string[]; +} + +export interface UpdatedSubspaceModelPayload { + subspaceName?: string; + modifiedTags?: ModifiedTagsModelPayload; + subspaceModelUuid: string; +} + +export interface ModifiedTagsModelPayload { + added?: TagModel[]; + updated?: TagModel[]; + deleted?: string[]; +} diff --git a/src/space-model/interfaces/update-subspace.interface.ts b/src/space-model/interfaces/update-subspace.interface.ts new file mode 100644 index 0000000..10332ad --- /dev/null +++ b/src/space-model/interfaces/update-subspace.interface.ts @@ -0,0 +1,39 @@ +import { SubspaceModelEntity } from '@app/common/modules/space-model'; + +export interface AddSubspaceModelInterface { + subspaceModel: SubspaceModelEntity; +} + +export interface ProductModelInterface {} + +export interface IModifySubspaceModelInterface { + spaceModelUuid: string; + new?: AddSubspaceModelInterface[]; + update?: IUpdateSubspaceModelInterface[]; + delete?: IDeletedSubsaceModelInterface[]; +} + +export interface IModifiedProductItemsModelsInterface { + delete?: string[]; +} + +export interface IUpdateSubspaceModelInterface { + subspaceName?: string; + uuid: string; + productModels?: IModifiedProductItemsModelsInterface[]; +} + +export interface IDeletedSubsaceModelInterface { + uuid: string; +} + +export interface IUpdatedProductModelInterface { + productModelUuid: string; + productModifiedItemModel: IModifiedProductItemsModelsInterface; +} + +export interface IModifiedProductModelsInterface { + add?: ProductModelInterface[]; + update?: IUpdatedProductModelInterface[]; + delete?: string[]; +} diff --git a/src/space-model/services/index.ts b/src/space-model/services/index.ts index 5c39727..20dca88 100644 --- a/src/space-model/services/index.ts +++ b/src/space-model/services/index.ts @@ -1,4 +1,3 @@ export * from './space-model.service'; -export * from './space-product-item-model.service'; -export * from './space-product-model.service'; export * from './subspace'; +export * from './tag-model.service'; diff --git a/src/space-model/services/space-model.service.ts b/src/space-model/services/space-model.service.ts index 0ce4083..89e9edb 100644 --- a/src/space-model/services/space-model.service.ts +++ b/src/space-model/services/space-model.service.ts @@ -1,11 +1,12 @@ -import { SpaceModelRepository } from '@app/common/modules/space-model'; +import { + SpaceModelEntity, + SpaceModelRepository, +} from '@app/common/modules/space-model'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; -import { CreateSpaceModelDto } from '../dtos'; -import { ProjectRepository } from '@app/common/modules/project/repositiories'; +import { CreateSpaceModelDto, UpdateSpaceModelDto } from '../dtos'; import { ProjectParam } from 'src/community/dtos'; import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; import { SubSpaceModelService } from './subspace/subspace-model.service'; -import { SpaceProductModelService } from './space-product-model.service'; import { DataSource } from 'typeorm'; import { TypeORMCustomModel, @@ -13,63 +14,69 @@ import { } from '@app/common/models/typeOrmCustom.model'; import { PageResponse } from '@app/common/dto/pagination.response.dto'; import { SpaceModelDto } from '@app/common/modules/space-model/dtos'; +import { SpaceModelParam } from '../dtos/space-model-param'; +import { ProjectService } from 'src/project/services'; +import { ProjectEntity } from '@app/common/modules/project/entities'; +import { TagModelService } from './tag-model.service'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { CommandBus } from '@nestjs/cqrs'; +import { PropogateUpdateSpaceModelCommand } from '../commands'; +import { + ModifiedTagsModelPayload, + ModifySubspaceModelPayload, +} from '../interfaces'; @Injectable() export class SpaceModelService { constructor( private readonly dataSource: DataSource, private readonly spaceModelRepository: SpaceModelRepository, - private readonly projectRepository: ProjectRepository, + private readonly projectService: ProjectService, private readonly subSpaceModelService: SubSpaceModelService, - private readonly spaceProductModelService: SpaceProductModelService, + private readonly tagModelService: TagModelService, + private commandBus: CommandBus, ) {} async createSpaceModel( createSpaceModelDto: CreateSpaceModelDto, params: ProjectParam, - ) { - const { modelName, subspaceModels, spaceProductModels } = - createSpaceModelDto; - const project = await this.validateProject(params.projectUuid); - + ): Promise { + const { modelName, subspaceModels, tags } = createSpaceModelDto; const queryRunner = this.dataSource.createQueryRunner(); - await queryRunner.connect(); await queryRunner.startTransaction(); try { - const isModelExist = await this.validateName( - modelName, - params.projectUuid, - ); - if (isModelExist) { - throw new HttpException( - `Model name "${modelName}" already exists in this project ${project.name}.`, - HttpStatus.CONFLICT, - ); - } + const project = await this.validateProject(params.projectUuid); + + await this.validateName(modelName, params.projectUuid); const spaceModel = this.spaceModelRepository.create({ modelName, project, }); + const savedSpaceModel = await queryRunner.manager.save(spaceModel); - if (subspaceModels) { - await this.subSpaceModelService.createSubSpaceModels( - subspaceModels, - savedSpaceModel, + if (subspaceModels?.length) { + savedSpaceModel.subspaceModels = + await this.subSpaceModelService.createSubSpaceModels( + subspaceModels, + savedSpaceModel, + queryRunner, + tags, + ); + } + + if (tags?.length) { + savedSpaceModel.tags = await this.tagModelService.createTags( + tags, queryRunner, + savedSpaceModel, + null, ); } - if (spaceProductModels) { - await this.spaceProductModelService.createSpaceProductModels( - spaceProductModels, - savedSpaceModel, - queryRunner, - ); - } await queryRunner.commitTransaction(); return new SuccessResponseDto({ @@ -80,14 +87,16 @@ export class SpaceModelService { } catch (error) { await queryRunner.rollbackTransaction(); - if (error instanceof HttpException) { - throw error; - } + const errorMessage = + error instanceof HttpException + ? error.message + : 'An unexpected error occurred'; + const statusCode = + error instanceof HttpException + ? error.getStatus() + : HttpStatus.INTERNAL_SERVER_ERROR; - throw new HttpException( - error.message || `An unexpected error occurred`, - HttpStatus.INTERNAL_SERVER_ERROR, - ); + throw new HttpException(errorMessage, statusCode); } finally { await queryRunner.release(); } @@ -104,8 +113,7 @@ export class SpaceModelService { pageable.where = { project: { uuid: param.projectUuid }, }; - pageable.include = - 'subspaceModels,spaceProductModels,subspaceModels.productModels,subspaceModels.productModels.itemModels,spaceProductModels.items'; + pageable.include = 'subspaceModels,tags,subspaceModels.tags'; const customModel = TypeORMCustomModel(this.spaceModelRepository); @@ -125,26 +133,159 @@ export class SpaceModelService { } } - async validateProject(projectUuid: string) { - const project = await this.projectRepository.findOne({ - where: { - uuid: projectUuid, - }, + async validateProject(projectUuid: string): Promise { + return await this.projectService.findOne(projectUuid); + } + + async update(dto: UpdateSpaceModelDto, param: SpaceModelParam) { + await this.validateProject(param.projectUuid); + const spaceModel = await this.validateSpaceModel(param.spaceModelUuid); + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + + let modifiedSubspaceModels: ModifySubspaceModelPayload = {}; + let modifiedTagsModelPayload: ModifiedTagsModelPayload = {}; + try { + const { modelName } = dto; + if (modelName) { + await this.validateName(modelName, param.projectUuid); + spaceModel.modelName = modelName; + await queryRunner.manager.save(spaceModel); + } + + if (dto.subspaceModels) { + modifiedSubspaceModels = + await this.subSpaceModelService.modifySubSpaceModels( + dto.subspaceModels, + spaceModel, + queryRunner, + ); + } + + if (dto.tags) { + modifiedTagsModelPayload = await this.tagModelService.modifyTags( + dto.tags, + queryRunner, + spaceModel, + ); + } + + await queryRunner.commitTransaction(); + + await this.commandBus.execute( + new PropogateUpdateSpaceModelCommand({ + spaceModel: spaceModel, + modifiedSpaceModels: { + modifiedSubspaceModels, + modifiedTags: modifiedTagsModelPayload, + }, + }), + ); + + return new SuccessResponseDto({ + message: 'SpaceModel updated successfully', + data: spaceModel, + }); + } catch (error) { + await queryRunner.rollbackTransaction(); + throw new HttpException( + error.message || 'Failed to update SpaceModel', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } finally { + await queryRunner.release(); + } + } + + async deleteSpaceModel(param: SpaceModelParam): Promise { + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + await this.validateProject(param.projectUuid); + const spaceModel = await this.validateSpaceModel(param.spaceModelUuid); + + if (spaceModel.subspaceModels?.length) { + const deleteSubspaceDtos = spaceModel.subspaceModels.map( + (subspace) => ({ + subspaceUuid: subspace.uuid, + }), + ); + + await this.subSpaceModelService.deleteSubspaceModels( + deleteSubspaceDtos, + queryRunner, + ); + } + + if (spaceModel.tags?.length) { + const deleteSpaceTagsDtos = spaceModel.tags.map((tag) => tag.uuid); + + await this.tagModelService.deleteTags(deleteSpaceTagsDtos, queryRunner); + } + + await queryRunner.manager.update( + this.spaceModelRepository.target, + { uuid: param.spaceModelUuid }, + { disabled: true }, + ); + + await queryRunner.commitTransaction(); + + return new SuccessResponseDto({ + message: `SpaceModel with UUID ${param.spaceModelUuid} deleted successfully.`, + statusCode: HttpStatus.OK, + }); + } catch (error) { + await queryRunner.rollbackTransaction(); + + const errorMessage = + error instanceof HttpException + ? error.message + : 'An unexpected error occurred while deleting the SpaceModel'; + const statusCode = + error instanceof HttpException + ? error.getStatus() + : HttpStatus.INTERNAL_SERVER_ERROR; + + throw new HttpException(errorMessage, statusCode); + } finally { + await queryRunner.release(); + } + } + + async validateName(modelName: string, projectUuid: string): Promise { + const isModelExist = await this.spaceModelRepository.findOne({ + where: { modelName, project: { uuid: projectUuid }, disabled: false }, }); - if (!project) { + if (isModelExist) { throw new HttpException( - `Project with uuid ${projectUuid} not found`, - HttpStatus.NOT_FOUND, + `Model name ${modelName} already exists in the project with UUID ${projectUuid}.`, + HttpStatus.CONFLICT, ); } - return project; } - async validateName(modelName: string, projectUuid: string): Promise { - const isModelExist = await this.spaceModelRepository.exists({ - where: { modelName, project: { uuid: projectUuid } }, + async validateSpaceModel(uuid: string): Promise { + const spaceModel = await this.spaceModelRepository.findOne({ + where: { + uuid, + disabled: false, + }, + relations: [ + 'subspaceModels', + 'tags', + 'tags.product', + 'subspaceModels.tags', + 'subspaceModels.tags.product', + ], }); - return isModelExist; + if (!spaceModel) { + throw new HttpException('space model not found', HttpStatus.NOT_FOUND); + } + return spaceModel; } } diff --git a/src/space-model/services/space-product-item-model.service.ts b/src/space-model/services/space-product-item-model.service.ts deleted file mode 100644 index c69cae8..0000000 --- a/src/space-model/services/space-product-item-model.service.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { - SpaceModelEntity, - SpaceProductItemModelRepository, - SpaceProductModelEntity, -} from '@app/common/modules/space-model'; -import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; -import { CreateProductItemModelDto } from '../dtos'; -import { QueryRunner } from 'typeorm'; -import { BaseProductItemService } from '../common'; - -@Injectable() -export class SpaceProductItemModelService extends BaseProductItemService { - constructor( - private readonly spaceProductItemRepository: SpaceProductItemModelRepository, - ) { - super(); - } - - async createProdutItemModel( - itemModelDtos: CreateProductItemModelDto[], - spaceProductModel: SpaceProductModelEntity, - spaceModel: SpaceModelEntity, - queryRunner: QueryRunner, - ) { - await this.validateTags(itemModelDtos, queryRunner, spaceModel); - try { - const productItems = itemModelDtos.map((dto) => - queryRunner.manager.create(this.spaceProductItemRepository.target, { - tag: dto.tag, - spaceProductModel, - }), - ); - - 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/space-product-model.service.ts b/src/space-model/services/space-product-model.service.ts deleted file mode 100644 index 0f1e93e..0000000 --- a/src/space-model/services/space-product-model.service.ts +++ /dev/null @@ -1,67 +0,0 @@ -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'; - -@Injectable() -export class SpaceProductModelService extends BaseProductModelService { - constructor( - private readonly spaceProductModelRepository: SpaceProductModelRepository, - private readonly spaceProductItemModelService: SpaceProductItemModelService, - productService: ProductService, - ) { - super(productService); - } - - async createSpaceProductModels( - spaceProductModelDtos: CreateSpaceProductModelDto[], - spaceModel: SpaceModelEntity, - 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.spaceProductModelRepository.target, - { - product, - productCount: dto.productCount, - spaceModel, - }, - ); - }), - ); - - const savedProductModels = await queryRunner.manager.save(productModels); - - await Promise.all( - spaceProductModelDtos.map((dto, index) => { - const savedModel = savedProductModels[index]; - return this.spaceProductItemModelService.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/services/subspace/index.ts b/src/space-model/services/subspace/index.ts index 78d7cd3..3965e6d 100644 --- a/src/space-model/services/subspace/index.ts +++ b/src/space-model/services/subspace/index.ts @@ -1,3 +1 @@ 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/subspace-model.service.ts b/src/space-model/services/subspace/subspace-model.service.ts index 222dee4..e8a7bd0 100644 --- a/src/space-model/services/subspace/subspace-model.service.ts +++ b/src/space-model/services/subspace/subspace-model.service.ts @@ -1,88 +1,302 @@ import { SpaceModelEntity, + SubspaceModelEntity, SubspaceModelRepository, } from '@app/common/modules/space-model'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; -import { CreateSubspaceModelDto } from '../../dtos'; -import { QueryRunner } from 'typeorm'; -import { SubspaceProductModelService } from './subspace-product-model.service'; +import { CreateSubspaceModelDto, CreateTagModelDto } from '../../dtos'; +import { In, QueryRunner } from 'typeorm'; +import { + IDeletedSubsaceModelInterface, + ModifySubspaceModelPayload, + UpdatedSubspaceModelPayload, +} from 'src/space-model/interfaces'; +import { + DeleteSubspaceModelDto, + ModifySubspaceModelDto, +} from 'src/space-model/dtos/subspaces-model-dtos'; +import { TagModelService } from '../tag-model.service'; +import { ModifyAction } from '@app/common/constants/modify-action.enum'; @Injectable() export class SubSpaceModelService { constructor( private readonly subspaceModelRepository: SubspaceModelRepository, - private readonly subSpaceProducetModelService: SubspaceProductModelService, + private readonly tagModelService: TagModelService, ) {} async createSubSpaceModels( subSpaceModelDtos: CreateSubspaceModelDto[], spaceModel: SpaceModelEntity, queryRunner: QueryRunner, - ) { - this.validateInputDtos(subSpaceModelDtos); + otherTags?: CreateTagModelDto[], + ): Promise { + this.validateInputDtos(subSpaceModelDtos, spaceModel); - try { - const subspaces = subSpaceModelDtos.map((subspaceDto) => - queryRunner.manager.create(this.subspaceModelRepository.target, { - subspaceName: subspaceDto.subspaceName, - spaceModel: spaceModel, - }), - ); + const subspaces = subSpaceModelDtos.map((subspaceDto) => + queryRunner.manager.create(this.subspaceModelRepository.target, { + subspaceName: subspaceDto.subspaceName, + spaceModel, + }), + ); - await queryRunner.manager.save(subspaces); + const savedSubspaces = await queryRunner.manager.save(subspaces); - await Promise.all( - subSpaceModelDtos.map((dto, index) => { - const subspaceModel = subspaces[index]; - return this.subSpaceProducetModelService.createSubspaceProductModels( - dto.spaceProductModels, - spaceModel, - subspaceModel, + await Promise.all( + subSpaceModelDtos.map(async (dto, index) => { + const subspace = savedSubspaces[index]; + + const otherDtoTags = subSpaceModelDtos + .filter((_, i) => i !== index) + .flatMap((otherDto) => otherDto.tags || []); + + if (dto.tags?.length) { + subspace.tags = await this.tagModelService.createTags( + dto.tags, queryRunner, + null, + subspace, + [...(otherTags || []), ...otherDtoTags], ); - }), + } + }), + ); + + return savedSubspaces; + } + + async deleteSubspaceModels( + deleteDtos: DeleteSubspaceModelDto[], + queryRunner: QueryRunner, + ): Promise { + const deleteResults: IDeletedSubsaceModelInterface[] = []; + + for (const dto of deleteDtos) { + const subspaceModel = await this.findOne(dto.subspaceUuid); + + await queryRunner.manager.update( + this.subspaceModelRepository.target, + { uuid: dto.subspaceUuid }, + { disabled: true }, ); - } catch (error) { - if (error instanceof HttpException) { - throw error; + + if (subspaceModel.tags?.length) { + const modifyTagDtos = subspaceModel.tags.map((tag) => ({ + uuid: tag.uuid, + action: ModifyAction.DELETE, + })); + await this.tagModelService.modifyTags( + modifyTagDtos, + queryRunner, + null, + subspaceModel, + ); } - throw new HttpException( - error.message || `An unexpected error occurred`, - HttpStatus.INTERNAL_SERVER_ERROR, + deleteResults.push({ uuid: dto.subspaceUuid }); + } + + return deleteResults; + } + + async modifySubSpaceModels( + subspaceDtos: ModifySubspaceModelDto[], + spaceModel: SpaceModelEntity, + queryRunner: QueryRunner, + ): Promise { + const modifiedSubspaceModels: ModifySubspaceModelPayload = {}; + for (const subspace of subspaceDtos) { + switch (subspace.action) { + case ModifyAction.ADD: + const subspaceModel = await this.handleAddAction( + subspace, + spaceModel, + queryRunner, + ); + modifiedSubspaceModels.addedSubspaceModels.push(subspaceModel); + break; + case ModifyAction.UPDATE: + const updatedSubspaceModel = await this.handleUpdateAction( + subspace, + queryRunner, + ); + modifiedSubspaceModels.updatedSubspaceModels.push( + updatedSubspaceModel, + ); + break; + case ModifyAction.DELETE: + await this.handleDeleteAction(subspace, queryRunner); + modifiedSubspaceModels.deletedSubspaceModels.push(subspace.uuid); + break; + default: + throw new HttpException( + `Invalid action "${subspace.action}".`, + HttpStatus.BAD_REQUEST, + ); + } + } + return modifiedSubspaceModels; + } + + private async handleAddAction( + subspace: ModifySubspaceModelDto, + spaceModel: SpaceModelEntity, + queryRunner: QueryRunner, + ): Promise { + const createTagDtos: CreateTagModelDto[] = + subspace.tags?.map((tag) => ({ + tag: tag.tag, + productUuid: tag.productUuid, + })) || []; + + const [createdSubspaceModel] = await this.createSubSpaceModels( + [ + { + subspaceName: subspace.subspaceName, + tags: createTagDtos, + }, + ], + spaceModel, + queryRunner, + ); + + return createdSubspaceModel; + } + + private async handleUpdateAction( + modifyDto: ModifySubspaceModelDto, + queryRunner: QueryRunner, + ): Promise { + const updatePayload: UpdatedSubspaceModelPayload = { + subspaceModelUuid: modifyDto.uuid, + }; + + const subspace = await this.findOne(modifyDto.uuid); + + await this.updateSubspaceName( + queryRunner, + subspace, + modifyDto.subspaceName, + ); + updatePayload.subspaceName = modifyDto.subspaceName; + + if (modifyDto.tags?.length) { + updatePayload.modifiedTags = await this.tagModelService.modifyTags( + modifyDto.tags, + queryRunner, + null, + subspace, + ); + } + + return updatePayload; + } + + private async handleDeleteAction( + subspace: ModifySubspaceModelDto, + queryRunner: QueryRunner, + ) { + const subspaceModel = await this.findOne(subspace.uuid); + + await queryRunner.manager.update( + this.subspaceModelRepository.target, + { uuid: subspace.uuid }, + { disabled: true }, + ); + + if (subspaceModel.tags?.length) { + const modifyTagDtos = subspaceModel.tags.map((tag) => ({ + uuid: tag.uuid, + action: ModifyAction.DELETE, + })); + await this.tagModelService.modifyTags( + modifyTagDtos, + queryRunner, + null, + subspaceModel, ); } } - private validateInputDtos(subSpaceModelDtos: CreateSubspaceModelDto[]) { + private async findOne(subspaceUuid: string): Promise { + const subspace = await this.subspaceModelRepository.findOne({ + where: { uuid: subspaceUuid }, + relations: ['tags'], + }); + if (!subspace) { + throw new HttpException( + `SubspaceModel with UUID ${subspaceUuid} not found.`, + HttpStatus.NOT_FOUND, + ); + } + return subspace; + } + + private validateInputDtos( + subSpaceModelDtos: CreateSubspaceModelDto[], + spaceModel: SpaceModelEntity, + ): void { if (subSpaceModelDtos.length === 0) { throw new HttpException( 'Subspace models cannot be empty.', HttpStatus.BAD_REQUEST, ); } - - const incomingNames = subSpaceModelDtos.map((dto) => dto.subspaceName); - this.validateName(incomingNames); + this.validateName( + subSpaceModelDtos.map((dto) => dto.subspaceName), + spaceModel, + ); } - private validateName(names: string[]) { + private async validateName( + names: string[], + spaceModel: SpaceModelEntity, + ): Promise { const seenNames = new Set(); const duplicateNames = new Set(); for (const name of names) { - if (seenNames.has(name)) { + if (!seenNames.add(name)) { duplicateNames.add(name); - } else { - seenNames.add(name); } } if (duplicateNames.size > 0) { throw new HttpException( - `Duplicate subspace names found in request: ${[...duplicateNames].join(', ')}`, + `Duplicate subspace model names found: ${[...duplicateNames].join(', ')}`, HttpStatus.CONFLICT, ); } + + const existingNames = await this.subspaceModelRepository.find({ + select: ['subspaceName'], + where: { + subspaceName: In([...seenNames]), + spaceModel: { + uuid: spaceModel.uuid, + }, + }, + }); + + if (existingNames.length > 0) { + const existingNamesList = existingNames + .map((e) => e.subspaceName) + .join(', '); + throw new HttpException( + `Subspace model names already exist in the space: ${existingNamesList}`, + HttpStatus.BAD_REQUEST, + ); + } + } + + private async updateSubspaceName( + queryRunner: QueryRunner, + subSpaceModel: SubspaceModelEntity, + subspaceName?: string, + ): Promise { + if (subspaceName) { + subSpaceModel.subspaceName = subspaceName; + await queryRunner.manager.save(subSpaceModel); + } } } 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 deleted file mode 100644 index 393a5f3..0000000 --- a/src/space-model/services/subspace/subspace-product-item-model.service.ts +++ /dev/null @@ -1,53 +0,0 @@ -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 deleted file mode 100644 index 30f9062..0000000 --- a/src/space-model/services/subspace/subspace-product-model.service.ts +++ /dev/null @@ -1,69 +0,0 @@ -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/services/tag-model.service.ts b/src/space-model/services/tag-model.service.ts new file mode 100644 index 0000000..d08fe38 --- /dev/null +++ b/src/space-model/services/tag-model.service.ts @@ -0,0 +1,295 @@ +import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; +import { QueryRunner } from 'typeorm'; +import { + SpaceModelEntity, + TagModel, +} from '@app/common/modules/space-model/entities'; +import { SubspaceModelEntity } from '@app/common/modules/space-model/entities'; +import { TagModelRepository } from '@app/common/modules/space-model'; +import { CreateTagModelDto, ModifyTagModelDto } from '../dtos'; +import { ProductService } from 'src/product/services'; +import { ModifyAction } from '@app/common/constants/modify-action.enum'; +import { ModifiedTagsModelPayload } from '../interfaces'; + +@Injectable() +export class TagModelService { + constructor( + private readonly tagModelRepository: TagModelRepository, + private readonly productService: ProductService, + ) {} + + async createTags( + tags: CreateTagModelDto[], + queryRunner: QueryRunner, + spaceModel?: SpaceModelEntity, + subspaceModel?: SubspaceModelEntity, + additionalTags?: CreateTagModelDto[], + ): Promise { + if (!tags.length) { + throw new HttpException('Tags cannot be empty.', HttpStatus.BAD_REQUEST); + } + + const combinedTags = additionalTags ? [...tags, ...additionalTags] : tags; + const duplicateTags = this.findDuplicateTags(combinedTags); + + if (duplicateTags.length > 0) { + throw new HttpException( + `Duplicate tags found for the same product: ${duplicateTags.join(', ')}`, + HttpStatus.BAD_REQUEST, + ); + } + + const tagEntities = await Promise.all( + tags.map(async (tagDto) => + this.prepareTagEntity(tagDto, queryRunner, spaceModel, subspaceModel), + ), + ); + try { + return await queryRunner.manager.save(tagEntities); + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + + throw new HttpException( + 'Failed to save tag models due to an unexpected error.', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async updateTag( + tag: ModifyTagModelDto, + queryRunner: QueryRunner, + spaceModel?: SpaceModelEntity, + subspaceModel?: SubspaceModelEntity, + ): Promise { + try { + const existingTag = await this.getTagByUuid(tag.uuid); + + if (spaceModel) { + await this.checkTagReuse(tag.tag, existingTag.product.uuid, spaceModel); + } else { + await this.checkTagReuse( + tag.tag, + existingTag.product.uuid, + subspaceModel.spaceModel, + ); + } + + if (tag.tag) { + existingTag.tag = tag.tag; + } + + return await queryRunner.manager.save(existingTag); + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + throw new HttpException( + error.message || 'Failed to update tags', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async deleteTags(tagUuids: string[], queryRunner: QueryRunner) { + try { + const deletePromises = tagUuids.map((id) => + queryRunner.manager.softDelete(this.tagModelRepository.target, id), + ); + + await Promise.all(deletePromises); + return { message: 'Tags deleted successfully', tagUuids }; + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + throw new HttpException( + error.message || 'Failed to delete tags', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + private findDuplicateTags(tags: CreateTagModelDto[]): string[] { + const seen = new Map(); + const duplicates: string[] = []; + + tags.forEach((tagDto) => { + const key = `${tagDto.productUuid}-${tagDto.tag}`; + if (seen.has(key)) { + duplicates.push(`${tagDto.tag} for Product: ${tagDto.productUuid}`); + } else { + seen.set(key, true); + } + }); + + return duplicates; + } + + async modifyTags( + tags: ModifyTagModelDto[], + queryRunner: QueryRunner, + spaceModel?: SpaceModelEntity, + subspaceModel?: SubspaceModelEntity, + ): Promise { + const modifiedTagModels: ModifiedTagsModelPayload = {}; + try { + for (const tag of tags) { + if (tag.action === ModifyAction.ADD) { + const createTagDto: CreateTagModelDto = { + tag: tag.tag as string, + productUuid: tag.productUuid as string, + }; + + const newModel = await this.createTags( + [createTagDto], + queryRunner, + spaceModel, + subspaceModel, + ); + modifiedTagModels.added.push(...newModel); + } else if (tag.action === ModifyAction.UPDATE) { + const updatedModel = await this.updateTag( + tag, + queryRunner, + spaceModel, + subspaceModel, + ); + modifiedTagModels.updated.push(updatedModel); + } else if (tag.action === ModifyAction.DELETE) { + await queryRunner.manager.update( + this.tagModelRepository.target, + { uuid: tag.uuid }, + { disabled: true }, + ); + modifiedTagModels.deleted.push(tag.uuid); + } else { + throw new HttpException( + `Invalid action "${tag.action}" provided.`, + HttpStatus.BAD_REQUEST, + ); + } + } + return modifiedTagModels; + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + + throw new HttpException( + `An error occurred while modifying tag models: ${error.message}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + private async checkTagReuse( + tag: string, + productUuid: string, + spaceModel: SpaceModelEntity, + ): Promise { + const tagExists = await this.tagModelRepository.exists({ + where: [ + { + tag, + spaceModel: { uuid: spaceModel.uuid }, + product: { uuid: productUuid }, + disabled: false, + }, + { + tag, + subspaceModel: { spaceModel: { uuid: spaceModel.uuid } }, + product: { uuid: productUuid }, + disabled: false, + }, + ], + }); + + if (tagExists) { + throw new HttpException(`Tag can't be reused`, HttpStatus.CONFLICT); + } + } + + private async prepareTagEntity( + tagDto: CreateTagModelDto, + queryRunner: QueryRunner, + spaceModel?: SpaceModelEntity, + subspaceModel?: SubspaceModelEntity, + ): Promise { + const product = await this.productService.findOne(tagDto.productUuid); + + if (!product) { + throw new HttpException( + `Product with UUID ${tagDto.productUuid} not found.`, + HttpStatus.NOT_FOUND, + ); + } + + if (spaceModel) { + await this.checkTagReuse(tagDto.tag, tagDto.productUuid, spaceModel); + } else { + await this.checkTagReuse( + tagDto.tag, + tagDto.productUuid, + subspaceModel.spaceModel, + ); + } + + return queryRunner.manager.create(TagModel, { + tag: tagDto.tag, + product: product.data, + spaceModel, + subspaceModel, + }); + } + + async getTagByUuid(uuid: string): Promise { + const tag = await this.tagModelRepository.findOne({ + where: { uuid }, + relations: ['product'], + }); + if (!tag) { + throw new HttpException( + `Tag model with ID ${uuid} not found.`, + HttpStatus.NOT_FOUND, + ); + } + return tag; + } + + async getTagByName( + tag: string, + subspaceUuid?: string, + spaceUuid?: string, + ): Promise { + const queryConditions: any = { tag }; + + if (spaceUuid) { + queryConditions.spaceModel = { uuid: spaceUuid }; + } else if (subspaceUuid) { + queryConditions.subspaceModel = { uuid: subspaceUuid }; + } else { + throw new HttpException( + 'Either spaceUuid or subspaceUuid must be provided.', + HttpStatus.BAD_REQUEST, + ); + } + queryConditions.disabled = false; + + const existingTag = await this.tagModelRepository.findOne({ + where: queryConditions, + relations: ['product'], + }); + + if (!existingTag) { + throw new HttpException( + `Tag model with tag "${tag}" not found.`, + HttpStatus.NOT_FOUND, + ); + } + + return existingTag; + } +} diff --git a/src/space-model/space-model.module.ts b/src/space-model/space-model.module.ts index ae275e2..7f72860 100644 --- a/src/space-model/space-model.module.ts +++ b/src/space-model/space-model.module.ts @@ -4,42 +4,73 @@ import { ConfigModule } from '@nestjs/config'; import { SpaceModelController } from './controllers'; import { SpaceModelService, - SpaceProductItemModelService, - SpaceProductModelService, SubSpaceModelService, - SubspaceProductItemModelService, + TagModelService, } from './services'; import { SpaceModelRepository, - SpaceProductItemModelRepository, - SpaceProductModelRepository, SubspaceModelRepository, - SubspaceProductItemModelRepository, - SubspaceProductModelRepository, + TagModelRepository, } 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'; +import { + PropogateDeleteSpaceModelHandler, + PropogateUpdateSpaceModelHandler, +} from './handlers'; +import { CqrsModule } from '@nestjs/cqrs'; +import { + SpaceLinkRepository, + SpaceRepository, + TagRepository, +} from '@app/common/modules/space'; +import { SubspaceRepository } from '@app/common/modules/space/repositories/subspace.repository'; +import { + SpaceLinkService, + SpaceService, + SubspaceDeviceService, + SubSpaceService, + ValidationService, +} from 'src/space/services'; +import { TagService } from 'src/space/services/tag'; +import { CommunityService } from 'src/community/services'; +import { DeviceRepository } from '@app/common/modules/device/repositories'; +import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; +import { CommunityRepository } from '@app/common/modules/community/repositories'; + +const CommandHandlers = [ + PropogateUpdateSpaceModelHandler, + PropogateDeleteSpaceModelHandler, +]; @Module({ - imports: [ConfigModule, SpaceRepositoryModule], + imports: [ConfigModule, SpaceRepositoryModule, CqrsModule], controllers: [SpaceModelController], providers: [ + ...CommandHandlers, SpaceModelService, + SpaceService, SpaceModelRepository, + SpaceRepository, ProjectRepository, SubSpaceModelService, - SpaceProductModelService, SubspaceModelRepository, - SpaceProductModelRepository, ProductRepository, - SpaceProductItemModelService, - SpaceProductItemModelRepository, - SubspaceProductItemModelService, - SubspaceProductItemModelRepository, - SubspaceProductModelService, - SubspaceProductModelRepository, + SubspaceRepository, + TagModelService, + TagModelRepository, + SubSpaceService, + ValidationService, + TagService, + SubspaceDeviceService, + CommunityService, + TagRepository, + DeviceRepository, + TuyaService, + CommunityRepository, + SpaceLinkService, + SpaceLinkRepository, ], - exports: [], + exports: [CqrsModule, SpaceModelService], }) export class SpaceModelModule {} diff --git a/src/space/commands/disable-space.command.ts b/src/space/commands/disable-space.command.ts new file mode 100644 index 0000000..1c66f21 --- /dev/null +++ b/src/space/commands/disable-space.command.ts @@ -0,0 +1,10 @@ +import { SpaceEntity } from '@app/common/modules/space'; + +export class DisableSpaceCommand { + constructor( + public readonly param: { + spaceUuid: string; + orphanSpace: SpaceEntity; + }, + ) {} +} diff --git a/src/space/commands/index.ts b/src/space/commands/index.ts new file mode 100644 index 0000000..a9a7b85 --- /dev/null +++ b/src/space/commands/index.ts @@ -0,0 +1 @@ +export * from './disable-space.command'; diff --git a/src/space/controllers/space.controller.ts b/src/space/controllers/space.controller.ts index 18d556d..f54a3ea 100644 --- a/src/space/controllers/space.controller.ts +++ b/src/space/controllers/space.controller.ts @@ -67,8 +67,8 @@ export class SpaceController { description: ControllerRoute.SPACE.ACTIONS.DELETE_SPACE_DESCRIPTION, }) @Delete('/:spaceUuid') - async deleteSpace(@Param() params: GetSpaceParam): Promise { - return this.spaceService.delete(params); + async deleteSpace(@Param() params: GetSpaceParam) { + return await this.spaceService.delete(params); } @ApiBearerAuth() diff --git a/src/space/controllers/subspace/subspace.controller.ts b/src/space/controllers/subspace/subspace.controller.ts index 37e264b..3ed36af 100644 --- a/src/space/controllers/subspace/subspace.controller.ts +++ b/src/space/controllers/subspace/subspace.controller.ts @@ -65,7 +65,7 @@ export class SubSpaceController { }) @Get(':subSpaceUuid') async findOne(@Param() params: GetSubSpaceParam): Promise { - return this.subSpaceService.findOne(params); + return this.subSpaceService.getOne(params); } @ApiBearerAuth() diff --git a/src/space/dtos/add.space.dto.ts b/src/space/dtos/add.space.dto.ts index dba8e92..a3c9a3b 100644 --- a/src/space/dtos/add.space.dto.ts +++ b/src/space/dtos/add.space.dto.ts @@ -11,41 +11,7 @@ import { 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[]; -} +import { CreateTagDto } from './tag'; export class AddSpaceDto { @ApiProperty({ @@ -103,26 +69,22 @@ export class AddSpaceDto { @IsOptional() direction?: string; - @ApiProperty({ - description: 'List of products assigned to this space', - type: [ProductAssignmentDto], - required: false, - }) - @IsArray() - @ValidateNested({ each: true }) - @IsOptional() - @Type(() => ProductAssignmentDto) - products?: ProductAssignmentDto[]; - @ApiProperty({ description: 'List of subspaces included in the model', type: [AddSubspaceDto], }) @IsOptional() - @IsArray() @ValidateNested({ each: true }) @Type(() => AddSubspaceDto) subspaces?: AddSubspaceDto[]; + + @ApiProperty({ + description: 'List of tags associated with the space model', + type: [CreateTagDto], + }) + @ValidateNested({ each: true }) + @Type(() => CreateTagDto) + tags?: CreateTagDto[]; } export class AddUserSpaceDto { diff --git a/src/space/dtos/index.ts b/src/space/dtos/index.ts index 3c85266..506efa9 100644 --- a/src/space/dtos/index.ts +++ b/src/space/dtos/index.ts @@ -5,3 +5,4 @@ export * from './user-space.param'; export * from './subspace'; export * from './project.param.dto'; export * from './update.space.dto'; +export * from './tag'; diff --git a/src/space/dtos/subspace/add.subspace.dto.ts b/src/space/dtos/subspace/add.subspace.dto.ts index 6b5078b..98b381a 100644 --- a/src/space/dtos/subspace/add.subspace.dto.ts +++ b/src/space/dtos/subspace/add.subspace.dto.ts @@ -1,13 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; +import { IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator'; +import { CreateTagDto } from '../tag'; import { Type } from 'class-transformer'; -import { - IsArray, - IsNotEmpty, - IsOptional, - IsString, - ValidateNested, -} from 'class-validator'; -import { ProductAssignmentDto } from '../add.space.dto'; export class AddSubspaceDto { @ApiProperty({ @@ -19,13 +13,11 @@ export class AddSubspaceDto { subspaceName: string; @ApiProperty({ - description: 'List of products assigned to this space', - type: [ProductAssignmentDto], - required: false, + description: 'List of tags associated with the subspace', + type: [CreateTagDto], }) @IsArray() @ValidateNested({ each: true }) - @IsOptional() - @Type(() => ProductAssignmentDto) - products?: ProductAssignmentDto[]; + @Type(() => CreateTagDto) + tags?: CreateTagDto[]; } diff --git a/src/space/dtos/subspace/delete.subspace.dto.ts b/src/space/dtos/subspace/delete.subspace.dto.ts new file mode 100644 index 0000000..3329a14 --- /dev/null +++ b/src/space/dtos/subspace/delete.subspace.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class DeleteSubspaceDto { + @ApiProperty({ + description: 'Uuid of the subspace model need to be deleted', + example: '982fc3a3-64dc-4afb-a5b5-65ee8fef0424', + }) + @IsNotEmpty() + @IsString() + subspaceUuid: string; +} diff --git a/src/space/dtos/subspace/index.ts b/src/space/dtos/subspace/index.ts index 0463a85..0071b44 100644 --- a/src/space/dtos/subspace/index.ts +++ b/src/space/dtos/subspace/index.ts @@ -1,3 +1,6 @@ export * from './add.subspace.dto'; export * from './get.subspace.param'; export * from './add.subspace-device.param'; +export * from './update.subspace.dto'; +export * from './delete.subspace.dto'; +export * from './modify.subspace.dto'; diff --git a/src/space/dtos/subspace/modify.subspace.dto.ts b/src/space/dtos/subspace/modify.subspace.dto.ts new file mode 100644 index 0000000..39a40e6 --- /dev/null +++ b/src/space/dtos/subspace/modify.subspace.dto.ts @@ -0,0 +1,47 @@ +import { ModifyAction } from '@app/common/constants/modify-action.enum'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { + IsEnum, + IsOptional, + IsString, + IsArray, + ValidateNested, +} from 'class-validator'; +import { ModifyTagDto } from '../tag/modify-tag.dto'; + +export class ModifySubspaceDto { + @ApiProperty({ + description: 'Action to perform: add, update, or delete', + example: ModifyAction.ADD, + }) + @IsEnum(ModifyAction) + action: ModifyAction; + + @ApiPropertyOptional({ + description: 'UUID of the subspace (required for update/delete)', + example: '123e4567-e89b-12d3-a456-426614174000', + }) + @IsOptional() + @IsString() + uuid?: string; + + @ApiPropertyOptional({ + description: 'Name of the subspace (required for add/update)', + example: 'Living Room', + }) + @IsOptional() + @IsString() + subspaceName?: string; + + @ApiPropertyOptional({ + description: + 'List of tag modifications (add/update/delete) for the subspace', + type: [ModifyTagDto], + }) + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => ModifyTagDto) + tags?: ModifyTagDto[]; +} diff --git a/src/space/dtos/subspace/update.subspace.dto.ts b/src/space/dtos/subspace/update.subspace.dto.ts new file mode 100644 index 0000000..0931d9e --- /dev/null +++ b/src/space/dtos/subspace/update.subspace.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class UpdateSubspaceDto { + @ApiProperty({ + description: 'Name of the subspace', + example: 'Living Room', + }) + @IsNotEmpty() + @IsString() + subspaceName?: string; + + @IsNotEmpty() + @IsString() + subspaceUuid: string; +} diff --git a/src/space/dtos/tag/create-tag-dto.ts b/src/space/dtos/tag/create-tag-dto.ts new file mode 100644 index 0000000..8e89155 --- /dev/null +++ b/src/space/dtos/tag/create-tag-dto.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class CreateTagDto { + @ApiProperty({ + description: 'Tag associated with the space or subspace', + example: 'Temperature Control', + }) + @IsNotEmpty() + @IsString() + tag: string; + + @ApiProperty({ + description: 'ID of the product associated with the tag', + example: '123e4567-e89b-12d3-a456-426614174000', + }) + @IsNotEmpty() + @IsString() + productUuid: string; +} diff --git a/src/space/dtos/tag/index.ts b/src/space/dtos/tag/index.ts new file mode 100644 index 0000000..599ba4b --- /dev/null +++ b/src/space/dtos/tag/index.ts @@ -0,0 +1 @@ +export * from './create-tag-dto'; diff --git a/src/space/dtos/tag/modify-tag.dto.ts b/src/space/dtos/tag/modify-tag.dto.ts new file mode 100644 index 0000000..6088a2a --- /dev/null +++ b/src/space/dtos/tag/modify-tag.dto.ts @@ -0,0 +1,37 @@ +import { ModifyAction } from '@app/common/constants/modify-action.enum'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsEnum, IsOptional, IsString } from 'class-validator'; + +export class ModifyTagDto { + @ApiProperty({ + description: 'Action to perform: add, update, or delete', + example: ModifyAction.ADD, + }) + @IsEnum(ModifyAction) + action: ModifyAction; + + @ApiPropertyOptional({ + description: 'UUID of the tag (required for update/delete)', + example: '123e4567-e89b-12d3-a456-426614174000', + }) + @IsOptional() + @IsString() + uuid?: string; + + @ApiPropertyOptional({ + description: 'Name of the tag (required for add/update)', + example: 'Temperature Sensor', + }) + @IsOptional() + @IsString() + tag?: string; + + @ApiPropertyOptional({ + description: + 'UUID of the product associated with the tag (required for add)', + example: 'c789a91e-549a-4753-9006-02f89e8170e0', + }) + @IsOptional() + @IsString() + productUuid?: string; +} diff --git a/src/space/dtos/update.space.dto.ts b/src/space/dtos/update.space.dto.ts index d40476b..02efb86 100644 --- a/src/space/dtos/update.space.dto.ts +++ b/src/space/dtos/update.space.dto.ts @@ -1,4 +1,61 @@ -import { PartialType } from '@nestjs/swagger'; -import { AddSpaceDto } from './add.space.dto'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsArray, + IsNumber, + IsOptional, + IsString, + ValidateNested, +} from 'class-validator'; +import { ModifySubspaceDto } from './subspace'; +import { Type } from 'class-transformer'; +import { ModifyTagDto } from './tag/modify-tag.dto'; -export class UpdateSpaceDto extends PartialType(AddSpaceDto) {} +export class UpdateSpaceDto { + @ApiProperty({ + description: 'Updated name of the space ', + example: 'New Space Name', + }) + @IsOptional() + @IsString() + spaceName?: string; + + @ApiProperty({ + description: 'Icon identifier for the space', + example: 'assets/location', + required: false, + }) + @IsString() + @IsOptional() + public icon?: string; + + @ApiProperty({ description: 'X position on canvas', example: 120 }) + @IsNumber() + @IsOptional() + x?: number; + + @ApiProperty({ description: 'Y position on canvas', example: 200 }) + @IsNumber() + @IsOptional() + y?: number; + + @ApiPropertyOptional({ + description: 'List of subspace modifications (add/update/delete)', + type: [ModifySubspaceDto], + }) + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => ModifySubspaceDto) + subspace?: ModifySubspaceDto[]; + + @ApiPropertyOptional({ + description: + 'List of tag modifications (add/update/delete) for the space model', + type: [ModifyTagDto], + }) + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => ModifyTagDto) + tags?: ModifyTagDto[]; +} diff --git a/src/space/handlers/disable-space.handler.ts b/src/space/handlers/disable-space.handler.ts new file mode 100644 index 0000000..64669fa --- /dev/null +++ b/src/space/handlers/disable-space.handler.ts @@ -0,0 +1,100 @@ +import { SpaceEntity } from '@app/common/modules/space'; +import { HttpException, HttpStatus } from '@nestjs/common'; +import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; +import { DeviceService } from 'src/device/services'; +import { UserSpaceService } from 'src/users/services'; +import { DataSource } from 'typeorm'; +import { DisableSpaceCommand } from '../commands'; +import { + SubSpaceService, + SpaceLinkService, + SpaceSceneService, +} from '../services'; +import { TagService } from '../services/tag'; + +@CommandHandler(DisableSpaceCommand) +export class DisableSpaceHandler + implements ICommandHandler +{ + constructor( + private readonly subSpaceService: SubSpaceService, + private readonly userService: UserSpaceService, + private readonly tagService: TagService, + private readonly deviceService: DeviceService, + private readonly spaceLinkService: SpaceLinkService, + private readonly sceneService: SpaceSceneService, + private readonly dataSource: DataSource, + ) {} + + async execute(command: DisableSpaceCommand): Promise { + const queryRunner = this.dataSource.createQueryRunner(); + + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + const { spaceUuid, orphanSpace } = command.param; + + const space = await queryRunner.manager.findOne(SpaceEntity, { + where: { uuid: spaceUuid, disabled: false }, + relations: [ + 'subspaces', + 'parent', + 'tags', + 'devices', + 'outgoingConnections', + 'incomingConnections', + 'scenes', + 'children', + 'userSpaces', + ], + }); + + if (!space) { + throw new HttpException( + `Space with UUID ${spaceUuid} not found`, + HttpStatus.NOT_FOUND, + ); + } + + if (space.children && space.children.length > 0) { + for (const child of space.children) { + await this.execute( + new DisableSpaceCommand({ spaceUuid: child.uuid, orphanSpace }), + ); + } + } + + const tagUuids = space.tags?.map((tag) => tag.uuid) || []; + const subspaceDtos = + space.subspaces?.map((subspace) => ({ + subspaceUuid: subspace.uuid, + })) || []; + const deletionTasks = [ + this.subSpaceService.deleteSubspaces(subspaceDtos, queryRunner), + this.userService.deleteUserSpace(space.uuid), + this.tagService.deleteTags(tagUuids, queryRunner), + this.deviceService.deleteDevice( + space.devices, + orphanSpace, + queryRunner, + ), + this.spaceLinkService.deleteSpaceLink(space, queryRunner), + this.sceneService.deleteScenes(space, queryRunner), + ]; + + await Promise.all(deletionTasks); + + // Mark space as disabled + space.disabled = true; + await queryRunner.manager.save(space); + + await queryRunner.commitTransaction(); + } catch (error) { + await queryRunner.rollbackTransaction(); + console.error(`Failed to disable space: ${error.message}`); + } finally { + await queryRunner.release(); + } + } +} diff --git a/src/space/handlers/index.ts b/src/space/handlers/index.ts new file mode 100644 index 0000000..ceb1ca9 --- /dev/null +++ b/src/space/handlers/index.ts @@ -0,0 +1 @@ +export * from './disable-space.handler'; diff --git a/src/space/interfaces/add-subspace.interface.ts b/src/space/interfaces/add-subspace.interface.ts new file mode 100644 index 0000000..207622c --- /dev/null +++ b/src/space/interfaces/add-subspace.interface.ts @@ -0,0 +1,5 @@ +import { SubspaceEntity } from '@app/common/modules/space'; + +export interface ModifySubspacePayload { + addedSubspaces?: SubspaceEntity[]; +} diff --git a/src/space/interfaces/index.ts b/src/space/interfaces/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/space/services/index.ts b/src/space/services/index.ts index c67ccae..5f86e3d 100644 --- a/src/space/services/index.ts +++ b/src/space/services/index.ts @@ -4,6 +4,4 @@ export * from './space-device.service'; 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-link/space-link.service.ts b/src/space/services/space-link/space-link.service.ts index 0acece0..80d18ed 100644 --- a/src/space/services/space-link/space-link.service.ts +++ b/src/space/services/space-link/space-link.service.ts @@ -1,40 +1,41 @@ -import { - SpaceLinkRepository, - SpaceRepository, -} from '@app/common/modules/space/repositories'; +import { SpaceEntity, SpaceLinkEntity } from '@app/common/modules/space'; +import { SpaceLinkRepository } from '@app/common/modules/space/repositories'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { QueryRunner } from 'typeorm'; @Injectable() export class SpaceLinkService { - constructor( - private readonly spaceRepository: SpaceRepository, - private readonly spaceLinkRepository: SpaceLinkRepository, - ) {} + constructor(private readonly spaceLinkRepository: SpaceLinkRepository) {} async saveSpaceLink( startSpaceId: string, endSpaceId: string, direction: string, + queryRunner: QueryRunner, ): Promise { try { // Check if a link between the startSpace and endSpace already exists - const existingLink = await this.spaceLinkRepository.findOne({ + const existingLink = await queryRunner.manager.findOne(SpaceLinkEntity, { where: { startSpace: { uuid: startSpaceId }, endSpace: { uuid: endSpaceId }, + disabled: false, }, }); if (existingLink) { // Update the direction if the link exists existingLink.direction = direction; - await this.spaceLinkRepository.save(existingLink); + await queryRunner.manager.save(SpaceLinkEntity, existingLink); return; } - const existingEndSpaceLink = await this.spaceLinkRepository.findOne({ - where: { endSpace: { uuid: endSpaceId } }, - }); + const existingEndSpaceLink = await queryRunner.manager.findOne( + SpaceLinkEntity, + { + where: { endSpace: { uuid: endSpaceId } }, + }, + ); if ( existingEndSpaceLink && @@ -46,7 +47,7 @@ export class SpaceLinkService { } // Find start space - const startSpace = await this.spaceRepository.findOne({ + const startSpace = await queryRunner.manager.findOne(SpaceEntity, { where: { uuid: startSpaceId }, }); @@ -58,7 +59,7 @@ export class SpaceLinkService { } // Find end space - const endSpace = await this.spaceRepository.findOne({ + const endSpace = await queryRunner.manager.findOne(SpaceEntity, { where: { uuid: endSpaceId }, }); @@ -76,7 +77,7 @@ export class SpaceLinkService { direction, }); - await this.spaceLinkRepository.save(spaceLink); + await queryRunner.manager.save(SpaceLinkEntity, spaceLink); } catch (error) { throw new HttpException( error.message || @@ -85,4 +86,35 @@ export class SpaceLinkService { ); } } + async deleteSpaceLink( + space: SpaceEntity, + queryRunner: QueryRunner, + ): Promise { + try { + const spaceLinks = await queryRunner.manager.find(SpaceLinkEntity, { + where: [ + { startSpace: space, disabled: false }, + { endSpace: space, disabled: false }, + ], + }); + + if (spaceLinks.length === 0) { + return; + } + + const linkIds = spaceLinks.map((link) => link.uuid); + + await queryRunner.manager + .createQueryBuilder() + .update(SpaceLinkEntity) + .set({ disabled: true }) + .whereInIds(linkIds) + .execute(); + } catch (error) { + throw new HttpException( + `Failed to disable space links for the given space: ${error.message}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } } diff --git a/src/space/services/space-product-items/index.ts b/src/space/services/space-product-items/index.ts deleted file mode 100644 index fff8634..0000000 --- a/src/space/services/space-product-items/index.ts +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index d4883b9..0000000 --- a/src/space/services/space-product-items/space-product-items.service.ts +++ /dev/null @@ -1,87 +0,0 @@ -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/index.ts b/src/space/services/space-products/index.ts deleted file mode 100644 index d0b92d2..0000000 --- a/src/space/services/space-products/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './space-products.service'; diff --git a/src/space/services/space-products/space-products.service.ts b/src/space/services/space-products/space-products.service.ts deleted file mode 100644 index 2fe75cb..0000000 --- a/src/space/services/space-products/space-products.service.ts +++ /dev/null @@ -1,226 +0,0 @@ -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 { 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 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: 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, - ); - - if (existingSpaceProducts) { - updatedProducts = await this.updateExistingProducts( - existingSpaceProducts, - uniqueProducts, - productEntities, - queryRunner, - ); - } - - const newProducts = await this.createNewProducts( - uniqueProducts, - productEntities, - space, - queryRunner, - ); - - return [...updatedProducts, ...newProducts]; - } catch (error) { - if (!(error instanceof HttpException)) { - throw new HttpException( - `An error occurred while assigning products to the space ${error}`, - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - throw error; - } - } - - private validateUniqueProducts( - products: ProductAssignmentDto[], - ): ProductAssignmentDto[] { - const productIds = new Set(); - const uniqueProducts = []; - - for (const product of products) { - if (productIds.has(product.productId)) { - throw new HttpException( - `Duplicate product ID found: ${product.productId}`, - HttpStatus.BAD_REQUEST, - ); - } - productIds.add(product.productId); - uniqueProducts.push(product); - } - - return uniqueProducts; - } - - private async getProductEntities( - 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); - throw new HttpException( - 'Failed to fetch product entities', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - - 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 4e77158..6c6b528 100644 --- a/src/space/services/space-scene.service.ts +++ b/src/space/services/space-scene.service.ts @@ -5,6 +5,9 @@ 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'; +import { SpaceEntity } from '@app/common/modules/space'; +import { QueryRunner } from 'typeorm'; +import { SceneEntity } from '@app/common/modules/scene/entities'; @Injectable() export class SpaceSceneService { @@ -48,4 +51,32 @@ export class SpaceSceneService { } } } + + async deleteScenes( + space: SpaceEntity, + queryRunner: QueryRunner, + ): Promise { + try { + const scenes = await queryRunner.manager.find(SceneEntity, { + where: { space }, + }); + + if (scenes.length === 0) { + return; + } + + const sceneUuids = scenes.map((scene) => scene.uuid); + + await Promise.all( + sceneUuids.map((uuid) => + this.sceneSevice.deleteScene({ sceneUuid: uuid }), + ), + ); + } catch (error) { + throw new HttpException( + `Failed to delete scenes: ${error.message}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } } diff --git a/src/space/services/space-validation.service.ts b/src/space/services/space-validation.service.ts index fa4a5f7..89a539a 100644 --- a/src/space/services/space-validation.service.ts +++ b/src/space/services/space-validation.service.ts @@ -1,4 +1,3 @@ -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'; @@ -21,14 +20,14 @@ export class ValidationService { async validateCommunityAndProject( communityUuid: string, projectUuid: string, - ): Promise { - await this.projectService.findOne(projectUuid); + ) { + const project = await this.projectService.findOne(projectUuid); const community = await this.communityService.getCommunityById({ communityUuid, projectUuid, }); - return community.data; + return { community: community.data, project: project }; } async validateSpaceWithinCommunityAndProject( @@ -43,7 +42,16 @@ export class ValidationService { async validateSpace(spaceUuid: string): Promise { const space = await this.spaceRepository.findOne({ - where: { uuid: spaceUuid }, + where: { uuid: spaceUuid, disabled: false }, + relations: [ + 'parent', + 'children', + 'subspaces', + 'tags', + 'subspaces.tags', + 'subspaces.devices', + 'devices', + ], }); if (!space) { @@ -61,11 +69,10 @@ export class ValidationService { where: { uuid: spaceModelUuid }, relations: [ 'subspaceModels', - 'subspaceModels.productModels.product', - 'subspaceModels.productModels', - 'spaceProductModels', - 'spaceProductModels.product', - 'spaceProductModels.items', + 'subspaceModels.tags', + 'tags', + 'subspaceModels.tags.product', + 'tags.product', ], }); diff --git a/src/space/services/space.service.ts b/src/space/services/space.service.ts index df0b0a4..9badabb 100644 --- a/src/space/services/space.service.ts +++ b/src/space/services/space.service.ts @@ -7,9 +7,10 @@ import { } from '@nestjs/common'; import { AddSpaceDto, + AddSubspaceDto, CommunitySpaceParam, + CreateTagDto, GetSpaceParam, - ProductAssignmentDto, UpdateSpaceDto, } from '../dtos'; import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; @@ -17,29 +18,35 @@ import { BaseResponseDto } from '@app/common/dto/base.response.dto'; 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 { CreateSubspaceModelDto } from 'src/space-model/dtos'; import { SubSpaceService } from './subspace'; -import { DataSource, Not } from 'typeorm'; +import { DataSource, Not, QueryRunner } from 'typeorm'; import { ValidationService } from './space-validation.service'; -import { ORPHAN_SPACE_NAME } from '@app/common/constants/orphan-constant'; - +import { + ORPHAN_COMMUNITY_NAME, + ORPHAN_SPACE_NAME, +} from '@app/common/constants/orphan-constant'; +import { CommandBus } from '@nestjs/cqrs'; +import { TagService } from './tag'; +import { SpaceModelService } from 'src/space-model/services'; +import { DisableSpaceCommand } from '../commands'; @Injectable() export class SpaceService { constructor( private readonly dataSource: DataSource, private readonly spaceRepository: SpaceRepository, private readonly spaceLinkService: SpaceLinkService, - private readonly spaceProductService: SpaceProductService, private readonly subSpaceService: SubSpaceService, private readonly validationService: ValidationService, + private readonly tagService: TagService, + private readonly spaceModelService: SpaceModelService, + private commandBus: CommandBus, ) {} async createSpace( addSpaceDto: AddSpaceDto, params: CommunitySpaceParam, ): Promise { - const { parentUuid, direction, products, spaceModelUuid, subspaces } = + const { parentUuid, direction, spaceModelUuid, subspaces, tags } = addSpaceDto; const { communityUuid, projectUuid } = params; @@ -60,7 +67,7 @@ export class SpaceService { projectUuid, ); - this.validateSpaceCreation(spaceModelUuid, products, subspaces); + this.validateSpaceCreation(addSpaceDto, spaceModelUuid); const parent = parentUuid ? await this.validationService.validateSpace(parentUuid) @@ -71,50 +78,34 @@ export class SpaceService { : null; try { - const newSpace = queryRunner.manager.create(SpaceEntity, { + const space = queryRunner.manager.create(SpaceEntity, { ...addSpaceDto, spaceModel, parent: parentUuid ? parent : null, community, }); - await queryRunner.manager.save(newSpace); + const newSpace = await queryRunner.manager.save(space); - if (direction && parent) { - await this.spaceLinkService.saveSpaceLink( - parent.uuid, - newSpace.uuid, - direction, - ); - } + await Promise.all([ + spaceModelUuid && + this.createFromModel(spaceModelUuid, queryRunner, newSpace), + direction && parent + ? this.spaceLinkService.saveSpaceLink( + parent.uuid, + newSpace.uuid, + direction, + queryRunner, + ) + : Promise.resolve(), + subspaces?.length + ? this.createSubspaces(subspaces, newSpace, queryRunner, tags) + : Promise.resolve(), + tags?.length + ? this.createTags(tags, queryRunner, newSpace) + : Promise.resolve(), + ]); - 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({ @@ -134,6 +125,41 @@ export class SpaceService { } } + async createFromModel( + spaceModelUuid: string, + queryRunner: QueryRunner, + space: SpaceEntity, + ) { + try { + const spaceModel = + await this.spaceModelService.validateSpaceModel(spaceModelUuid); + + space.spaceModel = spaceModel; + await queryRunner.manager.save(SpaceEntity, space); + + await this.subSpaceService.createSubSpaceFromModel( + spaceModel.subspaceModels, + space, + queryRunner, + ); + + await this.tagService.createTagsFromModel( + queryRunner, + spaceModel.tags, + space, + null, + ); + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + throw new HttpException( + 'An error occurred while creating the space from space model', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async getSpacesHierarchyForCommunity( params: CommunitySpaceParam, ): Promise { @@ -204,21 +230,31 @@ export class SpaceService { try { const { communityUuid, spaceUuid, projectUuid } = params; - const space = - await this.validationService.validateSpaceWithinCommunityAndProject( + const { project } = + await this.validationService.validateCommunityAndProject( communityUuid, projectUuid, - spaceUuid, ); + const space = await this.validationService.validateSpace(spaceUuid); + if (space.spaceName === ORPHAN_SPACE_NAME) { throw new HttpException( `space ${ORPHAN_SPACE_NAME} cannot be deleted`, HttpStatus.BAD_REQUEST, ); } - // Delete the space - await this.spaceRepository.remove(space); + + const orphanSpace = await this.spaceRepository.findOne({ + where: { + community: { + uuid: `${ORPHAN_COMMUNITY_NAME}-${project.name}`, + }, + spaceName: ORPHAN_SPACE_NAME, + }, + }); + + await this.disableSpace(space, orphanSpace); return new SuccessResponseDto({ message: `Space with ID ${spaceUuid} successfully deleted`, @@ -235,6 +271,12 @@ export class SpaceService { } } + async disableSpace(space: SpaceEntity, orphanSpace: SpaceEntity) { + await this.commandBus.execute( + new DisableSpaceCommand({ spaceUuid: space.uuid, orphanSpace }), + ); + } + async updateSpace( params: GetSpaceParam, updateSpaceDto: UpdateSpaceDto, @@ -255,30 +297,39 @@ export class SpaceService { if (space.spaceName === ORPHAN_SPACE_NAME) { throw new HttpException( - `space ${ORPHAN_SPACE_NAME} cannot be updated`, + `Space "${ORPHAN_SPACE_NAME}" cannot be updated`, HttpStatus.BAD_REQUEST, ); } - // If a parentId is provided, check if the parent exists - const { parentUuid, products } = updateSpaceDto; - const parent = parentUuid - ? await this.validationService.validateSpace(parentUuid) - : null; + this.updateSpaceProperties(space, updateSpaceDto); - // Update other space properties from updateSpaceDto - Object.assign(space, updateSpaceDto, { parent }); + await queryRunner.manager.save(space); - // Save the updated space - const updatedSpace = await queryRunner.manager.save(space); + const hasSubspace = updateSpaceDto.subspace?.length > 0; + const hasTags = updateSpaceDto.tags?.length > 0; - if (products && products.length > 0) { - await this.spaceProductService.assignProductsToSpace( - updatedSpace, - products, + if (hasSubspace || hasTags) { + await this.tagService.unlinkModels(space.tags, queryRunner); + await this.subSpaceService.unlinkModels(space.subspaces, queryRunner); + } + + if (hasSubspace) { + await this.subSpaceService.modifySubSpace( + updateSpaceDto.subspace, queryRunner, + space, ); } + + if (hasTags) { + await this.tagService.modifyTags( + updateSpaceDto.tags, + queryRunner, + space, + ); + } + await queryRunner.commitTransaction(); return new SuccessResponseDto({ @@ -292,6 +343,7 @@ export class SpaceService { if (error instanceof HttpException) { throw error; } + throw new HttpException( 'An error occurred while updating the space', HttpStatus.INTERNAL_SERVER_ERROR, @@ -301,6 +353,49 @@ export class SpaceService { } } + async unlinkSpaceFromModel( + space: SpaceEntity, + queryRunner: QueryRunner, + ): Promise { + try { + await queryRunner.manager.update( + this.spaceRepository.target, + { uuid: space.uuid }, + { + spaceModel: null, + }, + ); + + // Unlink subspaces and tags if they exist + if (space.subspaces || space.tags) { + if (space.tags) { + await this.tagService.unlinkModels(space.tags, queryRunner); + } + + if (space.subspaces) { + await this.subSpaceService.unlinkModels(space.subspaces, queryRunner); + } + } + } catch (error) { + throw new HttpException( + `Failed to unlink space model: ${error.message}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + private updateSpaceProperties( + space: SpaceEntity, + updateSpaceDto: UpdateSpaceDto, + ): void { + const { spaceName, x, y, icon } = updateSpaceDto; + + if (spaceName) space.spaceName = spaceName; + if (x) space.x = x; + if (y) space.y = y; + if (icon) space.icon = icon; + } + async getSpacesHierarchyForSpace( params: GetSpaceParam, ): Promise { @@ -314,7 +409,7 @@ export class SpaceService { try { // Get all spaces that are children of the provided space, including the parent-child relations const spaces = await this.spaceRepository.find({ - where: { parent: { uuid: spaceUuid } }, + where: { parent: { uuid: spaceUuid }, disabled: false }, relations: ['parent', 'children'], // Include parent and children relations }); @@ -391,15 +486,36 @@ export class SpaceService { } private validateSpaceCreation( + addSpaceDto: AddSpaceDto, spaceModelUuid?: string, - products?: ProductAssignmentDto[], - subSpaces?: CreateSubspaceModelDto[], ) { - if (spaceModelUuid && (products?.length || subSpaces?.length)) { + if (spaceModelUuid && (addSpaceDto.tags || addSpaceDto.subspaces)) { throw new HttpException( 'For space creation choose either space model or products and subspace', HttpStatus.CONFLICT, ); } } + + private async createSubspaces( + subspaces: AddSubspaceDto[], + space: SpaceEntity, + queryRunner: QueryRunner, + tags: CreateTagDto[], + ): Promise { + space.subspaces = await this.subSpaceService.createSubspacesFromDto( + subspaces, + space, + queryRunner, + tags, + ); + } + + private async createTags( + tags: CreateTagDto[], + queryRunner: QueryRunner, + space: SpaceEntity, + ): Promise { + space.tags = await this.tagService.createTags(tags, queryRunner, space); + } } diff --git a/src/space/services/subspace/index.ts b/src/space/services/subspace/index.ts index b51a84a..973d199 100644 --- a/src/space/services/subspace/index.ts +++ b/src/space/services/subspace/index.ts @@ -1,4 +1,2 @@ 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 d6f6320..130da1d 100644 --- a/src/space/services/subspace/subspace-device.service.ts +++ b/src/space/services/subspace/subspace-device.service.ts @@ -9,6 +9,8 @@ import { ProductRepository } from '@app/common/modules/product/repositories'; import { GetDeviceDetailsInterface } from '../../../device/interfaces/get.device.interface'; import { ValidationService } from '../space-validation.service'; import { SubspaceRepository } from '@app/common/modules/space/repositories/subspace.repository'; +import { In, QueryRunner } from 'typeorm'; +import { DeviceEntity } from '@app/common/modules/device/entities'; @Injectable() export class SubspaceDeviceService { @@ -173,6 +175,30 @@ export class SubspaceDeviceService { return device; } + async deleteSubspaceDevices( + devices: DeviceEntity[], + queryRunner: QueryRunner, + ): Promise { + const deviceUuids = devices.map((device) => device.uuid); + + try { + if (deviceUuids.length === 0) { + return; + } + + await queryRunner.manager.update( + this.deviceRepository.target, + { uuid: In(deviceUuids) }, + { subspace: null }, + ); + } catch (error) { + throw new HttpException( + `Failed to delete devices with IDs ${deviceUuids.join(', ')}: ${error.message}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + private throwNotFound(entity: string, uuid: string) { throw new HttpException( `${entity} with ID ${uuid} not found`, diff --git a/src/space/services/subspace/subspace-product-item.service.ts b/src/space/services/subspace/subspace-product-item.service.ts deleted file mode 100644 index 647a597..0000000 --- a/src/space/services/subspace/subspace-product-item.service.ts +++ /dev/null @@ -1,92 +0,0 @@ -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 deleted file mode 100644 index e789311..0000000 --- a/src/space/services/subspace/subspace-product.service.ts +++ /dev/null @@ -1,128 +0,0 @@ -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 6b9a1a3..7c5cf6d 100644 --- a/src/space/services/subspace/subspace.service.ts +++ b/src/space/services/subspace/subspace.service.ts @@ -1,6 +1,13 @@ import { BaseResponseDto } from '@app/common/dto/base.response.dto'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; -import { AddSubspaceDto, GetSpaceParam, GetSubSpaceParam } from '../../dtos'; +import { + AddSubspaceDto, + CreateTagDto, + DeleteSubspaceDto, + GetSpaceParam, + GetSubSpaceParam, + ModifySubspaceDto, +} from '../../dtos'; import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; import { TypeORMCustomModel, @@ -8,25 +15,26 @@ 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 { QueryRunner } from 'typeorm'; +import { In, QueryRunner } from 'typeorm'; import { SpaceEntity, SubspaceEntity, } from '@app/common/modules/space/entities'; -import { - SpaceModelEntity, - SubspaceModelEntity, -} from '@app/common/modules/space-model'; +import { 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'; +import { TagService } from '../tag'; +import { ModifyAction } from '@app/common/constants/modify-action.enum'; +import { SubspaceDeviceService } from './subspace-device.service'; +import { ModifyTagDto } from 'src/space/dtos/tag/modify-tag.dto'; @Injectable() export class SubSpaceService { constructor( private readonly subspaceRepository: SubspaceRepository, private readonly validationService: ValidationService, - private readonly productService: SubspaceProductService, + private readonly tagService: TagService, + public readonly deviceService: SubspaceDeviceService, ) {} async createSubspaces( @@ -38,10 +46,15 @@ export class SubSpaceService { queryRunner: QueryRunner, ): Promise { try { + const subspaceNames = subspaceData.map((data) => data.subspaceName); + + await this.checkExistingNamesInSpace( + subspaceNames, + subspaceData[0].space, + ); const subspaces = subspaceData.map((data) => queryRunner.manager.create(this.subspaceRepository.target, data), ); - return await queryRunner.manager.save(subspaces); } catch (error) { throw new HttpException( @@ -52,15 +65,13 @@ export class SubSpaceService { } async createSubSpaceFromModel( - spaceModel: SpaceModelEntity, + subspaceModels: SubspaceModelEntity[], space: SpaceEntity, queryRunner: QueryRunner, ): Promise { - const subSpaceModels = spaceModel.subspaceModels; + if (!subspaceModels?.length) return; - if (!subSpaceModels?.length) return; - - const subspaceData = subSpaceModels.map((subSpaceModel) => ({ + const subspaceData = subspaceModels.map((subSpaceModel) => ({ subspaceName: subSpaceModel.subspaceName, space, subSpaceModel, @@ -69,13 +80,14 @@ export class SubSpaceService { const subspaces = await this.createSubspaces(subspaceData, queryRunner); await Promise.all( - subSpaceModels.map((model, index) => { - this.productService.createFromModel( - model, - subspaces[index], + subspaceModels.map((model, index) => + this.tagService.createTagsFromModel( queryRunner, - ); - }), + model.tags || [], + null, + subspaces[index], + ), + ), ); } @@ -83,8 +95,14 @@ export class SubSpaceService { addSubspaceDtos: AddSubspaceDto[], space: SpaceEntity, queryRunner: QueryRunner, + otherTags?: CreateTagDto[], ): Promise { try { + await this.validateName( + addSubspaceDtos.map((dto) => dto.subspaceName), + space, + ); + const subspaceData = addSubspaceDtos.map((dto) => ({ subspaceName: dto.subspaceName, space, @@ -93,20 +111,31 @@ export class SubSpaceService { const subspaces = await this.createSubspaces(subspaceData, queryRunner); await Promise.all( - addSubspaceDtos.map((dto, index) => - this.productService.createFromDto( - dto.products, - subspaces[index], - queryRunner, - space, - ), - ), + addSubspaceDtos.map(async (dto, index) => { + const otherDtoTags = addSubspaceDtos + .filter((_, i) => i !== index) + .flatMap((otherDto) => otherDto.tags || []); + const subspace = subspaces[index]; + if (dto.tags?.length) { + subspace.tags = await this.tagService.createTags( + dto.tags, + queryRunner, + null, + subspace, + [...(otherTags || []), ...otherDtoTags], + ); + } + }), ); - return subspaces; } catch (error) { - throw new Error( - `Transaction failed: Unable to create subspaces and products. ${error.message}`, + if (error instanceof HttpException) { + throw error; + } + + throw new HttpException( + 'Failed to save subspaces due to an unexpected error.', + HttpStatus.INTERNAL_SERVER_ERROR, ); } } @@ -123,6 +152,10 @@ export class SubSpaceService { ); try { + await this.checkExistingNamesInSpace( + [addSubspaceDto.subspaceName], + space, + ); const newSubspace = this.subspaceRepository.create({ ...addSubspaceDto, space, @@ -167,43 +200,6 @@ export class SubSpaceService { } } - async findOne(params: GetSubSpaceParam): Promise { - const { communityUuid, subSpaceUuid, spaceUuid, projectUuid } = params; - await this.validationService.validateSpaceWithinCommunityAndProject( - communityUuid, - projectUuid, - spaceUuid, - ); - try { - const subSpace = await this.subspaceRepository.findOne({ - where: { - uuid: subSpaceUuid, - }, - }); - - // If space is not found, throw a NotFoundException - if (!subSpace) { - throw new HttpException( - `Sub Space with UUID ${subSpaceUuid} not found`, - HttpStatus.NOT_FOUND, - ); - } - return new SuccessResponseDto({ - message: `Subspace with ID ${subSpaceUuid} successfully fetched`, - data: subSpace, - }); - } catch (error) { - if (error instanceof HttpException) { - throw error; // If it's an HttpException, rethrow it - } else { - throw new HttpException( - 'An error occurred while deleting the subspace', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - } - async updateSubSpace( params: GetSubSpaceParam, updateSubSpaceDto: AddSubspaceDto, @@ -279,4 +275,266 @@ export class SubSpaceService { ); } } + + async deleteSubspaces( + deleteDtos: DeleteSubspaceDto[], + queryRunner: QueryRunner, + ) { + const deleteResults: { uuid: string }[] = []; + + for (const dto of deleteDtos) { + const subspace = await this.findOne(dto.subspaceUuid); + + await queryRunner.manager.update( + this.subspaceRepository.target, + { uuid: dto.subspaceUuid }, + { disabled: true }, + ); + + if (subspace.tags?.length) { + const modifyTagDtos = subspace.tags.map((tag) => ({ + uuid: tag.uuid, + action: ModifyAction.DELETE, + })); + await this.tagService.modifyTags( + modifyTagDtos, + queryRunner, + null, + subspace, + ); + } + + if (subspace.devices) + await this.deviceService.deleteSubspaceDevices( + subspace.devices, + queryRunner, + ); + + deleteResults.push({ uuid: dto.subspaceUuid }); + } + + return deleteResults; + } + + async modifySubSpace( + subspaceDtos: ModifySubspaceDto[], + queryRunner: QueryRunner, + space?: SpaceEntity, + ) { + for (const subspace of subspaceDtos) { + switch (subspace.action) { + case ModifyAction.ADD: + await this.handleAddAction(subspace, space, queryRunner); + break; + case ModifyAction.UPDATE: + await this.handleUpdateAction(subspace, queryRunner); + break; + case ModifyAction.DELETE: + await this.handleDeleteAction(subspace, queryRunner); + break; + default: + throw new HttpException( + `Invalid action "${subspace.action}".`, + HttpStatus.BAD_REQUEST, + ); + } + } + } + + async unlinkModels( + subspaces: SubspaceEntity[], + queryRunner: QueryRunner, + ): Promise { + if (!subspaces || subspaces.length === 0) { + return; + } + try { + const allTags = subspaces.flatMap((subSpace) => { + subSpace.subSpaceModel = null; + return subSpace.tags || []; + }); + + await this.tagService.unlinkModels(allTags, queryRunner); + + await queryRunner.manager.save(subspaces); + } catch (error) { + if (error instanceof HttpException) throw error; + throw new HttpException( + `Failed to unlink subspace models: ${error.message}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async getOne(params: GetSubSpaceParam): Promise { + await this.validationService.validateSpaceWithinCommunityAndProject( + params.communityUuid, + params.projectUuid, + params.spaceUuid, + ); + const subspace = await this.findOne(params.subSpaceUuid); + return new SuccessResponseDto({ + message: `Successfully retrieved subspace`, + data: subspace, + }); + } + + private async handleAddAction( + subspace: ModifySubspaceDto, + space: SpaceEntity, + queryRunner: QueryRunner, + ): Promise { + const createTagDtos: CreateTagDto[] = + subspace.tags?.map((tag) => ({ + tag: tag.tag as string, + productUuid: tag.productUuid as string, + })) || []; + const subSpace = await this.createSubspacesFromDto( + [{ subspaceName: subspace.subspaceName, tags: createTagDtos }], + space, + queryRunner, + ); + return subSpace[0]; + } + + private async handleUpdateAction( + modifyDto: ModifySubspaceDto, + queryRunner: QueryRunner, + ): Promise { + const subspace = await this.findOne(modifyDto.uuid); + await this.update( + queryRunner, + subspace, + modifyDto.subspaceName, + modifyDto.tags, + ); + } + + async update( + queryRunner: QueryRunner, + subspace: SubspaceEntity, + subspaceName?: string, + modifyTagDto?: ModifyTagDto[], + ) { + await this.updateSubspaceName(queryRunner, subspace, subspaceName); + + if (modifyTagDto?.length) { + await this.tagService.modifyTags( + modifyTagDto, + queryRunner, + null, + subspace, + ); + } + } + + async handleDeleteAction( + modifyDto: ModifySubspaceDto, + queryRunner: QueryRunner, + ): Promise { + const subspace = await this.findOne(modifyDto.uuid); + + await queryRunner.manager.update( + this.subspaceRepository.target, + { uuid: subspace.uuid }, + { disabled: true }, + ); + + if (subspace.tags?.length) { + const modifyTagDtos = subspace.tags.map((tag) => ({ + uuid: tag.uuid, + action: ModifyAction.DELETE, + })); + await this.tagService.modifyTags( + modifyTagDtos, + queryRunner, + null, + subspace, + ); + } + + if (subspace.devices.length > 0) { + await this.deviceService.deleteSubspaceDevices( + subspace.devices, + queryRunner, + ); + } + } + + private async findOne(subspaceUuid: string): Promise { + const subspace = await this.subspaceRepository.findOne({ + where: { uuid: subspaceUuid }, + relations: ['tags', 'space', 'devices', 'tags.product', 'tags.device'], + }); + if (!subspace) { + throw new HttpException( + `SubspaceModel with UUID ${subspaceUuid} not found.`, + HttpStatus.NOT_FOUND, + ); + } + return subspace; + } + + async updateSubspaceName( + queryRunner: QueryRunner, + subSpace: SubspaceEntity, + subspaceName?: string, + ): Promise { + if (subspaceName) { + subSpace.subspaceName = subspaceName; + await queryRunner.manager.save(subSpace); + } + } + + private async checkForDuplicateNames(names: string[]): Promise { + const seenNames = new Set(); + const duplicateNames = new Set(); + + for (const name of names) { + if (!seenNames.add(name)) { + duplicateNames.add(name); + } + } + + if (duplicateNames.size > 0) { + throw new HttpException( + `Duplicate subspace names found: ${[...duplicateNames].join(', ')}`, + HttpStatus.CONFLICT, + ); + } + } + + private async checkExistingNamesInSpace( + names: string[], + space: SpaceEntity, + ): Promise { + const existingNames = await this.subspaceRepository.find({ + select: ['subspaceName'], + where: { + subspaceName: In(names), + space: { + uuid: space.uuid, + }, + disabled: false, + }, + }); + + if (existingNames.length > 0) { + const existingNamesList = existingNames + .map((e) => e.subspaceName) + .join(', '); + throw new HttpException( + `Subspace names already exist in the space: ${existingNamesList}`, + HttpStatus.BAD_REQUEST, + ); + } + } + + private async validateName( + names: string[], + space: SpaceEntity, + ): Promise { + await this.checkForDuplicateNames(names); + await this.checkExistingNamesInSpace(names, space); + } } diff --git a/src/space/services/tag/index.ts b/src/space/services/tag/index.ts new file mode 100644 index 0000000..0cbeec4 --- /dev/null +++ b/src/space/services/tag/index.ts @@ -0,0 +1 @@ +export * from './tag.service'; diff --git a/src/space/services/tag/tag.service.ts b/src/space/services/tag/tag.service.ts new file mode 100644 index 0000000..4c3bb8f --- /dev/null +++ b/src/space/services/tag/tag.service.ts @@ -0,0 +1,350 @@ +import { ModifyAction } from '@app/common/constants/modify-action.enum'; +import { + SpaceEntity, + SubspaceEntity, + TagEntity, + TagRepository, +} from '@app/common/modules/space'; +import { TagModel } from '@app/common/modules/space-model'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { ProductService } from 'src/product/services'; +import { CreateTagDto } from 'src/space/dtos'; +import { ModifyTagDto } from 'src/space/dtos/tag/modify-tag.dto'; +import { QueryRunner } from 'typeorm'; + +@Injectable() +export class TagService { + constructor( + private readonly tagRepository: TagRepository, + private readonly productService: ProductService, + ) {} + + async createTags( + tags: CreateTagDto[], + queryRunner: QueryRunner, + space?: SpaceEntity, + subspace?: SubspaceEntity, + additionalTags?: CreateTagDto[], + ): Promise { + this.validateTagsInput(tags); + + const combinedTags = this.combineTags(tags, additionalTags); + this.ensureNoDuplicateTags(combinedTags); + + try { + const tagEntities = await Promise.all( + tags.map(async (tagDto) => + this.prepareTagEntity(tagDto, queryRunner, space, subspace), + ), + ); + + return await queryRunner.manager.save(tagEntities); + } catch (error) { + throw this.handleUnexpectedError('Failed to save tags', error); + } + } + + async createTagsFromModel( + queryRunner: QueryRunner, + tagModels: TagModel[], + space?: SpaceEntity, + subspace?: SubspaceEntity, + ): Promise { + if (!tagModels?.length) return; + + const tags = tagModels.map((model) => + queryRunner.manager.create(this.tagRepository.target, { + tag: model.tag, + space: space || undefined, + subspace: subspace || undefined, + product: model.product, + }), + ); + + await queryRunner.manager.save(tags); + } + + async updateTag( + tag: ModifyTagDto, + queryRunner: QueryRunner, + space?: SpaceEntity, + subspace?: SubspaceEntity, + ): Promise { + try { + const existingTag = await this.getTagByUuid(tag.uuid); + + const contextSpace = space ?? subspace?.space; + + if (contextSpace) { + await this.checkTagReuse( + tag.tag, + existingTag.product.uuid, + contextSpace, + ); + } + + return await queryRunner.manager.save( + Object.assign(existingTag, { tag: tag.tag }), + ); + } catch (error) { + throw this.handleUnexpectedError('Failed to update tags', error); + } + } + + async updateTagsFromModel( + model: TagModel, + queryRunner: QueryRunner, + ): Promise { + try { + const tags = await this.tagRepository.find({ + where: { + model: { + uuid: model.uuid, + }, + }, + }); + + if (!tags.length) return; + + await queryRunner.manager.update( + this.tagRepository.target, + { model: { uuid: model.uuid } }, + { tag: model.tag }, + ); + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + + throw new HttpException( + `Failed to update tags for model with UUID: ${model.uuid}. Reason: ${error.message}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async deleteTags(tagUuids: string[], queryRunner: QueryRunner) { + if (!tagUuids?.length) return; + + try { + await Promise.all( + tagUuids.map((id) => + queryRunner.manager.update( + this.tagRepository.target, + { uuid: id }, + { disabled: true, device: null }, + ), + ), + ); + return { message: 'Tags deleted successfully', tagUuids }; + } catch (error) { + throw this.handleUnexpectedError('Failed to update tags', error); + } + } + + async deleteTagFromModel(modelUuid: string, queryRunner: QueryRunner) { + try { + const tags = await this.tagRepository.find({ + where: { + model: { + uuid: modelUuid, + }, + }, + }); + + if (!tags.length) return; + + await queryRunner.manager.update( + this.tagRepository.target, + { model: { uuid: modelUuid } }, + { disabled: true, device: null }, + ); + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + + throw new HttpException( + `Failed to update tags for model with UUID: ${modelUuid}. Reason: ${error.message}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async modifyTags( + tags: ModifyTagDto[], + queryRunner: QueryRunner, + space?: SpaceEntity, + subspace?: SubspaceEntity, + ): Promise { + if (!tags?.length) return; + + try { + await Promise.all( + tags.map(async (tag) => { + switch (tag.action) { + case ModifyAction.ADD: + await this.createTags( + [{ tag: tag.tag, productUuid: tag.productUuid }], + queryRunner, + space, + subspace, + ); + break; + case ModifyAction.UPDATE: + await this.updateTag(tag, queryRunner, space, subspace); + break; + case ModifyAction.DELETE: + await this.deleteTags([tag.uuid], queryRunner); + + break; + default: + throw new HttpException( + `Invalid action "${tag.action}" provided.`, + HttpStatus.BAD_REQUEST, + ); + } + }), + ); + } catch (error) { + throw this.handleUnexpectedError('Failed to modify tags', error); + } + } + + async unlinkModels(tags: TagEntity[], queryRunner: QueryRunner) { + if (!tags?.length) return; + + try { + tags.forEach((tag) => { + tag.model = null; + }); + + await queryRunner.manager.save(tags); + } catch (error) { + throw this.handleUnexpectedError('Failed to unlink tag models', error); + } + } + + private findDuplicateTags(tags: CreateTagDto[]): string[] { + const seen = new Map(); + const duplicates: string[] = []; + + tags.forEach((tagDto) => { + const key = `${tagDto.productUuid}-${tagDto.tag}`; + if (seen.has(key)) { + duplicates.push(`${tagDto.tag} for Product: ${tagDto.productUuid}`); + } else { + seen.set(key, true); + } + }); + + return duplicates; + } + + private async checkTagReuse( + tag: string, + productUuid: string, + space: SpaceEntity, + ): Promise { + const { uuid: spaceUuid } = space; + + const tagExists = await this.tagRepository.exists({ + where: [ + { + tag, + space: { uuid: spaceUuid }, + product: { uuid: productUuid }, + disabled: false, + }, + { + tag, + subspace: { space: { uuid: spaceUuid } }, + product: { uuid: productUuid }, + disabled: false, + }, + ], + }); + + if (tagExists) { + throw new HttpException(`Tag can't be reused`, HttpStatus.CONFLICT); + } + } + + private async prepareTagEntity( + tagDto: CreateTagDto, + queryRunner: QueryRunner, + space?: SpaceEntity, + subspace?: SubspaceEntity, + ): Promise { + const product = await this.productService.findOne(tagDto.productUuid); + + if (!product) { + throw new HttpException( + `Product with UUID ${tagDto.productUuid} not found.`, + HttpStatus.NOT_FOUND, + ); + } + + await this.checkTagReuse( + tagDto.tag, + tagDto.productUuid, + space ?? subspace.space, + ); + + return queryRunner.manager.create(TagEntity, { + tag: tagDto.tag, + product: product.data, + space, + subspace, + }); + } + + private async getTagByUuid(uuid: string): Promise { + const tag = await this.tagRepository.findOne({ + where: { uuid }, + relations: ['product'], + }); + + if (!tag) { + throw new HttpException( + `Tag with ID ${uuid} not found.`, + HttpStatus.NOT_FOUND, + ); + } + return tag; + } + + private handleUnexpectedError( + message: string, + error: unknown, + ): HttpException { + if (error instanceof HttpException) throw error; + return new HttpException( + `${message}: ${(error as Error)?.message || 'Unknown error'}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + private combineTags( + primaryTags: CreateTagDto[], + additionalTags?: CreateTagDto[], + ): CreateTagDto[] { + return additionalTags ? [...primaryTags, ...additionalTags] : primaryTags; + } + + private ensureNoDuplicateTags(tags: CreateTagDto[]): void { + const duplicates = this.findDuplicateTags(tags); + + if (duplicates.length > 0) { + throw new HttpException( + `Duplicate tags found: ${duplicates.join(', ')}`, + HttpStatus.BAD_REQUEST, + ); + } + } + + private validateTagsInput(tags: CreateTagDto[]): void { + if (!tags?.length) { + return; + } + } +} diff --git a/src/space/space.module.ts b/src/space/space.module.ts index 81d99e2..f9a2317 100644 --- a/src/space/space.module.ts +++ b/src/space/space.module.ts @@ -12,27 +12,26 @@ import { import { SpaceDeviceService, SpaceLinkService, - SpaceProductItemService, - SpaceProductService, SpaceSceneService, SpaceService, SpaceUserService, SubspaceDeviceService, - SubspaceProductItemService, SubSpaceService, } from './services'; import { - SpaceProductRepository, SpaceRepository, SpaceLinkRepository, - SpaceProductItemRepository, + TagRepository, } from '@app/common/modules/space/repositories'; import { CommunityRepository } from '@app/common/modules/community/repositories'; import { UserRepository, UserSpaceRepository, } from '@app/common/modules/user/repositories'; -import { DeviceRepository } from '@app/common/modules/device/repositories'; +import { + DeviceRepository, + DeviceUserPermissionRepository, +} from '@app/common/modules/device/repositories'; import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; import { ProductRepository } from '@app/common/modules/product/repositories'; import { SceneService } from '../scene/services'; @@ -45,18 +44,30 @@ 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 { + SpaceModelRepository, + SubspaceModelRepository, + TagModelRepository, +} from '@app/common/modules/space-model'; import { CommunityModule } from 'src/community/community.module'; import { ValidationService } from './services'; +import { SubspaceRepository } from '@app/common/modules/space/repositories/subspace.repository'; +import { TagService } from './services/tag'; import { - SubspaceProductItemRepository, - SubspaceProductRepository, - SubspaceRepository, -} from '@app/common/modules/space/repositories/subspace.repository'; -import { SubspaceProductService } from './services'; + SpaceModelService, + SubSpaceModelService, + TagModelService, +} from 'src/space-model/services'; +import { UserSpaceService } from 'src/users/services'; +import { UserDevicePermissionService } from 'src/user-device-permission/services'; +import { PermissionTypeRepository } from '@app/common/modules/permission/repositories'; +import { CqrsModule } from '@nestjs/cqrs'; +import { DisableSpaceHandler } from './handlers'; + +export const CommandHandlers = [DisableSpaceHandler]; @Module({ - imports: [ConfigModule, SpaceRepositoryModule, CommunityModule], + imports: [ConfigModule, SpaceRepositoryModule, CommunityModule, CqrsModule], controllers: [ SpaceController, SpaceUserController, @@ -67,17 +78,20 @@ import { SubspaceProductService } from './services'; ], providers: [ ValidationService, + TagModelRepository, + TagRepository, SpaceService, TuyaService, + TagService, ProductRepository, SubSpaceService, SpaceDeviceService, SpaceLinkService, SubspaceDeviceService, SpaceRepository, + SubspaceRepository, DeviceRepository, CommunityRepository, - SubspaceRepository, SpaceLinkRepository, UserSpaceRepository, UserRepository, @@ -88,19 +102,19 @@ import { SubspaceProductService } from './services'; SceneRepository, DeviceService, DeviceStatusFirebaseService, - SubspaceProductItemRepository, DeviceStatusLogRepository, SceneDeviceRepository, - SpaceProductService, - SpaceProductRepository, + SpaceModelService, + SubSpaceModelService, + TagModelService, ProjectRepository, SpaceModelRepository, - SubspaceRepository, - SpaceProductItemService, - SpaceProductItemRepository, - SubspaceProductService, - SubspaceProductItemService, - SubspaceProductRepository, + SubspaceModelRepository, + UserSpaceService, + UserDevicePermissionService, + DeviceUserPermissionRepository, + PermissionTypeRepository, + ...CommandHandlers, ], exports: [SpaceService], }) diff --git a/src/users/services/user-space.service.ts b/src/users/services/user-space.service.ts index 1c7ac6e..0ee9af5 100644 --- a/src/users/services/user-space.service.ts +++ b/src/users/services/user-space.service.ts @@ -150,4 +150,20 @@ export class UserSpaceService { await Promise.all(permissionPromises); } + + async deleteUserSpace(spaceUuid: string) { + try { + await this.userSpaceRepository + .createQueryBuilder() + .delete() + .where('spaceUuid = :spaceUuid', { spaceUuid }) + .execute(); + } catch (error) { + console.error(`Error deleting user-space associations: ${error.message}`); + throw new HttpException( + `Failed to delete user-space associations: ${error.message}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } }