diff --git a/src/space-model/services/space-model.service.ts b/src/space-model/services/space-model.service.ts index 40d9970..ad89d37 100644 --- a/src/space-model/services/space-model.service.ts +++ b/src/space-model/services/space-model.service.ts @@ -199,6 +199,12 @@ export class SpaceModelService { ); } + if (spaceModel.tags?.length) { + const deleteSpaceTagsDtos = spaceModel.tags.map((tag) => tag.uuid); + + await this.tagModelService.deleteTags(deleteSpaceTagsDtos, queryRunner); + } + await queryRunner.manager.update( this.spaceModelRepository.target, { uuid: param.spaceModelUuid }, @@ -248,6 +254,7 @@ export class SpaceModelService { uuid, disabled: true, }, + relations: ['subspaceModels', 'tags'], }); if (!spaceModel) { throw new HttpException('space model not found', HttpStatus.NOT_FOUND); diff --git a/src/space-model/services/subspace/subspace-model.service.ts b/src/space-model/services/subspace/subspace-model.service.ts index 507a16a..6b8cb9f 100644 --- a/src/space-model/services/subspace/subspace-model.service.ts +++ b/src/space-model/services/subspace/subspace-model.service.ts @@ -5,7 +5,7 @@ import { } from '@app/common/modules/space-model'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { CreateSubspaceModelDto, CreateTagModelDto } from '../../dtos'; -import { QueryRunner } from 'typeorm'; +import { In, QueryRunner } from 'typeorm'; import { IDeletedSubsaceModelInterface } from 'src/space-model/interfaces'; import { DeleteSubspaceModelDto, @@ -27,7 +27,7 @@ export class SubSpaceModelService { queryRunner: QueryRunner, otherTags?: CreateTagModelDto[], ): Promise { - this.validateInputDtos(subSpaceModelDtos); + this.validateInputDtos(subSpaceModelDtos, spaceModel); const subspaces = subSpaceModelDtos.map((subspaceDto) => queryRunner.manager.create(this.subspaceModelRepository.target, { @@ -41,13 +41,18 @@ export class SubSpaceModelService { await Promise.all( subSpaceModelDtos.map(async (dto, index) => { const subspace = savedSubspaces[index]; + + 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, + [...(otherTags || []), ...otherDtoTags], ); } }), @@ -194,34 +199,61 @@ export class SubSpaceModelService { return subspace; } - private validateInputDtos(subSpaceModelDtos: CreateSubspaceModelDto[]): void { + private validateInputDtos( + subSpaceModelDtos: CreateSubspaceModelDto[], + spaceModel: SpaceModelEntity, + ): void { if (subSpaceModelDtos.length === 0) { throw new HttpException( 'Subspace models cannot be empty.', HttpStatus.BAD_REQUEST, ); } - this.validateName(subSpaceModelDtos.map((dto) => dto.subspaceName)); + this.validateName( + subSpaceModelDtos.map((dto) => dto.subspaceName), + spaceModel, + ); } - private validateName(names: string[]): void { + private async validateName( + names: string[], + spaceModel: SpaceModelEntity, + ): Promise { const seenNames = new Set(); const duplicateNames = new Set(); for (const name of names) { - if (seenNames.has(name)) { + if (!seenNames.add(name)) { duplicateNames.add(name); - } else { - seenNames.add(name); } } if (duplicateNames.size > 0) { throw new HttpException( - `Duplicate subspace names found: ${[...duplicateNames].join(', ')}`, + `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, + }, + }, + }); + + 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, + ); + } } private async updateSubspaceName( diff --git a/src/space-model/services/tag-model.service.ts b/src/space-model/services/tag-model.service.ts index 8207e0b..5682bc2 100644 --- a/src/space-model/services/tag-model.service.ts +++ b/src/space-model/services/tag-model.service.ts @@ -180,18 +180,24 @@ export class TagModelService { 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 }, - }, + 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 (isTagInSpaceModel || isTagInSubspaceModel) { + if (tagExists) { throw new HttpException(`Tag can't be reused`, HttpStatus.CONFLICT); } } diff --git a/src/space/controllers/subspace/subspace.controller.ts b/src/space/controllers/subspace/subspace.controller.ts index 37e264b..3ed36af 100644 --- a/src/space/controllers/subspace/subspace.controller.ts +++ b/src/space/controllers/subspace/subspace.controller.ts @@ -65,7 +65,7 @@ export class SubSpaceController { }) @Get(':subSpaceUuid') async findOne(@Param() params: GetSubSpaceParam): Promise { - return this.subSpaceService.findOne(params); + return this.subSpaceService.getOne(params); } @ApiBearerAuth() diff --git a/src/space/dtos/update.space.dto.ts b/src/space/dtos/update.space.dto.ts index ae50dea..9bc7950 100644 --- a/src/space/dtos/update.space.dto.ts +++ b/src/space/dtos/update.space.dto.ts @@ -30,11 +30,13 @@ export class UpdateSpaceDto { @ApiProperty({ description: 'X position on canvas', example: 120 }) @IsNumber() - x: number; + @IsOptional() + x?: number; @ApiProperty({ description: 'Y position on canvas', example: 200 }) @IsNumber() - y: number; + @IsOptional() + y?: number; @ApiPropertyOptional({ description: 'List of subspace modifications (add/update/delete)', diff --git a/src/space/services/space-validation.service.ts b/src/space/services/space-validation.service.ts index fa4a5f7..77ff011 100644 --- a/src/space/services/space-validation.service.ts +++ b/src/space/services/space-validation.service.ts @@ -44,6 +44,7 @@ export class ValidationService { async validateSpace(spaceUuid: string): Promise { const space = await this.spaceRepository.findOne({ where: { uuid: spaceUuid }, + relations: ['subspaces', 'tags'], }); if (!space) { diff --git a/src/space/services/space.service.ts b/src/space/services/space.service.ts index 7498981..0bdbff9 100644 --- a/src/space/services/space.service.ts +++ b/src/space/services/space.service.ts @@ -58,7 +58,7 @@ export class SpaceService { projectUuid, ); - this.validateSpaceCreation(spaceModelUuid); + this.validateSpaceCreation(addSpaceDto, spaceModelUuid); const parent = parentUuid ? await this.validationService.validateSpace(parentUuid) @@ -91,6 +91,7 @@ export class SpaceService { subspaces, newSpace, queryRunner, + tags, ); } @@ -189,6 +190,10 @@ export class SpaceService { } async delete(params: GetSpaceParam): Promise { + const queryRunner = this.dataSource.createQueryRunner(); + + await queryRunner.connect(); + await queryRunner.startTransaction(); try { const { communityUuid, spaceUuid, projectUuid } = params; @@ -205,14 +210,38 @@ export class SpaceService { HttpStatus.BAD_REQUEST, ); } - // Delete the space - await this.spaceRepository.remove(space); + + if (space.tags?.length) { + const deleteSpaceTagsDtos = space.tags.map((tag) => tag.uuid); + await this.tagService.deleteTags(deleteSpaceTagsDtos, queryRunner); + } + + if (space.subspaces?.length) { + const deleteSubspaceDtos = space.subspaces.map((subspace) => ({ + subspaceUuid: subspace.uuid, + })); + + await this.subSpaceService.deleteSubspaces( + deleteSubspaceDtos, + queryRunner, + ); + } + + await queryRunner.manager.update( + this.spaceRepository.target, + { uuid: params.spaceUuid }, + { disabled: true }, + ); + + await queryRunner.commitTransaction(); return new SuccessResponseDto({ message: `Space with ID ${spaceUuid} successfully deleted`, statusCode: HttpStatus.OK, }); } catch (error) { + await queryRunner.rollbackTransaction(); + if (error instanceof HttpException) { throw error; } @@ -220,6 +249,8 @@ export class SpaceService { 'An error occurred while deleting the space', HttpStatus.INTERNAL_SERVER_ERROR, ); + } finally { + await queryRunner.release(); } } @@ -384,8 +415,11 @@ export class SpaceService { return rootSpaces; } - private validateSpaceCreation(spaceModelUuid?: string) { - if (spaceModelUuid) { + private validateSpaceCreation( + addSpaceDto: AddSpaceDto, + spaceModelUuid?: string, + ) { + if (spaceModelUuid && (addSpaceDto.tags || addSpaceDto.subspaces)) { throw new HttpException( 'For space creation choose either space model or products and subspace', HttpStatus.CONFLICT, diff --git a/src/space/services/subspace/subspace.service.ts b/src/space/services/subspace/subspace.service.ts index 8795ff7..519608d 100644 --- a/src/space/services/subspace/subspace.service.ts +++ b/src/space/services/subspace/subspace.service.ts @@ -15,7 +15,7 @@ import { } from '@app/common/models/typeOrmCustom.model'; import { PageResponse } from '@app/common/dto/pagination.response.dto'; import { SubspaceDto } from '@app/common/modules/space/dtos'; -import { QueryRunner } from 'typeorm'; +import { In, QueryRunner } from 'typeorm'; import { SpaceEntity, SubspaceEntity, @@ -84,6 +84,11 @@ export class SubSpaceService { otherTags?: CreateTagDto[], ): Promise { try { + await this.validateName( + addSubspaceDtos.map((dto) => dto.subspaceName), + space, + ); + const subspaceData = addSubspaceDtos.map((dto) => ({ subspaceName: dto.subspaceName, space, @@ -93,6 +98,9 @@ export class SubSpaceService { await Promise.all( addSubspaceDtos.map(async (dto, index) => { + const otherDtoTags = addSubspaceDtos + .filter((_, i) => i !== index) + .flatMap((otherDto) => otherDto.tags || []); const subspace = subspaces[index]; if (dto.tags?.length) { subspace.tags = await this.tagService.createTags( @@ -100,15 +108,20 @@ export class SubSpaceService { queryRunner, null, subspace, - otherTags, + [...(otherTags || []), ...otherDtoTags], ); } }), ); return subspaces; } catch (error) { - throw new Error( - `Transaction failed: Unable to create subspaces and products. ${error.message}`, + if (error instanceof HttpException) { + throw error; + } + + throw new HttpException( + 'Failed to save subspaces due to an unexpected error.', + HttpStatus.INTERNAL_SERVER_ERROR, ); } } @@ -304,6 +317,19 @@ export class SubSpaceService { } } + async getOne(params: GetSubSpaceParam): Promise { + await this.validationService.validateSpaceWithinCommunityAndProject( + params.communityUuid, + params.projectUuid, + params.spaceUuid, + ); + const subspace = await this.findOne(params.subSpaceUuid); + return new SuccessResponseDto({ + message: `Successfully retrieved subspace`, + data: subspace, + }); + } + private async handleAddAction( subspace: ModifySubspaceDto, space: SpaceEntity, @@ -372,7 +398,7 @@ export class SubSpaceService { private async findOne(subspaceUuid: string): Promise { const subspace = await this.subspaceRepository.findOne({ where: { uuid: subspaceUuid }, - relations: ['tags'], + relations: ['tags', 'space'], }); if (!subspace) { throw new HttpException( @@ -393,4 +419,45 @@ export class SubSpaceService { await queryRunner.manager.save(subSpace); } } + + private async validateName( + names: string[], + space: SpaceEntity, + ): Promise { + const seenNames = new Set(); + const duplicateNames = new Set(); + + for (const name of names) { + if (!seenNames.add(name)) { + duplicateNames.add(name); + } + } + + if (duplicateNames.size > 0) { + throw new HttpException( + `Duplicate subspace names found: ${[...duplicateNames].join(', ')}`, + HttpStatus.CONFLICT, + ); + } + + const existingNames = await this.subspaceRepository.find({ + select: ['subspaceName'], + where: { + subspaceName: In([...seenNames]), + space: { + uuid: space.uuid, + }, + }, + }); + + if (existingNames.length > 0) { + const existingNamesList = existingNames + .map((e) => e.subspaceName) + .join(', '); + throw new HttpException( + `Subspace names already exist in the space: ${existingNamesList}`, + HttpStatus.BAD_REQUEST, + ); + } + } } diff --git a/src/space/services/tag/tag.service.ts b/src/space/services/tag/tag.service.ts index 9514cc4..173d91d 100644 --- a/src/space/services/tag/tag.service.ts +++ b/src/space/services/tag/tag.service.ts @@ -135,7 +135,6 @@ export class TagService { subspace?: SubspaceEntity, ): Promise { try { - console.log(tags); for (const tag of tags) { if (tag.action === ModifyAction.ADD) { const createTagDto: CreateTagDto = { @@ -176,18 +175,26 @@ export class TagService { productUuid: string, space: SpaceEntity, ): Promise { - const isTagInSpace = await this.tagRepository.exists({ - where: { tag, space, product: { uuid: productUuid } }, - }); - const isTagInSubspace = await this.tagRepository.exists({ - where: { - tag, - subspace: { space }, - product: { uuid: productUuid }, - }, + const { uuid: spaceUuid } = space; + + const tagExists = await this.tagRepository.exists({ + where: [ + { + tag, + space: { uuid: spaceUuid }, + product: { uuid: productUuid }, + disabled: false, + }, + { + tag, + subspace: { space: { uuid: spaceUuid } }, + product: { uuid: productUuid }, + disabled: false, + }, + ], }); - if (isTagInSpace || isTagInSubspace) { + if (tagExists) { throw new HttpException(`Tag can't be reused`, HttpStatus.CONFLICT); } }