Merge branch 'feat/project-tag' of https://github.com/SyncrowIOT/backend into feat/project-tag

This commit is contained in:
hannathkadher
2025-02-26 17:51:55 +04:00
9 changed files with 164 additions and 706 deletions

View File

@ -70,9 +70,9 @@ export class SpaceModelController {
@UseGuards(PermissionsGuard) @UseGuards(PermissionsGuard)
@Permissions('SPACE_MODEL_VIEW') @Permissions('SPACE_MODEL_VIEW')
@ApiOperation({ @ApiOperation({
summary: ControllerRoute.SPACE_MODEL.ACTIONS.LIST_SPACE_MODEL_SUMMARY, summary: ControllerRoute.SPACE_MODEL.ACTIONS.GET_SPACE_MODEL_SUMMARY,
description: description:
ControllerRoute.SPACE_MODEL.ACTIONS.LIST_SPACE_MODEL_DESCRIPTION, ControllerRoute.SPACE_MODEL.ACTIONS.GET_SPACE_MODEL_DESCRIPTION,
}) })
@Get(':spaceModelUuid') @Get(':spaceModelUuid')
async get(@Param() param: SpaceModelParam): Promise<BaseResponseDto> { async get(@Param() param: SpaceModelParam): Promise<BaseResponseDto> {

View File

@ -3,7 +3,7 @@ import { IsUUID } from 'class-validator';
import { ProjectParam } from './project-param.dto'; import { ProjectParam } from './project-param.dto';
export class SpaceModelParam extends ProjectParam { export class SpaceModelParam extends ProjectParam {
@ApiProperty({ @ApiProperty({
description: 'UUID of the Space', description: 'UUID of the Space model',
example: 'd290f1ee-6c54-4b01-90e6-d701748f0851', example: 'd290f1ee-6c54-4b01-90e6-d701748f0851',
}) })
@IsUUID() @IsUUID()

View File

@ -10,7 +10,6 @@ import {
import { DataSource, QueryRunner } from 'typeorm'; import { DataSource, QueryRunner } from 'typeorm';
import { SubSpaceService } from 'src/space/services'; import { SubSpaceService } from 'src/space/services';
import { TagService } from 'src/space/services/tag'; import { TagService } from 'src/space/services/tag';
import { TagModelService } from '../services';
import { UpdatedSubspaceModelPayload } from '../interfaces'; import { UpdatedSubspaceModelPayload } from '../interfaces';
import { ModifyAction } from '@app/common/constants/modify-action.enum'; import { ModifyAction } from '@app/common/constants/modify-action.enum';
import { ModifySubspaceDto } from 'src/space/dtos'; import { ModifySubspaceDto } from 'src/space/dtos';
@ -26,7 +25,6 @@ export class PropogateUpdateSpaceModelHandler
private readonly dataSource: DataSource, private readonly dataSource: DataSource,
private readonly subSpaceService: SubSpaceService, private readonly subSpaceService: SubSpaceService,
private readonly tagService: TagService, private readonly tagService: TagService,
private readonly tagModelService: TagModelService,
) {} ) {}
async execute(command: PropogateUpdateSpaceModelCommand): Promise<void> { async execute(command: PropogateUpdateSpaceModelCommand): Promise<void> {

View File

@ -1,3 +1,2 @@
export * from './space-model.service'; export * from './space-model.service';
export * from './subspace'; export * from './subspace';
export * from './tag-model.service';

View File

@ -311,16 +311,6 @@ export class SpaceModelProductAllocationService {
} }
} }
private async getAllocationByTagUuid(
tagUuid: string,
queryRunner: QueryRunner,
): Promise<SpaceModelProductAllocationEntity | null> {
return queryRunner.manager.findOne(SpaceModelProductAllocationEntity, {
where: { tags: { uuid: tagUuid } },
relations: ['tags'],
});
}
private async validateTagWithinSpaceModel( private async validateTagWithinSpaceModel(
queryRunner: QueryRunner, queryRunner: QueryRunner,
tag: NewTagEntity, tag: NewTagEntity,
@ -351,4 +341,29 @@ export class SpaceModelProductAllocationService {
); );
} }
} }
async clearAllAllocations(spaceModelUuid: string, queryRunner: QueryRunner) {
try {
await queryRunner.manager
.createQueryBuilder()
.delete()
.from(SpaceModelProductAllocationEntity, 'allocation')
.relation(SpaceModelProductAllocationEntity, 'tags')
.of(
await queryRunner.manager.find(SpaceModelProductAllocationEntity, {
where: { spaceModel: { uuid: spaceModelUuid } },
}),
)
.execute();
await queryRunner.manager
.createQueryBuilder()
.delete()
.from(SpaceModelProductAllocationEntity)
.where('spaceModelUuid = :spaceModelUuid', { spaceModelUuid })
.execute();
} catch (error) {
throw this.handleError(error, `Failed to clear all allocations`);
}
}
} }

View File

@ -1,30 +1,24 @@
import { import {
SpaceModelEntity, SpaceModelEntity,
SpaceModelProductAllocationEntity,
SpaceModelRepository, SpaceModelRepository,
SubspaceModelProductAllocationEntity,
} from '@app/common/modules/space-model'; } from '@app/common/modules/space-model';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { import { CreateSpaceModelDto, UpdateSpaceModelDto } from '../dtos';
CreateSpaceModelDto,
LinkSpacesToModelDto,
UpdateSpaceModelDto,
} from '../dtos';
import { ProjectParam } from 'src/community/dtos'; import { ProjectParam } from 'src/community/dtos';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { SubSpaceModelService } from './subspace/subspace-model.service'; import { SubSpaceModelService } from './subspace/subspace-model.service';
import { DataSource, In, QueryRunner } from 'typeorm'; import { DataSource, QueryRunner, SelectQueryBuilder } from 'typeorm';
import { import {
TypeORMCustomModel, TypeORMCustomModel,
TypeORMCustomModelFindAllQuery, TypeORMCustomModelFindAllQuery,
} from '@app/common/models/typeOrmCustom.model'; } from '@app/common/models/typeOrmCustom.model';
import { PageResponse } from '@app/common/dto/pagination.response.dto';
import { SpaceModelParam } from '../dtos/space-model-param'; import { SpaceModelParam } from '../dtos/space-model-param';
import { ProjectService } from 'src/project/services'; import { ProjectService } from 'src/project/services';
import { ProjectEntity } from '@app/common/modules/project/entities'; import { ProjectEntity } from '@app/common/modules/project/entities';
import { TagModelService } from './tag-model.service';
import { BaseResponseDto } from '@app/common/dto/base.response.dto'; import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { CommandBus } from '@nestjs/cqrs'; import { CommandBus } from '@nestjs/cqrs';
import { SpaceModelDto } from '@app/common/modules/space-model/dtos';
import { TagService as NewTagService } from 'src/tags/services';
import { ProcessTagDto } from 'src/tags/dtos'; import { ProcessTagDto } from 'src/tags/dtos';
import { SpaceModelProductAllocationService } from './space-model-product-allocation.service'; import { SpaceModelProductAllocationService } from './space-model-product-allocation.service';
import { SpaceRepository } from '@app/common/modules/space'; import { SpaceRepository } from '@app/common/modules/space';
@ -36,9 +30,7 @@ export class SpaceModelService {
private readonly spaceModelRepository: SpaceModelRepository, private readonly spaceModelRepository: SpaceModelRepository,
private readonly projectService: ProjectService, private readonly projectService: ProjectService,
private readonly subSpaceModelService: SubSpaceModelService, private readonly subSpaceModelService: SubSpaceModelService,
private readonly tagModelService: TagModelService,
private commandBus: CommandBus, private commandBus: CommandBus,
private readonly tagService: NewTagService,
private readonly spaceModelProductAllocationService: SpaceModelProductAllocationService, private readonly spaceModelProductAllocationService: SpaceModelProductAllocationService,
private readonly spaceRepository: SpaceRepository, private readonly spaceRepository: SpaceRepository,
) {} ) {}
@ -134,34 +126,9 @@ export class SpaceModelService {
disabled: false, disabled: false,
}; };
pageable.include = pageable.include =
'subspaceModels,tags,subspaceModels.tags,tags.product,subspaceModels.tags.product'; 'subspaceModels.productAllocations,subspaceModelProductAllocations.tags,subspaceModels, productAllocations, productAllocations.tags';
const queryBuilder = await this.spaceModelRepository const queryBuilder = this.buildSpaceModelQuery(param.projectUuid);
.createQueryBuilder('spaceModel')
.leftJoinAndSelect(
'spaceModel.subspaceModels',
'subspaceModels',
'subspaceModels.disabled = :subspaceDisabled',
{ subspaceDisabled: false },
)
.leftJoinAndSelect(
'spaceModel.tags',
'tags',
'tags.disabled = :tagsDisabled',
{ tagsDisabled: false },
)
.leftJoinAndSelect('tags.product', 'spaceTagproduct')
.leftJoinAndSelect(
'subspaceModels.tags',
'subspaceModelTags',
'subspaceModelTags.disabled = :subspaceModelTagsDisabled',
{ subspaceModelTagsDisabled: false },
)
.leftJoinAndSelect('subspaceModelTags.product', 'subspaceTagproduct')
.where('spaceModel.disabled = :disabled', { disabled: false })
.andWhere('spaceModel.project = :projectUuid', {
projectUuid: param.projectUuid,
});
const customModel = TypeORMCustomModel(this.spaceModelRepository); const customModel = TypeORMCustomModel(this.spaceModelRepository);
const { baseResponseDto, paginationResponseDto } = const { baseResponseDto, paginationResponseDto } =
@ -174,10 +141,14 @@ export class SpaceModelService {
queryBuilder, queryBuilder,
); );
return new PageResponse<SpaceModelDto>( const formattedData = this.transformSpaceModelData(baseResponseDto.data);
baseResponseDto,
paginationResponseDto, return {
); code: 200,
data: formattedData,
message: 'Success get list spaceModel',
...paginationResponseDto,
};
} catch (error) { } catch (error) {
throw new HttpException( throw new HttpException(
`Error fetching paginated list: ${error.message}`, `Error fetching paginated list: ${error.message}`,
@ -193,7 +164,10 @@ export class SpaceModelService {
async update(dto: UpdateSpaceModelDto, param: SpaceModelParam) { async update(dto: UpdateSpaceModelDto, param: SpaceModelParam) {
const queryRunner = this.dataSource.createQueryRunner(); const queryRunner = this.dataSource.createQueryRunner();
await this.validateProject(param.projectUuid); await this.validateProject(param.projectUuid);
const spaceModel = await this.validateSpaceModel(param.spaceModelUuid); const spaceModel = await this.validateSpaceModel(
param.spaceModelUuid,
param.projectUuid,
);
await queryRunner.connect(); await queryRunner.connect();
try { try {
@ -259,7 +233,10 @@ export class SpaceModelService {
try { try {
await this.validateProject(param.projectUuid); await this.validateProject(param.projectUuid);
const spaceModel = await this.validateSpaceModel(param.spaceModelUuid); const spaceModel = await this.validateSpaceModel(
param.spaceModelUuid,
param.projectUuid,
);
if (spaceModel.subspaceModels?.length) { if (spaceModel.subspaceModels?.length) {
const deleteSubspaceUuids = spaceModel.subspaceModels.map( const deleteSubspaceUuids = spaceModel.subspaceModels.map(
@ -272,10 +249,11 @@ export class SpaceModelService {
); );
} }
if (spaceModel.tags?.length) { if (spaceModel.productAllocations?.length) {
const deleteSpaceTagsDtos = spaceModel.tags.map((tag) => tag.uuid); await this.spaceModelProductAllocationService.clearAllAllocations(
spaceModel.uuid,
await this.tagModelService.deleteTags(deleteSpaceTagsDtos, queryRunner); queryRunner,
);
} }
await queryRunner.manager.update( await queryRunner.manager.update(
@ -341,11 +319,15 @@ export class SpaceModelService {
async findOne(params: SpaceModelParam): Promise<BaseResponseDto> { async findOne(params: SpaceModelParam): Promise<BaseResponseDto> {
try { try {
await this.validateProject(params.projectUuid); await this.validateProject(params.projectUuid);
const spaceModel = await this.validateSpaceModel(params.spaceModelUuid); const spaceModel = await this.validateSpaceModel(
params.spaceModelUuid,
params.projectUuid,
);
const response = this.formatSpaceModelResponse(spaceModel);
return new SuccessResponseDto({ return new SuccessResponseDto({
message: 'SpaceModel retrieved successfully', message: 'SpaceModel retrieved successfully',
data: spaceModel, data: response,
}); });
} catch (error) { } catch (error) {
throw new HttpException( throw new HttpException(
@ -355,38 +337,19 @@ export class SpaceModelService {
} }
} }
async validateSpaceModel(uuid: string): Promise<SpaceModelEntity> { async validateSpaceModel(
const spaceModel = await this.spaceModelRepository uuid: string,
.createQueryBuilder('spaceModel') projectUuid?: string,
.leftJoinAndSelect( ): Promise<SpaceModelEntity> {
'spaceModel.subspaceModels', const query = this.buildSpaceModelQuery(projectUuid);
'subspaceModels', const result = await query
'subspaceModels.disabled = :subspaceDisabled',
{ subspaceDisabled: false },
)
.leftJoinAndSelect(
'spaceModel.tags',
'tags',
'tags.disabled = :tagsDisabled',
{ tagsDisabled: false },
)
.leftJoinAndSelect('tags.product', 'spaceTagproduct')
.leftJoinAndSelect(
'subspaceModels.tags',
'subspaceModelTags',
'subspaceModelTags.disabled = :subspaceModelTagsDisabled',
{ subspaceModelTagsDisabled: false },
)
.leftJoinAndSelect('subspaceModelTags.product', 'subspaceTagproduct')
.where('spaceModel.disabled = :disabled', { disabled: false })
.where('spaceModel.disabled = :disabled', { disabled: false })
.andWhere('spaceModel.uuid = :uuid', { uuid }) .andWhere('spaceModel.uuid = :uuid', { uuid })
.getOne(); .getOne();
if (!spaceModel) { if (!result) {
throw new HttpException('space model not found', HttpStatus.NOT_FOUND); throw new HttpException('space model not found', HttpStatus.NOT_FOUND);
} }
return spaceModel; return result;
} }
private validateUniqueTags(allTags: ProcessTagDto[]) { private validateUniqueTags(allTags: ProcessTagDto[]) {
@ -415,33 +378,102 @@ export class SpaceModelService {
} }
} }
async linkSpacesToModel(params: SpaceModelParam, dto: LinkSpacesToModelDto) { private buildSpaceModelQuery(
await this.validateProject(params.projectUuid); projectUuid: string,
const spaceModel = await this.validateSpaceModel(params.spaceModelUuid); ): SelectQueryBuilder<SpaceModelEntity> {
return this.spaceModelRepository
.createQueryBuilder('spaceModel')
.leftJoinAndSelect(
'spaceModel.subspaceModels',
'subspaceModels',
'subspaceModels.disabled = :subspaceDisabled',
{ subspaceDisabled: false },
)
.leftJoinAndSelect(
'subspaceModels.productAllocations',
'subspaceModelProductAllocations',
)
.leftJoinAndSelect(
'subspaceModelProductAllocations.tags',
'subspaceModelTags',
)
.leftJoinAndSelect('subspaceModelTags.product', 'subspaceModelTagProduct')
.leftJoinAndSelect('spaceModel.productAllocations', 'productAllocations')
.leftJoinAndSelect('productAllocations.product', 'allocatedProduct')
.leftJoinAndSelect('productAllocations.tags', 'productTags')
.leftJoinAndSelect('productTags.product', 'productTagProduct')
.where('spaceModel.disabled = false')
.andWhere('spaceModel.project = :projectUuid', { projectUuid });
}
const spaces = await this.spaceRepository.find({ private transformSpaceModelData(spaceModelsArray: SpaceModelEntity[]): any[] {
where: { uuid: In(dto.spaceUuids) }, if (!Array.isArray(spaceModelsArray)) return [];
relations: [
'spaceModel',
'devices',
'subspaces',
'productAllocations',
'subspace.productAllocations',
],
});
if (!spaces.length) { return spaceModelsArray.map((spaceModel) => ({
throw new HttpException( uuid: spaceModel.uuid,
`No spaces found for the given UUIDs`, createdAt: spaceModel.createdAt,
HttpStatus.NOT_FOUND, updatedAt: spaceModel.updatedAt,
); modelName: spaceModel.modelName,
} disabled: spaceModel.disabled,
subspaceModels: (spaceModel.subspaceModels ?? []).map((subspace) => ({
uuid: subspace.uuid,
createdAt: subspace.createdAt,
updatedAt: subspace.updatedAt,
subspaceName: subspace.subspaceName,
disabled: subspace.disabled,
tags: this.extractTags(subspace.productAllocations),
})),
tags: this.extractTags(spaceModel.productAllocations),
}));
}
for (const space of spaces) { private extractTags(
const hasDependencies = productAllocations:
space.devices.length > 0 || | SpaceModelProductAllocationEntity[]
space.subspaces.length > 0 || | SubspaceModelProductAllocationEntity[]
space.productAllocations.length > 0; | undefined,
} ): any[] {
if (!productAllocations) return [];
return productAllocations
.flatMap((allocation) => allocation.tags ?? [])
.map((tag) => ({
uuid: tag.uuid,
createdAt: tag.createdAt,
updatedAt: tag.updatedAt,
tag: tag.tag,
disabled: tag.disabled,
product: tag.product
? {
uuid: tag.product.uuid,
createdAt: tag.product.createdAt,
updatedAt: tag.product.updatedAt,
catName: tag.product.catName,
prodId: tag.product.prodId,
name: tag.product.name,
prodType: tag.product.prodType,
}
: null,
}));
}
private formatSpaceModelResponse(spaceModel: SpaceModelEntity): any {
return {
uuid: spaceModel.uuid,
createdAt: spaceModel.createdAt,
updatedAt: spaceModel.updatedAt,
modelName: spaceModel.modelName,
disabled: spaceModel.disabled,
subspaceModels:
spaceModel.subspaceModels?.map((subspace) => ({
uuid: subspace.uuid,
createdAt: subspace.createdAt,
updatedAt: subspace.updatedAt,
subspaceName: subspace.subspaceName,
disabled: subspace.disabled,
tags: this.extractTags(subspace.productAllocations),
})) ?? [],
tags: this.extractTags(spaceModel.productAllocations),
};
} }
} }

View File

@ -1,579 +0,0 @@
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
import { QueryRunner } from 'typeorm';
import {
SpaceModelEntity,
TagModel,
} 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 {
CreateTagModelDto,
ModifySubspaceModelDto,
ModifyTagModelDto,
} from '../dtos';
import { ProductService } from 'src/product/services';
import { ModifyAction } from '@app/common/constants/modify-action.enum';
import { ModifiedTagsModelPayload } from '../interfaces';
import { NewTagRepository } from '@app/common/modules/tag/repositories/tag-repository';
@Injectable()
export class TagModelService {
constructor(
private readonly tagModelRepository: TagModelRepository,
private readonly productService: ProductService,
private readonly tagRepository: NewTagRepository,
) {}
async createTags(
tags: CreateTagModelDto[],
queryRunner: QueryRunner,
spaceModel?: SpaceModelEntity,
subspaceModel?: SubspaceModelEntity,
additionalTags?: CreateTagModelDto[],
tagsToDelete?: ModifyTagModelDto[],
): Promise<TagModel[]> {
if (!tags.length) {
throw new HttpException('Tags cannot be empty.', HttpStatus.BAD_REQUEST);
}
const combinedTags = additionalTags ? [...tags, ...additionalTags] : tags;
const duplicateTags = this.findDuplicateTags(combinedTags);
if (duplicateTags.length > 0) {
throw new HttpException(
`Duplicate tags found for the same product: ${duplicateTags.join(', ')}`,
HttpStatus.BAD_REQUEST,
);
}
const tagEntitiesToCreate = tags.filter((tagDto) => !tagDto.uuid);
const tagEntitiesToUpdate = tags.filter((tagDto) => !!tagDto.uuid);
try {
const createdTags = await this.bulkSaveTags(
tagEntitiesToCreate,
queryRunner,
spaceModel,
subspaceModel,
tagsToDelete,
);
// Update existing tags
const updatedTags = await this.moveTags(
tagEntitiesToUpdate,
queryRunner,
spaceModel,
subspaceModel,
);
// Combine created and updated tags
return [...createdTags, ...updatedTags];
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
`Failed to create tag models due to an unexpected error.: ${error}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async bulkSaveTags(
tags: CreateTagModelDto[],
queryRunner: QueryRunner,
spaceModel?: SpaceModelEntity,
subspaceModel?: SubspaceModelEntity,
tagsToDelete?: ModifyTagModelDto[],
): Promise<TagModel[]> {
if (!tags.length) {
return [];
}
const tagEntities = await Promise.all(
tags.map((tagDto) =>
this.prepareTagEntity(
tagDto,
queryRunner,
spaceModel,
subspaceModel,
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: CreateTagModelDto[],
queryRunner: QueryRunner,
spaceModel?: SpaceModelEntity,
subspaceModel?: SubspaceModelEntity,
): Promise<TagModel[]> {
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 (subspaceModel && subspaceModel.spaceModel) {
await queryRunner.manager.update(
this.tagModelRepository.target,
{ uuid: tag.uuid },
{ subspaceModel, spaceModel: null },
);
tag.subspaceModel = subspaceModel;
}
if (!subspaceModel && spaceModel) {
await queryRunner.manager.update(
this.tagModelRepository.target,
{ uuid: tag.uuid },
{ subspaceModel: null, spaceModel: spaceModel },
);
tag.subspaceModel = null;
tag.spaceModel = spaceModel;
}
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 updateTag(
tag: ModifyTagModelDto,
queryRunner: QueryRunner,
spaceModel?: SpaceModelEntity,
subspaceModel?: SubspaceModelEntity,
): Promise<TagModel> {
try {
const existingTag = await this.getTagByUuid(tag.uuid);
if (tag.tag !== existingTag.tag) {
if (spaceModel) {
await this.checkTagReuse(
tag.tag,
existingTag.product.uuid,
spaceModel,
);
} else {
await this.checkTagReuse(
tag.tag,
existingTag.product.uuid,
subspaceModel.spaceModel,
);
}
if (tag.tag) {
existingTag.tag = tag.tag;
}
}
return await queryRunner.manager.save(existingTag);
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
error.message || 'Failed to update tags',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async deleteTags(tagUuids: string[], queryRunner: QueryRunner) {
try {
const deletePromises = tagUuids.map((id) =>
queryRunner.manager.update(
this.tagModelRepository.target,
{ uuid: id },
{ disabled: true },
),
);
await Promise.all(deletePromises);
return { message: 'Tags deleted successfully', tagUuids };
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
error.message || 'Failed to delete tags',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
private findDuplicateTags(tags: CreateTagModelDto[]): string[] {
const seen = new Map<string, boolean>();
const duplicates: string[] = [];
tags.forEach((tagDto) => {
const key = `${tagDto.productUuid}-${tagDto.tag}`;
if (seen.has(key)) {
duplicates.push(`${tagDto.tag} for Product: ${tagDto.productUuid}`);
} else {
seen.set(key, true);
}
});
return duplicates;
}
async modifyTags(
tags: ModifyTagModelDto[],
queryRunner: QueryRunner,
spaceModel?: SpaceModelEntity,
subspaceModel?: SubspaceModelEntity,
): Promise<ModifiedTagsModelPayload> {
const modifiedTagModels: ModifiedTagsModelPayload = {
added: [],
updated: [],
deleted: [],
};
try {
const tagsToDelete = tags.filter(
(tag) => tag.action === ModifyAction.DELETE,
);
for (const tag of tags) {
if (tag.action === ModifyAction.ADD) {
const createTagDto: CreateTagModelDto = {
tag: tag.tag as string,
uuid: tag.uuid,
productUuid: tag.productUuid as string,
};
const newModel = await this.createTags(
[createTagDto],
queryRunner,
spaceModel,
subspaceModel,
null,
tagsToDelete,
);
modifiedTagModels.added.push(...newModel);
} else if (tag.action === ModifyAction.UPDATE) {
const updatedModel = await this.updateTag(
tag,
queryRunner,
spaceModel,
subspaceModel,
);
modifiedTagModels.updated.push(updatedModel);
} else if (tag.action === ModifyAction.DELETE) {
await queryRunner.manager.update(
this.tagModelRepository.target,
{ uuid: tag.uuid },
{ disabled: true },
);
modifiedTagModels.deleted.push(tag.uuid);
} else {
throw new HttpException(
`Invalid action "${tag.action}" provided.`,
HttpStatus.BAD_REQUEST,
);
}
}
return modifiedTagModels;
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
`An error occurred while modifying tag models: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
private async checkTagReuse(
tag: string,
productUuid: string,
spaceModel: SpaceModelEntity,
tagsToDelete?: ModifyTagModelDto[],
): Promise<void> {
try {
// Query to find existing tags
const tagExists = await this.tagModelRepository.find({
where: [
{
tag,
spaceModel: { uuid: spaceModel.uuid },
product: { uuid: productUuid },
disabled: false,
},
{
tag,
subspaceModel: { spaceModel: { uuid: spaceModel.uuid } },
product: { uuid: productUuid },
disabled: false,
},
],
});
// Remove tags that are marked for deletion
const filteredTagExists = tagExists.filter(
(existingTag) =>
!tagsToDelete?.some(
(deleteTag) => deleteTag.uuid === existingTag.uuid,
),
);
// If any tags remain, throw an exception
if (filteredTagExists.length > 0) {
throw new HttpException(
`Tag ${tag} can't be reused`,
HttpStatus.CONFLICT,
);
}
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
console.error(`Error while checking tag reuse: ${error.message}`);
throw new HttpException(
`An error occurred while checking tag reuse: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
private async prepareTagEntity(
tagDto: CreateTagModelDto,
queryRunner: QueryRunner,
spaceModel?: SpaceModelEntity,
subspaceModel?: SubspaceModelEntity,
tagsToDelete?: ModifyTagModelDto[],
): Promise<TagModel> {
try {
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,
tagsToDelete,
);
} 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: spaceModel,
subspaceModel: subspaceModel,
});
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
`An error occurred while preparing the tag entity: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async getTagByUuid(uuid: string): Promise<TagModel> {
const tag = await this.tagModelRepository.findOne({
where: { uuid, disabled: false },
relations: ['product'],
});
if (!tag) {
throw new HttpException(
`Tag model with ID ${uuid} not found.`,
HttpStatus.NOT_FOUND,
);
}
return tag;
}
async getTagByName(
tag: string,
subspaceUuid?: string,
spaceUuid?: string,
): Promise<TagModel> {
const queryConditions: any = { tag };
if (spaceUuid) {
queryConditions.spaceModel = { uuid: spaceUuid };
} else if (subspaceUuid) {
queryConditions.subspaceModel = { uuid: subspaceUuid };
} else {
throw new HttpException(
'Either spaceUuid or subspaceUuid must be provided.',
HttpStatus.BAD_REQUEST,
);
}
queryConditions.disabled = false;
const existingTag = await this.tagModelRepository.findOne({
where: queryConditions,
relations: ['product'],
});
if (!existingTag) {
throw new HttpException(
`Tag model with tag "${tag}" not found.`,
HttpStatus.NOT_FOUND,
);
}
return existingTag;
}
getSubspaceTagsToBeAdded(
spaceTags?: ModifyTagModelDto[],
subspaceModels?: ModifySubspaceModelDto[],
): ModifyTagModelDto[] {
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: ModifyTagModelDto[],
subspaceModels: ModifySubspaceModelDto[],
): ModifySubspaceModelDto[] {
if (!subspaceModels || subspaceModels.length === 0) {
return [];
}
// Extract tags marked for addition in spaceTags
const spaceTagsToAdd = spaceTags.filter((tag) => tag.action === 'add');
const subspaceTagsToAdd = subspaceModels.flatMap(
(subspace) => subspace.tags?.filter((tag) => tag.action === 'add') || [],
);
const subspaceTagsToDelete = subspaceModels.flatMap(
(subspace) =>
subspace.tags?.filter((tag) => tag.action === 'delete') || [],
);
const subspaceTagsToDeleteUuids = new Set(
subspaceTagsToDelete.map((tag) => tag.uuid),
);
const commonTagsInSubspaces = subspaceTagsToAdd.filter((tag) =>
subspaceTagsToDeleteUuids.has(tag.uuid),
);
// 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
let modifiedSubspaces = subspaceModels.map((subspace) => ({
...subspace,
tags: subspace.tags?.filter((tag) => !commonTagUuids.has(tag.uuid)) || [],
}));
modifiedSubspaces = modifiedSubspaces.map((subspace) => ({
...subspace,
tags:
subspace.tags?.filter(
(tag) =>
!(
tag.action === 'delete' &&
commonTagsInSubspaces.some(
(commonTag) => commonTag.uuid === tag.uuid,
)
),
) || [],
}));
return modifiedSubspaces;
}
}

View File

@ -2,11 +2,7 @@ import { SpaceRepositoryModule } from '@app/common/modules/space/space.repositor
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import { SpaceModelController } from './controllers'; import { SpaceModelController } from './controllers';
import { import { SpaceModelService, SubSpaceModelService } from './services';
SpaceModelService,
SubSpaceModelService,
TagModelService,
} from './services';
import { import {
SpaceModelProductAllocationRepoitory, SpaceModelProductAllocationRepoitory,
SpaceModelRepository, SpaceModelRepository,
@ -64,7 +60,6 @@ const CommandHandlers = [
SubspaceModelRepository, SubspaceModelRepository,
ProductRepository, ProductRepository,
SubspaceRepository, SubspaceRepository,
TagModelService,
TagModelRepository, TagModelRepository,
SubSpaceService, SubSpaceService,
ValidationService, ValidationService,

View File

@ -59,7 +59,6 @@ import { TagService } from './services/tag';
import { import {
SpaceModelService, SpaceModelService,
SubSpaceModelService, SubSpaceModelService,
TagModelService,
} from 'src/space-model/services'; } from 'src/space-model/services';
import { UserService, UserSpaceService } from 'src/users/services'; import { UserService, UserSpaceService } from 'src/users/services';
import { UserDevicePermissionService } from 'src/user-device-permission/services'; import { UserDevicePermissionService } from 'src/user-device-permission/services';
@ -120,7 +119,6 @@ export const CommandHandlers = [DisableSpaceHandler];
SceneDeviceRepository, SceneDeviceRepository,
SpaceModelService, SpaceModelService,
SubSpaceModelService, SubSpaceModelService,
TagModelService,
ProjectRepository, ProjectRepository,
SpaceModelRepository, SpaceModelRepository,
SubspaceModelRepository, SubspaceModelRepository,