From c2958498e5e99e0e37b452f2ef3f73ad011d4fd3 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 22 Oct 2024 02:13:53 -0500 Subject: [PATCH] finished scene icons --- libs/common/src/database/database.module.ts | 3 + libs/common/src/modules/scene/dtos/index.ts | 1 + .../src/modules/scene/dtos/scene.dto.ts | 30 ++++ .../src/modules/scene/entities/index.ts | 1 + .../modules/scene/entities/scene.entity.ts | 57 ++++++++ .../src/modules/scene/repositories/index.ts | 1 + .../scene/repositories/scene.repository.ts | 17 +++ .../modules/scene/scene.repository.module.ts | 11 ++ src/scene/controllers/scene.controller.ts | 38 ++++- src/scene/dtos/scene.dto.ts | 61 +++++++- src/scene/scene.module.ts | 6 + src/scene/services/scene.service.ts | 130 ++++++++++++++---- 12 files changed, 324 insertions(+), 32 deletions(-) create mode 100644 libs/common/src/modules/scene/dtos/index.ts create mode 100644 libs/common/src/modules/scene/dtos/scene.dto.ts create mode 100644 libs/common/src/modules/scene/entities/index.ts create mode 100644 libs/common/src/modules/scene/entities/scene.entity.ts create mode 100644 libs/common/src/modules/scene/repositories/index.ts create mode 100644 libs/common/src/modules/scene/repositories/scene.repository.ts create mode 100644 libs/common/src/modules/scene/scene.repository.module.ts diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index 9de32e9..5d9d7f1 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -20,6 +20,7 @@ import { RegionEntity } from '../modules/region/entities'; import { TimeZoneEntity } from '../modules/timezone/entities'; import { VisitorPasswordEntity } from '../modules/visitor-password/entities'; import { DeviceStatusLogEntity } from '../modules/device-status-log/entities'; +import { SceneEntity, SceneIconEntity } from '../modules/scene/entities'; @Module({ imports: [ @@ -54,6 +55,8 @@ import { DeviceStatusLogEntity } from '../modules/device-status-log/entities'; TimeZoneEntity, VisitorPasswordEntity, DeviceStatusLogEntity, + SceneEntity, + SceneIconEntity, ], namingStrategy: new SnakeNamingStrategy(), synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))), diff --git a/libs/common/src/modules/scene/dtos/index.ts b/libs/common/src/modules/scene/dtos/index.ts new file mode 100644 index 0000000..b1ca9c8 --- /dev/null +++ b/libs/common/src/modules/scene/dtos/index.ts @@ -0,0 +1 @@ +export * from './scene.dto'; diff --git a/libs/common/src/modules/scene/dtos/scene.dto.ts b/libs/common/src/modules/scene/dtos/scene.dto.ts new file mode 100644 index 0000000..c128a6a --- /dev/null +++ b/libs/common/src/modules/scene/dtos/scene.dto.ts @@ -0,0 +1,30 @@ +import { IsBoolean, IsNotEmpty, IsString } from 'class-validator'; + +export class SceneDto { + @IsString() + @IsNotEmpty() + public uuid: string; + + @IsString() + @IsNotEmpty() + public sceneTuyaUuid: string; + @IsString() + @IsNotEmpty() + public unitUuid: string; + @IsString() + @IsNotEmpty() + public iconUuid: string; + + @IsBoolean() + @IsNotEmpty() + public inHomePage: boolean; +} +export class SceneIconDto { + @IsString() + @IsNotEmpty() + public uuid: string; + + @IsString() + @IsNotEmpty() + public icon: string; +} diff --git a/libs/common/src/modules/scene/entities/index.ts b/libs/common/src/modules/scene/entities/index.ts new file mode 100644 index 0000000..b7cd2fb --- /dev/null +++ b/libs/common/src/modules/scene/entities/index.ts @@ -0,0 +1 @@ +export * from './scene.entity'; diff --git a/libs/common/src/modules/scene/entities/scene.entity.ts b/libs/common/src/modules/scene/entities/scene.entity.ts new file mode 100644 index 0000000..d94d172 --- /dev/null +++ b/libs/common/src/modules/scene/entities/scene.entity.ts @@ -0,0 +1,57 @@ +import { Column, Entity, ManyToOne, OneToMany } from 'typeorm'; +import { SceneDto, SceneIconDto } from '../dtos'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; + +// Define SceneIconEntity before SceneEntity +@Entity({ name: 'scene-icon' }) +export class SceneIconEntity extends AbstractEntity { + @Column({ + nullable: true, + type: 'text', + }) + public icon: string; + + @OneToMany( + () => SceneEntity, + (scenesIconEntity) => scenesIconEntity.sceneIcon, + ) + scenesIconEntity: SceneEntity[]; + + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} + +@Entity({ name: 'scene' }) +export class SceneEntity extends AbstractEntity { + @Column({ + type: 'uuid', + default: () => 'gen_random_uuid()', + nullable: false, + }) + public uuid: string; + + @Column({ + nullable: false, + }) + sceneTuyaUuid: string; + @Column({ + nullable: false, + }) + unitUuid: string; + @Column({ + nullable: false, + }) + showInHomePage: boolean; + + @ManyToOne(() => SceneIconEntity, (icon) => icon.scenesIconEntity, { + nullable: false, + }) + sceneIcon: SceneIconEntity; + + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} diff --git a/libs/common/src/modules/scene/repositories/index.ts b/libs/common/src/modules/scene/repositories/index.ts new file mode 100644 index 0000000..75c83c9 --- /dev/null +++ b/libs/common/src/modules/scene/repositories/index.ts @@ -0,0 +1 @@ +export * from './scene.repository'; diff --git a/libs/common/src/modules/scene/repositories/scene.repository.ts b/libs/common/src/modules/scene/repositories/scene.repository.ts new file mode 100644 index 0000000..d077592 --- /dev/null +++ b/libs/common/src/modules/scene/repositories/scene.repository.ts @@ -0,0 +1,17 @@ +import { DataSource, Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { SceneEntity, SceneIconEntity } from '../entities'; + +@Injectable() +export class SceneRepository extends Repository { + constructor(private dataSource: DataSource) { + super(SceneEntity, dataSource.createEntityManager()); + } +} + +@Injectable() +export class SceneIconRepository extends Repository { + constructor(private dataSource: DataSource) { + super(SceneIconEntity, dataSource.createEntityManager()); + } +} diff --git a/libs/common/src/modules/scene/scene.repository.module.ts b/libs/common/src/modules/scene/scene.repository.module.ts new file mode 100644 index 0000000..8bda28a --- /dev/null +++ b/libs/common/src/modules/scene/scene.repository.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { SceneEntity, SceneIconEntity } from './entities'; + +@Module({ + providers: [], + exports: [], + controllers: [], + imports: [TypeOrmModule.forFeature([SceneEntity, SceneIconEntity])], +}) +export class SceneRepositoryModule {} diff --git a/src/scene/controllers/scene.controller.ts b/src/scene/controllers/scene.controller.ts index 1186433..4e986e2 100644 --- a/src/scene/controllers/scene.controller.ts +++ b/src/scene/controllers/scene.controller.ts @@ -8,10 +8,16 @@ import { Param, Post, Put, + Query, UseGuards, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; -import { AddSceneTapToRunDto, UpdateSceneTapToRunDto } from '../dtos/scene.dto'; +import { + AddSceneIconDto, + AddSceneTapToRunDto, + GetSceneDto, + UpdateSceneTapToRunDto, +} from '../dtos/scene.dto'; import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; @@ -39,9 +45,14 @@ export class SceneController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get('tap-to-run/:unitUuid') - async getTapToRunSceneByUnit(@Param('unitUuid') unitUuid: string) { - const tapToRunScenes = - await this.sceneService.getTapToRunSceneByUnit(unitUuid); + async getTapToRunSceneByUnit( + @Param('unitUuid') unitUuid: string, + @Query() inHomePage: GetSceneDto, + ) { + const tapToRunScenes = await this.sceneService.getTapToRunSceneByUnit( + unitUuid, + inHomePage, + ); return tapToRunScenes; } @ApiBearerAuth() @@ -95,4 +106,23 @@ export class SceneController { data: tapToRunScene, }; } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Post('icon') + async addSceneIcon(@Body() addSceneIconDto: AddSceneIconDto) { + const tapToRunScene = await this.sceneService.addSceneIcon(addSceneIconDto); + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'Icon added successfully', + data: tapToRunScene, + }; + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('icon') + async getAllIcons() { + const icons = await this.sceneService.getAllIcons(); + return icons; + } } diff --git a/src/scene/dtos/scene.dto.ts b/src/scene/dtos/scene.dto.ts index ca39212..62690bd 100644 --- a/src/scene/dtos/scene.dto.ts +++ b/src/scene/dtos/scene.dto.ts @@ -6,8 +6,10 @@ import { ValidateNested, IsOptional, IsNumber, + IsBoolean, } from 'class-validator'; -import { Type } from 'class-transformer'; +import { Transform, Type } from 'class-transformer'; +import { BooleanValues } from '@app/common/constants/boolean-values.enum'; class ExecutorProperty { @ApiProperty({ @@ -70,7 +72,20 @@ export class AddSceneTapToRunDto { @IsString() @IsNotEmpty() public unitUuid: string; - + @ApiProperty({ + description: 'Icon UUID', + required: true, + }) + @IsString() + @IsNotEmpty() + public iconUuid: string; + @ApiProperty({ + description: 'show in home page', + required: true, + }) + @IsBoolean() + @IsNotEmpty() + public showInHomePage: boolean; @ApiProperty({ description: 'Scene name', required: true, @@ -109,7 +124,20 @@ export class UpdateSceneTapToRunDto { @IsString() @IsNotEmpty() public sceneName: string; - + @ApiProperty({ + description: 'Icon UUID', + required: true, + }) + @IsString() + @IsNotEmpty() + public iconUuid: string; + @ApiProperty({ + description: 'show in home page', + required: true, + }) + @IsBoolean() + @IsNotEmpty() + public showInHomePage: boolean; @ApiProperty({ description: 'Decision expression', required: true, @@ -132,3 +160,30 @@ export class UpdateSceneTapToRunDto { Object.assign(this, dto); } } +export class AddSceneIconDto { + @ApiProperty({ + description: 'Icon', + required: true, + }) + @IsString() + @IsNotEmpty() + public icon: string; + + constructor(dto: Partial) { + Object.assign(this, dto); + } +} +export class GetSceneDto { + @ApiProperty({ + example: true, + description: 'showInHomePage', + required: true, + default: false, + }) + @IsNotEmpty() + @IsBoolean() + @Transform((value) => { + return value.obj.showInHomePage === BooleanValues.TRUE; + }) + public showInHomePage: boolean = false; +} diff --git a/src/scene/scene.module.ts b/src/scene/scene.module.ts index c673748..cdbf2d4 100644 --- a/src/scene/scene.module.ts +++ b/src/scene/scene.module.ts @@ -8,6 +8,10 @@ import { DeviceService } from 'src/device/services'; 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 { + SceneIconRepository, + SceneRepository, +} from '@app/common/modules/scene/repositories'; @Module({ imports: [ConfigModule, SpaceRepositoryModule, DeviceStatusFirebaseModule], @@ -18,6 +22,8 @@ import { DeviceStatusFirebaseModule } from '@app/common/firebase/devices-status/ DeviceService, DeviceRepository, ProductRepository, + SceneIconRepository, + SceneRepository, ], exports: [SceneService], }) diff --git a/src/scene/services/scene.service.ts b/src/scene/services/scene.service.ts index 30e53aa..ba5436b 100644 --- a/src/scene/services/scene.service.ts +++ b/src/scene/services/scene.service.ts @@ -5,7 +5,12 @@ import { BadRequestException, } from '@nestjs/common'; import { SpaceRepository } from '@app/common/modules/space/repositories'; -import { AddSceneTapToRunDto, UpdateSceneTapToRunDto } from '../dtos'; +import { + AddSceneIconDto, + AddSceneTapToRunDto, + GetSceneDto, + UpdateSceneTapToRunDto, +} from '../dtos'; import { GetUnitByUuidInterface } from 'src/unit/interface/unit.interface'; import { ConfigService } from '@nestjs/config'; import { TuyaContext } from '@tuya/tuya-connector-nodejs'; @@ -14,12 +19,15 @@ import { DeviceService } from 'src/device/services'; import { AddTapToRunSceneInterface, DeleteTapToRunSceneInterface, - GetTapToRunSceneByUnitInterface, SceneDetailsResult, } from '../interface/scene.interface'; import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; import { SpaceType } from '@app/common/constants/space-type.enum'; import { ActionExecutorEnum } from '@app/common/constants/automation.enum'; +import { + SceneIconRepository, + SceneRepository, +} from '@app/common/modules/scene/repositories'; @Injectable() export class SceneService { @@ -27,6 +35,8 @@ export class SceneService { constructor( private readonly configService: ConfigService, private readonly spaceRepository: SpaceRepository, + private readonly sceneIconRepository: SceneIconRepository, + private readonly sceneRepository: SceneRepository, private readonly deviceService: DeviceService, ) { const accessKey = this.configService.get('auth-config.ACCESS_KEY'); @@ -42,6 +52,7 @@ export class SceneService { async addTapToRunScene( addSceneTapToRunDto: AddSceneTapToRunDto, spaceTuyaId = null, + unitUuid?: string, ) { try { let unitSpaceTuyaId; @@ -89,6 +100,15 @@ export class SceneService { }); if (!response.success) { throw new HttpException(response.msg, HttpStatus.BAD_REQUEST); + } else { + await this.sceneRepository.save({ + sceneTuyaUuid: response.result.id, + sceneIcon: { + uuid: addSceneTapToRunDto.iconUuid, + }, + showInHomePage: addSceneTapToRunDto.showInHomePage, + unitUuid: unitUuid ? unitUuid : addSceneTapToRunDto.unitUuid, + }); } return { id: response.result.id, @@ -134,33 +154,32 @@ export class SceneService { } } } - async getTapToRunSceneByUnit(unitUuid: string) { + async getTapToRunSceneByUnit(unitUuid: string, inHomePage: GetSceneDto) { try { - const unit = await this.getUnitByUuid(unitUuid); - if (!unit.spaceTuyaUuid) { - throw new BadRequestException('Invalid unit UUID'); - } + const showInHomePage = inHomePage?.showInHomePage; - const path = `/v2.0/cloud/scene/rule?space_id=${unit.spaceTuyaUuid}&type=scene`; - const response: GetTapToRunSceneByUnitInterface = await this.tuya.request( - { - method: 'GET', - path, + const scenesData = await this.sceneRepository.find({ + where: { + unitUuid, + ...(showInHomePage ? { showInHomePage } : {}), }, + }); + + const scenes = await Promise.all( + scenesData.map(async (scene) => { + const sceneData = await this.getTapToRunSceneDetails( + scene.sceneTuyaUuid, + false, + ); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { actions, ...rest } = sceneData; + return { + rest, + }; + }), ); - if (!response.success) { - throw new HttpException(response.msg, HttpStatus.BAD_REQUEST); - } - - return response.result.list.map((item) => { - return { - id: item.id, - name: item.name, - status: item.status, - type: 'tap_to_run', - }; - }); + return scenes; } catch (err) { if (err instanceof BadRequestException) { throw err; // Re-throw BadRequestException @@ -197,6 +216,8 @@ export class SceneService { if (!response.success) { throw new HttpException('Scene not found', HttpStatus.NOT_FOUND); + } else { + await this.sceneRepository.delete({ sceneTuyaUuid: sceneId }); } return response; @@ -273,11 +294,17 @@ export class SceneService { } } } - + const scene = await this.sceneRepository.findOne({ + where: { sceneTuyaUuid: sceneId }, + relations: ['sceneIcon'], + }); return { id: responseData.id, name: responseData.name, status: responseData.status, + icon: scene.sceneIcon?.icon, + iconUuid: scene.sceneIcon?.uuid, + showInHome: scene.showInHomePage, type: 'tap_to_run', actions: actions, ...(withSpaceId && { spaceId: responseData.spaceId }), @@ -334,13 +361,32 @@ export class SceneService { const addSceneTapToRunDto: AddSceneTapToRunDto = { ...updateSceneTapToRunDto, unitUuid: null, + iconUuid: updateSceneTapToRunDto.iconUuid, + showInHomePage: updateSceneTapToRunDto.showInHomePage, }; + const scene = await this.sceneRepository.findOne({ + where: { sceneTuyaUuid: sceneId }, + }); + const newTapToRunScene = await this.addTapToRunScene( addSceneTapToRunDto, spaceTuyaId.spaceId, + scene.unitUuid, ); if (newTapToRunScene.id) { await this.deleteTapToRunScene(null, sceneId, spaceTuyaId.spaceId); + + await this.sceneRepository.update( + { sceneTuyaUuid: sceneId }, + { + sceneTuyaUuid: newTapToRunScene.id, + showInHomePage: addSceneTapToRunDto.showInHomePage, + sceneIcon: { + uuid: addSceneTapToRunDto.iconUuid, + }, + unitUuid: scene.unitUuid, + }, + ); return newTapToRunScene; } } catch (err) { @@ -354,4 +400,38 @@ export class SceneService { } } } + async addSceneIcon(addSceneIconDto: AddSceneIconDto) { + try { + const icon = await this.sceneIconRepository.save({ + icon: addSceneIconDto.icon, + }); + return icon; + } catch (err) { + throw new HttpException( + { + status: HttpStatus.INTERNAL_SERVER_ERROR, + error: 'Failed to add scene icon', + message: err.message || 'Unexpected error occurred', + }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async getAllIcons() { + try { + const icons = await this.sceneIconRepository.find(); + return icons; + } catch (err) { + // Improved error handling + throw new HttpException( + { + statusCode: HttpStatus.INTERNAL_SERVER_ERROR, + message: 'Failed to fetch icons', + error: + err.message || 'An unexpected error occurred while fetching icons', + }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } }