mirror of
https://github.com/SyncrowIOT/backend.git
synced 2025-07-16 10:46:17 +00:00
572 lines
18 KiB
TypeScript
572 lines
18 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 { SubspaceProductAllocationEntity } from '@app/common/modules/space/entities/subspace/subspace-product-allocation.entity';
|
|
import { SubspaceEntity } from '@app/common/modules/space/entities/subspace/subspace.entity';
|
|
import { SubspaceProductAllocationRepository } from '@app/common/modules/space/repositories/subspace.repository';
|
|
import { NewTagEntity } from '@app/common/modules/tag';
|
|
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
|
import { ModifyTagDto } from 'src/space/dtos/tag/modify-tag.dto';
|
|
import { ISingleSubspace } from 'src/space/interfaces/single-subspace.interface';
|
|
import { ProcessTagDto } from 'src/tags/dtos';
|
|
import { TagService as NewTagService } from 'src/tags/services';
|
|
import { In, QueryRunner } from 'typeorm';
|
|
|
|
@Injectable()
|
|
export class SubspaceProductAllocationService {
|
|
constructor(
|
|
private readonly tagService: NewTagService,
|
|
|
|
private readonly spaceProductAllocationRepository: SpaceProductAllocationRepository,
|
|
private readonly subspaceProductAllocationRepository: SubspaceProductAllocationRepository,
|
|
) {}
|
|
|
|
async createSubspaceProductAllocations(
|
|
subspace: SubspaceEntity,
|
|
processedTags: NewTagEntity[],
|
|
queryRunner?: QueryRunner,
|
|
spaceAllocationsToExclude?: SpaceProductAllocationEntity[],
|
|
): Promise<void> {
|
|
try {
|
|
if (!processedTags.length) return;
|
|
|
|
const allocations: SubspaceProductAllocationEntity[] = [];
|
|
|
|
for (const tag of processedTags) {
|
|
await this.validateTagWithinSubspace(
|
|
queryRunner,
|
|
tag,
|
|
subspace,
|
|
spaceAllocationsToExclude,
|
|
);
|
|
|
|
let allocation = await this.getAllocationByProduct(
|
|
tag.product,
|
|
subspace,
|
|
queryRunner,
|
|
);
|
|
|
|
if (!allocation) {
|
|
allocation = this.createNewSubspaceAllocation(
|
|
subspace,
|
|
tag,
|
|
queryRunner,
|
|
);
|
|
allocations.push(allocation);
|
|
} else if (!allocation.tags.some((t) => t.uuid === tag.uuid)) {
|
|
allocation.tags.push(tag);
|
|
await this.saveAllocation(allocation, queryRunner);
|
|
}
|
|
}
|
|
|
|
if (allocations.length > 0) {
|
|
await this.saveAllocations(allocations, queryRunner);
|
|
}
|
|
} catch (error) {
|
|
throw this.handleError(
|
|
error,
|
|
'Failed to create subspace product allocations',
|
|
);
|
|
}
|
|
}
|
|
async updateSubspaceProductAllocations(
|
|
subspaces: ISingleSubspace[],
|
|
projectUuid: string,
|
|
queryRunner: QueryRunner,
|
|
space: SpaceEntity,
|
|
spaceTagUpdateDtos?: ModifyTagDto[],
|
|
) {
|
|
const spaceAllocationToExclude: SpaceProductAllocationEntity[] = [];
|
|
for (const subspace of subspaces) {
|
|
if (!subspace.tags || subspace.tags.length === 0) continue;
|
|
const tagDtos = subspace.tags;
|
|
const tagsToAddDto: ProcessTagDto[] = tagDtos
|
|
.filter((dto) => dto.action === ModifyAction.ADD)
|
|
.map((dto) => ({
|
|
name: dto.name,
|
|
productUuid: dto.productUuid,
|
|
uuid: dto.newTagUuid,
|
|
}));
|
|
|
|
const tagsToDeleteDto = tagDtos.filter(
|
|
(dto) => dto.action === ModifyAction.DELETE,
|
|
);
|
|
|
|
if (tagsToAddDto.length > 0) {
|
|
let processedTags = await this.tagService.processTags(
|
|
tagsToAddDto,
|
|
projectUuid,
|
|
queryRunner,
|
|
);
|
|
|
|
for (const subspaceDto of subspaces) {
|
|
if (
|
|
subspaceDto !== subspace &&
|
|
subspaceDto.action === ModifyAction.UPDATE &&
|
|
subspaceDto.tags
|
|
) {
|
|
const deletedTags = subspaceDto.tags.filter(
|
|
(tagDto) =>
|
|
tagDto.action === ModifyAction.DELETE &&
|
|
processedTags.some((tag) => tag.uuid === tagDto.tagUuid),
|
|
);
|
|
|
|
for (const deletedTag of deletedTags) {
|
|
const allocation = await queryRunner.manager.findOne(
|
|
SubspaceProductAllocationEntity,
|
|
{
|
|
where: {
|
|
subspace: { uuid: subspaceDto.subspace.uuid },
|
|
},
|
|
relations: ['tags', 'product', 'subspace'],
|
|
},
|
|
);
|
|
|
|
const isCommonTag = allocation.tags.some(
|
|
(tag) => tag.uuid === deletedTag.tagUuid,
|
|
);
|
|
|
|
if (allocation && isCommonTag) {
|
|
const tagEntity = allocation.tags.find(
|
|
(tag) => tag.uuid === deletedTag.tagUuid,
|
|
);
|
|
|
|
allocation.tags = allocation.tags.filter(
|
|
(tag) => tag.uuid !== deletedTag.tagUuid,
|
|
);
|
|
|
|
await queryRunner.manager.save(allocation);
|
|
|
|
const productAllocationExistInSubspace =
|
|
await queryRunner.manager.findOne(
|
|
SubspaceProductAllocationEntity,
|
|
{
|
|
where: {
|
|
subspace: {
|
|
uuid: subspaceDto.subspace.uuid,
|
|
},
|
|
product: { uuid: allocation.product.uuid },
|
|
},
|
|
relations: ['tags'],
|
|
},
|
|
);
|
|
|
|
if (productAllocationExistInSubspace) {
|
|
productAllocationExistInSubspace.tags.push(tagEntity);
|
|
await queryRunner.manager.save(
|
|
productAllocationExistInSubspace,
|
|
);
|
|
} else {
|
|
const newProductAllocation = queryRunner.manager.create(
|
|
SubspaceProductAllocationEntity,
|
|
{
|
|
subspace: subspace.subspace,
|
|
product: allocation.product,
|
|
tags: [tagEntity],
|
|
},
|
|
);
|
|
|
|
await queryRunner.manager.save(newProductAllocation);
|
|
}
|
|
|
|
processedTags = processedTags.filter(
|
|
(tag) => tag.uuid !== deletedTag.tagUuid,
|
|
);
|
|
|
|
subspaceDto.tags = subspaceDto.tags.filter(
|
|
(tagDto) => tagDto.tagUuid !== deletedTag.tagUuid,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
if (
|
|
subspaceDto !== subspace &&
|
|
subspaceDto.action === ModifyAction.DELETE
|
|
) {
|
|
const allocation = await queryRunner.manager.findOne(
|
|
SubspaceProductAllocationEntity,
|
|
{
|
|
where: {
|
|
subspace: { uuid: subspaceDto.subspace.uuid },
|
|
},
|
|
relations: ['tags'],
|
|
},
|
|
);
|
|
|
|
const repeatedTags = allocation?.tags.filter((tag) =>
|
|
processedTags.some(
|
|
(processedTag) => processedTag.uuid === tag.uuid,
|
|
),
|
|
);
|
|
if (repeatedTags.length > 0) {
|
|
allocation.tags = allocation.tags.filter(
|
|
(tag) =>
|
|
!repeatedTags.some(
|
|
(repeatedTag) => repeatedTag.uuid === tag.uuid,
|
|
),
|
|
);
|
|
|
|
await queryRunner.manager.save(allocation);
|
|
|
|
const productAllocationExistInSubspace =
|
|
await queryRunner.manager.findOne(
|
|
SubspaceProductAllocationEntity,
|
|
{
|
|
where: {
|
|
subspace: { uuid: subspaceDto.subspace.uuid },
|
|
product: { uuid: allocation.product.uuid },
|
|
},
|
|
relations: ['tags'],
|
|
},
|
|
);
|
|
|
|
if (productAllocationExistInSubspace) {
|
|
productAllocationExistInSubspace.tags.push(...repeatedTags);
|
|
await queryRunner.manager.save(
|
|
productAllocationExistInSubspace,
|
|
);
|
|
} else {
|
|
const newProductAllocation = queryRunner.manager.create(
|
|
SubspaceProductAllocationEntity,
|
|
{
|
|
subspace: subspace.subspace,
|
|
product: allocation.product,
|
|
tags: repeatedTags,
|
|
},
|
|
);
|
|
|
|
await queryRunner.manager.save(newProductAllocation);
|
|
}
|
|
|
|
const newAllocation = queryRunner.manager.create(
|
|
SubspaceProductAllocationEntity,
|
|
{
|
|
subspace: subspace.subspace,
|
|
product: allocation.product,
|
|
tags: repeatedTags,
|
|
},
|
|
);
|
|
|
|
await queryRunner.manager.save(newAllocation);
|
|
}
|
|
}
|
|
}
|
|
if (spaceTagUpdateDtos) {
|
|
const deletedSpaceTags = spaceTagUpdateDtos.filter(
|
|
(tagDto) =>
|
|
tagDto.action === ModifyAction.DELETE &&
|
|
processedTags.some((tag) => tag.uuid === tagDto.tagUuid),
|
|
);
|
|
for (const deletedTag of deletedSpaceTags) {
|
|
const allocation = await queryRunner.manager.findOne(
|
|
SpaceProductAllocationEntity,
|
|
{
|
|
where: {
|
|
space: { uuid: space.uuid },
|
|
tags: { uuid: deletedTag.tagUuid },
|
|
},
|
|
relations: ['tags', 'subspace'],
|
|
},
|
|
);
|
|
|
|
if (
|
|
allocation &&
|
|
allocation.tags.some((tag) => tag.uuid === deletedTag.tagUuid)
|
|
) {
|
|
spaceAllocationToExclude.push(allocation);
|
|
}
|
|
}
|
|
}
|
|
|
|
await this.createSubspaceProductAllocations(
|
|
subspace.subspace,
|
|
processedTags,
|
|
queryRunner,
|
|
spaceAllocationToExclude,
|
|
);
|
|
}
|
|
if (tagsToDeleteDto.length > 0) {
|
|
await this.processDeleteActions(tagsToDeleteDto, queryRunner);
|
|
}
|
|
}
|
|
}
|
|
|
|
async processDeleteActions(
|
|
dtos: ModifyTagDto[],
|
|
queryRunner: QueryRunner,
|
|
): Promise<SubspaceProductAllocationEntity[]> {
|
|
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(
|
|
SubspaceProductAllocationEntity,
|
|
{
|
|
where: { tags: { uuid: In(tagUuidsToDelete) } },
|
|
relations: ['tags'],
|
|
},
|
|
);
|
|
|
|
if (!allocationsToUpdate || allocationsToUpdate.length === 0) return [];
|
|
|
|
const deletedAllocations: SubspaceProductAllocationEntity[] = [];
|
|
const allocationUpdates: SubspaceProductAllocationEntity[] = [];
|
|
|
|
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(
|
|
SubspaceProductAllocationEntity,
|
|
allocationUpdates,
|
|
);
|
|
}
|
|
|
|
if (deletedAllocations.length > 0) {
|
|
await queryRunner.manager.remove(
|
|
SubspaceProductAllocationEntity,
|
|
deletedAllocations,
|
|
);
|
|
}
|
|
|
|
await queryRunner.manager
|
|
.createQueryBuilder()
|
|
.delete()
|
|
.from('subspace_product_tags')
|
|
.where(
|
|
'subspace_product_allocation_uuid NOT IN ' +
|
|
queryRunner.manager
|
|
.createQueryBuilder()
|
|
.select('allocation.uuid')
|
|
.from(SubspaceProductAllocationEntity, 'allocation')
|
|
.getQuery() +
|
|
')',
|
|
)
|
|
.execute();
|
|
|
|
return deletedAllocations;
|
|
} catch (error) {
|
|
throw this.handleError(error, `Failed to delete tags in subspace`);
|
|
}
|
|
}
|
|
|
|
async unlinkModels(
|
|
allocations: SubspaceProductAllocationEntity[],
|
|
queryRunner: QueryRunner,
|
|
) {
|
|
try {
|
|
if (allocations.length === 0) return;
|
|
|
|
const allocationUuids = allocations.map((allocation) => allocation.uuid);
|
|
|
|
await queryRunner.manager.update(
|
|
SubspaceProductAllocationEntity,
|
|
{ uuid: In(allocationUuids) },
|
|
{ inheritedFromModel: null },
|
|
);
|
|
} catch (error) {
|
|
throw new HttpException(
|
|
'Failed to unlink models',
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
);
|
|
}
|
|
}
|
|
|
|
private async validateTagWithinSubspace(
|
|
queryRunner: QueryRunner | undefined,
|
|
tag: NewTagEntity,
|
|
subspace: SubspaceEntity,
|
|
spaceAllocationsToExclude?: SpaceProductAllocationEntity[],
|
|
): Promise<void> {
|
|
const existingTagInSpace = await (queryRunner
|
|
? queryRunner.manager.findOne(SpaceProductAllocationEntity, {
|
|
where: {
|
|
product: tag.product,
|
|
space: subspace.space,
|
|
tags: { uuid: tag.uuid },
|
|
},
|
|
relations: ['tags'],
|
|
})
|
|
: this.spaceProductAllocationRepository.findOne({
|
|
where: {
|
|
product: tag.product,
|
|
space: subspace.space,
|
|
tags: { uuid: tag.uuid },
|
|
},
|
|
relations: ['tags'],
|
|
}));
|
|
|
|
const isExcluded = spaceAllocationsToExclude?.some(
|
|
(excludedAllocation) =>
|
|
excludedAllocation.product.uuid === tag.product.uuid &&
|
|
excludedAllocation.tags.some((t) => t.uuid === tag.uuid),
|
|
);
|
|
|
|
if (!isExcluded && existingTagInSpace) {
|
|
throw new HttpException(
|
|
`Tag ${tag.uuid} (Product: ${tag.product.uuid}) is already allocated at the space level (${subspace.space.uuid}). Cannot allocate the same tag in a subspace.`,
|
|
HttpStatus.BAD_REQUEST,
|
|
);
|
|
}
|
|
|
|
const existingTagInSameSpace = await (queryRunner
|
|
? queryRunner.manager.findOne(SubspaceProductAllocationEntity, {
|
|
where: {
|
|
product: tag.product,
|
|
subspace: { space: subspace.space },
|
|
tags: { uuid: tag.uuid },
|
|
},
|
|
relations: ['subspace', 'tags'],
|
|
})
|
|
: this.subspaceProductAllocationRepository.findOne({
|
|
where: {
|
|
product: tag.product,
|
|
subspace: { space: subspace.space },
|
|
tags: { uuid: tag.uuid },
|
|
},
|
|
relations: ['subspace', 'tags'],
|
|
}));
|
|
|
|
if (
|
|
existingTagInSameSpace &&
|
|
existingTagInSameSpace.subspace.uuid !== subspace.uuid
|
|
) {
|
|
throw new HttpException(
|
|
`Tag ${tag.uuid} (Product: ${tag.product.uuid}) is already allocated in another subspace (${existingTagInSameSpace.subspace.uuid}) within the same space (${subspace.space.uuid}).`,
|
|
HttpStatus.BAD_REQUEST,
|
|
);
|
|
}
|
|
}
|
|
private createNewSubspaceAllocation(
|
|
subspace: SubspaceEntity,
|
|
tag: NewTagEntity,
|
|
queryRunner?: QueryRunner,
|
|
): SubspaceProductAllocationEntity {
|
|
return queryRunner
|
|
? queryRunner.manager.create(SubspaceProductAllocationEntity, {
|
|
subspace,
|
|
product: tag.product,
|
|
tags: [tag],
|
|
})
|
|
: this.subspaceProductAllocationRepository.create({
|
|
subspace,
|
|
product: tag.product,
|
|
tags: [tag],
|
|
});
|
|
}
|
|
private async getAllocationByProduct(
|
|
product: ProductEntity,
|
|
subspace: SubspaceEntity,
|
|
queryRunner?: QueryRunner,
|
|
): Promise<SubspaceProductAllocationEntity | null> {
|
|
return queryRunner
|
|
? queryRunner.manager.findOne(SubspaceProductAllocationEntity, {
|
|
where: { subspace, product },
|
|
relations: ['tags'],
|
|
})
|
|
: this.subspaceProductAllocationRepository.findOne({
|
|
where: { subspace, product },
|
|
relations: ['tags'],
|
|
});
|
|
}
|
|
private async saveAllocation(
|
|
allocation: SubspaceProductAllocationEntity,
|
|
queryRunner?: QueryRunner,
|
|
): Promise<void> {
|
|
if (queryRunner) {
|
|
await queryRunner.manager.save(
|
|
SubspaceProductAllocationEntity,
|
|
allocation,
|
|
);
|
|
} else {
|
|
await this.subspaceProductAllocationRepository.save(allocation);
|
|
}
|
|
}
|
|
|
|
private async saveAllocations(
|
|
allocations: SubspaceProductAllocationEntity[],
|
|
queryRunner?: QueryRunner,
|
|
): Promise<void> {
|
|
if (queryRunner) {
|
|
await queryRunner.manager.save(
|
|
SubspaceProductAllocationEntity,
|
|
allocations,
|
|
);
|
|
} else {
|
|
await this.subspaceProductAllocationRepository.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(subspaceUuids: string[], queryRunner: QueryRunner) {
|
|
try {
|
|
const allocationUuids = await queryRunner.manager
|
|
.createQueryBuilder(SubspaceProductAllocationEntity, 'allocation')
|
|
.select('allocation.uuid')
|
|
.where('allocation.subspace_uuid IN (:...subspaceUuids)', {
|
|
subspaceUuids,
|
|
})
|
|
.getRawMany()
|
|
.then((results) => results.map((r) => r.allocation_uuid));
|
|
|
|
if (allocationUuids.length === 0) {
|
|
return;
|
|
}
|
|
|
|
await queryRunner.manager
|
|
.createQueryBuilder()
|
|
.delete()
|
|
.from('subspace_product_tags')
|
|
.where('subspace_product_allocation_uuid IN (:...allocationUuids)', {
|
|
allocationUuids,
|
|
})
|
|
.execute();
|
|
|
|
await queryRunner.manager
|
|
.createQueryBuilder()
|
|
.delete()
|
|
.from(SubspaceProductAllocationEntity)
|
|
.where('subspace_uuid IN (:...subspaceUuids)', { subspaceUuids })
|
|
.execute();
|
|
} catch (error) {
|
|
throw new HttpException(
|
|
error instanceof HttpException
|
|
? error.message
|
|
: 'An unexpected error occurred while clearing allocations',
|
|
error instanceof HttpException
|
|
? error.getStatus()
|
|
: HttpStatus.INTERNAL_SERVER_ERROR,
|
|
);
|
|
}
|
|
}
|
|
}
|