mirror of
https://github.com/SyncrowIOT/backend.git
synced 2026-10-25 21:07:54 +00:00
573 lines
17 KiB
TypeScript
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');
|
|
}
|
|
}
|
|
}
|