diff --git a/libs/common/src/modules/space-model/entities/tag-model.entity.ts b/libs/common/src/modules/space-model/entities/tag-model.entity.ts index 3f7805c..e2a70ee 100644 --- a/libs/common/src/modules/space-model/entities/tag-model.entity.ts +++ b/libs/common/src/modules/space-model/entities/tag-model.entity.ts @@ -1,11 +1,4 @@ -import { - Column, - Entity, - JoinColumn, - ManyToOne, - OneToMany, - Unique, -} from 'typeorm'; +import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { TagModelDto } from '../dtos/tag-model.dto'; import { SpaceModelEntity } from './space-model.entity'; @@ -14,7 +7,6 @@ import { ProductEntity } from '../../product/entities'; import { TagEntity } from '../../space/entities/tag.entity'; @Entity({ name: 'tag_model' }) -@Unique(['tag', 'product', 'spaceModel', 'subspaceModel']) export class TagModel extends AbstractEntity { @Column({ type: 'varchar', length: 255 }) tag: string; diff --git a/src/space-model/dtos/tag-model-dtos/create-tag-model.dto.ts b/src/space-model/dtos/tag-model-dtos/create-tag-model.dto.ts index 5f4ec66..c271eef 100644 --- a/src/space-model/dtos/tag-model-dtos/create-tag-model.dto.ts +++ b/src/space-model/dtos/tag-model-dtos/create-tag-model.dto.ts @@ -1,5 +1,5 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; export class CreateTagModelDto { @ApiProperty({ @@ -10,6 +10,14 @@ export class CreateTagModelDto { @IsString() tag: string; + @ApiPropertyOptional({ + description: 'UUID of the tag model (required for update/delete)', + example: '123e4567-e89b-12d3-a456-426614174000', + }) + @IsOptional() + @IsString() + uuid?: string; + @ApiProperty({ description: 'ID of the product associated with the tag', example: '123e4567-e89b-12d3-a456-426614174000', diff --git a/src/space-model/services/space-model.service.ts b/src/space-model/services/space-model.service.ts index 5ef531c..05906ca 100644 --- a/src/space-model/services/space-model.service.ts +++ b/src/space-model/services/space-model.service.ts @@ -177,7 +177,6 @@ export class SpaceModelService { async update(dto: UpdateSpaceModelDto, param: SpaceModelParam) { const queryRunner = this.dataSource.createQueryRunner(); - await this.validateProject(param.projectUuid); const spaceModel = await this.validateSpaceModel(param.spaceModelUuid); await queryRunner.connect(); @@ -201,10 +200,20 @@ 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( - dto.subspaceModels, + modifiedSubspaces, spaceModel, queryRunner, ); @@ -212,9 +221,10 @@ export class SpaceModelService { if (dto.tags) { modifiedTagsModelPayload = await this.tagModelService.modifyTags( - dto.tags, + spaceTagsAfterMove, queryRunner, spaceModel, + null, ); } @@ -236,7 +246,6 @@ export class SpaceModelService { }); } catch (error) { await queryRunner.rollbackTransaction(); - if (error instanceof HttpException) { throw error; } diff --git a/src/space-model/services/subspace/subspace-model.service.ts b/src/space-model/services/subspace/subspace-model.service.ts index f309856..dd3fe91 100644 --- a/src/space-model/services/subspace/subspace-model.service.ts +++ b/src/space-model/services/subspace/subspace-model.service.ts @@ -50,8 +50,7 @@ export class SubSpaceModelService { const otherDtoTags = subSpaceModelDtos .filter((_, i) => i !== index) .flatMap((otherDto) => otherDto.tags || []); - - if (dto.tags?.length) { + if (dto.tags && dto.tags.length > 0) { subspace.tags = await this.tagModelService.createTags( dto.tags, queryRunner, @@ -174,6 +173,7 @@ export class SubSpaceModelService { const createTagDtos: CreateTagModelDto[] = subspace.tags?.map((tag) => ({ tag: tag.tag, + uuid: tag.uuid, productUuid: tag.productUuid, })) || []; diff --git a/src/space-model/services/tag-model.service.ts b/src/space-model/services/tag-model.service.ts index 14f2cf2..0f77b3d 100644 --- a/src/space-model/services/tag-model.service.ts +++ b/src/space-model/services/tag-model.service.ts @@ -6,7 +6,11 @@ import { } from '@app/common/modules/space-model/entities'; import { SubspaceModelEntity } from '@app/common/modules/space-model/entities'; import { TagModelRepository } from '@app/common/modules/space-model'; -import { CreateTagModelDto, ModifyTagModelDto } from '../dtos'; +import { + CreateTagModelDto, + ModifySubspaceModelDto, + ModifyTagModelDto, +} from '../dtos'; import { ProductService } from 'src/product/services'; import { ModifyAction } from '@app/common/constants/modify-action.enum'; import { ModifiedTagsModelPayload } from '../interfaces'; @@ -24,6 +28,7 @@ export class TagModelService { spaceModel?: SpaceModelEntity, subspaceModel?: SubspaceModelEntity, additionalTags?: CreateTagModelDto[], + tagsToDelete?: ModifyTagModelDto[], ): Promise { if (!tags.length) { throw new HttpException('Tags cannot be empty.', HttpStatus.BAD_REQUEST); @@ -39,11 +44,63 @@ export class TagModelService { ); } + const tagEntitiesToCreate = tags.filter((tagDto) => tagDto.uuid === null); + const tagEntitiesToUpdate = tags.filter((tagDto) => tagDto.uuid !== null); + + try { + const createdTags = await this.bulkSaveTags( + tagEntitiesToCreate, + queryRunner, + spaceModel, + subspaceModel, + tagsToDelete, + ); + + // Update existing tags + const updatedTags = await this.moveTags( + tagEntitiesToUpdate, + queryRunner, + spaceModel, + subspaceModel, + ); + + // Combine created and updated tags + return [...createdTags, ...updatedTags]; + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + + throw new HttpException( + `Failed to create tag models due to an unexpected error.: ${error}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async bulkSaveTags( + tags: CreateTagModelDto[], + queryRunner: QueryRunner, + spaceModel?: SpaceModelEntity, + subspaceModel?: SubspaceModelEntity, + tagsToDelete?: ModifyTagModelDto[], + ): Promise { + if (!tags.length) { + return []; + } + const tagEntities = await Promise.all( - tags.map(async (tagDto) => - this.prepareTagEntity(tagDto, queryRunner, spaceModel, subspaceModel), + tags.map((tagDto) => + this.prepareTagEntity( + tagDto, + queryRunner, + spaceModel, + subspaceModel, + tagsToDelete, + ), ), ); + try { return await queryRunner.manager.save(tagEntities); } catch (error) { @@ -52,7 +109,66 @@ export class TagModelService { } throw new HttpException( - 'Failed to save tag models due to an unexpected error.', + `Failed to save tag models due to an unexpected error: ${error.message}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async moveTags( + tags: CreateTagModelDto[], + queryRunner: QueryRunner, + spaceModel?: SpaceModelEntity, + subspaceModel?: SubspaceModelEntity, + ): Promise { + if (!tags.length) { + return []; + } + + try { + return await Promise.all( + tags.map(async (tagDto) => { + try { + const tag = await this.getTagByUuid(tagDto.uuid); + if (!tag) { + throw new HttpException( + `Tag with UUID ${tagDto.uuid} not found.`, + HttpStatus.NOT_FOUND, + ); + } + + if (subspaceModel && subspaceModel.spaceModel) { + await queryRunner.manager.update( + this.tagModelRepository.target, + { uuid: tag.uuid }, + { subspaceModel, spaceModel: null }, + ); + tag.subspaceModel = subspaceModel; + } + + if (subspaceModel === null && spaceModel) { + await queryRunner.manager.update( + this.tagModelRepository.target, + { uuid: tag.uuid }, + { subspaceModel: null, spaceModel: spaceModel }, + ); + tag.subspaceModel = null; + tag.spaceModel = spaceModel; + } + + return tag; + } catch (error) { + console.error( + `Error moving tag with UUID ${tagDto.uuid}: ${error.message}`, + ); + throw error; // Re-throw the error to propagate it to the parent Promise.all + } + }), + ); + } catch (error) { + console.error(`Error in moveTags: ${error.message}`); + throw new HttpException( + `Failed to move tags due to an unexpected error: ${error.message}`, HttpStatus.INTERNAL_SERVER_ERROR, ); } @@ -149,10 +265,15 @@ export class TagModelService { deleted: [], }; try { + const tagsToDelete = tags.filter( + (tag) => tag.action === ModifyAction.DELETE, + ); + for (const tag of tags) { if (tag.action === ModifyAction.ADD) { const createTagDto: CreateTagModelDto = { tag: tag.tag as string, + uuid: tag.uuid, productUuid: tag.productUuid as string, }; @@ -161,6 +282,8 @@ export class TagModelService { queryRunner, spaceModel, subspaceModel, + null, + tagsToDelete, ); modifiedTagModels.added.push(...newModel); } else if (tag.action === ModifyAction.UPDATE) { @@ -202,9 +325,11 @@ export class TagModelService { tag: string, productUuid: string, spaceModel: SpaceModelEntity, + tagsToDelete?: ModifyTagModelDto[], ): Promise { try { - const tagExists = await this.tagModelRepository.exists({ + // Query to find existing tags + const tagExists = await this.tagModelRepository.find({ where: [ { tag, @@ -221,7 +346,16 @@ export class TagModelService { ], }); - if (tagExists) { + // Remove tags that are marked for deletion + const filteredTagExists = tagExists.filter( + (existingTag) => + !tagsToDelete?.some( + (deleteTag) => deleteTag.uuid === existingTag.uuid, + ), + ); + + // If any tags remain, throw an exception + if (filteredTagExists.length > 0) { throw new HttpException( `Tag ${tag} can't be reused`, HttpStatus.CONFLICT, @@ -231,6 +365,7 @@ export class TagModelService { if (error instanceof HttpException) { throw error; } + console.error(`Error while checking tag reuse: ${error.message}`); throw new HttpException( `An error occurred while checking tag reuse: ${error.message}`, HttpStatus.INTERNAL_SERVER_ERROR, @@ -243,6 +378,7 @@ export class TagModelService { queryRunner: QueryRunner, spaceModel?: SpaceModelEntity, subspaceModel?: SubspaceModelEntity, + tagsToDelete?: ModifyTagModelDto[], ): Promise { try { const product = await this.productService.findOne(tagDto.productUuid); @@ -255,7 +391,12 @@ export class TagModelService { } if (spaceModel) { - await this.checkTagReuse(tagDto.tag, tagDto.productUuid, spaceModel); + await this.checkTagReuse( + tagDto.tag, + tagDto.productUuid, + spaceModel, + tagsToDelete, + ); } else if (subspaceModel && subspaceModel.spaceModel) { await this.checkTagReuse( tagDto.tag, @@ -268,12 +409,14 @@ export class TagModelService { HttpStatus.BAD_REQUEST, ); } + const tagSpace = + spaceModel !== null ? spaceModel : subspaceModel.spaceModel; return queryRunner.manager.create(TagModel, { tag: tagDto.tag, product: product.data, - spaceModel, - subspaceModel, + spaceModel: tagSpace, + subspaceModel: subspaceModel, }); } catch (error) { if (error instanceof HttpException) { @@ -333,4 +476,73 @@ export class TagModelService { return existingTag; } + + getSubspaceTagsToBeAdded( + spaceTags: ModifyTagModelDto[], + subspaceModels: ModifySubspaceModelDto[], + ): ModifyTagModelDto[] { + if (!subspaceModels || subspaceModels.length === 0) { + return spaceTags; + } + + const spaceTagsToDelete = spaceTags.filter( + (tag) => tag.action === 'delete', + ); + + const tagsToAdd = subspaceModels.flatMap( + (subspace) => subspace.tags?.filter((tag) => tag.action === 'add') || [], + ); + + const commonTagUuids = new Set( + tagsToAdd + .filter((tagToAdd) => + spaceTagsToDelete.some( + (tagToDelete) => tagToAdd.uuid === tagToDelete.uuid, + ), + ) + .map((tag) => tag.uuid), + ); + + const remainingTags = spaceTags.filter( + (tag) => !commonTagUuids.has(tag.uuid), // Exclude tags in commonTagUuids + ); + + return remainingTags; + } + + getModifiedSubspaces( + spaceTags: ModifyTagModelDto[], + subspaceModels: ModifySubspaceModelDto[], + ): ModifySubspaceModelDto[] { + if (!subspaceModels || subspaceModels.length === 0) { + return []; + } + + // Extract tags marked for addition in spaceTags + const spaceTagsToAdd = spaceTags.filter((tag) => tag.action === 'add'); + + // Find UUIDs of tags that are common between spaceTagsToAdd and subspace tags marked for deletion + const commonTagUuids = new Set( + spaceTagsToAdd + .flatMap((tagToAdd) => + subspaceModels.flatMap( + (subspace) => + subspace.tags?.filter( + (tagToDelete) => + tagToDelete.action === 'delete' && + tagToAdd.uuid === tagToDelete.uuid, + ) || [], + ), + ) + .map((tag) => tag.uuid), + ); + + // Modify subspaceModels by removing tags with UUIDs present in commonTagUuids + const modifiedSubspaces = subspaceModels.map((subspace) => ({ + ...subspace, + tags: subspace.tags?.filter((tag) => !commonTagUuids.has(tag.uuid)) || [], + })); + + return modifiedSubspaces; + } }