From 799fcb6fb9ef677b53c26582d48cebef061f2026 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 13 May 2025 03:06:43 +0300 Subject: [PATCH] feat: add DEVICE_SPACE_COMMUNITY route and controller for device retrieval by space or community --- libs/common/src/constants/controller-route.ts | 9 ++ src/automation/automation.module.ts | 2 + .../device-space-community.controller.ts | 52 +++++++ src/device/device.module.ts | 9 +- src/device/dtos/get.device.dto.ts | 25 +++- src/device/services/device.service.ts | 141 ++++++++++++++++++ src/door-lock/door.lock.module.ts | 2 + src/group/group.module.ts | 2 + src/scene/scene.module.ts | 2 + .../visitor-password.module.ts | 2 + 10 files changed, 244 insertions(+), 2 deletions(-) create mode 100644 src/device/controllers/device-space-community.controller.ts diff --git a/libs/common/src/constants/controller-route.ts b/libs/common/src/constants/controller-route.ts index 8fdffbe..56c9bb4 100644 --- a/libs/common/src/constants/controller-route.ts +++ b/libs/common/src/constants/controller-route.ts @@ -624,6 +624,15 @@ export class ControllerRoute { 'This endpoint retrieves all devices in the system.'; }; }; + static DEVICE_SPACE_COMMUNITY = class { + public static readonly ROUTE = 'devices/recursive-child'; + static ACTIONS = class { + public static readonly GET_ALL_DEVICES_BY_SPACE_OR_COMMUNITY_WITH_RECURSIVE_CHILD_SUMMARY = + 'Get all devices by space or community with recursive child'; + public static readonly GET_ALL_DEVICES_BY_SPACE_OR_COMMUNITY_WITH_RECURSIVE_CHILD_DESCRIPTION = + 'This endpoint retrieves all devices in the system by space or community with recursive child.'; + }; + }; static DEVICE_PERMISSION = class { public static readonly ROUTE = 'device-permission'; diff --git a/src/automation/automation.module.ts b/src/automation/automation.module.ts index c90d106..d24bdb2 100644 --- a/src/automation/automation.module.ts +++ b/src/automation/automation.module.ts @@ -18,6 +18,7 @@ import { SceneDeviceRepository } from '@app/common/modules/scene-device/reposito import { AutomationRepository } from '@app/common/modules/automation/repositories'; import { ProjectRepository } from '@app/common/modules/project/repositiories/project.repository'; import { AutomationSpaceController } from './controllers/automation-space.controller'; +import { CommunityRepository } from '@app/common/modules/community/repositories'; @Module({ imports: [ConfigModule, SpaceRepositoryModule, DeviceStatusFirebaseModule], @@ -35,6 +36,7 @@ import { AutomationSpaceController } from './controllers/automation-space.contro SceneDeviceRepository, AutomationRepository, ProjectRepository, + CommunityRepository, ], exports: [AutomationService], }) diff --git a/src/device/controllers/device-space-community.controller.ts b/src/device/controllers/device-space-community.controller.ts new file mode 100644 index 0000000..e19f1f5 --- /dev/null +++ b/src/device/controllers/device-space-community.controller.ts @@ -0,0 +1,52 @@ +import { DeviceService } from '../services/device.service'; +import { Controller, Get, Query, UseGuards } from '@nestjs/common'; +import { + ApiTags, + ApiBearerAuth, + ApiOperation, + ApiQuery, +} from '@nestjs/swagger'; +import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; +import { ControllerRoute } from '@app/common/constants/controller-route'; +import { PermissionsGuard } from 'src/guards/permissions.guard'; +import { Permissions } from 'src/decorators/permissions.decorator'; +import { GetDevicesBySpaceOrCommunityDto } from '../dtos'; + +@ApiTags('Device Module') +@Controller({ + version: EnableDisableStatusEnum.ENABLED, + path: ControllerRoute.DEVICE_SPACE_COMMUNITY.ROUTE, +}) +export class DeviceSpaceOrCommunityController { + constructor(private readonly deviceService: DeviceService) {} + + @ApiBearerAuth() + @UseGuards(PermissionsGuard) + @Permissions('DEVICE_VIEW') + @Get() + @ApiOperation({ + summary: + ControllerRoute.DEVICE_SPACE_COMMUNITY.ACTIONS + .GET_ALL_DEVICES_BY_SPACE_OR_COMMUNITY_WITH_RECURSIVE_CHILD_SUMMARY, + description: + ControllerRoute.DEVICE_SPACE_COMMUNITY.ACTIONS + .GET_ALL_DEVICES_BY_SPACE_OR_COMMUNITY_WITH_RECURSIVE_CHILD_DESCRIPTION, + }) + @ApiQuery({ + name: 'spaceUuid', + description: 'UUID of the Space', + required: false, + }) + @ApiQuery({ + name: 'communityUuid', + description: 'UUID of the Community', + required: false, + }) + async getAllDevicesBySpaceOrCommunityWithChild( + @Query() query: GetDevicesBySpaceOrCommunityDto, + ) { + return await this.deviceService.getAllDevicesBySpaceOrCommunityWithChild( + query, + ); + } +} diff --git a/src/device/device.module.ts b/src/device/device.module.ts index fdd3350..6a5a469 100644 --- a/src/device/device.module.ts +++ b/src/device/device.module.ts @@ -22,6 +22,8 @@ import { SceneDeviceRepository } from '@app/common/modules/scene-device/reposito import { AutomationRepository } from '@app/common/modules/automation/repositories'; import { ProjectRepository } from '@app/common/modules/project/repositiories'; import { DeviceProjectController } from './controllers/device-project.controller'; +import { CommunityRepository } from '@app/common/modules/community/repositories'; +import { DeviceSpaceOrCommunityController } from './controllers/device-space-community.controller'; @Module({ imports: [ ConfigModule, @@ -30,7 +32,11 @@ import { DeviceProjectController } from './controllers/device-project.controller DeviceRepositoryModule, DeviceStatusFirebaseModule, ], - controllers: [DeviceController, DeviceProjectController], + controllers: [ + DeviceController, + DeviceProjectController, + DeviceSpaceOrCommunityController, + ], providers: [ DeviceService, ProductRepository, @@ -46,6 +52,7 @@ import { DeviceProjectController } from './controllers/device-project.controller SceneRepository, SceneDeviceRepository, AutomationRepository, + CommunityRepository, ], exports: [DeviceService], }) diff --git a/src/device/dtos/get.device.dto.ts b/src/device/dtos/get.device.dto.ts index 9080062..1ffc109 100644 --- a/src/device/dtos/get.device.dto.ts +++ b/src/device/dtos/get.device.dto.ts @@ -1,6 +1,12 @@ import { DeviceTypeEnum } from '@app/common/constants/device-type.enum'; import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator'; +import { + IsEnum, + IsNotEmpty, + IsOptional, + IsString, + IsUUID, +} from 'class-validator'; export class GetDeviceBySpaceUuidDto { @ApiProperty({ @@ -44,3 +50,20 @@ export class GetDoorLockDevices { @IsOptional() public deviceType: DeviceTypeEnum; } +export class GetDevicesBySpaceOrCommunityDto { + @ApiProperty({ + description: 'Device Product Type', + example: 'PC', + required: true, + }) + @IsString() + @IsNotEmpty() + public deviceType: string; + @IsUUID('4', { message: 'Invalid space UUID format' }) + @IsOptional() + spaceUuid?: string; + + @IsUUID('4', { message: 'Invalid community UUID format' }) + @IsOptional() + communityUuid?: string; +} diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index 59cee2f..1e0a8a0 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -31,6 +31,7 @@ import { import { GetDeviceBySpaceUuidDto, GetDeviceLogsDto, + GetDevicesBySpaceOrCommunityDto, GetDoorLockDevices, } from '../dtos/get.device.dto'; import { @@ -65,6 +66,7 @@ import { ProjectRepository } from '@app/common/modules/project/repositiories'; import { ProjectParam } from '../dtos'; import { BatchDeviceTypeEnum } from '@app/common/constants/batch-device.enum'; import { DeviceTypeEnum } from '@app/common/constants/device-type.enum'; +import { CommunityRepository } from '@app/common/modules/community/repositories'; @Injectable() export class DeviceService { @@ -80,6 +82,7 @@ export class DeviceService { private readonly sceneService: SceneService, private readonly tuyaService: TuyaService, private readonly projectRepository: ProjectRepository, + private readonly communityRepository: CommunityRepository, ) { const accessKey = this.configService.get('auth-config.ACCESS_KEY'); const secretKey = this.configService.get('auth-config.SECRET_KEY'); @@ -1722,4 +1725,142 @@ export class DeviceService { statusCode: HttpStatus.OK, }); } + async getAllDevicesBySpaceOrCommunityWithChild( + query: GetDevicesBySpaceOrCommunityDto, + ): Promise { + try { + const { spaceUuid, communityUuid, deviceType } = query; + if (!spaceUuid && !communityUuid) { + throw new BadRequestException( + 'Either spaceUuid or communityUuid must be provided', + ); + } + + // Get devices based on space or community + const devices = spaceUuid + ? await this.getAllDevicesBySpace(spaceUuid) + : await this.getAllDevicesByCommunity(communityUuid); + + if (!devices?.length) { + return new SuccessResponseDto({ + message: `No devices found for ${spaceUuid ? 'space' : 'community'}`, + data: [], + statusCode: HttpStatus.CREATED, + }); + } + + const devicesFilterd = devices.filter( + (device) => device.productDevice?.prodType === deviceType, + ); + + if (devicesFilterd.length === 0) { + return new SuccessResponseDto({ + message: `No ${deviceType} devices found for ${spaceUuid ? 'space' : 'community'}`, + data: [], + statusCode: HttpStatus.CREATED, + }); + } + + return new SuccessResponseDto({ + message: `Devices fetched successfully`, + data: devicesFilterd, + statusCode: HttpStatus.OK, + }); + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + throw new HttpException( + error.message || 'Internal server error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async getAllDevicesBySpace(spaceUuid: string): Promise { + const space = await this.spaceRepository.findOne({ + where: { uuid: spaceUuid }, + relations: ['children', 'devices', 'devices.productDevice'], + }); + + if (!space) { + throw new NotFoundException('Space not found'); + } + + const allDevices: DeviceEntity[] = [...space.devices]; + + // Recursive fetch function + const fetchChildren = async (parentSpace: SpaceEntity) => { + const children = await this.spaceRepository.find({ + where: { parent: { uuid: parentSpace.uuid } }, + relations: ['children', 'devices', 'devices.productDevice'], + }); + + for (const child of children) { + allDevices.push(...child.devices); + + if (child.children.length > 0) { + await fetchChildren(child); + } + } + }; + + // Start recursive fetch + await fetchChildren(space); + + return allDevices; + } + async getAllDevicesByCommunity( + communityUuid: string, + ): Promise { + // Fetch the community and its top-level spaces + const community = await this.communityRepository.findOne({ + where: { uuid: communityUuid }, + relations: [ + 'spaces', + 'spaces.children', + 'spaces.devices', + 'spaces.devices.productDevice', + ], + }); + + if (!community) { + throw new NotFoundException('Community not found'); + } + + const allDevices: DeviceEntity[] = []; + + // Recursive fetch function for spaces + const fetchSpaceDevices = async (space: SpaceEntity) => { + if (space.devices && space.devices.length > 0) { + allDevices.push(...space.devices); + } + + if (space.children && space.children.length > 0) { + for (const childSpace of space.children) { + const fullChildSpace = await this.spaceRepository.findOne({ + where: { uuid: childSpace.uuid }, + relations: ['children', 'devices', 'devices.productDevice'], + }); + + if (fullChildSpace) { + await fetchSpaceDevices(fullChildSpace); + } + } + } + }; + + // Start recursive fetch for all top-level spaces + for (const space of community.spaces) { + const fullSpace = await this.spaceRepository.findOne({ + where: { uuid: space.uuid }, + relations: ['children', 'devices', 'devices.productDevice'], + }); + + if (fullSpace) { + await fetchSpaceDevices(fullSpace); + } + } + + return allDevices; + } } diff --git a/src/door-lock/door.lock.module.ts b/src/door-lock/door.lock.module.ts index 459a4fb..3f2cee7 100644 --- a/src/door-lock/door.lock.module.ts +++ b/src/door-lock/door.lock.module.ts @@ -28,6 +28,7 @@ import { } from '@app/common/modules/power-clamp/repositories'; import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; import { OccupancyService } from '@app/common/helper/services/occupancy.service'; +import { CommunityRepository } from '@app/common/modules/community/repositories'; @Module({ imports: [ConfigModule, DeviceRepositoryModule], controllers: [DoorLockController], @@ -54,6 +55,7 @@ import { OccupancyService } from '@app/common/helper/services/occupancy.service' PowerClampMonthlyRepository, SqlLoaderService, OccupancyService, + CommunityRepository, ], exports: [DoorLockService], }) diff --git a/src/group/group.module.ts b/src/group/group.module.ts index 8380793..b50bac9 100644 --- a/src/group/group.module.ts +++ b/src/group/group.module.ts @@ -26,6 +26,7 @@ import { } from '@app/common/modules/power-clamp/repositories'; import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; import { OccupancyService } from '@app/common/helper/services/occupancy.service'; +import { CommunityRepository } from '@app/common/modules/community/repositories'; @Module({ imports: [ConfigModule, DeviceRepositoryModule], controllers: [GroupController], @@ -51,6 +52,7 @@ import { OccupancyService } from '@app/common/helper/services/occupancy.service' PowerClampMonthlyRepository, SqlLoaderService, OccupancyService, + CommunityRepository, ], exports: [GroupService], }) diff --git a/src/scene/scene.module.ts b/src/scene/scene.module.ts index 0fe961b..7243d9c 100644 --- a/src/scene/scene.module.ts +++ b/src/scene/scene.module.ts @@ -16,6 +16,7 @@ import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories'; import { AutomationRepository } from '@app/common/modules/automation/repositories'; import { ProjectRepository } from '@app/common/modules/project/repositiories'; +import { CommunityRepository } from '@app/common/modules/community/repositories'; @Module({ imports: [ConfigModule, SpaceRepositoryModule, DeviceStatusFirebaseModule], @@ -32,6 +33,7 @@ import { ProjectRepository } from '@app/common/modules/project/repositiories'; ProjectRepository, SceneDeviceRepository, AutomationRepository, + CommunityRepository, ], exports: [SceneService], }) diff --git a/src/vistor-password/visitor-password.module.ts b/src/vistor-password/visitor-password.module.ts index 91f9c2e..6924713 100644 --- a/src/vistor-password/visitor-password.module.ts +++ b/src/vistor-password/visitor-password.module.ts @@ -30,6 +30,7 @@ import { } from '@app/common/modules/power-clamp/repositories'; import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; import { OccupancyService } from '@app/common/helper/services/occupancy.service'; +import { CommunityRepository } from '@app/common/modules/community/repositories'; @Module({ imports: [ConfigModule, DeviceRepositoryModule, DoorLockModule], controllers: [VisitorPasswordController], @@ -57,6 +58,7 @@ import { OccupancyService } from '@app/common/helper/services/occupancy.service' PowerClampMonthlyRepository, SqlLoaderService, OccupancyService, + CommunityRepository, ], exports: [VisitorPasswordService], })