diff --git a/libs/common/src/modules/space-model/entities/space-model.entity.ts b/libs/common/src/modules/space-model/entities/space-model.entity.ts index 18d0688..648e90d 100644 --- a/libs/common/src/modules/space-model/entities/space-model.entity.ts +++ b/libs/common/src/modules/space-model/entities/space-model.entity.ts @@ -1,11 +1,4 @@ -import { - Entity, - Column, - OneToMany, - ManyToOne, - JoinColumn, - Unique, -} from 'typeorm'; +import { Entity, Column, OneToMany, ManyToOne, JoinColumn } from 'typeorm'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { SpaceModelDto } from '../dtos'; import { SubspaceModelEntity } from './subspace-model'; @@ -14,7 +7,6 @@ import { SpaceEntity } from '../../space/entities'; import { TagModel } from './tag-model.entity'; @Entity({ name: 'space-model' }) -@Unique(['modelName', 'project']) export class SpaceModelEntity extends AbstractEntity { @Column({ type: 'uuid', diff --git a/src/space-model/commands/propogate-subspace-update-command.ts b/src/space-model/commands/propogate-subspace-update-command.ts index 3b8ccdb..a2b5203 100644 --- a/src/space-model/commands/propogate-subspace-update-command.ts +++ b/src/space-model/commands/propogate-subspace-update-command.ts @@ -1,12 +1,14 @@ import { ICommand } from '@nestjs/cqrs'; import { SpaceModelEntity } from '@app/common/modules/space-model'; import { ModifyspaceModelPayload } from '../interfaces'; +import { QueryRunner } from 'typeorm'; export class PropogateUpdateSpaceModelCommand implements ICommand { constructor( public readonly param: { spaceModel: SpaceModelEntity; modifiedSpaceModels: ModifyspaceModelPayload; + queryRunner: QueryRunner; }, ) {} } diff --git a/src/space-model/handlers/propate-subspace-handler.ts b/src/space-model/handlers/propate-subspace-handler.ts index f59aeb2..7c0bfd9 100644 --- a/src/space-model/handlers/propate-subspace-handler.ts +++ b/src/space-model/handlers/propate-subspace-handler.ts @@ -29,36 +29,37 @@ export class PropogateUpdateSpaceModelHandler ) {} async execute(command: PropogateUpdateSpaceModelCommand): Promise { - const { spaceModel, modifiedSpaceModels } = command.param; - const queryRunner = this.dataSource.createQueryRunner(); + const { spaceModel, modifiedSpaceModels, queryRunner } = command.param; try { - await queryRunner.connect(); - await queryRunner.startTransaction(); - const spaces = await this.spaceRepository.find({ + const spaces = await queryRunner.manager.find(SpaceEntity, { where: { spaceModel }, }); - if ( - modifiedSpaceModels.modifiedSubspaceModels.addedSubspaceModels.length > - 0 - ) { + + const { modifiedSubspaceModels = {}, modifiedTags = {} } = + modifiedSpaceModels; + + const { + addedSubspaceModels = [], + updatedSubspaceModels = [], + deletedSubspaceModels = [], + } = modifiedSubspaceModels; + + const { added = [], updated = [], deleted = [] } = modifiedTags; + + if (addedSubspaceModels.length > 0) { await this.addSubspaceModels( modifiedSpaceModels.modifiedSubspaceModels.addedSubspaceModels, spaces, queryRunner, ); - } else if ( - modifiedSpaceModels.modifiedSubspaceModels.updatedSubspaceModels - .length > 0 - ) { + } else if (updatedSubspaceModels.length > 0) { await this.updateSubspaceModels( modifiedSpaceModels.modifiedSubspaceModels.updatedSubspaceModels, queryRunner, ); } - if ( - modifiedSpaceModels.modifiedSubspaceModels.deletedSubspaceModels?.length - ) { + if (deletedSubspaceModels.length > 0) { const dtos: ModifySubspaceDto[] = modifiedSpaceModels.modifiedSubspaceModels.deletedSubspaceModels.map( (model) => ({ @@ -69,7 +70,7 @@ export class PropogateUpdateSpaceModelHandler await this.subSpaceService.modifySubSpace(dtos, queryRunner); } - if (modifiedSpaceModels.modifiedTags.added.length > 0) { + if (added.length > 0) { await this.createTags( modifiedSpaceModels.modifiedTags.added, queryRunner, @@ -78,26 +79,21 @@ export class PropogateUpdateSpaceModelHandler ); } - if (modifiedSpaceModels.modifiedTags.updated.length > 0) { + if (updated.length > 0) { await this.updateTags( modifiedSpaceModels.modifiedTags.updated, queryRunner, ); } - if (modifiedSpaceModels.modifiedTags.deleted.length > 0) { + if (deleted.length > 0) { await this.deleteTags( modifiedSpaceModels.modifiedTags.deleted, queryRunner, ); } - - await queryRunner.commitTransaction(); } catch (error) { - await queryRunner.rollbackTransaction(); - throw error; - } finally { - await queryRunner.release(); + console.error(error); } } diff --git a/src/space-model/services/space-model.service.ts b/src/space-model/services/space-model.service.ts index ed40153..723267e 100644 --- a/src/space-model/services/space-model.service.ts +++ b/src/space-model/services/space-model.service.ts @@ -7,7 +7,7 @@ import { CreateSpaceModelDto, UpdateSpaceModelDto } from '../dtos'; import { ProjectParam } from 'src/community/dtos'; import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; import { SubSpaceModelService } from './subspace/subspace-model.service'; -import { DataSource } from 'typeorm'; +import { DataSource, QueryRunner } from 'typeorm'; import { TypeORMCustomModel, TypeORMCustomModelFindAllQuery, @@ -49,7 +49,11 @@ export class SpaceModelService { try { const project = await this.validateProject(params.projectUuid); - await this.validateName(modelName, params.projectUuid); + await this.validateNameUsingQueryRunner( + modelName, + params.projectUuid, + queryRunner, + ); const spaceModel = this.spaceModelRepository.create({ modelName, @@ -140,20 +144,29 @@ 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); - const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); - await queryRunner.startTransaction(); let modifiedSubspaceModels: ModifySubspaceModelPayload = {}; let modifiedTagsModelPayload: ModifiedTagsModelPayload = {}; try { + await queryRunner.startTransaction(); + const { modelName } = dto; if (modelName) { - await this.validateName(modelName, param.projectUuid); + await this.validateNameUsingQueryRunner( + modelName, + param.projectUuid, + queryRunner, + ); spaceModel.modelName = modelName; - await queryRunner.manager.save(spaceModel); + await queryRunner.manager.save( + this.spaceModelRepository.target, + spaceModel, + ); } if (dto.subspaceModels) { @@ -182,6 +195,7 @@ export class SpaceModelService { modifiedSubspaceModels, modifiedTags: modifiedTagsModelPayload, }, + queryRunner, }), ); @@ -191,6 +205,11 @@ export class SpaceModelService { }); } catch (error) { await queryRunner.rollbackTransaction(); + + if (error instanceof HttpException) { + throw error; + } + throw new HttpException( error.message || 'Failed to update SpaceModel', HttpStatus.INTERNAL_SERVER_ERROR, @@ -271,6 +290,23 @@ export class SpaceModelService { } } + async validateNameUsingQueryRunner( + modelName: string, + projectUuid: string, + queryRunner: QueryRunner, + ): Promise { + const isModelExist = await queryRunner.manager.findOne(SpaceModelEntity, { + where: { modelName, project: { uuid: projectUuid }, disabled: false }, + }); + + if (isModelExist) { + throw new HttpException( + `Model name ${modelName} already exists in the project with UUID ${projectUuid}.`, + HttpStatus.CONFLICT, + ); + } + } + async validateSpaceModel(uuid: string): Promise { const spaceModel = await this.spaceModelRepository.findOne({ where: { diff --git a/src/space-model/services/subspace/subspace-model.service.ts b/src/space-model/services/subspace/subspace-model.service.ts index e8a7bd0..9a52565 100644 --- a/src/space-model/services/subspace/subspace-model.service.ts +++ b/src/space-model/services/subspace/subspace-model.service.ts @@ -31,38 +31,50 @@ export class SubSpaceModelService { queryRunner: QueryRunner, otherTags?: CreateTagModelDto[], ): Promise { - this.validateInputDtos(subSpaceModelDtos, spaceModel); + try { + await this.validateInputDtos(subSpaceModelDtos, spaceModel); - const subspaces = subSpaceModelDtos.map((subspaceDto) => - queryRunner.manager.create(this.subspaceModelRepository.target, { - subspaceName: subspaceDto.subspaceName, - spaceModel, - }), - ); + const subspaces = subSpaceModelDtos.map((subspaceDto) => + queryRunner.manager.create(this.subspaceModelRepository.target, { + subspaceName: subspaceDto.subspaceName, + spaceModel, + }), + ); - const savedSubspaces = await queryRunner.manager.save(subspaces); + const savedSubspaces = await queryRunner.manager.save(subspaces); - await Promise.all( - subSpaceModelDtos.map(async (dto, index) => { - const subspace = savedSubspaces[index]; + await Promise.all( + subSpaceModelDtos.map(async (dto, index) => { + const subspace = savedSubspaces[index]; - const otherDtoTags = subSpaceModelDtos - .filter((_, i) => i !== index) - .flatMap((otherDto) => otherDto.tags || []); + const otherDtoTags = subSpaceModelDtos + .filter((_, i) => i !== index) + .flatMap((otherDto) => otherDto.tags || []); - if (dto.tags?.length) { - subspace.tags = await this.tagModelService.createTags( - dto.tags, - queryRunner, - null, - subspace, - [...(otherTags || []), ...otherDtoTags], - ); - } - }), - ); + if (dto.tags?.length) { + subspace.tags = await this.tagModelService.createTags( + dto.tags, + queryRunner, + null, + subspace, + [...(otherTags || []), ...otherDtoTags], + ); + } + }), + ); - return savedSubspaces; + return savedSubspaces; + } catch (error) { + if (error instanceof HttpException) { + throw error; // Rethrow known HttpExceptions + } + + // Handle unexpected errors + throw new HttpException( + `An error occurred while creating subspace models: ${error.message}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } } async deleteSubspaceModels( @@ -104,38 +116,53 @@ export class SubSpaceModelService { spaceModel: SpaceModelEntity, queryRunner: QueryRunner, ): Promise { - const modifiedSubspaceModels: ModifySubspaceModelPayload = {}; - 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, - ); + 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, + ); + } } + 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, + ); } - return modifiedSubspaceModels; } private async handleAddAction( @@ -143,24 +170,35 @@ export class SubSpaceModelService { spaceModel: SpaceModelEntity, queryRunner: QueryRunner, ): Promise { - const createTagDtos: CreateTagModelDto[] = - subspace.tags?.map((tag) => ({ - tag: tag.tag, - productUuid: tag.productUuid, - })) || []; + try { + const createTagDtos: CreateTagModelDto[] = + subspace.tags?.map((tag) => ({ + tag: tag.tag, + productUuid: tag.productUuid, + })) || []; - const [createdSubspaceModel] = await this.createSubSpaceModels( - [ - { - subspaceName: subspace.subspaceName, - tags: createTagDtos, - }, - ], - spaceModel, - queryRunner, - ); + const [createdSubspaceModel] = await this.createSubSpaceModels( + [ + { + subspaceName: subspace.subspaceName, + tags: createTagDtos, + }, + ], + spaceModel, + queryRunner, + ); - return createdSubspaceModel; + 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( @@ -221,7 +259,7 @@ export class SubSpaceModelService { private async findOne(subspaceUuid: string): Promise { const subspace = await this.subspaceModelRepository.findOne({ where: { uuid: subspaceUuid }, - relations: ['tags'], + relations: ['tags', 'spaceModel'], }); if (!subspace) { throw new HttpException( @@ -232,59 +270,86 @@ export class SubSpaceModelService { return subspace; } - private validateInputDtos( + private async validateInputDtos( subSpaceModelDtos: CreateSubspaceModelDto[], spaceModel: SpaceModelEntity, - ): void { - if (subSpaceModelDtos.length === 0) { + ): Promise { + 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( - 'Subspace models cannot be empty.', - HttpStatus.BAD_REQUEST, + `An error occurred while validating subspace models: ${error.message}`, + HttpStatus.INTERNAL_SERVER_ERROR, ); } - this.validateName( - subSpaceModelDtos.map((dto) => dto.subspaceName), - spaceModel, - ); } private async validateName( names: string[], spaceModel: SpaceModelEntity, ): Promise { - const seenNames = new Set(); - const duplicateNames = new Set(); + try { + const seenNames = new Set(); + const duplicateNames = new Set(); - for (const name of names) { - if (!seenNames.add(name)) { - duplicateNames.add(name); + // Check for duplicate names within the input array + for (const name of names) { + if (!seenNames.add(name)) { + duplicateNames.add(name); + } } - } - if (duplicateNames.size > 0) { - throw new HttpException( - `Duplicate subspace model names found: ${[...duplicateNames].join(', ')}`, - HttpStatus.CONFLICT, - ); - } + if (duplicateNames.size > 0) { + throw new HttpException( + `Duplicate subspace model names found: ${[...duplicateNames].join(', ')}`, + HttpStatus.CONFLICT, + ); + } - const existingNames = await this.subspaceModelRepository.find({ - select: ['subspaceName'], - where: { - subspaceName: In([...seenNames]), - spaceModel: { - uuid: spaceModel.uuid, + // Check for existing names in the database + const existingNames = await this.subspaceModelRepository.find({ + select: ['subspaceName'], + where: { + subspaceName: In([...seenNames]), + spaceModel: { + uuid: spaceModel.uuid, + }, }, - }, - }); + }); - if (existingNames.length > 0) { - const existingNamesList = existingNames - .map((e) => e.subspaceName) - .join(', '); + if (existingNames.length > 0) { + const existingNamesList = existingNames + .map((e) => e.subspaceName) + .join(', '); + throw new HttpException( + `Subspace model names already exist in the space: ${existingNamesList}`, + HttpStatus.BAD_REQUEST, + ); + } + } catch (error) { + if (error instanceof HttpException) { + throw error; // Rethrow known HttpExceptions + } + + // Handle unexpected errors throw new HttpException( - `Subspace model names already exist in the space: ${existingNamesList}`, - HttpStatus.BAD_REQUEST, + `An error occurred while validating subspace model names: ${error.message}`, + HttpStatus.INTERNAL_SERVER_ERROR, ); } } diff --git a/src/space-model/services/tag-model.service.ts b/src/space-model/services/tag-model.service.ts index d08fe38..b251b28 100644 --- a/src/space-model/services/tag-model.service.ts +++ b/src/space-model/services/tag-model.service.ts @@ -134,7 +134,11 @@ export class TagModelService { spaceModel?: SpaceModelEntity, subspaceModel?: SubspaceModelEntity, ): Promise { - const modifiedTagModels: ModifiedTagsModelPayload = {}; + const modifiedTagModels: ModifiedTagsModelPayload = { + added: [], + updated: [], + deleted: [], + }; try { for (const tag of tags) { if (tag.action === ModifyAction.ADD) { @@ -190,25 +194,35 @@ export class TagModelService { productUuid: string, spaceModel: SpaceModelEntity, ): Promise { - const tagExists = await this.tagModelRepository.exists({ - where: [ - { - tag, - spaceModel: { uuid: spaceModel.uuid }, - product: { uuid: productUuid }, - disabled: false, - }, - { - tag, - subspaceModel: { spaceModel: { uuid: spaceModel.uuid } }, - product: { uuid: productUuid }, - disabled: false, - }, - ], - }); + try { + const tagExists = await this.tagModelRepository.exists({ + where: [ + { + tag, + spaceModel: { uuid: spaceModel.uuid }, + product: { uuid: productUuid }, + disabled: false, + }, + { + tag, + subspaceModel: { spaceModel: { uuid: spaceModel.uuid } }, + product: { uuid: productUuid }, + disabled: false, + }, + ], + }); - if (tagExists) { - throw new HttpException(`Tag can't be reused`, HttpStatus.CONFLICT); + if (tagExists) { + throw new HttpException(`Tag can't be reused`, HttpStatus.CONFLICT); + } + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + throw new HttpException( + `An error occurred while checking tag reuse: ${error.message}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); } } @@ -218,31 +232,46 @@ export class TagModelService { spaceModel?: SpaceModelEntity, subspaceModel?: SubspaceModelEntity, ): Promise { - const product = await this.productService.findOne(tagDto.productUuid); + try { + const product = await this.productService.findOne(tagDto.productUuid); - if (!product) { + 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 if (subspaceModel && subspaceModel.spaceModel) { + await this.checkTagReuse( + tagDto.tag, + tagDto.productUuid, + subspaceModel.spaceModel, + ); + } else { + throw new HttpException( + `Invalid subspaceModel or spaceModel provided.`, + HttpStatus.BAD_REQUEST, + ); + } + + return queryRunner.manager.create(TagModel, { + tag: tagDto.tag, + product: product.data, + spaceModel, + subspaceModel, + }); + } catch (error) { + if (error instanceof HttpException) { + throw error; + } throw new HttpException( - `Product with UUID ${tagDto.productUuid} not found.`, - HttpStatus.NOT_FOUND, + `An error occurred while preparing the tag entity: ${error.message}`, + HttpStatus.INTERNAL_SERVER_ERROR, ); } - - 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, - }); } async getTagByUuid(uuid: string): Promise {