diff --git a/libs/common/src/constants/modify-action.enum.ts b/libs/common/src/constants/modify-action.enum.ts new file mode 100644 index 0000000..28d6f77 --- /dev/null +++ b/libs/common/src/constants/modify-action.enum.ts @@ -0,0 +1,5 @@ +export enum ModifyAction { + ADD = 'add', + UPDATE = 'update', + DELETE = 'delete', +} 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 499ec58..2ff86d4 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 @@ -26,4 +26,10 @@ export class TagModel extends AbstractEntity { }) @JoinColumn({ name: 'subspace_id' }) subspaceModel: SubspaceModelEntity; + + @Column({ + nullable: false, + default: false, + }) + public disabled: boolean; } diff --git a/src/space-model/dtos/subspaces-model-dtos/index.ts b/src/space-model/dtos/subspaces-model-dtos/index.ts index 698a520..28b01ef 100644 --- a/src/space-model/dtos/subspaces-model-dtos/index.ts +++ b/src/space-model/dtos/subspaces-model-dtos/index.ts @@ -1,3 +1,4 @@ export * from './delete-subspace-model.dto'; export * from './create-subspace-model.dto'; export * from './update-subspace-model.dto'; +export * from './modify-subspace-model.dto'; diff --git a/src/space-model/dtos/subspaces-model-dtos/modify-subspace-model.dto.ts b/src/space-model/dtos/subspaces-model-dtos/modify-subspace-model.dto.ts new file mode 100644 index 0000000..9b5a2a5 --- /dev/null +++ b/src/space-model/dtos/subspaces-model-dtos/modify-subspace-model.dto.ts @@ -0,0 +1,40 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsString, IsOptional, IsArray, ValidateNested } from 'class-validator'; +import { ModifyTagModelDto } from '../tag-model-dtos'; + +export class ModifySubspaceModelDto { + @ApiProperty({ + description: 'Action to perform: add, update, or delete', + example: 'add', + }) + @IsString() + action: 'add' | 'update' | 'delete'; + + @ApiPropertyOptional({ + description: 'UUID of the subspace (required for update/delete)', + example: '123e4567-e89b-12d3-a456-426614174000', + }) + @IsOptional() + @IsString() + uuid?: string; + + @ApiPropertyOptional({ + description: 'Name of the subspace (required for add/update)', + example: 'Living Room', + }) + @IsOptional() + @IsString() + subspaceName?: string; + + @ApiPropertyOptional({ + description: + 'List of tag modifications (add/update/delete) for the subspace', + type: [ModifyTagModelDto], + }) + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => ModifyTagModelDto) + tags?: ModifyTagModelDto[]; +} diff --git a/src/space-model/dtos/tag-model-dtos/index.ts b/src/space-model/dtos/tag-model-dtos/index.ts index 75c60f8..a0f136d 100644 --- a/src/space-model/dtos/tag-model-dtos/index.ts +++ b/src/space-model/dtos/tag-model-dtos/index.ts @@ -1,2 +1,3 @@ export * from './create-tag-model.dto'; export * from './update-tag-model.dto'; +export * from './modify-tag-model.dto'; diff --git a/src/space-model/dtos/tag-model-dtos/modify-tag-model.dto.ts b/src/space-model/dtos/tag-model-dtos/modify-tag-model.dto.ts new file mode 100644 index 0000000..e3d390d --- /dev/null +++ b/src/space-model/dtos/tag-model-dtos/modify-tag-model.dto.ts @@ -0,0 +1,37 @@ +import { ModifyAction } from '@app/common/constants/modify-action.enum'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsString, IsOptional, IsEnum } from 'class-validator'; + +export class ModifyTagModelDto { + @ApiProperty({ + description: 'Action to perform: add, update, or delete', + example: ModifyAction.ADD, + }) + @IsEnum(ModifyAction) + action: ModifyAction; + + @ApiPropertyOptional({ + description: 'UUID of the tag (required for update/delete)', + example: '123e4567-e89b-12d3-a456-426614174000', + }) + @IsOptional() + @IsString() + uuid?: string; + + @ApiPropertyOptional({ + description: 'Name of the tag (required for add/update)', + example: 'Temperature Sensor', + }) + @IsOptional() + @IsString() + tag?: string; + + @ApiPropertyOptional({ + description: + 'UUID of the product associated with the tag (required for add)', + example: 'c789a91e-549a-4753-9006-02f89e8170e0', + }) + @IsOptional() + @IsString() + productUuid?: string; +} diff --git a/src/space-model/dtos/update-space-model.dto.ts b/src/space-model/dtos/update-space-model.dto.ts index 7a7051b..d1110ea 100644 --- a/src/space-model/dtos/update-space-model.dto.ts +++ b/src/space-model/dtos/update-space-model.dto.ts @@ -1,11 +1,13 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { IsArray, IsOptional, IsString, ValidateNested } from 'class-validator'; import { CreateSubspaceModelDto } from './subspaces-model-dtos/create-subspace-model.dto'; import { Type } from 'class-transformer'; import { DeleteSubspaceModelDto, + ModifySubspaceModelDto, UpdateSubspaceModelDto, } from './subspaces-model-dtos'; +import { ModifyTagModelDto } from './tag-model-dtos'; export class ModifySubspacesModelDto { @ApiProperty({ @@ -51,6 +53,24 @@ export class UpdateSpaceModelDto { @IsString() modelName?: string; + @ApiPropertyOptional({ + description: 'List of subspace modifications (add/update/delete)', + type: [ModifySubspaceModelDto], + }) @IsOptional() - subspaceModels?: ModifySubspacesModelDto; + @IsArray() + @ValidateNested({ each: true }) + @Type(() => ModifySubspaceModelDto) + subspaceModels?: ModifySubspaceModelDto[]; + + @ApiPropertyOptional({ + description: + 'List of tag modifications (add/update/delete) for the space model', + type: [ModifyTagModelDto], + }) + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => ModifyTagModelDto) + tags?: ModifyTagModelDto[]; } diff --git a/src/space-model/services/space-model.service.ts b/src/space-model/services/space-model.service.ts index 3227f9b..8e08ae3 100644 --- a/src/space-model/services/space-model.service.ts +++ b/src/space-model/services/space-model.service.ts @@ -42,16 +42,7 @@ export class SpaceModelService { try { const project = await this.validateProject(params.projectUuid); - const isModelExist = await this.validateName( - modelName, - params.projectUuid, - ); - if (isModelExist) { - throw new HttpException( - `Model name "${modelName}" already exists in this project ${params.projectUuid}.`, - HttpStatus.CONFLICT, - ); - } + await this.validateName(modelName, params.projectUuid); const spaceModel = this.spaceModelRepository.create({ modelName, @@ -148,9 +139,11 @@ export class SpaceModelService { await queryRunner.startTransaction(); try { const { modelName } = dto; - if (modelName) spaceModel.modelName = modelName; - - await queryRunner.manager.save(spaceModel); + if (modelName) { + await this.validateName(modelName, param.projectUuid); + spaceModel.modelName = modelName; + await queryRunner.manager.save(spaceModel); + } if (dto.subspaceModels) { await this.subSpaceModelService.modifySubSpaceModels( @@ -159,7 +152,6 @@ export class SpaceModelService { queryRunner, ); } - await queryRunner.commitTransaction(); return new SuccessResponseDto({ @@ -177,11 +169,17 @@ export class SpaceModelService { } } - async validateName(modelName: string, projectUuid: string): Promise { - const isModelExist = await this.spaceModelRepository.exists({ + async validateName(modelName: string, projectUuid: string): Promise { + const isModelExist = await this.spaceModelRepository.findOne({ where: { modelName, project: { uuid: projectUuid } }, }); - return isModelExist; + + if (isModelExist) { + throw new HttpException( + `Model name ${modelName} already exists in the project with UUID ${projectUuid}.`, + HttpStatus.CONFLICT, + ); + } } async validateSpaceModel(uuid: string): Promise { diff --git a/src/space-model/services/subspace/subspace-model.service.ts b/src/space-model/services/subspace/subspace-model.service.ts index 6a719c0..4c57673 100644 --- a/src/space-model/services/subspace/subspace-model.service.ts +++ b/src/space-model/services/subspace/subspace-model.service.ts @@ -7,16 +7,17 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { CreateSubspaceModelDto, UpdateSubspaceModelDto, - ModifySubspacesModelDto, CreateTagModelDto, } from '../../dtos'; import { QueryRunner } from 'typeorm'; import { IDeletedSubsaceModelInterface, - IModifySubspaceModelInterface, IUpdateSubspaceModelInterface, } from 'src/space-model/interfaces'; -import { DeleteSubspaceModelDto } from 'src/space-model/dtos/subspaces-model-dtos'; +import { + DeleteSubspaceModelDto, + ModifySubspaceModelDto, +} from 'src/space-model/dtos/subspaces-model-dtos'; import { TagModelService } from '../tag-model.service'; @Injectable() @@ -25,11 +26,12 @@ export class SubSpaceModelService { private readonly subspaceModelRepository: SubspaceModelRepository, private readonly tagModelService: TagModelService, ) {} + async createSubSpaceModels( subSpaceModelDtos: CreateSubspaceModelDto[], spaceModel: SpaceModelEntity, queryRunner: QueryRunner, - otherTags: CreateTagModelDto[], + otherTags?: CreateTagModelDto[], ): Promise { this.validateInputDtos(subSpaceModelDtos); @@ -62,6 +64,9 @@ export class SubSpaceModelService { return savedSubspaces; } catch (error) { + if (error instanceof HttpException) { + throw error; + } throw new HttpException( error.message || 'Failed to create subspaces.', HttpStatus.INTERNAL_SERVER_ERROR, @@ -183,7 +188,6 @@ export class SubSpaceModelService { where: { uuid: subspaceUuid, }, - relations: ['productModels', 'productModels.itemModels'], }); if (!subspace) { throw new HttpException( @@ -226,43 +230,49 @@ export class SubSpaceModelService { } async modifySubSpaceModels( - dto: ModifySubspacesModelDto, + subspaceDtos: ModifySubspaceModelDto[], spaceModel: SpaceModelEntity, queryRunner: QueryRunner, - ): Promise { - const subspaces: IModifySubspaceModelInterface = { - spaceModelUuid: spaceModel.uuid, - }; - + ) { try { - const actions = []; + for (const subspace of subspaceDtos) { + if (subspace.action === 'add') { + const createTagDtos: CreateTagModelDto[] = + subspace.tags?.map((tag) => ({ + tag: tag.tag as string, + productUuid: tag.productUuid as string, + })) || []; + await this.createSubSpaceModels( + [{ subspaceName: subspace.subspaceName, tags: createTagDtos }], + spaceModel, + queryRunner, + ); + } else if (subspace.action === 'update') { + const existingSubspace = await this.findOne(subspace.uuid); - if (dto.add) { + if (!existingSubspace) { + throw new HttpException( + `Subspace with ID ${subspace.uuid} not found.`, + HttpStatus.NOT_FOUND, + ); + } + + existingSubspace.subspaceName = + subspace.subspaceName || existingSubspace.subspaceName; + + const updatedSubspace = + await queryRunner.manager.save(existingSubspace); + + if (subspace.tags) { + await this.tagModelService.modifyTags( + subspace.tags, + queryRunner, + null, + updatedSubspace, + ); + } + } } - - if (dto.update) { - actions.push( - this.updateSubspaceModels(dto.update, spaceModel, queryRunner).then( - (updatedSubspaces) => { - subspaces.update = updatedSubspaces; - }, - ), - ); - } - - if (dto.delete) { - actions.push( - this.deleteSubspaceModels(dto.delete, queryRunner).then( - (deletedSubspaces) => { - subspaces.delete = deletedSubspaces; - }, - ), - ); - } - - await Promise.all(actions); - - return subspaces; } catch (error) { throw new HttpException( error.message || 'Failed to modify SpaceModels', diff --git a/src/space-model/services/tag-model.service.ts b/src/space-model/services/tag-model.service.ts index 6a057cf..21be316 100644 --- a/src/space-model/services/tag-model.service.ts +++ b/src/space-model/services/tag-model.service.ts @@ -6,7 +6,7 @@ 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, UpdateTagModelDto } from '../dtos'; +import { CreateTagModelDto, ModifyTagModelDto } from '../dtos'; import { ProductService } from 'src/product/services'; @Injectable() @@ -21,44 +21,34 @@ export class TagModelService { queryRunner: QueryRunner, spaceModel?: SpaceModelEntity, subspaceModel?: SubspaceModelEntity, - otherTags?: CreateTagModelDto[], + additionalTags?: CreateTagModelDto[], ): Promise { - let alltags: CreateTagModelDto[] = []; if (!tags.length) { throw new HttpException('Tags cannot be empty.', HttpStatus.BAD_REQUEST); } - if (otherTags) { - alltags = [...tags, ...otherTags]; - } - const duplicates = this.checkForDuplicates(alltags); - if (duplicates.length > 0) { + + const combinedTags = additionalTags ? [...tags, ...additionalTags] : tags; + const duplicateTags = this.findDuplicateTags(combinedTags); + + if (duplicateTags.length > 0) { throw new HttpException( - `Duplicate tags found for the same product: ${duplicates.join(', ')}`, + `Duplicate tags found for the same product: ${duplicateTags.join(', ')}`, HttpStatus.BAD_REQUEST, ); } + const tagEntities = await Promise.all( - tags.map(async (tagDto) => { - const product = await this.productService.findOne(tagDto.productUuid); - if (!product) { - throw new HttpException( - `Product with UUID ${tagDto.productUuid} not found.`, - HttpStatus.NOT_FOUND, - ); - } - - return queryRunner.manager.create(TagModel, { - tag: tagDto.tag, - product: product.data, - spaceModel, - subspaceModel, - }); - }), + tags.map(async (tagDto) => + this.prepareTagEntity(tagDto, queryRunner, spaceModel, subspaceModel), + ), ); - try { return await queryRunner.manager.save(tagEntities); } catch (error) { + if (error instanceof HttpException) { + throw error; + } + throw new HttpException( 'Failed to save tag models due to an unexpected error.', HttpStatus.INTERNAL_SERVER_ERROR, @@ -66,27 +56,34 @@ export class TagModelService { } } - async updateTags(tags: UpdateTagModelDto[], queryRunner: QueryRunner) { + async updateTag( + tag: ModifyTagModelDto, + queryRunner: QueryRunner, + spaceModel?: SpaceModelEntity, + subspaceModel?: SubspaceModelEntity, + ): Promise { try { - const updatePromises = tags.map(async (tagDto) => { - const existingTag = await this.tagModelRepository.findOne({ - where: { uuid: tagDto.uuid }, - }); + const existingTag = await this.getTagByUuid(tag.uuid); - if (!existingTag) { - throw new HttpException( - `Tag with ID ${tagDto.uuid} not found`, - HttpStatus.NOT_FOUND, - ); - } + if (spaceModel) { + await this.checkTagReuse(tag.tag, existingTag.product.uuid, spaceModel); + } else { + await this.checkTagReuse( + tag.tag, + existingTag.product.uuid, + subspaceModel.spaceModel, + ); + } - existingTag.tag = tagDto.tag || existingTag.tag; + if (tag.tag) { + existingTag.tag = tag.tag; + } - return queryRunner.manager.save(existingTag); - }); - - return await Promise.all(updatePromises); + return await queryRunner.manager.save(existingTag); } catch (error) { + if (error instanceof HttpException) { + throw error; + } throw new HttpException( error.message || 'Failed to update tags', HttpStatus.INTERNAL_SERVER_ERROR, @@ -103,6 +100,9 @@ export class TagModelService { await Promise.all(deletePromises); return { message: 'Tags deleted successfully', tagUuids }; } catch (error) { + if (error instanceof HttpException) { + throw error; + } throw new HttpException( error.message || 'Failed to delete tags', HttpStatus.INTERNAL_SERVER_ERROR, @@ -110,7 +110,7 @@ export class TagModelService { } } - private checkForDuplicates(tags: CreateTagModelDto[]): string[] { + private findDuplicateTags(tags: CreateTagModelDto[]): string[] { const seen = new Map(); const duplicates: string[] = []; @@ -125,4 +125,119 @@ export class TagModelService { return duplicates; } + + async modifyTags( + tags: ModifyTagModelDto[], + queryRunner: QueryRunner, + spaceModel?: SpaceModelEntity, + subspaceModel?: SubspaceModelEntity, + ): Promise { + try { + for (const tag of tags) { + if (tag.action === 'add') { + const createTagDto: CreateTagModelDto = { + tag: tag.tag as string, + productUuid: tag.productUuid as string, + }; + + await this.createTags( + [createTagDto], + queryRunner, + spaceModel, + subspaceModel, + ); + } else if (tag.action === 'update') { + await this.updateTag(tag, queryRunner, spaceModel, subspaceModel); + } else if (tag.action === 'delete') { + await queryRunner.manager.update( + this.tagModelRepository.target, + { uuid: tag.uuid }, + { disabled: true }, + ); + } else { + throw new HttpException( + `Invalid action "${tag.action}" provided.`, + HttpStatus.BAD_REQUEST, + ); + } + } + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + + throw new HttpException( + `An error occurred while modifying tags: ${error.message}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + private async checkTagReuse( + tag: string, + productUuid: string, + spaceModel: SpaceModelEntity, + ): Promise { + const isTagInSpaceModel = await this.tagModelRepository.exists({ + where: { tag, spaceModel, product: { uuid: productUuid } }, + }); + const isTagInSubspaceModel = await this.tagModelRepository.exists({ + where: { + tag, + subspaceModel: { spaceModel }, + product: { uuid: productUuid }, + }, + }); + + if (isTagInSpaceModel || isTagInSubspaceModel) { + throw new HttpException(`Tag can't be reused`, HttpStatus.CONFLICT); + } + } + + private async prepareTagEntity( + tagDto: CreateTagModelDto, + queryRunner: QueryRunner, + spaceModel?: SpaceModelEntity, + subspaceModel?: SubspaceModelEntity, + ): Promise { + const product = await this.productService.findOne(tagDto.productUuid); + + if (!product) { + throw new HttpException( + `Product with UUID ${tagDto.productUuid} not found.`, + HttpStatus.NOT_FOUND, + ); + } + + if (spaceModel) { + await this.checkTagReuse(tagDto.tag, tagDto.productUuid, spaceModel); + } else { + await this.checkTagReuse( + tagDto.tag, + tagDto.productUuid, + subspaceModel.spaceModel, + ); + } + + return queryRunner.manager.create(TagModel, { + tag: tagDto.tag, + product: product.data, + spaceModel, + subspaceModel, + }); + } + + private async getTagByUuid(uuid: string): Promise { + const tag = await this.tagModelRepository.findOne({ + where: { uuid }, + relations: ['product'], + }); + if (!tag) { + throw new HttpException( + `Tag with ID ${uuid} not found.`, + HttpStatus.NOT_FOUND, + ); + } + return tag; + } }