diff --git a/src/group/controllers/group.controller.ts b/src/group/controllers/group.controller.ts index be0c7a3..2b6dd82 100644 --- a/src/group/controllers/group.controller.ts +++ b/src/group/controllers/group.controller.ts @@ -5,17 +5,18 @@ import { Get, Post, UseGuards, - Query, Param, Put, Delete, + HttpException, + HttpStatus, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; import { AddGroupDto } from '../dtos/add.group.dto'; -import { GetGroupDto } from '../dtos/get.group.dto'; import { ControlGroupDto } from '../dtos/control.group.dto'; import { RenameGroupDto } from '../dtos/rename.group.dto copy'; +import { CheckProductUuidForAllDevicesGuard } from 'src/guards/device.product.guard'; @ApiTags('Group Module') @Controller({ @@ -25,34 +26,43 @@ import { RenameGroupDto } from '../dtos/rename.group.dto copy'; export class GroupController { constructor(private readonly groupService: GroupService) {} - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Get() - async getGroupsByHomeId(@Query() getGroupsDto: GetGroupDto) { + // @ApiBearerAuth() + // @UseGuards(JwtAuthGuard) + @Get('space/:spaceUuid') + async getGroupsBySpaceUuid(@Param('spaceUuid') spaceUuid: string) { try { - return await this.groupService.getGroupsByHomeId(getGroupsDto); - } catch (err) { - throw new Error(err); + return await this.groupService.getGroupsBySpaceUuid(spaceUuid); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); } } - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Get(':groupId') - async getGroupsByGroupId(@Param('groupId') groupId: number) { + // @ApiBearerAuth() + // @UseGuards(JwtAuthGuard) + @Get(':groupUuid') + async getGroupsByGroupId(@Param('groupUuid') groupUuid: string) { try { - return await this.groupService.getGroupsByGroupId(groupId); - } catch (err) { - throw new Error(err); + return await this.groupService.getGroupsByGroupUuid(groupUuid); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); } } - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + // @ApiBearerAuth() + @UseGuards(CheckProductUuidForAllDevicesGuard) @Post() async addGroup(@Body() addGroupDto: AddGroupDto) { try { return await this.groupService.addGroup(addGroupDto); - } catch (err) { - throw new Error(err); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); } } @@ -67,25 +77,37 @@ export class GroupController { } } - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Put('rename') - async renameGroup(@Body() renameGroupDto: RenameGroupDto) { + // @ApiBearerAuth() + // @UseGuards(JwtAuthGuard) + @Put('rename/:groupUuid') + async renameGroupByUuid( + @Param('groupUuid') groupUuid: string, + @Body() renameGroupDto: RenameGroupDto, + ) { try { - return await this.groupService.renameGroup(renameGroupDto); - } catch (err) { - throw new Error(err); + return await this.groupService.renameGroupByUuid( + groupUuid, + renameGroupDto, + ); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); } } - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Delete(':groupId') - async deleteGroup(@Param('groupId') groupId: number) { + // @ApiBearerAuth() + // @UseGuards(JwtAuthGuard) + @Delete(':groupUuid') + async deleteGroup(@Param('groupUuid') groupUuid: string) { try { - return await this.groupService.deleteGroup(groupId); - } catch (err) { - throw new Error(err); + return await this.groupService.deleteGroup(groupUuid); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); } } } diff --git a/src/group/dtos/add.group.dto.ts b/src/group/dtos/add.group.dto.ts index b91f793..aa2a562 100644 --- a/src/group/dtos/add.group.dto.ts +++ b/src/group/dtos/add.group.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString, IsNumberString } from 'class-validator'; +import { IsNotEmpty, IsString, IsArray } from 'class-validator'; export class AddGroupDto { @ApiProperty({ @@ -11,26 +11,10 @@ export class AddGroupDto { public groupName: string; @ApiProperty({ - description: 'homeId', + description: 'deviceUuids', required: true, }) - @IsNumberString() + @IsArray() @IsNotEmpty() - public homeId: string; - - @ApiProperty({ - description: 'productId', - required: true, - }) - @IsString() - @IsNotEmpty() - public productId: string; - - @ApiProperty({ - description: 'The list of up to 20 device IDs, separated with commas (,)', - required: true, - }) - @IsString() - @IsNotEmpty() - public deviceIds: string; + public deviceUuids: [string]; } diff --git a/src/group/dtos/get.group.dto.ts b/src/group/dtos/get.group.dto.ts deleted file mode 100644 index aad234b..0000000 --- a/src/group/dtos/get.group.dto.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsNumberString } from 'class-validator'; - -export class GetGroupDto { - @ApiProperty({ - description: 'homeId', - required: true, - }) - @IsNumberString() - @IsNotEmpty() - public homeId: string; - - @ApiProperty({ - description: 'pageSize', - required: true, - }) - @IsNumberString() - @IsNotEmpty() - public pageSize: number; - - @ApiProperty({ - description: 'pageNo', - required: true, - }) - @IsNumberString() - @IsNotEmpty() - public pageNo: number; -} diff --git a/src/group/dtos/rename.group.dto copy.ts b/src/group/dtos/rename.group.dto copy.ts index a85f41b..f2b0c00 100644 --- a/src/group/dtos/rename.group.dto copy.ts +++ b/src/group/dtos/rename.group.dto copy.ts @@ -1,15 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString, IsNumberString } from 'class-validator'; +import { IsNotEmpty, IsString } from 'class-validator'; export class RenameGroupDto { - @ApiProperty({ - description: 'groupId', - required: true, - }) - @IsNumberString() - @IsNotEmpty() - public groupId: string; - @ApiProperty({ description: 'groupName', required: true, diff --git a/src/group/group.module.ts b/src/group/group.module.ts index 3969d39..61d95ab 100644 --- a/src/group/group.module.ts +++ b/src/group/group.module.ts @@ -2,10 +2,26 @@ import { Module } from '@nestjs/common'; import { GroupService } from './services/group.service'; import { GroupController } from './controllers/group.controller'; import { ConfigModule } from '@nestjs/config'; +import { GroupRepositoryModule } from '@app/common/modules/group/group.repository.module'; +import { GroupRepository } from '@app/common/modules/group/repositories'; +import { GroupDeviceRepositoryModule } from '@app/common/modules/group-device/group.device.repository.module'; +import { GroupDeviceRepository } from '@app/common/modules/group-device/repositories'; +import { DeviceRepositoryModule } from '@app/common/modules/device'; +import { DeviceRepository } from '@app/common/modules/device/repositories'; @Module({ - imports: [ConfigModule], + imports: [ + ConfigModule, + GroupRepositoryModule, + GroupDeviceRepositoryModule, + DeviceRepositoryModule, + ], controllers: [GroupController], - providers: [GroupService], + providers: [ + GroupService, + GroupRepository, + GroupDeviceRepository, + DeviceRepository, + ], exports: [GroupService], }) export class GroupModule {} diff --git a/src/group/interfaces/get.group.interface.ts b/src/group/interfaces/get.group.interface.ts index 970c343..525fa04 100644 --- a/src/group/interfaces/get.group.interface.ts +++ b/src/group/interfaces/get.group.interface.ts @@ -1,25 +1,15 @@ -export class GetGroupDetailsInterface { - result: { - id: string; - name: string; - }; +export interface GetGroupDetailsInterface { + groupUuid: string; + groupName: string; + createdAt: Date; + updatedAt: Date; } -export class GetGroupsInterface { - result: { - count: number; - data_list: []; - }; +export interface GetGroupsBySpaceUuidInterface { + groupUuid: string; + groupName: string; } -export class addGroupInterface { - success: boolean; - msg: string; - result: { - id: string; - }; -} - -export class controlGroupInterface { +export interface controlGroupInterface { success: boolean; result: boolean; msg: string; diff --git a/src/group/services/group.service.ts b/src/group/services/group.service.ts index 07fbacf..f559737 100644 --- a/src/group/services/group.service.ts +++ b/src/group/services/group.service.ts @@ -1,21 +1,30 @@ -import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; +import { + Injectable, + HttpException, + HttpStatus, + BadRequestException, +} from '@nestjs/common'; import { TuyaContext } from '@tuya/tuya-connector-nodejs'; import { ConfigService } from '@nestjs/config'; import { AddGroupDto } from '../dtos/add.group.dto'; import { GetGroupDetailsInterface, - GetGroupsInterface, - addGroupInterface, + GetGroupsBySpaceUuidInterface, controlGroupInterface, } from '../interfaces/get.group.interface'; -import { GetGroupDto } from '../dtos/get.group.dto'; import { ControlGroupDto } from '../dtos/control.group.dto'; import { RenameGroupDto } from '../dtos/rename.group.dto copy'; +import { GroupRepository } from '@app/common/modules/group/repositories'; +import { GroupDeviceRepository } from '@app/common/modules/group-device/repositories'; @Injectable() export class GroupService { private tuya: TuyaContext; - constructor(private readonly configService: ConfigService) { + constructor( + private readonly configService: ConfigService, + private readonly groupRepository: GroupRepository, + private readonly groupDeviceRepository: GroupDeviceRepository, + ) { const accessKey = this.configService.get('auth-config.ACCESS_KEY'); const secretKey = this.configService.get('auth-config.SECRET_KEY'); // const clientId = this.configService.get('auth-config.CLIENT_ID'); @@ -26,83 +35,80 @@ export class GroupService { }); } - async getGroupsByHomeId(getGroupDto: GetGroupDto) { + async getGroupsBySpaceUuid( + spaceUuid: string, + ): Promise { try { - const response = await this.getGroupsTuya(getGroupDto); - - const groups = response.result.data_list.map((group: any) => ({ - groupId: group.id, - groupName: group.name, - })); - - return { - count: response.result.count, - groups: groups, - }; - } catch (error) { - throw new HttpException( - 'Error fetching groups', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - - async getGroupsTuya(getGroupDto: GetGroupDto): Promise { - try { - const path = `/v2.0/cloud/thing/group`; - const response = await this.tuya.request({ - method: 'GET', - path, - query: { - space_id: getGroupDto.homeId, - page_size: getGroupDto.pageSize, - page_no: getGroupDto.pageNo, + const groupDevices = await this.groupDeviceRepository.find({ + relations: ['group', 'device'], + where: { + device: { spaceUuid }, + isActive: true, }, }); - return response as unknown as GetGroupsInterface; + + // Extract and return only the group entities + const groups = groupDevices.map((groupDevice) => { + return { + groupUuid: groupDevice.uuid, + groupName: groupDevice.group.groupName, + }; + }); + if (groups.length > 0) { + return groups; + } else { + throw new HttpException( + 'this space has no groups', + HttpStatus.NOT_FOUND, + ); + } } catch (error) { throw new HttpException( - 'Error fetching groups ', - HttpStatus.INTERNAL_SERVER_ERROR, + error.message || 'Error fetching groups', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, ); } } async addGroup(addGroupDto: AddGroupDto) { - const response = await this.addGroupTuya(addGroupDto); + try { + const group = await this.groupRepository.save({ + groupName: addGroupDto.groupName, + }); - if (response.success) { - return { - success: true, - groupId: response.result.id, - }; - } else { + const groupDevicePromises = addGroupDto.deviceUuids.map( + async (deviceUuid) => { + await this.saveGroupDevice(group.uuid, deviceUuid); + }, + ); + + await Promise.all(groupDevicePromises); + } catch (err) { + if (err.code === '23505') { + throw new HttpException( + 'User already belongs to this group', + HttpStatus.BAD_REQUEST, + ); + } throw new HttpException( - response.msg || 'Unknown error', - HttpStatus.BAD_REQUEST, + err.message || 'Internal Server Error', + HttpStatus.INTERNAL_SERVER_ERROR, ); } } - async addGroupTuya(addGroupDto: AddGroupDto): Promise { + + private async saveGroupDevice(groupUuid: string, deviceUuid: string) { try { - const path = `/v2.0/cloud/thing/group`; - const response = await this.tuya.request({ - method: 'POST', - path, - body: { - space_id: addGroupDto.homeId, - name: addGroupDto.groupName, - product_id: addGroupDto.productId, - device_ids: addGroupDto.deviceIds, + await this.groupDeviceRepository.save({ + group: { + uuid: groupUuid, + }, + device: { + uuid: deviceUuid, }, }); - - return response as addGroupInterface; } catch (error) { - throw new HttpException( - 'Error adding group', - HttpStatus.INTERNAL_SERVER_ERROR, - ); + throw error; } } @@ -141,105 +147,76 @@ export class GroupService { } } - async renameGroup(renameGroupDto: RenameGroupDto) { - const response = await this.renameGroupTuya(renameGroupDto); - - if (response.success) { - return { - success: response.success, - result: response.result, - msg: response.msg, - }; - } else { - throw new HttpException( - response.msg || 'Unknown error', - HttpStatus.BAD_REQUEST, - ); - } - } - async renameGroupTuya( + async renameGroupByUuid( + groupUuid: string, renameGroupDto: RenameGroupDto, - ): Promise { + ): Promise { try { - const path = `/v2.0/cloud/thing/group/${renameGroupDto.groupId}/${renameGroupDto.groupName}`; - const response = await this.tuya.request({ - method: 'PUT', - path, + await this.groupRepository.update( + { uuid: groupUuid }, + { groupName: renameGroupDto.groupName }, + ); + + // Fetch the updated floor + const updatedGroup = await this.groupRepository.findOneOrFail({ + where: { uuid: groupUuid }, }); - - return response as controlGroupInterface; - } catch (error) { - throw new HttpException( - 'Error rename group', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - - async deleteGroup(groupId: number) { - const response = await this.deleteGroupTuya(groupId); - - if (response.success) { return { - success: response.success, - result: response.result, - msg: response.msg, - }; - } else { - throw new HttpException( - response.msg || 'Unknown error', - HttpStatus.BAD_REQUEST, - ); - } - } - async deleteGroupTuya(groupId: number): Promise { - try { - const path = `/v2.0/cloud/thing/group/${groupId}`; - const response = await this.tuya.request({ - method: 'DELETE', - path, - }); - - return response as controlGroupInterface; - } catch (error) { - throw new HttpException( - 'Error delete group', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - - async getGroupsByGroupId(groupId: number) { - try { - const response = await this.getGroupsByGroupIdTuya(groupId); - - return { - groupId: response.result.id, - groupName: response.result.name, + groupUuid: updatedGroup.uuid, + groupName: updatedGroup.groupName, }; + } catch (error) { + throw new HttpException('Group not found', HttpStatus.NOT_FOUND); + } + } + + async deleteGroup(groupUuid: string) { + try { + const group = await this.getGroupsByGroupUuid(groupUuid); + + if (!group) { + throw new HttpException('Group not found', HttpStatus.NOT_FOUND); + } + + await this.groupRepository.update( + { uuid: groupUuid }, + { isActive: false }, + ); + + return { message: 'Group deleted successfully' }; } catch (error) { throw new HttpException( - 'Error fetching group', - HttpStatus.INTERNAL_SERVER_ERROR, + error.message || 'Error deleting group', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, ); } } - async getGroupsByGroupIdTuya( - groupId: number, + async getGroupsByGroupUuid( + groupUuid: string, ): Promise { try { - const path = `/v2.0/cloud/thing/group/${groupId}`; - const response = await this.tuya.request({ - method: 'GET', - path, + const group = await this.groupRepository.findOne({ + where: { + uuid: groupUuid, + isActive: true, + }, }); - return response as GetGroupDetailsInterface; - } catch (error) { - throw new HttpException( - 'Error fetching group ', - HttpStatus.INTERNAL_SERVER_ERROR, - ); + if (!group) { + throw new BadRequestException('Invalid group UUID'); + } + return { + groupUuid: group.uuid, + groupName: group.groupName, + createdAt: group.createdAt, + updatedAt: group.updatedAt, + }; + } catch (err) { + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException('Group not found', HttpStatus.NOT_FOUND); + } } } } diff --git a/src/guards/device.product.guard.ts b/src/guards/device.product.guard.ts new file mode 100644 index 0000000..2118401 --- /dev/null +++ b/src/guards/device.product.guard.ts @@ -0,0 +1,71 @@ +import { DeviceRepository } from '@app/common/modules/device/repositories'; +import { + Injectable, + CanActivate, + HttpStatus, + BadRequestException, + ExecutionContext, +} from '@nestjs/common'; + +@Injectable() +export class CheckProductUuidForAllDevicesGuard implements CanActivate { + constructor(private readonly deviceRepository: DeviceRepository) {} + + async canActivate(context: ExecutionContext): Promise { + const req = context.switchToHttp().getRequest(); + + try { + const { deviceUuids } = req.body; + console.log(deviceUuids); + + await this.checkAllDevicesHaveSameProductUuid(deviceUuids); + + return true; + } catch (error) { + this.handleGuardError(error, context); + return false; + } + } + + async checkAllDevicesHaveSameProductUuid(deviceUuids: string[]) { + const firstDevice = await this.deviceRepository.findOne({ + where: { uuid: deviceUuids[0] }, + }); + + if (!firstDevice) { + throw new BadRequestException('First device not found'); + } + + const firstProductUuid = firstDevice.productUuid; + + for (let i = 1; i < deviceUuids.length; i++) { + const device = await this.deviceRepository.findOne({ + where: { uuid: deviceUuids[i] }, + }); + + if (!device) { + throw new BadRequestException(`Device ${deviceUuids[i]} not found`); + } + + if (device.productUuid !== firstProductUuid) { + throw new BadRequestException(`Devices have different product UUIDs`); + } + } + } + + private handleGuardError(error: Error, context: ExecutionContext) { + const response = context.switchToHttp().getResponse(); + console.error(error); + + if (error instanceof BadRequestException) { + response + .status(HttpStatus.BAD_REQUEST) + .json({ statusCode: HttpStatus.BAD_REQUEST, message: error.message }); + } else { + response.status(HttpStatus.NOT_FOUND).json({ + statusCode: HttpStatus.NOT_FOUND, + message: 'Device not found', + }); + } + } +}