mirror of
https://github.com/SyncrowIOT/backend.git
synced 2025-07-14 18:05:48 +00:00
Merge branch 'bufix/move-tags-between' into dev
This commit is contained in:
@ -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>) {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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',
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user