import { ORPHAN_COMMUNITY_NAME, ORPHAN_SPACE_NAME, } from '@app/common/constants/orphan-constant'; import { BaseResponseDto } from '@app/common/dto/base.response.dto'; import { PageResponse } from '@app/common/dto/pagination.response.dto'; import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; import { ExtendedTypeORMCustomModelFindAllQuery, TypeORMCustomModel, } from '@app/common/models/typeOrmCustom.model'; import { CommunityDto } from '@app/common/modules/community/dtos'; import { CommunityEntity } from '@app/common/modules/community/entities'; import { CommunityRepository } from '@app/common/modules/community/repositories'; import { DeviceEntity } from '@app/common/modules/device/entities'; import { ProjectRepository } from '@app/common/modules/project/repositiories'; import { SpaceRepository } from '@app/common/modules/space'; import { SpaceEntity } from '@app/common/modules/space/entities/space.entity'; import { addSpaceUuidToDevices } from '@app/common/util/device-utils'; import { HttpException, HttpStatus, Injectable, NotFoundException, } from '@nestjs/common'; import { SpaceService } from 'src/space/services'; import { QueryRunner, SelectQueryBuilder } from 'typeorm'; import { AddCommunityDto, GetCommunityParams, ProjectParam } from '../dtos'; import { UpdateCommunityNameDto } from '../dtos/update.community.dto'; @Injectable() export class CommunityService { constructor( private readonly communityRepository: CommunityRepository, private readonly projectRepository: ProjectRepository, private readonly spaceService: SpaceService, private readonly tuyaService: TuyaService, private readonly spaceRepository: SpaceRepository, ) {} async createCommunity( param: ProjectParam, dto: AddCommunityDto, ): Promise { const { name, description } = dto; const project = await this.validateProject(param.projectUuid); await this.validateName(name); // Create the new community entity const community = this.communityRepository.create({ name: name, description: description, project: project, }); // 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); } } async getCommunityById( params: GetCommunityParams, queryRunner?: QueryRunner, ): Promise { const { communityUuid, projectUuid } = params; await this.validateProject(projectUuid); const communityRepository = queryRunner?.manager.getRepository(CommunityEntity) || this.communityRepository; const community = await 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( { projectUuid }: ProjectParam, pageable: Partial, ): Promise { try { const project = await this.validateProject(projectUuid); /** * TODO: removing this breaks the code (should be fixed when refactoring @see TypeORMCustomModel */ pageable.where = {}; let qb: undefined | SelectQueryBuilder = undefined; qb = this.communityRepository .createQueryBuilder('c') .leftJoin('c.spaces', 's', 's.disabled = false') .where('c.project = :projectUuid', { projectUuid }) .andWhere(`c.name != '${ORPHAN_COMMUNITY_NAME}-${project.name}'`) .orderBy('c.createdAt', 'DESC') .distinct(true); if (pageable.search) { qb.andWhere( `c.name ILIKE '%${pageable.search}%' OR s.space_name ILIKE '%${pageable.search}%'`, ); } const customModel = TypeORMCustomModel(this.communityRepository); const { baseResponseDto, paginationResponseDto } = await customModel.findAll({ ...pageable, modelName: 'community' }, qb); // todo: refactor this to minimize the number of queries if (pageable.includeSpaces) { const communitiesWithSpaces = await Promise.all( baseResponseDto.data.map(async (community: CommunityDto) => { const spaces = await this.spaceService.getSpacesHierarchyForCommunity( { communityUuid: community.uuid, projectUuid: projectUuid, }, { onlyWithDevices: false, }, ); return { ...community, spaces: spaces.data, }; }), ); baseResponseDto.data = communitiesWithSpaces; } return new PageResponse( baseResponseDto, paginationResponseDto, ); } catch (error) { // Generic error handling throw new HttpException( error.message || 'An error occurred while fetching communities.', HttpStatus.INTERNAL_SERVER_ERROR, ); } } async getCommunitiesV2( { projectUuid }: ProjectParam, { search, includeSpaces, ...pageable }: Partial, ) { try { const project = await this.validateProject(projectUuid); let qb: undefined | SelectQueryBuilder = undefined; qb = this.communityRepository .createQueryBuilder('c') .where('c.project = :projectUuid', { projectUuid }) .andWhere(`c.name != '${ORPHAN_COMMUNITY_NAME}-${project.name}'`) .distinct(true); if (includeSpaces) { qb.leftJoinAndSelect( 'c.spaces', 'space', 'space.disabled = :disabled AND space.spaceName != :orphanSpaceName', { disabled: false, orphanSpaceName: ORPHAN_SPACE_NAME }, ) .leftJoinAndSelect('space.parent', 'parent') .leftJoinAndSelect( 'space.children', 'children', 'children.disabled = :disabled', { disabled: false }, ); // .leftJoinAndSelect('space.spaceModel', 'spaceModel') } if (search) { qb.andWhere( `c.name ILIKE :search ${includeSpaces ? 'OR space.space_name ILIKE :search' : ''}`, { search }, ); } const customModel = TypeORMCustomModel(this.communityRepository); const { baseResponseDto, paginationResponseDto } = await customModel.findAll({ ...pageable, modelName: 'community' }, qb); if (includeSpaces) { baseResponseDto.data = baseResponseDto.data.map((community) => ({ ...community, spaces: this.spaceService.buildSpaceHierarchy(community.spaces || []), })); } return new PageResponse( baseResponseDto, paginationResponseDto, ); } catch (error) { // Generic error handling if (error instanceof HttpException) { throw error; } throw new HttpException( error.message || 'An error occurred while fetching communities.', HttpStatus.INTERNAL_SERVER_ERROR, ); } } async updateCommunity( params: GetCommunityParams, updateCommunityDto: UpdateCommunityNameDto, ): Promise { const { communityUuid, projectUuid } = params; const project = await this.validateProject(projectUuid); 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, ); } if (community.name === `${ORPHAN_COMMUNITY_NAME}-${project.name}`) { throw new HttpException( `Community with ID ${communityUuid} cannot be updated`, HttpStatus.BAD_REQUEST, ); } try { const { name } = updateCommunityDto; if (name != community.name) await this.validateName(name); community.name = name; const updatedCommunity = await this.communityRepository.save(community); return new SuccessResponseDto({ message: 'Success update Community', data: updatedCommunity, }); } 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( `An Internal exception has been occured ${err}`, HttpStatus.INTERNAL_SERVER_ERROR, ); } } } async deleteCommunity(params: GetCommunityParams): Promise { const { communityUuid, projectUuid } = params; const project = await this.validateProject(projectUuid); 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, ); } if (community.name === `${ORPHAN_COMMUNITY_NAME}-${project.name}`) { throw new HttpException( `Community with ID ${communityUuid} cannot be deleted`, HttpStatus.BAD_REQUEST, ); } 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( 'An error occurred while deleting the community', HttpStatus.INTERNAL_SERVER_ERROR, ); } } } private async createTuyaSpace(name: string): Promise { try { const response = await this.tuyaService.createSpace({ name }); return response; } catch (error) { throw new HttpException( 'Failed to create a Tuya space', HttpStatus.INTERNAL_SERVER_ERROR, ); } } async validateProject(uuid: string) { const project = await this.projectRepository.findOne({ where: { uuid }, }); if (!project) { throw new HttpException( `A project with the uuid '${uuid}' doesn't exists.`, HttpStatus.BAD_REQUEST, ); } return project; } private async validateName(name: string) { const existingCommunity = await this.communityRepository.findOneBy({ name, }); if (existingCommunity) { throw new HttpException( `A community with the name '${name}' already exists.`, HttpStatus.BAD_REQUEST, ); } } async getAllDevicesByCommunity( communityUuid: string, ): Promise { const community = await this.communityRepository.findOne({ where: { uuid: communityUuid }, relations: [ 'spaces', 'spaces.children', 'spaces.devices', 'spaces.devices.productDevice', ], }); if (!community) { throw new NotFoundException('Community not found'); } const allDevices: DeviceEntity[] = []; const visitedSpaceUuids = new Set(); // Recursive fetch function with visited check const fetchSpaceDevices = async (space: SpaceEntity) => { if (visitedSpaceUuids.has(space.uuid)) return; visitedSpaceUuids.add(space.uuid); if (space.devices?.length) { allDevices.push(...addSpaceUuidToDevices(space.devices, space.uuid)); } if (space.children?.length) { for (const child of space.children) { const fullChild = await this.spaceRepository.findOne({ where: { uuid: child.uuid }, relations: ['children', 'devices', 'devices.productDevice'], }); if (fullChild) { await fetchSpaceDevices(fullChild); } } } }; for (const space of community.spaces) { await fetchSpaceDevices(space); } return allDevices; } }