Merge pull request #273 from SyncrowIOT/feat/project-tag

New tag entitiy
This commit is contained in:
hannathkadher
2025-03-02 00:25:55 +04:00
committed by GitHub
93 changed files with 6462 additions and 3250 deletions

View File

@ -14,6 +14,7 @@ import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { SpaceModelService } from '../services';
import {
CreateSpaceModelDto,
LinkSpacesToModelDto,
SpaceModelParam,
UpdateSpaceModelDto,
} from '../dtos';
@ -70,9 +71,9 @@ export class SpaceModelController {
@UseGuards(PermissionsGuard)
@Permissions('SPACE_MODEL_VIEW')
@ApiOperation({
summary: ControllerRoute.SPACE_MODEL.ACTIONS.LIST_SPACE_MODEL_SUMMARY,
summary: ControllerRoute.SPACE_MODEL.ACTIONS.GET_SPACE_MODEL_SUMMARY,
description:
ControllerRoute.SPACE_MODEL.ACTIONS.LIST_SPACE_MODEL_DESCRIPTION,
ControllerRoute.SPACE_MODEL.ACTIONS.GET_SPACE_MODEL_DESCRIPTION,
})
@Get(':spaceModelUuid')
async get(@Param() param: SpaceModelParam): Promise<BaseResponseDto> {
@ -107,4 +108,20 @@ export class SpaceModelController {
async delete(@Param() param: SpaceModelParam): Promise<BaseResponseDto> {
return await this.spaceModelService.deleteSpaceModel(param);
}
@ApiBearerAuth()
@UseGuards(PermissionsGuard)
@Permissions('SPACE_MODEL_LINK')
@ApiOperation({
summary: ControllerRoute.SPACE_MODEL.ACTIONS.DELETE_SPACE_MODEL_SUMMARY,
description:
ControllerRoute.SPACE_MODEL.ACTIONS.DELETE_SPACE_MODEL_DESCRIPTION,
})
@Post(':spaceModelUuid/spaces/link')
async link(
@Param() params: SpaceModelParam,
@Body() dto: LinkSpacesToModelDto,
): Promise<BaseResponseDto> {
return await this.spaceModelService.linkSpaceModel(params, dto);
}
}

View File

@ -2,7 +2,7 @@ import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString, IsArray, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
import { CreateSubspaceModelDto } from './subspaces-model-dtos/create-subspace-model.dto';
import { CreateTagModelDto } from './tag-model-dtos/create-tag-model.dto';
import { ProcessTagDto } from 'src/tags/dtos';
export class CreateSpaceModelDto {
@ApiProperty({
@ -24,10 +24,10 @@ export class CreateSpaceModelDto {
@ApiProperty({
description: 'List of tags associated with the space model',
type: [CreateTagModelDto],
type: [ProcessTagDto],
})
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreateTagModelDto)
tags?: CreateTagModelDto[];
@Type(() => ProcessTagDto)
tags?: ProcessTagDto[];
}

View File

@ -4,3 +4,4 @@ export * from './update-space-model.dto';
export * from './space-model-param';
export * from './subspaces-model-dtos';
export * from './tag-model-dtos';
export * from './link-space-model.dto';

View File

@ -0,0 +1,25 @@
import { IsArray, ArrayNotEmpty, IsUUID, IsBoolean } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class LinkSpacesToModelDto {
@ApiProperty({
description: 'List of space UUIDs to be linked to the space model',
type: [String],
example: [
'550e8400-e29b-41d4-a716-446655440000',
'f47ac10b-58cc-4372-a567-0e02b2c3d479',
],
})
@IsArray()
@ArrayNotEmpty()
@IsUUID()
spaceUuids: string[];
@ApiProperty({
description: 'Whether to overwrite existing space model links',
type: Boolean,
example: false,
})
@IsBoolean()
overwrite: boolean;
}

View File

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

View File

@ -1,7 +1,7 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator';
import { CreateTagModelDto } from '../tag-model-dtos/create-tag-model.dto';
import { Type } from 'class-transformer';
import { ProcessTagDto } from 'src/tags/dtos';
export class CreateSubspaceModelDto {
@ApiProperty({
@ -14,10 +14,10 @@ export class CreateSubspaceModelDto {
@ApiProperty({
description: 'List of tag models associated with the subspace',
type: [CreateTagModelDto],
type: [ProcessTagDto],
})
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreateTagModelDto)
tags?: CreateTagModelDto[];
@Type(() => ProcessTagDto)
tags?: ProcessTagDto[];
}

View File

@ -3,7 +3,7 @@ import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
export class CreateTagModelDto {
@ApiProperty({
description: 'Tag models associated with the space or subspace models',
description: 'Tag associated with the space or subspace models',
example: 'Temperature Control',
})
@IsNotEmpty()

View File

@ -1,6 +1,6 @@
import { ModifyAction } from '@app/common/constants/modify-action.enum';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsString, IsOptional, IsEnum } from 'class-validator';
import { IsString, IsOptional, IsEnum, IsUUID } from 'class-validator';
export class ModifyTagModelDto {
@ApiProperty({
@ -11,20 +11,29 @@ export class ModifyTagModelDto {
action: ModifyAction;
@ApiPropertyOptional({
description: 'UUID of the tag model (required for update/delete)',
description: 'UUID of the new tag',
example: '123e4567-e89b-12d3-a456-426614174000',
})
@IsOptional()
@IsString()
uuid?: string;
@IsUUID()
newTagUuid: string;
@ApiPropertyOptional({
description: 'Name of the tag model (required for add/update)',
description:
'UUID of an existing tag (required for update/delete, optional for add)',
example: 'a1b2c3d4-5678-90ef-abcd-1234567890ef',
})
@IsOptional()
@IsUUID()
tagUuid?: string;
@ApiPropertyOptional({
description: 'Name of the tag (required for add/update)',
example: 'Temperature Sensor',
})
@IsOptional()
@IsString()
tag?: string;
name?: string;
@ApiPropertyOptional({
description:
@ -32,6 +41,6 @@ export class ModifyTagModelDto {
example: 'c789a91e-549a-4753-9006-02f89e8170e0',
})
@IsOptional()
@IsString()
@IsUUID()
productUuid?: string;
}

View File

@ -1,6 +1,6 @@
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { PropogateUpdateSpaceModelCommand } from '../commands';
import { SpaceEntity, SpaceRepository } from '@app/common/modules/space';
import { SpaceRepository } from '@app/common/modules/space';
import { SubspaceRepository } from '@app/common/modules/space/repositories/subspace.repository';
import {
SpaceModelEntity,
@ -10,10 +10,10 @@ import {
import { DataSource, QueryRunner } from 'typeorm';
import { SubSpaceService } from 'src/space/services';
import { TagService } from 'src/space/services/tag';
import { TagModelService } from '../services';
import { UpdatedSubspaceModelPayload } from '../interfaces';
import { ModifyAction } from '@app/common/constants/modify-action.enum';
import { ModifySubspaceDto } from 'src/space/dtos';
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
@CommandHandler(PropogateUpdateSpaceModelCommand)
export class PropogateUpdateSpaceModelHandler
@ -25,7 +25,6 @@ export class PropogateUpdateSpaceModelHandler
private readonly dataSource: DataSource,
private readonly subSpaceService: SubSpaceService,
private readonly tagService: TagService,
private readonly tagModelService: TagModelService,
) {}
async execute(command: PropogateUpdateSpaceModelCommand): Promise<void> {

View File

@ -1,2 +1,3 @@
export * from './update-subspace.interface';
export * from './modify-subspace.interface';
export * from './single-subspace.interface';

View File

@ -0,0 +1,9 @@
import { SubspaceModelEntity } from '@app/common/modules/space-model';
import { ModifyTagModelDto } from '../dtos';
import { ModifyAction } from '@app/common/constants/modify-action.enum';
export interface ISingleSubspaceModel {
subspaceModel: SubspaceModelEntity;
action: ModifyAction;
tags: ModifyTagModelDto[];
}

View File

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

View File

@ -0,0 +1,369 @@
import { In, QueryRunner } from 'typeorm';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import {
SpaceModelEntity,
SpaceModelProductAllocationEntity,
SpaceModelProductAllocationRepoitory,
SubspaceModelProductAllocationEntity,
} from '@app/common/modules/space-model';
import { TagService as NewTagService } from 'src/tags/services';
import { ProcessTagDto } from 'src/tags/dtos';
import { ModifySubspaceModelDto, ModifyTagModelDto } from '../dtos';
import { ModifyAction } from '@app/common/constants/modify-action.enum';
import { NewTagEntity } from '@app/common/modules/tag';
import { ProductEntity } from '@app/common/modules/product/entities';
@Injectable()
export class SpaceModelProductAllocationService {
constructor(
private readonly tagService: NewTagService,
private readonly spaceModelProductAllocationRepository: SpaceModelProductAllocationRepoitory,
) {}
async createProductAllocations(
projectUuid: string,
spaceModel: SpaceModelEntity,
tags: ProcessTagDto[],
queryRunner?: QueryRunner,
modifySubspaceModels?: ModifySubspaceModelDto[],
): Promise<SpaceModelProductAllocationEntity[]> {
try {
if (!tags.length) return [];
const processedTags = await this.tagService.processTags(
tags,
projectUuid,
queryRunner,
);
const productAllocations: SpaceModelProductAllocationEntity[] = [];
const existingAllocations = new Map<
string,
SpaceModelProductAllocationEntity
>();
for (const tag of processedTags) {
let isTagNeeded = true;
if (modifySubspaceModels) {
const relatedSubspaces = await queryRunner.manager.find(
SubspaceModelProductAllocationEntity,
{
where: {
product: tag.product,
subspaceModel: { spaceModel: { uuid: spaceModel.uuid } },
tags: { uuid: tag.uuid },
},
relations: ['subspaceModel', 'tags'],
},
);
for (const subspaceWithTag of relatedSubspaces) {
const modifyingSubspace = modifySubspaceModels.find(
(subspace) =>
subspace.action === ModifyAction.UPDATE &&
subspace.uuid === subspaceWithTag.subspaceModel.uuid,
);
if (
modifyingSubspace &&
modifyingSubspace.tags &&
modifyingSubspace.tags.some(
(subspaceTag) =>
subspaceTag.action === ModifyAction.DELETE &&
subspaceTag.tagUuid === tag.uuid,
)
) {
isTagNeeded = true;
break;
}
}
}
if (isTagNeeded) {
await this.validateTagWithinSpaceModel(queryRunner, tag, spaceModel);
let allocation = existingAllocations.get(tag.product.uuid);
if (!allocation) {
allocation = await this.getAllocationByProduct(
tag.product,
spaceModel,
queryRunner,
);
if (allocation) {
existingAllocations.set(tag.product.uuid, allocation);
}
}
if (!allocation) {
allocation = this.createNewAllocation(spaceModel, tag, queryRunner);
productAllocations.push(allocation);
} else if (!allocation.tags.some((t) => t.uuid === tag.uuid)) {
allocation.tags.push(tag);
await this.saveAllocation(allocation, queryRunner);
}
}
}
if (productAllocations.length > 0) {
await this.saveAllocations(productAllocations, queryRunner);
}
return productAllocations;
} catch (error) {
throw this.handleError(error, 'Failed to create product allocations');
}
}
async updateProductAllocations(
dtos: ModifyTagModelDto[],
projectUuid: string,
spaceModel: SpaceModelEntity,
queryRunner: QueryRunner,
modifySubspaceModels?: ModifySubspaceModelDto[],
): Promise<void> {
try {
await Promise.all([
this.processAddActions(
dtos,
projectUuid,
spaceModel,
queryRunner,
modifySubspaceModels,
),
this.processDeleteActions(dtos, queryRunner),
]);
} catch (error) {
throw this.handleError(error, 'Error while updating product allocations');
}
}
private async processAddActions(
dtos: ModifyTagModelDto[],
projectUuid: string,
spaceModel: SpaceModelEntity,
queryRunner: QueryRunner,
modifySubspaceModels?: ModifySubspaceModelDto[],
): Promise<void> {
const addDtos: ProcessTagDto[] = dtos
.filter((dto) => dto.action === ModifyAction.ADD)
.map((dto) => ({
name: dto.name,
productUuid: dto.productUuid,
uuid: dto.newTagUuid,
}));
if (addDtos.length > 0) {
await this.createProductAllocations(
projectUuid,
spaceModel,
addDtos,
queryRunner,
modifySubspaceModels,
);
}
}
private createNewAllocation(
spaceModel: SpaceModelEntity,
tag: NewTagEntity,
queryRunner?: QueryRunner,
): SpaceModelProductAllocationEntity {
return queryRunner
? queryRunner.manager.create(SpaceModelProductAllocationEntity, {
spaceModel,
product: tag.product,
tags: [tag],
})
: this.spaceModelProductAllocationRepository.create({
spaceModel,
product: tag.product,
tags: [tag],
});
}
private async getAllocationByProduct(
product: ProductEntity,
spaceModel: SpaceModelEntity,
queryRunner?: QueryRunner,
): Promise<SpaceModelProductAllocationEntity | null> {
return queryRunner
? queryRunner.manager.findOne(SpaceModelProductAllocationEntity, {
where: { spaceModel, product: product },
relations: ['tags'],
})
: this.spaceModelProductAllocationRepository.findOne({
where: { spaceModel, product: product },
relations: ['tags'],
});
}
private async saveAllocation(
allocation: SpaceModelProductAllocationEntity,
queryRunner?: QueryRunner,
) {
queryRunner
? await queryRunner.manager.save(
SpaceModelProductAllocationEntity,
allocation,
)
: await this.spaceModelProductAllocationRepository.save(allocation);
}
private async saveAllocations(
allocations: SpaceModelProductAllocationEntity[],
queryRunner?: QueryRunner,
) {
queryRunner
? await queryRunner.manager.save(
SpaceModelProductAllocationEntity,
allocations,
)
: await this.spaceModelProductAllocationRepository.save(allocations);
}
private handleError(error: any, message: string): HttpException {
return new HttpException(
error instanceof HttpException ? error.message : message,
error instanceof HttpException
? error.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR,
);
}
private async processDeleteActions(
dtos: ModifyTagModelDto[],
queryRunner: QueryRunner,
): Promise<SpaceModelProductAllocationEntity[]> {
try {
if (!dtos || dtos.length === 0) {
throw new Error('No DTOs provided for deletion.');
}
const tagUuidsToDelete = dtos
.filter((dto) => dto.action === ModifyAction.DELETE && dto.tagUuid)
.map((dto) => dto.tagUuid);
if (tagUuidsToDelete.length === 0) return [];
const allocationsToUpdate = await queryRunner.manager.find(
SpaceModelProductAllocationEntity,
{
where: { tags: { uuid: In(tagUuidsToDelete) } },
relations: ['tags'],
},
);
if (!allocationsToUpdate || allocationsToUpdate.length === 0) return [];
const deletedAllocations: SpaceModelProductAllocationEntity[] = [];
const allocationUpdates: SpaceModelProductAllocationEntity[] = [];
for (const allocation of allocationsToUpdate) {
const updatedTags = allocation.tags.filter(
(tag) => !tagUuidsToDelete.includes(tag.uuid),
);
if (updatedTags.length === allocation.tags.length) {
continue;
}
if (updatedTags.length === 0) {
deletedAllocations.push(allocation);
} else {
allocation.tags = updatedTags;
allocationUpdates.push(allocation);
}
}
if (allocationUpdates.length > 0) {
await queryRunner.manager.save(
SpaceModelProductAllocationEntity,
allocationUpdates,
);
}
if (deletedAllocations.length > 0) {
await queryRunner.manager.remove(
SpaceModelProductAllocationEntity,
deletedAllocations,
);
}
await queryRunner.manager
.createQueryBuilder()
.delete()
.from('space_model_product_tags')
.where(
'space_model_product_allocation_id NOT IN ' +
queryRunner.manager
.createQueryBuilder()
.select('uuid')
.from(SpaceModelProductAllocationEntity, 'allocation')
.getQuery(),
)
.execute();
return deletedAllocations;
} catch (error) {
throw this.handleError(error, `Failed to delete tags in space model`);
}
}
private async validateTagWithinSpaceModel(
queryRunner: QueryRunner,
tag: NewTagEntity,
spaceModel: SpaceModelEntity,
) {
const existingAllocationsForProduct = await queryRunner.manager.find(
SpaceModelProductAllocationEntity,
{
where: { spaceModel, product: tag.product },
relations: ['tags'],
},
);
//Flatten all existing tags for this product in the space
const existingTagsForProduct = existingAllocationsForProduct.flatMap(
(allocation) => allocation.tags,
);
// Check if the tag is already assigned to the same product in this subspace
const isDuplicateTag = existingTagsForProduct.some(
(existingTag) => existingTag.uuid === tag.uuid,
);
if (isDuplicateTag) {
throw new HttpException(
`Tag ${tag.uuid} is already allocated to product ${tag.product.uuid} within this space (${spaceModel.uuid}).`,
HttpStatus.BAD_REQUEST,
);
}
}
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,33 +1,45 @@
import {
SpaceModelEntity,
SpaceModelProductAllocationEntity,
SpaceModelRepository,
SubspaceModelProductAllocationEntity,
} from '@app/common/modules/space-model';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { CreateSpaceModelDto, UpdateSpaceModelDto } from '../dtos';
import {
CreateSpaceModelDto,
LinkSpacesToModelDto,
UpdateSpaceModelDto,
} from '../dtos';
import { ProjectParam } from 'src/community/dtos';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { SubSpaceModelService } from './subspace/subspace-model.service';
import { DataSource, QueryRunner } from 'typeorm';
import { DataSource, In, QueryRunner, SelectQueryBuilder } from 'typeorm';
import {
TypeORMCustomModel,
TypeORMCustomModelFindAllQuery,
} from '@app/common/models/typeOrmCustom.model';
import { PageResponse } from '@app/common/dto/pagination.response.dto';
import { SpaceModelParam } from '../dtos/space-model-param';
import { ProjectService } from 'src/project/services';
import { ProjectEntity } from '@app/common/modules/project/entities';
import { TagModelService } from './tag-model.service';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { CommandBus } from '@nestjs/cqrs';
import { ProcessTagDto } from 'src/tags/dtos';
import { SpaceModelProductAllocationService } from './space-model-product-allocation.service';
import { PropogateDeleteSpaceModelCommand } from '../commands';
import {
PropogateDeleteSpaceModelCommand,
PropogateUpdateSpaceModelCommand,
} from '../commands';
SpaceProductAllocationRepository,
SpaceRepository,
} from '@app/common/modules/space';
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
import {
ModifiedTagsModelPayload,
ModifySubspaceModelPayload,
} from '../interfaces';
import { SpaceModelDto } from '@app/common/modules/space-model/dtos';
SubspaceProductAllocationRepository,
SubspaceRepository,
} from '@app/common/modules/space/repositories/subspace.repository';
import {
ORPHAN_COMMUNITY_NAME,
ORPHAN_SPACE_NAME,
} from '@app/common/constants/orphan-constant';
import { DeviceRepository } from '@app/common/modules/device/repositories';
@Injectable()
export class SpaceModelService {
@ -36,8 +48,13 @@ export class SpaceModelService {
private readonly spaceModelRepository: SpaceModelRepository,
private readonly projectService: ProjectService,
private readonly subSpaceModelService: SubSpaceModelService,
private readonly tagModelService: TagModelService,
private commandBus: CommandBus,
private readonly spaceModelProductAllocationService: SpaceModelProductAllocationService,
private readonly spaceRepository: SpaceRepository,
private readonly spaceProductAllocationRepository: SpaceProductAllocationRepository,
private readonly subspaceRepository: SubspaceRepository,
private readonly subspaceProductAllocationRepository: SubspaceProductAllocationRepository,
private readonly deviceRepository: DeviceRepository,
) {}
async createSpaceModel(
@ -46,11 +63,15 @@ export class SpaceModelService {
): Promise<BaseResponseDto> {
const { modelName, subspaceModels, tags } = createSpaceModelDto;
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const project = await this.validateProject(params.projectUuid);
const project = await this.projectService.findOne(params.projectUuid);
if (!project) {
throw new HttpException('Project not found', HttpStatus.NOT_FOUND);
}
await this.validateNameUsingQueryRunner(
modelName,
@ -65,22 +86,27 @@ export class SpaceModelService {
const savedSpaceModel = await queryRunner.manager.save(spaceModel);
const subspaceTags =
this.subSpaceModelService.extractTagsFromSubspaceModels(subspaceModels);
const allTags = [...tags, ...subspaceTags];
this.validateUniqueTags(allTags);
if (subspaceModels?.length) {
savedSpaceModel.subspaceModels =
await this.subSpaceModelService.createSubSpaceModels(
subspaceModels,
await this.subSpaceModelService.createModels(
savedSpaceModel,
subspaceModels,
queryRunner,
tags,
);
}
if (tags?.length) {
savedSpaceModel.tags = await this.tagModelService.createTags(
await this.spaceModelProductAllocationService.createProductAllocations(
params.projectUuid,
spaceModel,
tags,
queryRunner,
savedSpaceModel,
null,
);
}
@ -97,7 +123,7 @@ export class SpaceModelService {
const errorMessage =
error instanceof HttpException
? error.message
: 'An unexpected error occurred';
: `An unexpected error occurred: ${error.message}`;
const statusCode =
error instanceof HttpException
? error.getStatus()
@ -122,34 +148,9 @@ export class SpaceModelService {
disabled: false,
};
pageable.include =
'subspaceModels,tags,subspaceModels.tags,tags.product,subspaceModels.tags.product';
'subspaceModels.productAllocations,subspaceModelProductAllocations.tags,subspaceModels, productAllocations, productAllocations.tags';
const queryBuilder = await this.spaceModelRepository
.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 queryBuilder = this.buildSpaceModelQuery(param.projectUuid);
const customModel = TypeORMCustomModel(this.spaceModelRepository);
const { baseResponseDto, paginationResponseDto } =
@ -162,10 +163,14 @@ export class SpaceModelService {
queryBuilder,
);
return new PageResponse<SpaceModelDto>(
baseResponseDto,
paginationResponseDto,
);
const formattedData = this.transformSpaceModelData(baseResponseDto.data);
return {
code: 200,
data: formattedData,
message: 'Success get list spaceModel',
...paginationResponseDto,
};
} catch (error) {
throw new HttpException(
`Error fetching paginated list: ${error.message}`,
@ -181,11 +186,12 @@ export class SpaceModelService {
async update(dto: UpdateSpaceModelDto, param: SpaceModelParam) {
const queryRunner = this.dataSource.createQueryRunner();
await this.validateProject(param.projectUuid);
const spaceModel = await this.validateSpaceModel(param.spaceModelUuid);
const spaceModel = await this.validateSpaceModel(
param.spaceModelUuid,
param.projectUuid,
);
await queryRunner.connect();
let modifiedSubspaceModels: ModifySubspaceModelPayload = {};
let modifiedTagsModelPayload: ModifiedTagsModelPayload = {};
try {
await queryRunner.startTransaction();
@ -203,46 +209,26 @@ export class SpaceModelService {
);
}
const spaceTagsAfterMove = this.tagModelService.getSubspaceTagsToBeAdded(
dto.tags,
dto.subspaceModels,
);
const modifiedSubspaces = this.tagModelService.getModifiedSubspaces(
dto.tags,
dto.subspaceModels,
);
if (dto.subspaceModels) {
modifiedSubspaceModels =
await this.subSpaceModelService.modifySubSpaceModels(
modifiedSubspaces,
spaceModel,
queryRunner,
);
}
if (dto.tags) {
modifiedTagsModelPayload = await this.tagModelService.modifyTags(
spaceTagsAfterMove,
queryRunner,
await this.subSpaceModelService.modifySubspaceModels(
dto.subspaceModels,
spaceModel,
null,
queryRunner,
param.projectUuid,
dto.tags,
);
}
await queryRunner.commitTransaction();
await this.commandBus.execute(
new PropogateUpdateSpaceModelCommand({
spaceModel: spaceModel,
modifiedSpaceModels: {
modifiedSubspaceModels,
modifiedTags: modifiedTagsModelPayload,
},
if (dto.tags) {
await this.spaceModelProductAllocationService.updateProductAllocations(
dto.tags,
param.projectUuid,
spaceModel,
queryRunner,
}),
);
dto.subspaceModels,
);
}
await queryRunner.commitTransaction();
return new SuccessResponseDto({
message: 'SpaceModel updated successfully',
@ -269,25 +255,27 @@ export class SpaceModelService {
try {
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) {
const deleteSubspaceDtos = spaceModel.subspaceModels.map(
(subspace) => ({
subspaceUuid: subspace.uuid,
}),
const deleteSubspaceUuids = spaceModel.subspaceModels.map(
(subspace) => subspace.uuid,
);
await this.subSpaceModelService.deleteSubspaceModels(
deleteSubspaceDtos,
await this.subSpaceModelService.clearModels(
deleteSubspaceUuids,
queryRunner,
);
}
if (spaceModel.tags?.length) {
const deleteSpaceTagsDtos = spaceModel.tags.map((tag) => tag.uuid);
await this.tagModelService.deleteTags(deleteSpaceTagsDtos, queryRunner);
if (spaceModel.productAllocations?.length) {
await this.spaceModelProductAllocationService.clearAllAllocations(
spaceModel.uuid,
queryRunner,
);
}
await queryRunner.manager.update(
@ -326,6 +314,205 @@ export class SpaceModelService {
}
}
async linkSpaceModel(
params: SpaceModelParam,
dto: LinkSpacesToModelDto,
): Promise<BaseResponseDto> {
const project = await this.validateProject(params.projectUuid);
try {
const spaceModel = await this.spaceModelRepository.findOne({
where: { uuid: params.spaceModelUuid },
relations: [
'productAllocations',
'subspaceModels',
'subspaceModels.productAllocations',
],
});
if (!spaceModel) {
throw new HttpException(
`Space Model with UUID ${params.spaceModelUuid} not found`,
HttpStatus.NOT_FOUND,
);
}
if (!spaceModel.productAllocations.length) {
throw new HttpException(
`Space Model ${params.spaceModelUuid} has no product allocations`,
HttpStatus.BAD_REQUEST,
);
}
const spaces = await this.spaceRepository.find({
where: { uuid: In(dto.spaceUuids), disabled: false },
relations: [
'spaceModel',
'devices',
'subspaces',
'productAllocations',
'subspaces.productAllocations',
'community',
],
});
if (!spaces.length) {
throw new HttpException(
`No spaces found for the given UUIDs`,
HttpStatus.NOT_FOUND,
);
}
await Promise.all(
spaces.map(async (space) => {
const hasDependencies =
space.devices.length > 0 ||
space.subspaces.length > 0 ||
space.productAllocations.length > 0;
if (!hasDependencies && !space.spaceModel) {
await this.linkToSpace(space, spaceModel);
} else if (dto.overwrite) {
await this.overwriteSpace(space, project);
await this.linkToSpace(space, spaceModel);
}
}),
);
return new SuccessResponseDto({
message: 'Spaces linked successfully',
data: dto.spaceUuids,
});
} catch (error) {
throw new HttpException(
`Failed to link space model: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async linkToSpace(
space: SpaceEntity,
spaceModel: SpaceModelEntity,
): Promise<void> {
try {
space.spaceModel = spaceModel;
await this.spaceRepository.save(space);
const spaceProductAllocations = spaceModel.productAllocations.map(
(modelAllocation) =>
this.spaceProductAllocationRepository.create({
space,
inheritedFromModel: modelAllocation,
product: modelAllocation.product,
tags: modelAllocation.tags,
}),
);
await this.spaceProductAllocationRepository.save(spaceProductAllocations);
if (!spaceModel.subspaceModels.length) {
throw new HttpException(
`Space Model ${spaceModel.uuid} has no subspaces`,
HttpStatus.BAD_REQUEST,
);
}
await Promise.all(
spaceModel.subspaceModels.map(async (subspaceModel) => {
const subspace = this.subspaceRepository.create({
subspaceName: subspaceModel.subspaceName,
subSpaceModel: subspaceModel,
space: space,
});
await this.subspaceRepository.save(subspace);
const subspaceAllocations = subspaceModel.productAllocations.map(
(modelAllocation) =>
this.subspaceProductAllocationRepository.create({
subspace,
inheritedFromModel: modelAllocation,
product: modelAllocation.product,
tags: modelAllocation.tags,
}),
);
if (subspaceAllocations.length) {
await this.subspaceProductAllocationRepository.save(
subspaceAllocations,
);
}
}),
);
} catch (error) {
throw new HttpException(
`Failed to link space ${space.uuid} to space model ${spaceModel.uuid}: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async overwriteSpace(
space: SpaceEntity,
project: ProjectEntity,
): Promise<void> {
try {
if (space.productAllocations.length) {
await this.spaceProductAllocationRepository.delete({
uuid: In(
space.productAllocations.map((allocation) => allocation.uuid),
),
});
}
await Promise.all(
space.subspaces.map(async (subspace) => {
await this.subspaceRepository.update(
{ uuid: subspace.uuid },
{ disabled: true },
);
if (subspace.productAllocations.length) {
await this.subspaceProductAllocationRepository.delete({
uuid: In(
subspace.productAllocations.map(
(allocation) => allocation.uuid,
),
),
});
}
}),
);
if (space.devices.length > 0) {
const orphanSpace = await this.spaceRepository.findOne({
where: {
community: {
name: `${ORPHAN_COMMUNITY_NAME}-${project.name}`,
},
spaceName: ORPHAN_SPACE_NAME,
},
});
if (!orphanSpace) {
throw new HttpException(
`Orphan space not found in community ${project.name}`,
HttpStatus.NOT_FOUND,
);
}
await this.deviceRepository.update(
{ uuid: In(space.devices.map((device) => device.uuid)) },
{ spaceDevice: orphanSpace },
);
}
} catch (error) {
throw new Error(
`Failed to overwrite space ${space.uuid}: ${error.message}`,
);
}
}
async validateName(modelName: string, projectUuid: string): Promise<void> {
const isModelExist = await this.spaceModelRepository.findOne({
where: { modelName, project: { uuid: projectUuid }, disabled: false },
@ -359,11 +546,15 @@ export class SpaceModelService {
async findOne(params: SpaceModelParam): Promise<BaseResponseDto> {
try {
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({
message: 'SpaceModel retrieved successfully',
data: spaceModel,
data: response,
});
} catch (error) {
throw new HttpException(
@ -373,8 +564,51 @@ export class SpaceModelService {
}
}
async validateSpaceModel(uuid: string): Promise<SpaceModelEntity> {
const spaceModel = await this.spaceModelRepository
async validateSpaceModel(
uuid: string,
projectUuid?: string,
): Promise<SpaceModelEntity> {
const query = this.buildSpaceModelQuery(projectUuid);
const result = await query
.andWhere('spaceModel.uuid = :uuid', { uuid })
.getOne();
if (!result) {
throw new HttpException('space model not found', HttpStatus.NOT_FOUND);
}
return result;
}
private validateUniqueTags(allTags: ProcessTagDto[]) {
const tagUuidSet = new Set<string>();
const tagNameProductSet = new Set<string>();
for (const tag of allTags) {
if (tag.uuid) {
if (tagUuidSet.has(tag.uuid)) {
throw new HttpException(
`Duplicate tag UUID found: ${tag.uuid}`,
HttpStatus.BAD_REQUEST,
);
}
tagUuidSet.add(tag.uuid);
} else {
const tagKey = `${tag.name}-${tag.productUuid}`;
if (tagNameProductSet.has(tagKey)) {
throw new HttpException(
`Duplicate tag found with name "${tag.name}" and product "${tag.productUuid}".`,
HttpStatus.BAD_REQUEST,
);
}
tagNameProductSet.add(tagKey);
}
}
}
private buildSpaceModelQuery(
projectUuid: string,
): SelectQueryBuilder<SpaceModelEntity> {
return this.spaceModelRepository
.createQueryBuilder('spaceModel')
.leftJoinAndSelect(
'spaceModel.subspaceModels',
@ -383,27 +617,90 @@ export class SpaceModelService {
{ subspaceDisabled: false },
)
.leftJoinAndSelect(
'spaceModel.tags',
'tags',
'tags.disabled = :tagsDisabled',
{ tagsDisabled: false },
'subspaceModels.productAllocations',
'subspaceModelProductAllocations',
)
.leftJoinAndSelect('tags.product', 'spaceTagproduct')
.leftJoinAndSelect(
'subspaceModels.tags',
'subspaceModelProductAllocations.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 })
.getOne();
.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 });
}
if (!spaceModel) {
throw new HttpException('space model not found', HttpStatus.NOT_FOUND);
}
return spaceModel;
private transformSpaceModelData(spaceModelsArray: SpaceModelEntity[]): any[] {
if (!Array.isArray(spaceModelsArray)) return [];
return spaceModelsArray.map((spaceModel) => ({
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),
}));
}
private extractTags(
productAllocations:
| SpaceModelProductAllocationEntity[]
| SubspaceModelProductAllocationEntity[]
| 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

@ -0,0 +1,517 @@
import { ModifyAction } from '@app/common/constants/modify-action.enum';
import {
SpaceModelEntity,
SpaceModelProductAllocationEntity,
SpaceModelProductAllocationRepoitory,
SubspaceModelEntity,
SubspaceModelProductAllocationEntity,
SubspaceModelProductAllocationRepoitory,
} from '@app/common/modules/space-model';
import { NewTagEntity } from '@app/common/modules/tag';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { ModifyTagModelDto } from 'src/space-model/dtos';
import { ISingleSubspaceModel } from 'src/space-model/interfaces';
import { ProcessTagDto } from 'src/tags/dtos';
import { TagService as NewTagService } from 'src/tags/services';
import { In, QueryRunner } from 'typeorm';
@Injectable()
export class SubspaceModelProductAllocationService {
constructor(
private readonly tagService: NewTagService,
private readonly subspaceModelProductAllocationRepository: SubspaceModelProductAllocationRepoitory,
private readonly spaceModelAllocationRepository: SpaceModelProductAllocationRepoitory,
) {}
async createProductAllocations(
subspaceModel: SubspaceModelEntity,
tags: NewTagEntity[],
queryRunner?: QueryRunner,
spaceAllocationsToExclude?: SpaceModelProductAllocationEntity[],
): Promise<SubspaceModelProductAllocationEntity[]> {
try {
const allocations: SubspaceModelProductAllocationEntity[] = [];
for (const tag of tags) {
// Step 1: Check if this specific tag is already allocated at the space level
const existingTagInSpaceModel = await (queryRunner
? queryRunner.manager.findOne(SpaceModelProductAllocationEntity, {
where: {
product: tag.product,
spaceModel: subspaceModel.spaceModel, // Check at the space level
tags: { uuid: tag.uuid }, // Check for the specific tag
},
relations: ['tags'],
})
: this.spaceModelAllocationRepository.findOne({
where: {
product: tag.product,
spaceModel: subspaceModel.spaceModel,
tags: { uuid: tag.uuid },
},
relations: ['tags'],
}));
const isExcluded = spaceAllocationsToExclude?.some(
(excludedAllocation) =>
excludedAllocation.product.uuid === tag.product.uuid &&
excludedAllocation.tags.some((t) => t.uuid === tag.uuid),
);
// If tag is found at the space level, prevent allocation at the subspace level
if (!isExcluded && existingTagInSpaceModel) {
throw new HttpException(
`Tag ${tag.uuid} (Product: ${tag.product.uuid}) is already allocated at the space level (${subspaceModel.spaceModel.uuid}). Cannot allocate the same tag in a subspace.`,
HttpStatus.BAD_REQUEST,
);
}
// Check if this specific tag is already allocated within another subspace of the same space
const existingTagInSameSpace = await (queryRunner
? queryRunner.manager.findOne(SubspaceModelProductAllocationEntity, {
where: {
product: tag.product,
subspaceModel: { spaceModel: subspaceModel.spaceModel },
tags: { uuid: tag.uuid }, // Ensure the exact tag is checked
},
relations: ['subspaceModel', 'tags'],
})
: this.subspaceModelProductAllocationRepository.findOne({
where: {
product: tag.product,
subspaceModel: { spaceModel: subspaceModel.spaceModel },
tags: { uuid: tag.uuid },
},
relations: ['subspaceModel', 'tags'],
}));
// Prevent duplicate allocation if tag exists in another subspace of the same space
if (
existingTagInSameSpace &&
existingTagInSameSpace.subspaceModel.uuid !== subspaceModel.uuid
) {
throw new HttpException(
`Tag ${tag.uuid} (Product: ${tag.product.uuid}) is already allocated in another subspace (${existingTagInSameSpace.subspaceModel.uuid}) within the same space (${subspaceModel.spaceModel.uuid}).`,
HttpStatus.BAD_REQUEST,
);
}
//Check if there are existing allocations for this product in the subspace
const existingAllocationsForProduct = await (queryRunner
? queryRunner.manager.find(SubspaceModelProductAllocationEntity, {
where: { subspaceModel, product: tag.product },
relations: ['tags'],
})
: this.subspaceModelProductAllocationRepository.find({
where: { subspaceModel, product: tag.product },
relations: ['tags'],
}));
//Flatten all existing tags for this product in the subspace
const existingTagsForProduct = existingAllocationsForProduct.flatMap(
(allocation) => allocation.tags,
);
// Check if the tag is already assigned to the same product in this subspace
const isDuplicateTag = existingTagsForProduct.some(
(existingTag) => existingTag.uuid === tag.uuid,
);
if (isDuplicateTag) {
throw new HttpException(
`Tag ${tag.uuid} is already allocated to product ${tag.product.uuid} within this subspace (${subspaceModel.uuid}).`,
HttpStatus.BAD_REQUEST,
);
}
// If no existing allocation, create a new one
if (existingAllocationsForProduct.length === 0) {
const allocation = queryRunner
? queryRunner.manager.create(SubspaceModelProductAllocationEntity, {
subspaceModel,
product: tag.product,
tags: [tag],
})
: this.subspaceModelProductAllocationRepository.create({
subspaceModel,
product: tag.product,
tags: [tag],
});
allocations.push(allocation);
} else {
//If allocation exists, add the tag to it
existingAllocationsForProduct[0].tags.push(tag);
if (queryRunner) {
await queryRunner.manager.save(
SubspaceModelProductAllocationEntity,
existingAllocationsForProduct[0],
);
} else {
await this.subspaceModelProductAllocationRepository.save(
existingAllocationsForProduct[0],
);
}
}
}
// Save newly created allocations
if (allocations.length > 0) {
if (queryRunner) {
await queryRunner.manager.save(
SubspaceModelProductAllocationEntity,
allocations,
);
} else {
await this.subspaceModelProductAllocationRepository.save(allocations);
}
}
return allocations;
} catch (error) {
throw new HttpException(
'An unexpected error occurred while creating subspace product allocations',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async processDeleteActions(
dtos: ModifyTagModelDto[],
queryRunner: QueryRunner,
): Promise<SubspaceModelProductAllocationEntity[]> {
try {
if (!dtos || dtos.length === 0) {
throw new Error('No DTOs provided for deletion.');
}
const tagUuidsToDelete = dtos
.filter((dto) => dto.action === ModifyAction.DELETE && dto.tagUuid)
.map((dto) => dto.tagUuid);
if (tagUuidsToDelete.length === 0) return [];
const allocationsToUpdate = await queryRunner.manager.find(
SubspaceModelProductAllocationEntity,
{
where: { tags: { uuid: In(tagUuidsToDelete) } },
relations: ['tags'],
},
);
if (!allocationsToUpdate || allocationsToUpdate.length === 0) return [];
const deletedAllocations: SubspaceModelProductAllocationEntity[] = [];
const allocationUpdates: SubspaceModelProductAllocationEntity[] = [];
for (const allocation of allocationsToUpdate) {
const updatedTags = allocation.tags.filter(
(tag) => !tagUuidsToDelete.includes(tag.uuid),
);
if (updatedTags.length === allocation.tags.length) {
continue;
}
if (updatedTags.length === 0) {
deletedAllocations.push(allocation);
} else {
allocation.tags = updatedTags;
allocationUpdates.push(allocation);
}
}
if (allocationUpdates.length > 0) {
await queryRunner.manager.save(
SubspaceModelProductAllocationEntity,
allocationUpdates,
);
}
if (deletedAllocations.length > 0) {
await queryRunner.manager.remove(
SubspaceModelProductAllocationEntity,
deletedAllocations,
);
}
await queryRunner.manager
.createQueryBuilder()
.delete()
.from('subspace_model_product_tags')
.where(
'subspace_model_product_allocation_id NOT IN ' +
queryRunner.manager
.createQueryBuilder()
.select('uuid')
.from(SubspaceModelProductAllocationEntity, 'allocation')
.getQuery(),
)
.execute();
return deletedAllocations;
} catch (error) {
throw this.handleError(error, `Failed to delete tags in subspace model`);
}
}
async updateAllocations(
subspaceModels: ISingleSubspaceModel[],
projectUuid: string,
queryRunner: QueryRunner,
spaceModel: SpaceModelEntity,
spaceTagUpdateDtos?: ModifyTagModelDto[],
) {
const spaceAllocationToExclude: SpaceModelProductAllocationEntity[] = [];
for (const subspaceModel of subspaceModels) {
const tagDtos = subspaceModel.tags;
if (tagDtos.length > 0) {
const tagsToAddDto: ProcessTagDto[] = tagDtos
.filter((dto) => dto.action === ModifyAction.ADD)
.map((dto) => ({
name: dto.name,
productUuid: dto.productUuid,
uuid: dto.newTagUuid,
}));
const tagsToDeleteDto = tagDtos.filter(
(dto) => dto.action === ModifyAction.DELETE,
);
if (tagsToAddDto.length > 0) {
let processedTags = await this.tagService.processTags(
tagsToAddDto,
projectUuid,
queryRunner,
);
for (const subspaceDto of subspaceModels) {
if (
subspaceDto !== subspaceModel &&
subspaceDto.action === ModifyAction.UPDATE &&
subspaceDto.tags
) {
// Tag is deleted from one subspace and added in another subspace
const deletedTags = subspaceDto.tags.filter(
(tagDto) =>
tagDto.action === ModifyAction.DELETE &&
processedTags.some((tag) => tag.uuid === tagDto.tagUuid),
);
for (const deletedTag of deletedTags) {
const allocation = await queryRunner.manager.findOne(
SubspaceModelProductAllocationEntity,
{
where: {
subspaceModel: { uuid: subspaceDto.subspaceModel.uuid },
},
relations: ['tags', 'product', 'subspace'],
},
);
const isCommonTag = allocation.tags.some(
(tag) => tag.uuid === deletedTag.tagUuid,
);
if (allocation && isCommonTag) {
const tagEntity = allocation.tags.find(
(tag) => tag.uuid === deletedTag.tagUuid,
);
allocation.tags = allocation.tags.filter(
(tag) => tag.uuid !== deletedTag.tagUuid,
);
await queryRunner.manager.save(allocation);
const productAllocationExistInSubspace =
await queryRunner.manager.findOne(
SubspaceModelProductAllocationEntity,
{
where: {
subspaceModel: {
uuid: subspaceDto.subspaceModel.uuid,
},
product: { uuid: allocation.product.uuid },
},
relations: ['tags'],
},
);
if (productAllocationExistInSubspace) {
productAllocationExistInSubspace.tags.push(tagEntity);
await queryRunner.manager.save(
productAllocationExistInSubspace,
);
} else {
const newProductAllocation = queryRunner.manager.create(
SubspaceModelProductAllocationEntity,
{
subspaceModel: subspaceModel.subspaceModel,
product: allocation.product,
tags: [tagEntity],
},
);
await queryRunner.manager.save(newProductAllocation);
}
// Remove the tag from processedTags to prevent duplication
processedTags = processedTags.filter(
(tag) => tag.uuid !== deletedTag.tagUuid,
);
// Remove the tag from subspaceDto.tags to ensure it's not processed again while processing other dtos.
subspaceDto.tags = subspaceDto.tags.filter(
(tagDto) => tagDto.tagUuid !== deletedTag.tagUuid,
);
}
}
}
if (
subspaceDto !== subspaceModel &&
subspaceDto.action === ModifyAction.DELETE
) {
const allocation = await queryRunner.manager.findOne(
SubspaceModelProductAllocationEntity,
{
where: {
subspaceModel: { uuid: subspaceDto.subspaceModel.uuid },
},
relations: ['tags'],
},
);
const repeatedTags = allocation?.tags.filter((tag) =>
processedTags.some(
(processedTag) => processedTag.uuid === tag.uuid,
),
);
if (repeatedTags.length > 0) {
allocation.tags = allocation.tags.filter(
(tag) =>
!repeatedTags.some(
(repeatedTag) => repeatedTag.uuid === tag.uuid,
),
);
await queryRunner.manager.save(allocation);
const productAllocationExistInSubspace =
await queryRunner.manager.findOne(
SubspaceModelProductAllocationEntity,
{
where: {
subspaceModel: { uuid: subspaceDto.subspaceModel.uuid },
product: { uuid: allocation.product.uuid },
},
relations: ['tags'],
},
);
if (productAllocationExistInSubspace) {
productAllocationExistInSubspace.tags.push(...repeatedTags);
await queryRunner.manager.save(
productAllocationExistInSubspace,
);
} else {
const newProductAllocation = queryRunner.manager.create(
SubspaceModelProductAllocationEntity,
{
subspaceModel: subspaceModel.subspaceModel,
product: allocation.product,
tags: repeatedTags,
},
);
await queryRunner.manager.save(newProductAllocation);
}
const newAllocation = queryRunner.manager.create(
SubspaceModelProductAllocationEntity,
{
subspaceModel: subspaceModel.subspaceModel,
product: allocation.product,
tags: repeatedTags,
},
);
await queryRunner.manager.save(newAllocation);
}
}
}
if (spaceTagUpdateDtos) {
const deletedSpaceTags = spaceTagUpdateDtos.filter(
(tagDto) =>
tagDto.action === ModifyAction.DELETE &&
processedTags.some((tag) => tag.uuid === tagDto.tagUuid),
);
for (const deletedTag of deletedSpaceTags) {
const allocation = await queryRunner.manager.findOne(
SpaceModelProductAllocationEntity,
{
where: {
spaceModel: { uuid: spaceModel.uuid },
tags: { uuid: deletedTag.tagUuid },
},
relations: ['tags', 'subspace'],
},
);
if (
allocation &&
allocation.tags.some((tag) => tag.uuid === deletedTag.tagUuid)
) {
spaceAllocationToExclude.push(allocation);
}
}
}
// Create new product allocations
await this.createProductAllocations(
subspaceModel.subspaceModel,
processedTags,
queryRunner,
spaceAllocationToExclude,
);
}
if (tagsToDeleteDto.length > 0) {
await this.processDeleteActions(tagsToDeleteDto, queryRunner);
}
}
}
}
async clearAllAllocations(subspaceIds: string[], queryRunner: QueryRunner) {
try {
await queryRunner.manager.delete(SubspaceModelProductAllocationEntity, {
subspaceModel: In(subspaceIds),
});
await queryRunner.manager
.createQueryBuilder()
.delete()
.from('subspace_model_product_tags') // Replace with entity name if you have one
.where(
'"subspace_model_product_allocation_uuid" IN (:...subspaceIds)',
{
subspaceIds,
},
)
.execute();
} catch (error) {
throw this.handleError(error, `Failed to clear all allocations`);
}
}
private handleError(error: any, message: string): HttpException {
return new HttpException(
error instanceof HttpException ? error.message : message,
error instanceof HttpException
? error.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}

View File

@ -1,308 +1,335 @@
import {
SpaceModelEntity,
SpaceModelProductAllocationEntity,
SubspaceModelEntity,
SubspaceModelProductAllocationEntity,
SubspaceModelRepository,
} from '@app/common/modules/space-model';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { CreateSubspaceModelDto, CreateTagModelDto } from '../../dtos';
import { Not, QueryRunner } from 'typeorm';
import {
IDeletedSubsaceModelInterface,
ModifySubspaceModelPayload,
UpdatedSubspaceModelPayload,
} from 'src/space-model/interfaces';
import {
DeleteSubspaceModelDto,
ModifySubspaceModelDto,
} from 'src/space-model/dtos/subspaces-model-dtos';
import { TagModelService } from '../tag-model.service';
import { CreateSubspaceModelDto, ModifyTagModelDto } from '../../dtos';
import { In, Not, QueryFailedError, QueryRunner } from 'typeorm';
import { ModifySubspaceModelDto } from 'src/space-model/dtos/subspaces-model-dtos';
import { ModifyAction } from '@app/common/constants/modify-action.enum';
import { ProcessTagDto } from 'src/tags/dtos';
import { TagService } from 'src/tags/services';
import { SubspaceModelProductAllocationService } from './subspace-model-product-allocation.service';
import { ISingleSubspaceModel } from 'src/space-model/interfaces';
@Injectable()
export class SubSpaceModelService {
constructor(
private readonly subspaceModelRepository: SubspaceModelRepository,
private readonly tagModelService: TagModelService,
private readonly tagService: TagService,
private readonly productAllocationService: SubspaceModelProductAllocationService,
) {}
async createSubSpaceModels(
subSpaceModelDtos: CreateSubspaceModelDto[],
async createModels(
spaceModel: SpaceModelEntity,
dtos: CreateSubspaceModelDto[],
queryRunner: QueryRunner,
otherTags?: CreateTagModelDto[],
): Promise<SubspaceModelEntity[]> {
) {
try {
await this.validateInputDtos(subSpaceModelDtos, spaceModel);
this.validateNamesInDTO(dtos.map((dto) => dto.subspaceName));
const subspaces = subSpaceModelDtos.map((subspaceDto) =>
const subspaceEntities: SubspaceModelEntity[] = dtos.map((dto) =>
queryRunner.manager.create(this.subspaceModelRepository.target, {
subspaceName: subspaceDto.subspaceName,
subspaceName: dto.subspaceName,
spaceModel,
}),
);
const savedSubspaces = await queryRunner.manager.save(subspaces);
const savedSubspaces = await queryRunner.manager.save(subspaceEntities);
await Promise.all(
subSpaceModelDtos.map(async (dto, index) => {
const subspace = savedSubspaces[index];
for (const [index, dto] of dtos.entries()) {
const subspaceModel = savedSubspaces[index];
const otherDtoTags = subSpaceModelDtos
.filter((_, i) => i !== index)
.flatMap((otherDto) => otherDto.tags || []);
if (dto.tags && dto.tags.length > 0) {
subspace.tags = await this.tagModelService.createTags(
dto.tags,
queryRunner,
null,
subspace,
[...(otherTags || []), ...otherDtoTags],
);
}
}),
);
const processedTags = await this.tagService.processTags(
dto.tags,
spaceModel.project.uuid,
queryRunner,
);
await this.productAllocationService.createProductAllocations(
subspaceModel,
processedTags,
queryRunner,
);
}
return savedSubspaces;
} catch (error) {
if (error instanceof HttpException) {
throw error; // Rethrow known HttpExceptions
}
// Handle unexpected errors
throw new HttpException(
`An error occurred while creating subspace models: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
error instanceof HttpException
? error.message
: 'An unexpected error occurred while creating subspace models',
error instanceof HttpException
? error.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async handleAddAction(
dtos: ModifySubspaceModelDto[],
spaceModel: SpaceModelEntity,
queryRunner: QueryRunner,
): Promise<ISingleSubspaceModel[]> {
if (!dtos.length) return [];
const subspaceNames = dtos.map((dto) => dto.subspaceName);
await this.checkDuplicateNamesBatch(subspaceNames, spaceModel.uuid);
const subspaceEntities = dtos.map((dto) =>
queryRunner.manager.create(SubspaceModelEntity, {
subspaceName: dto.subspaceName,
spaceModel,
}),
);
const savedSubspaces = await queryRunner.manager.save(subspaceEntities);
return savedSubspaces.map((subspace, index) => ({
subspaceModel: subspace,
action: ModifyAction.ADD,
tags: dtos[index].tags ? dtos[index].tags : [],
}));
}
async modifySubspaceModels(
dtos: ModifySubspaceModelDto[],
spaceModel: SpaceModelEntity,
queryRunner: QueryRunner,
projectUuid: string,
spaceTagUpdateDtos?: ModifyTagModelDto[],
) {
if (!dtos || dtos.length === 0) {
return;
}
const addDtos = dtos.filter((dto) => dto.action === ModifyAction.ADD);
const combinedDtos = dtos.filter((dto) => dto.action !== ModifyAction.ADD);
const deleteDtos = dtos.filter((dto) => dto.action !== ModifyAction.DELETE);
const updatedModels = await this.updateSubspaceModel(
combinedDtos,
spaceModel,
queryRunner,
);
const addedModels = await this.handleAddAction(
addDtos,
spaceModel,
queryRunner,
);
const combineModels = [...addedModels, ...updatedModels];
await this.productAllocationService.updateAllocations(
combineModels,
projectUuid,
queryRunner,
spaceModel,
spaceTagUpdateDtos,
);
await this.deleteSubspaceModels(deleteDtos, queryRunner);
}
async deleteSubspaceModels(
deleteDtos: DeleteSubspaceModelDto[],
deleteDtos: ModifySubspaceModelDto[],
queryRunner: QueryRunner,
): Promise<IDeletedSubsaceModelInterface[]> {
const deleteResults: IDeletedSubsaceModelInterface[] = [];
) {
try {
if (!deleteDtos || deleteDtos.length === 0) {
throw new Error('No subspaces provided for deletion.');
}
for (const dto of deleteDtos) {
const subspaceModel = await this.findOne(dto.subspaceUuid);
const deleteResults = [];
const subspaceUuids = deleteDtos.map((dto) => dto.uuid).filter(Boolean);
if (subspaceUuids.length === 0) {
throw new Error('Invalid subspace UUIDs provided.');
}
const subspaces = await this.getSubspacesByUuids(
queryRunner,
subspaceUuids,
);
if (!subspaces.length) return deleteResults;
await queryRunner.manager.update(
this.subspaceModelRepository.target,
{ uuid: dto.subspaceUuid },
SubspaceModelEntity,
{ uuid: In(subspaceUuids) },
{ disabled: true },
);
if (subspaceModel.tags?.length) {
const modifyTagDtos = subspaceModel.tags.map((tag) => ({
uuid: tag.uuid,
action: ModifyAction.DELETE,
}));
await this.tagModelService.modifyTags(
modifyTagDtos,
queryRunner,
null,
subspaceModel,
const allocationsToRemove = subspaces.flatMap(
(subspace) => subspace.productAllocations,
);
if (allocationsToRemove.length > 0) {
const spaceAllocationsMap = new Map<
string,
SpaceModelProductAllocationEntity
>();
for (const allocation of allocationsToRemove) {
const product = allocation.product;
const tags = allocation.tags;
const spaceModel = allocation.subspaceModel.spaceModel;
const spaceAllocationKey = `${spaceModel.uuid}-${product.uuid}`;
if (!spaceAllocationsMap.has(spaceAllocationKey)) {
const spaceAllocation = await queryRunner.manager.findOne(
SpaceModelProductAllocationEntity,
{
where: {
spaceModel: { uuid: spaceModel.uuid },
product: { uuid: product.uuid },
},
relations: ['tags'],
},
);
if (spaceAllocation) {
spaceAllocationsMap.set(spaceAllocationKey, spaceAllocation);
}
}
const spaceAllocation = spaceAllocationsMap.get(spaceAllocationKey);
if (spaceAllocation) {
const existingTagUuids = new Set(
spaceAllocation.tags.map((tag) => tag.uuid),
);
const newTags = tags.filter(
(tag) => !existingTagUuids.has(tag.uuid),
);
if (newTags.length > 0) {
spaceAllocation.tags.push(...newTags);
await queryRunner.manager.save(spaceAllocation);
}
} else {
const newSpaceAllocation = queryRunner.manager.create(
SpaceModelProductAllocationEntity,
{
spaceModel: spaceModel,
product: product,
tags: tags,
},
);
await queryRunner.manager.save(newSpaceAllocation);
}
}
await queryRunner.manager.remove(
SubspaceModelProductAllocationEntity,
allocationsToRemove,
);
}
deleteResults.push({ uuid: dto.subspaceUuid });
}
await queryRunner.manager
.createQueryBuilder()
.delete()
.from('subspace_model_product_tags')
.where(
'subspace_model_product_allocation_id NOT IN ' +
queryRunner.manager
.createQueryBuilder()
.select('uuid')
.from(SubspaceModelProductAllocationEntity, 'allocation')
.getQuery(),
)
.execute();
return deleteResults;
}
deleteResults.push(...subspaceUuids.map((uuid) => ({ uuid })));
async modifySubSpaceModels(
subspaceDtos: ModifySubspaceModelDto[],
spaceModel: SpaceModelEntity,
queryRunner: QueryRunner,
): Promise<ModifySubspaceModelPayload> {
const modifiedSubspaceModels: ModifySubspaceModelPayload = {
addedSubspaceModels: [],
updatedSubspaceModels: [],
deletedSubspaceModels: [],
};
try {
for (const subspace of subspaceDtos) {
switch (subspace.action) {
case ModifyAction.ADD:
const subspaceModel = await this.handleAddAction(
subspace,
spaceModel,
queryRunner,
);
modifiedSubspaceModels.addedSubspaceModels.push(subspaceModel);
break;
case ModifyAction.UPDATE:
const updatedSubspaceModel = await this.handleUpdateAction(
subspace,
queryRunner,
);
modifiedSubspaceModels.updatedSubspaceModels.push(
updatedSubspaceModel,
);
break;
case ModifyAction.DELETE:
await this.handleDeleteAction(subspace, queryRunner);
modifiedSubspaceModels.deletedSubspaceModels.push(subspace.uuid);
break;
default:
throw new HttpException(
`Invalid action "${subspace.action}".`,
HttpStatus.BAD_REQUEST,
);
}
}
return modifiedSubspaceModels;
return deleteResults;
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
`An error occurred while modifying subspace models: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
private async handleAddAction(
subspace: ModifySubspaceModelDto,
spaceModel: SpaceModelEntity,
queryRunner: QueryRunner,
): Promise<SubspaceModelEntity> {
try {
const createTagDtos: CreateTagModelDto[] =
subspace.tags?.map((tag) => ({
tag: tag.tag,
uuid: tag.uuid,
productUuid: tag.productUuid,
})) || [];
const [createdSubspaceModel] = await this.createSubSpaceModels(
[
{
subspaceName: subspace.subspaceName,
tags: createTagDtos,
},
],
spaceModel,
queryRunner,
);
return createdSubspaceModel;
} catch (error) {
if (error instanceof HttpException) {
throw error; // Rethrow known HttpExceptions
}
throw new HttpException(
`An error occurred while adding subspace: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
private async handleUpdateAction(
modifyDto: ModifySubspaceModelDto,
queryRunner: QueryRunner,
): Promise<UpdatedSubspaceModelPayload> {
const updatePayload: UpdatedSubspaceModelPayload = {
subspaceModelUuid: modifyDto.uuid,
};
const subspace = await this.findOne(modifyDto.uuid);
await this.updateSubspaceName(
queryRunner,
subspace,
modifyDto.subspaceName,
);
updatePayload.subspaceName = modifyDto.subspaceName;
if (modifyDto.tags?.length) {
updatePayload.modifiedTags = await this.tagModelService.modifyTags(
modifyDto.tags,
queryRunner,
null,
subspace,
);
}
return updatePayload;
}
private async handleDeleteAction(
subspace: ModifySubspaceModelDto,
queryRunner: QueryRunner,
) {
const subspaceModel = await this.findOne(subspace.uuid);
await queryRunner.manager.update(
this.subspaceModelRepository.target,
{ uuid: subspace.uuid },
{ disabled: true },
);
if (subspaceModel.tags?.length) {
const modifyTagDtos: CreateTagModelDto[] = subspaceModel.tags.map(
(tag) => ({
uuid: tag.uuid,
action: ModifyAction.ADD,
tag: tag.tag,
productUuid: tag.product.uuid,
}),
);
await this.tagModelService.moveTags(
modifyTagDtos,
queryRunner,
subspaceModel.spaceModel,
null,
);
}
}
private async findOne(subspaceUuid: string): Promise<SubspaceModelEntity> {
const subspace = await this.subspaceModelRepository.findOne({
where: { uuid: subspaceUuid, disabled: false },
relations: ['tags', 'spaceModel', 'tags.product'],
});
if (!subspace) {
throw new HttpException(
`SubspaceModel with UUID ${subspaceUuid} not found.`,
HttpStatus.NOT_FOUND,
);
}
return subspace;
}
private async validateInputDtos(
subSpaceModelDtos: CreateSubspaceModelDto[],
spaceModel: SpaceModelEntity,
): Promise<void> {
try {
if (subSpaceModelDtos.length === 0) {
if (error instanceof QueryFailedError) {
throw new HttpException(
'Subspace models cannot be empty.',
`Database query failed: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
} else if (error instanceof TypeError) {
throw new HttpException(
`Invalid data encountered: ${error.message}`,
HttpStatus.BAD_REQUEST,
);
} else {
throw new HttpException(
`Unexpected error during subspace deletion: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
await this.validateName(
subSpaceModelDtos.map((dto) => dto.subspaceName),
spaceModel,
async clearModels(subspaceUuids: string[], queryRunner: QueryRunner) {
try {
const subspaces = await this.getSubspacesByUuids(
queryRunner,
subspaceUuids,
);
if (!subspaces.length) return;
await queryRunner.manager.update(
SubspaceModelEntity,
{ uuid: In(subspaceUuids) },
{ disabled: true },
);
await this.productAllocationService.clearAllAllocations(
subspaceUuids,
queryRunner,
);
} catch (error) {
if (error instanceof HttpException) {
throw error; // Rethrow known HttpExceptions to preserve their message and status
}
// Wrap unexpected errors
throw new HttpException(
`An error occurred while validating subspace models: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
error instanceof HttpException
? error.message
: 'An unexpected error occurred while clearing subspace models',
error instanceof HttpException
? error.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async updateSubspaceModel(
dtos: ModifySubspaceModelDto[],
spaceModel: SpaceModelEntity,
queryRunner: QueryRunner,
): Promise<ISingleSubspaceModel[]> {
if (!dtos.length) return [];
const updatedSubspaces: {
subspaceModel: SubspaceModelEntity;
tags: ModifyTagModelDto[];
action: ModifyAction.UPDATE;
}[] = [];
for (const dto of dtos) {
if (!dto.subspaceName) continue;
await this.checkDuplicateNames(dto.subspaceName, spaceModel.uuid);
const existingSubspace = await queryRunner.manager.findOne(
this.subspaceModelRepository.target,
{ where: { uuid: dto.uuid } },
);
if (
existingSubspace &&
existingSubspace.subspaceName !== dto.subspaceName
) {
existingSubspace.subspaceName = dto.subspaceName;
await queryRunner.manager.save(existingSubspace);
updatedSubspaces.push({
subspaceModel: existingSubspace,
tags: dto.tags ?? [],
action: ModifyAction.UPDATE,
});
}
}
return updatedSubspaces;
}
private async checkDuplicateNames(
subspaceName: string,
spaceModelUuid: string,
@ -327,10 +354,33 @@ export class SubSpaceModelService {
}
}
private async validateName(
names: string[],
spaceModel: SpaceModelEntity,
async checkDuplicateNamesBatch(
subspaceNames: string[],
spaceModelUuid: string,
): Promise<void> {
if (!subspaceNames.length) return;
const existingSubspaces = await this.subspaceModelRepository.find({
where: {
subspaceName: In(subspaceNames),
spaceModel: { uuid: spaceModelUuid },
disabled: false,
},
select: ['subspaceName'],
});
if (existingSubspaces.length > 0) {
const duplicateNames = existingSubspaces.map(
(subspace) => subspace.subspaceName,
);
throw new HttpException(
`Duplicate subspace names found: ${duplicateNames.join(', ')}`,
HttpStatus.CONFLICT,
);
}
}
private async validateNamesInDTO(names: string[]) {
const seenNames = new Set<string>();
const duplicateNames = new Set<string>();
@ -346,26 +396,31 @@ export class SubSpaceModelService {
HttpStatus.CONFLICT,
);
}
for (const name of names) {
await this.checkDuplicateNames(name, spaceModel.uuid);
}
}
private async updateSubspaceName(
queryRunner: QueryRunner,
subSpaceModel: SubspaceModelEntity,
subspaceName?: string,
): Promise<void> {
if (subspaceName) {
await this.checkDuplicateNames(
subspaceName,
subSpaceModel.spaceModel.uuid,
subSpaceModel.uuid,
);
extractTagsFromSubspaceModels(
subspaceModels: CreateSubspaceModelDto[],
): ProcessTagDto[] {
return subspaceModels.flatMap((subspace) => subspace.tags || []);
}
subSpaceModel.subspaceName = subspaceName;
await queryRunner.manager.save(subSpaceModel);
}
extractTagsFromModifiedSubspaceModels(
subspaceModels: ModifySubspaceModelDto[],
): ModifyTagModelDto[] {
return subspaceModels.flatMap((subspace) => subspace.tags || []);
}
private async getSubspacesByUuids(
queryRunner: QueryRunner,
subspaceUuids: string[],
): Promise<SubspaceModelEntity[]> {
return await queryRunner.manager.find(SubspaceModelEntity, {
where: { uuid: In(subspaceUuids) },
relations: [
'productAllocations',
'productAllocations.tags',
'spaceModel',
],
});
}
}

View File

@ -1,577 +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';
@Injectable()
export class TagModelService {
constructor(
private readonly tagModelRepository: TagModelRepository,
private readonly productService: ProductService,
) {}
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,13 +2,11 @@ import { SpaceRepositoryModule } from '@app/common/modules/space/space.repositor
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { SpaceModelController } from './controllers';
import { SpaceModelService, SubSpaceModelService } from './services';
import {
SpaceModelService,
SubSpaceModelService,
TagModelService,
} from './services';
import {
SpaceModelProductAllocationRepoitory,
SpaceModelRepository,
SubspaceModelProductAllocationRepoitory,
SubspaceModelRepository,
TagModelRepository,
} from '@app/common/modules/space-model';
@ -22,10 +20,14 @@ import { CqrsModule } from '@nestjs/cqrs';
import {
InviteSpaceRepository,
SpaceLinkRepository,
SpaceProductAllocationRepository,
SpaceRepository,
TagRepository,
} from '@app/common/modules/space';
import { SubspaceRepository } from '@app/common/modules/space/repositories/subspace.repository';
import {
SubspaceProductAllocationRepository,
SubspaceRepository,
} from '@app/common/modules/space/repositories/subspace.repository';
import {
SpaceLinkService,
SpaceService,
@ -38,6 +40,12 @@ import { CommunityService } from 'src/community/services';
import { DeviceRepository } from '@app/common/modules/device/repositories';
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
import { CommunityRepository } from '@app/common/modules/community/repositories';
import { TagService as NewTagService } from 'src/tags/services/tags.service';
import { NewTagRepository } from '@app/common/modules/tag/repositories/tag-repository';
import { SpaceModelProductAllocationService } from './services/space-model-product-allocation.service';
import { SubspaceModelProductAllocationService } from './services/subspace/subspace-model-product-allocation.service';
import { SpaceProductAllocationService } from 'src/space/services/space-product-allocation.service';
import { SubspaceProductAllocationService } from 'src/space/services/subspace/subspace-product-allocation.service';
const CommandHandlers = [
PropogateUpdateSpaceModelHandler,
@ -58,7 +66,6 @@ const CommandHandlers = [
SubspaceModelRepository,
ProductRepository,
SubspaceRepository,
TagModelService,
TagModelRepository,
SubSpaceService,
ValidationService,
@ -72,6 +79,17 @@ const CommandHandlers = [
SpaceLinkService,
SpaceLinkRepository,
InviteSpaceRepository,
NewTagService,
SpaceModelProductAllocationRepoitory,
SubspaceModelProductAllocationRepoitory,
NewTagRepository,
SpaceModelProductAllocationService,
SubspaceModelProductAllocationRepoitory,
SubspaceModelProductAllocationService,
SpaceProductAllocationService,
SubspaceProductAllocationService,
SpaceProductAllocationRepository,
SubspaceProductAllocationRepository,
],
exports: [CqrsModule, SpaceModelService],
})