added tag action

This commit is contained in:
hannathkadher
2024-12-23 20:34:44 +04:00
parent a4740e8bbd
commit b0eec7c38e
10 changed files with 332 additions and 99 deletions

View File

@ -0,0 +1,5 @@
export enum ModifyAction {
ADD = 'add',
UPDATE = 'update',
DELETE = 'delete',
}

View File

@ -26,4 +26,10 @@ export class TagModel extends AbstractEntity<TagModelDto> {
}) })
@JoinColumn({ name: 'subspace_id' }) @JoinColumn({ name: 'subspace_id' })
subspaceModel: SubspaceModelEntity; subspaceModel: SubspaceModelEntity;
@Column({
nullable: false,
default: false,
})
public disabled: boolean;
} }

View File

@ -1,3 +1,4 @@
export * from './delete-subspace-model.dto'; export * from './delete-subspace-model.dto';
export * from './create-subspace-model.dto'; export * from './create-subspace-model.dto';
export * from './update-subspace-model.dto'; export * from './update-subspace-model.dto';
export * from './modify-subspace-model.dto';

View File

@ -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[];
}

View File

@ -1,2 +1,3 @@
export * from './create-tag-model.dto'; export * from './create-tag-model.dto';
export * from './update-tag-model.dto'; export * from './update-tag-model.dto';
export * from './modify-tag-model.dto';

View File

@ -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;
}

View File

@ -1,11 +1,13 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsArray, IsOptional, IsString, ValidateNested } from 'class-validator'; import { IsArray, IsOptional, IsString, ValidateNested } from 'class-validator';
import { CreateSubspaceModelDto } from './subspaces-model-dtos/create-subspace-model.dto'; import { CreateSubspaceModelDto } from './subspaces-model-dtos/create-subspace-model.dto';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { import {
DeleteSubspaceModelDto, DeleteSubspaceModelDto,
ModifySubspaceModelDto,
UpdateSubspaceModelDto, UpdateSubspaceModelDto,
} from './subspaces-model-dtos'; } from './subspaces-model-dtos';
import { ModifyTagModelDto } from './tag-model-dtos';
export class ModifySubspacesModelDto { export class ModifySubspacesModelDto {
@ApiProperty({ @ApiProperty({
@ -51,6 +53,24 @@ export class UpdateSpaceModelDto {
@IsString() @IsString()
modelName?: string; modelName?: string;
@ApiPropertyOptional({
description: 'List of subspace modifications (add/update/delete)',
type: [ModifySubspaceModelDto],
})
@IsOptional() @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[];
} }

View File

@ -42,16 +42,7 @@ export class SpaceModelService {
try { try {
const project = await this.validateProject(params.projectUuid); const project = await this.validateProject(params.projectUuid);
const isModelExist = await this.validateName( await this.validateName(modelName, params.projectUuid);
modelName,
params.projectUuid,
);
if (isModelExist) {
throw new HttpException(
`Model name "${modelName}" already exists in this project ${params.projectUuid}.`,
HttpStatus.CONFLICT,
);
}
const spaceModel = this.spaceModelRepository.create({ const spaceModel = this.spaceModelRepository.create({
modelName, modelName,
@ -148,9 +139,11 @@ export class SpaceModelService {
await queryRunner.startTransaction(); await queryRunner.startTransaction();
try { try {
const { modelName } = dto; const { modelName } = dto;
if (modelName) spaceModel.modelName = modelName; if (modelName) {
await this.validateName(modelName, param.projectUuid);
spaceModel.modelName = modelName;
await queryRunner.manager.save(spaceModel); await queryRunner.manager.save(spaceModel);
}
if (dto.subspaceModels) { if (dto.subspaceModels) {
await this.subSpaceModelService.modifySubSpaceModels( await this.subSpaceModelService.modifySubSpaceModels(
@ -159,7 +152,6 @@ export class SpaceModelService {
queryRunner, queryRunner,
); );
} }
await queryRunner.commitTransaction(); await queryRunner.commitTransaction();
return new SuccessResponseDto({ return new SuccessResponseDto({
@ -177,11 +169,17 @@ export class SpaceModelService {
} }
} }
async validateName(modelName: string, projectUuid: string): Promise<boolean> { async validateName(modelName: string, projectUuid: string): Promise<void> {
const isModelExist = await this.spaceModelRepository.exists({ const isModelExist = await this.spaceModelRepository.findOne({
where: { modelName, project: { uuid: projectUuid } }, 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<SpaceModelEntity> { async validateSpaceModel(uuid: string): Promise<SpaceModelEntity> {

View File

@ -7,16 +7,17 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { import {
CreateSubspaceModelDto, CreateSubspaceModelDto,
UpdateSubspaceModelDto, UpdateSubspaceModelDto,
ModifySubspacesModelDto,
CreateTagModelDto, CreateTagModelDto,
} from '../../dtos'; } from '../../dtos';
import { QueryRunner } from 'typeorm'; import { QueryRunner } from 'typeorm';
import { import {
IDeletedSubsaceModelInterface, IDeletedSubsaceModelInterface,
IModifySubspaceModelInterface,
IUpdateSubspaceModelInterface, IUpdateSubspaceModelInterface,
} from 'src/space-model/interfaces'; } 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'; import { TagModelService } from '../tag-model.service';
@Injectable() @Injectable()
@ -25,11 +26,12 @@ export class SubSpaceModelService {
private readonly subspaceModelRepository: SubspaceModelRepository, private readonly subspaceModelRepository: SubspaceModelRepository,
private readonly tagModelService: TagModelService, private readonly tagModelService: TagModelService,
) {} ) {}
async createSubSpaceModels( async createSubSpaceModels(
subSpaceModelDtos: CreateSubspaceModelDto[], subSpaceModelDtos: CreateSubspaceModelDto[],
spaceModel: SpaceModelEntity, spaceModel: SpaceModelEntity,
queryRunner: QueryRunner, queryRunner: QueryRunner,
otherTags: CreateTagModelDto[], otherTags?: CreateTagModelDto[],
): Promise<SubspaceModelEntity[]> { ): Promise<SubspaceModelEntity[]> {
this.validateInputDtos(subSpaceModelDtos); this.validateInputDtos(subSpaceModelDtos);
@ -62,6 +64,9 @@ export class SubSpaceModelService {
return savedSubspaces; return savedSubspaces;
} catch (error) { } catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException( throw new HttpException(
error.message || 'Failed to create subspaces.', error.message || 'Failed to create subspaces.',
HttpStatus.INTERNAL_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR,
@ -183,7 +188,6 @@ export class SubSpaceModelService {
where: { where: {
uuid: subspaceUuid, uuid: subspaceUuid,
}, },
relations: ['productModels', 'productModels.itemModels'],
}); });
if (!subspace) { if (!subspace) {
throw new HttpException( throw new HttpException(
@ -226,43 +230,49 @@ export class SubSpaceModelService {
} }
async modifySubSpaceModels( async modifySubSpaceModels(
dto: ModifySubspacesModelDto, subspaceDtos: ModifySubspaceModelDto[],
spaceModel: SpaceModelEntity, spaceModel: SpaceModelEntity,
queryRunner: QueryRunner, queryRunner: QueryRunner,
): Promise<IModifySubspaceModelInterface> { ) {
const subspaces: IModifySubspaceModelInterface = {
spaceModelUuid: spaceModel.uuid,
};
try { 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.`,
if (dto.update) { HttpStatus.NOT_FOUND,
actions.push(
this.updateSubspaceModels(dto.update, spaceModel, queryRunner).then(
(updatedSubspaces) => {
subspaces.update = updatedSubspaces;
},
),
); );
} }
if (dto.delete) { existingSubspace.subspaceName =
actions.push( subspace.subspaceName || existingSubspace.subspaceName;
this.deleteSubspaceModels(dto.delete, queryRunner).then(
(deletedSubspaces) => { const updatedSubspace =
subspaces.delete = deletedSubspaces; await queryRunner.manager.save(existingSubspace);
},
), if (subspace.tags) {
await this.tagModelService.modifyTags(
subspace.tags,
queryRunner,
null,
updatedSubspace,
); );
} }
}
await Promise.all(actions); }
return subspaces;
} catch (error) { } catch (error) {
throw new HttpException( throw new HttpException(
error.message || 'Failed to modify SpaceModels', error.message || 'Failed to modify SpaceModels',

View File

@ -6,7 +6,7 @@ import {
} from '@app/common/modules/space-model/entities'; } from '@app/common/modules/space-model/entities';
import { SubspaceModelEntity } 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 { TagModelRepository } from '@app/common/modules/space-model';
import { CreateTagModelDto, UpdateTagModelDto } from '../dtos'; import { CreateTagModelDto, ModifyTagModelDto } from '../dtos';
import { ProductService } from 'src/product/services'; import { ProductService } from 'src/product/services';
@Injectable() @Injectable()
@ -21,44 +21,34 @@ export class TagModelService {
queryRunner: QueryRunner, queryRunner: QueryRunner,
spaceModel?: SpaceModelEntity, spaceModel?: SpaceModelEntity,
subspaceModel?: SubspaceModelEntity, subspaceModel?: SubspaceModelEntity,
otherTags?: CreateTagModelDto[], additionalTags?: CreateTagModelDto[],
): Promise<TagModel[]> { ): Promise<TagModel[]> {
let alltags: CreateTagModelDto[] = [];
if (!tags.length) { if (!tags.length) {
throw new HttpException('Tags cannot be empty.', HttpStatus.BAD_REQUEST); throw new HttpException('Tags cannot be empty.', HttpStatus.BAD_REQUEST);
} }
if (otherTags) {
alltags = [...tags, ...otherTags]; const combinedTags = additionalTags ? [...tags, ...additionalTags] : tags;
} const duplicateTags = this.findDuplicateTags(combinedTags);
const duplicates = this.checkForDuplicates(alltags);
if (duplicates.length > 0) { if (duplicateTags.length > 0) {
throw new HttpException( throw new HttpException(
`Duplicate tags found for the same product: ${duplicates.join(', ')}`, `Duplicate tags found for the same product: ${duplicateTags.join(', ')}`,
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
); );
} }
const tagEntities = await Promise.all( const tagEntities = await Promise.all(
tags.map(async (tagDto) => { tags.map(async (tagDto) =>
const product = await this.productService.findOne(tagDto.productUuid); this.prepareTagEntity(tagDto, queryRunner, spaceModel, subspaceModel),
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,
});
}),
);
try { try {
return await queryRunner.manager.save(tagEntities); return await queryRunner.manager.save(tagEntities);
} catch (error) { } catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException( throw new HttpException(
'Failed to save tag models due to an unexpected error.', 'Failed to save tag models due to an unexpected error.',
HttpStatus.INTERNAL_SERVER_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<TagModel> {
try { try {
const updatePromises = tags.map(async (tagDto) => { const existingTag = await this.getTagByUuid(tag.uuid);
const existingTag = await this.tagModelRepository.findOne({
where: { uuid: tagDto.uuid },
});
if (!existingTag) { if (spaceModel) {
throw new HttpException( await this.checkTagReuse(tag.tag, existingTag.product.uuid, spaceModel);
`Tag with ID ${tagDto.uuid} not found`, } else {
HttpStatus.NOT_FOUND, 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 queryRunner.manager.save(existingTag);
});
return await Promise.all(updatePromises);
} catch (error) { } catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException( throw new HttpException(
error.message || 'Failed to update tags', error.message || 'Failed to update tags',
HttpStatus.INTERNAL_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR,
@ -103,6 +100,9 @@ export class TagModelService {
await Promise.all(deletePromises); await Promise.all(deletePromises);
return { message: 'Tags deleted successfully', tagUuids }; return { message: 'Tags deleted successfully', tagUuids };
} catch (error) { } catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException( throw new HttpException(
error.message || 'Failed to delete tags', error.message || 'Failed to delete tags',
HttpStatus.INTERNAL_SERVER_ERROR, 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<string, boolean>(); const seen = new Map<string, boolean>();
const duplicates: string[] = []; const duplicates: string[] = [];
@ -125,4 +125,119 @@ export class TagModelService {
return duplicates; return duplicates;
} }
async modifyTags(
tags: ModifyTagModelDto[],
queryRunner: QueryRunner,
spaceModel?: SpaceModelEntity,
subspaceModel?: SubspaceModelEntity,
): Promise<void> {
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<void> {
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<TagModel> {
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<TagModel> {
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;
}
} }