Files
backend/src/space/services/subspace/subspace.service.ts
2025-02-04 15:03:47 +04:00

543 lines
15 KiB
TypeScript

import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import {
AddSubspaceDto,
CreateTagDto,
DeleteSubspaceDto,
GetSpaceParam,
GetSubSpaceParam,
ModifySubspaceDto,
} from '../../dtos';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import {
TypeORMCustomModel,
TypeORMCustomModelFindAllQuery,
} from '@app/common/models/typeOrmCustom.model';
import { PageResponse } from '@app/common/dto/pagination.response.dto';
import { SubspaceDto } from '@app/common/modules/space/dtos';
import { In, QueryRunner } from 'typeorm';
import {
SpaceEntity,
SubspaceEntity,
} from '@app/common/modules/space/entities';
import { SubspaceModelEntity } from '@app/common/modules/space-model';
import { ValidationService } from '../space-validation.service';
import { SubspaceRepository } from '@app/common/modules/space/repositories/subspace.repository';
import { TagService } from '../tag';
import { ModifyAction } from '@app/common/constants/modify-action.enum';
import { SubspaceDeviceService } from './subspace-device.service';
import { ModifyTagDto } from 'src/space/dtos/tag/modify-tag.dto';
@Injectable()
export class SubSpaceService {
constructor(
private readonly subspaceRepository: SubspaceRepository,
private readonly validationService: ValidationService,
private readonly tagService: TagService,
public readonly deviceService: SubspaceDeviceService,
) {}
async createSubspaces(
subspaceData: Array<{
subspaceName: string;
space: SpaceEntity;
subSpaceModel?: SubspaceModelEntity;
}>,
queryRunner: QueryRunner,
): Promise<SubspaceEntity[]> {
try {
const subspaceNames = subspaceData.map((data) => data.subspaceName);
await this.checkExistingNamesInSpace(
subspaceNames,
subspaceData[0].space,
);
const subspaces = subspaceData.map((data) =>
queryRunner.manager.create(this.subspaceRepository.target, data),
);
return await queryRunner.manager.save(subspaces);
} catch (error) {
throw new HttpException(
`An unexpected error occurred while creating subspaces. ${error}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async createSubSpaceFromModel(
subspaceModels: SubspaceModelEntity[],
space: SpaceEntity,
queryRunner: QueryRunner,
): Promise<void> {
if (!subspaceModels?.length) return;
const subspaceData = subspaceModels.map((subSpaceModel) => ({
subspaceName: subSpaceModel.subspaceName,
space,
subSpaceModel,
}));
const subspaces = await this.createSubspaces(subspaceData, queryRunner);
await Promise.all(
subspaceModels.map((model, index) =>
this.tagService.createTagsFromModel(
queryRunner,
model.tags || [],
null,
subspaces[index],
),
),
);
}
async createSubspacesFromDto(
addSubspaceDtos: AddSubspaceDto[],
space: SpaceEntity,
queryRunner: QueryRunner,
otherTags?: CreateTagDto[],
): Promise<SubspaceEntity[]> {
try {
this.checkForDuplicateNames(
addSubspaceDtos.map(({ subspaceName }) => subspaceName),
);
const subspaceData = addSubspaceDtos.map((dto) => ({
subspaceName: dto.subspaceName,
space,
}));
const subspaces = await this.createSubspaces(subspaceData, queryRunner);
await Promise.all(
addSubspaceDtos.map(async (dto, index) => {
const otherDtoTags = addSubspaceDtos
.filter((_, i) => i !== index)
.flatMap((otherDto) => otherDto.tags || []);
const subspace = subspaces[index];
if (dto.tags?.length) {
subspace.tags = await this.tagService.createTags(
dto.tags,
queryRunner,
null,
subspace,
[...(otherTags || []), ...otherDtoTags],
);
}
}),
);
return subspaces;
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
'Failed to save subspaces due to an unexpected error.',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async createSubspace(
addSubspaceDto: AddSubspaceDto,
params: GetSpaceParam,
): Promise<BaseResponseDto> {
const space =
await this.validationService.validateSpaceWithinCommunityAndProject(
params.communityUuid,
params.projectUuid,
params.spaceUuid,
);
try {
await this.checkExistingNamesInSpace(
[addSubspaceDto.subspaceName],
space,
);
const newSubspace = this.subspaceRepository.create({
...addSubspaceDto,
space,
});
await this.subspaceRepository.save(newSubspace);
return new SuccessResponseDto({
statusCode: HttpStatus.CREATED,
data: newSubspace,
message: 'Subspace created successfully',
});
} catch (error) {
throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
async list(
params: GetSpaceParam,
pageable: Partial<TypeORMCustomModelFindAllQuery>,
): Promise<BaseResponseDto> {
const { communityUuid, spaceUuid, projectUuid } = params;
await this.validationService.checkCommunityAndProjectSpaceExistence(
communityUuid,
projectUuid,
spaceUuid,
);
try {
pageable.modelName = 'subspace';
pageable.where = { space: { uuid: spaceUuid } };
const customModel = TypeORMCustomModel(this.subspaceRepository);
const { baseResponseDto, paginationResponseDto } =
await customModel.findAll(pageable);
return new PageResponse<SubspaceDto>(
baseResponseDto,
paginationResponseDto,
);
} catch (error) {
throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
async updateSubSpace(
params: GetSubSpaceParam,
updateSubSpaceDto: AddSubspaceDto,
): Promise<BaseResponseDto> {
const { spaceUuid, communityUuid, subSpaceUuid, projectUuid } = params;
await this.validationService.checkCommunityAndProjectSpaceExistence(
communityUuid,
projectUuid,
spaceUuid,
);
const subSpace = await this.subspaceRepository.findOne({
where: { uuid: subSpaceUuid },
});
if (!subSpace) {
throw new HttpException(
`SubSpace with ID ${subSpaceUuid} not found`,
HttpStatus.NOT_FOUND,
);
}
try {
Object.assign(subSpace, updateSubSpaceDto);
await this.subspaceRepository.save(subSpace);
return new SuccessResponseDto({
message: `Subspace with ID ${subSpaceUuid} successfully updated`,
data: subSpace,
statusCode: HttpStatus.OK,
});
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
'An error occurred while updating the subspace',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async delete(params: GetSubSpaceParam): Promise<BaseResponseDto> {
const { spaceUuid, communityUuid, subSpaceUuid, projectUuid } = params;
await this.validationService.checkCommunityAndProjectSpaceExistence(
communityUuid,
projectUuid,
spaceUuid,
);
const subspace = await this.subspaceRepository.findOne({
where: { uuid: subSpaceUuid },
});
if (!subspace) {
throw new HttpException(
`Subspace with ID ${subSpaceUuid} not found`,
HttpStatus.NOT_FOUND,
);
}
try {
await this.subspaceRepository.remove(subspace);
return new SuccessResponseDto({
message: `Subspace with ID ${subSpaceUuid} successfully deleted`,
statusCode: HttpStatus.OK,
});
} catch (error) {
throw new HttpException(
'An error occurred while deleting the subspace',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async deleteSubspaces(
deleteDtos: DeleteSubspaceDto[],
queryRunner: QueryRunner,
) {
const deleteResults: { uuid: string }[] = [];
for (const dto of deleteDtos) {
const subspace = await this.findOne(dto.subspaceUuid);
await queryRunner.manager.update(
this.subspaceRepository.target,
{ uuid: dto.subspaceUuid },
{ disabled: true },
);
if (subspace.tags?.length) {
const modifyTagDtos = subspace.tags.map((tag) => ({
uuid: tag.uuid,
action: ModifyAction.DELETE,
}));
await this.tagService.modifyTags(
modifyTagDtos,
queryRunner,
null,
subspace,
);
}
if (subspace.devices)
await this.deviceService.deleteSubspaceDevices(
subspace.devices,
queryRunner,
);
deleteResults.push({ uuid: dto.subspaceUuid });
}
return deleteResults;
}
async modifySubSpace(
subspaceDtos: ModifySubspaceDto[],
queryRunner: QueryRunner,
space?: SpaceEntity,
) {
for (const subspace of subspaceDtos) {
switch (subspace.action) {
case ModifyAction.ADD:
await this.handleAddAction(subspace, space, queryRunner);
break;
case ModifyAction.UPDATE:
await this.handleUpdateAction(subspace, queryRunner);
break;
case ModifyAction.DELETE:
await this.handleDeleteAction(subspace, queryRunner);
break;
default:
throw new HttpException(
`Invalid action "${subspace.action}".`,
HttpStatus.BAD_REQUEST,
);
}
}
}
async unlinkModels(
subspaces: SubspaceEntity[],
queryRunner: QueryRunner,
): Promise<void> {
if (!subspaces || subspaces.length === 0) {
return;
}
try {
const allTags = subspaces.flatMap((subSpace) => {
subSpace.subSpaceModel = null;
return subSpace.tags || [];
});
await this.tagService.unlinkModels(allTags, queryRunner);
await queryRunner.manager.save(subspaces);
} catch (error) {
if (error instanceof HttpException) throw error;
throw new HttpException(
`Failed to unlink subspace models: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async getOne(params: GetSubSpaceParam): Promise<BaseResponseDto> {
await this.validationService.checkCommunityAndProjectSpaceExistence(
params.communityUuid,
params.projectUuid,
params.spaceUuid,
);
const subspace = await this.findOne(params.subSpaceUuid);
return new SuccessResponseDto({
message: `Successfully retrieved subspace`,
data: subspace,
});
}
private async handleAddAction(
subspace: ModifySubspaceDto,
space: SpaceEntity,
queryRunner: QueryRunner,
): Promise<SubspaceEntity> {
const createTagDtos: CreateTagDto[] =
subspace.tags?.map((tag) => ({
tag: tag.tag as string,
uuid: tag.uuid,
productUuid: tag.productUuid as string,
})) || [];
const subSpace = await this.createSubspacesFromDto(
[{ subspaceName: subspace.subspaceName, tags: createTagDtos }],
space,
queryRunner,
);
return subSpace[0];
}
private async handleUpdateAction(
modifyDto: ModifySubspaceDto,
queryRunner: QueryRunner,
): Promise<void> {
const subspace = await this.findOne(modifyDto.uuid);
await this.update(
queryRunner,
subspace,
modifyDto.subspaceName,
modifyDto.tags,
);
}
async update(
queryRunner: QueryRunner,
subspace: SubspaceEntity,
subspaceName?: string,
modifyTagDto?: ModifyTagDto[],
) {
await this.updateSubspaceName(queryRunner, subspace, subspaceName);
if (modifyTagDto?.length) {
await this.tagService.modifyTags(
modifyTagDto,
queryRunner,
null,
subspace,
);
}
}
async handleDeleteAction(
modifyDto: ModifySubspaceDto,
queryRunner: QueryRunner,
): Promise<void> {
const subspace = await this.findOne(modifyDto.uuid);
await queryRunner.manager.update(
this.subspaceRepository.target,
{ uuid: subspace.uuid },
{ disabled: true },
);
if (subspace.tags?.length) {
const modifyTagDtos: CreateTagDto[] = subspace.tags.map((tag) => ({
uuid: tag.uuid,
action: ModifyAction.ADD,
tag: tag.tag,
productUuid: tag.product.uuid,
}));
await this.tagService.moveTags(
modifyTagDtos,
queryRunner,
subspace.space,
null,
);
}
if (subspace.devices.length > 0) {
await this.deviceService.deleteSubspaceDevices(
subspace.devices,
queryRunner,
);
}
}
private async findOne(subspaceUuid: string): Promise<SubspaceEntity> {
const subspace = await this.subspaceRepository.findOne({
where: { uuid: subspaceUuid },
relations: ['tags', 'space', 'devices', 'tags.product', 'tags.device'],
});
if (!subspace) {
throw new HttpException(
`SubspaceModel with UUID ${subspaceUuid} not found.`,
HttpStatus.NOT_FOUND,
);
}
return subspace;
}
async updateSubspaceName(
queryRunner: QueryRunner,
subSpace: SubspaceEntity,
subspaceName?: string,
): Promise<void> {
if (subspaceName) {
subSpace.subspaceName = subspaceName;
await queryRunner.manager.save(subSpace);
}
}
private async checkForDuplicateNames(names: string[]): Promise<void> {
const seenNames = new Set<string>();
const duplicateNames = new Set<string>();
for (const name of names) {
if (!seenNames.add(name)) {
duplicateNames.add(name);
}
}
if (duplicateNames.size > 0) {
throw new HttpException(
`Duplicate subspace names found: ${[...duplicateNames].join(', ')}`,
HttpStatus.CONFLICT,
);
}
}
private async checkExistingNamesInSpace(
names: string[],
space: SpaceEntity,
): Promise<void> {
const existingNames = await this.subspaceRepository.find({
select: ['subspaceName'],
where: {
subspaceName: In(names),
space: {
uuid: space.uuid,
},
disabled: false,
},
});
if (existingNames.length > 0) {
const existingNamesList = existingNames
.map((e) => e.subspaceName)
.join(', ');
throw new HttpException(
`Subspace names already exist in the space: ${existingNamesList}`,
HttpStatus.BAD_REQUEST,
);
}
}
private async validateName(
names: string[],
space: SpaceEntity,
): Promise<void> {
await this.checkForDuplicateNames(names);
await this.checkExistingNamesInSpace(names, space);
}
}