Merge branch 'SP-1181-BE-Update-create-space-model-and-edit-space-model-to-use-new-tag-entity' of https://github.com/SyncrowIOT/backend into SP-1181-BE-Update-create-space-model-and-edit-space-model-to-use-new-tag-entity

This commit is contained in:
hannathkadher
2025-02-17 23:07:22 +04:00
11 changed files with 3089 additions and 2164 deletions

4850
package-lock.json generated

File diff suppressed because it is too large Load Diff

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

@ -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 { CreateTagDto, 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,5 +1,7 @@
import {
SpaceModelEntity,
SpaceModelProductAllocationEntity,
SpaceModelProductAllocationRepoitory,
SpaceModelRepository,
} from '@app/common/modules/space-model';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
@ -25,6 +27,10 @@ import {
ModifySubspaceModelPayload,
} from '../interfaces';
import { SpaceModelDto } from '@app/common/modules/space-model/dtos';
import { TagService as NewTagService } from 'src/tags/services';
import { ProductRepository } from '@app/common/modules/product/repositories';
import { NewTagEntity } from '@app/common/modules/tag';
import { ProcessTagDto } from 'src/tags/dtos';
@Injectable()
export class SpaceModelService {
@ -35,6 +41,9 @@ export class SpaceModelService {
private readonly subSpaceModelService: SubSpaceModelService,
private readonly tagModelService: TagModelService,
private commandBus: CommandBus,
private readonly tagService: NewTagService,
private readonly spaceModelProductAllocationRepository: SpaceModelProductAllocationRepoitory,
private readonly productRepository: ProductRepository,
) {}
async createSpaceModel(
@ -45,7 +54,7 @@ export class SpaceModelService {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
console.log("c");
try {
const project = await this.validateProject(params.projectUuid);
@ -62,23 +71,27 @@ export class SpaceModelService {
const savedSpaceModel = await queryRunner.manager.save(spaceModel);
this.validateUniqueTags(
tags,
this.subSpaceModelService.extractSubspaceTags(subspaceModels),
);
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(
const processedTags = await this.tagService.processTags(
tags,
queryRunner,
savedSpaceModel,
null,
params.projectUuid,
);
await this.createProductAllocations(spaceModel, processedTags);
}
await queryRunner.commitTransaction();
@ -89,12 +102,13 @@ export class SpaceModelService {
statusCode: HttpStatus.CREATED,
});
} catch (error) {
console.log("jj");
await queryRunner.rollbackTransaction();
const errorMessage =
error instanceof HttpException
? error.message
: 'An unexpected error occurred';
: `An unexpected error occurred ${error.message}`;
const statusCode =
error instanceof HttpException
? error.getStatus()
@ -397,4 +411,83 @@ export class SpaceModelService {
}
return spaceModel;
}
private async createProductAllocations(
spaceModel: SpaceModelEntity,
tags: NewTagEntity[],
): Promise<void> {
const productAllocations: SpaceModelProductAllocationEntity[] = [];
for (const tag of tags) {
const product = await this.productRepository.findOne({
where: { uuid: tag.product.uuid },
});
if (!product) {
throw new HttpException(
`Product with UUID ${tag.product.uuid} not found.`,
HttpStatus.NOT_FOUND,
);
}
const existingAllocation =
await this.spaceModelProductAllocationRepository.findOne({
where: { spaceModel, product },
});
if (!existingAllocation) {
const productAllocation =
this.spaceModelProductAllocationRepository.create({
spaceModel,
product,
tags: [tag],
});
productAllocations.push(productAllocation);
} else {
if (
!existingAllocation.tags.some(
(existingTag) => existingTag.uuid === tag.uuid,
)
) {
existingAllocation.tags.push(tag);
await this.spaceModelProductAllocationRepository.save(
existingAllocation,
);
}
}
}
if (productAllocations.length > 0) {
await this.spaceModelProductAllocationRepository.save(productAllocations);
}
}
private validateUniqueTags(
spaceTags: ProcessTagDto[],
subspaceTags: ProcessTagDto[],
) {
const allTags = [...spaceTags, ...subspaceTags];
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,
);
}
} 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,
);
}
}
}
}
}

View File

@ -1,6 +1,8 @@
import {
SpaceModelEntity,
SubspaceModelEntity,
SubspaceModelProductAllocationEntity,
SubspaceModelProductAllocationRepoitory,
SubspaceModelRepository,
} from '@app/common/modules/space-model';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
@ -17,19 +19,60 @@ import {
} from 'src/space-model/dtos/subspaces-model-dtos';
import { TagModelService } from '../tag-model.service';
import { ModifyAction } from '@app/common/constants/modify-action.enum';
import { ProcessTagDto } from 'src/tags/dtos';
import { TagService } from 'src/tags/services';
import { NewTagEntity } from '@app/common/modules/tag';
@Injectable()
export class SubSpaceModelService {
constructor(
private readonly subspaceModelRepository: SubspaceModelRepository,
private readonly tagModelService: TagModelService,
private readonly tagService: TagService,
private readonly subspaceModelProductAllocationRepository: SubspaceModelProductAllocationRepoitory,
) {}
async createModels(
spaceModel: SpaceModelEntity,
dtos: CreateSubspaceModelDto[],
queryRunner: QueryRunner,
) {
this.validateNamesInDTO(dtos.map((dto) => dto.subspaceName));
const subspaceEntities: SubspaceModelEntity[] = [];
for (const dto of dtos) {
// Create Subspace Model entity
const subspaceModel = queryRunner.manager.create(
this.subspaceModelRepository.target,
{
subspaceName: dto.subspaceName,
spaceModel,
},
);
subspaceEntities.push(subspaceModel);
}
const savedSubspaces = await queryRunner.manager.save(subspaceEntities);
for (const [index, dto] of dtos.entries()) {
const subspaceModel = savedSubspaces[index];
const processedTags = await this.tagService.processTags(
dto.tags,
spaceModel.project.uuid,
);
await this.createSubspaceProductAllocations(subspaceModel, processedTags);
}
return savedSubspaces;
}
async createSubSpaceModels(
subSpaceModelDtos: CreateSubspaceModelDto[],
spaceModel: SpaceModelEntity,
queryRunner: QueryRunner,
otherTags?: CreateTagModelDto[],
otherTags?: ProcessTagDto[],
): Promise<SubspaceModelEntity[]> {
try {
await this.validateInputDtos(subSpaceModelDtos, spaceModel);
@ -51,13 +94,13 @@ export class SubSpaceModelService {
.filter((_, i) => i !== index)
.flatMap((otherDto) => otherDto.tags || []);
if (dto.tags && dto.tags.length > 0) {
subspace.tags = await this.tagModelService.createTags(
/* subspace.tags = await this.tagModelService.createTags(
dto.tags,
queryRunner,
null,
subspace,
[...(otherTags || []), ...otherDtoTags],
);
); */
}
}),
);
@ -170,11 +213,13 @@ export class SubSpaceModelService {
queryRunner: QueryRunner,
): Promise<SubspaceModelEntity> {
try {
const createTagDtos: CreateTagModelDto[] =
const createTagDtos: ProcessTagDto[] =
subspace.tags?.map((tag) => ({
tag: tag.tag,
name: tag.tag,
uuid: tag.uuid,
productUuid: tag.productUuid,
projectUuid: null,
})) || [];
const [createdSubspaceModel] = await this.createSubSpaceModels(
@ -327,10 +372,7 @@ export class SubSpaceModelService {
}
}
private async validateName(
names: string[],
spaceModel: SpaceModelEntity,
): Promise<void> {
private async validateNamesInDTO(names: string[]) {
const seenNames = new Set<string>();
const duplicateNames = new Set<string>();
@ -346,7 +388,13 @@ export class SubSpaceModelService {
HttpStatus.CONFLICT,
);
}
}
private async validateName(
names: string[],
spaceModel: SpaceModelEntity,
): Promise<void> {
this.validateNamesInDTO(names);
for (const name of names) {
await this.checkDuplicateNames(name, spaceModel.uuid);
}
@ -368,4 +416,51 @@ export class SubSpaceModelService {
await queryRunner.manager.save(subSpaceModel);
}
}
extractSubspaceTags(
subspaceModels: CreateSubspaceModelDto[],
): ProcessTagDto[] {
return subspaceModels.flatMap((subspace) => subspace.tags || []);
}
private async createSubspaceProductAllocations(
subspaceModel: SubspaceModelEntity,
tags: NewTagEntity[],
): Promise<void> {
const allocations: SubspaceModelProductAllocationEntity[] = [];
for (const tag of tags) {
const existingAllocation =
await this.subspaceModelProductAllocationRepository.findOne({
where: { subspaceModel, product: tag.product },
});
if (!existingAllocation) {
const allocation = this.subspaceModelProductAllocationRepository.create(
{
subspaceModel,
product: tag.product,
tags: [tag],
},
);
allocations.push(allocation);
} else {
if (
!existingAllocation.tags.some(
(existingTag) => existingTag.uuid === tag.uuid,
)
) {
existingAllocation.tags.push(tag);
await this.subspaceModelProductAllocationRepository.save(
existingAllocation,
);
}
}
}
if (allocations.length > 0) {
await this.subspaceModelProductAllocationRepository.save(allocations);
}
}
}

View File

@ -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(

View File

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

View File

@ -1 +1,3 @@
export * from './tags.dto';
export * from './bulk-create-tag.dto';
export * from './process-tag-dto';

View File

@ -0,0 +1,36 @@
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 project associated with the tag',
example: '123e4567-e89b-12d3-a456-426614174000',
})
@IsUUID()
@IsOptional()
projectUuid: string;
@ApiProperty({
description: 'UUID of the tag',
example: '123e4567-e89b-12d3-a456-426614174000',
})
@IsUUID()
@IsOptional()
uuid: string;
}

View File

@ -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';
@ -13,6 +15,8 @@ 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 } from 'typeorm';
@Injectable()
export class TagService {
@ -21,6 +25,7 @@ export class TagService {
private readonly productRepository: ProductRepository,
private readonly projectRepository: ProjectRepository,
) {}
async getTagsByProjectUuid(params: GetTagsParam): Promise<BaseResponseDto> {
const { projectUuid } = params;
await this.getProjectByUuid(projectUuid);
@ -58,6 +63,72 @@ export class TagService {
});
}
async processTags(
tags: ProcessTagDto[],
projectUuid: string,
): Promise<NewTagEntity[]> {
if (!tags || tags.length === 0) return [];
const newTags: CreateTagDto[] = [];
const existingTagUuids: string[] = [];
for (const tag of tags) {
if (tag.uuid) {
existingTagUuids.push(tag.uuid);
} else {
newTags.push(tag);
}
}
const existingTags = await this.tagRepository.find({
where: { uuid: In(existingTagUuids), project: { uuid: projectUuid } },
relations: ['product'],
});
if (existingTags.length !== existingTagUuids.length) {
throw new HttpException(
`Some provided tag UUIDs do not exist in the project.`,
HttpStatus.NOT_FOUND,
);
}
const createdTags = newTags.length
? (await this.bulkCreateTags({ projectUuid, tags: newTags })).data
: [];
return [...existingTags, ...createdTags];
}
async bulkCreateTags(dto: BulkCreateTagsDto): Promise<BaseResponseDto> {
const { projectUuid, tags } = dto;
const project = await this.getProjectByUuid(projectUuid);
const productUuids = tags.map((tag) => tag.productUuid);
const productMap = await this.getProductMap(productUuids);
const existingTagNames = 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) {
await this.tagRepository.save(newTags);
}
return new SuccessResponseDto({
message: `Tags processed successfully`,
data: newTags,
});
}
private async getProductByUuid(uuid: string): Promise<ProductEntity> {
const product = await this.productRepository.findOne({ where: { uuid } });
if (!product) {
@ -88,4 +159,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;
}
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));
}
}