Merge pull request #135 from SyncrowIOT/feature/space-management

Feature/space-management
This commit is contained in:
faris Aljohari
2024-11-04 00:00:33 -06:00
committed by GitHub
160 changed files with 4092 additions and 3838 deletions

View File

@ -3,14 +3,10 @@ import { ConfigModule } from '@nestjs/config';
import config from './config';
import { AuthenticationModule } from './auth/auth.module';
import { UserModule } from './users/user.module';
import { RoomModule } from './room/room.module';
import { GroupModule } from './group/group.module';
import { DeviceModule } from './device/device.module';
import { UserDevicePermissionModule } from './user-device-permission/user-device-permission.module';
import { CommunityModule } from './community/community.module';
import { BuildingModule } from './building/building.module';
import { FloorModule } from './floor/floor.module';
import { UnitModule } from './unit/unit.module';
import { RoleModule } from './role/role.module';
import { SeederModule } from '@app/common/seed/seeder.module';
import { UserNotificationModule } from './user-notification/user-notification.module';
@ -24,6 +20,7 @@ import { RegionModule } from './region/region.module';
import { TimeZoneModule } from './timezone/timezone.module';
import { VisitorPasswordModule } from './vistor-password/visitor-password.module';
import { ScheduleModule } from './schedule/schedule.module';
import { SpaceModule } from './space/space.module';
@Module({
imports: [
ConfigModule.forRoot({
@ -33,11 +30,9 @@ import { ScheduleModule } from './schedule/schedule.module';
UserModule,
RoleModule,
CommunityModule,
BuildingModule,
FloorModule,
UnitModule,
RoomModule,
RoomModule,
SpaceModule,
GroupModule,
DeviceModule,
DeviceMessagesSubscriptionModule,

View File

@ -8,12 +8,14 @@ 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 { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
@Module({
imports: [ConfigModule, SpaceRepositoryModule, DeviceStatusFirebaseModule],
controllers: [AutomationController],
providers: [
AutomationService,
TuyaService,
SpaceRepository,
DeviceService,
DeviceRepository,

View File

@ -18,6 +18,11 @@ import {
} from '../dtos/automation.dto';
import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard';
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
import {
AutomationParamDto,
DeleteAutomationParamDto,
SpaceParamDto,
} from '../dtos';
@ApiTags('Automation Module')
@Controller({
@ -42,28 +47,29 @@ export class AutomationController {
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get(':unitUuid')
async getAutomationByUnit(@Param('unitUuid') unitUuid: string) {
const automation =
await this.automationService.getAutomationByUnit(unitUuid);
@Get(':spaceUuid')
async getAutomationBySpace(@Param() param: SpaceParamDto) {
const automation = await this.automationService.getAutomationBySpace(
param.spaceUuid,
);
return automation;
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get('details/:automationId')
async getAutomationDetails(@Param('automationId') automationId: string) {
const automation =
await this.automationService.getAutomationDetails(automationId);
@Get('details/:automationUuid')
async getAutomationDetails(@Param() param: AutomationParamDto) {
const automation = await this.automationService.getAutomationDetails(
param.automationUuid,
);
return automation;
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Delete(':unitUuid/:automationId')
async deleteAutomation(
@Param('unitUuid') unitUuid: string,
@Param('automationId') automationId: string,
) {
await this.automationService.deleteAutomation(unitUuid, automationId);
async deleteAutomation(@Param() param: DeleteAutomationParamDto) {
await this.automationService.deleteAutomation(param);
return {
statusCode: HttpStatus.OK,
message: 'Automation Deleted Successfully',
@ -74,11 +80,11 @@ export class AutomationController {
@Put(':automationId')
async updateAutomation(
@Body() updateAutomationDto: UpdateAutomationDto,
@Param('automationId') automationId: string,
@Param() param: AutomationParamDto,
) {
const automation = await this.automationService.updateAutomation(
updateAutomationDto,
automationId,
param.automationUuid,
);
return {
statusCode: HttpStatus.CREATED,
@ -87,16 +93,17 @@ export class AutomationController {
data: automation,
};
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Put('status/:automationId')
async updateAutomationStatus(
@Body() updateAutomationStatusDto: UpdateAutomationStatusDto,
@Param('automationId') automationId: string,
@Param() param: AutomationParamDto,
) {
await this.automationService.updateAutomationStatus(
updateAutomationStatusDto,
automationId,
param.automationUuid,
);
return {
statusCode: HttpStatus.CREATED,

View File

@ -114,10 +114,10 @@ class Action {
}
export class AddAutomationDto {
@ApiProperty({ description: 'Unit ID', required: true })
@ApiProperty({ description: 'Space ID', required: true })
@IsString()
@IsNotEmpty()
public unitUuid: string;
public spaceUuid: string;
@ApiProperty({ description: 'Automation name', required: true })
@IsString()
@ -197,10 +197,10 @@ export class UpdateAutomationDto {
}
}
export class UpdateAutomationStatusDto {
@ApiProperty({ description: 'Unit uuid', required: true })
@ApiProperty({ description: 'Space uuid', required: true })
@IsString()
@IsNotEmpty()
public unitUuid: string;
public spaceUuid: string;
@ApiProperty({ description: 'Is enable', required: true })
@IsBoolean()

View File

@ -0,0 +1,11 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsUUID } from 'class-validator';
export class AutomationParamDto {
@ApiProperty({
description: 'UUID of the automation',
example: 'd290f1ee-6c54-4b01-90e6-d701748f0851',
})
@IsUUID()
automationUuid: string;
}

View File

@ -0,0 +1,18 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsUUID } from 'class-validator';
export class DeleteAutomationParamDto {
@ApiProperty({
description: 'UUID of the Space',
example: 'd290f1ee-6c54-4b01-90e6-d701748f0851',
})
@IsUUID()
spaceUuid: string;
@ApiProperty({
description: 'UUID of the Automation',
example: 'd290f1ee-6c54-4b01-90e6-d701748f0851',
})
@IsUUID()
automationUuid: string;
}

View File

@ -1 +1,4 @@
export * from './automation.dto';
export * from './space.param.dto';
export * from './automation.param.dto';
export * from './delete.automation.param.dto';

View File

@ -0,0 +1,11 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsUUID } from 'class-validator';
export class SpaceParamDto {
@ApiProperty({
description: 'UUID of the space',
example: 'd290f1ee-6c54-4b01-90e6-d701748f0851',
})
@IsUUID()
spaceUuid: string;
}

View File

@ -5,7 +5,7 @@ export interface AddAutomationInterface {
id: string;
};
}
export interface GetAutomationByUnitInterface {
export interface GetAutomationBySpaceInterface {
success: boolean;
msg?: string;
result: {

View File

@ -7,27 +7,29 @@ import {
import { SpaceRepository } from '@app/common/modules/space/repositories';
import {
AddAutomationDto,
DeleteAutomationParamDto,
UpdateAutomationDto,
UpdateAutomationStatusDto,
} from '../dtos';
import { GetUnitByUuidInterface } from 'src/unit/interface/unit.interface';
import { ConfigService } from '@nestjs/config';
import { TuyaContext } from '@tuya/tuya-connector-nodejs';
import { convertKeysToSnakeCase } from '@app/common/helper/snakeCaseConverter';
import { DeviceService } from 'src/device/services';
import {
Action,
AddAutomationInterface,
AutomationDetailsResult,
AutomationResponseData,
Condition,
DeleteAutomationInterface,
GetAutomationByUnitInterface,
GetAutomationBySpaceInterface,
} from '../interface/automation.interface';
import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter';
import { SpaceType } from '@app/common/constants/space-type.enum';
import {
ActionExecutorEnum,
EntityTypeEnum,
} from '@app/common/constants/automation.enum';
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
@Injectable()
export class AutomationService {
@ -36,6 +38,7 @@ export class AutomationService {
private readonly configService: ConfigService,
private readonly spaceRepository: SpaceRepository,
private readonly deviceService: DeviceService,
private readonly tuyaService: TuyaService,
) {
const accessKey = this.configService.get<string>('auth-config.ACCESS_KEY');
const secretKey = this.configService.get<string>('auth-config.SECRET_KEY');
@ -49,75 +52,39 @@ export class AutomationService {
async addAutomation(addAutomationDto: AddAutomationDto, spaceTuyaId = null) {
try {
let unitSpaceTuyaId;
if (!spaceTuyaId) {
const unitDetails = await this.getUnitByUuid(addAutomationDto.unitUuid);
const { automationName, effectiveTime, decisionExpr } = addAutomationDto;
const space = await this.getSpaceByUuid(addAutomationDto.spaceUuid);
unitSpaceTuyaId = unitDetails.spaceTuyaUuid;
if (!unitDetails) {
throw new BadRequestException('Invalid unit UUID');
}
} else {
unitSpaceTuyaId = spaceTuyaId;
}
const actions = addAutomationDto.actions.map((action) =>
convertKeysToSnakeCase(action),
);
const conditions = addAutomationDto.conditions.map((condition) =>
convertKeysToSnakeCase(condition),
const actions = await this.processEntities<Action>(
addAutomationDto.actions,
'actionExecutor',
{ [ActionExecutorEnum.DEVICE_ISSUE]: true },
this.deviceService,
);
for (const action of actions) {
if (action.action_executor === ActionExecutorEnum.DEVICE_ISSUE) {
const device = await this.deviceService.getDeviceByDeviceUuid(
action.entity_id,
false,
);
if (device) {
action.entity_id = device.deviceTuyaUuid;
}
}
}
const conditions = await this.processEntities<Condition>(
addAutomationDto.conditions,
'entityType',
{ [EntityTypeEnum.DEVICE_REPORT]: true },
this.deviceService,
);
for (const condition of conditions) {
if (condition.entity_type === EntityTypeEnum.DEVICE_REPORT) {
const device = await this.deviceService.getDeviceByDeviceUuid(
condition.entity_id,
false,
);
if (device) {
condition.entity_id = device.deviceTuyaUuid;
}
}
}
const response = (await this.tuyaService.createAutomation(
space.spaceTuyaUuid,
automationName,
effectiveTime,
decisionExpr,
conditions,
actions,
)) as AddAutomationInterface;
const path = `/v2.0/cloud/scene/rule`;
const response: AddAutomationInterface = await this.tuya.request({
method: 'POST',
path,
body: {
space_id: unitSpaceTuyaId,
name: addAutomationDto.automationName,
effective_time: {
...addAutomationDto.effectiveTime,
timezone_id: 'Asia/Dubai',
},
type: 'automation',
decision_expr: addAutomationDto.decisionExpr,
conditions: conditions,
actions: actions,
},
});
if (!response.success) {
throw new HttpException(response.msg, HttpStatus.BAD_REQUEST);
}
return {
id: response.result.id,
id: response?.result.id,
};
} catch (err) {
console.log(err);
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
throw err;
} else {
throw new HttpException(
err.message || 'Automation not found',
@ -126,45 +93,42 @@ export class AutomationService {
}
}
}
async getUnitByUuid(unitUuid: string): Promise<GetUnitByUuidInterface> {
async getSpaceByUuid(spaceUuid: string) {
try {
const unit = await this.spaceRepository.findOne({
const space = await this.spaceRepository.findOne({
where: {
uuid: unitUuid,
spaceType: {
type: SpaceType.UNIT,
},
uuid: spaceUuid,
},
relations: ['spaceType'],
relations: ['community'],
});
if (!unit || !unit.spaceType || unit.spaceType.type !== SpaceType.UNIT) {
throw new BadRequestException('Invalid unit UUID');
if (!space) {
throw new BadRequestException(`Invalid space UUID ${spaceUuid}`);
}
return {
uuid: unit.uuid,
createdAt: unit.createdAt,
updatedAt: unit.updatedAt,
name: unit.spaceName,
type: unit.spaceType.type,
spaceTuyaUuid: unit.spaceTuyaUuid,
uuid: space.uuid,
createdAt: space.createdAt,
updatedAt: space.updatedAt,
name: space.spaceName,
spaceTuyaUuid: space.community.externalId,
};
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException('Unit not found', HttpStatus.NOT_FOUND);
throw new HttpException('Space not found', HttpStatus.NOT_FOUND);
}
}
}
async getAutomationByUnit(unitUuid: string) {
async getAutomationBySpace(spaceUuid: string) {
try {
const unit = await this.getUnitByUuid(unitUuid);
if (!unit.spaceTuyaUuid) {
throw new BadRequestException('Invalid unit UUID');
const space = await this.getSpaceByUuid(spaceUuid);
if (!space.spaceTuyaUuid) {
throw new BadRequestException(`Invalid space UUID ${spaceUuid}`);
}
const path = `/v2.0/cloud/scene/rule?space_id=${unit.spaceTuyaUuid}&type=automation`;
const response: GetAutomationByUnitInterface = await this.tuya.request({
const path = `/v2.0/cloud/scene/rule?space_id=${space.spaceTuyaUuid}&type=automation`;
const response: GetAutomationBySpaceInterface = await this.tuya.request({
method: 'GET',
path,
});
@ -192,11 +156,12 @@ export class AutomationService {
}
}
}
async getTapToRunSceneDetailsTuya(
sceneId: string,
sceneUuid: string,
): Promise<AutomationDetailsResult> {
try {
const path = `/v2.0/cloud/scene/rule/${sceneId}`;
const path = `/v2.0/cloud/scene/rule/${sceneUuid}`;
const response = await this.tuya.request({
method: 'GET',
path,
@ -224,9 +189,9 @@ export class AutomationService {
}
}
}
async getAutomationDetails(automationId: string, withSpaceId = false) {
async getAutomationDetails(automationUuid: string, withSpaceId = false) {
try {
const path = `/v2.0/cloud/scene/rule/${automationId}`;
const path = `/v2.0/cloud/scene/rule/${automationUuid}`;
const response = await this.tuya.request({
method: 'GET',
path,
@ -312,24 +277,21 @@ export class AutomationService {
}
}
async deleteAutomation(
unitUuid: string,
automationId: string,
spaceTuyaId = null,
) {
async deleteAutomation(param: DeleteAutomationParamDto, spaceTuyaId = null) {
try {
let unitSpaceTuyaId;
const { automationUuid, spaceUuid } = param;
let tuyaSpaceId;
if (!spaceTuyaId) {
const unitDetails = await this.getUnitByUuid(unitUuid);
unitSpaceTuyaId = unitDetails.spaceTuyaUuid;
if (!unitSpaceTuyaId) {
throw new BadRequestException('Invalid unit UUID');
const space = await this.getSpaceByUuid(spaceUuid);
tuyaSpaceId = space.spaceTuyaUuid;
if (!tuyaSpaceId) {
throw new BadRequestException(`Invalid space UUID ${spaceUuid}`);
}
} else {
unitSpaceTuyaId = spaceTuyaId;
tuyaSpaceId = spaceTuyaId;
}
const path = `/v2.0/cloud/scene/rule?ids=${automationId}&space_id=${unitSpaceTuyaId}`;
const path = `/v2.0/cloud/scene/rule?ids=${automationUuid}&space_id=${tuyaSpaceId}`;
const response: DeleteAutomationInterface = await this.tuya.request({
method: 'DELETE',
path,
@ -354,10 +316,10 @@ export class AutomationService {
async updateAutomation(
updateAutomationDto: UpdateAutomationDto,
automationId: string,
automationUuid: string,
) {
try {
const spaceTuyaId = await this.getAutomationDetails(automationId, true);
const spaceTuyaId = await this.getAutomationDetails(automationUuid, true);
if (!spaceTuyaId.spaceId) {
throw new HttpException(
"Automation doesn't exist",
@ -366,14 +328,18 @@ export class AutomationService {
}
const addAutomation = {
...updateAutomationDto,
unitUuid: null,
spaceUuid: null,
};
const newAutomation = await this.addAutomation(
addAutomation,
spaceTuyaId.spaceId,
);
const params: DeleteAutomationParamDto = {
spaceUuid: spaceTuyaId.spaceId,
automationUuid: automationUuid,
};
if (newAutomation.id) {
await this.deleteAutomation(null, automationId, spaceTuyaId.spaceId);
await this.deleteAutomation(null, params);
return newAutomation;
}
} catch (err) {
@ -389,22 +355,24 @@ export class AutomationService {
}
async updateAutomationStatus(
updateAutomationStatusDto: UpdateAutomationStatusDto,
automationId: string,
automationUuid: string,
) {
try {
const unitDetails = await this.getUnitByUuid(
updateAutomationStatusDto.unitUuid,
const space = await this.getSpaceByUuid(
updateAutomationStatusDto.spaceUuid,
);
if (!unitDetails.spaceTuyaUuid) {
throw new BadRequestException('Invalid unit UUID');
if (!space.spaceTuyaUuid) {
throw new BadRequestException(
`Invalid space UUID ${updateAutomationStatusDto.spaceUuid}`,
);
}
const path = `/v2.0/cloud/scene/rule/state?space_id=${unitDetails.spaceTuyaUuid}`;
const path = `/v2.0/cloud/scene/rule/state?space_id=${space.spaceTuyaUuid}`;
const response: DeleteAutomationInterface = await this.tuya.request({
method: 'PUT',
path,
body: {
ids: automationId,
ids: automationUuid,
is_enable: updateAutomationStatusDto.isEnable,
},
});
@ -425,4 +393,42 @@ export class AutomationService {
}
}
}
async processEntities<T extends Action | Condition>(
entities: T[], // Accepts either Action[] or Condition[]
lookupKey: keyof T, // The key to look up, specific to T
entityTypeOrExecutorMap: {
[key in ActionExecutorEnum | EntityTypeEnum]?: boolean;
},
deviceService: {
getDeviceByDeviceUuid: (
id: string,
flag: boolean,
) => Promise<{ deviceTuyaUuid: string } | null>;
},
): Promise<T[]> {
// Returns the same type as provided in the input
return Promise.all(
entities.map(async (entity) => {
// Convert keys to snake case (assuming a utility function exists)
const processedEntity = convertKeysToSnakeCase(entity) as T;
// Check if entity needs device UUID lookup
const key = processedEntity[lookupKey];
if (
entityTypeOrExecutorMap[key as ActionExecutorEnum | EntityTypeEnum]
) {
const device = await deviceService.getDeviceByDeviceUuid(
processedEntity.entityId,
false,
);
if (device) {
processedEntity.entityId = device.deviceTuyaUuid;
}
}
return processedEntity;
}),
);
}
}

View File

@ -1,24 +0,0 @@
import { Module } from '@nestjs/common';
import { BuildingService } from './services/building.service';
import { BuildingController } from './controllers/building.controller';
import { ConfigModule } from '@nestjs/config';
import { SpaceRepositoryModule } from '@app/common/modules/space/space.repository.module';
import { SpaceRepository } from '@app/common/modules/space/repositories';
import { SpaceTypeRepository } from '@app/common/modules/space/repositories';
import { UserSpaceRepository } from '@app/common/modules/user/repositories';
import { UserRepository } from '@app/common/modules/user/repositories';
import { UserRepositoryModule } from '@app/common/modules/user/user.repository.module';
@Module({
imports: [ConfigModule, SpaceRepositoryModule, UserRepositoryModule],
controllers: [BuildingController],
providers: [
BuildingService,
SpaceRepository,
SpaceTypeRepository,
UserSpaceRepository,
UserRepository,
],
exports: [BuildingService],
})
export class BuildingModule {}

View File

@ -1,106 +0,0 @@
import { BuildingService } from '../services/building.service';
import {
Body,
Controller,
Get,
HttpStatus,
Param,
Post,
Put,
Query,
UseGuards,
} from '@nestjs/common';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import { AddBuildingDto, AddUserBuildingDto } from '../dtos/add.building.dto';
import { GetBuildingChildDto } from '../dtos/get.building.dto';
import { UpdateBuildingNameDto } from '../dtos/update.building.dto';
import { CheckCommunityTypeGuard } from 'src/guards/community.type.guard';
import { CheckUserBuildingGuard } from 'src/guards/user.building.guard';
import { AdminRoleGuard } from 'src/guards/admin.role.guard';
import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard';
import { BuildingPermissionGuard } from 'src/guards/building.permission.guard';
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
import { SpaceType } from '@app/common/constants/space-type.enum';
@ApiTags('Building Module')
@Controller({
version: EnableDisableStatusEnum.ENABLED,
path: SpaceType.BUILDING,
})
export class BuildingController {
constructor(private readonly buildingService: BuildingService) {}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, CheckCommunityTypeGuard)
@Post()
async addBuilding(@Body() addBuildingDto: AddBuildingDto) {
const building = await this.buildingService.addBuilding(addBuildingDto);
return {
statusCode: HttpStatus.CREATED,
success: true,
message: 'Building added successfully',
data: building,
};
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, BuildingPermissionGuard)
@Get(':buildingUuid')
async getBuildingByUuid(@Param('buildingUuid') buildingUuid: string) {
const building = await this.buildingService.getBuildingByUuid(buildingUuid);
return building;
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, BuildingPermissionGuard)
@Get('child/:buildingUuid')
async getBuildingChildByUuid(
@Param('buildingUuid') buildingUuid: string,
@Query() query: GetBuildingChildDto,
) {
const building = await this.buildingService.getBuildingChildByUuid(
buildingUuid,
query,
);
return building;
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, BuildingPermissionGuard)
@Get('parent/:buildingUuid')
async getBuildingParentByUuid(@Param('buildingUuid') buildingUuid: string) {
const building =
await this.buildingService.getBuildingParentByUuid(buildingUuid);
return building;
}
@ApiBearerAuth()
@UseGuards(AdminRoleGuard, CheckUserBuildingGuard)
@Post('user')
async addUserBuilding(@Body() addUserBuildingDto: AddUserBuildingDto) {
await this.buildingService.addUserBuilding(addUserBuildingDto);
return {
statusCode: HttpStatus.CREATED,
success: true,
message: 'user building added successfully',
};
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get('user/:userUuid')
async getBuildingsByUserId(@Param('userUuid') userUuid: string) {
return await this.buildingService.getBuildingsByUserId(userUuid);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, BuildingPermissionGuard)
@Put(':buildingUuid')
async renameBuildingByUuid(
@Param('buildingUuid') buildingUuid: string,
@Body() updateBuildingDto: UpdateBuildingNameDto,
) {
const building = await this.buildingService.renameBuildingByUuid(
buildingUuid,
updateBuildingDto,
);
return building;
}
}

View File

@ -1 +0,0 @@
export * from './building.controller';

View File

@ -1,42 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class AddBuildingDto {
@ApiProperty({
description: 'buildingName',
required: true,
})
@IsString()
@IsNotEmpty()
public buildingName: string;
@ApiProperty({
description: 'communityUuid',
required: true,
})
@IsString()
@IsNotEmpty()
public communityUuid: string;
constructor(dto: Partial<AddBuildingDto>) {
Object.assign(this, dto);
}
}
export class AddUserBuildingDto {
@ApiProperty({
description: 'buildingUuid',
required: true,
})
@IsString()
@IsNotEmpty()
public buildingUuid: string;
@ApiProperty({
description: 'userUuid',
required: true,
})
@IsString()
@IsNotEmpty()
public userUuid: string;
constructor(dto: Partial<AddUserBuildingDto>) {
Object.assign(this, dto);
}
}

View File

@ -1,52 +0,0 @@
import { BooleanValues } from '@app/common/constants/boolean-values.enum';
import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import {
IsBoolean,
IsInt,
IsNotEmpty,
IsOptional,
IsString,
Min,
} from 'class-validator';
export class GetBuildingDto {
@ApiProperty({
description: 'buildingUuid',
required: true,
})
@IsString()
@IsNotEmpty()
public buildingUuid: string;
}
export class GetBuildingChildDto {
@ApiProperty({ example: 1, description: 'Page number', required: true })
@IsInt({ message: 'Page must be a number' })
@Min(1, { message: 'Page must not be less than 1' })
@IsNotEmpty()
public page: number;
@ApiProperty({
example: 10,
description: 'Number of items per page',
required: true,
})
@IsInt({ message: 'Page size must be a number' })
@Min(1, { message: 'Page size must not be less than 1' })
@IsNotEmpty()
public pageSize: number;
@ApiProperty({
example: true,
description: 'Flag to determine whether to fetch full hierarchy',
required: false,
default: false,
})
@IsOptional()
@IsBoolean()
@Transform((value) => {
return value.obj.includeSubSpaces === BooleanValues.TRUE;
})
public includeSubSpaces: boolean = false;
}

View File

@ -1 +0,0 @@
export * from './add.building.dto';

View File

@ -1,16 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class UpdateBuildingNameDto {
@ApiProperty({
description: 'buildingName',
required: true,
})
@IsString()
@IsNotEmpty()
public buildingName: string;
constructor(dto: Partial<UpdateBuildingNameDto>) {
Object.assign(this, dto);
}
}

View File

@ -1,31 +0,0 @@
export interface GetBuildingByUuidInterface {
uuid: string;
createdAt: Date;
updatedAt: Date;
name: string;
type: string;
}
export interface BuildingChildInterface {
uuid: string;
name: string;
type: string;
totalCount?: number;
children?: BuildingChildInterface[];
}
export interface BuildingParentInterface {
uuid: string;
name: string;
type: string;
parent?: BuildingParentInterface;
}
export interface RenameBuildingByUuidInterface {
uuid: string;
name: string;
type: string;
}
export interface GetBuildingByUserUuidInterface {
uuid: string;
name: string;
type: string;
}

View File

@ -1,317 +0,0 @@
import { GetBuildingChildDto } from '../dtos/get.building.dto';
import { SpaceTypeRepository } from '../../../libs/common/src/modules/space/repositories/space.repository';
import {
Injectable,
HttpException,
HttpStatus,
BadRequestException,
} from '@nestjs/common';
import { SpaceRepository } from '@app/common/modules/space/repositories';
import { AddBuildingDto, AddUserBuildingDto } from '../dtos';
import {
BuildingChildInterface,
BuildingParentInterface,
GetBuildingByUserUuidInterface,
GetBuildingByUuidInterface,
RenameBuildingByUuidInterface,
} from '../interface/building.interface';
import { SpaceEntity } from '@app/common/modules/space/entities';
import { UpdateBuildingNameDto } from '../dtos/update.building.dto';
import { UserSpaceRepository } from '@app/common/modules/user/repositories';
import { SpaceType } from '@app/common/constants/space-type.enum';
import { CommonErrorCodes } from '@app/common/constants/error-codes.enum';
@Injectable()
export class BuildingService {
constructor(
private readonly spaceRepository: SpaceRepository,
private readonly spaceTypeRepository: SpaceTypeRepository,
private readonly userSpaceRepository: UserSpaceRepository,
) {}
async addBuilding(addBuildingDto: AddBuildingDto) {
try {
const spaceType = await this.spaceTypeRepository.findOne({
where: {
type: SpaceType.BUILDING,
},
});
if (!spaceType) {
throw new BadRequestException('Invalid building UUID');
}
const building = await this.spaceRepository.save({
spaceName: addBuildingDto.buildingName,
parent: { uuid: addBuildingDto.communityUuid },
spaceType: { uuid: spaceType.uuid },
});
return building;
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException('Building not found', HttpStatus.NOT_FOUND);
}
}
}
async getBuildingByUuid(
buildingUuid: string,
): Promise<GetBuildingByUuidInterface> {
try {
const building = await this.spaceRepository.findOne({
where: {
uuid: buildingUuid,
spaceType: {
type: SpaceType.BUILDING,
},
},
relations: ['spaceType'],
});
if (
!building ||
!building.spaceType ||
building.spaceType.type !== SpaceType.BUILDING
) {
throw new BadRequestException('Invalid building UUID');
}
return {
uuid: building.uuid,
createdAt: building.createdAt,
updatedAt: building.updatedAt,
name: building.spaceName,
type: building.spaceType.type,
};
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException('Building not found', HttpStatus.NOT_FOUND);
}
}
}
async getBuildingChildByUuid(
buildingUuid: string,
getBuildingChildDto: GetBuildingChildDto,
): Promise<BuildingChildInterface> {
try {
const { includeSubSpaces, page, pageSize } = getBuildingChildDto;
const space = await this.spaceRepository.findOneOrFail({
where: { uuid: buildingUuid },
relations: ['children', 'spaceType'],
});
if (
!space ||
!space.spaceType ||
space.spaceType.type !== SpaceType.BUILDING
) {
throw new BadRequestException('Invalid building UUID');
}
const totalCount = await this.spaceRepository.count({
where: { parent: { uuid: space.uuid } },
});
const children = await this.buildHierarchy(
space,
includeSubSpaces,
page,
pageSize,
);
return {
uuid: space.uuid,
name: space.spaceName,
type: space.spaceType.type,
totalCount,
children,
};
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException('Building not found', HttpStatus.NOT_FOUND);
}
}
}
private async buildHierarchy(
space: SpaceEntity,
includeSubSpaces: boolean,
page: number,
pageSize: number,
): Promise<BuildingChildInterface[]> {
const children = await this.spaceRepository.find({
where: { parent: { uuid: space.uuid } },
relations: ['spaceType'],
skip: (page - 1) * pageSize,
take: pageSize,
});
if (!children || children.length === 0 || !includeSubSpaces) {
return children
.filter(
(child) =>
child.spaceType.type !== SpaceType.BUILDING &&
child.spaceType.type !== SpaceType.COMMUNITY,
) // Filter remaining building and community types
.map((child) => ({
uuid: child.uuid,
name: child.spaceName,
type: child.spaceType.type,
}));
}
const childHierarchies = await Promise.all(
children
.filter(
(child) =>
child.spaceType.type !== SpaceType.BUILDING &&
child.spaceType.type !== SpaceType.COMMUNITY,
) // Filter remaining building and community types
.map(async (child) => ({
uuid: child.uuid,
name: child.spaceName,
type: child.spaceType.type,
children: await this.buildHierarchy(child, true, 1, pageSize),
})),
);
return childHierarchies;
}
async getBuildingParentByUuid(
buildingUuid: string,
): Promise<BuildingParentInterface> {
try {
const building = await this.spaceRepository.findOne({
where: {
uuid: buildingUuid,
spaceType: {
type: SpaceType.BUILDING,
},
},
relations: ['spaceType', 'parent', 'parent.spaceType'],
});
if (
!building ||
!building.spaceType ||
building.spaceType.type !== SpaceType.BUILDING
) {
throw new BadRequestException('Invalid building UUID');
}
return {
uuid: building.uuid,
name: building.spaceName,
type: building.spaceType.type,
parent: {
uuid: building.parent.uuid,
name: building.parent.spaceName,
type: building.parent.spaceType.type,
},
};
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException('Building not found', HttpStatus.NOT_FOUND);
}
}
}
async getBuildingsByUserId(
userUuid: string,
): Promise<GetBuildingByUserUuidInterface[]> {
try {
const buildings = await this.userSpaceRepository.find({
relations: ['space', 'space.spaceType'],
where: {
user: { uuid: userUuid },
space: { spaceType: { type: SpaceType.BUILDING } },
},
});
if (buildings.length === 0) {
throw new HttpException(
'this user has no buildings',
HttpStatus.NOT_FOUND,
);
}
const spaces = buildings.map((building) => ({
uuid: building.space.uuid,
name: building.space.spaceName,
type: building.space.spaceType.type,
}));
return spaces;
} catch (err) {
if (err instanceof HttpException) {
throw err;
} else {
throw new HttpException('user not found', HttpStatus.NOT_FOUND);
}
}
}
async addUserBuilding(addUserBuildingDto: AddUserBuildingDto) {
try {
await this.userSpaceRepository.save({
user: { uuid: addUserBuildingDto.userUuid },
space: { uuid: addUserBuildingDto.buildingUuid },
});
} catch (err) {
if (err.code === CommonErrorCodes.DUPLICATE_ENTITY) {
throw new HttpException(
'User already belongs to this building',
HttpStatus.BAD_REQUEST,
);
}
throw new HttpException(
err.message || 'Internal Server Error',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async renameBuildingByUuid(
buildingUuid: string,
updateBuildingNameDto: UpdateBuildingNameDto,
): Promise<RenameBuildingByUuidInterface> {
try {
const building = await this.spaceRepository.findOneOrFail({
where: { uuid: buildingUuid },
relations: ['spaceType'],
});
if (
!building ||
!building.spaceType ||
building.spaceType.type !== SpaceType.BUILDING
) {
throw new BadRequestException('Invalid building UUID');
}
await this.spaceRepository.update(
{ uuid: buildingUuid },
{ spaceName: updateBuildingNameDto.buildingName },
);
// Fetch the updated building
const updatedBuilding = await this.spaceRepository.findOneOrFail({
where: { uuid: buildingUuid },
relations: ['spaceType'],
});
return {
uuid: updatedBuilding.uuid,
name: updatedBuilding.spaceName,
type: updatedBuilding.spaceType.type,
};
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException('Building not found', HttpStatus.NOT_FOUND);
}
}
}
}

View File

@ -1 +0,0 @@
export * from './building.service';

View File

@ -4,11 +4,11 @@ import { CommunityController } from './controllers/community.controller';
import { ConfigModule } from '@nestjs/config';
import { SpaceRepositoryModule } from '@app/common/modules/space/space.repository.module';
import { SpaceRepository } from '@app/common/modules/space/repositories';
import { SpaceTypeRepository } from '@app/common/modules/space/repositories';
import { UserSpaceRepository } from '@app/common/modules/user/repositories';
import { UserRepositoryModule } from '@app/common/modules/user/user.repository.module';
import { UserRepository } from '@app/common/modules/user/repositories';
import { SpacePermissionService } from '@app/common/helper/services';
import { CommunityRepository } from '@app/common/modules/community/repositories';
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
@Module({
imports: [ConfigModule, SpaceRepositoryModule, UserRepositoryModule],
@ -16,9 +16,9 @@ import { SpacePermissionService } from '@app/common/helper/services';
providers: [
CommunityService,
SpaceRepository,
SpaceTypeRepository,
UserSpaceRepository,
UserRepository,
TuyaService,
CommunityRepository,
SpacePermissionService,
],
exports: [CommunityService, SpacePermissionService],

View File

@ -2,31 +2,28 @@ import { CommunityService } from '../services/community.service';
import {
Body,
Controller,
Delete,
Get,
HttpStatus,
Param,
Post,
Put,
Query,
UseGuards,
} from '@nestjs/common';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import {
AddCommunityDto,
AddUserCommunityDto,
} from '../dtos/add.community.dto';
import { GetCommunityChildDto } from '../dtos/get.community.dto';
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
import { AddCommunityDto } from '../dtos/add.community.dto';
import { GetCommunityParams } from '../dtos/get.community.dto';
import { UpdateCommunityNameDto } from '../dtos/update.community.dto';
import { AdminRoleGuard } from 'src/guards/admin.role.guard';
// import { CheckUserCommunityGuard } from 'src/guards/user.community.guard';
import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard';
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
import { SpaceType } from '@app/common/constants/space-type.enum';
// import { CommunityPermissionGuard } from 'src/guards/community.permission.guard';
import { ControllerRoute } from '@app/common/constants/controller-route';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { PaginationRequestGetListDto } from '@app/common/dto/pagination.request.dto';
@ApiTags('Community Module')
@Controller({
version: EnableDisableStatusEnum.ENABLED,
path: SpaceType.COMMUNITY,
version: '1',
path: ControllerRoute.COMMUNITY.ROUTE,
})
export class CommunityController {
constructor(private readonly communityService: CommunityService) {}
@ -34,73 +31,70 @@ export class CommunityController {
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Post()
async addCommunity(@Body() addCommunityDto: AddCommunityDto) {
const community = await this.communityService.addCommunity(addCommunityDto);
return {
statusCode: HttpStatus.CREATED,
success: true,
message: 'Community added successfully',
data: community,
};
@ApiOperation({
summary: ControllerRoute.COMMUNITY.ACTIONS.CREATE_COMMUNITY_SUMMARY,
description: ControllerRoute.COMMUNITY.ACTIONS.CREATE_COMMUNITY_DESCRIPTION,
})
async createCommunity(
@Body() addCommunityDto: AddCommunityDto,
): Promise<BaseResponseDto> {
return await this.communityService.createCommunity(addCommunityDto);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get(':communityUuid')
async getCommunityByUuid(@Param('communityUuid') communityUuid: string) {
const community =
await this.communityService.getCommunityByUuid(communityUuid);
return community;
@ApiOperation({
summary: ControllerRoute.COMMUNITY.ACTIONS.GET_COMMUNITY_BY_ID_SUMMARY,
description:
ControllerRoute.COMMUNITY.ACTIONS.GET_COMMUNITY_BY_ID_DESCRIPTION,
})
@Get('/:communityUuid')
async getCommunityByUuid(
@Param() params: GetCommunityParams,
): Promise<BaseResponseDto> {
return await this.communityService.getCommunityById(params.communityUuid);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@ApiOperation({
summary: ControllerRoute.COMMUNITY.ACTIONS.LIST_COMMUNITY_SUMMARY,
description: ControllerRoute.COMMUNITY.ACTIONS.LIST_COMMUNITY_DESCRIPTION,
})
@Get()
async getCommunities() {
const communities = await this.communityService.getCommunities();
return communities;
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get('child/:communityUuid')
async getCommunityChildByUuid(
@Param('communityUuid') communityUuid: string,
@Query() query: GetCommunityChildDto,
) {
const community = await this.communityService.getCommunityChildByUuid(
communityUuid,
query,
);
return community;
async getCommunities(
@Query() query: PaginationRequestGetListDto,
): Promise<BaseResponseDto> {
return this.communityService.getCommunities(query);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get('user/:userUuid')
async getCommunitiesByUserId(@Param('userUuid') userUuid: string) {
return await this.communityService.getCommunitiesByUserId(userUuid);
}
@ApiBearerAuth()
@UseGuards(AdminRoleGuard)
@Post('user')
async addUserCommunity(@Body() addUserCommunityDto: AddUserCommunityDto) {
await this.communityService.addUserCommunity(addUserCommunityDto);
return {
statusCode: HttpStatus.CREATED,
success: true,
message: 'user community added successfully',
};
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Put(':communityUuid')
async renameCommunityByUuid(
@Param('communityUuid') communityUuid: string,
@ApiOperation({
summary: ControllerRoute.COMMUNITY.ACTIONS.UPDATE_COMMUNITY_SUMMARY,
description: ControllerRoute.COMMUNITY.ACTIONS.UPDATE_COMMUNITY_DESCRIPTION,
})
@Put('/:communityUuid')
async updateCommunity(
@Param() param: GetCommunityParams,
@Body() updateCommunityDto: UpdateCommunityNameDto,
) {
const community = await this.communityService.renameCommunityByUuid(
communityUuid,
return this.communityService.updateCommunity(
param.communityUuid,
updateCommunityDto,
);
return community;
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Delete('/:communityUuid')
@ApiOperation({
summary: ControllerRoute.COMMUNITY.ACTIONS.DELETE_COMMUNITY_SUMMARY,
description: ControllerRoute.COMMUNITY.ACTIONS.DELETE_COMMUNITY_DESCRIPTION,
})
async deleteCommunity(
@Param() param: GetCommunityParams,
): Promise<BaseResponseDto> {
return this.communityService.deleteCommunity(param.communityUuid);
}
}

View File

@ -1,19 +1,30 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
import { IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
export class AddCommunityDto {
@ApiProperty({
description: 'communityName',
description: 'The name of the community',
example: 'Community A',
required: true,
})
@IsString()
@IsNotEmpty()
public communityName: string;
public name: string;
@ApiProperty({
description: 'A description of the community',
example: 'This is a community for developers.',
required: false,
})
@IsString()
@IsOptional()
public description?: string;
constructor(dto: Partial<AddCommunityDto>) {
Object.assign(this, dto);
}
}
export class AddUserCommunityDto {
@ApiProperty({
description: 'communityUuid',

View File

@ -7,6 +7,7 @@ import {
IsNotEmpty,
IsOptional,
IsString,
IsUUID,
Min,
} from 'class-validator';
@ -20,6 +21,18 @@ export class GetCommunityDto {
public communityUuid: string;
}
export class GetCommunityParams {
@ApiProperty({
description: 'Community id of the specific community',
required: true,
name: 'communityUuid',
})
@IsUUID()
@IsString()
@IsNotEmpty()
public communityUuid: string;
}
export class GetCommunityChildDto {
@ApiProperty({ example: 1, description: 'Page number', required: true })
@IsInt({ message: 'Page must be a number' })

View File

@ -3,12 +3,12 @@ import { IsNotEmpty, IsString } from 'class-validator';
export class UpdateCommunityNameDto {
@ApiProperty({
description: 'communityName',
description: 'community name',
required: true,
})
@IsString()
@IsNotEmpty()
public communityName: string;
public name: string;
constructor(dto: Partial<UpdateCommunityNameDto>) {
Object.assign(this, dto);

View File

@ -1,278 +1,172 @@
import { GetCommunityChildDto } from './../dtos/get.community.dto';
import { SpaceTypeRepository } from './../../../libs/common/src/modules/space/repositories/space.repository';
import {
Injectable,
HttpException,
HttpStatus,
BadRequestException,
} from '@nestjs/common';
import { SpaceRepository } from '@app/common/modules/space/repositories';
import { AddCommunityDto, AddUserCommunityDto } from '../dtos';
import {
CommunityChildInterface,
GetCommunitiesInterface,
GetCommunityByUserUuidInterface,
GetCommunityByUuidInterface,
RenameCommunityByUuidInterface,
} from '../interface/community.interface';
import { SpaceEntity } from '@app/common/modules/space/entities';
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
import { AddCommunityDto } from '../dtos';
import { UpdateCommunityNameDto } from '../dtos/update.community.dto';
import { UserSpaceRepository } from '@app/common/modules/user/repositories';
import { SpaceType } from '@app/common/constants/space-type.enum';
import { CommonErrorCodes } from '@app/common/constants/error-codes.enum';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import {
TypeORMCustomModel,
TypeORMCustomModelFindAllQuery,
} from '@app/common/models/typeOrmCustom.model';
import { PageResponse } from '@app/common/dto/pagination.response.dto';
import { CommunityRepository } from '@app/common/modules/community/repositories';
import { CommunityDto } from '@app/common/modules/community/dtos';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
@Injectable()
export class CommunityService {
constructor(
private readonly spaceRepository: SpaceRepository,
private readonly spaceTypeRepository: SpaceTypeRepository,
private readonly userSpaceRepository: UserSpaceRepository,
private readonly communityRepository: CommunityRepository,
private readonly tuyaService: TuyaService,
) {}
async addCommunity(addCommunityDto: AddCommunityDto) {
try {
const spaceType = await this.spaceTypeRepository.findOne({
where: {
type: SpaceType.COMMUNITY,
},
});
async createCommunity(dto: AddCommunityDto): Promise<BaseResponseDto> {
const { name, description } = dto;
const community = await this.spaceRepository.save({
spaceName: addCommunityDto.communityName,
spaceType: { uuid: spaceType.uuid },
});
return community;
} catch (err) {
throw new HttpException(err.message, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
async getCommunityByUuid(
communityUuid: string,
): Promise<GetCommunityByUuidInterface> {
try {
const community = await this.spaceRepository.findOne({
where: {
uuid: communityUuid,
spaceType: {
type: SpaceType.COMMUNITY,
},
},
relations: ['spaceType'],
});
if (
!community ||
!community.spaceType ||
community.spaceType.type !== SpaceType.COMMUNITY
) {
throw new BadRequestException('Invalid community UUID');
}
return {
uuid: community.uuid,
createdAt: community.createdAt,
updatedAt: community.updatedAt,
name: community.spaceName,
type: community.spaceType.type,
};
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException('Community not found', HttpStatus.NOT_FOUND);
}
}
}
async getCommunities(): Promise<GetCommunitiesInterface> {
try {
const community = await this.spaceRepository.find({
where: { spaceType: { type: SpaceType.COMMUNITY } },
relations: ['spaceType'],
});
return community.map((community) => ({
uuid: community.uuid,
createdAt: community.createdAt,
updatedAt: community.updatedAt,
name: community.spaceName,
type: community.spaceType.type,
}));
} catch (err) {
throw new HttpException(err.message, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
async getCommunityChildByUuid(
communityUuid: string,
getCommunityChildDto: GetCommunityChildDto,
): Promise<CommunityChildInterface> {
try {
const { includeSubSpaces, page, pageSize } = getCommunityChildDto;
const space = await this.spaceRepository.findOneOrFail({
where: { uuid: communityUuid },
relations: ['children', 'spaceType'],
});
if (
!space ||
!space.spaceType ||
space.spaceType.type !== SpaceType.COMMUNITY
) {
throw new BadRequestException('Invalid community UUID');
}
const totalCount = await this.spaceRepository.count({
where: { parent: { uuid: space.uuid } },
});
const children = await this.buildHierarchy(
space,
includeSubSpaces,
page,
pageSize,
const existingCommunity = await this.communityRepository.findOneBy({
name,
});
if (existingCommunity) {
throw new HttpException(
`A community with the name '${name}' already exists.`,
HttpStatus.BAD_REQUEST,
);
return {
uuid: space.uuid,
name: space.spaceName,
type: space.spaceType.type,
totalCount,
children,
};
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException('Community not found', HttpStatus.NOT_FOUND);
}
}
}
private async buildHierarchy(
space: SpaceEntity,
includeSubSpaces: boolean,
page: number,
pageSize: number,
): Promise<CommunityChildInterface[]> {
const children = await this.spaceRepository.find({
where: { parent: { uuid: space.uuid } },
relations: ['spaceType'],
skip: (page - 1) * pageSize,
take: pageSize,
// Create the new community entity
const community = this.communityRepository.create({
name: name,
description: description,
});
if (!children || children.length === 0 || !includeSubSpaces) {
return children
.filter((child) => child.spaceType.type !== SpaceType.COMMUNITY) // Filter remaining community type
.map((child) => ({
uuid: child.uuid,
name: child.spaceName,
type: child.spaceType.type,
}));
// Save the community to the database
try {
const externalId = await this.createTuyaSpace(name);
community.externalId = externalId;
await this.communityRepository.save(community);
return new SuccessResponseDto({
statusCode: HttpStatus.CREATED,
success: true,
data: community,
message: 'Community created successfully',
});
} catch (error) {
throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR);
}
const childHierarchies = await Promise.all(
children
.filter((child) => child.spaceType.type !== SpaceType.COMMUNITY) // Filter remaining community type
.map(async (child) => ({
uuid: child.uuid,
name: child.spaceName,
type: child.spaceType.type,
children: await this.buildHierarchy(child, true, 1, pageSize),
})),
);
return childHierarchies;
}
async getCommunitiesByUserId(
userUuid: string,
): Promise<GetCommunityByUserUuidInterface[]> {
async getCommunityById(communityUuid: string): Promise<BaseResponseDto> {
const community = await this.communityRepository.findOneBy({
uuid: communityUuid,
});
// If the community is not found, throw a 404 NotFoundException
if (!community) {
throw new HttpException(
`Community with ID ${communityUuid} not found.`,
HttpStatus.NOT_FOUND,
);
}
// Return a success response
return new SuccessResponseDto({
data: community,
message: 'Community fetched successfully',
});
}
async getCommunities(
pageable: Partial<TypeORMCustomModelFindAllQuery>,
): Promise<BaseResponseDto> {
pageable.modelName = 'community';
const customModel = TypeORMCustomModel(this.communityRepository);
const { baseResponseDto, paginationResponseDto } =
await customModel.findAll(pageable);
return new PageResponse<CommunityDto>(
baseResponseDto,
paginationResponseDto,
);
}
async updateCommunity(
communityUuid: string,
updateCommunityDto: UpdateCommunityNameDto,
): Promise<BaseResponseDto> {
const community = await this.communityRepository.findOne({
where: { uuid: communityUuid },
});
// If the community doesn't exist, throw a 404 error
if (!community) {
throw new HttpException(
`Community with ID ${communityUuid} not found`,
HttpStatus.NOT_FOUND,
);
}
try {
const communities = await this.userSpaceRepository.find({
relations: ['space', 'space.spaceType'],
where: {
user: { uuid: userUuid },
space: { spaceType: { type: SpaceType.COMMUNITY } },
},
const { name } = updateCommunityDto;
community.name = name;
const updatedCommunity = await this.communityRepository.save(community);
return new SuccessResponseDto<CommunityDto>({
message: 'Success update Community',
data: updatedCommunity,
});
if (communities.length === 0) {
throw new HttpException(
'this user has no communities',
HttpStatus.NOT_FOUND,
);
} catch (err) {
// Catch and handle any errors
if (err instanceof HttpException) {
throw err; // If it's an HttpException, rethrow it
} else {
// Throw a generic 404 error if anything else goes wrong
throw new HttpException('Community not found', HttpStatus.NOT_FOUND);
}
const spaces = communities.map((community) => ({
uuid: community.space.uuid,
name: community.space.spaceName,
type: community.space.spaceType.type,
}));
}
}
return spaces;
async deleteCommunity(communityUuid: string): Promise<BaseResponseDto> {
const community = await this.communityRepository.findOne({
where: { uuid: communityUuid },
});
// If the community is not found, throw an error
if (!community) {
throw new HttpException(
`Community with ID ${communityUuid} not found`,
HttpStatus.NOT_FOUND,
);
}
try {
await this.communityRepository.remove(community);
return new SuccessResponseDto({
message: `Community with ID ${communityUuid} has been successfully deleted`,
});
} catch (err) {
if (err instanceof HttpException) {
throw err;
} else {
throw new HttpException('user not found', HttpStatus.NOT_FOUND);
throw new HttpException(
'An error occurred while deleting the community',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
async addUserCommunity(addUserCommunityDto: AddUserCommunityDto) {
private async createTuyaSpace(name: string): Promise<string> {
try {
await this.userSpaceRepository.save({
user: { uuid: addUserCommunityDto.userUuid },
space: { uuid: addUserCommunityDto.communityUuid },
});
} catch (err) {
if (err.code === CommonErrorCodes.DUPLICATE_ENTITY) {
throw new HttpException(
'User already belongs to this community',
HttpStatus.BAD_REQUEST,
);
}
const response = await this.tuyaService.createSpace({ name });
return response;
} catch (error) {
throw new HttpException(
err.message || 'Internal Server Error',
'Failed to create a Tuya space',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async renameCommunityByUuid(
communityUuid: string,
updateCommunityDto: UpdateCommunityNameDto,
): Promise<RenameCommunityByUuidInterface> {
try {
const community = await this.spaceRepository.findOneOrFail({
where: { uuid: communityUuid },
relations: ['spaceType'],
});
if (
!community ||
!community.spaceType ||
community.spaceType.type !== SpaceType.COMMUNITY
) {
throw new BadRequestException('Invalid community UUID');
}
await this.spaceRepository.update(
{ uuid: communityUuid },
{ spaceName: updateCommunityDto.communityName },
);
// Fetch the updated community
const updatedCommunity = await this.spaceRepository.findOneOrFail({
where: { uuid: communityUuid },
relations: ['spaceType'],
});
return {
uuid: updatedCommunity.uuid,
name: updatedCommunity.spaceName,
type: updatedCommunity.spaceType.type,
};
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException('Community not found', HttpStatus.NOT_FOUND);
}
}
}
}

View File

@ -12,11 +12,8 @@ import {
Put,
} from '@nestjs/common';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import { AddDeviceDto, UpdateDeviceInRoomDto } from '../dtos/add.device.dto';
import {
GetDeviceByRoomUuidDto,
GetDeviceLogsDto,
} from '../dtos/get.device.dto';
import { AddDeviceDto, UpdateDeviceInSpaceDto } from '../dtos/add.device.dto';
import { GetDeviceLogsDto } from '../dtos/get.device.dto';
import {
ControlDeviceDto,
BatchControlDevicesDto,
@ -24,13 +21,10 @@ import {
BatchFactoryResetDevicesDto,
} from '../dtos/control.device.dto';
import { CheckRoomGuard } from 'src/guards/room.guard';
import { CheckUserHavePermission } from 'src/guards/user.device.permission.guard';
import { CheckUserHaveControllablePermission } from 'src/guards/user.device.controllable.permission.guard';
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 { SpaceType } from '@app/common/constants/space-type.enum';
@ApiTags('Device Module')
@Controller({
@ -58,33 +52,21 @@ export class DeviceController {
async getDevicesByUser(@Param('userUuid') userUuid: string) {
return await this.deviceService.getDevicesByUser(userUuid);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, CheckRoomGuard)
@Get(SpaceType.ROOM)
async getDevicesByRoomId(
@Query() getDeviceByRoomUuidDto: GetDeviceByRoomUuidDto,
@Req() req: any,
) {
const userUuid = req.user.uuid;
return await this.deviceService.getDevicesByRoomId(
getDeviceByRoomUuidDto,
userUuid,
);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get('unit/:unitUuid')
async getDevicesByUnitId(@Param('unitUuid') unitUuid: string) {
return await this.deviceService.getDevicesByUnitId(unitUuid);
@Get('space/:spaceUuid')
async getDevicesByUnitId(@Param('spaceUuid') spaceUuid: string) {
return await this.deviceService.getDevicesBySpaceUuid(spaceUuid);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, CheckRoomGuard)
@Put('room')
@Put('space')
async updateDeviceInRoom(
@Body() updateDeviceInRoomDto: UpdateDeviceInRoomDto,
@Body() updateDeviceInSpaceDto: UpdateDeviceInSpaceDto,
) {
const device = await this.deviceService.updateDeviceInRoom(
updateDeviceInRoomDto,
const device = await this.deviceService.updateDeviceInSpace(
updateDeviceInSpaceDto,
);
return {
@ -96,7 +78,7 @@ export class DeviceController {
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, CheckUserHavePermission)
@UseGuards(JwtAuthGuard)
@Get(':deviceUuid')
async getDeviceDetailsByDeviceId(
@Param('deviceUuid') deviceUuid: string,
@ -109,7 +91,7 @@ export class DeviceController {
);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, CheckUserHavePermission)
@UseGuards(JwtAuthGuard)
@Get(':deviceUuid/functions')
async getDeviceInstructionByDeviceId(
@Param('deviceUuid') deviceUuid: string,
@ -117,14 +99,14 @@ export class DeviceController {
return await this.deviceService.getDeviceInstructionByDeviceId(deviceUuid);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, CheckUserHavePermission)
@UseGuards(JwtAuthGuard)
@Get(':deviceUuid/functions/status')
async getDevicesInstructionStatus(@Param('deviceUuid') deviceUuid: string) {
return await this.deviceService.getDevicesInstructionStatus(deviceUuid);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, CheckUserHaveControllablePermission)
@UseGuards(JwtAuthGuard)
@Post(':deviceUuid/control')
async controlDevice(
@Body() controlDeviceDto: ControlDeviceDto,
@ -156,6 +138,7 @@ export class DeviceController {
async getAllDevices() {
return await this.deviceService.getAllDevices();
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get('report-logs/:deviceUuid')

View File

@ -18,7 +18,7 @@ export class AddDeviceDto {
@IsNotEmpty()
public userUuid: string;
}
export class UpdateDeviceInRoomDto {
export class UpdateDeviceInSpaceDto {
@ApiProperty({
description: 'deviceUuid',
required: true,
@ -28,10 +28,10 @@ export class UpdateDeviceInRoomDto {
public deviceUuid: string;
@ApiProperty({
description: 'roomUuid',
description: 'spaceUuid',
required: true,
})
@IsString()
@IsNotEmpty()
public roomUuid: string;
public spaceUuid: string;
}

View File

@ -1,14 +1,14 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
export class GetDeviceByRoomUuidDto {
export class GetDeviceBySpaceUuidDto {
@ApiProperty({
description: 'roomUuid',
description: 'spaceUuid',
required: true,
})
@IsString()
@IsNotEmpty()
public roomUuid: string;
public spaceUuid: string;
}
export class GetDeviceLogsDto {
@ApiProperty({

View File

@ -1,6 +1,6 @@
export interface GetDeviceDetailsInterface {
activeTime: number;
assetId: string;
assetId?: string;
category: string;
categoryName: string;
createTime: number;
@ -13,6 +13,7 @@ export interface GetDeviceDetailsInterface {
lon: string;
model: string;
name: string;
battery?: number;
nodeId: string;
online: boolean;
productId?: string;
@ -23,6 +24,18 @@ export interface GetDeviceDetailsInterface {
uuid: string;
productType: string;
productUuid: string;
spaces?: SpaceInterface[];
community?: CommunityInterface;
}
export interface SpaceInterface {
uuid: string;
spaceName: string;
}
export interface CommunityInterface {
uuid: string;
name: string;
}
export interface addDeviceInRoomInterface {

View File

@ -8,7 +8,7 @@ import {
} from '@nestjs/common';
import { TuyaContext } from '@tuya/tuya-connector-nodejs';
import { ConfigService } from '@nestjs/config';
import { AddDeviceDto, UpdateDeviceInRoomDto } from '../dtos/add.device.dto';
import { AddDeviceDto, UpdateDeviceInSpaceDto } from '../dtos/add.device.dto';
import {
DeviceInstructionResponse,
GetDeviceDetailsFunctionsInterface,
@ -20,7 +20,7 @@ import {
updateDeviceFirmwareInterface,
} from '../interfaces/get.device.interface';
import {
GetDeviceByRoomUuidDto,
GetDeviceBySpaceUuidDto,
GetDeviceLogsDto,
} from '../dtos/get.device.dto';
import {
@ -39,6 +39,7 @@ import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status
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';
@Injectable()
export class DeviceService {
@ -70,6 +71,7 @@ export class DeviceService {
...(withProductDevice && { relations: ['productDevice'] }),
});
}
async getDeviceByDeviceTuyaUuid(deviceTuyaUuid: string) {
return await this.deviceRepository.findOne({
where: {
@ -78,6 +80,7 @@ export class DeviceService {
relations: ['productDevice'],
});
}
async addDeviceUser(addDeviceDto: AddDeviceDto) {
try {
const device = await this.getDeviceDetailsByDeviceIdTuya(
@ -117,12 +120,13 @@ export class DeviceService {
);
} else {
throw new HttpException(
error.message || 'Failed to add device in room',
error.message || 'Failed to add device in space',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
async getDevicesByUser(
userUuid: string,
): Promise<GetDeviceDetailsInterface[]> {
@ -169,14 +173,15 @@ export class DeviceService {
);
}
}
async getDevicesByRoomId(
getDeviceByRoomUuidDto: GetDeviceByRoomUuidDto,
async getDevicesBySpaceId(
getDeviceBySpaceUuidDto: GetDeviceBySpaceUuidDto,
userUuid: string,
): Promise<GetDeviceDetailsInterface[]> {
try {
const devices = await this.deviceRepository.find({
where: {
spaceDevice: { uuid: getDeviceByRoomUuidDto.roomUuid },
spaceDevice: { uuid: getDeviceBySpaceUuidDto.spaceUuid },
isActive: true,
permission: {
userUuid,
@ -211,22 +216,22 @@ export class DeviceService {
} catch (error) {
// Handle the error here
throw new HttpException(
'Error fetching devices by room',
'Error fetching devices by space',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async updateDeviceInRoom(updateDeviceInRoomDto: UpdateDeviceInRoomDto) {
async updateDeviceInSpace(updateDeviceInSpaceDto: UpdateDeviceInSpaceDto) {
try {
await this.deviceRepository.update(
{ uuid: updateDeviceInRoomDto.deviceUuid },
{ uuid: updateDeviceInSpaceDto.deviceUuid },
{
spaceDevice: { uuid: updateDeviceInRoomDto.roomUuid },
spaceDevice: { uuid: updateDeviceInSpaceDto.spaceUuid },
},
);
const device = await this.deviceRepository.findOne({
where: {
uuid: updateDeviceInRoomDto.deviceUuid,
uuid: updateDeviceInSpaceDto.deviceUuid,
},
relations: ['spaceDevice', 'spaceDevice.parent'],
});
@ -239,15 +244,16 @@ export class DeviceService {
return {
uuid: device.uuid,
roomUuid: device.spaceDevice.uuid,
spaceUuid: device.spaceDevice.uuid,
};
} catch (error) {
throw new HttpException(
'Failed to add device in room',
'Failed to add device in space',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async transferDeviceInSpacesTuya(
deviceId: string,
spaceId: string,
@ -295,6 +301,7 @@ export class DeviceService {
);
}
}
async factoryResetDeviceTuya(
deviceUuid: string,
): Promise<controlDeviceInterface> {
@ -337,6 +344,7 @@ export class DeviceService {
);
}
}
async batchControlDevices(batchControlDevicesDto: BatchControlDevicesDto) {
const { devicesUuid } = batchControlDevicesDto;
@ -562,13 +570,12 @@ export class DeviceService {
async getDeviceInstructionByDeviceId(
deviceUuid: string,
): Promise<DeviceInstructionResponse> {
const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid);
if (!deviceDetails) {
throw new NotFoundException('Device Not Found');
}
try {
const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid);
if (!deviceDetails) {
throw new NotFoundException('Device Not Found');
}
const response = await this.getDeviceInstructionByDeviceIdTuya(
deviceDetails.deviceTuyaUuid,
);
@ -623,6 +630,7 @@ 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,
@ -778,22 +786,22 @@ export class DeviceService {
);
}
}
async getDevicesByUnitId(unitUuid: string) {
async getDevicesBySpaceUuid(SpaceUuid: string) {
try {
const spaces = await this.spaceRepository.find({
where: {
parent: {
uuid: unitUuid,
uuid: SpaceUuid,
},
devicesSpaceEntity: {
devices: {
isActive: true,
},
},
relations: ['devicesSpaceEntity', 'devicesSpaceEntity.productDevice'],
relations: ['devices', 'devices.productDevice'],
});
const devices = spaces.flatMap((space) => {
return space.devicesSpaceEntity.map((device) => device);
return space.devices.map((device) => device);
});
const devicesData = await Promise.all(
@ -814,7 +822,7 @@ export class DeviceService {
return devicesData;
} catch (error) {
throw new HttpException(
'This unit does not have any devices',
'This space does not have any devices',
HttpStatus.NOT_FOUND,
);
}
@ -825,11 +833,13 @@ export class DeviceService {
where: { isActive: true },
relations: [
'spaceDevice.parent',
'spaceDevice.community',
'productDevice',
'permission',
'permission.permissionType',
],
});
const devicesData = await Promise.allSettled(
devices.map(async (device) => {
let battery = null;
@ -874,20 +884,24 @@ export class DeviceService {
battery = batteryStatus.value;
}
}
const spaceDevice = device?.spaceDevice;
const parentDevice = spaceDevice?.parent;
const spaceHierarchy = await this.getFullSpaceHierarchy(
device?.spaceDevice,
);
const orderedHierarchy = spaceHierarchy.reverse();
return {
room: {
uuid: spaceDevice?.uuid,
name: spaceDevice?.spaceName,
},
unit: {
uuid: parentDevice?.uuid,
name: parentDevice?.spaceName,
},
spaces: orderedHierarchy.map((space) => ({
uuid: space.uuid,
spaceName: space.spaceName,
})),
productUuid: device.productDevice.uuid,
productType: device.productDevice.prodType,
permissionType: device.permission[0].permissionType.type,
community: {
uuid: device.spaceDevice.community.uuid,
name: device.spaceDevice.community.name,
},
// permissionType: device.permission[0].permissionType.type,
...(await this.getDeviceDetailsByDeviceIdTuya(
device.deviceTuyaUuid,
)),
@ -913,6 +927,7 @@ export class DeviceService {
);
}
}
async getDeviceLogs(deviceUuid: string, query: GetDeviceLogsDto) {
try {
const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid);
@ -966,6 +981,40 @@ export class DeviceService {
);
}
}
async getFullSpaceHierarchy(
space: SpaceEntity,
): Promise<{ uuid: string; spaceName: string }[]> {
try {
// Fetch only the relevant spaces, starting with the target space
const targetSpace = await this.spaceRepository.findOne({
where: { uuid: space.uuid },
relations: ['parent', 'children'],
});
// Fetch only the ancestors of the target space
const ancestors = await this.fetchAncestors(targetSpace);
// Optionally, fetch descendants if required
const descendants = await this.fetchDescendants(targetSpace);
const fullHierarchy = [...ancestors, targetSpace, ...descendants].map(
(space) => ({
uuid: space.uuid,
spaceName: space.spaceName,
}),
);
return fullHierarchy;
} catch (error) {
console.error('Error fetching space hierarchy:', error.message);
throw new HttpException(
'Error fetching space hierarchy',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async getPowerClampInstructionStatus(powerClampUuid: string) {
try {
const deviceDetails = await this.getDeviceByDeviceUuid(powerClampUuid);
@ -1043,4 +1092,48 @@ export class DeviceService {
);
}
}
private async fetchAncestors(space: SpaceEntity): Promise<SpaceEntity[]> {
const ancestors: SpaceEntity[] = [];
let currentSpace = space;
while (currentSpace && currentSpace.parent) {
// Fetch the parent space
const parent = await this.spaceRepository.findOne({
where: { uuid: currentSpace.parent.uuid },
relations: ['parent'], // To continue fetching upwards
});
if (parent) {
ancestors.push(parent);
currentSpace = parent;
} else {
currentSpace = null;
}
}
// Return the ancestors in reverse order to have the root at the start
return ancestors.reverse();
}
private async fetchDescendants(space: SpaceEntity): Promise<SpaceEntity[]> {
const descendants: SpaceEntity[] = [];
// Fetch the immediate children of the current space
const children = await this.spaceRepository.find({
where: { parent: { uuid: space.uuid } },
relations: ['children'], // To continue fetching downwards
});
for (const child of children) {
// Add the child to the descendants list
descendants.push(child);
// Recursively fetch the child's descendants
const childDescendants = await this.fetchDescendants(child);
descendants.push(...childDescendants);
}
return descendants;
}
}

View File

@ -1,104 +0,0 @@
import { FloorService } from '../services/floor.service';
import {
Body,
Controller,
Get,
HttpStatus,
Param,
Post,
Put,
Query,
UseGuards,
} from '@nestjs/common';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import { AddFloorDto, AddUserFloorDto } from '../dtos/add.floor.dto';
import { GetFloorChildDto } from '../dtos/get.floor.dto';
import { UpdateFloorNameDto } from '../dtos/update.floor.dto';
import { CheckBuildingTypeGuard } from 'src/guards/building.type.guard';
import { CheckUserFloorGuard } from 'src/guards/user.floor.guard';
import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard';
import { AdminRoleGuard } from 'src/guards/admin.role.guard';
import { FloorPermissionGuard } from 'src/guards/floor.permission.guard';
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
import { SpaceType } from '@app/common/constants/space-type.enum';
@ApiTags('Floor Module')
@Controller({
version: EnableDisableStatusEnum.ENABLED,
path: SpaceType.FLOOR,
})
export class FloorController {
constructor(private readonly floorService: FloorService) {}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, CheckBuildingTypeGuard)
@Post()
async addFloor(@Body() addFloorDto: AddFloorDto) {
const floor = await this.floorService.addFloor(addFloorDto);
return {
statusCode: HttpStatus.CREATED,
success: true,
message: 'Floor added successfully',
data: floor,
};
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, FloorPermissionGuard)
@Get(':floorUuid')
async getFloorByUuid(@Param('floorUuid') floorUuid: string) {
const floor = await this.floorService.getFloorByUuid(floorUuid);
return floor;
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, FloorPermissionGuard)
@Get('child/:floorUuid')
async getFloorChildByUuid(
@Param('floorUuid') floorUuid: string,
@Query() query: GetFloorChildDto,
) {
const floor = await this.floorService.getFloorChildByUuid(floorUuid, query);
return floor;
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, FloorPermissionGuard)
@Get('parent/:floorUuid')
async getFloorParentByUuid(@Param('floorUuid') floorUuid: string) {
const floor = await this.floorService.getFloorParentByUuid(floorUuid);
return floor;
}
@ApiBearerAuth()
@UseGuards(AdminRoleGuard, CheckUserFloorGuard)
@Post('user')
async addUserFloor(@Body() addUserFloorDto: AddUserFloorDto) {
await this.floorService.addUserFloor(addUserFloorDto);
return {
statusCode: HttpStatus.CREATED,
success: true,
message: 'user floor added successfully',
};
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get('user/:userUuid')
async getFloorsByUserId(@Param('userUuid') userUuid: string) {
return await this.floorService.getFloorsByUserId(userUuid);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, FloorPermissionGuard)
@Put(':floorUuid')
async renameFloorByUuid(
@Param('floorUuid') floorUuid: string,
@Body() updateFloorNameDto: UpdateFloorNameDto,
) {
const floor = await this.floorService.renameFloorByUuid(
floorUuid,
updateFloorNameDto,
);
return floor;
}
}

View File

@ -1 +0,0 @@
export * from './floor.controller';

View File

@ -1,42 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class AddFloorDto {
@ApiProperty({
description: 'floorName',
required: true,
})
@IsString()
@IsNotEmpty()
public floorName: string;
@ApiProperty({
description: 'buildingUuid',
required: true,
})
@IsString()
@IsNotEmpty()
public buildingUuid: string;
constructor(dto: Partial<AddFloorDto>) {
Object.assign(this, dto);
}
}
export class AddUserFloorDto {
@ApiProperty({
description: 'floorUuid',
required: true,
})
@IsString()
@IsNotEmpty()
public floorUuid: string;
@ApiProperty({
description: 'userUuid',
required: true,
})
@IsString()
@IsNotEmpty()
public userUuid: string;
constructor(dto: Partial<AddUserFloorDto>) {
Object.assign(this, dto);
}
}

View File

@ -1,52 +0,0 @@
import { BooleanValues } from '@app/common/constants/boolean-values.enum';
import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import {
IsBoolean,
IsInt,
IsNotEmpty,
IsOptional,
IsString,
Min,
} from 'class-validator';
export class GetFloorDto {
@ApiProperty({
description: 'floorUuid',
required: true,
})
@IsString()
@IsNotEmpty()
public floorUuid: string;
}
export class GetFloorChildDto {
@ApiProperty({ example: 1, description: 'Page number', required: true })
@IsInt({ message: 'Page must be a number' })
@Min(1, { message: 'Page must not be less than 1' })
@IsNotEmpty()
public page: number;
@ApiProperty({
example: 10,
description: 'Number of items per page',
required: true,
})
@IsInt({ message: 'Page size must be a number' })
@Min(1, { message: 'Page size must not be less than 1' })
@IsNotEmpty()
public pageSize: number;
@ApiProperty({
example: true,
description: 'Flag to determine whether to fetch full hierarchy',
required: false,
default: false,
})
@IsOptional()
@IsBoolean()
@Transform((value) => {
return value.obj.includeSubSpaces === BooleanValues.TRUE;
})
public includeSubSpaces: boolean = false;
}

View File

@ -1 +0,0 @@
export * from './add.floor.dto';

View File

@ -1,16 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class UpdateFloorNameDto {
@ApiProperty({
description: 'floorName',
required: true,
})
@IsString()
@IsNotEmpty()
public floorName: string;
constructor(dto: Partial<UpdateFloorNameDto>) {
Object.assign(this, dto);
}
}

View File

@ -1,24 +0,0 @@
import { Module } from '@nestjs/common';
import { FloorService } from './services/floor.service';
import { FloorController } from './controllers/floor.controller';
import { ConfigModule } from '@nestjs/config';
import { SpaceRepositoryModule } from '@app/common/modules/space/space.repository.module';
import { SpaceRepository } from '@app/common/modules/space/repositories';
import { SpaceTypeRepository } from '@app/common/modules/space/repositories';
import { UserSpaceRepository } from '@app/common/modules/user/repositories';
import { UserRepositoryModule } from '@app/common/modules/user/user.repository.module';
import { UserRepository } from '@app/common/modules/user/repositories';
@Module({
imports: [ConfigModule, SpaceRepositoryModule, UserRepositoryModule],
controllers: [FloorController],
providers: [
FloorService,
SpaceRepository,
SpaceTypeRepository,
UserSpaceRepository,
UserRepository,
],
exports: [FloorService],
})
export class FloorModule {}

View File

@ -1,32 +0,0 @@
export interface GetFloorByUuidInterface {
uuid: string;
createdAt: Date;
updatedAt: Date;
name: string;
type: string;
}
export interface FloorChildInterface {
uuid: string;
name: string;
type: string;
totalCount?: number;
children?: FloorChildInterface[];
}
export interface FloorParentInterface {
uuid: string;
name: string;
type: string;
parent?: FloorParentInterface;
}
export interface RenameFloorByUuidInterface {
uuid: string;
name: string;
type: string;
}
export interface GetFloorByUserUuidInterface {
uuid: string;
name: string;
type: string;
}

View File

@ -1,310 +0,0 @@
import { GetFloorChildDto } from '../dtos/get.floor.dto';
import { SpaceTypeRepository } from '../../../libs/common/src/modules/space/repositories/space.repository';
import {
Injectable,
HttpException,
HttpStatus,
BadRequestException,
} from '@nestjs/common';
import { SpaceRepository } from '@app/common/modules/space/repositories';
import { AddFloorDto, AddUserFloorDto } from '../dtos';
import {
FloorChildInterface,
FloorParentInterface,
GetFloorByUserUuidInterface,
GetFloorByUuidInterface,
RenameFloorByUuidInterface,
} from '../interface/floor.interface';
import { SpaceEntity } from '@app/common/modules/space/entities';
import { UpdateFloorNameDto } from '../dtos/update.floor.dto';
import { UserSpaceRepository } from '@app/common/modules/user/repositories';
import { SpaceType } from '@app/common/constants/space-type.enum';
import { CommonErrorCodes } from '@app/common/constants/error-codes.enum';
@Injectable()
export class FloorService {
constructor(
private readonly spaceRepository: SpaceRepository,
private readonly spaceTypeRepository: SpaceTypeRepository,
private readonly userSpaceRepository: UserSpaceRepository,
) {}
async addFloor(addFloorDto: AddFloorDto) {
try {
const spaceType = await this.spaceTypeRepository.findOne({
where: {
type: SpaceType.FLOOR,
},
});
const floor = await this.spaceRepository.save({
spaceName: addFloorDto.floorName,
parent: { uuid: addFloorDto.buildingUuid },
spaceType: { uuid: spaceType.uuid },
});
return floor;
} catch (err) {
throw new HttpException(err.message, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
async getFloorByUuid(floorUuid: string): Promise<GetFloorByUuidInterface> {
try {
const floor = await this.spaceRepository.findOne({
where: {
uuid: floorUuid,
spaceType: {
type: SpaceType.FLOOR,
},
},
relations: ['spaceType'],
});
if (
!floor ||
!floor.spaceType ||
floor.spaceType.type !== SpaceType.FLOOR
) {
throw new BadRequestException('Invalid floor UUID');
}
return {
uuid: floor.uuid,
createdAt: floor.createdAt,
updatedAt: floor.updatedAt,
name: floor.spaceName,
type: floor.spaceType.type,
};
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException('Floor not found', HttpStatus.NOT_FOUND);
}
}
}
async getFloorChildByUuid(
floorUuid: string,
getFloorChildDto: GetFloorChildDto,
): Promise<FloorChildInterface> {
try {
const { includeSubSpaces, page, pageSize } = getFloorChildDto;
const space = await this.spaceRepository.findOneOrFail({
where: { uuid: floorUuid },
relations: ['children', 'spaceType'],
});
if (
!space ||
!space.spaceType ||
space.spaceType.type !== SpaceType.FLOOR
) {
throw new BadRequestException('Invalid floor UUID');
}
const totalCount = await this.spaceRepository.count({
where: { parent: { uuid: space.uuid } },
});
const children = await this.buildHierarchy(
space,
includeSubSpaces,
page,
pageSize,
);
return {
uuid: space.uuid,
name: space.spaceName,
type: space.spaceType.type,
totalCount,
children,
};
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException('Floor not found', HttpStatus.NOT_FOUND);
}
}
}
private async buildHierarchy(
space: SpaceEntity,
includeSubSpaces: boolean,
page: number,
pageSize: number,
): Promise<FloorChildInterface[]> {
const children = await this.spaceRepository.find({
where: { parent: { uuid: space.uuid } },
relations: ['spaceType'],
skip: (page - 1) * pageSize,
take: pageSize,
});
if (!children || children.length === 0 || !includeSubSpaces) {
return children
.filter(
(child) =>
child.spaceType.type !== SpaceType.FLOOR &&
child.spaceType.type !== SpaceType.BUILDING &&
child.spaceType.type !== SpaceType.COMMUNITY,
) // Filter remaining floor and building and community types
.map((child) => ({
uuid: child.uuid,
name: child.spaceName,
type: child.spaceType.type,
}));
}
const childHierarchies = await Promise.all(
children
.filter(
(child) =>
child.spaceType.type !== SpaceType.FLOOR &&
child.spaceType.type !== SpaceType.BUILDING &&
child.spaceType.type !== SpaceType.COMMUNITY,
) // Filter remaining floor and building and community types
.map(async (child) => ({
uuid: child.uuid,
name: child.spaceName,
type: child.spaceType.type,
children: await this.buildHierarchy(child, true, 1, pageSize),
})),
);
return childHierarchies;
}
async getFloorParentByUuid(floorUuid: string): Promise<FloorParentInterface> {
try {
const floor = await this.spaceRepository.findOne({
where: {
uuid: floorUuid,
spaceType: {
type: SpaceType.FLOOR,
},
},
relations: ['spaceType', 'parent', 'parent.spaceType'],
});
if (
!floor ||
!floor.spaceType ||
floor.spaceType.type !== SpaceType.FLOOR
) {
throw new BadRequestException('Invalid floor UUID');
}
return {
uuid: floor.uuid,
name: floor.spaceName,
type: floor.spaceType.type,
parent: {
uuid: floor.parent.uuid,
name: floor.parent.spaceName,
type: floor.parent.spaceType.type,
},
};
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException('Floor not found', HttpStatus.NOT_FOUND);
}
}
}
async getFloorsByUserId(
userUuid: string,
): Promise<GetFloorByUserUuidInterface[]> {
try {
const floors = await this.userSpaceRepository.find({
relations: ['space', 'space.spaceType'],
where: {
user: { uuid: userUuid },
space: { spaceType: { type: SpaceType.FLOOR } },
},
});
if (floors.length === 0) {
throw new HttpException(
'this user has no floors',
HttpStatus.NOT_FOUND,
);
}
const spaces = floors.map((floor) => ({
uuid: floor.space.uuid,
name: floor.space.spaceName,
type: floor.space.spaceType.type,
}));
return spaces;
} catch (err) {
if (err instanceof HttpException) {
throw err;
} else {
throw new HttpException('user not found', HttpStatus.NOT_FOUND);
}
}
}
async addUserFloor(addUserFloorDto: AddUserFloorDto) {
try {
await this.userSpaceRepository.save({
user: { uuid: addUserFloorDto.userUuid },
space: { uuid: addUserFloorDto.floorUuid },
});
} catch (err) {
if (err.code === CommonErrorCodes.DUPLICATE_ENTITY) {
throw new HttpException(
'User already belongs to this floor',
HttpStatus.BAD_REQUEST,
);
}
throw new HttpException(
err.message || 'Internal Server Error',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async renameFloorByUuid(
floorUuid: string,
updateFloorDto: UpdateFloorNameDto,
): Promise<RenameFloorByUuidInterface> {
try {
const floor = await this.spaceRepository.findOneOrFail({
where: { uuid: floorUuid },
relations: ['spaceType'],
});
if (
!floor ||
!floor.spaceType ||
floor.spaceType.type !== SpaceType.FLOOR
) {
throw new BadRequestException('Invalid floor UUID');
}
await this.spaceRepository.update(
{ uuid: floorUuid },
{ spaceName: updateFloorDto.floorName },
);
// Fetch the updated floor
const updatedFloor = await this.spaceRepository.findOneOrFail({
where: { uuid: floorUuid },
relations: ['spaceType'],
});
return {
uuid: updatedFloor.uuid,
name: updatedFloor.spaceName,
type: updatedFloor.spaceType.type,
};
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException('Floor not found', HttpStatus.NOT_FOUND);
}
}
}
}

View File

@ -1 +0,0 @@
export * from './floor.service';

View File

@ -14,13 +14,14 @@ export class GroupController {
constructor(private readonly groupService: GroupService) {}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, UnitPermissionGuard)
@Get(':unitUuid')
async getGroupsBySpaceUuid(@Param('unitUuid') unitUuid: string) {
return await this.groupService.getGroupsByUnitUuid(unitUuid);
@UseGuards(JwtAuthGuard)
@Get(':spaceUuid')
async getGroupsBySpaceUuid(@Param('spaceUuid') spaceUuid: string) {
return await this.groupService.getGroupsByUnitUuid(spaceUuid);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, UnitPermissionGuard)
@UseGuards(JwtAuthGuard)
@Get(':unitUuid/devices/:groupName')
async getUnitDevicesByGroupName(
@Param('unitUuid') unitUuid: string,

View File

@ -31,17 +31,13 @@ export class GroupService {
try {
const spaces = await this.spaceRepository.find({
where: {
parent: {
uuid: unitUuid,
},
uuid: unitUuid,
},
relations: ['devicesSpaceEntity', 'devicesSpaceEntity.productDevice'],
relations: ['devices', 'devices.productDevice'],
});
const groupNames = spaces.flatMap((space) => {
return space.devicesSpaceEntity.map(
(device) => device.productDevice.prodType,
);
return space.devices.map((device) => device.productDevice.prodType);
});
const uniqueGroupNames = [...new Set(groupNames)];
@ -82,7 +78,7 @@ export class GroupService {
parent: {
uuid: unitUuid,
},
devicesSpaceEntity: {
devices: {
productDevice: {
prodType: groupName,
},
@ -95,18 +91,18 @@ export class GroupService {
},
},
relations: [
'devicesSpaceEntity',
'devicesSpaceEntity.productDevice',
'devicesSpaceEntity.spaceDevice',
'devicesSpaceEntity.permission',
'devicesSpaceEntity.permission.permissionType',
'devices',
'devices.productDevice',
'devices.spaceDevice',
'devices.permission',
'devices.permission.permissionType',
],
});
const devices = await Promise.all(
spaces.flatMap(async (space) => {
return await Promise.all(
space.devicesSpaceEntity.map(async (device) => {
space.devices.map(async (device) => {
const deviceDetails = await this.getDeviceDetailsByDeviceIdTuya(
device.deviceTuyaUuid,
);

View File

@ -1,36 +0,0 @@
import { SpaceType } from '@app/common/constants/space-type.enum';
import { SpacePermissionService } from '@app/common/helper/services/space.permission.service';
import {
BadRequestException,
CanActivate,
ExecutionContext,
Injectable,
} from '@nestjs/common';
@Injectable()
export class BuildingPermissionGuard implements CanActivate {
constructor(private readonly permissionService: SpacePermissionService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = context.switchToHttp().getRequest();
try {
const { buildingUuid } = req.params;
const { user } = req;
if (!buildingUuid) {
throw new BadRequestException('buildingUuid is required');
}
await this.permissionService.checkUserPermission(
buildingUuid,
user.uuid,
SpaceType.BUILDING,
);
return true;
} catch (error) {
throw error;
}
}
}

View File

@ -1,67 +0,0 @@
import { SpaceType } from '@app/common/constants/space-type.enum';
import { SpaceRepository } from '@app/common/modules/space/repositories';
import {
Injectable,
CanActivate,
HttpStatus,
BadRequestException,
ExecutionContext,
} from '@nestjs/common';
@Injectable()
export class CheckBuildingTypeGuard implements CanActivate {
constructor(private readonly spaceRepository: SpaceRepository) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = context.switchToHttp().getRequest();
try {
const { floorName, buildingUuid } = req.body;
if (!floorName) {
throw new BadRequestException('floorName is required');
}
if (!buildingUuid) {
throw new BadRequestException('buildingUuid is required');
}
await this.checkBuildingIsBuildingType(buildingUuid);
return true;
} catch (error) {
this.handleGuardError(error, context);
return false;
}
}
async checkBuildingIsBuildingType(buildingUuid: string) {
const buildingData = await this.spaceRepository.findOne({
where: { uuid: buildingUuid },
relations: ['spaceType'],
});
if (
!buildingData ||
!buildingData.spaceType ||
buildingData.spaceType.type !== SpaceType.BUILDING
) {
throw new BadRequestException('Invalid building UUID');
}
}
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: 'Building not found',
});
}
}
}

View File

@ -1,5 +1,5 @@
import { SpaceType } from '@app/common/constants/space-type.enum';
import { SpacePermissionService } from '@app/common/helper/services/space.permission.service';
import { RoleType } from '@app/common/constants/role.type.enum';
import { CommunityPermissionService } from '@app/common/helper/services/community.permission.service';
import {
BadRequestException,
CanActivate,
@ -9,7 +9,7 @@ import {
@Injectable()
export class CommunityPermissionGuard implements CanActivate {
constructor(private readonly permissionService: SpacePermissionService) {}
constructor(private readonly permissionService: CommunityPermissionService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = context.switchToHttp().getRequest();
@ -18,15 +18,22 @@ export class CommunityPermissionGuard implements CanActivate {
const { communityUuid } = req.params;
const { user } = req;
if (
user &&
user.roles &&
user.roles.some(
(role) =>
role.type === RoleType.ADMIN || role.type === RoleType.SUPER_ADMIN,
)
) {
return true;
}
if (!communityUuid) {
throw new BadRequestException('communityUuid is required');
}
await this.permissionService.checkUserPermission(
communityUuid,
user.uuid,
SpaceType.COMMUNITY,
);
await this.permissionService.checkUserPermission(communityUuid);
return true;
} catch (error) {

View File

@ -1,68 +0,0 @@
import {
CanActivate,
ExecutionContext,
Injectable,
HttpStatus,
} from '@nestjs/common';
import { SpaceRepository } from '@app/common/modules/space/repositories';
import { BadRequestException } from '@nestjs/common';
import { SpaceType } from '@app/common/constants/space-type.enum';
@Injectable()
export class CheckCommunityTypeGuard implements CanActivate {
constructor(private readonly spaceRepository: SpaceRepository) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = context.switchToHttp().getRequest();
try {
const { buildingName, communityUuid } = req.body;
if (!buildingName) {
throw new BadRequestException('buildingName is required');
}
if (!communityUuid) {
throw new BadRequestException('communityUuid is required');
}
await this.checkCommunityIsCommunityType(communityUuid);
return true;
} catch (error) {
this.handleGuardError(error, context);
return false;
}
}
private async checkCommunityIsCommunityType(communityUuid: string) {
const communityData = await this.spaceRepository.findOne({
where: { uuid: communityUuid },
relations: ['spaceType'],
});
if (
!communityData ||
!communityData.spaceType ||
communityData.spaceType.type !== SpaceType.COMMUNITY
) {
throw new BadRequestException('Invalid community UUID');
}
}
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: 'Community not found',
});
}
}
}

View File

@ -1,36 +0,0 @@
import { SpaceType } from '@app/common/constants/space-type.enum';
import { SpacePermissionService } from '@app/common/helper/services/space.permission.service';
import {
BadRequestException,
CanActivate,
ExecutionContext,
Injectable,
} from '@nestjs/common';
@Injectable()
export class FloorPermissionGuard implements CanActivate {
constructor(private readonly permissionService: SpacePermissionService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = context.switchToHttp().getRequest();
try {
const { floorUuid } = req.params;
const { user } = req;
if (!floorUuid) {
throw new BadRequestException('floorUuid is required');
}
await this.permissionService.checkUserPermission(
floorUuid,
user.uuid,
SpaceType.FLOOR,
);
return true;
} catch (error) {
throw error;
}
}
}

View File

@ -1,4 +1,3 @@
import { SpaceType } from '@app/common/constants/space-type.enum';
import { SpaceRepository } from '@app/common/modules/space/repositories';
import {
Injectable,
@ -37,14 +36,9 @@ export class CheckFloorTypeGuard implements CanActivate {
async checkFloorIsFloorType(floorUuid: string) {
const floorData = await this.spaceRepository.findOne({
where: { uuid: floorUuid },
relations: ['spaceType'],
});
if (
!floorData ||
!floorData.spaceType ||
floorData.spaceType.type !== SpaceType.FLOOR
) {
if (!floorData) {
throw new BadRequestException('Invalid floor UUID');
}
}

View File

@ -8,7 +8,6 @@ import {
import { SpaceRepository } from '@app/common/modules/space/repositories';
import { BadRequestException, NotFoundException } from '@nestjs/common';
import { DeviceRepository } from '@app/common/modules/device/repositories';
import { SpaceType } from '@app/common/constants/space-type.enum';
@Injectable()
export class CheckRoomGuard implements CanActivate {
@ -43,9 +42,6 @@ export class CheckRoomGuard implements CanActivate {
const room = await this.spaceRepository.findOne({
where: {
uuid: roomUuid,
spaceType: {
type: SpaceType.ROOM,
},
},
});
if (!room) {

View File

@ -1,36 +0,0 @@
import { SpaceType } from '@app/common/constants/space-type.enum';
import { SpacePermissionService } from '@app/common/helper/services/space.permission.service';
import {
BadRequestException,
CanActivate,
ExecutionContext,
Injectable,
} from '@nestjs/common';
@Injectable()
export class RoomPermissionGuard implements CanActivate {
constructor(private readonly permissionService: SpacePermissionService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = context.switchToHttp().getRequest();
try {
const { roomUuid } = req.params;
const { user } = req;
if (!roomUuid) {
throw new BadRequestException('roomUuid is required');
}
await this.permissionService.checkUserPermission(
roomUuid,
user.uuid,
SpaceType.ROOM,
);
return true;
} catch (error) {
throw error;
}
}
}

View File

@ -1,4 +1,3 @@
import { SpaceType } from '@app/common/constants/space-type.enum';
import { SpacePermissionService } from '@app/common/helper/services/space.permission.service';
import {
BadRequestException,
@ -22,11 +21,7 @@ export class UnitPermissionGuard implements CanActivate {
throw new BadRequestException('unitUuid is required');
}
await this.permissionService.checkUserPermission(
unitUuid,
user.uuid,
SpaceType.UNIT,
);
await this.permissionService.checkUserPermission(unitUuid, user.uuid);
return true;
} catch (error) {

View File

@ -1,4 +1,3 @@
import { SpaceType } from '@app/common/constants/space-type.enum';
import { SpaceRepository } from '@app/common/modules/space/repositories';
import {
Injectable,
@ -34,18 +33,13 @@ export class CheckUnitTypeGuard implements CanActivate {
}
}
async checkFloorIsFloorType(unitUuid: string) {
const unitData = await this.spaceRepository.findOne({
where: { uuid: unitUuid },
relations: ['spaceType'],
async checkFloorIsFloorType(spaceUuid: string) {
const space = await this.spaceRepository.findOne({
where: { uuid: spaceUuid },
});
if (
!unitData ||
!unitData.spaceType ||
unitData.spaceType.type !== SpaceType.UNIT
) {
throw new BadRequestException('Invalid unit UUID');
if (!space) {
throw new BadRequestException(`Invalid space UUID ${spaceUuid}`);
}
}

View File

@ -1,71 +0,0 @@
import {
CanActivate,
ExecutionContext,
Injectable,
HttpStatus,
} from '@nestjs/common';
import { SpaceRepository } from '@app/common/modules/space/repositories';
import { BadRequestException, NotFoundException } from '@nestjs/common';
import { UserRepository } from '@app/common/modules/user/repositories';
import { SpaceType } from '@app/common/constants/space-type.enum';
@Injectable()
export class CheckUserBuildingGuard implements CanActivate {
constructor(
private readonly spaceRepository: SpaceRepository,
private readonly userRepository: UserRepository,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = context.switchToHttp().getRequest();
try {
const { userUuid, buildingUuid } = req.body;
await this.checkUserIsFound(userUuid);
await this.checkBuildingIsFound(buildingUuid);
return true;
} catch (error) {
this.handleGuardError(error, context);
return false;
}
}
private async checkUserIsFound(userUuid: string) {
const userData = await this.userRepository.findOne({
where: { uuid: userUuid },
});
if (!userData) {
throw new NotFoundException('User not found');
}
}
private async checkBuildingIsFound(spaceUuid: string) {
const spaceData = await this.spaceRepository.findOne({
where: { uuid: spaceUuid, spaceType: { type: SpaceType.BUILDING } },
relations: ['spaceType'],
});
if (!spaceData) {
throw new NotFoundException('Building not found');
}
}
private handleGuardError(error: Error, context: ExecutionContext) {
const response = context.switchToHttp().getResponse();
if (
error instanceof BadRequestException ||
error instanceof NotFoundException
) {
response
.status(HttpStatus.NOT_FOUND)
.json({ statusCode: HttpStatus.NOT_FOUND, message: error.message });
} else {
response.status(HttpStatus.BAD_REQUEST).json({
statusCode: HttpStatus.BAD_REQUEST,
message: 'invalid userUuid or buildingUuid',
});
}
}
}

View File

@ -7,7 +7,6 @@ import {
import { SpaceRepository } from '@app/common/modules/space/repositories';
import { BadRequestException, NotFoundException } from '@nestjs/common';
import { UserRepository } from '@app/common/modules/user/repositories';
import { SpaceType } from '@app/common/constants/space-type.enum';
@Injectable()
export class CheckUserCommunityGuard implements CanActivate {
@ -44,8 +43,7 @@ export class CheckUserCommunityGuard implements CanActivate {
private async checkCommunityIsFound(spaceUuid: string) {
const spaceData = await this.spaceRepository.findOne({
where: { uuid: spaceUuid, spaceType: { type: SpaceType.COMMUNITY } },
relations: ['spaceType'],
where: { uuid: spaceUuid },
});
if (!spaceData) {
throw new NotFoundException('Community not found');

View File

@ -1,71 +0,0 @@
import {
CanActivate,
ExecutionContext,
Injectable,
HttpStatus,
} from '@nestjs/common';
import { SpaceRepository } from '@app/common/modules/space/repositories';
import { BadRequestException, NotFoundException } from '@nestjs/common';
import { UserRepository } from '@app/common/modules/user/repositories';
import { SpaceType } from '@app/common/constants/space-type.enum';
@Injectable()
export class CheckUserFloorGuard implements CanActivate {
constructor(
private readonly spaceRepository: SpaceRepository,
private readonly userRepository: UserRepository,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = context.switchToHttp().getRequest();
try {
const { userUuid, floorUuid } = req.body;
await this.checkUserIsFound(userUuid);
await this.checkFloorIsFound(floorUuid);
return true;
} catch (error) {
this.handleGuardError(error, context);
return false;
}
}
private async checkUserIsFound(userUuid: string) {
const userData = await this.userRepository.findOne({
where: { uuid: userUuid },
});
if (!userData) {
throw new NotFoundException('User not found');
}
}
private async checkFloorIsFound(spaceUuid: string) {
const spaceData = await this.spaceRepository.findOne({
where: { uuid: spaceUuid, spaceType: { type: SpaceType.FLOOR } },
relations: ['spaceType'],
});
if (!spaceData) {
throw new NotFoundException('Floor not found');
}
}
private handleGuardError(error: Error, context: ExecutionContext) {
const response = context.switchToHttp().getResponse();
if (
error instanceof BadRequestException ||
error instanceof NotFoundException
) {
response
.status(HttpStatus.NOT_FOUND)
.json({ statusCode: HttpStatus.NOT_FOUND, message: error.message });
} else {
response.status(HttpStatus.BAD_REQUEST).json({
statusCode: HttpStatus.BAD_REQUEST,
message: 'invalid userUuid or floorUuid',
});
}
}
}

View File

@ -1,71 +0,0 @@
import {
CanActivate,
ExecutionContext,
Injectable,
HttpStatus,
} from '@nestjs/common';
import { SpaceRepository } from '@app/common/modules/space/repositories';
import { BadRequestException, NotFoundException } from '@nestjs/common';
import { UserRepository } from '@app/common/modules/user/repositories';
import { SpaceType } from '@app/common/constants/space-type.enum';
@Injectable()
export class CheckUserRoomGuard implements CanActivate {
constructor(
private readonly spaceRepository: SpaceRepository,
private readonly userRepository: UserRepository,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = context.switchToHttp().getRequest();
try {
const { userUuid, roomUuid } = req.body;
await this.checkUserIsFound(userUuid);
await this.checkRoomIsFound(roomUuid);
return true;
} catch (error) {
this.handleGuardError(error, context);
return false;
}
}
private async checkUserIsFound(userUuid: string) {
const userData = await this.userRepository.findOne({
where: { uuid: userUuid },
});
if (!userData) {
throw new NotFoundException('User not found');
}
}
private async checkRoomIsFound(spaceUuid: string) {
const spaceData = await this.spaceRepository.findOne({
where: { uuid: spaceUuid, spaceType: { type: SpaceType.ROOM } },
relations: ['spaceType'],
});
if (!spaceData) {
throw new NotFoundException('Room not found');
}
}
private handleGuardError(error: Error, context: ExecutionContext) {
const response = context.switchToHttp().getResponse();
if (
error instanceof BadRequestException ||
error instanceof NotFoundException
) {
response
.status(HttpStatus.NOT_FOUND)
.json({ statusCode: HttpStatus.NOT_FOUND, message: error.message });
} else {
response.status(HttpStatus.BAD_REQUEST).json({
statusCode: HttpStatus.BAD_REQUEST,
message: 'invalid userUuid or roomUuid',
});
}
}
}

View File

@ -1,71 +0,0 @@
import {
CanActivate,
ExecutionContext,
Injectable,
HttpStatus,
} from '@nestjs/common';
import { SpaceRepository } from '@app/common/modules/space/repositories';
import { BadRequestException, NotFoundException } from '@nestjs/common';
import { UserRepository } from '@app/common/modules/user/repositories';
import { SpaceType } from '@app/common/constants/space-type.enum';
@Injectable()
export class CheckUserUnitGuard implements CanActivate {
constructor(
private readonly spaceRepository: SpaceRepository,
private readonly userRepository: UserRepository,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = context.switchToHttp().getRequest();
try {
const { userUuid, unitUuid } = req.body;
await this.checkUserIsFound(userUuid);
await this.checkUnitIsFound(unitUuid);
return true;
} catch (error) {
this.handleGuardError(error, context);
return false;
}
}
private async checkUserIsFound(userUuid: string) {
const userData = await this.userRepository.findOne({
where: { uuid: userUuid },
});
if (!userData) {
throw new NotFoundException('User not found');
}
}
private async checkUnitIsFound(spaceUuid: string) {
const spaceData = await this.spaceRepository.findOne({
where: { uuid: spaceUuid, spaceType: { type: SpaceType.UNIT } },
relations: ['spaceType'],
});
if (!spaceData) {
throw new NotFoundException('Unit not found');
}
}
private handleGuardError(error: Error, context: ExecutionContext) {
const response = context.switchToHttp().getResponse();
if (
error instanceof BadRequestException ||
error instanceof NotFoundException
) {
response
.status(HttpStatus.NOT_FOUND)
.json({ statusCode: HttpStatus.NOT_FOUND, message: error.message });
} else {
response.status(HttpStatus.BAD_REQUEST).json({
statusCode: HttpStatus.BAD_REQUEST,
message: 'invalid userUuid or unitUuid',
});
}
}
}

View File

@ -1 +0,0 @@
export * from './room.controller';

View File

@ -1,90 +0,0 @@
import { RoomService } from '../services/room.service';
import {
Body,
Controller,
Get,
HttpStatus,
Param,
Post,
Put,
UseGuards,
} from '@nestjs/common';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import { AddRoomDto, AddUserRoomDto } from '../dtos/add.room.dto';
import { UpdateRoomNameDto } from '../dtos/update.room.dto';
import { CheckUnitTypeGuard } from 'src/guards/unit.type.guard';
import { CheckUserRoomGuard } from 'src/guards/user.room.guard';
import { AdminRoleGuard } from 'src/guards/admin.role.guard';
import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard';
import { RoomPermissionGuard } from 'src/guards/room.permission.guard';
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
import { SpaceType } from '@app/common/constants/space-type.enum';
@ApiTags('Room Module')
@Controller({
version: EnableDisableStatusEnum.ENABLED,
path: SpaceType.ROOM,
})
export class RoomController {
constructor(private readonly roomService: RoomService) {}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, CheckUnitTypeGuard)
@Post()
async addRoom(@Body() addRoomDto: AddRoomDto) {
const room = await this.roomService.addRoom(addRoomDto);
return {
statusCode: HttpStatus.CREATED,
success: true,
message: 'Room added successfully',
data: room,
};
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, RoomPermissionGuard)
@Get(':roomUuid')
async getRoomByUuid(@Param('roomUuid') roomUuid: string) {
const room = await this.roomService.getRoomByUuid(roomUuid);
return room;
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, RoomPermissionGuard)
@Get('parent/:roomUuid')
async getRoomParentByUuid(@Param('roomUuid') roomUuid: string) {
const room = await this.roomService.getRoomParentByUuid(roomUuid);
return room;
}
@ApiBearerAuth()
@UseGuards(AdminRoleGuard, CheckUserRoomGuard)
@Post('user')
async addUserRoom(@Body() addUserRoomDto: AddUserRoomDto) {
await this.roomService.addUserRoom(addUserRoomDto);
return {
statusCode: HttpStatus.CREATED,
success: true,
message: 'user room added successfully',
};
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get('user/:userUuid')
async getRoomsByUserId(@Param('userUuid') userUuid: string) {
return await this.roomService.getRoomsByUserId(userUuid);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, RoomPermissionGuard)
@Put(':roomUuid')
async renameRoomByUuid(
@Param('roomUuid') roomUuid: string,
@Body() updateRoomNameDto: UpdateRoomNameDto,
) {
const room = await this.roomService.renameRoomByUuid(
roomUuid,
updateRoomNameDto,
);
return room;
}
}

View File

@ -1,42 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class AddRoomDto {
@ApiProperty({
description: 'roomName',
required: true,
})
@IsString()
@IsNotEmpty()
public roomName: string;
@ApiProperty({
description: 'unitUuid',
required: true,
})
@IsString()
@IsNotEmpty()
public unitUuid: string;
constructor(dto: Partial<AddRoomDto>) {
Object.assign(this, dto);
}
}
export class AddUserRoomDto {
@ApiProperty({
description: 'roomUuid',
required: true,
})
@IsString()
@IsNotEmpty()
public roomUuid: string;
@ApiProperty({
description: 'userUuid',
required: true,
})
@IsString()
@IsNotEmpty()
public userUuid: string;
constructor(dto: Partial<AddUserRoomDto>) {
Object.assign(this, dto);
}
}

View File

@ -1 +0,0 @@
export * from './add.room.dto';

View File

@ -1,16 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class UpdateRoomNameDto {
@ApiProperty({
description: 'roomName',
required: true,
})
@IsString()
@IsNotEmpty()
public roomName: string;
constructor(dto: Partial<UpdateRoomNameDto>) {
Object.assign(this, dto);
}
}

View File

@ -1,24 +0,0 @@
export interface GetRoomByUuidInterface {
uuid: string;
createdAt: Date;
updatedAt: Date;
name: string;
type: string;
}
export interface RoomParentInterface {
uuid: string;
name: string;
type: string;
parent?: RoomParentInterface;
}
export interface RenameRoomByUuidInterface {
uuid: string;
name: string;
type: string;
}
export interface GetRoomByUserUuidInterface {
uuid: string;
name: string;
type: string;
}

View File

@ -1,24 +0,0 @@
import { Module } from '@nestjs/common';
import { RoomService } from './services/room.service';
import { RoomController } from './controllers/room.controller';
import { ConfigModule } from '@nestjs/config';
import { SpaceRepositoryModule } from '@app/common/modules/space/space.repository.module';
import { SpaceRepository } from '@app/common/modules/space/repositories';
import { SpaceTypeRepository } from '@app/common/modules/space/repositories';
import { UserSpaceRepository } from '@app/common/modules/user/repositories';
import { UserRepositoryModule } from '@app/common/modules/user/user.repository.module';
import { UserRepository } from '@app/common/modules/user/repositories';
@Module({
imports: [ConfigModule, SpaceRepositoryModule, UserRepositoryModule],
controllers: [RoomController],
providers: [
RoomService,
SpaceRepository,
SpaceTypeRepository,
UserSpaceRepository,
UserRepository,
],
exports: [RoomService],
})
export class RoomModule {}

View File

@ -1 +0,0 @@
export * from './room.service';

View File

@ -1,200 +0,0 @@
import { SpaceTypeRepository } from '../../../libs/common/src/modules/space/repositories/space.repository';
import {
Injectable,
HttpException,
HttpStatus,
BadRequestException,
} from '@nestjs/common';
import { SpaceRepository } from '@app/common/modules/space/repositories';
import { AddRoomDto, AddUserRoomDto } from '../dtos';
import {
RoomParentInterface,
GetRoomByUuidInterface,
RenameRoomByUuidInterface,
GetRoomByUserUuidInterface,
} from '../interface/room.interface';
import { UpdateRoomNameDto } from '../dtos/update.room.dto';
import { UserSpaceRepository } from '@app/common/modules/user/repositories';
import { SpaceType } from '@app/common/constants/space-type.enum';
import { CommonErrorCodes } from '@app/common/constants/error-codes.enum';
@Injectable()
export class RoomService {
constructor(
private readonly spaceRepository: SpaceRepository,
private readonly spaceTypeRepository: SpaceTypeRepository,
private readonly userSpaceRepository: UserSpaceRepository,
) {}
async addRoom(addRoomDto: AddRoomDto) {
try {
const spaceType = await this.spaceTypeRepository.findOne({
where: {
type: SpaceType.ROOM,
},
});
const room = await this.spaceRepository.save({
spaceName: addRoomDto.roomName,
parent: { uuid: addRoomDto.unitUuid },
spaceType: { uuid: spaceType.uuid },
});
return room;
} catch (err) {
throw new HttpException(err.message, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
async getRoomByUuid(roomUuid: string): Promise<GetRoomByUuidInterface> {
try {
const room = await this.spaceRepository.findOne({
where: {
uuid: roomUuid,
spaceType: {
type: SpaceType.ROOM,
},
},
relations: ['spaceType'],
});
if (!room || !room.spaceType || room.spaceType.type !== SpaceType.ROOM) {
throw new BadRequestException('Invalid room UUID');
}
return {
uuid: room.uuid,
createdAt: room.createdAt,
updatedAt: room.updatedAt,
name: room.spaceName,
type: room.spaceType.type,
};
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException('Room not found', HttpStatus.NOT_FOUND);
}
}
}
async getRoomParentByUuid(roomUuid: string): Promise<RoomParentInterface> {
try {
const room = await this.spaceRepository.findOne({
where: {
uuid: roomUuid,
spaceType: {
type: SpaceType.ROOM,
},
},
relations: ['spaceType', 'parent', 'parent.spaceType'],
});
if (!room || !room.spaceType || room.spaceType.type !== SpaceType.ROOM) {
throw new BadRequestException('Invalid room UUID');
}
return {
uuid: room.uuid,
name: room.spaceName,
type: room.spaceType.type,
parent: {
uuid: room.parent.uuid,
name: room.parent.spaceName,
type: room.parent.spaceType.type,
},
};
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException('Room not found', HttpStatus.NOT_FOUND);
}
}
}
async getRoomsByUserId(
userUuid: string,
): Promise<GetRoomByUserUuidInterface[]> {
try {
const rooms = await this.userSpaceRepository.find({
relations: ['space', 'space.spaceType'],
where: {
user: { uuid: userUuid },
space: { spaceType: { type: SpaceType.ROOM } },
},
});
if (rooms.length === 0) {
throw new HttpException('this user has no rooms', HttpStatus.NOT_FOUND);
}
const spaces = rooms.map((room) => ({
uuid: room.space.uuid,
name: room.space.spaceName,
type: room.space.spaceType.type,
}));
return spaces;
} catch (err) {
if (err instanceof HttpException) {
throw err;
} else {
throw new HttpException('user not found', HttpStatus.NOT_FOUND);
}
}
}
async addUserRoom(addUserRoomDto: AddUserRoomDto) {
try {
await this.userSpaceRepository.save({
user: { uuid: addUserRoomDto.userUuid },
space: { uuid: addUserRoomDto.roomUuid },
});
} catch (err) {
if (err.code === CommonErrorCodes.DUPLICATE_ENTITY) {
throw new HttpException(
'User already belongs to this room',
HttpStatus.BAD_REQUEST,
);
}
throw new HttpException(
err.message || 'Internal Server Error',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async renameRoomByUuid(
roomUuid: string,
updateRoomNameDto: UpdateRoomNameDto,
): Promise<RenameRoomByUuidInterface> {
try {
const room = await this.spaceRepository.findOneOrFail({
where: { uuid: roomUuid },
relations: ['spaceType'],
});
if (!room || !room.spaceType || room.spaceType.type !== SpaceType.ROOM) {
throw new BadRequestException('Invalid room UUID');
}
await this.spaceRepository.update(
{ uuid: roomUuid },
{ spaceName: updateRoomNameDto.roomName },
);
// Fetch the updated room
const updateRoom = await this.spaceRepository.findOneOrFail({
where: { uuid: roomUuid },
relations: ['spaceType'],
});
return {
uuid: updateRoom.uuid,
name: updateRoom.spaceName,
type: updateRoom.spaceType.type,
};
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException('Room not found', HttpStatus.NOT_FOUND);
}
}
}
}

View File

@ -8,23 +8,24 @@ import {
Param,
Post,
Put,
Query,
UseGuards,
} from '@nestjs/common';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
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';
import { SceneParamDto } from '../dtos';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { ControllerRoute } from '@app/common/constants/controller-route';
@ApiTags('Scene Module')
@Controller({
version: EnableDisableStatusEnum.ENABLED,
path: 'scene',
path: ControllerRoute.SCENE.ROUTE,
})
export class SceneController {
constructor(private readonly sceneService: SceneService) {}
@ -32,80 +33,74 @@ export class SceneController {
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Post('tap-to-run')
async addTapToRunScene(@Body() addSceneTapToRunDto: AddSceneTapToRunDto) {
const tapToRunScene =
await this.sceneService.addTapToRunScene(addSceneTapToRunDto);
return {
statusCode: HttpStatus.CREATED,
success: true,
message: 'Scene added successfully',
data: tapToRunScene,
};
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get('tap-to-run/:unitUuid')
async getTapToRunSceneByUnit(
@Param('unitUuid') unitUuid: string,
@Query() inHomePage: GetSceneDto,
) {
const tapToRunScenes = await this.sceneService.getTapToRunSceneByUnit(
unitUuid,
inHomePage,
);
return tapToRunScenes;
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Delete('tap-to-run/:unitUuid/:sceneId')
async deleteTapToRunScene(
@Param('unitUuid') unitUuid: string,
@Param('sceneId') sceneId: string,
) {
await this.sceneService.deleteTapToRunScene(unitUuid, sceneId);
return {
statusCode: HttpStatus.OK,
message: 'Scene Deleted Successfully',
};
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Post('tap-to-run/trigger/:sceneId')
async triggerTapToRunScene(@Param('sceneId') sceneId: string) {
await this.sceneService.triggerTapToRunScene(sceneId);
return {
statusCode: HttpStatus.CREATED,
success: true,
message: 'Scene trigger successfully',
};
@ApiOperation({
summary: ControllerRoute.SCENE.ACTIONS.CREATE_TAP_TO_RUN_SCENE_SUMMARY,
description:
ControllerRoute.SCENE.ACTIONS.CREATE_TAP_TO_RUN_SCENE_DESCRIPTION,
})
async addTapToRunScene(
@Body() addSceneTapToRunDto: AddSceneTapToRunDto,
): Promise<BaseResponseDto> {
return await this.sceneService.createScene(addSceneTapToRunDto);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get('tap-to-run/details/:sceneId')
async getTapToRunSceneDetails(@Param('sceneId') sceneId: string) {
const tapToRunScenes =
await this.sceneService.getTapToRunSceneDetails(sceneId);
return tapToRunScenes;
@Delete('tap-to-run/:sceneUuid')
@ApiOperation({
summary: ControllerRoute.SCENE.ACTIONS.DELETE_TAP_TO_RUN_SCENE_SUMMARY,
description:
ControllerRoute.SCENE.ACTIONS.DELETE_TAP_TO_RUN_SCENE_DESCRIPTION,
})
async deleteTapToRunScene(
@Param() param: SceneParamDto,
): Promise<BaseResponseDto> {
return await this.sceneService.deleteScene(param);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Put('tap-to-run/:sceneId')
@Post('tap-to-run/:sceneUuid/trigger')
@ApiOperation({
summary: ControllerRoute.SCENE.ACTIONS.TRIGGER_TAP_TO_RUN_SCENE_SUMMARY,
description:
ControllerRoute.SCENE.ACTIONS.TRIGGER_TAP_TO_RUN_SCENE_DESCRIPTION,
})
async triggerTapToRunScene(@Param() param: SceneParamDto) {
return await this.sceneService.triggerTapToRunScene(param.sceneUuid);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get('tap-to-run/:sceneUuid')
@ApiOperation({
summary: ControllerRoute.SCENE.ACTIONS.GET_TAP_TO_RUN_SCENE_SUMMARY,
description: ControllerRoute.SCENE.ACTIONS.GET_TAP_TO_RUN_SCENE_DESCRIPTION,
})
async getTapToRunSceneDetails(
@Param() param: SceneParamDto,
): Promise<BaseResponseDto> {
return await this.sceneService.getSceneByUuid(param.sceneUuid);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Put('tap-to-run/:sceneUuid')
@ApiOperation({
summary: ControllerRoute.SCENE.ACTIONS.UPDATE_TAP_TO_RUN_SCENE_SUMMARY,
description:
ControllerRoute.SCENE.ACTIONS.UPDATE_TAP_TO_RUN_SCENE_DESCRIPTION,
})
async updateTapToRunScene(
@Body() updateSceneTapToRunDto: UpdateSceneTapToRunDto,
@Param('sceneId') sceneId: string,
@Param() param: SceneParamDto,
) {
const tapToRunScene = await this.sceneService.updateTapToRunScene(
return await this.sceneService.updateTapToRunScene(
updateSceneTapToRunDto,
sceneId,
param.sceneUuid,
);
return {
statusCode: HttpStatus.CREATED,
success: true,
message: 'Scene updated successfully',
data: tapToRunScene,
};
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Post('icon')

View File

@ -0,0 +1,18 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsUUID } from 'class-validator';
export class DeleteSceneParamDto {
@ApiProperty({
description: 'UUID of the Space',
example: 'd290f1ee-6c54-4b01-90e6-d701748f0851',
})
@IsUUID()
spaceUuid: string;
@ApiProperty({
description: 'UUID of the Scene',
example: 'd290f1ee-6c54-4b01-90e6-d701748f0851',
})
@IsUUID()
sceneUuid: string;
}

View File

@ -1 +1,4 @@
export * from './scene.dto';
export * from './space.param.dto';
export * from './scene.param.dto';
export * from './delete.scene.param.dto';

View File

@ -36,7 +36,7 @@ class ExecutorProperty {
public delaySeconds?: number;
}
class Action {
export class Action {
@ApiProperty({
description: 'Entity ID',
required: true,
@ -66,12 +66,13 @@ class Action {
export class AddSceneTapToRunDto {
@ApiProperty({
description: 'Unit UUID',
description: 'Space UUID',
required: true,
})
@IsString()
@IsNotEmpty()
public unitUuid: string;
public spaceUuid: string;
@ApiProperty({
description: 'Icon UUID',
required: false,

View File

@ -0,0 +1,11 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsUUID } from 'class-validator';
export class SceneParamDto {
@ApiProperty({
description: 'UUID of the Scene',
example: 'd290f1ee-6c54-4b01-90e6-d701748f0851',
})
@IsUUID()
sceneUuid: string;
}

View File

@ -0,0 +1,11 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsUUID } from 'class-validator';
export class SpaceParamDto {
@ApiProperty({
description: 'UUID of the space',
example: 'd290f1ee-6c54-4b01-90e6-d701748f0851',
})
@IsUUID()
spaceUuid: string;
}

View File

@ -1,3 +1,5 @@
import { Action } from '../dtos';
export interface AddTapToRunSceneInterface {
success: boolean;
msg?: string;
@ -25,4 +27,19 @@ export interface SceneDetailsResult {
id: string;
name: string;
type: string;
actions?: any;
status?: string;
}
export interface SceneDetails {
uuid: string;
sceneTuyaId: string;
name: string;
status: string;
icon?: string;
iconUuid?: string;
showInHome: boolean;
type: string;
actions: Action[];
spaceId: string;
}

View File

@ -12,6 +12,7 @@ import {
SceneIconRepository,
SceneRepository,
} from '@app/common/modules/scene/repositories';
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
@Module({
imports: [ConfigModule, SpaceRepositoryModule, DeviceStatusFirebaseModule],
@ -20,6 +21,7 @@ import {
SceneService,
SpaceRepository,
DeviceService,
TuyaService,
DeviceRepository,
ProductRepository,
SceneIconRepository,

View File

@ -6,190 +6,220 @@ import {
} from '@nestjs/common';
import { SpaceRepository } from '@app/common/modules/space/repositories';
import {
Action,
AddSceneIconDto,
AddSceneTapToRunDto,
GetSceneDto,
SceneParamDto,
UpdateSceneTapToRunDto,
} from '../dtos';
import { GetUnitByUuidInterface } from 'src/unit/interface/unit.interface';
import { ConfigService } from '@nestjs/config';
import { TuyaContext } from '@tuya/tuya-connector-nodejs';
import { convertKeysToSnakeCase } from '@app/common/helper/snakeCaseConverter';
import { DeviceService } from 'src/device/services';
import {
AddTapToRunSceneInterface,
DeleteTapToRunSceneInterface,
SceneDetails,
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';
import { SceneIconType } from '@app/common/constants/secne-icon-type.enum';
import {
SceneEntity,
SceneIconEntity,
} from '@app/common/modules/scene/entities';
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
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';
@Injectable()
export class SceneService {
private tuya: TuyaContext;
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<string>('auth-config.ACCESS_KEY');
const secretKey = this.configService.get<string>('auth-config.SECRET_KEY');
const tuyaEuUrl = this.configService.get<string>('tuya-config.TUYA_EU_URL');
this.tuya = new TuyaContext({
baseUrl: tuyaEuUrl,
accessKey,
secretKey,
});
private readonly tuyaService: TuyaService,
) {}
async createScene(
addSceneTapToRunDto: AddSceneTapToRunDto,
): Promise<BaseResponseDto> {
try {
const { spaceUuid } = addSceneTapToRunDto;
const space = await this.getSpaceByUuid(spaceUuid);
const scene = await this.create(space.spaceTuyaUuid, addSceneTapToRunDto);
return new SuccessResponseDto({
message: `Successfully created new scene with uuid ${scene.uuid}`,
data: scene,
statusCode: HttpStatus.CREATED,
});
} catch (err) {
console.error(
`Error in createScene for space UUID ${addSceneTapToRunDto.spaceUuid}:`,
err.message,
);
throw err instanceof HttpException
? err
: new HttpException(
'Failed to create scene',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async addTapToRunScene(
async create(
spaceTuyaUuid: string,
addSceneTapToRunDto: AddSceneTapToRunDto,
spaceTuyaId = null,
unitUuid?: string,
) {
): Promise<SceneEntity> {
const { iconUuid, showInHomePage, spaceUuid } = addSceneTapToRunDto;
try {
let unitSpaceTuyaId;
if (!spaceTuyaId) {
const unitDetails = await this.getUnitByUuid(
addSceneTapToRunDto.unitUuid,
const [defaultSceneIcon] = await Promise.all([
this.getDefaultSceneIcon(),
]);
if (!defaultSceneIcon) {
throw new HttpException(
'Default scene icon not found',
HttpStatus.INTERNAL_SERVER_ERROR,
);
unitSpaceTuyaId = unitDetails.spaceTuyaUuid;
if (!unitDetails) {
throw new BadRequestException('Invalid unit UUID');
}
} else {
unitSpaceTuyaId = spaceTuyaId;
}
const actions = addSceneTapToRunDto.actions.map((action) => {
return {
...action,
};
const response = await this.createSceneExternalService(
spaceTuyaUuid,
addSceneTapToRunDto,
);
const scene = await this.sceneRepository.save({
sceneTuyaUuid: response.result.id,
sceneIcon: { uuid: iconUuid || defaultSceneIcon.uuid },
showInHomePage,
spaceUuid,
});
const convertedData = convertKeysToSnakeCase(actions);
for (const action of convertedData) {
if (action.action_executor === ActionExecutorEnum.DEVICE_ISSUE) {
const device = await this.deviceService.getDeviceByDeviceUuid(
action.entity_id,
false,
);
if (device) {
action.entity_id = device.deviceTuyaUuid;
}
}
}
const path = `/v2.0/cloud/scene/rule`;
const response: AddTapToRunSceneInterface = await this.tuya.request({
method: 'POST',
path,
body: {
space_id: unitSpaceTuyaId,
name: addSceneTapToRunDto.sceneName,
type: 'scene',
decision_expr: addSceneTapToRunDto.decisionExpr,
actions: convertedData,
},
});
if (!response.success) {
throw new HttpException(response.msg, HttpStatus.BAD_REQUEST);
} else {
const defaultSceneIcon = await this.sceneIconRepository.findOne({
where: { iconType: SceneIconType.Default },
});
await this.sceneRepository.save({
sceneTuyaUuid: response.result.id,
sceneIcon: {
uuid: addSceneTapToRunDto.iconUuid
? addSceneTapToRunDto.iconUuid
: defaultSceneIcon.uuid,
},
showInHomePage: addSceneTapToRunDto.showInHomePage,
unitUuid: unitUuid ? unitUuid : addSceneTapToRunDto.unitUuid,
});
}
return {
id: response.result.id,
};
return scene;
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
if (err instanceof HttpException) {
throw err;
} else if (err.message?.includes('tuya')) {
throw new HttpException(
'API error: Failed to create scene',
HttpStatus.BAD_GATEWAY,
);
} else {
throw new HttpException(
err.message || 'Scene not found',
err.status || HttpStatus.NOT_FOUND,
'Database error: Failed to save scene',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
async getUnitByUuid(unitUuid: string): Promise<GetUnitByUuidInterface> {
async createSceneExternalService(
spaceTuyaUuid: string,
addSceneTapToRunDto: AddSceneTapToRunDto,
) {
const { sceneName, decisionExpr, actions } = addSceneTapToRunDto;
try {
const unit = await this.spaceRepository.findOne({
where: {
uuid: unitUuid,
spaceType: {
type: SpaceType.UNIT,
},
},
relations: ['spaceType'],
});
if (!unit || !unit.spaceType || unit.spaceType.type !== SpaceType.UNIT) {
throw new BadRequestException('Invalid unit UUID');
const formattedActions = await this.prepareActions(actions);
const response = (await this.tuyaService.addTapToRunScene(
spaceTuyaUuid,
sceneName,
formattedActions,
decisionExpr,
)) as AddTapToRunSceneInterface;
if (!response.result?.id) {
throw new HttpException(
'Failed to create scene in Tuya',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
return {
uuid: unit.uuid,
createdAt: unit.createdAt,
updatedAt: unit.updatedAt,
name: unit.spaceName,
type: unit.spaceType.type,
spaceTuyaUuid: unit.spaceTuyaUuid,
};
return response;
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
if (err instanceof HttpException) {
throw err;
} else if (err.message?.includes('tuya')) {
throw new HttpException(
'API error: Failed to create scene',
HttpStatus.BAD_GATEWAY,
);
} else {
throw new HttpException('Unit not found', HttpStatus.NOT_FOUND);
throw new HttpException(
'An Internal error has been occured',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
async getTapToRunSceneByUnit(unitUuid: string, inHomePage: GetSceneDto) {
async findScenesBySpace(spaceUuid: string, filter: GetSceneDto) {
try {
const showInHomePage = inHomePage?.showInHomePage;
await this.getSpaceByUuid(spaceUuid);
const showInHomePage = filter?.showInHomePage;
const scenesData = await this.sceneRepository.find({
where: {
unitUuid,
...(showInHomePage ? { showInHomePage } : {}),
spaceUuid,
...(showInHomePage !== undefined ? { showInHomePage } : {}),
},
});
if (!scenesData.length) {
throw new HttpException(
`No scenes found for space UUID ${spaceUuid}`,
HttpStatus.NOT_FOUND,
);
}
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,
};
const { actions, ...sceneDetails } = await this.getScene(scene);
return sceneDetails;
}),
);
return scenes;
} catch (err) {
console.error(
`Error fetching Tap-to-Run scenes for space UUID ${spaceUuid}:`,
err.message,
);
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
throw err;
} else {
throw new HttpException(
'An error occurred while retrieving scenes',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
async triggerTapToRunScene(sceneUuid: string) {
try {
const scene = await this.findScene(sceneUuid);
await this.tuyaService.triggerScene(scene.sceneTuyaUuid);
return new SuccessResponseDto({
message: `Scene with ID ${sceneUuid} triggered successfully`,
});
} catch (err) {
if (err instanceof HttpException) {
throw err;
} else {
throw new HttpException(
err.message || 'Scene not found',
@ -198,215 +228,103 @@ export class SceneService {
}
}
}
async deleteTapToRunScene(
unitUuid: string,
sceneId: string,
spaceTuyaId = null,
) {
try {
let unitSpaceTuyaId;
if (!spaceTuyaId) {
const unitDetails = await this.getUnitByUuid(unitUuid);
unitSpaceTuyaId = unitDetails.spaceTuyaUuid;
if (!unitSpaceTuyaId) {
throw new BadRequestException('Invalid unit UUID');
}
} else {
unitSpaceTuyaId = spaceTuyaId;
}
const path = `/v2.0/cloud/scene/rule?ids=${sceneId}&space_id=${unitSpaceTuyaId}`;
const response: DeleteTapToRunSceneInterface = await this.tuya.request({
method: 'DELETE',
path,
});
if (!response.success) {
throw new HttpException('Scene not found', HttpStatus.NOT_FOUND);
} else {
await this.sceneRepository.delete({ sceneTuyaUuid: sceneId });
}
return response;
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException(
err.message || 'Scene not found',
err.status || HttpStatus.NOT_FOUND,
);
}
}
}
async triggerTapToRunScene(sceneId: string) {
try {
const path = `/v2.0/cloud/scene/rule/${sceneId}/actions/trigger`;
const response: DeleteTapToRunSceneInterface = await this.tuya.request({
method: 'POST',
path,
});
if (!response.success) {
throw new HttpException(response.msg, HttpStatus.BAD_REQUEST);
}
return response;
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException(
err.message || 'Scene not found',
err.status || HttpStatus.NOT_FOUND,
);
}
}
}
async getTapToRunSceneDetails(sceneId: string, withSpaceId = false) {
try {
const path = `/v2.0/cloud/scene/rule/${sceneId}`;
const response = await this.tuya.request({
method: 'GET',
path,
});
if (!response.success) {
throw new HttpException(response.msg, HttpStatus.BAD_REQUEST);
}
const responseData = convertKeysToCamelCase(response.result);
const actions = responseData.actions.map((action) => {
return {
...action,
};
});
for (const action of actions) {
if (action.actionExecutor === ActionExecutorEnum.DEVICE_ISSUE) {
const device = await this.deviceService.getDeviceByDeviceTuyaUuid(
action.entityId,
);
if (device) {
action.entityId = device.uuid;
}
} else if (
action.actionExecutor !== ActionExecutorEnum.DEVICE_ISSUE &&
action.actionExecutor !== ActionExecutorEnum.DELAY
) {
const sceneDetails = await this.getTapToRunSceneDetailsTuya(
action.entityId,
);
if (sceneDetails.id) {
action.name = sceneDetails.name;
action.type = sceneDetails.type;
}
}
}
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 }),
};
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException('Scene not found', HttpStatus.NOT_FOUND);
}
}
}
async getTapToRunSceneDetailsTuya(
async fetchSceneDetailsFromTuya(
sceneId: string,
): Promise<SceneDetailsResult> {
try {
const path = `/v2.0/cloud/scene/rule/${sceneId}`;
const response = await this.tuya.request({
method: 'GET',
path,
});
if (!response.success) {
throw new HttpException(response.msg, HttpStatus.BAD_REQUEST);
}
const response = await this.tuyaService.getSceneRule(sceneId);
const camelCaseResponse = convertKeysToCamelCase(response);
const { id, name, type } = camelCaseResponse.result;
const {
id,
name,
type,
status,
actions: tuyaActions = [],
} = camelCaseResponse.result;
const actions = tuyaActions.map((action) => ({ ...action }));
return {
id,
name,
type,
status,
actions,
} as SceneDetailsResult;
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
console.error(
`Error fetching scene details for scene ID ${sceneId}:`,
err,
);
if (err instanceof HttpException) {
throw err;
} else {
throw new HttpException(
'Scene not found for Tuya',
HttpStatus.NOT_FOUND,
'An error occurred while fetching scene details from Tuya',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
async updateTapToRunScene(
updateSceneTapToRunDto: UpdateSceneTapToRunDto,
sceneId: string,
sceneUuid: string,
) {
try {
const spaceTuyaId = await this.getTapToRunSceneDetails(sceneId, true);
if (!spaceTuyaId.spaceId) {
throw new HttpException("Scene doesn't exist", HttpStatus.NOT_FOUND);
}
const scene = await this.findScene(sceneUuid);
const space = await this.getSpaceByUuid(scene.spaceUuid);
const addSceneTapToRunDto: AddSceneTapToRunDto = {
...updateSceneTapToRunDto,
unitUuid: null,
spaceUuid: scene.spaceUuid,
iconUuid: updateSceneTapToRunDto.iconUuid,
showInHomePage: updateSceneTapToRunDto.showInHomePage,
};
const scene = await this.sceneRepository.findOne({
where: { sceneTuyaUuid: sceneId },
});
const newTapToRunScene = await this.addTapToRunScene(
const createdTuyaSceneResponse = await this.createSceneExternalService(
space.spaceTuyaUuid,
addSceneTapToRunDto,
spaceTuyaId.spaceId,
scene.unitUuid,
);
if (newTapToRunScene.id) {
await this.deleteTapToRunScene(null, sceneId, spaceTuyaId.spaceId);
const newSceneTuyaUuid = createdTuyaSceneResponse.result?.id;
await this.sceneRepository.update(
{ sceneTuyaUuid: sceneId },
{
sceneTuyaUuid: newTapToRunScene.id,
showInHomePage: addSceneTapToRunDto.showInHomePage,
sceneIcon: {
uuid: addSceneTapToRunDto.iconUuid,
},
unitUuid: scene.unitUuid,
},
if (!newSceneTuyaUuid) {
throw new HttpException(
`Failed to create a external new scene`,
HttpStatus.BAD_GATEWAY,
);
return newTapToRunScene;
}
await this.delete(scene.sceneTuyaUuid, space.spaceTuyaUuid);
const updatedScene = await this.sceneRepository.update(
{ uuid: sceneUuid },
{
sceneTuyaUuid: newSceneTuyaUuid,
showInHomePage: addSceneTapToRunDto.showInHomePage,
sceneIcon: {
uuid: addSceneTapToRunDto.iconUuid,
},
spaceUuid: scene.spaceUuid,
},
);
return new SuccessResponseDto({
data: updatedScene,
message: `Scene with ID ${sceneUuid} updated successfully`,
});
} catch (err) {
if (err instanceof BadRequestException) {
if (err instanceof HttpException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException(
err.message || 'Scene not found',
err.message || `Scene not found for id ${sceneUuid}`,
err.status || HttpStatus.NOT_FOUND,
);
}
}
}
async addSceneIcon(addSceneIconDto: AddSceneIconDto) {
try {
const icon = await this.sceneIconRepository.save({
@ -424,6 +342,7 @@ export class SceneService {
);
}
}
async getAllIcons() {
try {
const icons = await this.sceneIconRepository.find({
@ -449,4 +368,209 @@ export class SceneService {
);
}
}
async getSceneByUuid(sceneUuid: string): Promise<BaseResponseDto> {
try {
const scene = await this.findScene(sceneUuid);
const sceneDetails = await this.getScene(scene);
return new SuccessResponseDto({
data: sceneDetails,
message: `Scene details for ${sceneUuid} retrieved successfully`,
});
} catch (error) {
console.error(
`Error fetching scene details for sceneUuid ${sceneUuid}:`,
error,
);
if (error instanceof HttpException) {
throw error;
} else {
throw new HttpException(
'An error occurred while retrieving scene details',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
async getScene(scene: SceneEntity): Promise<SceneDetails> {
try {
const { actions, name, status } = await this.fetchSceneDetailsFromTuya(
scene.sceneTuyaUuid,
);
for (const action of actions) {
if (action.actionExecutor === ActionExecutorEnum.DEVICE_ISSUE) {
const device = await this.deviceService.getDeviceByDeviceTuyaUuid(
action.entityId,
);
if (device) {
action.entityId = device.uuid;
}
} else if (
action.actionExecutor !== ActionExecutorEnum.DEVICE_ISSUE &&
action.actionExecutor !== ActionExecutorEnum.DELAY
) {
const sceneDetails = await this.fetchSceneDetailsFromTuya(
action.entityId,
);
if (sceneDetails.id) {
action.name = sceneDetails.name;
action.type = sceneDetails.type;
}
}
}
return {
uuid: scene.uuid,
sceneTuyaId: scene.sceneTuyaUuid,
name,
status,
icon: scene.sceneIcon?.icon,
iconUuid: scene.sceneIcon?.uuid,
showInHome: scene.showInHomePage,
type: 'tap_to_run',
actions,
spaceId: scene.spaceUuid,
};
} catch (err) {
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,
);
}
}
}
async deleteScene(params: SceneParamDto): Promise<BaseResponseDto> {
try {
const { sceneUuid } = params;
const scene = await this.findScene(sceneUuid);
const space = await this.getSpaceByUuid(scene.spaceUuid);
await this.delete(scene.sceneTuyaUuid, space.spaceTuyaUuid);
return new SuccessResponseDto({
message: `Scene with ID ${sceneUuid} deleted successfully`,
});
} catch (err) {
if (err instanceof HttpException) {
throw err;
} else {
throw new HttpException(
err.message || `Scene not found for id ${params.sceneUuid}`,
err.status || HttpStatus.NOT_FOUND,
);
}
}
}
async findScene(sceneUuid: string): Promise<SceneEntity> {
const scene = await this.sceneRepository.findOne({
where: { uuid: sceneUuid },
relations: ['sceneIcon'],
});
if (!scene) {
throw new HttpException(
`Invalid scene with id ${sceneUuid}`,
HttpStatus.NOT_FOUND,
);
}
return scene;
}
async delete(tuyaSceneId: string, tuyaSpaceId: string) {
try {
const response = (await this.tuyaService.deleteSceneRule(
tuyaSceneId,
tuyaSpaceId,
)) as DeleteTapToRunSceneInterface;
return response;
} catch (error) {
if (error instanceof HttpException) {
throw error;
} else {
throw new HttpException(
'Failed to delete scene rule in Tuya',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
private async prepareActions(actions: Action[]): Promise<ConvertedAction[]> {
const convertedData = convertKeysToSnakeCase(actions) as ConvertedAction[];
await Promise.all(
convertedData.map(async (action) => {
if (action.action_executor === ActionExecutorEnum.DEVICE_ISSUE) {
const device = await this.deviceService.getDeviceByDeviceUuid(
action.entity_id,
false,
);
if (device) {
action.entity_id = device.deviceTuyaUuid;
}
}
}),
);
return convertedData;
}
private async getDefaultSceneIcon(): Promise<SceneIconEntity> {
const defaultIcon = await this.sceneIconRepository.findOne({
where: { iconType: SceneIconType.Default },
});
return defaultIcon;
}
async getSpaceByUuid(spaceUuid: string) {
try {
const space = await this.spaceRepository.findOne({
where: {
uuid: spaceUuid,
},
relations: ['community'],
});
if (!space) {
throw new HttpException(
`Invalid space UUID ${spaceUuid}`,
HttpStatusCode.BadRequest,
);
}
if (!space.community.externalId) {
throw new HttpException(
`Space doesn't have any association with tuya${spaceUuid}`,
HttpStatusCode.BadRequest,
);
}
return {
uuid: space.uuid,
createdAt: space.createdAt,
updatedAt: space.updatedAt,
name: space.spaceName,
spaceTuyaUuid: space.community.externalId,
};
} catch (err) {
if (err instanceof BadRequestException) {
console.log(err);
throw err;
} else {
throw new HttpException(
`Space with id ${spaceUuid} not found`,
HttpStatus.NOT_FOUND,
);
}
}
}
}

View File

@ -0,0 +1,5 @@
export * from './space.controller';
export * from './space-user.controller';
export * from './space-device.controller';
export * from './space-scene.controller';
export * from './subspace';

View File

@ -0,0 +1,30 @@
import { ControllerRoute } from '@app/common/constants/controller-route';
import { Controller, Get, Param, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard';
import { GetSpaceParam } from '../dtos';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { SpaceDeviceService } from '../services';
@ApiTags('Space Module')
@Controller({
version: '1',
path: ControllerRoute.SPACE_DEVICES.ROUTE,
})
export class SpaceDeviceController {
constructor(private readonly spaceDeviceService: SpaceDeviceService) {}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@ApiOperation({
summary: ControllerRoute.SPACE_DEVICES.ACTIONS.LIST_SPACE_DEVICE_SUMMARY,
description:
ControllerRoute.SPACE_DEVICES.ACTIONS.LIST_SPACE_DEVICE_DESCRIPTION,
})
@Get()
async listDevicesInSpace(
@Param() params: GetSpaceParam,
): Promise<BaseResponseDto> {
return await this.spaceDeviceService.listDevicesInSpace(params);
}
}

View File

@ -0,0 +1,34 @@
import { ControllerRoute } from '@app/common/constants/controller-route';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard';
import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { SpaceSceneService } from '../services';
import { GetSceneDto } from '../../scene/dtos';
import { GetSpaceParam } from '../dtos';
@ApiTags('Space Module')
@Controller({
version: '1',
path: ControllerRoute.SPACE_SCENE.ROUTE,
})
export class SpaceSceneController {
constructor(private readonly sceneService: SpaceSceneService) {}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@ApiOperation({
summary:
ControllerRoute.SPACE_SCENE.ACTIONS.GET_TAP_TO_RUN_SCENE_BY_SPACE_SUMMARY,
description:
ControllerRoute.SPACE_SCENE.ACTIONS
.GET_TAP_TO_RUN_SCENE_BY_SPACE_DESCRIPTION,
})
@Get()
async getTapToRunSceneByUnit(
@Param() params: GetSpaceParam,
@Query() inHomePage: GetSceneDto,
): Promise<BaseResponseDto> {
return await this.sceneService.getScenes(params, inHomePage);
}
}

View File

@ -0,0 +1,51 @@
import { ControllerRoute } from '@app/common/constants/controller-route';
import { Controller, Delete, Param, Post, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { SpaceUserService } from '../services';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard';
import { UserSpaceParam } from '../dtos';
@ApiTags('Space Module')
@Controller({
version: '1',
path: ControllerRoute.SPACE_USER.ROUTE,
})
export class SpaceUserController {
constructor(private readonly spaceUserService: SpaceUserService) {}
@ApiBearerAuth()
@Post('/:userUuid')
@UseGuards(JwtAuthGuard)
@ApiOperation({
summary:
ControllerRoute.SPACE_USER.ACTIONS.ASSOCIATE_SPACE_USER_DESCRIPTION,
description:
ControllerRoute.SPACE_USER.ACTIONS.ASSOCIATE_SPACE_USER_DESCRIPTION,
})
async associateUserToSpace(
@Param() params: UserSpaceParam,
): Promise<BaseResponseDto> {
return this.spaceUserService.associateUserToSpace(
params.userUuid,
params.spaceUuid,
);
}
@ApiBearerAuth()
@Delete('/:userUuid')
@UseGuards(JwtAuthGuard)
@ApiOperation({
summary: ControllerRoute.SPACE_USER.ACTIONS.DISSOCIATE_SPACE_USER_SUMMARY,
description:
ControllerRoute.SPACE_USER.ACTIONS.DISSOCIATE_SPACE_USER_DESCRIPTION,
})
async disassociateUserFromSpace(
@Param() params: UserSpaceParam,
): Promise<BaseResponseDto> {
return this.spaceUserService.disassociateUserFromSpace(
params.userUuid,
params.spaceUuid,
);
}
}

View File

@ -0,0 +1,127 @@
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { SpaceService } from '../services';
import { ControllerRoute } from '@app/common/constants/controller-route';
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
UseGuards,
} from '@nestjs/common';
import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard';
import { AddSpaceDto, CommunitySpaceParam } from '../dtos';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { GetSpaceParam } from '../dtos/get.space.param';
@ApiTags('Space Module')
@Controller({
version: '1',
path: ControllerRoute.SPACE.ROUTE,
})
export class SpaceController {
constructor(private readonly spaceService: SpaceService) {}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@ApiOperation({
summary: ControllerRoute.SPACE.ACTIONS.CREATE_SPACE_SUMMARY,
description: ControllerRoute.SPACE.ACTIONS.CREATE_SPACE_DESCRIPTION,
})
@Post()
async createSpace(
@Body() addSpaceDto: AddSpaceDto,
@Param() communitySpaceParam: CommunitySpaceParam,
): Promise<BaseResponseDto> {
return await this.spaceService.createSpace(
addSpaceDto,
communitySpaceParam.communityUuid,
);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@ApiOperation({
summary:
ControllerRoute.SPACE.ACTIONS.GET_COMMUNITY_SPACES_HIERARCHY_SUMMARY,
description:
ControllerRoute.SPACE.ACTIONS.GET_COMMUNITY_SPACES_HIERARCHY_DESCRIPTION,
})
@Get()
async getHierarchy(
@Param() param: CommunitySpaceParam,
): Promise<BaseResponseDto> {
return this.spaceService.getSpacesHierarchyForCommunity(
param.communityUuid,
);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@ApiOperation({
summary: ControllerRoute.SPACE.ACTIONS.DELETE_SPACE_SUMMARY,
description: ControllerRoute.SPACE.ACTIONS.DELETE_SPACE_DESCRIPTION,
})
@Delete('/:spaceUuid')
async deleteSpace(@Param() params: GetSpaceParam): Promise<BaseResponseDto> {
return this.spaceService.delete(params.spaceUuid, params.communityUuid);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Put('/:spaceUuid')
@ApiOperation({
summary: ControllerRoute.SPACE.ACTIONS.UPDATE_SPACE_SUMMARY,
description: ControllerRoute.SPACE.ACTIONS.UPDATE_SPACE_SUMMARY,
})
async updateSpace(
@Param() params: GetSpaceParam,
@Body() updateSpaceDto: AddSpaceDto,
): Promise<BaseResponseDto> {
return this.spaceService.updateSpace(
params.spaceUuid,
params.communityUuid,
updateSpaceDto,
);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@ApiOperation({
summary: ControllerRoute.SPACE.ACTIONS.GET_SPACE_SUMMARY,
description: ControllerRoute.SPACE.ACTIONS.GET_SPACE_DESCRIPTION,
})
@Get('/:spaceUuid')
async get(@Param() params: GetSpaceParam): Promise<BaseResponseDto> {
return this.spaceService.findOne(params.spaceUuid);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@ApiOperation({
summary: ControllerRoute.SPACE.ACTIONS.GET_HEIRARCHY_SUMMARY,
description: ControllerRoute.SPACE.ACTIONS.GET_HEIRARCHY_DESCRIPTION,
})
@Get('/:spaceUuid/hierarchy')
async getHierarchyUnderSpace(
@Param() params: GetSpaceParam,
): Promise<BaseResponseDto> {
return this.spaceService.getSpacesHierarchyForSpace(params.spaceUuid);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@ApiOperation({
summary: ControllerRoute.SPACE.ACTIONS.CREATE_INVITATION_CODE_SPACE_SUMMARY,
description:
ControllerRoute.SPACE.ACTIONS.CREATE_INVITATION_CODE_SPACE_DESCRIPTION,
})
@Post(':spaceUuid/invitation-code')
async generateSpaceInvitationCode(
@Param() params: GetSpaceParam,
): Promise<BaseResponseDto> {
return this.spaceService.getSpaceInvitationCode(params.spaceUuid);
}
}

View File

@ -0,0 +1,2 @@
export * from './subspace.controller';
export * from './subspace-device.controller';

View File

@ -0,0 +1,64 @@
import { ControllerRoute } from '@app/common/constants/controller-route';
import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard';
import { Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { DeviceSubSpaceParam, GetSubSpaceParam } from '../../dtos';
import { SubspaceDeviceService } from 'src/space/services';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
@ApiTags('Space Module')
@Controller({
version: '1',
path: ControllerRoute.SUBSPACE_DEVICE.ROUTE,
})
export class SubSpaceDeviceController {
constructor(private readonly subspaceDeviceService: SubspaceDeviceService) {}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@ApiOperation({
summary:
ControllerRoute.SUBSPACE_DEVICE.ACTIONS.LIST_SUBSPACE_DEVICE_SUMMARY,
description:
ControllerRoute.SUBSPACE_DEVICE.ACTIONS.LIST_SUBSPACE_DEVICE_DESCRIPTION,
})
@Get()
async listDevicesInSubspace(
@Param() params: GetSubSpaceParam,
): Promise<BaseResponseDto> {
return await this.subspaceDeviceService.listDevicesInSubspace(params);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@ApiOperation({
summary:
ControllerRoute.SUBSPACE_DEVICE.ACTIONS.ASSOCIATE_SUBSPACE_DEVICE_SUMMARY,
description:
ControllerRoute.SUBSPACE_DEVICE.ACTIONS
.ASSOCIATE_SUBSPACE_DEVICE_DESCRIPTION,
})
@Post('/:deviceUuid')
async associateDeviceToSubspace(
@Param() params: DeviceSubSpaceParam,
): Promise<BaseResponseDto> {
return await this.subspaceDeviceService.associateDeviceToSubspace(params);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@ApiOperation({
summary:
ControllerRoute.SUBSPACE_DEVICE.ACTIONS
.DISASSOCIATE_SUBSPACE_DEVICE_SUMMARY,
description:
ControllerRoute.SUBSPACE_DEVICE.ACTIONS
.DISASSOCIATE_SUBSPACE_DEVICE_DESCRIPTION,
})
@Post('/:deviceUuid')
async disassociateDeviceFromSubspace(
@Param() params: DeviceSubSpaceParam,
): Promise<BaseResponseDto> {
return await this.subspaceDeviceService.associateDeviceToSubspace(params);
}
}

View File

@ -0,0 +1,91 @@
import { ControllerRoute } from '@app/common/constants/controller-route';
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
Query,
UseGuards,
} from '@nestjs/common';
import { SubSpaceService } from '../../services';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { AddSubspaceDto, GetSpaceParam, GetSubSpaceParam } from '../../dtos';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard';
import { PaginationRequestGetListDto } from '@app/common/dto/pagination.request.dto';
@ApiTags('Space Module')
@Controller({
version: '1',
path: ControllerRoute.SUBSPACE.ROUTE,
})
export class SubSpaceController {
constructor(private readonly subSpaceService: SubSpaceService) {}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Post()
@ApiOperation({
summary: ControllerRoute.SUBSPACE.ACTIONS.CREATE_SUBSPACE_SUMMARY,
description: ControllerRoute.SUBSPACE.ACTIONS.CREATE_SUBSPACE_DESCRIPTION,
})
async createSubspace(
@Param() params: GetSpaceParam,
@Body() addSubspaceDto: AddSubspaceDto,
): Promise<BaseResponseDto> {
return this.subSpaceService.createSubspace(addSubspaceDto, params);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@ApiOperation({
summary: ControllerRoute.SUBSPACE.ACTIONS.LIST_SUBSPACES_SUMMARY,
description: ControllerRoute.SUBSPACE.ACTIONS.LIST_SUBSPACES_DESCRIPTION,
})
@Get()
async list(
@Param() params: GetSpaceParam,
@Query() query: PaginationRequestGetListDto,
): Promise<BaseResponseDto> {
return this.subSpaceService.list(params, query);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@ApiOperation({
summary: ControllerRoute.SUBSPACE.ACTIONS.GET_SUBSPACE_SUMMARY,
description: ControllerRoute.SUBSPACE.ACTIONS.GET_SUBSPACE_DESCRIPTION,
})
@Get(':subSpaceUuid')
async findOne(@Param() params: GetSubSpaceParam): Promise<BaseResponseDto> {
return this.subSpaceService.findOne(params);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@ApiOperation({
summary: ControllerRoute.SUBSPACE.ACTIONS.UPDATE_SUBSPACE_SUMMARY,
description: ControllerRoute.SUBSPACE.ACTIONS.UPDATE_SUBSPACE_DESCRIPTION,
})
@Put(':subSpaceUuid')
async updateSubspace(
@Param() params: GetSubSpaceParam,
@Body() updateSubSpaceDto: AddSubspaceDto,
): Promise<BaseResponseDto> {
return this.subSpaceService.updateSubSpace(params, updateSubSpaceDto);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@ApiOperation({
summary: ControllerRoute.SUBSPACE.ACTIONS.DELETE_SUBSPACE_SUMMARY,
description: ControllerRoute.SUBSPACE.ACTIONS.DELETE_SUBSPACE_DESCRIPTION,
})
@Delete(':subSpaceUuid')
async delete(@Param() params: GetSubSpaceParam): Promise<BaseResponseDto> {
return this.subSpaceService.delete(params);
}
}

View File

@ -0,0 +1,35 @@
import { ApiProperty } from '@nestjs/swagger';
import {
IsBoolean,
IsNotEmpty,
IsOptional,
IsString,
IsUUID,
} from 'class-validator';
export class AddSpaceDto {
@ApiProperty({
description: 'Name of the space (e.g., Floor 1, Unit 101)',
example: 'Unit 101',
})
@IsString()
@IsNotEmpty()
spaceName: string;
@ApiProperty({
description: 'UUID of the parent space (if any, for hierarchical spaces)',
example: 'f5d7e9c3-44bc-4b12-88f1-1b3cda84752e',
required: false,
})
@IsUUID()
@IsOptional()
parentUuid?: string;
@ApiProperty({
description: 'Indicates whether the space is private or public',
example: false,
default: false,
})
@IsBoolean()
isPrivate: boolean;
}

View File

@ -0,0 +1,11 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsUUID } from 'class-validator';
export class CommunitySpaceParam {
@ApiProperty({
description: 'UUID of the community this space belongs to',
example: 'd290f1ee-6c54-4b01-90e6-d701748f0851',
})
@IsUUID()
communityUuid: string;
}

View File

@ -0,0 +1,12 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsUUID } from 'class-validator';
import { CommunitySpaceParam } from './community-space.param';
export class GetSpaceParam extends CommunitySpaceParam {
@ApiProperty({
description: 'UUID of the Space',
example: 'd290f1ee-6c54-4b01-90e6-d701748f0851',
})
@IsUUID()
spaceUuid: string;
}

5
src/space/dtos/index.ts Normal file
View File

@ -0,0 +1,5 @@
export * from './add.space.dto';
export * from './community-space.param';
export * from './get.space.param';
export * from './user-space.param';
export * from './subspace';

View File

@ -0,0 +1,12 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsUUID } from 'class-validator';
import { GetSubSpaceParam } from './get.subspace.param';
export class DeviceSubSpaceParam extends GetSubSpaceParam {
@ApiProperty({
description: 'UUID of the device',
example: 'd290f1ee-6c54-4b01-90e6-d701748f0851',
})
@IsUUID()
deviceUuid: string;
}

View File

@ -0,0 +1,12 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class AddSubspaceDto {
@ApiProperty({
example: 'Meeting Room 1',
description: 'Name of the subspace',
})
@IsNotEmpty()
@IsString()
subspaceName: string;
}

View File

@ -0,0 +1,12 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsUUID } from 'class-validator';
import { GetSpaceParam } from '../get.space.param';
export class GetSubSpaceParam extends GetSpaceParam {
@ApiProperty({
description: 'UUID of the sub space',
example: 'd290f1ee-6c54-4b01-90e6-d701748f0851',
})
@IsUUID()
subSpaceUuid: string;
}

View File

@ -0,0 +1,3 @@
export * from './add.subspace.dto';
export * from './get.subspace.param';
export * from './add.subspace-device.param';

View File

@ -0,0 +1,12 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsUUID } from 'class-validator';
import { GetSpaceParam } from './get.space.param';
export class UserSpaceParam extends GetSpaceParam {
@ApiProperty({
description: 'Uuid of the user to be associated/ dissociated',
example: 'd290f1ee-6c54-4b01-90e6-d701748f0851',
})
@IsUUID()
userUuid: string;
}

View File

@ -0,0 +1,5 @@
export * from './space.service';
export * from './space-user.service';
export * from './space-device.service';
export * from './subspace';
export * from './space-scene.service';

View File

@ -0,0 +1,115 @@
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
import { CommunityRepository } from '@app/common/modules/community/repositories';
import { SpaceRepository } from '@app/common/modules/space/repositories';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { GetDeviceDetailsInterface } from 'src/device/interfaces/get.device.interface';
import { GetSpaceParam } from '../dtos';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter';
import { ProductRepository } from '@app/common/modules/product/repositories';
@Injectable()
export class SpaceDeviceService {
constructor(
private readonly spaceRepository: SpaceRepository,
private readonly tuyaService: TuyaService,
private readonly productRepository: ProductRepository,
private readonly communityRepository: CommunityRepository,
) {}
async listDevicesInSpace(params: GetSpaceParam): Promise<BaseResponseDto> {
const { spaceUuid, communityUuid } = params;
try {
const space = await this.validateCommunityAndSpace(
communityUuid,
spaceUuid,
);
const detailedDevices = await Promise.all(
space.devices.map(async (device) => {
const tuyaDetails = await this.getDeviceDetailsByDeviceIdTuya(
device.deviceTuyaUuid,
);
return {
uuid: device.uuid,
deviceTuyaUuid: device.deviceTuyaUuid,
productUuid: device.productDevice.uuid,
productType: device.productDevice.prodType,
isActive: device.isActive,
createdAt: device.createdAt,
updatedAt: device.updatedAt,
...tuyaDetails,
};
}),
);
return new SuccessResponseDto({
data: detailedDevices,
message: 'Successfully retrieved list of devices',
});
} catch (error) {
console.error('Error listing devices in space:', error);
throw new HttpException(
error.message || 'Failed to retrieve devices in space',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async validateCommunityAndSpace(communityUuid: string, spaceUuid: string) {
const community = await this.communityRepository.findOne({
where: { uuid: communityUuid },
});
if (!community) {
this.throwNotFound('Community', communityUuid);
}
const space = await this.spaceRepository.findOne({
where: { uuid: spaceUuid, community: { uuid: communityUuid } },
relations: ['devices', 'devices.productDevice'],
});
if (!space) {
this.throwNotFound('Space', spaceUuid);
}
return space;
}
private throwNotFound(entity: string, uuid: string) {
throw new HttpException(
`${entity} with ID ${uuid} not found`,
HttpStatus.NOT_FOUND,
);
}
private async getDeviceDetailsByDeviceIdTuya(
deviceId: string,
): Promise<GetDeviceDetailsInterface> {
try {
const tuyaDeviceDetails =
await this.tuyaService.getDeviceDetails(deviceId);
// Convert keys to camel case
const camelCaseResponse = convertKeysToCamelCase(tuyaDeviceDetails);
const product = await this.productRepository.findOne({
where: {
prodId: camelCaseResponse.productId,
},
});
// Exclude specific keys and add `productUuid`
const { ...rest } = camelCaseResponse;
return {
...rest,
productUuid: product?.uuid,
} as GetDeviceDetailsInterface;
} catch (error) {
throw new HttpException(
`Error fetching device details from Tuya for device id ${deviceId}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}

View File

@ -0,0 +1,55 @@
import {
BadRequestException,
HttpException,
HttpStatus,
Injectable,
} from '@nestjs/common';
import { GetSpaceParam } from '../dtos';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { SpaceService } from './space.service';
import { SceneService } from '../../scene/services';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { GetSceneDto } from '../../scene/dtos';
@Injectable()
export class SpaceSceneService {
constructor(
private readonly spaceSevice: SpaceService,
private readonly sceneSevice: SceneService,
) {}
async getScenes(
params: GetSpaceParam,
getSceneDto: GetSceneDto,
): Promise<BaseResponseDto> {
try {
const { spaceUuid, communityUuid } = params;
await this.spaceSevice.validateCommunityAndSpace(
communityUuid,
spaceUuid,
);
const scenes = await this.sceneSevice.findScenesBySpace(
spaceUuid,
getSceneDto,
);
return new SuccessResponseDto({
message: `Scenes retrieved successfully for space ${spaceUuid}`,
data: scenes,
});
} catch (error) {
console.error('Error retrieving scenes:', error);
if (error instanceof BadRequestException) {
throw new HttpException(error.message, HttpStatus.BAD_REQUEST);
} else {
throw new HttpException(
'An error occurred while retrieving scenes',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
}

View File

@ -0,0 +1,108 @@
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { SpaceRepository } from '@app/common/modules/space/repositories';
import {
UserRepository,
UserSpaceRepository,
} from '@app/common/modules/user/repositories';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
@Injectable()
export class SpaceUserService {
constructor(
private readonly spaceRepository: SpaceRepository,
private readonly userRepository: UserRepository,
private readonly userSpaceRepository: UserSpaceRepository,
) {}
async associateUserToSpace(
userUuid: string,
spaceUuid: string,
): Promise<BaseResponseDto> {
// Find the user by ID
const user = await this.userRepository.findOne({
where: { uuid: userUuid },
});
if (!user) {
throw new HttpException(
`User with ID ${userUuid} not found`,
HttpStatus.NOT_FOUND,
);
}
// Find the space by ID
const space = await this.spaceRepository.findOne({
where: { uuid: spaceUuid },
});
if (!space) {
throw new HttpException(
`Space with ID ${spaceUuid} not found`,
HttpStatus.NOT_FOUND,
);
}
// Check if the association already exists
const existingAssociation = await this.userSpaceRepository.findOne({
where: { user: { uuid: userUuid }, space: { uuid: spaceUuid } },
});
if (existingAssociation) {
throw new HttpException(
`User is already associated with the space`,
HttpStatus.CONFLICT,
);
}
const userSpace = this.userSpaceRepository.create({ user, space });
await this.userSpaceRepository.save(userSpace);
return new SuccessResponseDto({
data: userSpace,
message: `Space ${spaceUuid} has been successfully associated t user ${userUuid}`,
});
}
async disassociateUserFromSpace(
userUuid: string,
spaceUuid: string,
): Promise<BaseResponseDto> {
// Find the user by ID
const user = await this.userRepository.findOne({
where: { uuid: userUuid },
});
if (!user) {
throw new HttpException(
`User with ID ${userUuid} not found`,
HttpStatus.NOT_FOUND,
);
}
// Find the space by ID
const space = await this.spaceRepository.findOne({
where: { uuid: spaceUuid },
});
if (!space) {
throw new HttpException(
`Space with ID ${spaceUuid} not found`,
HttpStatus.NOT_FOUND,
);
}
// Find the existing association
const existingAssociation = await this.userSpaceRepository.findOne({
where: { user: { uuid: userUuid }, space: { uuid: spaceUuid } },
});
if (!existingAssociation) {
throw new HttpException(
`No association found between user ${userUuid} and space ${spaceUuid}`,
HttpStatus.NOT_FOUND,
);
}
// Remove the association
await this.userSpaceRepository.remove(existingAssociation);
return new SuccessResponseDto({
message: `User ${userUuid} has been successfully disassociated from space ${spaceUuid}`,
});
}
}

View File

@ -0,0 +1,385 @@
import { SpaceRepository } from '@app/common/modules/space/repositories';
import {
BadRequestException,
HttpException,
HttpStatus,
Injectable,
} from '@nestjs/common';
import { AddSpaceDto } from '../dtos';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
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';
@Injectable()
export class SpaceService {
constructor(
private readonly spaceRepository: SpaceRepository,
private readonly communityRepository: CommunityRepository,
) {}
async createSpace(
addSpaceDto: AddSpaceDto,
communityId: string,
): Promise<BaseResponseDto> {
let parent: SpaceEntity | null = null;
const { parentUuid } = addSpaceDto;
const community = await this.communityRepository.findOne({
where: { uuid: communityId },
});
// If the community doesn't exist, throw a 404 error
if (!community) {
throw new HttpException(
`Community with ID ${communityId} not found`,
HttpStatus.NOT_FOUND,
);
}
if (parentUuid) {
parent = await this.spaceRepository.findOne({
where: { uuid: parentUuid },
});
// If the community doesn't exist, throw a 404 error
if (!parent) {
throw new HttpException(
`Parent with ID ${parentUuid} not found`,
HttpStatus.NOT_FOUND,
);
}
}
try {
const newSpace = this.spaceRepository.create({
...addSpaceDto,
community,
parent: parentUuid ? parent : null,
});
await this.spaceRepository.save(newSpace);
return new SuccessResponseDto({
statusCode: HttpStatus.CREATED,
data: newSpace,
message: 'Space created successfully',
});
} catch (error) {
throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
async getSpacesHierarchyForCommunity(
communityUuid: string,
): Promise<BaseResponseDto> {
const community = await this.communityRepository.findOne({
where: { uuid: communityUuid },
});
// If the community doesn't exist, throw a 404 error
if (!community) {
throw new HttpException(
`Community with ID ${communityUuid} not found`,
HttpStatus.NOT_FOUND,
);
}
try {
// Get all spaces related to the community, including the parent-child relations
const spaces = await this.spaceRepository.find({
where: { community: { uuid: communityUuid } },
relations: ['parent', 'children'], // Include parent and children relations
});
// Organize spaces into a hierarchical structure
const spaceHierarchy = this.buildSpaceHierarchy(spaces);
return new SuccessResponseDto({
message: `Spaces in community ${communityUuid} successfully fetched in hierarchy`,
data: spaceHierarchy,
statusCode: HttpStatus.OK,
});
} catch (error) {
throw new HttpException(
'An error occurred while fetching the spaces',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async findOne(spaceUuid: string): Promise<BaseResponseDto> {
try {
const space = await this.spaceRepository.findOne({
where: {
uuid: spaceUuid,
},
});
// If space is not found, throw a NotFoundException
if (!space) {
throw new HttpException(
`Space with UUID ${spaceUuid} not found`,
HttpStatus.NOT_FOUND,
);
}
return new SuccessResponseDto({
message: `Space with ID ${spaceUuid} successfully fetched`,
data: space,
});
} catch (error) {
if (error instanceof HttpException) {
throw error; // If it's an HttpException, rethrow it
} else {
throw new HttpException(
'An error occurred while deleting the community',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
async delete(
spaceUuid: string,
communityUuid: string,
): Promise<BaseResponseDto> {
try {
// First, check if the community exists
const community = await this.communityRepository.findOne({
where: { uuid: communityUuid },
});
if (!community) {
throw new HttpException(
`Community with ID ${communityUuid} not found`,
HttpStatus.NOT_FOUND,
);
}
// Check if the space exists
const space = await this.spaceRepository.findOne({
where: { uuid: spaceUuid, community: { uuid: communityUuid } },
});
if (!space) {
throw new HttpException(
`Space with ID ${spaceUuid} not found`,
HttpStatus.NOT_FOUND,
);
}
// Delete the space
await this.spaceRepository.remove(space);
return new SuccessResponseDto({
message: `Space with ID ${spaceUuid} successfully deleted`,
statusCode: HttpStatus.OK,
});
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
'An error occurred while deleting the space',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async updateSpace(
spaceUuid: string,
communityId: string,
updateSpaceDto: AddSpaceDto,
): Promise<BaseResponseDto> {
try {
// First, check if the community exists
const community = await this.communityRepository.findOne({
where: { uuid: communityId },
});
if (!community) {
throw new HttpException(
`Community with ID ${communityId} not found`,
HttpStatus.NOT_FOUND,
);
}
// Check if the space exists
const space = await this.spaceRepository.findOne({
where: { uuid: spaceUuid, community: { uuid: communityId } },
});
if (!space) {
throw new HttpException(
`Space with ID ${spaceUuid} not found`,
HttpStatus.NOT_FOUND,
);
}
// If a parentId is provided, check if the parent exists
const { parentUuid } = updateSpaceDto;
let parent: SpaceEntity | null = null;
if (parentUuid) {
parent = await this.spaceRepository.findOne({
where: { uuid: parentUuid, community: { uuid: communityId } },
});
// If the parent doesn't exist, throw a 404 error
if (!parent) {
throw new HttpException(
`Parent space with ID ${parentUuid} not found`,
HttpStatus.NOT_FOUND,
);
}
// Set the parent of the current space
space.parent = parent;
} else {
space.parent = null; // If no parent is provided, clear the parent
}
// Update other space properties from updateSpaceDto
Object.assign(space, updateSpaceDto);
// Save the updated space
await this.spaceRepository.save(space);
return new SuccessResponseDto({
message: `Space with ID ${spaceUuid} successfully updated`,
data: space,
statusCode: HttpStatus.OK,
});
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
'An error occurred while updating the space',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async getSpacesHierarchyForSpace(
spaceUuid: string,
): Promise<BaseResponseDto> {
const space = await this.spaceRepository.findOne({
where: { uuid: spaceUuid },
});
// If the space doesn't exist, throw a 404 error
if (!space) {
throw new HttpException(
`Space with ID ${spaceUuid} not found`,
HttpStatus.NOT_FOUND,
);
}
try {
// Get all spaces that are children of the provided space, including the parent-child relations
const spaces = await this.spaceRepository.find({
where: { parent: { uuid: spaceUuid } },
relations: ['parent', 'children'], // Include parent and children relations
});
// Organize spaces into a hierarchical structure
const spaceHierarchy = this.buildSpaceHierarchy(spaces);
return new SuccessResponseDto({
message: `Spaces under space ${spaceUuid} successfully fetched in hierarchy`,
data: spaceHierarchy,
statusCode: HttpStatus.OK,
});
} catch (error) {
throw new HttpException(
'An error occurred while fetching the spaces under the space',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async getSpaceInvitationCode(spaceUuid: string): Promise<any> {
try {
const invitationCode = generateRandomString(6);
const space = await this.spaceRepository.findOne({
where: {
uuid: spaceUuid,
},
});
if (!space) {
throw new HttpException(
`Space with ID ${spaceUuid} not found`,
HttpStatus.NOT_FOUND,
);
}
space.invitationCode = invitationCode;
await this.spaceRepository.save(space);
return new SuccessResponseDto({
message: `Invitation code has been successfuly added to the space`,
data: space,
});
} catch (err) {
if (err instanceof BadRequestException) {
throw err;
} else {
throw new HttpException('Space not found', HttpStatus.NOT_FOUND);
}
}
}
private buildSpaceHierarchy(spaces: SpaceEntity[]): SpaceEntity[] {
const map = new Map<string, SpaceEntity>();
// Step 1: Create a map of spaces by UUID
spaces.forEach((space) => {
map.set(
space.uuid,
this.spaceRepository.create({
...space,
children: [], // Add children if needed
}),
);
});
// Step 2: Organize the hierarchy
const rootSpaces: SpaceEntity[] = [];
spaces.forEach((space) => {
if (space.parent && space.parent.uuid) {
const parent = map.get(space.parent.uuid);
parent?.children?.push(map.get(space.uuid));
} else {
rootSpaces.push(map.get(space.uuid));
}
});
return rootSpaces; // Return the root spaces with children nested within them
}
async validateCommunityAndSpace(communityUuid: string, spaceUuid: string) {
const community = await this.communityRepository.findOne({
where: { uuid: communityUuid },
});
if (!community) {
this.throwNotFound('Community', communityUuid);
}
const space = await this.spaceRepository.findOne({
where: { uuid: spaceUuid, community: { uuid: communityUuid } },
relations: ['devices', 'devices.productDevice'],
});
if (!space) {
this.throwNotFound('Space', spaceUuid);
}
return space;
}
private throwNotFound(entity: string, uuid: string) {
throw new HttpException(
`${entity} with ID ${uuid} not found`,
HttpStatus.NOT_FOUND,
);
}
}

View File

@ -0,0 +1,2 @@
export * from './subspace.service';
export * from './subspace-device.service';

View File

@ -0,0 +1,181 @@
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { DeviceRepository } from '@app/common/modules/device/repositories';
import {
SpaceRepository,
SubspaceRepository,
} from '@app/common/modules/space/repositories';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { DeviceSubSpaceParam, GetSubSpaceParam } from '../../dtos';
import { CommunityRepository } from '@app/common/modules/community/repositories';
import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter';
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
import { ProductRepository } from '@app/common/modules/product/repositories';
import { GetDeviceDetailsInterface } from '../../../device/interfaces/get.device.interface';
@Injectable()
export class SubspaceDeviceService {
constructor(
private readonly spaceRepository: SpaceRepository,
private readonly communityRepository: CommunityRepository,
private readonly subspaceRepository: SubspaceRepository,
private readonly deviceRepository: DeviceRepository,
private readonly tuyaService: TuyaService,
private readonly productRepository: ProductRepository,
) {}
async listDevicesInSubspace(
params: GetSubSpaceParam,
): Promise<BaseResponseDto> {
const { subSpaceUuid, spaceUuid, communityUuid } = params;
await this.validateCommunityAndSpace(communityUuid, spaceUuid);
const subspace = await this.findSubspaceWithDevices(subSpaceUuid);
const detailedDevices = await Promise.all(
subspace.devices.map(async (device) => {
const tuyaDetails = await this.getDeviceDetailsByDeviceIdTuya(
device.deviceTuyaUuid,
);
return {
uuid: device.uuid,
deviceTuyaUuid: device.deviceTuyaUuid,
productUuid: device.productDevice.uuid,
productType: device.productDevice.prodType,
isActive: device.isActive,
createdAt: device.createdAt,
updatedAt: device.updatedAt,
...tuyaDetails,
};
}),
);
return new SuccessResponseDto({
data: detailedDevices,
message: 'Successfully retrieved list of devices',
});
}
async associateDeviceToSubspace(params: DeviceSubSpaceParam) {
const { subSpaceUuid, deviceUuid, spaceUuid, communityUuid } = params;
await this.validateCommunityAndSpace(communityUuid, spaceUuid);
const subspace = await this.findSubspace(subSpaceUuid);
const device = await this.findDevice(deviceUuid);
device.subspace = subspace;
await this.deviceRepository.save(device);
return new SuccessResponseDto({
data: device,
message: 'Successfully associated device to subspace',
});
}
async disassociateDeviceFromSubspace(params: DeviceSubSpaceParam) {
const { subSpaceUuid, deviceUuid, spaceUuid, communityUuid } = params;
await this.validateCommunityAndSpace(communityUuid, spaceUuid);
await this.findSubspace(subSpaceUuid);
const device = await this.findDevice(deviceUuid);
device.subspace = null;
await this.deviceRepository.save(device);
return new SuccessResponseDto({
data: device,
message: 'Successfully dissociated device from subspace',
});
}
// Helper method to validate community and space
private async validateCommunityAndSpace(
communityUuid: string,
spaceUuid: string,
) {
const community = await this.communityRepository.findOne({
where: { uuid: communityUuid },
});
if (!community) {
this.throwNotFound('Community', communityUuid);
}
const space = await this.spaceRepository.findOne({
where: { uuid: spaceUuid, community: { uuid: communityUuid } },
});
if (!space) {
this.throwNotFound('Space', spaceUuid);
}
return space;
}
// Helper method to find subspace with devices relation
private async findSubspaceWithDevices(subSpaceUuid: string) {
const subspace = await this.subspaceRepository.findOne({
where: { uuid: subSpaceUuid },
relations: ['devices', 'devices.productDevice'],
});
if (!subspace) {
this.throwNotFound('Subspace', subSpaceUuid);
}
return subspace;
}
private async findSubspace(subSpaceUuid: string) {
const subspace = await this.subspaceRepository.findOne({
where: { uuid: subSpaceUuid },
});
if (!subspace) {
this.throwNotFound('Subspace', subSpaceUuid);
}
return subspace;
}
private async findDevice(deviceUuid: string) {
const device = await this.deviceRepository.findOne({
where: { uuid: deviceUuid },
});
if (!device) {
this.throwNotFound('Device', deviceUuid);
}
return device;
}
private throwNotFound(entity: string, uuid: string) {
throw new HttpException(
`${entity} with ID ${uuid} not found`,
HttpStatus.NOT_FOUND,
);
}
private async getDeviceDetailsByDeviceIdTuya(
deviceId: string,
): Promise<GetDeviceDetailsInterface> {
try {
const tuyaDeviceDetails =
await this.tuyaService.getDeviceDetails(deviceId);
const camelCaseResponse = convertKeysToCamelCase(tuyaDeviceDetails);
const product = await this.productRepository.findOne({
where: {
prodId: camelCaseResponse.productId,
},
});
const { uuid, ...rest } = camelCaseResponse;
return {
...rest,
productUuid: product?.uuid,
} as GetDeviceDetailsInterface;
} catch (error) {
throw new HttpException(
'Error fetching device details from Tuya',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}

Some files were not shown because too many files have changed in this diff Show More