Files
backend/src/space/services/space.service.ts
2025-03-06 12:33:31 +03:00

755 lines
22 KiB
TypeScript

import {
InviteSpaceRepository,
SpaceRepository,
} from '@app/common/modules/space/repositories';
import {
BadRequestException,
HttpException,
HttpStatus,
Injectable,
} from '@nestjs/common';
import {
AddSpaceDto,
AddSubspaceDto,
CommunitySpaceParam,
GetSpaceParam,
UpdateSpaceDto,
} from '../dtos';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { generateRandomString } from '@app/common/helper/randomString';
import { SpaceLinkService } from './space-link';
import { SubSpaceService } from './subspace';
import { DataSource, QueryRunner } from 'typeorm';
import { ValidationService } from './space-validation.service';
import {
ORPHAN_COMMUNITY_NAME,
ORPHAN_SPACE_NAME,
} from '@app/common/constants/orphan-constant';
import { CommandBus } from '@nestjs/cqrs';
import { TagService } from './tag';
import { TagService as NewTagService } from 'src/tags/services/tags.service';
import { SpaceModelService } from 'src/space-model/services';
import { DisableSpaceCommand } from '../commands';
import { GetSpaceDto } from '../dtos/get.space.dto';
import { removeCircularReferences } from '@app/common/helper/removeCircularReferences';
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
import { ProcessTagDto } from 'src/tags/dtos';
import { SpaceProductAllocationService } from './space-product-allocation.service';
import { SubspaceProductAllocationService } from './subspace/subspace-product-allocation.service';
import { SubspaceEntity } from '@app/common/modules/space/entities/subspace/subspace.entity';
@Injectable()
export class SpaceService {
constructor(
private readonly dataSource: DataSource,
private readonly spaceRepository: SpaceRepository,
private readonly inviteSpaceRepository: InviteSpaceRepository,
private readonly spaceLinkService: SpaceLinkService,
private readonly subSpaceService: SubSpaceService,
private readonly validationService: ValidationService,
private readonly tagService: TagService,
private readonly newTagService: NewTagService,
private readonly spaceModelService: SpaceModelService,
private commandBus: CommandBus,
private readonly spaceProductAllocationService: SpaceProductAllocationService,
private readonly subspaceProductAllocationService: SubspaceProductAllocationService,
) {}
async createSpace(
addSpaceDto: AddSpaceDto,
params: CommunitySpaceParam,
): Promise<BaseResponseDto> {
const { parentUuid, direction, spaceModelUuid, subspaces, tags } =
addSpaceDto;
const { communityUuid, projectUuid } = params;
if (addSpaceDto.spaceName === ORPHAN_SPACE_NAME) {
throw new HttpException(
`Name ${ORPHAN_SPACE_NAME} cannot be used`,
HttpStatus.BAD_REQUEST,
);
}
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
const { community } =
await this.validationService.validateCommunityAndProject(
communityUuid,
projectUuid,
);
this.validateSpaceCreation(addSpaceDto, spaceModelUuid);
const parent = parentUuid
? await this.validationService.validateSpace(parentUuid)
: null;
const spaceModel = spaceModelUuid
? await this.validationService.validateSpaceModel(spaceModelUuid)
: null;
try {
const space = queryRunner.manager.create(SpaceEntity, {
...addSpaceDto,
spaceModel,
parent: parentUuid ? parent : null,
community,
});
const newSpace = await queryRunner.manager.save(space);
const subspaceTags =
this.subSpaceService.extractTagsFromSubspace(subspaces);
const allTags = [...tags, ...subspaceTags];
this.validateUniqueTags(allTags);
if (spaceModelUuid) {
const hasDependencies = subspaces?.length > 0 || tags?.length > 0;
if (!hasDependencies && !newSpace.spaceModel) {
await this.spaceModelService.linkToSpace(
newSpace,
spaceModel,
queryRunner,
);
} else if (hasDependencies) {
throw new HttpException(
`Space cannot be linked to a model because it has existing dependencies (subspaces or tags).`,
HttpStatus.BAD_REQUEST,
);
}
}
await Promise.all([
direction && parent
? this.spaceLinkService.saveSpaceLink(
parent.uuid,
newSpace.uuid,
direction,
queryRunner,
)
: Promise.resolve(),
subspaces?.length
? this.createSubspaces(
subspaces,
newSpace,
queryRunner,
tags,
projectUuid,
)
: Promise.resolve(),
tags?.length
? this.createTags(tags, projectUuid, queryRunner, newSpace)
: Promise.resolve(),
]);
await queryRunner.commitTransaction();
return new SuccessResponseDto({
statusCode: HttpStatus.CREATED,
data: JSON.parse(JSON.stringify(newSpace, removeCircularReferences())),
message: 'Space created successfully',
});
} catch (error) {
await queryRunner.rollbackTransaction();
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR);
} finally {
await queryRunner.release();
}
}
private validateUniqueTags(allTags: ProcessTagDto[]) {
const tagUuidSet = new Set<string>();
const tagNameProductSet = new Set<string>();
for (const tag of allTags) {
if (tag.uuid) {
if (tagUuidSet.has(tag.uuid)) {
throw new HttpException(
`Duplicate tag UUID found: ${tag.uuid}`,
HttpStatus.BAD_REQUEST,
);
}
tagUuidSet.add(tag.uuid);
} else {
const tagKey = `${tag.name}-${tag.productUuid}`;
if (tagNameProductSet.has(tagKey)) {
throw new HttpException(
`Duplicate tag found with name "${tag.name}" and product "${tag.productUuid}".`,
HttpStatus.BAD_REQUEST,
);
}
tagNameProductSet.add(tagKey);
}
}
}
async createFromModel(
spaceModelUuid: string,
queryRunner: QueryRunner,
space: SpaceEntity,
) {
try {
const spaceModel =
await this.spaceModelService.validateSpaceModel(spaceModelUuid);
space.spaceModel = spaceModel;
await queryRunner.manager.save(SpaceEntity, space);
await this.subSpaceService.createSubSpaceFromModel(
spaceModel.subspaceModels,
space,
queryRunner,
);
await this.tagService.createTagsFromModel(
queryRunner,
spaceModel.tags,
space,
null,
);
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
'An error occurred while creating the space from space model',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async getSpacesHierarchyForCommunity(
params: CommunitySpaceParam,
getSpaceDto?: GetSpaceDto,
): Promise<BaseResponseDto> {
const { communityUuid, projectUuid } = params;
const { onlyWithDevices } = getSpaceDto;
await this.validationService.validateCommunityAndProject(
communityUuid,
projectUuid,
);
try {
const queryBuilder = this.spaceRepository
.createQueryBuilder('space')
.leftJoinAndSelect('space.parent', 'parent')
.leftJoinAndSelect(
'space.children',
'children',
'children.disabled = :disabled',
{ disabled: false },
)
.leftJoinAndSelect(
'space.incomingConnections',
'incomingConnections',
'incomingConnections.disabled = :incomingConnectionDisabled',
{ incomingConnectionDisabled: false },
)
.leftJoinAndSelect('space.productAllocations', 'productAllocations')
.leftJoinAndSelect('productAllocations.tags', 'tags')
.leftJoinAndSelect('tags.product', 'tagProduct')
.leftJoinAndSelect(
'space.subspaces',
'subspaces',
'subspaces.disabled = :subspaceDisabled',
{ subspaceDisabled: false },
)
.leftJoinAndSelect(
'subspaces.productAllocations',
'subspaceProductAllocations',
)
.leftJoinAndSelect('subspaceProductAllocations.tags', 'subspaceTags')
.leftJoinAndSelect('subspaceTags.product', 'subspaceTagProduct')
.leftJoinAndSelect('space.spaceModel', 'spaceModel')
.where('space.community_id = :communityUuid', { communityUuid })
.andWhere('space.spaceName != :orphanSpaceName', {
orphanSpaceName: ORPHAN_SPACE_NAME,
})
.andWhere('space.disabled = :disabled', { disabled: false });
if (onlyWithDevices) {
queryBuilder.innerJoin('space.devices', 'devices');
}
const spaces = await queryBuilder.getMany();
const spaceHierarchy = this.buildSpaceHierarchy(spaces);
return new SuccessResponseDto({
message: `Spaces in community ${communityUuid} successfully fetched in hierarchy`,
data: onlyWithDevices ? spaces : spaceHierarchy,
statusCode: HttpStatus.OK,
});
} catch (error) {
throw new HttpException(
`An error occurred while fetching the spaces ${error}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async findOne(params: GetSpaceParam): Promise<BaseResponseDto> {
const { communityUuid, spaceUuid, projectUuid } = params;
try {
await this.validationService.validateCommunityAndProject(
communityUuid,
projectUuid,
);
const queryBuilder = this.spaceRepository
.createQueryBuilder('space')
.leftJoinAndSelect('space.parent', 'parent')
.leftJoinAndSelect(
'space.children',
'children',
'children.disabled = :disabled',
{ disabled: false },
)
.leftJoinAndSelect(
'space.incomingConnections',
'incomingConnections',
'incomingConnections.disabled = :incomingConnectionDisabled',
{ incomingConnectionDisabled: false },
)
.leftJoinAndSelect(
'space.tags',
'tags',
'tags.disabled = :tagDisabled',
{ tagDisabled: false },
)
.leftJoinAndSelect('tags.product', 'tagProduct')
.leftJoinAndSelect(
'space.subspaces',
'subspaces',
'subspaces.disabled = :subspaceDisabled',
{ subspaceDisabled: false },
)
.leftJoinAndSelect(
'subspaces.tags',
'subspaceTags',
'subspaceTags.disabled = :subspaceTagsDisabled',
{ subspaceTagsDisabled: false },
)
.leftJoinAndSelect('subspaceTags.product', 'subspaceTagProduct')
.where('space.community_id = :communityUuid', { communityUuid })
.andWhere('space.spaceName != :orphanSpaceName', {
orphanSpaceName: ORPHAN_SPACE_NAME,
})
.andWhere('space.uuid = :spaceUuid', { spaceUuid })
.andWhere('space.disabled = :disabled', { disabled: false });
const space = await queryBuilder.getOne();
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(params: GetSpaceParam): Promise<BaseResponseDto> {
try {
const { communityUuid, spaceUuid, projectUuid } = params;
const { project } =
await this.validationService.validateCommunityAndProject(
communityUuid,
projectUuid,
);
const space = await this.validationService.validateSpace(spaceUuid);
if (space.spaceName === ORPHAN_SPACE_NAME) {
throw new HttpException(
`space ${ORPHAN_SPACE_NAME} cannot be deleted`,
HttpStatus.BAD_REQUEST,
);
}
const orphanSpace = await this.spaceRepository.findOne({
where: {
community: {
name: `${ORPHAN_COMMUNITY_NAME}-${project.name}`,
},
spaceName: ORPHAN_SPACE_NAME,
},
});
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
await this.spaceProductAllocationService.clearAllAllocations(
spaceUuid,
queryRunner,
);
const subspaces = await queryRunner.manager.find(SubspaceEntity, {
where: { space: { uuid: spaceUuid } },
});
const subspaceUuids = subspaces.map((subspace) => subspace.uuid);
if (subspaceUuids.length > 0) {
await this.subSpaceService.clearSubspaces(subspaceUuids, queryRunner);
}
await this.disableSpace(space, orphanSpace);
await queryRunner.commitTransaction();
} catch (error) {
await queryRunner.rollbackTransaction();
throw error;
} finally {
await queryRunner.release();
}
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 ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async disableSpace(space: SpaceEntity, orphanSpace: SpaceEntity) {
await this.commandBus.execute(
new DisableSpaceCommand({ spaceUuid: space.uuid, orphanSpace }),
);
}
async updateSpace(
params: GetSpaceParam,
updateSpaceDto: UpdateSpaceDto,
): Promise<BaseResponseDto> {
const { communityUuid, spaceUuid, projectUuid } = params;
const queryRunner = this.dataSource.createQueryRunner();
try {
await queryRunner.connect();
await queryRunner.startTransaction();
const project = await this.spaceModelService.validateProject(
params.projectUuid,
);
const space =
await this.validationService.validateSpaceWithinCommunityAndProject(
communityUuid,
projectUuid,
spaceUuid,
);
if (space.spaceName === ORPHAN_SPACE_NAME) {
throw new HttpException(
`Space "${ORPHAN_SPACE_NAME}" cannot be updated`,
HttpStatus.BAD_REQUEST,
);
}
this.updateSpaceProperties(space, updateSpaceDto);
if (updateSpaceDto.spaceModelUuid) {
const spaceModel = await this.validationService.validateSpaceModel(
updateSpaceDto.spaceModelUuid,
);
const hasDependencies =
space.devices?.length > 0 ||
space.subspaces?.length > 0 ||
space.productAllocations?.length > 0;
if (!hasDependencies && !space.spaceModel) {
await this.spaceModelService.linkToSpace(
space,
spaceModel,
queryRunner,
);
} else if (hasDependencies) {
await this.spaceModelService.overwriteSpace(
space,
project,
queryRunner,
);
await this.spaceModelService.linkToSpace(
space,
spaceModel,
queryRunner,
);
}
}
const hasSubspace = updateSpaceDto.subspace?.length > 0;
const hasTags = updateSpaceDto.tags?.length > 0;
if (hasSubspace || hasTags) {
space.spaceModel = null;
await this.tagService.unlinkModels(space.tags, queryRunner);
await this.subSpaceService.unlinkModels(space.subspaces, queryRunner);
}
await queryRunner.manager.save(space);
if (hasSubspace) {
const modifiedSubspaces = this.tagService.getModifiedSubspaces(
updateSpaceDto.tags,
updateSpaceDto.subspace,
);
await this.subSpaceService.modifySubSpace(
modifiedSubspaces,
queryRunner,
space,
projectUuid,
updateSpaceDto.tags,
);
}
if (hasTags) {
const spaceTagsAfterMove = this.tagService.getSubspaceTagsToBeAdded(
updateSpaceDto.tags,
updateSpaceDto.subspace,
);
await this.tagService.modifyTags(
spaceTagsAfterMove,
queryRunner,
space,
);
}
if (updateSpaceDto.tags) {
await this.spaceProductAllocationService.updateSpaceProductAllocations(
updateSpaceDto.tags,
projectUuid,
space,
queryRunner,
updateSpaceDto.subspace,
);
}
await queryRunner.commitTransaction();
return new SuccessResponseDto({
message: `Space with ID ${spaceUuid} successfully updated`,
statusCode: HttpStatus.OK,
});
} catch (error) {
await queryRunner.rollbackTransaction();
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
'An error occurred while updating the space',
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
await queryRunner.release();
}
}
async unlinkSpaceFromModel(
space: SpaceEntity,
queryRunner: QueryRunner,
): Promise<void> {
try {
await queryRunner.manager.update(
this.spaceRepository.target,
{ uuid: space.uuid },
{
spaceModel: null,
},
);
// Unlink subspaces and tags if they exist
if (space.subspaces || space.tags) {
if (space.tags) {
await this.tagService.unlinkModels(space.tags, queryRunner);
}
if (space.subspaces) {
await this.subSpaceService.unlinkModels(space.subspaces, queryRunner);
}
}
} catch (error) {
throw new HttpException(
`Failed to unlink space model: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
private updateSpaceProperties(
space: SpaceEntity,
updateSpaceDto: UpdateSpaceDto,
): void {
const { spaceName, x, y, icon } = updateSpaceDto;
if (spaceName) space.spaceName = spaceName;
if (x) space.x = x;
if (y) space.y = y;
if (icon) space.icon = icon;
}
async getSpacesHierarchyForSpace(
params: GetSpaceParam,
): Promise<BaseResponseDto> {
const { spaceUuid, communityUuid, projectUuid } = params;
await this.validationService.checkCommunityAndProjectSpaceExistence(
communityUuid,
projectUuid,
spaceUuid,
);
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 }, disabled: false },
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 ${error}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async generateSpaceInvitationCode(params: GetSpaceParam): Promise<any> {
const { communityUuid, spaceUuid, projectUuid } = params;
try {
const invitationCode = generateRandomString(6);
const space =
await this.validationService.validateSpaceWithinCommunityAndProject(
communityUuid,
projectUuid,
spaceUuid,
);
await this.inviteSpaceRepository.save({
space: { uuid: spaceUuid },
invitationCode,
});
return new SuccessResponseDto({
message: `Invitation code has been successfuly added to the space`,
data: {
invitationCode,
spaceName: space.spaceName,
spaceUuid: space.uuid,
},
});
} 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;
}
private validateSpaceCreation(
addSpaceDto: AddSpaceDto,
spaceModelUuid?: string,
) {
const hasTagsOrSubspaces =
(addSpaceDto.tags && addSpaceDto.tags.length > 0) ||
(addSpaceDto.subspaces && addSpaceDto.subspaces.length > 0);
if (spaceModelUuid && hasTagsOrSubspaces) {
throw new HttpException(
'For space creation choose either space model or products and subspace',
HttpStatus.CONFLICT,
);
}
}
private async createSubspaces(
subspaces: AddSubspaceDto[],
space: SpaceEntity,
queryRunner: QueryRunner,
tags: ProcessTagDto[],
projectUuid: string,
): Promise<void> {
space.subspaces = await this.subSpaceService.createSubspacesFromDto(
subspaces,
space,
queryRunner,
tags,
projectUuid,
);
}
private async createTags(
tags: ProcessTagDto[],
projectUuid: string,
queryRunner: QueryRunner,
space: SpaceEntity,
): Promise<void> {
const processedTags = await this.newTagService.processTags(
tags,
projectUuid,
queryRunner,
);
await this.spaceProductAllocationService.createSpaceProductAllocations(
space,
processedTags,
queryRunner,
);
}
}