updated subspace model covering all edge cases

This commit is contained in:
hannathkadher
2025-02-24 18:41:32 +04:00
parent d13da50fd7
commit 4391339e5e
6 changed files with 910 additions and 488 deletions

View File

@ -31,7 +31,7 @@ export class SpaceModelProductAllocationEntity extends AbstractEntity<SpaceModel
@ManyToOne(() => ProductEntity, { nullable: false, onDelete: 'CASCADE' })
public product: ProductEntity;
@ManyToMany(() => NewTagEntity)
@ManyToMany(() => NewTagEntity, { cascade: true, onDelete: 'CASCADE' })
@JoinTable({ name: 'space_model_product_tags' })
public tags: NewTagEntity[];

View File

@ -1,6 +1,6 @@
import { ModifyAction } from '@app/common/constants/modify-action.enum';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsString, IsOptional, IsEnum } from 'class-validator';
import { IsString, IsOptional, IsEnum, IsUUID } from 'class-validator';
export class ModifyTagModelDto {
@ApiProperty({
@ -11,20 +11,29 @@ export class ModifyTagModelDto {
action: ModifyAction;
@ApiPropertyOptional({
description: 'UUID of the tag model (required for update/delete)',
description: 'UUID of the new tag',
example: '123e4567-e89b-12d3-a456-426614174000',
})
@IsOptional()
@IsString()
uuid?: string;
@IsUUID()
newTagUuid: string;
@ApiPropertyOptional({
description: 'Name of the tag model (required for add/update)',
description:
'UUID of an existing tag (required for update/delete, optional for add)',
example: 'a1b2c3d4-5678-90ef-abcd-1234567890ef',
})
@IsOptional()
@IsUUID()
tagUuid?: string;
@ApiPropertyOptional({
description: 'Name of the tag (required for add/update)',
example: 'Temperature Sensor',
})
@IsOptional()
@IsString()
tag?: string;
name?: string;
@ApiPropertyOptional({
description:
@ -32,6 +41,6 @@ export class ModifyTagModelDto {
example: 'c789a91e-549a-4753-9006-02f89e8170e0',
})
@IsOptional()
@IsString()
@IsUUID()
productUuid?: string;
}

View File

@ -0,0 +1,220 @@
import { QueryRunner } from 'typeorm';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import {
SpaceModelEntity,
SpaceModelProductAllocationEntity,
SpaceModelProductAllocationRepoitory,
} from '@app/common/modules/space-model';
import { TagService as NewTagService } from 'src/tags/services';
import { ProcessTagDto } from 'src/tags/dtos';
import { ModifyTagModelDto } from '../dtos';
import { ModifyAction } from '@app/common/constants/modify-action.enum';
import { NewTagEntity } from '@app/common/modules/tag';
import { ProductEntity } from '@app/common/modules/product/entities';
@Injectable()
export class SpaceModelProductAllocationService {
constructor(
private readonly tagService: NewTagService,
private readonly spaceModelProductAllocationRepository: SpaceModelProductAllocationRepoitory,
) {}
async createProductAllocations(
projectUuid: string,
spaceModel: SpaceModelEntity,
tags: ProcessTagDto[],
queryRunner?: QueryRunner,
): Promise<SpaceModelProductAllocationEntity[]> {
try {
const processedTags = await this.tagService.processTags(
tags,
projectUuid,
queryRunner,
);
const productAllocations: SpaceModelProductAllocationEntity[] = [];
for (const tag of processedTags) {
let allocation = await this.getAllocationByProduct(
tag.product,
spaceModel,
queryRunner,
);
if (!allocation) {
allocation = this.createNewAllocation(spaceModel, tag, queryRunner);
productAllocations.push(allocation);
} else if (
!allocation.tags.some((existingTag) => existingTag.uuid === tag.uuid)
) {
allocation.tags.push(tag);
await this.saveAllocation(allocation, queryRunner);
}
}
if (productAllocations.length > 0) {
await this.saveAllocations(productAllocations, queryRunner);
}
return productAllocations;
} catch (error) {
throw this.handleError(error, 'Failed to create product allocations');
}
}
async updateProductAllocations(
dtos: ModifyTagModelDto[],
projectUuid: string,
spaceModel: SpaceModelEntity,
queryRunner: QueryRunner,
): Promise<void> {
try {
await Promise.all([
this.processAddActions(dtos, projectUuid, spaceModel, queryRunner),
this.processDeleteActions(dtos, queryRunner),
]);
} catch (error) {
throw this.handleError(error, 'Error while updating product allocations');
}
}
private async processAddActions(
dtos: ModifyTagModelDto[],
projectUuid: string,
spaceModel: SpaceModelEntity,
queryRunner: QueryRunner,
): 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) {
await this.createProductAllocations(
projectUuid,
spaceModel,
addDtos,
queryRunner,
);
}
}
private createNewAllocation(
spaceModel: SpaceModelEntity,
tag: NewTagEntity,
queryRunner?: QueryRunner,
): SpaceModelProductAllocationEntity {
return queryRunner
? queryRunner.manager.create(SpaceModelProductAllocationEntity, {
spaceModel,
product: tag.product,
tags: [tag],
})
: this.spaceModelProductAllocationRepository.create({
spaceModel,
product: tag.product,
tags: [tag],
});
}
private async getAllocationByProduct(
product: ProductEntity,
spaceModel: SpaceModelEntity,
queryRunner?: QueryRunner,
): Promise<SpaceModelProductAllocationEntity | null> {
return queryRunner
? queryRunner.manager.findOne(SpaceModelProductAllocationEntity, {
where: { spaceModel, product: product },
relations: ['tags'],
})
: this.spaceModelProductAllocationRepository.findOne({
where: { spaceModel, product: product },
relations: ['tags'],
});
}
private async saveAllocation(
allocation: SpaceModelProductAllocationEntity,
queryRunner?: QueryRunner,
) {
queryRunner
? await queryRunner.manager.save(
SpaceModelProductAllocationEntity,
allocation,
)
: await this.spaceModelProductAllocationRepository.save(allocation);
}
private async saveAllocations(
allocations: SpaceModelProductAllocationEntity[],
queryRunner?: QueryRunner,
) {
queryRunner
? await queryRunner.manager.save(
SpaceModelProductAllocationEntity,
allocations,
)
: await this.spaceModelProductAllocationRepository.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,
);
}
private async processDeleteActions(
dtos: ModifyTagModelDto[],
queryRunner: QueryRunner,
): Promise<void> {
const deleteDtos = dtos.filter((dto) => dto.action === ModifyAction.DELETE);
await Promise.all(
deleteDtos.map(async (dto) => {
if (!dto.tagUuid) return;
const allocation = await queryRunner.manager.findOne(
SpaceModelProductAllocationEntity,
{
where: { tags: { uuid: dto.tagUuid } },
relations: ['tags'],
},
);
if (!allocation) {
throw new HttpException(
`Allocation with tag ${dto.tagUuid} not found`,
HttpStatus.NOT_FOUND,
);
}
const existingTag = allocation.tags.find(
(tag) => tag.uuid === dto.tagUuid,
);
if (!existingTag) return;
await queryRunner.manager
.createQueryBuilder()
.relation(SpaceModelProductAllocationEntity, 'tags')
.of(allocation)
.remove(existingTag);
}),
);
}
private async getAllocationByTagUuid(
tagUuid: string,
queryRunner: QueryRunner,
): Promise<SpaceModelProductAllocationEntity | null> {
return queryRunner.manager.findOne(SpaceModelProductAllocationEntity, {
where: { tags: { uuid: tagUuid } },
relations: ['tags'],
});
}
}

View File

@ -1,7 +1,5 @@
import {
SpaceModelEntity,
SpaceModelProductAllocationEntity,
SpaceModelProductAllocationRepoitory,
SpaceModelRepository,
} from '@app/common/modules/space-model';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
@ -21,16 +19,10 @@ import { ProjectEntity } from '@app/common/modules/project/entities';
import { TagModelService } from './tag-model.service';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { CommandBus } from '@nestjs/cqrs';
import { PropogateUpdateSpaceModelCommand } from '../commands';
import {
ModifiedTagsModelPayload,
ModifySubspaceModelPayload,
} from '../interfaces';
import { SpaceModelDto } from '@app/common/modules/space-model/dtos';
import { TagService as NewTagService } from 'src/tags/services';
import { ProductRepository } from '@app/common/modules/product/repositories';
import { NewTagEntity } from '@app/common/modules/tag';
import { ProcessTagDto } from 'src/tags/dtos';
import { SpaceModelProductAllocationService } from './space-model-product-allocation.service';
@Injectable()
export class SpaceModelService {
@ -42,8 +34,7 @@ export class SpaceModelService {
private readonly tagModelService: TagModelService,
private commandBus: CommandBus,
private readonly tagService: NewTagService,
private readonly spaceModelProductAllocationRepository: SpaceModelProductAllocationRepoitory,
private readonly productRepository: ProductRepository,
private readonly spaceModelProductAllocationService: SpaceModelProductAllocationService,
) {}
async createSpaceModel(
@ -75,10 +66,11 @@ export class SpaceModelService {
const savedSpaceModel = await queryRunner.manager.save(spaceModel);
this.validateUniqueTags(
tags,
this.subSpaceModelService.extractSubspaceTags(subspaceModels),
);
const subspaceTags =
this.subSpaceModelService.extractTagsFromSubspaceModels(subspaceModels);
const allTags = [...tags, ...subspaceTags];
this.validateUniqueTags(allTags);
if (subspaceModels?.length) {
savedSpaceModel.subspaceModels =
@ -90,14 +82,10 @@ export class SpaceModelService {
}
if (tags?.length) {
const processedTags = await this.tagService.processTags(
tags,
await this.spaceModelProductAllocationService.createProductAllocations(
params.projectUuid,
queryRunner,
);
await this.createProductAllocations(
spaceModel,
processedTags,
tags,
queryRunner,
);
}
@ -202,8 +190,6 @@ export class SpaceModelService {
const spaceModel = await this.validateSpaceModel(param.spaceModelUuid);
await queryRunner.connect();
let modifiedSubspaceModels: ModifySubspaceModelPayload = {};
let modifiedTagsModelPayload: ModifiedTagsModelPayload = {};
try {
await queryRunner.startTransaction();
@ -221,46 +207,25 @@ export class SpaceModelService {
);
}
const spaceTagsAfterMove = this.tagModelService.getSubspaceTagsToBeAdded(
dto.tags,
dto.subspaceModels,
);
const modifiedSubspaces = this.tagModelService.getModifiedSubspaces(
dto.tags,
dto.subspaceModels,
);
if (dto.subspaceModels) {
modifiedSubspaceModels =
await this.subSpaceModelService.modifySubSpaceModels(
modifiedSubspaces,
spaceModel,
queryRunner,
);
}
if (dto.tags) {
modifiedTagsModelPayload = await this.tagModelService.modifyTags(
spaceTagsAfterMove,
queryRunner,
await this.subSpaceModelService.modifySubspaceModels(
dto.subspaceModels,
spaceModel,
null,
queryRunner,
param.projectUuid,
dto.tags,
);
}
await queryRunner.commitTransaction();
await this.commandBus.execute(
new PropogateUpdateSpaceModelCommand({
spaceModel: spaceModel,
modifiedSpaceModels: {
modifiedSubspaceModels,
modifiedTags: modifiedTagsModelPayload,
},
if (dto.tags) {
await this.spaceModelProductAllocationService.updateProductAllocations(
dto.tags,
param.projectUuid,
spaceModel,
queryRunner,
}),
);
);
}
await queryRunner.commitTransaction();
return new SuccessResponseDto({
message: 'SpaceModel updated successfully',
@ -419,88 +384,7 @@ export class SpaceModelService {
return spaceModel;
}
private async createProductAllocations(
spaceModel: SpaceModelEntity,
tags: NewTagEntity[],
queryRunner?: QueryRunner,
): Promise<void> {
try {
const productAllocations: SpaceModelProductAllocationEntity[] = [];
for (const tag of tags) {
const existingAllocation = await (queryRunner
? queryRunner.manager.findOne(SpaceModelProductAllocationEntity, {
where: { spaceModel, product: tag.product },
relations: ['tags'],
})
: this.spaceModelProductAllocationRepository.findOne({
where: { spaceModel, product: tag.product },
relations: ['tags'],
}));
if (!existingAllocation) {
const productAllocation = queryRunner
? queryRunner.manager.create(SpaceModelProductAllocationEntity, {
spaceModel,
product: tag.product,
tags: [tag],
})
: this.spaceModelProductAllocationRepository.create({
spaceModel,
product: tag.product,
tags: [tag],
});
productAllocations.push(productAllocation);
} else {
if (
!existingAllocation.tags.some(
(existingTag) => existingTag.uuid === tag.uuid,
)
) {
existingAllocation.tags.push(tag);
if (queryRunner) {
await queryRunner.manager.save(
SpaceModelProductAllocationEntity,
existingAllocation,
);
} else {
await this.spaceModelProductAllocationRepository.save(
existingAllocation,
);
}
}
}
}
if (productAllocations.length > 0) {
if (queryRunner) {
await queryRunner.manager.save(
SpaceModelProductAllocationEntity,
productAllocations,
);
} else {
await this.spaceModelProductAllocationRepository.save(
productAllocations,
);
}
}
} catch (error) {
console.error('[ERROR] Failed to create product allocations:', error);
throw new HttpException(
'An unexpected error occurred while creating product allocations',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
private validateUniqueTags(
spaceTags: ProcessTagDto[],
subspaceTags: ProcessTagDto[],
) {
const allTags = [...spaceTags, ...subspaceTags];
private validateUniqueTags(allTags: ProcessTagDto[]) {
const tagUuidSet = new Set<string>();
const tagNameProductSet = new Set<string>();

View File

@ -0,0 +1,500 @@
import { ModifyAction } from '@app/common/constants/modify-action.enum';
import {
SpaceModelEntity,
SpaceModelProductAllocationEntity,
SpaceModelProductAllocationRepoitory,
SubspaceModelEntity,
SubspaceModelProductAllocationEntity,
SubspaceModelProductAllocationRepoitory,
} from '@app/common/modules/space-model';
import { NewTagEntity } from '@app/common/modules/tag';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import {
ModifySubspaceModelDto,
ModifyTagModelDto,
} from 'src/space-model/dtos';
import { ProcessTagDto } from 'src/tags/dtos';
import { TagService as NewTagService } from 'src/tags/services';
import { In, QueryRunner } from 'typeorm';
@Injectable()
export class SubspaceModelProductAllocationService {
constructor(
private readonly tagService: NewTagService,
private readonly subspaceModelProductAllocationRepository: SubspaceModelProductAllocationRepoitory,
private readonly spaceModelAllocationRepository: SpaceModelProductAllocationRepoitory,
) {}
async createProductAllocations(
subspaceModel: SubspaceModelEntity,
tags: NewTagEntity[],
queryRunner?: QueryRunner,
spaceAllocationsToExclude?: SpaceModelProductAllocationEntity[],
): Promise<SubspaceModelProductAllocationEntity[]> {
try {
const allocations: SubspaceModelProductAllocationEntity[] = [];
for (const tag of tags) {
// Step 1: Check if this specific tag is already allocated at the space level
const existingTagInSpaceModel = await (queryRunner
? queryRunner.manager.findOne(SpaceModelProductAllocationEntity, {
where: {
product: tag.product,
spaceModel: subspaceModel.spaceModel, // Check at the space level
tags: { uuid: tag.uuid }, // Check for the specific tag
},
relations: ['tags'],
})
: this.spaceModelAllocationRepository.findOne({
where: {
product: tag.product,
spaceModel: subspaceModel.spaceModel,
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 tag is found at the space level, prevent allocation at the subspace level
if (!isExcluded && existingTagInSpaceModel) {
throw new HttpException(
`Tag ${tag.uuid} (Product: ${tag.product.uuid}) is already allocated at the space level (${subspaceModel.spaceModel.uuid}). Cannot allocate the same tag in a subspace.`,
HttpStatus.BAD_REQUEST,
);
}
// Check if this specific tag is already allocated within another subspace of the same space
const existingTagInSameSpace = await (queryRunner
? queryRunner.manager.findOne(SubspaceModelProductAllocationEntity, {
where: {
product: tag.product,
subspaceModel: { spaceModel: subspaceModel.spaceModel },
tags: { uuid: tag.uuid }, // Ensure the exact tag is checked
},
relations: ['subspaceModel', 'tags'],
})
: this.subspaceModelProductAllocationRepository.findOne({
where: {
product: tag.product,
subspaceModel: { spaceModel: subspaceModel.spaceModel },
tags: { uuid: tag.uuid },
},
relations: ['subspaceModel', 'tags'],
}));
// Prevent duplicate allocation if tag exists in another subspace of the same space
if (
existingTagInSameSpace &&
existingTagInSameSpace.subspaceModel.uuid !== subspaceModel.uuid
) {
throw new HttpException(
`Tag ${tag.uuid} (Product: ${tag.product.uuid}) is already allocated in another subspace (${existingTagInSameSpace.subspaceModel.uuid}) within the same space (${subspaceModel.spaceModel.uuid}).`,
HttpStatus.BAD_REQUEST,
);
}
//Check if there are existing allocations for this product in the subspace
const existingAllocationsForProduct = await (queryRunner
? queryRunner.manager.find(SubspaceModelProductAllocationEntity, {
where: { subspaceModel, product: tag.product },
relations: ['tags'],
})
: this.subspaceModelProductAllocationRepository.find({
where: { subspaceModel, product: tag.product },
relations: ['tags'],
}));
//Flatten all existing tags for this product in the subspace
const existingTagsForProduct = existingAllocationsForProduct.flatMap(
(allocation) => allocation.tags,
);
// Check if the tag is already assigned to the same product in this subspace
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 subspace (${subspaceModel.uuid}).`,
HttpStatus.BAD_REQUEST,
);
}
// If no existing allocation, create a new one
if (existingAllocationsForProduct.length === 0) {
const allocation = queryRunner
? queryRunner.manager.create(SubspaceModelProductAllocationEntity, {
subspaceModel,
product: tag.product,
tags: [tag],
})
: this.subspaceModelProductAllocationRepository.create({
subspaceModel,
product: tag.product,
tags: [tag],
});
allocations.push(allocation);
} else {
//If allocation exists, add the tag to it
existingAllocationsForProduct[0].tags.push(tag);
if (queryRunner) {
await queryRunner.manager.save(
SubspaceModelProductAllocationEntity,
existingAllocationsForProduct[0],
);
} else {
await this.subspaceModelProductAllocationRepository.save(
existingAllocationsForProduct[0],
);
}
}
}
// Save newly created allocations
if (allocations.length > 0) {
if (queryRunner) {
await queryRunner.manager.save(
SubspaceModelProductAllocationEntity,
allocations,
);
} else {
await this.subspaceModelProductAllocationRepository.save(allocations);
}
}
return allocations;
} catch (error) {
throw new HttpException(
'An unexpected error occurred while creating subspace product allocations',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async processAddActions(
dtos: ModifyTagModelDto[],
projectUuid: string,
subspaceModel: SubspaceModelEntity,
otherSubspaceModelUpdateDtos: ModifySubspaceModelDto[],
queryRunner: QueryRunner,
spaceModel: SpaceModelEntity,
spaceTagUpdateDtos?: ModifyTagModelDto[],
): Promise<{
updatedSubspaceDtos: ModifySubspaceModelDto[];
addedAllocations: SubspaceModelProductAllocationEntity[];
}> {
let addedAllocations: SubspaceModelProductAllocationEntity[] = [];
const updatedAllocations: SubspaceModelProductAllocationEntity[] = [];
const spaceAllocationToExclude: SpaceModelProductAllocationEntity[] = [];
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) {
let processedTags = await this.tagService.processTags(
addDtos,
projectUuid,
queryRunner,
);
for (const subspaceDto of otherSubspaceModelUpdateDtos) {
if (subspaceDto.action === ModifyAction.UPDATE && subspaceDto.tags) {
// Tag is deleted from one subspace and added in another subspace
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(
SubspaceModelProductAllocationEntity,
{
where: {
subspaceModel: { uuid: subspaceDto.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(
SubspaceModelProductAllocationEntity,
{
where: {
subspaceModel: { uuid: subspaceDto.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(
SubspaceModelProductAllocationEntity,
{
subspaceModel: subspaceModel,
product: allocation.product,
tags: [deletedTag],
},
);
await queryRunner.manager.save(newProductAllocation);
}
updatedAllocations.push(allocation);
// Remove the tag from processedTags to prevent duplication
processedTags = processedTags.filter(
(tag) => tag.uuid !== deletedTag.tagUuid,
);
// Remove the tag from subspaceDto.tags to ensure it's not processed again while processing other dtos.
subspaceDto.tags = subspaceDto.tags.filter(
(tagDto) => tagDto.tagUuid !== deletedTag.tagUuid,
);
}
}
}
if (subspaceDto.action === ModifyAction.DELETE) {
const allocation = await queryRunner.manager.findOne(
SubspaceModelProductAllocationEntity,
{
where: { subspaceModel: { uuid: subspaceDto.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(
SubspaceModelProductAllocationEntity,
{
where: {
subspaceModel: { uuid: subspaceDto.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(
SubspaceModelProductAllocationEntity,
{
subspaceModel: subspaceModel,
product: allocation.product,
tags: repeatedTags,
},
);
await queryRunner.manager.save(newProductAllocation);
}
const newAllocation = queryRunner.manager.create(
SubspaceModelProductAllocationEntity,
{
subspaceModel: subspaceModel,
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(
SpaceModelProductAllocationEntity,
{
where: {
spaceModel: { uuid: spaceModel.uuid },
tags: { uuid: deletedTag.tagUuid },
},
relations: ['tags', 'subspace'],
},
);
if (
allocation &&
allocation.tags.some((tag) => tag.uuid === deletedTag.tagUuid)
) {
spaceAllocationToExclude.push(allocation);
}
}
}
// Create new product allocations
const newAllocations = await this.createProductAllocations(
subspaceModel,
processedTags,
queryRunner,
spaceAllocationToExclude,
);
addedAllocations = [...newAllocations, ...updatedAllocations]; // Combine newly created and updated allocations
}
return {
updatedSubspaceDtos: otherSubspaceModelUpdateDtos, // Updated subspace DTOs
addedAllocations, // Newly created allocations
};
}
async processDeleteActions(
dtos: ModifyTagModelDto[],
queryRunner: QueryRunner,
): Promise<void> {
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(
SubspaceModelProductAllocationEntity,
{
where: { tags: { uuid: In(tagUuidsToDelete) } },
relations: ['tags'],
},
);
if (!allocationsToUpdate || allocationsToUpdate.length === 0) return;
const allocationUpdates: SubspaceModelProductAllocationEntity[] = [];
for (const allocation of allocationsToUpdate) {
const updatedTags = allocation.tags.filter(
(tag) => !tagUuidsToDelete.includes(tag.uuid),
);
if (updatedTags.length === allocation.tags.length) {
continue;
}
allocation.tags = updatedTags;
allocationUpdates.push(allocation);
}
if (allocationUpdates.length > 0) {
await queryRunner.manager.save(
SubspaceModelProductAllocationEntity,
allocationUpdates,
);
}
await queryRunner.manager
.createQueryBuilder()
.delete()
.from('subspace_model_product_tags')
.where(
'subspace_model_product_allocation_id NOT IN ' +
queryRunner.manager
.createQueryBuilder()
.select('uuid')
.from(SubspaceModelProductAllocationEntity, 'allocation')
.getQuery(),
)
.execute();
} catch (error) {
throw this.handleError(error, `Failed to delete tags in subspace model`);
}
}
async updateProductAllocations(
dtos: ModifyTagModelDto[],
projectUuid: string,
subspaceModel: SubspaceModelEntity,
otherSubspaceModelUpdateDtos: ModifySubspaceModelDto[],
spaceModel: SpaceModelEntity,
queryRunner: QueryRunner,
spaceTagUpdateDtos?: ModifyTagModelDto[],
): Promise<void> {
try {
await Promise.all([
this.processAddActions(
dtos,
projectUuid,
subspaceModel,
otherSubspaceModelUpdateDtos,
queryRunner,
spaceModel,
spaceTagUpdateDtos,
),
this.processDeleteActions(dtos, queryRunner),
]);
} catch (error) {
throw this.handleError(error, 'Error while updating product 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,
);
}
}

View File

@ -1,27 +1,19 @@
import {
SpaceModelEntity,
SpaceModelProductAllocationEntity,
SubspaceModelEntity,
SubspaceModelProductAllocationEntity,
SubspaceModelProductAllocationRepoitory,
SubspaceModelRepository,
} from '@app/common/modules/space-model';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { CreateSubspaceModelDto, CreateTagModelDto } from '../../dtos';
import { CreateSubspaceModelDto, ModifyTagModelDto } from '../../dtos';
import { Not, QueryRunner } from 'typeorm';
import {
IDeletedSubsaceModelInterface,
ModifySubspaceModelPayload,
UpdatedSubspaceModelPayload,
} from 'src/space-model/interfaces';
import {
DeleteSubspaceModelDto,
ModifySubspaceModelDto,
} from 'src/space-model/dtos/subspaces-model-dtos';
import { IDeletedSubsaceModelInterface } from 'src/space-model/interfaces';
import { ModifySubspaceModelDto } from 'src/space-model/dtos/subspaces-model-dtos';
import { TagModelService } from '../tag-model.service';
import { ModifyAction } from '@app/common/constants/modify-action.enum';
import { ProcessTagDto } from 'src/tags/dtos';
import { TagService } from 'src/tags/services';
import { NewTagEntity } from '@app/common/modules/tag';
import { SubspaceModelProductAllocationService } from './subspace-model-product-allocation.service';
@Injectable()
export class SubSpaceModelService {
@ -29,8 +21,9 @@ export class SubSpaceModelService {
private readonly subspaceModelRepository: SubspaceModelRepository,
private readonly tagModelService: TagModelService,
private readonly tagService: TagService,
private readonly subspaceModelProductAllocationRepository: SubspaceModelProductAllocationRepoitory,
private readonly productAllocationService: SubspaceModelProductAllocationService,
) {}
async createModels(
spaceModel: SpaceModelEntity,
dtos: CreateSubspaceModelDto[],
@ -57,7 +50,7 @@ export class SubSpaceModelService {
queryRunner,
);
await this.createSubspaceProductAllocations(
await this.productAllocationService.createProductAllocations(
subspaceModel,
processedTags,
queryRunner,
@ -77,241 +70,177 @@ export class SubSpaceModelService {
}
}
async createSubSpaceModels(
subSpaceModelDtos: CreateSubspaceModelDto[],
async handleAddAction(
dtos: ModifySubspaceModelDto[],
spaceModel: SpaceModelEntity,
queryRunner: QueryRunner,
): Promise<SubspaceModelEntity[]> {
try {
await this.validateInputDtos(subSpaceModelDtos, spaceModel);
const subspaces = subSpaceModelDtos.map((subspaceDto) =>
queryRunner.manager.create(this.subspaceModelRepository.target, {
subspaceName: subspaceDto.subspaceName,
spaceModel,
}),
);
const savedSubspaces = await queryRunner.manager.save(subspaces);
await Promise.all(
subSpaceModelDtos.map(async (dto, index) => {
if (dto.tags && dto.tags.length > 0) {
/* subspace.tags = await this.tagModelService.createTags(
dto.tags,
queryRunner,
null,
subspace,
[...(otherTags || []), ...otherDtoTags],
); */
}
}),
);
return savedSubspaces;
} catch (error) {
if (error instanceof HttpException) {
throw error; // Rethrow known HttpExceptions
projectUuid: string,
otherSubspaceModelUpdateDtos?: ModifySubspaceModelDto[],
spaceTagUpdateDtos?: ModifyTagModelDto[],
) {
for (const dto of dtos) {
if (dto.subspaceName) {
await this.checkDuplicateNames(dto.subspaceName, spaceModel.uuid);
const subspaceModel = queryRunner.manager.create(
this.subspaceModelRepository.target,
{
subspaceName: dto.subspaceName,
spaceModel,
},
);
const savedSubspace = await queryRunner.manager.save(subspaceModel);
if (dto.tags) {
await this.productAllocationService.updateProductAllocations(
dto.tags,
projectUuid,
subspaceModel,
otherSubspaceModelUpdateDtos ? otherSubspaceModelUpdateDtos : [],
spaceModel,
queryRunner,
spaceTagUpdateDtos,
);
}
}
// Handle unexpected errors
throw new HttpException(
`An error occurred while creating subspace models: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async modifySubspaceModels(
dtos: ModifySubspaceModelDto[],
spaceModel: SpaceModelEntity,
queryRunner: QueryRunner,
projectUuid: string,
spaceTagUpdateDtos?: ModifyTagModelDto[],
) {
const addDtos = dtos.filter((dto) => dto.action === ModifyAction.ADD);
const otherDtos = dtos.filter((dto) => dto.action !== ModifyAction.ADD);
const deleteDtos = dtos.filter((dto) => dto.action !== ModifyAction.DELETE);
await this.handleAddAction(
addDtos,
spaceModel,
queryRunner,
projectUuid,
otherDtos,
spaceTagUpdateDtos,
);
await this.updateSubspaceModel(
otherDtos,
spaceModel,
queryRunner,
projectUuid,
spaceTagUpdateDtos,
);
await this.deleteSubspaceModels(deleteDtos, queryRunner);
}
async deleteSubspaceModels(
deleteDtos: DeleteSubspaceModelDto[],
deleteDtos: ModifySubspaceModelDto[],
queryRunner: QueryRunner,
): Promise<IDeletedSubsaceModelInterface[]> {
const deleteResults: IDeletedSubsaceModelInterface[] = [];
const deleteResults = [];
for (const dto of deleteDtos) {
const subspaceModel = await this.findOne(dto.subspaceUuid);
const subspaceModel = await this.findOne(dto.uuid);
await queryRunner.manager.update(
this.subspaceModelRepository.target,
{ uuid: dto.subspaceUuid },
{ uuid: dto.uuid },
{ disabled: true },
);
if (subspaceModel.tags?.length) {
const modifyTagDtos = subspaceModel.tags.map((tag) => ({
uuid: tag.uuid,
action: ModifyAction.DELETE,
}));
await this.tagModelService.modifyTags(
modifyTagDtos,
queryRunner,
null,
subspaceModel,
);
if (subspaceModel.productAllocations?.length) {
for (const allocation of subspaceModel.productAllocations) {
const product = allocation.product;
const tags = allocation.tags;
const spaceAllocation = await queryRunner.manager.findOne(
SpaceModelProductAllocationEntity,
{
where: {
spaceModel: subspaceModel.spaceModel,
product: { uuid: product.uuid },
},
relations: ['tags'],
},
);
if (spaceAllocation) {
const existingTagUuids = new Set(
spaceAllocation.tags.map((tag) => tag.uuid),
);
const newTags = tags.filter(
(tag) => !existingTagUuids.has(tag.uuid),
);
spaceAllocation.tags.push(...newTags);
await queryRunner.manager.save(spaceAllocation);
} else {
const newSpaceAllocation = queryRunner.manager.create(
SpaceModelProductAllocationEntity,
{
spaceModel: subspaceModel.spaceModel,
product: product,
tags: tags,
},
);
await queryRunner.manager.save(newSpaceAllocation);
}
await queryRunner.manager.remove(allocation);
}
}
deleteResults.push({ uuid: dto.subspaceUuid });
deleteResults.push({ uuid: dto.uuid });
}
return deleteResults;
}
async modifySubSpaceModels(
subspaceDtos: ModifySubspaceModelDto[],
async updateSubspaceModel(
dtos: ModifySubspaceModelDto[],
spaceModel: SpaceModelEntity,
queryRunner: QueryRunner,
): Promise<ModifySubspaceModelPayload> {
const modifiedSubspaceModels: ModifySubspaceModelPayload = {
addedSubspaceModels: [],
updatedSubspaceModels: [],
deletedSubspaceModels: [],
};
try {
for (const subspace of subspaceDtos) {
switch (subspace.action) {
case ModifyAction.ADD:
const subspaceModel = await this.handleAddAction(
subspace,
spaceModel,
queryRunner,
);
modifiedSubspaceModels.addedSubspaceModels.push(subspaceModel);
break;
case ModifyAction.UPDATE:
const updatedSubspaceModel = await this.handleUpdateAction(
subspace,
queryRunner,
);
modifiedSubspaceModels.updatedSubspaceModels.push(
updatedSubspaceModel,
);
break;
case ModifyAction.DELETE:
await this.handleDeleteAction(subspace, queryRunner);
modifiedSubspaceModels.deletedSubspaceModels.push(subspace.uuid);
break;
default:
throw new HttpException(
`Invalid action "${subspace.action}".`,
HttpStatus.BAD_REQUEST,
);
projectUuid: string,
spaceTagUpdateDtos?: ModifyTagModelDto[],
) {
for (const dto of dtos) {
const otherDtos = dtos.filter((d) => d !== dto);
if (dto.subspaceName) {
await this.checkDuplicateNames(dto.subspaceName, spaceModel.uuid);
const subspace = queryRunner.manager.create(
this.subspaceModelRepository.target,
{
subspaceName: dto.subspaceName,
spaceModel,
},
);
const savedSubspace = await queryRunner.manager.save(subspace);
if (dto.tags) {
await this.productAllocationService.updateProductAllocations(
dto.tags,
projectUuid,
savedSubspace,
otherDtos,
spaceModel,
queryRunner,
spaceTagUpdateDtos,
);
}
}
return modifiedSubspaceModels;
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
`An error occurred while modifying subspace models: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
private async handleAddAction(
subspace: ModifySubspaceModelDto,
spaceModel: SpaceModelEntity,
queryRunner: QueryRunner,
): Promise<SubspaceModelEntity> {
try {
const createTagDtos: ProcessTagDto[] =
subspace.tags?.map((tag) => ({
name: tag.tag,
uuid: tag.uuid,
productUuid: tag.productUuid,
projectUuid: null,
})) || [];
const [createdSubspaceModel] = await this.createSubSpaceModels(
[
{
subspaceName: subspace.subspaceName,
tags: createTagDtos,
},
],
spaceModel,
queryRunner,
);
return createdSubspaceModel;
} catch (error) {
if (error instanceof HttpException) {
throw error; // Rethrow known HttpExceptions
}
throw new HttpException(
`An error occurred while adding subspace: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
private async handleUpdateAction(
modifyDto: ModifySubspaceModelDto,
queryRunner: QueryRunner,
): Promise<UpdatedSubspaceModelPayload> {
const updatePayload: UpdatedSubspaceModelPayload = {
subspaceModelUuid: modifyDto.uuid,
};
const subspace = await this.findOne(modifyDto.uuid);
await this.updateSubspaceName(
queryRunner,
subspace,
modifyDto.subspaceName,
);
updatePayload.subspaceName = modifyDto.subspaceName;
if (modifyDto.tags?.length) {
updatePayload.modifiedTags = await this.tagModelService.modifyTags(
modifyDto.tags,
queryRunner,
null,
subspace,
);
}
return updatePayload;
}
private async handleDeleteAction(
subspace: ModifySubspaceModelDto,
queryRunner: QueryRunner,
) {
const subspaceModel = await this.findOne(subspace.uuid);
await queryRunner.manager.update(
this.subspaceModelRepository.target,
{ uuid: subspace.uuid },
{ disabled: true },
);
if (subspaceModel.tags?.length) {
const modifyTagDtos: CreateTagModelDto[] = subspaceModel.tags.map(
(tag) => ({
uuid: tag.uuid,
action: ModifyAction.ADD,
tag: tag.tag,
productUuid: tag.product.uuid,
}),
);
await this.tagModelService.moveTags(
modifyTagDtos,
queryRunner,
subspaceModel.spaceModel,
null,
);
}
}
private async findOne(subspaceUuid: string): Promise<SubspaceModelEntity> {
const subspace = await this.subspaceModelRepository.findOne({
where: { uuid: subspaceUuid, disabled: false },
relations: ['tags', 'spaceModel', 'tags.product'],
relations: [
'productAllocations',
'spaceModel',
'productAllocations.tags',
],
});
if (!subspace) {
throw new HttpException(
@ -322,35 +251,6 @@ export class SubSpaceModelService {
return subspace;
}
private async validateInputDtos(
subSpaceModelDtos: CreateSubspaceModelDto[],
spaceModel: SpaceModelEntity,
): Promise<void> {
try {
if (subSpaceModelDtos.length === 0) {
throw new HttpException(
'Subspace models cannot be empty.',
HttpStatus.BAD_REQUEST,
);
}
await this.validateName(
subSpaceModelDtos.map((dto) => dto.subspaceName),
spaceModel,
);
} catch (error) {
if (error instanceof HttpException) {
throw error; // Rethrow known HttpExceptions to preserve their message and status
}
// Wrap unexpected errors
throw new HttpException(
`An error occurred while validating subspace models: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
private async checkDuplicateNames(
subspaceName: string,
spaceModelUuid: string,
@ -393,106 +293,15 @@ export class SubSpaceModelService {
}
}
private async validateName(
names: string[],
spaceModel: SpaceModelEntity,
): Promise<void> {
this.validateNamesInDTO(names);
for (const name of names) {
await this.checkDuplicateNames(name, spaceModel.uuid);
}
}
private async updateSubspaceName(
queryRunner: QueryRunner,
subSpaceModel: SubspaceModelEntity,
subspaceName?: string,
): Promise<void> {
if (subspaceName) {
await this.checkDuplicateNames(
subspaceName,
subSpaceModel.spaceModel.uuid,
subSpaceModel.uuid,
);
subSpaceModel.subspaceName = subspaceName;
await queryRunner.manager.save(subSpaceModel);
}
}
extractSubspaceTags(
extractTagsFromSubspaceModels(
subspaceModels: CreateSubspaceModelDto[],
): ProcessTagDto[] {
return subspaceModels.flatMap((subspace) => subspace.tags || []);
}
private async createSubspaceProductAllocations(
subspaceModel: SubspaceModelEntity,
tags: NewTagEntity[],
queryRunner?: QueryRunner,
): Promise<void> {
try {
const allocations: SubspaceModelProductAllocationEntity[] = [];
for (const tag of tags) {
const existingAllocation = await (queryRunner
? queryRunner.manager.findOne(SubspaceModelProductAllocationEntity, {
where: { subspaceModel, product: tag.product },
})
: this.subspaceModelProductAllocationRepository.findOne({
where: { subspaceModel, product: tag.product },
}));
if (!existingAllocation) {
const allocation = queryRunner
? queryRunner.manager.create(SubspaceModelProductAllocationEntity, {
subspaceModel,
product: tag.product,
tags: [tag],
})
: this.subspaceModelProductAllocationRepository.create({
subspaceModel,
product: tag.product,
tags: [tag],
});
allocations.push(allocation);
} else {
if (
!existingAllocation.tags.some(
(existingTag) => existingTag.uuid === tag.uuid,
)
) {
existingAllocation.tags.push(tag);
if (queryRunner) {
await queryRunner.manager.save(
SubspaceModelProductAllocationEntity,
existingAllocation,
);
} else {
await this.subspaceModelProductAllocationRepository.save(
existingAllocation,
);
}
}
}
}
if (allocations.length > 0) {
if (queryRunner) {
await queryRunner.manager.save(
SubspaceModelProductAllocationEntity,
allocations,
);
} else {
await this.subspaceModelProductAllocationRepository.save(allocations);
}
}
} catch (error) {
throw new HttpException(
'An unexpected error occurred while creating subspace product allocations',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
extractTagsFromModifiedSubspaceModels(
subspaceModels: ModifySubspaceModelDto[],
): ModifyTagModelDto[] {
return subspaceModels.flatMap((subspace) => subspace.tags || []);
}
}