Files
backend/src/space/services/space-product-allocation.service.ts
T
2025-03-07 23:01:38 +04:00

573 lines
17 KiB
TypeScript

import { ModifyAction } from '@app/common/constants/modify-action.enum';
import { ProductEntity } from '@app/common/modules/product/entities';
import { SpaceProductAllocationRepository } from '@app/common/modules/space';
import { SpaceProductAllocationEntity } from '@app/common/modules/space/entities/space-product-allocation.entity';
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
import { NewTagEntity } from '@app/common/modules/tag';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { In, QueryRunner } from 'typeorm';
import { SubspaceProductAllocationEntity } from '@app/common/modules/space/entities/subspace/subspace-product-allocation.entity';
import { ModifyTagDto } from '../dtos/tag/modify-tag.dto';
import { ModifySubspaceDto } from '../dtos';
import { ProcessTagDto } from 'src/tags/dtos';
import { TagService as NewTagService } from 'src/tags/services';
import { SpaceModelProductAllocationEntity } from '@app/common/modules/space-model';
import { DeviceEntity } from '@app/common/modules/device/entities';
import { ProjectEntity } from '@app/common/modules/project/entities';
import { ValidationService } from './space-validation.service';
@Injectable()
export class SpaceProductAllocationService {
constructor(
private readonly tagService: NewTagService,
private readonly spaceProductAllocationRepository: SpaceProductAllocationRepository,
private readonly spaceService: ValidationService,
) {}
async createSpaceProductAllocations(
space: SpaceEntity,
processedTags: NewTagEntity[],
queryRunner: QueryRunner,
modifySubspaces?: ModifySubspaceDto[],
): Promise<void> {
try {
if (!processedTags.length) return;
const productAllocations: SpaceProductAllocationEntity[] = [];
const existingAllocations = new Map<
string,
SpaceProductAllocationEntity
>();
for (const tag of processedTags) {
let isTagNeeded = true;
if (modifySubspaces) {
const relatedSubspaces = await queryRunner.manager.find(
SubspaceProductAllocationEntity,
{
where: {
product: tag.product,
subspace: { space: { uuid: space.uuid } },
tags: { uuid: tag.uuid },
},
relations: ['subspace', 'tags'],
},
);
for (const subspaceWithTag of relatedSubspaces) {
const modifyingSubspace = modifySubspaces.find(
(subspace) =>
subspace.action === ModifyAction.UPDATE &&
subspace.uuid === subspaceWithTag.subspace.uuid,
);
if (
modifyingSubspace &&
modifyingSubspace.tags &&
modifyingSubspace.tags.some(
(subspaceTag) =>
subspaceTag.action === ModifyAction.DELETE &&
subspaceTag.tagUuid === tag.uuid,
)
) {
isTagNeeded = true;
break;
}
}
}
if (isTagNeeded) {
await this.validateTagWithinSpace(queryRunner, tag, space);
let allocation = existingAllocations.get(tag.product.uuid);
if (!allocation) {
allocation = await this.getAllocationByProduct(
tag.product,
space,
queryRunner,
);
if (allocation) {
existingAllocations.set(tag.product.uuid, allocation);
}
}
if (!allocation) {
allocation = this.createNewAllocation(space, tag, queryRunner);
productAllocations.push(allocation);
} else if (!allocation.tags.some((t) => t.uuid === tag.uuid)) {
allocation.tags.push(tag);
await this.saveAllocation(allocation, queryRunner);
}
}
}
if (productAllocations.length > 0) {
await this.saveAllocations(productAllocations, queryRunner);
}
} catch (error) {
throw this.handleError(
error,
'Failed to create space product allocations',
);
}
}
async createAllocationFromModel(
modelAllocations: SpaceModelProductAllocationEntity[],
queryRunner: QueryRunner,
spaces?: SpaceEntity[],
) {
if (!spaces || spaces.length === 0 || !modelAllocations.length) return;
const allocations: SpaceProductAllocationEntity[] = [];
for (const space of spaces) {
for (const modelAllocation of modelAllocations) {
const allocation = queryRunner.manager.create(
SpaceProductAllocationEntity,
{
space,
product: modelAllocation.product,
tags: modelAllocation.tags,
inheritedFromModel: modelAllocation,
},
);
allocations.push(allocation);
}
}
if (allocations.length > 0) {
await queryRunner.manager.save(SpaceProductAllocationEntity, allocations);
}
}
async addTagToAllocationFromModel(
modelAllocation: SpaceModelProductAllocationEntity,
queryRunner: QueryRunner,
tag: NewTagEntity,
spaces?: SpaceEntity[],
) {
try {
if (!spaces || spaces.length === 0 || !modelAllocation) return;
const spaceAllocations = await queryRunner.manager.find(
SpaceProductAllocationEntity,
{
where: { inheritedFromModel: { uuid: modelAllocation.uuid } },
relations: ['tags'],
},
);
if (spaceAllocations.length === 0) return;
for (const allocation of spaceAllocations) {
allocation.tags.push(tag);
}
await queryRunner.manager.save(
SpaceProductAllocationEntity,
spaceAllocations,
);
} catch (error) {
throw new HttpException(
'Failed to add tag to allocation from model',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async updateSpaceProductAllocations(
dtos: ModifyTagDto[],
projectUuid: string,
space: SpaceEntity,
queryRunner: QueryRunner,
modifySubspace?: ModifySubspaceDto[],
): Promise<void> {
try {
await Promise.all([
this.processAddActions(
dtos,
projectUuid,
space,
queryRunner,
modifySubspace,
),
this.processDeleteActions(dtos, queryRunner, space),
]);
} catch (error) {
throw this.handleError(error, 'Error while updating product allocations');
}
}
async unlinkModels(space: SpaceEntity, queryRunner: QueryRunner) {
try {
if (!space.productAllocations || space.productAllocations.length === 0)
return;
const allocationUuids = space.productAllocations.map(
(allocation) => allocation.uuid,
);
await queryRunner.manager.update(
SpaceProductAllocationEntity,
{ uuid: In(allocationUuids) },
{ inheritedFromModel: null },
);
} catch (error) {
throw new HttpException(
'Failed to unlink models',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async propagateDeleteToInheritedAllocations(
queryRunner: QueryRunner,
allocationsToUpdate: SpaceModelProductAllocationEntity[],
tagUuidsToDelete: string[],
project: ProjectEntity,
spaces?: SpaceEntity[],
): Promise<void> {
try {
const inheritedAllocationUpdates: SpaceProductAllocationEntity[] = [];
const inheritedAllocationsToDelete: SpaceProductAllocationEntity[] = [];
for (const allocation of allocationsToUpdate) {
for (const inheritedAllocation of allocation.inheritedSpaceAllocations) {
const updatedInheritedTags = inheritedAllocation.tags.filter(
(tag) => !tagUuidsToDelete.includes(tag.uuid),
);
if (updatedInheritedTags.length === inheritedAllocation.tags.length) {
continue;
}
if (updatedInheritedTags.length === 0) {
inheritedAllocationsToDelete.push(inheritedAllocation);
} else {
inheritedAllocation.tags = updatedInheritedTags;
inheritedAllocationUpdates.push(inheritedAllocation);
}
}
}
if (inheritedAllocationUpdates.length > 0) {
await queryRunner.manager.save(
SpaceProductAllocationEntity,
inheritedAllocationUpdates,
);
}
if (inheritedAllocationsToDelete.length > 0) {
await queryRunner.manager.remove(
SpaceProductAllocationEntity,
inheritedAllocationsToDelete,
);
}
if (spaces && spaces.length > 0) {
await this.moveDevicesToOrphanSpace(
queryRunner,
spaces,
tagUuidsToDelete,
project,
);
}
await queryRunner.manager
.createQueryBuilder()
.delete()
.from('space_product_tags')
.where(
'space_product_allocation_uuid NOT IN (' +
queryRunner.manager
.createQueryBuilder()
.select('allocation.uuid')
.from(SpaceProductAllocationEntity, 'allocation')
.getQuery() +
')',
)
.execute();
} catch (error) {
throw this.handleError(
error,
`Failed to propagate tag deletion to inherited allocations`,
);
}
}
async moveDevicesToOrphanSpace(
queryRunner: QueryRunner,
spaces: SpaceEntity[],
tagUuidsToDelete: string[],
project: ProjectEntity,
): Promise<void> {
try {
const orphanSpace = await this.spaceService.getOrphanSpace(project);
const devicesToMove = await queryRunner.manager
.createQueryBuilder(DeviceEntity, 'device')
.leftJoinAndSelect('device.tag', 'tag')
.where('device.spaceDevice IN (:...spaceUuids)', {
spaceUuids: spaces.map((space) => space.uuid),
})
.andWhere('tag.uuid IN (:...tagUuidsToDelete)', { tagUuidsToDelete })
.getMany();
if (devicesToMove.length === 0) return;
await queryRunner.manager
.createQueryBuilder()
.update(DeviceEntity)
.set({ spaceDevice: orphanSpace })
.where('uuid IN (:...deviceUuids)', {
deviceUuids: devicesToMove.map((device) => device.uuid),
})
.execute();
} catch (error) {
throw this.handleError(error, `Failed to move devices to orphan space`);
}
}
private async processDeleteActions(
dtos: ModifyTagDto[],
queryRunner: QueryRunner,
space: SpaceEntity,
): Promise<SpaceProductAllocationEntity[]> {
try {
if (!dtos || dtos.length === 0) {
throw new Error('No DTOs provided for deletion.');
}
const tagUuidsToDelete = dtos
.filter((dto) => dto.action === ModifyAction.DELETE && dto.tagUuid)
.map((dto) => dto.tagUuid);
if (tagUuidsToDelete.length === 0) return [];
const allocationsToUpdate = await queryRunner.manager.find(
SpaceProductAllocationEntity,
{
where: {
tags: { uuid: In(tagUuidsToDelete) },
space: { uuid: space.uuid },
},
relations: ['tags'],
},
);
if (!allocationsToUpdate || allocationsToUpdate.length === 0) return [];
const deletedAllocations: SpaceProductAllocationEntity[] = [];
const allocationUpdates: SpaceProductAllocationEntity[] = [];
for (const allocation of allocationsToUpdate) {
const updatedTags = allocation.tags.filter(
(tag) => !tagUuidsToDelete.includes(tag.uuid),
);
if (updatedTags.length === allocation.tags.length) {
continue;
}
if (updatedTags.length === 0) {
deletedAllocations.push(allocation);
} else {
allocation.tags = updatedTags;
allocationUpdates.push(allocation);
}
}
if (allocationUpdates.length > 0) {
await queryRunner.manager.save(
SpaceProductAllocationEntity,
allocationUpdates,
);
}
if (deletedAllocations.length > 0) {
await queryRunner.manager.remove(
SpaceProductAllocationEntity,
deletedAllocations,
);
}
await queryRunner.manager
.createQueryBuilder()
.delete()
.from('space_product_tags')
.where(
'space_product_allocation_uuid NOT IN (' +
queryRunner.manager
.createQueryBuilder()
.select('allocation.uuid')
.from(SpaceProductAllocationEntity, 'allocation')
.getQuery() +
')',
)
.execute();
return deletedAllocations;
} catch (error) {
throw this.handleError(error, `Failed to delete tags in space`);
}
}
private async processAddActions(
dtos: ModifyTagDto[],
projectUuid: string,
space: SpaceEntity,
queryRunner: QueryRunner,
modifySubspace?: ModifySubspaceDto[],
): Promise<void> {
const addDtos: ProcessTagDto[] = dtos
.filter((dto) => dto.action === ModifyAction.ADD)
.map((dto) => ({
name: dto.name,
productUuid: dto.productUuid,
uuid: dto.newTagUuid,
}));
if (addDtos.length > 0) {
const processedTags = await this.tagService.processTags(
addDtos,
projectUuid,
queryRunner,
);
await this.createSpaceProductAllocations(
space,
processedTags,
queryRunner,
modifySubspace,
);
}
}
private async validateTagWithinSpace(
queryRunner: QueryRunner,
tag: NewTagEntity,
space: SpaceEntity,
) {
const existingAllocationsForProduct = await queryRunner.manager.find(
SpaceProductAllocationEntity,
{
where: { space, product: tag.product },
relations: ['tags'],
},
);
const existingTagsForProduct = existingAllocationsForProduct.flatMap(
(allocation) => allocation.tags,
);
const isDuplicateTag = existingTagsForProduct.some(
(existingTag) => existingTag.uuid === tag.uuid,
);
if (isDuplicateTag) {
throw new HttpException(
`Tag ${tag.uuid} is already allocated to product ${tag.product.uuid} within this space (${space.uuid}).`,
HttpStatus.BAD_REQUEST,
);
}
}
private async getAllocationByProduct(
product: ProductEntity,
space: SpaceEntity,
queryRunner?: QueryRunner,
): Promise<SpaceProductAllocationEntity | null> {
return queryRunner
? queryRunner.manager.findOne(SpaceProductAllocationEntity, {
where: { space, product: product },
relations: ['tags'],
})
: this.spaceProductAllocationRepository.findOne({
where: { space, product: product },
relations: ['tags'],
});
}
createNewAllocation(
space: SpaceEntity,
tag: NewTagEntity,
queryRunner?: QueryRunner,
): SpaceProductAllocationEntity {
return queryRunner
? queryRunner.manager.create(SpaceProductAllocationEntity, {
space,
product: tag.product,
tags: [tag],
})
: this.spaceProductAllocationRepository.create({
space,
product: tag.product,
tags: [tag],
});
}
private async saveAllocation(
allocation: SpaceProductAllocationEntity,
queryRunner?: QueryRunner,
) {
queryRunner
? await queryRunner.manager.save(SpaceProductAllocationEntity, allocation)
: await this.spaceProductAllocationRepository.save(allocation);
}
async saveAllocations(
allocations: SpaceProductAllocationEntity[],
queryRunner?: QueryRunner,
) {
queryRunner
? await queryRunner.manager.save(
SpaceProductAllocationEntity,
allocations,
)
: await this.spaceProductAllocationRepository.save(allocations);
}
private handleError(error: any, message: string): HttpException {
return new HttpException(
error instanceof HttpException ? error.message : message,
error instanceof HttpException
? error.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR,
);
}
async clearAllAllocations(spaceUuid: string, queryRunner: QueryRunner) {
try {
const allocationUuids = await queryRunner.manager
.createQueryBuilder(SpaceProductAllocationEntity, 'allocation')
.select('allocation.uuid')
.where('allocation.space_uuid = :spaceUuid', { spaceUuid })
.getRawMany()
.then((results) => results.map((r) => r.allocation_uuid));
if (allocationUuids.length === 0) {
return;
}
await queryRunner.manager
.createQueryBuilder()
.delete()
.from('space_product_tags')
.where('space_product_allocation_uuid IN (:...allocationUuids)', {
allocationUuids,
})
.execute();
await queryRunner.manager
.createQueryBuilder()
.delete()
.from(SpaceProductAllocationEntity)
.where('space_uuid = :spaceUuid', { spaceUuid })
.execute();
} catch (error) {
throw this.handleError(error, 'Failed to clear all allocations');
}
}
}