Merge branch 'bufix/move-tags-between' into dev

This commit is contained in:
hannathkadher
2025-01-29 14:55:26 +04:00
6 changed files with 275 additions and 38 deletions

View File

@ -78,6 +78,7 @@ export class DeviceEntity extends AbstractEntity<DeviceDto> {
@OneToOne(() => TagEntity, (tag) => tag.device, { @OneToOne(() => TagEntity, (tag) => tag.device, {
nullable: true, nullable: true,
}) })
@JoinColumn({ name: 'tag_id' })
tag: TagEntity; tag: TagEntity;
constructor(partial: Partial<DeviceEntity>) { constructor(partial: Partial<DeviceEntity>) {

View File

@ -395,7 +395,6 @@ export class SpaceModelService {
if (!spaceModel) { if (!spaceModel) {
throw new HttpException('space model not found', HttpStatus.NOT_FOUND); throw new HttpException('space model not found', HttpStatus.NOT_FOUND);
} }
return spaceModel; return spaceModel;
} }
} }

View File

@ -409,13 +409,11 @@ export class TagModelService {
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
); );
} }
const tagSpace =
spaceModel !== null ? spaceModel : subspaceModel.spaceModel;
return queryRunner.manager.create(TagModel, { return queryRunner.manager.create(TagModel, {
tag: tagDto.tag, tag: tagDto.tag,
product: product.data, product: product.data,
spaceModel: tagSpace, spaceModel: spaceModel,
subspaceModel: subspaceModel, subspaceModel: subspaceModel,
}); });
} catch (error) { } catch (error) {
@ -478,14 +476,14 @@ export class TagModelService {
} }
getSubspaceTagsToBeAdded( getSubspaceTagsToBeAdded(
spaceTags: ModifyTagModelDto[], spaceTags?: ModifyTagModelDto[],
subspaceModels: ModifySubspaceModelDto[], subspaceModels?: ModifySubspaceModelDto[],
): ModifyTagModelDto[] { ): ModifyTagModelDto[] {
if (!subspaceModels || subspaceModels.length === 0) { if (!subspaceModels || subspaceModels.length === 0) {
return spaceTags; return spaceTags;
} }
const spaceTagsToDelete = spaceTags.filter( const spaceTagsToDelete = spaceTags?.filter(
(tag) => tag.action === 'delete', (tag) => tag.action === 'delete',
); );

View File

@ -1,5 +1,5 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator'; import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
export class CreateTagDto { export class CreateTagDto {
@ApiProperty({ @ApiProperty({
@ -10,6 +10,14 @@ export class CreateTagDto {
@IsString() @IsString()
tag: string; tag: string;
@ApiPropertyOptional({
description: 'UUID of the tag (required for update/delete)',
example: '123e4567-e89b-12d3-a456-426614174000',
})
@IsOptional()
@IsString()
uuid?: string;
@ApiProperty({ @ApiProperty({
description: 'ID of the product associated with the tag', description: 'ID of the product associated with the tag',
example: '123e4567-e89b-12d3-a456-426614174000', example: '123e4567-e89b-12d3-a456-426614174000',

View File

@ -356,16 +356,26 @@ export class SpaceService {
} }
if (hasSubspace) { if (hasSubspace) {
await this.subSpaceService.modifySubSpace( const modifiedSubspaces = this.tagService.getModifiedSubspaces(
updateSpaceDto.tags,
updateSpaceDto.subspace, updateSpaceDto.subspace,
);
await this.subSpaceService.modifySubSpace(
modifiedSubspaces,
queryRunner, queryRunner,
space, space,
); );
} }
if (hasTags) { if (hasTags) {
await this.tagService.modifyTags( const spaceTagsAfterMove = this.tagService.getSubspaceTagsToBeAdded(
updateSpaceDto.tags, updateSpaceDto.tags,
updateSpaceDto.subspace,
);
await this.tagService.modifyTags(
spaceTagsAfterMove,
queryRunner, queryRunner,
space, space,
); );

View File

@ -8,7 +8,8 @@ import {
import { TagModel } from '@app/common/modules/space-model'; import { TagModel } from '@app/common/modules/space-model';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { ProductService } from 'src/product/services'; import { ProductService } from 'src/product/services';
import { CreateTagDto } from 'src/space/dtos'; import { ModifyTagModelDto } from 'src/space-model/dtos';
import { CreateTagDto, ModifySubspaceDto } from 'src/space/dtos';
import { ModifyTagDto } from 'src/space/dtos/tag/modify-tag.dto'; import { ModifyTagDto } from 'src/space/dtos/tag/modify-tag.dto';
import { QueryRunner } from 'typeorm'; import { QueryRunner } from 'typeorm';
@ -25,25 +26,133 @@ export class TagService {
space?: SpaceEntity, space?: SpaceEntity,
subspace?: SubspaceEntity, subspace?: SubspaceEntity,
additionalTags?: CreateTagDto[], additionalTags?: CreateTagDto[],
tagsToDelete?: ModifyTagDto[],
): Promise<TagEntity[]> { ): Promise<TagEntity[]> {
this.validateTagsInput(tags); this.validateTagsInput(tags);
const combinedTags = this.combineTags(tags, additionalTags); const combinedTags = this.combineTags(tags, additionalTags);
this.ensureNoDuplicateTags(combinedTags); this.ensureNoDuplicateTags(combinedTags);
const tagEntitiesToCreate = tags.filter((tagDto) => tagDto.uuid === null);
const tagEntitiesToUpdate = tags.filter((tagDto) => tagDto.uuid !== null);
try { try {
const tagEntities = await Promise.all( const createdTags = await this.bulkSaveTags(
tags.map(async (tagDto) => tagEntitiesToCreate,
this.prepareTagEntity(tagDto, queryRunner, space, subspace), queryRunner,
), space,
subspace,
tagsToDelete,
);
const updatedTags = await this.moveTags(
tagEntitiesToUpdate,
queryRunner,
space,
subspace,
); );
return await queryRunner.manager.save(tagEntities); return [...createdTags, ...updatedTags];
} catch (error) { } catch (error) {
throw this.handleUnexpectedError('Failed to save tags', error); throw this.handleUnexpectedError('Failed to save tags', error);
} }
} }
async bulkSaveTags(
tags: CreateTagDto[],
queryRunner: QueryRunner,
space?: SpaceEntity,
subspace?: SubspaceEntity,
tagsToDelete?: ModifyTagDto[],
): Promise<TagEntity[]> {
if (!tags.length) {
return [];
}
const tagEntities = await Promise.all(
tags.map((tagDto) =>
this.prepareTagEntity(
tagDto,
queryRunner,
space,
subspace,
tagsToDelete,
),
),
);
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: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async moveTags(
tags: CreateTagDto[],
queryRunner: QueryRunner,
space?: SpaceEntity,
subspace?: SubspaceEntity,
): Promise<TagEntity[]> {
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 (subspace && subspace.space) {
await queryRunner.manager.update(
this.tagRepository.target,
{ uuid: tag.uuid },
{ subspace, space: null },
);
tag.subspace = subspace;
}
if (subspace === null && space) {
await queryRunner.manager.update(
this.tagRepository.target,
{ uuid: tag.uuid },
{ subspace: null, space: space },
);
tag.subspace = null;
tag.space = space;
}
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,
);
}
}
async createTagsFromModel( async createTagsFromModel(
queryRunner: QueryRunner, queryRunner: QueryRunner,
tagModels: TagModel[], tagModels: TagModel[],
@ -170,6 +279,7 @@ export class TagService {
); );
} }
} }
async modifyTags( async modifyTags(
tags: ModifyTagDto[], tags: ModifyTagDto[],
queryRunner: QueryRunner, queryRunner: QueryRunner,
@ -179,15 +289,27 @@ export class TagService {
if (!tags?.length) return; if (!tags?.length) return;
try { try {
const tagsToDelete = tags.filter(
(tag) => tag.action === ModifyAction.DELETE,
);
await Promise.all( await Promise.all(
tags.map(async (tag) => { tags.map(async (tag) => {
switch (tag.action) { switch (tag.action) {
case ModifyAction.ADD: case ModifyAction.ADD:
await this.createTags( await this.createTags(
[{ tag: tag.tag, productUuid: tag.productUuid }], [
{
tag: tag.tag,
productUuid: tag.productUuid,
uuid: tag.uuid,
},
],
queryRunner, queryRunner,
space, space,
subspace, subspace,
null,
tagsToDelete,
); );
break; break;
case ModifyAction.UPDATE: case ModifyAction.UPDATE:
@ -195,7 +317,6 @@ export class TagService {
break; break;
case ModifyAction.DELETE: case ModifyAction.DELETE:
await this.deleteTags([tag.uuid], queryRunner); await this.deleteTags([tag.uuid], queryRunner);
break; break;
default: default:
throw new HttpException( throw new HttpException(
@ -244,10 +365,11 @@ export class TagService {
tag: string, tag: string,
productUuid: string, productUuid: string,
space: SpaceEntity, space: SpaceEntity,
tagsToDelete?: ModifyTagDto[],
): Promise<void> { ): Promise<void> {
const { uuid: spaceUuid } = space; const { uuid: spaceUuid } = space;
const tagExists = await this.tagRepository.exists({ const tagExists = await this.tagRepository.find({
where: [ where: [
{ {
tag, tag,
@ -264,7 +386,12 @@ export class TagService {
], ],
}); });
if (tagExists) { const filteredTagExists = tagExists.filter(
(existingTag) =>
!tagsToDelete?.some((deleteTag) => deleteTag.uuid === existingTag.uuid),
);
if (filteredTagExists.length > 0) {
throw new HttpException(`Tag can't be reused`, HttpStatus.CONFLICT); throw new HttpException(`Tag can't be reused`, HttpStatus.CONFLICT);
} }
} }
@ -274,28 +401,53 @@ export class TagService {
queryRunner: QueryRunner, queryRunner: QueryRunner,
space?: SpaceEntity, space?: SpaceEntity,
subspace?: SubspaceEntity, subspace?: SubspaceEntity,
tagsToDelete?: ModifyTagDto[],
): Promise<TagEntity> { ): Promise<TagEntity> {
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 (space) {
await this.checkTagReuse(
tagDto.tag,
tagDto.productUuid,
space,
tagsToDelete,
);
} else if (subspace && subspace.space) {
await this.checkTagReuse(
tagDto.tag,
tagDto.productUuid,
subspace.space,
);
} else {
throw new HttpException(
`Invalid subspace or space provided.`,
HttpStatus.BAD_REQUEST,
);
}
return queryRunner.manager.create(TagEntity, {
tag: tagDto.tag,
product: product.data,
space: space,
subspace: subspace,
});
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException( throw new HttpException(
`Product with UUID ${tagDto.productUuid} not found.`, `An error occurred while preparing the tag entity: ${error.message}`,
HttpStatus.NOT_FOUND, HttpStatus.INTERNAL_SERVER_ERROR,
); );
} }
await this.checkTagReuse(
tagDto.tag,
tagDto.productUuid,
space ?? subspace.space,
);
return queryRunner.manager.create(TagEntity, {
tag: tagDto.tag,
product: product.data,
space,
subspace,
});
} }
private async getTagByUuid(uuid: string): Promise<TagEntity> { private async getTagByUuid(uuid: string): Promise<TagEntity> {
@ -347,4 +499,73 @@ export class TagService {
return; return;
} }
} }
getSubspaceTagsToBeAdded(
spaceTags?: ModifyTagDto[],
subspaceModels?: ModifySubspaceDto[],
): ModifyTagDto[] {
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?: ModifyTagDto[],
subspaceModels?: ModifySubspaceDto[],
): ModifySubspaceDto[] {
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;
}
} }