mirror of
https://github.com/SyncrowIOT/backend.git
synced 2026-10-23 09:07:54 +00:00
Merge pull request #264 from SyncrowIOT/SP-1181-BE-Update-create-space-model-and-edit-space-model-to-use-new-tag-entity
This commit is contained in:
@@ -320,7 +320,7 @@ export class ControllerRoute {
|
||||
};
|
||||
|
||||
static TAG = class {
|
||||
public static readonly ROUTE = 'tags';
|
||||
public static readonly ROUTE = '/projects/:projectUuid/tags';
|
||||
static ACTIONS = class {
|
||||
public static readonly CREATE_TAG_SUMMARY = 'Create a new tag';
|
||||
public static readonly CREATE_TAG_DESCRIPTION =
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsUUID } from 'class-validator';
|
||||
|
||||
export class GetTagsParam {
|
||||
export class ProjectParam {
|
||||
@ApiProperty({
|
||||
description: 'UUID of the Project',
|
||||
example: 'd290f1ee-6c54-4b01-90e6-d701748f0851',
|
||||
+1
-1
@@ -31,7 +31,7 @@ export class SpaceModelProductAllocationEntity extends AbstractEntity<SpaceModel
|
||||
@ManyToOne(() => ProductEntity, { nullable: false, onDelete: 'CASCADE' })
|
||||
public product: ProductEntity;
|
||||
|
||||
@ManyToMany(() => NewTagEntity)
|
||||
@ManyToMany(() => NewTagEntity, { cascade: true, onDelete: 'CASCADE' })
|
||||
@JoinTable({ name: 'space_model_product_tags' })
|
||||
public tags: NewTagEntity[];
|
||||
|
||||
|
||||
Generated
+2713
-2137
File diff suppressed because it is too large
Load Diff
@@ -183,6 +183,12 @@ export class ProjectService {
|
||||
|
||||
async findOne(uuid: string): Promise<ProjectEntity> {
|
||||
const project = await this.projectRepository.findOne({ where: { uuid } });
|
||||
if (!project) {
|
||||
throw new HttpException(
|
||||
`Invalid project with uuid ${uuid}`,
|
||||
HttpStatus.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
return project;
|
||||
}
|
||||
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './update-subspace.interface';
|
||||
export * from './modify-subspace.interface';
|
||||
export * from './single-subspace.interface';
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
import { 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 getAllocationByTagUuid(
|
||||
tagUuid: string,
|
||||
queryRunner: QueryRunner,
|
||||
): Promise<SpaceModelProductAllocationEntity | null> {
|
||||
return queryRunner.manager.findOne(SpaceModelProductAllocationEntity, {
|
||||
where: { tags: { uuid: tagUuid } },
|
||||
relations: ['tags'],
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,12 +19,10 @@ 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 { PropogateUpdateSpaceModelCommand } from '../commands';
|
||||
import {
|
||||
ModifiedTagsModelPayload,
|
||||
ModifySubspaceModelPayload,
|
||||
} from '../interfaces';
|
||||
import { SpaceModelDto } from '@app/common/modules/space-model/dtos';
|
||||
import { TagService as NewTagService } from 'src/tags/services';
|
||||
import { ProcessTagDto } from 'src/tags/dtos';
|
||||
import { SpaceModelProductAllocationService } from './space-model-product-allocation.service';
|
||||
|
||||
@Injectable()
|
||||
export class SpaceModelService {
|
||||
@@ -35,6 +33,8 @@ export class SpaceModelService {
|
||||
private readonly subSpaceModelService: SubSpaceModelService,
|
||||
private readonly tagModelService: TagModelService,
|
||||
private commandBus: CommandBus,
|
||||
private readonly tagService: NewTagService,
|
||||
private readonly spaceModelProductAllocationService: SpaceModelProductAllocationService,
|
||||
) {}
|
||||
|
||||
async createSpaceModel(
|
||||
@@ -43,11 +43,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,
|
||||
@@ -62,22 +66,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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -94,7 +103,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()
|
||||
@@ -181,8 +190,6 @@ export class SpaceModelService {
|
||||
const spaceModel = await this.validateSpaceModel(param.spaceModelUuid);
|
||||
await queryRunner.connect();
|
||||
|
||||
let modifiedSubspaceModels: ModifySubspaceModelPayload = {};
|
||||
let modifiedTagsModelPayload: ModifiedTagsModelPayload = {};
|
||||
try {
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
@@ -200,46 +207,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,14 +256,12 @@ export class SpaceModelService {
|
||||
const spaceModel = await this.validateSpaceModel(param.spaceModelUuid);
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
@@ -397,4 +382,30 @@ export class SpaceModelService {
|
||||
}
|
||||
return spaceModel;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,308 +1,336 @@
|
||||
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';
|
||||
import { SubspaceEntity } from '@app/common/modules/space/entities/subspace/subspace.entity';
|
||||
|
||||
@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 +355,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 +397,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',
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,12 +14,14 @@ import {
|
||||
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(
|
||||
|
||||
@@ -8,7 +8,9 @@ import {
|
||||
TagModelService,
|
||||
} from './services';
|
||||
import {
|
||||
SpaceModelProductAllocationRepoitory,
|
||||
SpaceModelRepository,
|
||||
SubspaceModelProductAllocationRepoitory,
|
||||
SubspaceModelRepository,
|
||||
TagModelRepository,
|
||||
} from '@app/common/modules/space-model';
|
||||
@@ -38,6 +40,8 @@ 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';
|
||||
|
||||
const CommandHandlers = [
|
||||
PropogateUpdateSpaceModelHandler,
|
||||
@@ -72,6 +76,10 @@ const CommandHandlers = [
|
||||
SpaceLinkService,
|
||||
SpaceLinkRepository,
|
||||
InviteSpaceRepository,
|
||||
NewTagService,
|
||||
SpaceModelProductAllocationRepoitory,
|
||||
SubspaceModelProductAllocationRepoitory,
|
||||
NewTagRepository,
|
||||
],
|
||||
exports: [CqrsModule, SpaceModelService],
|
||||
})
|
||||
|
||||
@@ -46,7 +46,9 @@ import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log
|
||||
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
|
||||
import { ProjectRepository } from '@app/common/modules/project/repositiories';
|
||||
import {
|
||||
SpaceModelProductAllocationRepoitory,
|
||||
SpaceModelRepository,
|
||||
SubspaceModelProductAllocationRepoitory,
|
||||
SubspaceModelRepository,
|
||||
TagModelRepository,
|
||||
} from '@app/common/modules/space-model';
|
||||
@@ -71,6 +73,8 @@ import {
|
||||
InviteUserSpaceRepository,
|
||||
} from '@app/common/modules/Invite-user/repositiories';
|
||||
import { AutomationRepository } from '@app/common/modules/automation/repositories';
|
||||
import { TagService as NewTagService } from 'src/tags/services/tags.service';
|
||||
import { NewTagRepository } from '@app/common/modules/tag/repositories/tag-repository';
|
||||
|
||||
export const CommandHandlers = [DisableSpaceHandler];
|
||||
|
||||
@@ -130,6 +134,11 @@ export const CommandHandlers = [DisableSpaceHandler];
|
||||
InviteUserRepository,
|
||||
InviteUserSpaceRepository,
|
||||
AutomationRepository,
|
||||
TagService,
|
||||
NewTagService,
|
||||
SpaceModelProductAllocationRepoitory,
|
||||
SubspaceModelProductAllocationRepoitory,
|
||||
NewTagRepository,
|
||||
],
|
||||
exports: [SpaceService],
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@ import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
|
||||
import { ControllerRoute } from '@app/common/constants/controller-route';
|
||||
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
|
||||
import { CreateTagDto } from '../dtos/tags.dto';
|
||||
import { GetTagsParam } from '../dtos/get-tags.param';
|
||||
import { ProjectParam } from '@app/common/dto/project-param.dto';
|
||||
|
||||
@ApiTags('Tag Module')
|
||||
@Controller({
|
||||
@@ -23,19 +23,22 @@ export class TagController {
|
||||
summary: ControllerRoute.TAG.ACTIONS.CREATE_TAG_SUMMARY,
|
||||
description: ControllerRoute.TAG.ACTIONS.CREATE_TAG_DESCRIPTION,
|
||||
})
|
||||
async createTag(@Body() dto: CreateTagDto): Promise<BaseResponseDto> {
|
||||
return this.tagService.createTag(dto);
|
||||
async createTag(
|
||||
@Body() dto: CreateTagDto,
|
||||
@Param() param: ProjectParam,
|
||||
): Promise<BaseResponseDto> {
|
||||
return this.tagService.createTag(dto, param);
|
||||
}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get('project/:projectUuid')
|
||||
@Get('tagUuid')
|
||||
@ApiOperation({
|
||||
summary: ControllerRoute.TAG.ACTIONS.GET_TAGS_BY_PROJECT_SUMMARY,
|
||||
description: ControllerRoute.TAG.ACTIONS.GET_TAGS_BY_PROJECT_DESCRIPTION,
|
||||
})
|
||||
async getTagsByProject(
|
||||
@Param() params: GetTagsParam,
|
||||
@Param() params: ProjectParam,
|
||||
): Promise<BaseResponseDto> {
|
||||
return this.tagService.getTagsByProjectUuid(params);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator';
|
||||
import { CreateTagDto } from './tags.dto';
|
||||
|
||||
export class BulkCreateTagsDto {
|
||||
@ApiProperty({
|
||||
description: 'Project UUID for which the tags are being created',
|
||||
example: '760e8400-e29b-41d4-a716-446655440001',
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
projectUuid: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'List of tags to be created',
|
||||
type: [CreateTagDto],
|
||||
})
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => CreateTagDto)
|
||||
tags: CreateTagDto[];
|
||||
}
|
||||
@@ -1 +1,3 @@
|
||||
export * from './tags.dto';
|
||||
export * from './bulk-create-tag.dto';
|
||||
export * from './process-tag-dto';
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsOptional, IsString, IsUUID } from 'class-validator';
|
||||
|
||||
export class ProcessTagDto {
|
||||
@ApiProperty({
|
||||
description: 'The name of the tag',
|
||||
example: 'New Tag',
|
||||
})
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
name: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'UUID of the product associated with the tag',
|
||||
example: '550e8400-e29b-41d4-a716-446655440000',
|
||||
})
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
productUuid: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'UUID of the tag',
|
||||
example: '123e4567-e89b-12d3-a456-426614174000',
|
||||
})
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
uuid: string;
|
||||
}
|
||||
@@ -17,12 +17,4 @@ export class CreateTagDto {
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
productUuid: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'UUID of the project associated with the tag',
|
||||
example: '123e4567-e89b-12d3-a456-426614174000',
|
||||
})
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
projectUuid: string;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
Injectable,
|
||||
ConflictException,
|
||||
NotFoundException,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
import { CreateTagDto } from '../dtos/tags.dto';
|
||||
import { ProductEntity } from '@app/common/modules/product/entities';
|
||||
@@ -12,7 +14,9 @@ import { ProjectEntity } from '@app/common/modules/project/entities';
|
||||
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
|
||||
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
|
||||
import { NewTagEntity } from '@app/common/modules/tag';
|
||||
import { GetTagsParam } from '../dtos/get-tags.param';
|
||||
import { BulkCreateTagsDto, ProcessTagDto } from '../dtos';
|
||||
import { In, QueryRunner } from 'typeorm';
|
||||
import { ProjectParam } from '@app/common/dto/project-param.dto';
|
||||
|
||||
@Injectable()
|
||||
export class TagService {
|
||||
@@ -21,7 +25,8 @@ export class TagService {
|
||||
private readonly productRepository: ProductRepository,
|
||||
private readonly projectRepository: ProjectRepository,
|
||||
) {}
|
||||
async getTagsByProjectUuid(params: GetTagsParam): Promise<BaseResponseDto> {
|
||||
|
||||
async getTagsByProjectUuid(params: ProjectParam): Promise<BaseResponseDto> {
|
||||
const { projectUuid } = params;
|
||||
await this.getProjectByUuid(projectUuid);
|
||||
|
||||
@@ -36,13 +41,16 @@ export class TagService {
|
||||
});
|
||||
}
|
||||
|
||||
async createTag(dto: CreateTagDto): Promise<BaseResponseDto> {
|
||||
const { name, productUuid, projectUuid } = dto;
|
||||
async createTag(
|
||||
dto: CreateTagDto,
|
||||
param: ProjectParam,
|
||||
): Promise<BaseResponseDto> {
|
||||
const { name, productUuid } = dto;
|
||||
|
||||
const product = await this.getProductByUuid(productUuid);
|
||||
const project = await this.getProjectByUuid(projectUuid);
|
||||
const project = await this.getProjectByUuid(param.projectUuid);
|
||||
|
||||
await this.validateTagUniqueness(name, projectUuid);
|
||||
await this.validateTagUniqueness(name, param.projectUuid);
|
||||
|
||||
const tag = this.tagRepository.create({
|
||||
name,
|
||||
@@ -58,6 +66,122 @@ export class TagService {
|
||||
});
|
||||
}
|
||||
|
||||
async processTags(
|
||||
tags: ProcessTagDto[],
|
||||
projectUuid: string,
|
||||
queryRunner?: QueryRunner,
|
||||
): Promise<NewTagEntity[]> {
|
||||
try {
|
||||
if (!tags || tags.length === 0) return [];
|
||||
|
||||
const newTags: CreateTagDto[] = [];
|
||||
const existingTagUuids: string[] = [];
|
||||
|
||||
for (const tag of tags) {
|
||||
tag.uuid ? existingTagUuids.push(tag.uuid) : newTags.push(tag);
|
||||
}
|
||||
|
||||
const existingTags = existingTagUuids.length
|
||||
? await (queryRunner
|
||||
? queryRunner.manager.find(NewTagEntity, {
|
||||
where: {
|
||||
uuid: In(existingTagUuids),
|
||||
project: { uuid: projectUuid },
|
||||
},
|
||||
relations: ['product'],
|
||||
})
|
||||
: this.tagRepository.find({
|
||||
where: {
|
||||
uuid: In(existingTagUuids),
|
||||
project: { uuid: projectUuid },
|
||||
},
|
||||
relations: ['product'],
|
||||
}))
|
||||
: [];
|
||||
|
||||
if (existingTags.length !== existingTagUuids.length) {
|
||||
const foundUuids = new Set(existingTags.map((tag) => tag.uuid));
|
||||
const missingUuids = existingTagUuids.filter(
|
||||
(uuid) => !foundUuids.has(uuid),
|
||||
);
|
||||
|
||||
throw new HttpException(
|
||||
`Some provided tag UUIDs do not exist in the project: ${missingUuids.join(', ')}`,
|
||||
HttpStatus.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const createdTags =
|
||||
newTags.length > 0
|
||||
? await this.bulkCreateTags(
|
||||
{ projectUuid, tags: newTags },
|
||||
queryRunner,
|
||||
)
|
||||
: [];
|
||||
|
||||
return [...existingTags, ...createdTags];
|
||||
} catch (error) {
|
||||
throw new HttpException(
|
||||
error instanceof HttpException
|
||||
? error.message
|
||||
: 'An unexpected error occurred while processing tags',
|
||||
error instanceof HttpException
|
||||
? error.getStatus()
|
||||
: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async bulkCreateTags(
|
||||
dto: BulkCreateTagsDto,
|
||||
queryRunner?: QueryRunner,
|
||||
): Promise<NewTagEntity[]> {
|
||||
try {
|
||||
const { projectUuid, tags } = dto;
|
||||
|
||||
const project = await this.getProjectByUuid(projectUuid);
|
||||
|
||||
const productUuids = Array.from(
|
||||
new Set(tags.map((tag) => tag.productUuid)),
|
||||
);
|
||||
|
||||
const productMap = await this.getProductMap(productUuids);
|
||||
|
||||
const existingTagNames = new Set(
|
||||
await this.getExistingTagNames(projectUuid),
|
||||
);
|
||||
|
||||
const newTags: NewTagEntity[] = tags
|
||||
.filter((tag) => !existingTagNames.has(tag.name))
|
||||
.map((tag) =>
|
||||
this.tagRepository.create({
|
||||
name: tag.name,
|
||||
product: productMap.get(tag.productUuid),
|
||||
project,
|
||||
}),
|
||||
);
|
||||
|
||||
if (newTags.length > 0) {
|
||||
if (queryRunner) {
|
||||
await queryRunner.manager.save(NewTagEntity, newTags);
|
||||
} else {
|
||||
await this.tagRepository.save(newTags);
|
||||
}
|
||||
}
|
||||
|
||||
return newTags;
|
||||
} catch (error) {
|
||||
throw new HttpException(
|
||||
error instanceof HttpException
|
||||
? error.message
|
||||
: 'An unexpected error occurred while creating tags',
|
||||
error instanceof HttpException
|
||||
? error.getStatus()
|
||||
: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async getProductByUuid(uuid: string): Promise<ProductEntity> {
|
||||
const product = await this.productRepository.findOne({ where: { uuid } });
|
||||
if (!product) {
|
||||
@@ -88,4 +212,31 @@ export class TagService {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async getProductMap(
|
||||
uuids: string[],
|
||||
): Promise<Map<string, ProductEntity>> {
|
||||
const products = await this.productRepository.find({
|
||||
where: { uuid: In(uuids) },
|
||||
});
|
||||
|
||||
if (products.length !== uuids.length) {
|
||||
const foundUuids = new Set(products.map((p) => p.uuid));
|
||||
const missingUuids = uuids.filter((id) => !foundUuids.has(id));
|
||||
throw new NotFoundException(
|
||||
`Products not found for UUIDs: ${missingUuids.join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
return new Map(products.map((product) => [product.uuid, product]));
|
||||
}
|
||||
|
||||
private async getExistingTagNames(projectUuid: string): Promise<Set<string>> {
|
||||
const tags = await this.tagRepository.find({
|
||||
where: { project: { uuid: projectUuid } },
|
||||
select: ['name'],
|
||||
});
|
||||
|
||||
return new Set(tags.map((tag) => tag.name));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,7 +258,6 @@ export class UserSpaceService {
|
||||
.where('space.uuid = :spaceUuid', { spaceUuid })
|
||||
.execute();
|
||||
} catch (error) {
|
||||
console.error(`Error deleting user-space associations: ${error.message}`);
|
||||
throw new HttpException(
|
||||
`Failed to delete user-space associations: ${error.message}`,
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
|
||||
Reference in New Issue
Block a user