From eb916b79b44ce20ef93cfe9001d3de43f5d3b4f8 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 16 Nov 2024 23:05:06 -0600 Subject: [PATCH 01/13] finished 4scene configration endpoints --- libs/common/src/common.module.ts | 10 +- libs/common/src/constants/four-scene.enum.ts | 6 + libs/common/src/database/database.module.ts | 2 + .../modules/device/entities/device.entity.ts | 14 +- .../src/modules/scene-device/dtos/index.ts | 1 + .../scene-device/dtos/scene-device.dto.ts | 20 +++ .../modules/scene-device/entities/index.ts | 1 + .../entities/scene-device.entity.ts | 50 ++++++ .../scene-device/repositories/index.ts | 1 + .../repositories/scene-device.repository.ts | 10 ++ .../scene-device.repository.module.ts | 11 ++ .../modules/scene/entities/scene.entity.ts | 4 + src/automation/automation.module.ts | 10 ++ src/automation/services/automation.service.ts | 21 +++ src/device/controllers/device.controller.ts | 38 ++++- src/device/device.module.ts | 14 ++ src/device/dtos/add.device.dto.ts | 27 +++- src/device/dtos/control.device.dto.ts | 9 ++ src/device/services/device.service.ts | 149 +++++++++++++++++- src/door-lock/door.lock.module.ts | 12 ++ src/group/services/group.service.ts | 1 - src/scene/scene.module.ts | 2 + src/scene/services/scene.service.ts | 17 +- src/space/space.module.ts | 2 + .../visitor-password.module.ts | 12 ++ 25 files changed, 429 insertions(+), 15 deletions(-) create mode 100644 libs/common/src/constants/four-scene.enum.ts create mode 100644 libs/common/src/modules/scene-device/dtos/index.ts create mode 100644 libs/common/src/modules/scene-device/dtos/scene-device.dto.ts create mode 100644 libs/common/src/modules/scene-device/entities/index.ts create mode 100644 libs/common/src/modules/scene-device/entities/scene-device.entity.ts create mode 100644 libs/common/src/modules/scene-device/repositories/index.ts create mode 100644 libs/common/src/modules/scene-device/repositories/scene-device.repository.ts create mode 100644 libs/common/src/modules/scene-device/scene-device.repository.module.ts diff --git a/libs/common/src/common.module.ts b/libs/common/src/common.module.ts index cbe733d..de0780a 100644 --- a/libs/common/src/common.module.ts +++ b/libs/common/src/common.module.ts @@ -8,8 +8,15 @@ import config from './config'; 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'; @Module({ - providers: [CommonService, EmailService, ErrorMessageService, TuyaService], + providers: [ + CommonService, + EmailService, + ErrorMessageService, + TuyaService, + SceneDeviceRepository, + ], exports: [ CommonService, TuyaService, @@ -17,6 +24,7 @@ import { TuyaService } from './integrations/tuya/services/tuya.service'; AuthModule, EmailService, ErrorMessageService, + SceneDeviceRepository, ], imports: [ ConfigModule.forRoot({ diff --git a/libs/common/src/constants/four-scene.enum.ts b/libs/common/src/constants/four-scene.enum.ts new file mode 100644 index 0000000..7f43d70 --- /dev/null +++ b/libs/common/src/constants/four-scene.enum.ts @@ -0,0 +1,6 @@ +export enum FourSceneSwitchesEnum { + Scene_1 = 'scene_1', + Scene_2 = 'scene_2', + Scene_3 = 'scene_3', + Scene_4 = 'scene_4', +} diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index 44e69c9..868ef5a 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -22,6 +22,7 @@ import { VisitorPasswordEntity } from '../modules/visitor-password/entities'; 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'; @Module({ imports: [ @@ -60,6 +61,7 @@ import { SceneEntity, SceneIconEntity } from '../modules/scene/entities'; DeviceStatusLogEntity, SceneEntity, SceneIconEntity, + SceneDeviceEntity, ], namingStrategy: new SnakeNamingStrategy(), synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))), diff --git a/libs/common/src/modules/device/entities/device.entity.ts b/libs/common/src/modules/device/entities/device.entity.ts index 727e290..9a75950 100644 --- a/libs/common/src/modules/device/entities/device.entity.ts +++ b/libs/common/src/modules/device/entities/device.entity.ts @@ -1,4 +1,12 @@ -import { Column, Entity, ManyToOne, OneToMany, Unique, Index, JoinColumn } from 'typeorm'; +import { + Column, + Entity, + ManyToOne, + OneToMany, + Unique, + Index, + JoinColumn, +} from 'typeorm'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { DeviceDto, DeviceUserPermissionDto } from '../dtos/device.dto'; import { SpaceEntity, SubspaceEntity } from '../../space/entities'; @@ -6,6 +14,7 @@ import { ProductEntity } from '../../product/entities'; import { UserEntity } from '../../user/entities'; import { DeviceNotificationDto } from '../dtos'; import { PermissionTypeEntity } from '../../permission/entities'; +import { SceneDeviceEntity } from '../../scene-device/entities'; @Entity({ name: 'device' }) @Unique(['deviceTuyaUuid']) @@ -62,6 +71,9 @@ export class DeviceEntity extends AbstractEntity { @Column({ nullable: false }) uuid: string; + @OneToMany(() => SceneDeviceEntity, (sceneDevice) => sceneDevice.device, {}) + sceneDevices: SceneDeviceEntity[]; + constructor(partial: Partial) { super(); Object.assign(this, partial); diff --git a/libs/common/src/modules/scene-device/dtos/index.ts b/libs/common/src/modules/scene-device/dtos/index.ts new file mode 100644 index 0000000..96f03a1 --- /dev/null +++ b/libs/common/src/modules/scene-device/dtos/index.ts @@ -0,0 +1 @@ +export * from './scene-device.dto'; diff --git a/libs/common/src/modules/scene-device/dtos/scene-device.dto.ts b/libs/common/src/modules/scene-device/dtos/scene-device.dto.ts new file mode 100644 index 0000000..fd7e26a --- /dev/null +++ b/libs/common/src/modules/scene-device/dtos/scene-device.dto.ts @@ -0,0 +1,20 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class SceneDeviceDto { + @IsString() + @IsNotEmpty() + public uuid: string; + + @IsString() + @IsNotEmpty() + public deviceUuid: string; + @IsString() + @IsNotEmpty() + public sceneUuid: string; + @IsString() + @IsNotEmpty() + public switchName: string; + @IsString() + @IsNotEmpty() + public automationTuyaUuid: string; +} diff --git a/libs/common/src/modules/scene-device/entities/index.ts b/libs/common/src/modules/scene-device/entities/index.ts new file mode 100644 index 0000000..337a094 --- /dev/null +++ b/libs/common/src/modules/scene-device/entities/index.ts @@ -0,0 +1 @@ +export * from './scene-device.entity'; 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 new file mode 100644 index 0000000..537a627 --- /dev/null +++ b/libs/common/src/modules/scene-device/entities/scene-device.entity.ts @@ -0,0 +1,50 @@ +import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm'; +import { SceneDeviceDto } from '../dtos'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { FourSceneSwitchesEnum } from '@app/common/constants/four-scene.enum'; +import { DeviceEntity } from '../../device/entities'; +import { SceneEntity } from '../../scene/entities'; + +@Entity({ name: 'scene-device' }) +export class SceneDeviceEntity extends AbstractEntity { + @Column({ + type: 'uuid', + default: () => 'gen_random_uuid()', + nullable: false, + }) + public uuid: string; + + @ManyToOne(() => DeviceEntity, (device) => device.sceneDevices, { + nullable: false, + }) + @JoinColumn({ name: 'device_uuid' }) + device: DeviceEntity; + + @Column({ + nullable: false, + }) + sceneUuid: string; + + @Column({ + nullable: false, + type: 'enum', + enum: FourSceneSwitchesEnum, + }) + switchName: FourSceneSwitchesEnum; + + @Column({ + nullable: false, + }) + automationTuyaUuid: string; + + @ManyToOne(() => SceneEntity, (scene) => scene.sceneDevices, { + nullable: false, + }) + @JoinColumn({ name: 'scene_uuid' }) + scene: SceneEntity; + + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} diff --git a/libs/common/src/modules/scene-device/repositories/index.ts b/libs/common/src/modules/scene-device/repositories/index.ts new file mode 100644 index 0000000..f10ccb5 --- /dev/null +++ b/libs/common/src/modules/scene-device/repositories/index.ts @@ -0,0 +1 @@ +export * from './scene-device.repository'; diff --git a/libs/common/src/modules/scene-device/repositories/scene-device.repository.ts b/libs/common/src/modules/scene-device/repositories/scene-device.repository.ts new file mode 100644 index 0000000..1d7915c --- /dev/null +++ b/libs/common/src/modules/scene-device/repositories/scene-device.repository.ts @@ -0,0 +1,10 @@ +import { DataSource, Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { SceneDeviceEntity } from '../entities'; + +@Injectable() +export class SceneDeviceRepository extends Repository { + constructor(private dataSource: DataSource) { + super(SceneDeviceEntity, dataSource.createEntityManager()); + } +} diff --git a/libs/common/src/modules/scene-device/scene-device.repository.module.ts b/libs/common/src/modules/scene-device/scene-device.repository.module.ts new file mode 100644 index 0000000..96fa8a7 --- /dev/null +++ b/libs/common/src/modules/scene-device/scene-device.repository.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { SceneDeviceEntity } from './entities'; + +@Module({ + providers: [], + exports: [], + controllers: [], + imports: [TypeOrmModule.forFeature([SceneDeviceEntity])], +}) +export class SceneDeviceRepositoryModule {} diff --git a/libs/common/src/modules/scene/entities/scene.entity.ts b/libs/common/src/modules/scene/entities/scene.entity.ts index 1e04fd7..58a880a 100644 --- a/libs/common/src/modules/scene/entities/scene.entity.ts +++ b/libs/common/src/modules/scene/entities/scene.entity.ts @@ -2,6 +2,7 @@ import { Column, Entity, ManyToOne, OneToMany } from 'typeorm'; import { SceneDto, SceneIconDto } from '../dtos'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { SceneIconType } from '@app/common/constants/secne-icon-type.enum'; +import { SceneDeviceEntity } from '../../scene-device/entities'; // Define SceneIconEntity before SceneEntity @Entity({ name: 'scene-icon' }) @@ -59,6 +60,9 @@ export class SceneEntity extends AbstractEntity { }) sceneIcon: SceneIconEntity; + @OneToMany(() => SceneDeviceEntity, (sceneDevice) => sceneDevice.scene) + sceneDevices: SceneDeviceEntity[]; + constructor(partial: Partial) { super(); Object.assign(this, partial); diff --git a/src/automation/automation.module.ts b/src/automation/automation.module.ts index 3a3d7db..0a05f0e 100644 --- a/src/automation/automation.module.ts +++ b/src/automation/automation.module.ts @@ -9,6 +9,12 @@ import { DeviceRepository } from '@app/common/modules/device/repositories'; import { ProductRepository } from '@app/common/modules/product/repositories'; import { DeviceStatusFirebaseModule } from '@app/common/firebase/devices-status/devices-status.module'; import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; +import { SceneService } from 'src/scene/services'; +import { + SceneIconRepository, + SceneRepository, +} from '@app/common/modules/scene/repositories'; +import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories'; @Module({ imports: [ConfigModule, SpaceRepositoryModule, DeviceStatusFirebaseModule], @@ -20,6 +26,10 @@ import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service DeviceService, DeviceRepository, ProductRepository, + SceneService, + SceneIconRepository, + SceneRepository, + SceneDeviceRepository, ], exports: [AutomationService], }) diff --git a/src/automation/services/automation.service.ts b/src/automation/services/automation.service.ts index 468a4ae..27d1b6a 100644 --- a/src/automation/services/automation.service.ts +++ b/src/automation/services/automation.service.ts @@ -30,6 +30,7 @@ import { } from '@app/common/constants/automation.enum'; import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; import { ConvertedAction } from '@app/common/integrations/tuya/interfaces'; +import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories'; @Injectable() export class AutomationService { @@ -39,6 +40,7 @@ export class AutomationService { private readonly spaceRepository: SpaceRepository, private readonly deviceService: DeviceService, private readonly tuyaService: TuyaService, + private readonly sceneDeviceRepository: SceneDeviceRepository, ) { const accessKey = this.configService.get('auth-config.ACCESS_KEY'); const secretKey = this.configService.get('auth-config.SECRET_KEY'); @@ -304,6 +306,16 @@ export class AutomationService { HttpStatus.BAD_REQUEST, ); } + const existingSceneDevice = await this.sceneDeviceRepository.findOne({ + where: { automationTuyaUuid: automationUuid }, + }); + + if (existingSceneDevice) { + await this.sceneDeviceRepository.delete({ + automationTuyaUuid: automationUuid, + }); + } + const response = this.tuyaService.deleteAutomation( automation.spaceId, automationUuid, @@ -323,6 +335,15 @@ export class AutomationService { async delete(tuyaSpaceId: string, automationUuid: string) { try { + const existingSceneDevice = await this.sceneDeviceRepository.findOne({ + where: { automationTuyaUuid: automationUuid }, + }); + + if (existingSceneDevice) { + await this.sceneDeviceRepository.delete({ + automationTuyaUuid: automationUuid, + }); + } const response = await this.tuyaService.deleteAutomation( tuyaSpaceId, automationUuid, diff --git a/src/device/controllers/device.controller.ts b/src/device/controllers/device.controller.ts index 5a2fba5..15bf622 100644 --- a/src/device/controllers/device.controller.ts +++ b/src/device/controllers/device.controller.ts @@ -12,13 +12,18 @@ import { Put, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; -import { AddDeviceDto, UpdateDeviceInSpaceDto } from '../dtos/add.device.dto'; +import { + AddDeviceDto, + AddSceneToFourSceneDeviceDto, + UpdateDeviceInSpaceDto, +} from '../dtos/add.device.dto'; import { GetDeviceLogsDto } from '../dtos/get.device.dto'; import { ControlDeviceDto, BatchControlDevicesDto, BatchStatusDevicesDto, BatchFactoryResetDevicesDto, + GetSceneFourSceneDeviceDto, } from '../dtos/control.device.dto'; import { CheckRoomGuard } from 'src/guards/room.guard'; import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; @@ -184,4 +189,35 @@ export class DeviceController { powerClampUuid, ); } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Post('four-scene/:deviceUuid') + async addSceneToFourSceneDevice( + @Param('deviceUuid') deviceUuid: string, + @Body() addSceneToFourSceneDeviceDto: AddSceneToFourSceneDeviceDto, + ) { + const device = await this.deviceService.addSceneToFourSceneDevice( + deviceUuid, + addSceneToFourSceneDeviceDto, + ); + + return { + statusCode: HttpStatus.CREATED, + success: true, + message: `scene added successfully to device ${deviceUuid}`, + data: device, + }; + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('four-scene/:deviceUuid') + async getSceneFourSceneDevice( + @Param('deviceUuid') deviceUuid: string, + @Query() getSceneFourSceneDeviceDto: GetSceneFourSceneDeviceDto, + ) { + return await this.deviceService.getSceneFourSceneDevice( + deviceUuid, + getSceneFourSceneDeviceDto, + ); + } } diff --git a/src/device/device.module.ts b/src/device/device.module.ts index 5d17318..39b1be6 100644 --- a/src/device/device.module.ts +++ b/src/device/device.module.ts @@ -11,9 +11,18 @@ import { SpaceRepository } from '@app/common/modules/space/repositories'; import { DeviceUserPermissionRepository } from '@app/common/modules/device/repositories'; import { UserRepository } from '@app/common/modules/user/repositories'; import { DeviceStatusFirebaseModule } from '@app/common/firebase/devices-status/devices-status.module'; +import { SpaceModule } from 'src/space/space.module'; +import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; +import { SceneService } from 'src/scene/services'; +import { + SceneIconRepository, + SceneRepository, +} from '@app/common/modules/scene/repositories'; +import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories'; @Module({ imports: [ ConfigModule, + SpaceModule, ProductRepositoryModule, DeviceRepositoryModule, DeviceStatusFirebaseModule, @@ -27,6 +36,11 @@ import { DeviceStatusFirebaseModule } from '@app/common/firebase/devices-status/ SpaceRepository, DeviceRepository, UserRepository, + TuyaService, + SceneService, + SceneIconRepository, + SceneRepository, + SceneDeviceRepository, ], exports: [DeviceService], }) diff --git a/src/device/dtos/add.device.dto.ts b/src/device/dtos/add.device.dto.ts index bff522f..b5884a8 100644 --- a/src/device/dtos/add.device.dto.ts +++ b/src/device/dtos/add.device.dto.ts @@ -1,5 +1,6 @@ +import { FourSceneSwitchesEnum } from '@app/common/constants/four-scene.enum'; import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; +import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; export class AddDeviceDto { @ApiProperty({ @@ -35,3 +36,27 @@ export class UpdateDeviceInSpaceDto { @IsNotEmpty() public spaceUuid: string; } +export class AddSceneToFourSceneDeviceDto { + @ApiProperty({ + description: 'switchName', + required: true, + }) + @IsEnum(FourSceneSwitchesEnum) + @IsNotEmpty() + switchName: FourSceneSwitchesEnum; + + @ApiProperty({ + description: 'sceneUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public sceneUuid: string; + @ApiProperty({ + description: 'spaceUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public spaceUuid: string; +} diff --git a/src/device/dtos/control.device.dto.ts b/src/device/dtos/control.device.dto.ts index d917a96..7a1e900 100644 --- a/src/device/dtos/control.device.dto.ts +++ b/src/device/dtos/control.device.dto.ts @@ -54,3 +54,12 @@ export class BatchFactoryResetDevicesDto { @IsNotEmpty() public devicesUuid: [string]; } +export class GetSceneFourSceneDeviceDto { + @ApiProperty({ + description: 'switchName', + required: true, + }) + @IsString() + @IsNotEmpty() + public switchName: string; +} diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index 09b905c..a402973 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -5,10 +5,16 @@ import { HttpStatus, NotFoundException, BadRequestException, + forwardRef, + Inject, } from '@nestjs/common'; import { TuyaContext } from '@tuya/tuya-connector-nodejs'; import { ConfigService } from '@nestjs/config'; -import { AddDeviceDto, UpdateDeviceInSpaceDto } from '../dtos/add.device.dto'; +import { + AddDeviceDto, + AddSceneToFourSceneDeviceDto, + UpdateDeviceInSpaceDto, +} from '../dtos/add.device.dto'; import { DeviceInstructionResponse, GetDeviceDetailsFunctionsInterface, @@ -28,6 +34,7 @@ import { BatchFactoryResetDevicesDto, BatchStatusDevicesDto, ControlDeviceDto, + GetSceneFourSceneDeviceDto, } from '../dtos/control.device.dto'; import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; import { DeviceRepository } from '@app/common/modules/device/repositories'; @@ -40,6 +47,11 @@ import { DeviceStatuses } from '@app/common/constants/device-status.enum'; import { CommonErrorCodes } from '@app/common/constants/error-codes.enum'; import { BatteryStatus } from '@app/common/constants/battery-status.enum'; import { SpaceEntity } from '@app/common/modules/space/entities'; +import { SceneService } from 'src/scene/services'; +import { AddAutomationDto } from 'src/automation/dtos'; +import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; +import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories'; +import { FourSceneSwitchesEnum } from '@app/common/constants/four-scene.enum'; @Injectable() export class DeviceService { @@ -47,9 +59,13 @@ export class DeviceService { constructor( private readonly configService: ConfigService, private readonly deviceRepository: DeviceRepository, + private readonly sceneDeviceRepository: SceneDeviceRepository, private readonly productRepository: ProductRepository, private readonly deviceStatusFirebaseService: DeviceStatusFirebaseService, private readonly spaceRepository: SpaceRepository, + @Inject(forwardRef(() => SceneService)) + private readonly sceneService: SceneService, + private readonly tuyaService: TuyaService, ) { const accessKey = this.configService.get('auth-config.ACCESS_KEY'); const secretKey = this.configService.get('auth-config.SECRET_KEY'); @@ -630,7 +646,6 @@ export class DeviceService { status: deviceStatus.result[0].status, }; } catch (error) { - console.log(error); throw new HttpException( 'Error fetching device functions status', HttpStatus.INTERNAL_SERVER_ERROR, @@ -1136,4 +1151,134 @@ export class DeviceService { return descendants; } + async addSceneToFourSceneDevice( + deviceUuid: string, + addSceneToFourSceneDeviceDto: AddSceneToFourSceneDeviceDto, + ) { + try { + const { spaceUuid, sceneUuid, switchName } = addSceneToFourSceneDeviceDto; + + if (!spaceUuid || !sceneUuid || !switchName) { + throw new BadRequestException('Missing required fields in DTO'); + } + + const [sceneData, spaceData, deviceData] = await Promise.all([ + this.sceneService.findScene(sceneUuid), + this.sceneService.getSpaceByUuid(spaceUuid), + this.getDeviceByDeviceUuid(deviceUuid), + ]); + + // Generate a shorter automation name (e.g., "Auto_ABC123_169") + const shortUuid = deviceUuid.slice(0, 6); // First 6 characters of the UUID + const timestamp = Math.floor(Date.now() / 1000); // Current timestamp in seconds + const automationName = `Auto_${shortUuid}_${timestamp}`; + + const addAutomationData: AddAutomationDto = { + spaceUuid: spaceData.spaceTuyaUuid, + automationName, + decisionExpr: 'and', + effectiveTime: { + start: '00:00', + end: '23:59', + loops: '1111111', + }, + conditions: [ + { + code: 1, + entityId: deviceData.deviceTuyaUuid, + entityType: 'device_report', + expr: { + comparator: '==', + statusCode: switchName, + statusValue: 'scene', + }, + }, + ], + actions: [ + { + actionExecutor: 'rule_trigger', + entityId: sceneData.sceneTuyaUuid, + }, + ], + }; + + // Create automation + const automation = await this.tuyaService.createAutomation( + addAutomationData.spaceUuid, + addAutomationData.automationName, + addAutomationData.effectiveTime, + addAutomationData.decisionExpr, + addAutomationData.conditions, + addAutomationData.actions, + ); + + if (automation.success) { + const existingSceneDevice = await this.sceneDeviceRepository.findOne({ + where: { + device: { uuid: deviceUuid }, + scene: { uuid: sceneUuid }, + switchName: switchName, + }, + relations: ['scene', 'device'], + }); + + if (existingSceneDevice) { + await this.tuyaService.deleteAutomation( + spaceData.spaceTuyaUuid, + existingSceneDevice.automationTuyaUuid, + ); + + existingSceneDevice.automationTuyaUuid = automation.result.id; + existingSceneDevice.scene = sceneData; + existingSceneDevice.device = deviceData; + existingSceneDevice.switchName = switchName; + + return await this.sceneDeviceRepository.save(existingSceneDevice); + } else { + const sceneDevice = await this.sceneDeviceRepository.save({ + scene: sceneData, + device: deviceData, + automationTuyaUuid: automation.result.id, + switchName: switchName, + }); + return sceneDevice; + } + } + } catch (err) { + const errorMessage = err.message || 'Error creating automation'; + const errorStatus = err.status || HttpStatus.INTERNAL_SERVER_ERROR; + throw new HttpException(errorMessage, errorStatus); + } + } + async getSceneFourSceneDevice( + deviceUuid: string, + getSceneFourSceneDeviceDto: GetSceneFourSceneDeviceDto, + ) { + try { + const sceneDevice = await this.sceneDeviceRepository.findOne({ + where: { + device: { uuid: deviceUuid }, + switchName: + getSceneFourSceneDeviceDto.switchName as FourSceneSwitchesEnum, // Cast the string to the enum + }, + relations: ['device', 'scene'], + }); + if (sceneDevice.uuid) { + const SceneDetails = await this.sceneService.getSceneByUuid( + sceneDevice.scene.uuid, + ); + return { + switchName: sceneDevice.switchName, + createdAt: sceneDevice.createdAt, + updatedAt: sceneDevice.updatedAt, + deviceUuid: sceneDevice.device.uuid, + scene: { + ...SceneDetails.data, + }, + }; + } + } catch (error) { + throw new HttpException('Scene device not found', HttpStatus.NOT_FOUND); + } + } } diff --git a/src/door-lock/door.lock.module.ts b/src/door-lock/door.lock.module.ts index d5f070e..9bbc0d5 100644 --- a/src/door-lock/door.lock.module.ts +++ b/src/door-lock/door.lock.module.ts @@ -11,6 +11,13 @@ import { ProductRepository } from '@app/common/modules/product/repositories'; import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service'; import { SpaceRepository } from '@app/common/modules/space/repositories'; import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories'; +import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; +import { SceneService } from 'src/scene/services'; +import { + SceneIconRepository, + SceneRepository, +} from '@app/common/modules/scene/repositories'; +import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories'; @Module({ imports: [ConfigModule, DeviceRepositoryModule], controllers: [DoorLockController], @@ -24,6 +31,11 @@ import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log DeviceStatusFirebaseService, SpaceRepository, DeviceStatusLogRepository, + TuyaService, + SceneService, + SceneIconRepository, + SceneRepository, + SceneDeviceRepository, ], exports: [DoorLockService], }) diff --git a/src/group/services/group.service.ts b/src/group/services/group.service.ts index 9759232..049e82d 100644 --- a/src/group/services/group.service.ts +++ b/src/group/services/group.service.ts @@ -120,7 +120,6 @@ export class GroupService { throw new HttpException('No devices found', HttpStatus.NOT_FOUND); return devices.flat(); // Flatten the array since flatMap was used } catch (error) { - console.log(error); throw new HttpException( 'This space does not have any devices for the specified group name', HttpStatus.NOT_FOUND, diff --git a/src/scene/scene.module.ts b/src/scene/scene.module.ts index 680de5f..9da0156 100644 --- a/src/scene/scene.module.ts +++ b/src/scene/scene.module.ts @@ -13,6 +13,7 @@ import { SceneRepository, } from '@app/common/modules/scene/repositories'; import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; +import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories'; @Module({ imports: [ConfigModule, SpaceRepositoryModule, DeviceStatusFirebaseModule], @@ -26,6 +27,7 @@ import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service ProductRepository, SceneIconRepository, SceneRepository, + SceneDeviceRepository, ], exports: [SceneService], }) diff --git a/src/scene/services/scene.service.ts b/src/scene/services/scene.service.ts index 08d05be..9c5791b 100644 --- a/src/scene/services/scene.service.ts +++ b/src/scene/services/scene.service.ts @@ -3,6 +3,8 @@ import { HttpException, HttpStatus, BadRequestException, + forwardRef, + Inject, } from '@nestjs/common'; import { SpaceRepository } from '@app/common/modules/space/repositories'; import { @@ -14,7 +16,6 @@ import { UpdateSceneTapToRunDto, } from '../dtos'; import { convertKeysToSnakeCase } from '@app/common/helper/snakeCaseConverter'; -import { DeviceService } from 'src/device/services'; import { AddTapToRunSceneInterface, DeleteTapToRunSceneInterface, @@ -37,6 +38,7 @@ import { BaseResponseDto } from '@app/common/dto/base.response.dto'; import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; import { HttpStatusCode } from 'axios'; import { ConvertedAction } from '@app/common/integrations/tuya/interfaces'; +import { DeviceService } from 'src/device/services'; @Injectable() export class SceneService { @@ -44,8 +46,9 @@ export class SceneService { private readonly spaceRepository: SpaceRepository, private readonly sceneIconRepository: SceneIconRepository, private readonly sceneRepository: SceneRepository, - private readonly deviceService: DeviceService, private readonly tuyaService: TuyaService, + @Inject(forwardRef(() => DeviceService)) + private readonly deviceService: DeviceService, ) {} async createScene( @@ -178,7 +181,10 @@ export class SceneService { }); if (!scenesData.length) { - return []; + throw new HttpException( + `No scenes found for space UUID ${spaceUuid} with showInHomePage ${showInHomePage} `, + HttpStatus.NOT_FOUND, + ); } const scenes = await Promise.all( @@ -438,7 +444,6 @@ export class SceneService { if (err instanceof BadRequestException) { throw err; } else { - console.log(err); throw new HttpException( `An error occurred while retrieving scene details for ${scene.uuid}`, HttpStatus.INTERNAL_SERVER_ERROR, @@ -454,9 +459,6 @@ export class SceneService { const space = await this.getSpaceByUuid(scene.spaceUuid); await this.delete(scene.sceneTuyaUuid, space.spaceTuyaUuid); - - await this.sceneRepository.remove(scene); - return new SuccessResponseDto({ message: `Scene with ID ${sceneUuid} deleted successfully`, }); @@ -564,7 +566,6 @@ export class SceneService { }; } catch (err) { if (err instanceof BadRequestException) { - console.log(err); throw err; } else { throw new HttpException( diff --git a/src/space/space.module.ts b/src/space/space.module.ts index cf6b766..c840bd9 100644 --- a/src/space/space.module.ts +++ b/src/space/space.module.ts @@ -37,6 +37,7 @@ import { import { DeviceService } from 'src/device/services'; import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service'; import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories'; +import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories'; @Module({ imports: [ConfigModule, SpaceRepositoryModule], @@ -69,6 +70,7 @@ import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log DeviceService, DeviceStatusFirebaseService, DeviceStatusLogRepository, + SceneDeviceRepository, ], exports: [SpaceService], }) diff --git a/src/vistor-password/visitor-password.module.ts b/src/vistor-password/visitor-password.module.ts index 990e1e7..e768ad5 100644 --- a/src/vistor-password/visitor-password.module.ts +++ b/src/vistor-password/visitor-password.module.ts @@ -13,6 +13,13 @@ import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status import { SpaceRepository } from '@app/common/modules/space/repositories'; import { VisitorPasswordRepository } from '@app/common/modules/visitor-password/repositories'; import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories'; +import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; +import { SceneService } from 'src/scene/services'; +import { + SceneIconRepository, + SceneRepository, +} from '@app/common/modules/scene/repositories'; +import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories'; @Module({ imports: [ConfigModule, DeviceRepositoryModule, DoorLockModule], controllers: [VisitorPasswordController], @@ -27,6 +34,11 @@ import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log DeviceRepository, VisitorPasswordRepository, DeviceStatusLogRepository, + TuyaService, + SceneService, + SceneIconRepository, + SceneRepository, + SceneDeviceRepository, ], exports: [VisitorPasswordService], }) From c7a698629231ebae69b75d5d9479ed3bb4b5a9aa Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 16 Nov 2024 23:18:20 -0600 Subject: [PATCH 02/13] finished get mac address api --- src/device/interfaces/get.device.interface.ts | 6 +++++ src/device/services/device.service.ts | 22 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/device/interfaces/get.device.interface.ts b/src/device/interfaces/get.device.interface.ts index d003d2d..3215364 100644 --- a/src/device/interfaces/get.device.interface.ts +++ b/src/device/interfaces/get.device.interface.ts @@ -90,3 +90,9 @@ export interface GetPowerClampFunctionsStatusInterface { success: boolean; msg: string; } +export interface GetMacAddressInterface { + uuid: string; + mac: string; + sn: string; + id: string; +} diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index a402973..81ae501 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -20,6 +20,7 @@ import { GetDeviceDetailsFunctionsInterface, GetDeviceDetailsFunctionsStatusInterface, GetDeviceDetailsInterface, + GetMacAddressInterface, GetPowerClampFunctionsStatusInterface, controlDeviceInterface, getDeviceLogsInterface, @@ -535,6 +536,9 @@ export class DeviceService { const response = await this.getDeviceDetailsByDeviceIdTuya( deviceDetails.deviceTuyaUuid, ); + const macAddress = await this.getMacAddressByDeviceIdTuya( + deviceDetails.deviceTuyaUuid, + ); return { ...response, @@ -542,6 +546,7 @@ export class DeviceService { productUuid: deviceDetails.productDevice.uuid, productType: deviceDetails.productDevice.prodType, permissionType: userDevicePermission, + macAddress: macAddress.mac, }; } catch (error) { throw new HttpException( @@ -582,7 +587,24 @@ export class DeviceService { ); } } + async getMacAddressByDeviceIdTuya( + deviceId: string, + ): Promise { + try { + const path = `/v1.0/devices/factory-infos?device_ids=${deviceId}`; + const response = await this.tuya.request({ + method: 'GET', + path, + }); + return response.result[0]; + } catch (error) { + throw new HttpException( + 'Error fetching mac address device from Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } async getDeviceInstructionByDeviceId( deviceUuid: string, ): Promise { From 6d73968631143956854c82ff44397767181e883c Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 16 Nov 2024 23:36:10 -0600 Subject: [PATCH 03/13] finished update name device api --- src/device/controllers/device.controller.ts | 21 +++++++++++ src/device/dtos/add.device.dto.ts | 9 +++++ src/device/services/device.service.ts | 42 +++++++++++++++++++++ 3 files changed, 72 insertions(+) diff --git a/src/device/controllers/device.controller.ts b/src/device/controllers/device.controller.ts index 15bf622..cf977d2 100644 --- a/src/device/controllers/device.controller.ts +++ b/src/device/controllers/device.controller.ts @@ -15,6 +15,7 @@ import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { AddDeviceDto, AddSceneToFourSceneDeviceDto, + UpdateDeviceDto, UpdateDeviceInSpaceDto, } from '../dtos/add.device.dto'; import { GetDeviceLogsDto } from '../dtos/get.device.dto'; @@ -95,6 +96,26 @@ export class DeviceController { userUuid, ); } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Put(':deviceUuid') + async updateDevice( + @Param('deviceUuid') deviceUuid: string, + @Body() updateDeviceDto: UpdateDeviceDto, + ) { + const device = await this.deviceService.updateDevice( + deviceUuid, + updateDeviceDto, + ); + + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'device updated successfully', + data: device, + }; + } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get(':deviceUuid/functions') diff --git a/src/device/dtos/add.device.dto.ts b/src/device/dtos/add.device.dto.ts index b5884a8..bdc76f6 100644 --- a/src/device/dtos/add.device.dto.ts +++ b/src/device/dtos/add.device.dto.ts @@ -60,3 +60,12 @@ export class AddSceneToFourSceneDeviceDto { @IsNotEmpty() public spaceUuid: string; } +export class UpdateDeviceDto { + @ApiProperty({ + description: 'deviceName', + required: true, + }) + @IsString() + @IsNotEmpty() + public deviceName: string; +} diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index 81ae501..0216cc7 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -13,6 +13,7 @@ import { ConfigService } from '@nestjs/config'; import { AddDeviceDto, AddSceneToFourSceneDeviceDto, + UpdateDeviceDto, UpdateDeviceInSpaceDto, } from '../dtos/add.device.dto'; import { @@ -291,6 +292,26 @@ export class DeviceService { ); } } + async updateDeviceNameTuya( + deviceId: string, + deviceName: string, + ): Promise { + try { + const path = `/v2.0/cloud/thing/${deviceId}/attribute`; + const response = await this.tuya.request({ + method: 'POST', + path, + body: { type: 1, data: deviceName }, + }); + + return response as controlDeviceInterface; + } catch (error) { + throw new HttpException( + 'Error updating device name from Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } async controlDevice(controlDeviceDto: ControlDeviceDto, deviceUuid: string) { try { const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid, false); @@ -555,6 +576,27 @@ export class DeviceService { ); } } + async updateDevice(deviceUuid: string, updateDeviceDto: UpdateDeviceDto) { + try { + const device = await this.getDeviceByDeviceUuid(deviceUuid); + if (device.deviceTuyaUuid) { + await this.updateDeviceNameTuya( + device.deviceTuyaUuid, + updateDeviceDto.deviceName, + ); + } + + return { + uuid: device.uuid, + deviceName: updateDeviceDto.deviceName, + }; + } catch (error) { + throw new HttpException( + 'Error updating device', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } async getDeviceDetailsByDeviceIdTuya( deviceId: string, ): Promise { From 145e335bebee59eb93675c2dfccc7d07f45ab4c3 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 17 Nov 2024 03:04:33 -0600 Subject: [PATCH 04/13] finshed add subspace to devices details --- src/device/services/device.service.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index 0216cc7..15276d9 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -82,11 +82,15 @@ export class DeviceService { deviceUuid: string, withProductDevice: boolean = true, ) { - return await this.deviceRepository.findOne({ - where: { - uuid: deviceUuid, - }, - ...(withProductDevice && { relations: ['productDevice'] }), + const relations = ['subspace']; + + if (withProductDevice) { + relations.push('productDevice'); + } + + return this.deviceRepository.findOne({ + where: { uuid: deviceUuid }, + relations, }); } @@ -568,6 +572,7 @@ export class DeviceService { productType: deviceDetails.productDevice.prodType, permissionType: userDevicePermission, macAddress: macAddress.mac, + subspace: deviceDetails.subspace ? deviceDetails.subspace : {}, }; } catch (error) { throw new HttpException( From ccf207d6529eeac1ac30c9676bcf35f0b7c11132 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 18 Nov 2024 00:23:54 -0600 Subject: [PATCH 05/13] finished get all switches --- src/device/controllers/device.controller.ts | 8 +++- src/device/services/device.service.ts | 47 +++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/src/device/controllers/device.controller.ts b/src/device/controllers/device.controller.ts index cf977d2..564b9fc 100644 --- a/src/device/controllers/device.controller.ts +++ b/src/device/controllers/device.controller.ts @@ -231,7 +231,7 @@ export class DeviceController { } @ApiBearerAuth() @UseGuards(JwtAuthGuard) - @Get('four-scene/:deviceUuid') + @Get('four-scene/switch/:deviceUuid') async getSceneFourSceneDevice( @Param('deviceUuid') deviceUuid: string, @Query() getSceneFourSceneDeviceDto: GetSceneFourSceneDeviceDto, @@ -241,4 +241,10 @@ export class DeviceController { getSceneFourSceneDeviceDto, ); } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('four-scene/:deviceUuid') + async getScenesFourSceneDevice(@Param('deviceUuid') deviceUuid: string) { + return await this.deviceService.getScenesFourSceneDevice(deviceUuid); + } } diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index 15276d9..950da44 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -1350,4 +1350,51 @@ export class DeviceService { throw new HttpException('Scene device not found', HttpStatus.NOT_FOUND); } } + async getScenesFourSceneDevice(deviceUuid: string): Promise { + try { + const sceneDevices = await this.sceneDeviceRepository.find({ + where: { device: { uuid: deviceUuid } }, + relations: ['device', 'scene'], + }); + + if (!sceneDevices.length) { + throw new HttpException( + 'No scenes found for the device', + HttpStatus.NOT_FOUND, + ); + } + + const results = await Promise.all( + sceneDevices.map(async (sceneDevice) => { + if (!sceneDevice.scene?.uuid) return null; + + try { + const sceneDetails = await this.sceneService.getSceneByUuid( + sceneDevice.scene.uuid, + ); + + return { + switchName: sceneDevice.switchName, + createdAt: sceneDevice.createdAt, + updatedAt: sceneDevice.updatedAt, + deviceUuid: sceneDevice.device.uuid, + scene: sceneDetails.data, + }; + } catch (error) { + throw new HttpException( + 'Failed to fetch scene details', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + }), + ); + + return results.filter(Boolean); + } catch (error) { + throw new HttpException( + error.message || 'Failed to fetch scenes for device', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } } From 7ea69a10b971fad1b5b734232a4ade148b077493 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 19 Nov 2024 20:01:09 -0600 Subject: [PATCH 06/13] finished fix duplication --- .../src/modules/scene-device/entities/scene-device.entity.ts | 3 ++- src/device/services/device.service.ts | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) 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 537a627..27882d6 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 @@ -1,4 +1,4 @@ -import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm'; +import { Column, Entity, JoinColumn, ManyToOne, Unique } from 'typeorm'; import { SceneDeviceDto } from '../dtos'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { FourSceneSwitchesEnum } from '@app/common/constants/four-scene.enum'; @@ -6,6 +6,7 @@ import { DeviceEntity } from '../../device/entities'; import { SceneEntity } from '../../scene/entities'; @Entity({ name: 'scene-device' }) +@Unique(['device', 'switchName']) export class SceneDeviceEntity extends AbstractEntity { @Column({ type: 'uuid', diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index 950da44..1e1d5f0 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -1285,7 +1285,6 @@ export class DeviceService { const existingSceneDevice = await this.sceneDeviceRepository.findOne({ where: { device: { uuid: deviceUuid }, - scene: { uuid: sceneUuid }, switchName: switchName, }, relations: ['scene', 'device'], From 9510905fb47bdd1d2ac80831bd39da50839486b3 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 19 Nov 2024 20:44:45 -0600 Subject: [PATCH 07/13] finished config with 6 scene device --- libs/common/src/constants/four-scene.enum.ts | 6 --- .../common/src/constants/product-type.enum.ts | 2 + .../src/constants/scene-switch-type.enum.ts | 8 ++++ .../entities/scene-device.entity.ts | 6 +-- src/device/controllers/device.controller.ts | 25 +++++------ src/device/dtos/add.device.dto.ts | 6 +-- src/device/services/device.service.ts | 16 ++++--- src/guards/scene.device.type.guard.ts | 42 +++++++++++++++++++ 8 files changed, 81 insertions(+), 30 deletions(-) delete mode 100644 libs/common/src/constants/four-scene.enum.ts create mode 100644 libs/common/src/constants/scene-switch-type.enum.ts create mode 100644 src/guards/scene.device.type.guard.ts diff --git a/libs/common/src/constants/four-scene.enum.ts b/libs/common/src/constants/four-scene.enum.ts deleted file mode 100644 index 7f43d70..0000000 --- a/libs/common/src/constants/four-scene.enum.ts +++ /dev/null @@ -1,6 +0,0 @@ -export enum FourSceneSwitchesEnum { - Scene_1 = 'scene_1', - Scene_2 = 'scene_2', - Scene_3 = 'scene_3', - Scene_4 = 'scene_4', -} diff --git a/libs/common/src/constants/product-type.enum.ts b/libs/common/src/constants/product-type.enum.ts index 2f518fb..d368865 100644 --- a/libs/common/src/constants/product-type.enum.ts +++ b/libs/common/src/constants/product-type.enum.ts @@ -16,4 +16,6 @@ export enum ProductType { GD = 'GD', CUR = 'CUR', PC = 'PC', + FOUR_S = '4S', + SIX_S = '6S', } diff --git a/libs/common/src/constants/scene-switch-type.enum.ts b/libs/common/src/constants/scene-switch-type.enum.ts new file mode 100644 index 0000000..62175d5 --- /dev/null +++ b/libs/common/src/constants/scene-switch-type.enum.ts @@ -0,0 +1,8 @@ +export enum SceneSwitchesTypeEnum { + SCENE_1 = 'scene_1', + SCENE_2 = 'scene_2', + SCENE_3 = 'scene_3', + SCENE_4 = 'scene_4', + SCENE_5 = 'scene_5', + SCENE_6 = 'scene_6', +} 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 537a627..768bf09 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 @@ -1,7 +1,7 @@ import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm'; import { SceneDeviceDto } from '../dtos'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; -import { FourSceneSwitchesEnum } from '@app/common/constants/four-scene.enum'; +import { SceneSwitchesTypeEnum } from '@app/common/constants/scene-switch-type.enum'; import { DeviceEntity } from '../../device/entities'; import { SceneEntity } from '../../scene/entities'; @@ -28,9 +28,9 @@ export class SceneDeviceEntity extends AbstractEntity { @Column({ nullable: false, type: 'enum', - enum: FourSceneSwitchesEnum, + enum: SceneSwitchesTypeEnum, }) - switchName: FourSceneSwitchesEnum; + switchName: SceneSwitchesTypeEnum; @Column({ nullable: false, diff --git a/src/device/controllers/device.controller.ts b/src/device/controllers/device.controller.ts index 564b9fc..81da9f3 100644 --- a/src/device/controllers/device.controller.ts +++ b/src/device/controllers/device.controller.ts @@ -31,6 +31,7 @@ import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; import { CheckDeviceGuard } from 'src/guards/device.guard'; import { SuperAdminRoleGuard } from 'src/guards/super.admin.role.guard'; import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; +import { CheckFourAndSixSceneDeviceTypeGuard } from 'src/guards/scene.device.type.guard'; @ApiTags('Device Module') @Controller({ @@ -211,13 +212,13 @@ export class DeviceController { ); } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Post('four-scene/:deviceUuid') - async addSceneToFourSceneDevice( + @UseGuards(JwtAuthGuard, CheckFourAndSixSceneDeviceTypeGuard) + @Post('scene-switch/:deviceUuid') + async addSceneToSceneDevice( @Param('deviceUuid') deviceUuid: string, @Body() addSceneToFourSceneDeviceDto: AddSceneToFourSceneDeviceDto, ) { - const device = await this.deviceService.addSceneToFourSceneDevice( + const device = await this.deviceService.addSceneToSceneDevice( deviceUuid, addSceneToFourSceneDeviceDto, ); @@ -230,21 +231,21 @@ export class DeviceController { }; } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Get('four-scene/switch/:deviceUuid') - async getSceneFourSceneDevice( + @UseGuards(JwtAuthGuard, CheckFourAndSixSceneDeviceTypeGuard) + @Get('scene-switch/switch/:deviceUuid') + async getSceneBySceneDevice( @Param('deviceUuid') deviceUuid: string, @Query() getSceneFourSceneDeviceDto: GetSceneFourSceneDeviceDto, ) { - return await this.deviceService.getSceneFourSceneDevice( + return await this.deviceService.getSceneBySceneDevice( deviceUuid, getSceneFourSceneDeviceDto, ); } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Get('four-scene/:deviceUuid') - async getScenesFourSceneDevice(@Param('deviceUuid') deviceUuid: string) { - return await this.deviceService.getScenesFourSceneDevice(deviceUuid); + @UseGuards(JwtAuthGuard, CheckFourAndSixSceneDeviceTypeGuard) + @Get('scene-switch/:deviceUuid') + async getScenesBySceneDevice(@Param('deviceUuid') deviceUuid: string) { + return await this.deviceService.getScenesBySceneDevice(deviceUuid); } } diff --git a/src/device/dtos/add.device.dto.ts b/src/device/dtos/add.device.dto.ts index bdc76f6..69f42c4 100644 --- a/src/device/dtos/add.device.dto.ts +++ b/src/device/dtos/add.device.dto.ts @@ -1,4 +1,4 @@ -import { FourSceneSwitchesEnum } from '@app/common/constants/four-scene.enum'; +import { SceneSwitchesTypeEnum } from '@app/common/constants/scene-switch-type.enum'; import { ApiProperty } from '@nestjs/swagger'; import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; @@ -41,9 +41,9 @@ export class AddSceneToFourSceneDeviceDto { description: 'switchName', required: true, }) - @IsEnum(FourSceneSwitchesEnum) + @IsEnum(SceneSwitchesTypeEnum) @IsNotEmpty() - switchName: FourSceneSwitchesEnum; + switchName: SceneSwitchesTypeEnum; @ApiProperty({ description: 'sceneUuid', diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index 950da44..2ff0490 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -53,7 +53,7 @@ import { SceneService } from 'src/scene/services'; import { AddAutomationDto } from 'src/automation/dtos'; import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories'; -import { FourSceneSwitchesEnum } from '@app/common/constants/four-scene.enum'; +import { SceneSwitchesTypeEnum } from '@app/common/constants/scene-switch-type.enum'; @Injectable() export class DeviceService { @@ -1220,7 +1220,7 @@ export class DeviceService { return descendants; } - async addSceneToFourSceneDevice( + async addSceneToSceneDevice( deviceUuid: string, addSceneToFourSceneDeviceDto: AddSceneToFourSceneDeviceDto, ) { @@ -1319,7 +1319,8 @@ export class DeviceService { throw new HttpException(errorMessage, errorStatus); } } - async getSceneFourSceneDevice( + + async getSceneBySceneDevice( deviceUuid: string, getSceneFourSceneDeviceDto: GetSceneFourSceneDeviceDto, ) { @@ -1328,7 +1329,7 @@ export class DeviceService { where: { device: { uuid: deviceUuid }, switchName: - getSceneFourSceneDeviceDto.switchName as FourSceneSwitchesEnum, // Cast the string to the enum + getSceneFourSceneDeviceDto.switchName as SceneSwitchesTypeEnum, // Cast the string to the enum }, relations: ['device', 'scene'], }); @@ -1347,10 +1348,13 @@ export class DeviceService { }; } } catch (error) { - throw new HttpException('Scene device not found', HttpStatus.NOT_FOUND); + throw new HttpException( + error.message || 'Scene device not found', + error.status || HttpStatus.NOT_FOUND, + ); } } - async getScenesFourSceneDevice(deviceUuid: string): Promise { + async getScenesBySceneDevice(deviceUuid: string): Promise { try { const sceneDevices = await this.sceneDeviceRepository.find({ where: { device: { uuid: deviceUuid } }, diff --git a/src/guards/scene.device.type.guard.ts b/src/guards/scene.device.type.guard.ts new file mode 100644 index 0000000..777e60e --- /dev/null +++ b/src/guards/scene.device.type.guard.ts @@ -0,0 +1,42 @@ +import { ProductType } from '@app/common/constants/product-type.enum'; +import { + Injectable, + CanActivate, + ExecutionContext, + BadRequestException, + HttpException, +} from '@nestjs/common'; +import { DeviceService } from 'src/device/services'; + +@Injectable() +export class CheckFourAndSixSceneDeviceTypeGuard implements CanActivate { + constructor(private readonly deviceService: DeviceService) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const deviceUuid = request.params.deviceUuid; + + if (!deviceUuid) { + throw new BadRequestException('Device UUID is required'); + } + + try { + const deviceDetails = + await this.deviceService.getDeviceByDeviceUuid(deviceUuid); + + if ( + deviceDetails.productDevice.prodType !== ProductType.FOUR_S && + deviceDetails.productDevice.prodType !== ProductType.SIX_S + ) { + throw new BadRequestException('The device type is not supported'); + } + + return true; + } catch (error) { + throw new HttpException( + error.message || 'An error occurred', + error.status || 500, + ); + } + } +} From 4a51c9170b5f120d4f3b5797b30145ac827e9152 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 19 Nov 2024 20:53:39 -0600 Subject: [PATCH 08/13] add constants values --- libs/common/src/constants/automation.enum.ts | 10 +++++++++ src/device/services/device.service.ts | 22 ++++++++++---------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/libs/common/src/constants/automation.enum.ts b/libs/common/src/constants/automation.enum.ts index 7e431eb..e9bb6a8 100644 --- a/libs/common/src/constants/automation.enum.ts +++ b/libs/common/src/constants/automation.enum.ts @@ -7,3 +7,13 @@ export enum ActionExecutorEnum { export enum EntityTypeEnum { DEVICE_REPORT = 'device_report', } +export const AUTOMATION_CONFIG = { + DEFAULT_START_TIME: '00:00', + DEFAULT_END_TIME: '23:59', + DEFAULT_LOOPS: '1111111', + DECISION_EXPR: 'and', + CONDITION_TYPE: 'device_report', + ACTION_EXECUTOR: 'rule_trigger', + COMPARATOR: '==', + SCENE_STATUS_VALUE: 'scene', +}; diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index 1e44c6d..4d45264 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -54,6 +54,7 @@ import { AddAutomationDto } from 'src/automation/dtos'; import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories'; import { SceneSwitchesTypeEnum } from '@app/common/constants/scene-switch-type.enum'; +import { AUTOMATION_CONFIG } from '@app/common/constants/automation.enum'; @Injectable() export class DeviceService { @@ -1220,6 +1221,7 @@ export class DeviceService { return descendants; } + async addSceneToSceneDevice( deviceUuid: string, addSceneToFourSceneDeviceDto: AddSceneToFourSceneDeviceDto, @@ -1237,7 +1239,6 @@ export class DeviceService { this.getDeviceByDeviceUuid(deviceUuid), ]); - // Generate a shorter automation name (e.g., "Auto_ABC123_169") const shortUuid = deviceUuid.slice(0, 6); // First 6 characters of the UUID const timestamp = Math.floor(Date.now() / 1000); // Current timestamp in seconds const automationName = `Auto_${shortUuid}_${timestamp}`; @@ -1245,33 +1246,32 @@ export class DeviceService { const addAutomationData: AddAutomationDto = { spaceUuid: spaceData.spaceTuyaUuid, automationName, - decisionExpr: 'and', + decisionExpr: AUTOMATION_CONFIG.DECISION_EXPR, effectiveTime: { - start: '00:00', - end: '23:59', - loops: '1111111', + start: AUTOMATION_CONFIG.DEFAULT_START_TIME, + end: AUTOMATION_CONFIG.DEFAULT_END_TIME, + loops: AUTOMATION_CONFIG.DEFAULT_LOOPS, }, conditions: [ { code: 1, entityId: deviceData.deviceTuyaUuid, - entityType: 'device_report', + entityType: AUTOMATION_CONFIG.CONDITION_TYPE, expr: { - comparator: '==', + comparator: AUTOMATION_CONFIG.COMPARATOR, statusCode: switchName, - statusValue: 'scene', + statusValue: AUTOMATION_CONFIG.SCENE_STATUS_VALUE, }, }, ], actions: [ { - actionExecutor: 'rule_trigger', + actionExecutor: AUTOMATION_CONFIG.ACTION_EXECUTOR, entityId: sceneData.sceneTuyaUuid, }, ], }; - // Create automation const automation = await this.tuyaService.createAutomation( addAutomationData.spaceUuid, addAutomationData.automationName, @@ -1328,7 +1328,7 @@ export class DeviceService { where: { device: { uuid: deviceUuid }, switchName: - getSceneFourSceneDeviceDto.switchName as SceneSwitchesTypeEnum, // Cast the string to the enum + getSceneFourSceneDeviceDto.switchName as SceneSwitchesTypeEnum, }, relations: ['device', 'scene'], }); From 1fe5ba70b9d20751eab689d6c81cc93e91e92551 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 19 Nov 2024 23:51:20 -0600 Subject: [PATCH 09/13] resolve review comments --- src/device/controllers/device.controller.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/device/controllers/device.controller.ts b/src/device/controllers/device.controller.ts index 81da9f3..8da3b03 100644 --- a/src/device/controllers/device.controller.ts +++ b/src/device/controllers/device.controller.ts @@ -213,7 +213,7 @@ export class DeviceController { } @ApiBearerAuth() @UseGuards(JwtAuthGuard, CheckFourAndSixSceneDeviceTypeGuard) - @Post('scene-switch/:deviceUuid') + @Post(':deviceUuid/scenes') async addSceneToSceneDevice( @Param('deviceUuid') deviceUuid: string, @Body() addSceneToFourSceneDeviceDto: AddSceneToFourSceneDeviceDto, @@ -232,7 +232,7 @@ export class DeviceController { } @ApiBearerAuth() @UseGuards(JwtAuthGuard, CheckFourAndSixSceneDeviceTypeGuard) - @Get('scene-switch/switch/:deviceUuid') + @Get(':deviceUuid/scenes/switch') async getSceneBySceneDevice( @Param('deviceUuid') deviceUuid: string, @Query() getSceneFourSceneDeviceDto: GetSceneFourSceneDeviceDto, @@ -244,7 +244,7 @@ export class DeviceController { } @ApiBearerAuth() @UseGuards(JwtAuthGuard, CheckFourAndSixSceneDeviceTypeGuard) - @Get('scene-switch/:deviceUuid') + @Get(':deviceUuid/scenes') async getScenesBySceneDevice(@Param('deviceUuid') deviceUuid: string) { return await this.deviceService.getScenesBySceneDevice(deviceUuid); } From 16444c625a620d0df451064a7cf60f47d1ead716 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 20 Nov 2024 00:34:12 -0600 Subject: [PATCH 10/13] remove duplication code --- src/device/controllers/device.controller.ts | 12 +-- src/device/dtos/control.device.dto.ts | 8 +- src/device/services/device.service.ts | 82 ++++++++++----------- 3 files changed, 44 insertions(+), 58 deletions(-) diff --git a/src/device/controllers/device.controller.ts b/src/device/controllers/device.controller.ts index 8da3b03..28392aa 100644 --- a/src/device/controllers/device.controller.ts +++ b/src/device/controllers/device.controller.ts @@ -232,20 +232,14 @@ export class DeviceController { } @ApiBearerAuth() @UseGuards(JwtAuthGuard, CheckFourAndSixSceneDeviceTypeGuard) - @Get(':deviceUuid/scenes/switch') - async getSceneBySceneDevice( + @Get(':deviceUuid/scenes') + async getScenesBySceneDevice( @Param('deviceUuid') deviceUuid: string, @Query() getSceneFourSceneDeviceDto: GetSceneFourSceneDeviceDto, ) { - return await this.deviceService.getSceneBySceneDevice( + return await this.deviceService.getScenesBySceneDevice( deviceUuid, getSceneFourSceneDeviceDto, ); } - @ApiBearerAuth() - @UseGuards(JwtAuthGuard, CheckFourAndSixSceneDeviceTypeGuard) - @Get(':deviceUuid/scenes') - async getScenesBySceneDevice(@Param('deviceUuid') deviceUuid: string) { - return await this.deviceService.getScenesBySceneDevice(deviceUuid); - } } diff --git a/src/device/dtos/control.device.dto.ts b/src/device/dtos/control.device.dto.ts index 7a1e900..1303640 100644 --- a/src/device/dtos/control.device.dto.ts +++ b/src/device/dtos/control.device.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsArray, IsNotEmpty, IsString } from 'class-validator'; +import { IsArray, IsNotEmpty, IsOptional, IsString } from 'class-validator'; export class ControlDeviceDto { @ApiProperty({ @@ -57,9 +57,9 @@ export class BatchFactoryResetDevicesDto { export class GetSceneFourSceneDeviceDto { @ApiProperty({ description: 'switchName', - required: true, + required: false, }) @IsString() - @IsNotEmpty() - public switchName: string; + @IsOptional() + public switchName?: string; } diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index 4d45264..eeca08c 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -1319,42 +1319,43 @@ export class DeviceService { } } - async getSceneBySceneDevice( + async getScenesBySceneDevice( deviceUuid: string, getSceneFourSceneDeviceDto: GetSceneFourSceneDeviceDto, - ) { + ): Promise { try { - const sceneDevice = await this.sceneDeviceRepository.findOne({ - where: { - device: { uuid: deviceUuid }, - switchName: - getSceneFourSceneDeviceDto.switchName as SceneSwitchesTypeEnum, - }, - relations: ['device', 'scene'], - }); - if (sceneDevice.uuid) { - const SceneDetails = await this.sceneService.getSceneByUuid( + if (getSceneFourSceneDeviceDto.switchName) { + // Query for a single record directly when switchName is provided + const sceneDevice = await this.sceneDeviceRepository.findOne({ + where: { + device: { uuid: deviceUuid }, + switchName: + getSceneFourSceneDeviceDto.switchName as SceneSwitchesTypeEnum, + }, + relations: ['device', 'scene'], + }); + + if (!sceneDevice) { + throw new HttpException( + `No scene found for device with UUID ${deviceUuid} and switch name ${getSceneFourSceneDeviceDto.switchName}`, + HttpStatus.NOT_FOUND, + ); + } + + const sceneDetails = await this.sceneService.getSceneByUuid( sceneDevice.scene.uuid, ); + return { switchName: sceneDevice.switchName, createdAt: sceneDevice.createdAt, updatedAt: sceneDevice.updatedAt, deviceUuid: sceneDevice.device.uuid, - scene: { - ...SceneDetails.data, - }, + scene: sceneDetails.data, }; } - } catch (error) { - throw new HttpException( - error.message || 'Scene device not found', - error.status || HttpStatus.NOT_FOUND, - ); - } - } - async getScenesBySceneDevice(deviceUuid: string): Promise { - try { + + // Query for multiple records if switchName is not provided const sceneDevices = await this.sceneDeviceRepository.find({ where: { device: { uuid: deviceUuid } }, relations: ['device', 'scene'], @@ -1362,37 +1363,28 @@ export class DeviceService { if (!sceneDevices.length) { throw new HttpException( - 'No scenes found for the device', + `No scenes found for device with UUID ${deviceUuid}`, HttpStatus.NOT_FOUND, ); } const results = await Promise.all( sceneDevices.map(async (sceneDevice) => { - if (!sceneDevice.scene?.uuid) return null; + const sceneDetails = await this.sceneService.getSceneByUuid( + sceneDevice.scene.uuid, + ); - try { - const sceneDetails = await this.sceneService.getSceneByUuid( - sceneDevice.scene.uuid, - ); - - return { - switchName: sceneDevice.switchName, - createdAt: sceneDevice.createdAt, - updatedAt: sceneDevice.updatedAt, - deviceUuid: sceneDevice.device.uuid, - scene: sceneDetails.data, - }; - } catch (error) { - throw new HttpException( - 'Failed to fetch scene details', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } + return { + switchName: sceneDevice.switchName, + createdAt: sceneDevice.createdAt, + updatedAt: sceneDevice.updatedAt, + deviceUuid: sceneDevice.device.uuid, + scene: sceneDetails.data, + }; }), ); - return results.filter(Boolean); + return results; } catch (error) { throw new HttpException( error.message || 'Failed to fetch scenes for device', From 2ed5d9dce7a05c1517021ef5d5d6b4329f9aee97 Mon Sep 17 00:00:00 2001 From: hannathkadher Date: Wed, 20 Nov 2024 16:16:17 +0400 Subject: [PATCH 11/13] Added space product relation --- libs/common/src/database/database.module.ts | 2 ++ .../product/entities/product.entity.ts | 4 +++ .../space/entities/space-product.entity.ts | 32 +++++++++++++++++++ .../modules/space/entities/space.entity.ts | 4 +++ .../space/repositories/space.repository.ts | 8 +++++ 5 files changed, 50 insertions(+) create mode 100644 libs/common/src/modules/space/entities/space-product.entity.ts diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index 868ef5a..92f3db6 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -23,6 +23,7 @@ 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'; @Module({ imports: [ @@ -49,6 +50,7 @@ import { SceneDeviceEntity } from '../modules/scene-device/entities'; SpaceEntity, SubspaceEntity, SpaceTypeEntity, + SpaceProductEntity, UserSpaceEntity, DeviceUserPermissionEntity, UserRoleEntity, diff --git a/libs/common/src/modules/product/entities/product.entity.ts b/libs/common/src/modules/product/entities/product.entity.ts index 6553dc1..a7cf957 100644 --- a/libs/common/src/modules/product/entities/product.entity.ts +++ b/libs/common/src/modules/product/entities/product.entity.ts @@ -2,6 +2,7 @@ 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'; @Entity({ name: 'product' }) export class ProductEntity extends AbstractEntity { @@ -21,6 +22,9 @@ export class ProductEntity extends AbstractEntity { }) public prodType: string; + @OneToMany(() => SpaceProductEntity, (spaceProduct) => spaceProduct.product) + spaceProducts: SpaceProductEntity[]; + @OneToMany( () => DeviceEntity, (devicesProductEntity) => devicesProductEntity.productDevice, diff --git a/libs/common/src/modules/space/entities/space-product.entity.ts b/libs/common/src/modules/space/entities/space-product.entity.ts new file mode 100644 index 0000000..3dd6cbe --- /dev/null +++ b/libs/common/src/modules/space/entities/space-product.entity.ts @@ -0,0 +1,32 @@ +import { Column, Entity, ManyToOne, JoinColumn } from 'typeorm'; +import { SpaceEntity } from './space.entity'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { ProductEntity } from '../../product/entities'; + +@Entity({ name: 'space-product' }) +export class SpaceProductEntity extends AbstractEntity { + @ManyToOne(() => SpaceEntity, (space) => space.spaceProducts, { + nullable: false, + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'space_id' }) + space: SpaceEntity; + + @ManyToOne(() => ProductEntity, (product) => product.spaceProducts, { + nullable: false, + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'product_id' }) + product: ProductEntity; + + @Column({ + nullable: false, + type: 'int', + }) + productCount: number; + + 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 196a522..837b875 100644 --- a/libs/common/src/modules/space/entities/space.entity.ts +++ b/libs/common/src/modules/space/entities/space.entity.ts @@ -12,6 +12,7 @@ import { UserSpaceEntity } from '../../user/entities'; import { DeviceEntity } from '../../device/entities'; import { CommunityEntity } from '../../community/entities'; import { SubspaceEntity } from './subspace.entity'; +import { SpaceProductEntity } from './space-product.entity'; @Entity({ name: 'space-type' }) export class SpaceTypeEntity extends AbstractEntity { @@ -83,6 +84,9 @@ export class SpaceEntity extends AbstractEntity { ) devices: DeviceEntity[]; + @OneToMany(() => SpaceProductEntity, (spaceProduct) => spaceProduct.space) + spaceProducts: SpaceProductEntity[]; + constructor(partial: Partial) { super(); Object.assign(this, partial); diff --git a/libs/common/src/modules/space/repositories/space.repository.ts b/libs/common/src/modules/space/repositories/space.repository.ts index 5e0962c..d34fc24 100644 --- a/libs/common/src/modules/space/repositories/space.repository.ts +++ b/libs/common/src/modules/space/repositories/space.repository.ts @@ -1,6 +1,7 @@ import { DataSource, Repository } from 'typeorm'; import { Injectable } from '@nestjs/common'; import { SpaceEntity, SpaceTypeEntity, SubspaceEntity } from '../entities'; +import { SpaceProductEntity } from '../entities/space-product.entity'; @Injectable() export class SpaceRepository extends Repository { @@ -22,3 +23,10 @@ export class SubspaceRepository extends Repository { super(SubspaceEntity, dataSource.createEntityManager()); } } + +@Injectable() +export class SpaceProductRepository extends Repository { + constructor(private dataSource: DataSource) { + super(SpaceProductEntity, dataSource.createEntityManager()); + } +} From bc1638aba488daf69cff691e477279968cecb042 Mon Sep 17 00:00:00 2001 From: hannathkadher Date: Wed, 20 Nov 2024 16:16:58 +0400 Subject: [PATCH 12/13] changed naming --- .../common/src/modules/space/entities/space-product.entity.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/common/src/modules/space/entities/space-product.entity.ts b/libs/common/src/modules/space/entities/space-product.entity.ts index 3dd6cbe..92ad411 100644 --- a/libs/common/src/modules/space/entities/space-product.entity.ts +++ b/libs/common/src/modules/space/entities/space-product.entity.ts @@ -9,14 +9,14 @@ export class SpaceProductEntity extends AbstractEntity { nullable: false, onDelete: 'CASCADE', }) - @JoinColumn({ name: 'space_id' }) + @JoinColumn({ name: 'space_uuid' }) space: SpaceEntity; @ManyToOne(() => ProductEntity, (product) => product.spaceProducts, { nullable: false, onDelete: 'CASCADE', }) - @JoinColumn({ name: 'product_id' }) + @JoinColumn({ name: 'product_uuid' }) product: ProductEntity; @Column({ From 11564eece780f565876f3eef7bb2bd7852762f26 Mon Sep 17 00:00:00 2001 From: hannathkadher Date: Wed, 20 Nov 2024 16:34:44 +0400 Subject: [PATCH 13/13] added space product service --- src/space/dtos/add.space.dto.ts | 17 ++++ src/space/services/index.ts | 1 + src/space/services/space-products/index.ts | 1 + .../space-products/space-products.service.ts | 95 +++++++++++++++++++ src/space/services/space.service.ts | 11 ++- src/space/space.module.ts | 4 + 6 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 src/space/services/space-products/index.ts create mode 100644 src/space/services/space-products/space-products.service.ts diff --git a/src/space/dtos/add.space.dto.ts b/src/space/dtos/add.space.dto.ts index a33bb34..832287f 100644 --- a/src/space/dtos/add.space.dto.ts +++ b/src/space/dtos/add.space.dto.ts @@ -1,10 +1,13 @@ import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; import { + IsArray, IsBoolean, IsNotEmpty, IsOptional, IsString, IsUUID, + ValidateNested, } from 'class-validator'; export class AddSpaceDto { @@ -32,6 +35,11 @@ export class AddSpaceDto { }) @IsBoolean() isPrivate: boolean; + + @IsArray() + @ValidateNested({ each: true }) + @Type(() => ProductAssignmentDto) + products: ProductAssignmentDto[]; } export class AddUserSpaceDto { @@ -69,7 +77,16 @@ export class AddUserSpaceUsingCodeDto { @IsString() @IsNotEmpty() public inviteCode: string; + constructor(dto: Partial) { Object.assign(this, dto); } } + +class ProductAssignmentDto { + @IsNotEmpty() + productId: string; + + @IsNotEmpty() + count: number; +} diff --git a/src/space/services/index.ts b/src/space/services/index.ts index c4e9405..89f8d9e 100644 --- a/src/space/services/index.ts +++ b/src/space/services/index.ts @@ -3,3 +3,4 @@ export * from './space-user.service'; export * from './space-device.service'; export * from './subspace'; export * from './space-scene.service'; +export * from './space-products'; diff --git a/src/space/services/space-products/index.ts b/src/space/services/space-products/index.ts new file mode 100644 index 0000000..d0b92d2 --- /dev/null +++ b/src/space/services/space-products/index.ts @@ -0,0 +1 @@ +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 new file mode 100644 index 0000000..7c4d634 --- /dev/null +++ b/src/space/services/space-products/space-products.service.ts @@ -0,0 +1,95 @@ +import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; +import { ProductRepository } from '@app/common/modules/product/repositories'; +import { SpaceEntity } from '@app/common/modules/space/entities'; +import { SpaceProductEntity } from '@app/common/modules/space/entities/space-product.entity'; +import { SpaceProductRepository } from '@app/common/modules/space/repositories'; +import { In } from 'typeorm'; + +@Injectable() +export class SpaceProductService { + constructor( + private readonly productRepository: ProductRepository, + private readonly spaceProductRepository: SpaceProductRepository, + ) {} + + async assignProductsToSpace( + space: SpaceEntity, + products: { productId: string; count: number }[], + ): Promise { + try { + const uniqueProducts = this.validateUniqueProducts(products); + const productEntities = await this.getProductEntities(uniqueProducts); + + const spaceProductEntities = uniqueProducts.map( + ({ productId, count }) => { + const product = productEntities.get(productId); + if (!product) { + throw new HttpException( + `Product with ID ${productId} not found`, + HttpStatus.NOT_FOUND, + ); + } + + return this.spaceProductRepository.create({ + space, + product, + productCount: count, + }); + }, + ); + + return await this.spaceProductRepository.save(spaceProductEntities); + } catch (error) { + console.error('Error assigning products to space:', error); + + if (!(error instanceof HttpException)) { + throw new HttpException( + 'An error occurred while assigning products to the space', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + throw error; + } + } + + private validateUniqueProducts( + products: { productId: string; count: number }[], + ): { productId: string; count: number }[] { + 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: { productId: string; count: number }[], + ): Promise> { + try { + const productIds = products.map((p) => p.productId); + + const productEntities = await this.productRepository.find({ + where: { prodId: In(productIds) }, + }); + + return new Map(productEntities.map((p) => [p.prodId, p])); + } catch (error) { + console.error('Error fetching product entities:', error); + throw new HttpException( + 'Failed to fetch product entities', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/space/services/space.service.ts b/src/space/services/space.service.ts index 78d45d7..7340c7c 100644 --- a/src/space/services/space.service.ts +++ b/src/space/services/space.service.ts @@ -11,12 +11,14 @@ import { BaseResponseDto } from '@app/common/dto/base.response.dto'; import { CommunityRepository } from '@app/common/modules/community/repositories'; import { SpaceEntity } from '@app/common/modules/space/entities'; import { generateRandomString } from '@app/common/helper/randomString'; +import { SpaceProductService } from './space-products'; @Injectable() export class SpaceService { constructor( private readonly spaceRepository: SpaceRepository, private readonly communityRepository: CommunityRepository, + private readonly spaceProductService: SpaceProductService, ) {} async createSpace( @@ -25,7 +27,7 @@ export class SpaceService { ): Promise { let parent: SpaceEntity | null = null; - const { parentUuid } = addSpaceDto; + const { parentUuid, products } = addSpaceDto; const community = await this.communityRepository.findOne({ where: { uuid: communityId }, }); @@ -60,6 +62,13 @@ export class SpaceService { await this.spaceRepository.save(newSpace); + if (products && products.length > 0) { + await this.spaceProductService.assignProductsToSpace( + newSpace, + products, + ); + } + return new SuccessResponseDto({ statusCode: HttpStatus.CREATED, data: newSpace, diff --git a/src/space/space.module.ts b/src/space/space.module.ts index c840bd9..030cb95 100644 --- a/src/space/space.module.ts +++ b/src/space/space.module.ts @@ -11,6 +11,7 @@ import { } from './controllers'; import { SpaceDeviceService, + SpaceProductService, SpaceSceneService, SpaceService, SpaceUserService, @@ -18,6 +19,7 @@ import { SubSpaceService, } from './services'; import { + SpaceProductRepository, SpaceRepository, SubspaceRepository, } from '@app/common/modules/space/repositories'; @@ -71,6 +73,8 @@ import { SceneDeviceRepository } from '@app/common/modules/scene-device/reposito DeviceStatusFirebaseService, DeviceStatusLogRepository, SceneDeviceRepository, + SpaceProductService, + SpaceProductRepository, ], exports: [SpaceService], })