feat: add DEVICE_SPACE_COMMUNITY route and controller for device retrieval by space or community

This commit is contained in:
faris Aljohari
2025-05-13 03:06:43 +03:00
parent 921770ea79
commit 799fcb6fb9
10 changed files with 244 additions and 2 deletions

View File

@ -624,6 +624,15 @@ export class ControllerRoute {
'This endpoint retrieves all devices in the system.'; '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 { static DEVICE_PERMISSION = class {
public static readonly ROUTE = 'device-permission'; public static readonly ROUTE = 'device-permission';

View File

@ -18,6 +18,7 @@ import { SceneDeviceRepository } from '@app/common/modules/scene-device/reposito
import { AutomationRepository } from '@app/common/modules/automation/repositories'; import { AutomationRepository } from '@app/common/modules/automation/repositories';
import { ProjectRepository } from '@app/common/modules/project/repositiories/project.repository'; import { ProjectRepository } from '@app/common/modules/project/repositiories/project.repository';
import { AutomationSpaceController } from './controllers/automation-space.controller'; import { AutomationSpaceController } from './controllers/automation-space.controller';
import { CommunityRepository } from '@app/common/modules/community/repositories';
@Module({ @Module({
imports: [ConfigModule, SpaceRepositoryModule, DeviceStatusFirebaseModule], imports: [ConfigModule, SpaceRepositoryModule, DeviceStatusFirebaseModule],
@ -35,6 +36,7 @@ import { AutomationSpaceController } from './controllers/automation-space.contro
SceneDeviceRepository, SceneDeviceRepository,
AutomationRepository, AutomationRepository,
ProjectRepository, ProjectRepository,
CommunityRepository,
], ],
exports: [AutomationService], exports: [AutomationService],
}) })

View File

@ -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,
);
}
}

View File

@ -22,6 +22,8 @@ import { SceneDeviceRepository } from '@app/common/modules/scene-device/reposito
import { AutomationRepository } from '@app/common/modules/automation/repositories'; import { AutomationRepository } from '@app/common/modules/automation/repositories';
import { ProjectRepository } from '@app/common/modules/project/repositiories'; import { ProjectRepository } from '@app/common/modules/project/repositiories';
import { DeviceProjectController } from './controllers/device-project.controller'; import { DeviceProjectController } from './controllers/device-project.controller';
import { CommunityRepository } from '@app/common/modules/community/repositories';
import { DeviceSpaceOrCommunityController } from './controllers/device-space-community.controller';
@Module({ @Module({
imports: [ imports: [
ConfigModule, ConfigModule,
@ -30,7 +32,11 @@ import { DeviceProjectController } from './controllers/device-project.controller
DeviceRepositoryModule, DeviceRepositoryModule,
DeviceStatusFirebaseModule, DeviceStatusFirebaseModule,
], ],
controllers: [DeviceController, DeviceProjectController], controllers: [
DeviceController,
DeviceProjectController,
DeviceSpaceOrCommunityController,
],
providers: [ providers: [
DeviceService, DeviceService,
ProductRepository, ProductRepository,
@ -46,6 +52,7 @@ import { DeviceProjectController } from './controllers/device-project.controller
SceneRepository, SceneRepository,
SceneDeviceRepository, SceneDeviceRepository,
AutomationRepository, AutomationRepository,
CommunityRepository,
], ],
exports: [DeviceService], exports: [DeviceService],
}) })

View File

@ -1,6 +1,12 @@
import { DeviceTypeEnum } from '@app/common/constants/device-type.enum'; import { DeviceTypeEnum } from '@app/common/constants/device-type.enum';
import { ApiProperty } from '@nestjs/swagger'; 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 { export class GetDeviceBySpaceUuidDto {
@ApiProperty({ @ApiProperty({
@ -44,3 +50,20 @@ export class GetDoorLockDevices {
@IsOptional() @IsOptional()
public deviceType: DeviceTypeEnum; 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;
}

View File

@ -31,6 +31,7 @@ import {
import { import {
GetDeviceBySpaceUuidDto, GetDeviceBySpaceUuidDto,
GetDeviceLogsDto, GetDeviceLogsDto,
GetDevicesBySpaceOrCommunityDto,
GetDoorLockDevices, GetDoorLockDevices,
} from '../dtos/get.device.dto'; } from '../dtos/get.device.dto';
import { import {
@ -65,6 +66,7 @@ import { ProjectRepository } from '@app/common/modules/project/repositiories';
import { ProjectParam } from '../dtos'; import { ProjectParam } from '../dtos';
import { BatchDeviceTypeEnum } from '@app/common/constants/batch-device.enum'; import { BatchDeviceTypeEnum } from '@app/common/constants/batch-device.enum';
import { DeviceTypeEnum } from '@app/common/constants/device-type.enum'; import { DeviceTypeEnum } from '@app/common/constants/device-type.enum';
import { CommunityRepository } from '@app/common/modules/community/repositories';
@Injectable() @Injectable()
export class DeviceService { export class DeviceService {
@ -80,6 +82,7 @@ export class DeviceService {
private readonly sceneService: SceneService, private readonly sceneService: SceneService,
private readonly tuyaService: TuyaService, private readonly tuyaService: TuyaService,
private readonly projectRepository: ProjectRepository, private readonly projectRepository: ProjectRepository,
private readonly communityRepository: CommunityRepository,
) { ) {
const accessKey = this.configService.get<string>('auth-config.ACCESS_KEY'); const accessKey = this.configService.get<string>('auth-config.ACCESS_KEY');
const secretKey = this.configService.get<string>('auth-config.SECRET_KEY'); const secretKey = this.configService.get<string>('auth-config.SECRET_KEY');
@ -1722,4 +1725,142 @@ export class DeviceService {
statusCode: HttpStatus.OK, statusCode: HttpStatus.OK,
}); });
} }
async getAllDevicesBySpaceOrCommunityWithChild(
query: GetDevicesBySpaceOrCommunityDto,
): Promise<BaseResponseDto> {
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<DeviceEntity[]> {
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<DeviceEntity[]> {
// 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;
}
} }

View File

@ -28,6 +28,7 @@ import {
} from '@app/common/modules/power-clamp/repositories'; } from '@app/common/modules/power-clamp/repositories';
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
import { OccupancyService } from '@app/common/helper/services/occupancy.service'; import { OccupancyService } from '@app/common/helper/services/occupancy.service';
import { CommunityRepository } from '@app/common/modules/community/repositories';
@Module({ @Module({
imports: [ConfigModule, DeviceRepositoryModule], imports: [ConfigModule, DeviceRepositoryModule],
controllers: [DoorLockController], controllers: [DoorLockController],
@ -54,6 +55,7 @@ import { OccupancyService } from '@app/common/helper/services/occupancy.service'
PowerClampMonthlyRepository, PowerClampMonthlyRepository,
SqlLoaderService, SqlLoaderService,
OccupancyService, OccupancyService,
CommunityRepository,
], ],
exports: [DoorLockService], exports: [DoorLockService],
}) })

View File

@ -26,6 +26,7 @@ import {
} from '@app/common/modules/power-clamp/repositories'; } from '@app/common/modules/power-clamp/repositories';
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
import { OccupancyService } from '@app/common/helper/services/occupancy.service'; import { OccupancyService } from '@app/common/helper/services/occupancy.service';
import { CommunityRepository } from '@app/common/modules/community/repositories';
@Module({ @Module({
imports: [ConfigModule, DeviceRepositoryModule], imports: [ConfigModule, DeviceRepositoryModule],
controllers: [GroupController], controllers: [GroupController],
@ -51,6 +52,7 @@ import { OccupancyService } from '@app/common/helper/services/occupancy.service'
PowerClampMonthlyRepository, PowerClampMonthlyRepository,
SqlLoaderService, SqlLoaderService,
OccupancyService, OccupancyService,
CommunityRepository,
], ],
exports: [GroupService], exports: [GroupService],
}) })

View File

@ -16,6 +16,7 @@ import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories'; import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
import { AutomationRepository } from '@app/common/modules/automation/repositories'; import { AutomationRepository } from '@app/common/modules/automation/repositories';
import { ProjectRepository } from '@app/common/modules/project/repositiories'; import { ProjectRepository } from '@app/common/modules/project/repositiories';
import { CommunityRepository } from '@app/common/modules/community/repositories';
@Module({ @Module({
imports: [ConfigModule, SpaceRepositoryModule, DeviceStatusFirebaseModule], imports: [ConfigModule, SpaceRepositoryModule, DeviceStatusFirebaseModule],
@ -32,6 +33,7 @@ import { ProjectRepository } from '@app/common/modules/project/repositiories';
ProjectRepository, ProjectRepository,
SceneDeviceRepository, SceneDeviceRepository,
AutomationRepository, AutomationRepository,
CommunityRepository,
], ],
exports: [SceneService], exports: [SceneService],
}) })

View File

@ -30,6 +30,7 @@ import {
} from '@app/common/modules/power-clamp/repositories'; } from '@app/common/modules/power-clamp/repositories';
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
import { OccupancyService } from '@app/common/helper/services/occupancy.service'; import { OccupancyService } from '@app/common/helper/services/occupancy.service';
import { CommunityRepository } from '@app/common/modules/community/repositories';
@Module({ @Module({
imports: [ConfigModule, DeviceRepositoryModule, DoorLockModule], imports: [ConfigModule, DeviceRepositoryModule, DoorLockModule],
controllers: [VisitorPasswordController], controllers: [VisitorPasswordController],
@ -57,6 +58,7 @@ import { OccupancyService } from '@app/common/helper/services/occupancy.service'
PowerClampMonthlyRepository, PowerClampMonthlyRepository,
SqlLoaderService, SqlLoaderService,
OccupancyService, OccupancyService,
CommunityRepository,
], ],
exports: [VisitorPasswordService], exports: [VisitorPasswordService],
}) })