Files
backend/src/community/services/community.service.ts

345 lines
10 KiB
TypeScript

import { ORPHAN_COMMUNITY_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 { 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<BaseResponseDto> {
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): Promise<BaseResponseDto> {
const { communityUuid, projectUuid } = params;
await this.validateProject(projectUuid);
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(
{ projectUuid }: ProjectParam,
pageable: Partial<ExtendedTypeORMCustomModelFindAllQuery>,
): Promise<BaseResponseDto> {
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<CommunityEntity> = 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}'`)
.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<CommunityDto>(
baseResponseDto,
paginationResponseDto,
);
} catch (error) {
// Generic error handling
throw new HttpException(
error.message || 'An error occurred while fetching communities.',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async updateCommunity(
params: GetCommunityParams,
updateCommunityDto: UpdateCommunityNameDto,
): Promise<BaseResponseDto> {
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<CommunityDto>({
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<BaseResponseDto> {
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<string> {
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<DeviceEntity[]> {
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<string>();
// 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;
}
}