Merge pull request #200 from SyncrowIOT/bugfix/update-space-model

fixed bugs
This commit is contained in:
hannathkadher
2025-01-06 13:21:46 +04:00
committed by GitHub
6 changed files with 308 additions and 188 deletions

View File

@ -1,11 +1,4 @@
import { import { Entity, Column, OneToMany, ManyToOne, JoinColumn } from 'typeorm';
Entity,
Column,
OneToMany,
ManyToOne,
JoinColumn,
Unique,
} from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { SpaceModelDto } from '../dtos'; import { SpaceModelDto } from '../dtos';
import { SubspaceModelEntity } from './subspace-model'; import { SubspaceModelEntity } from './subspace-model';
@ -14,7 +7,6 @@ import { SpaceEntity } from '../../space/entities';
import { TagModel } from './tag-model.entity'; import { TagModel } from './tag-model.entity';
@Entity({ name: 'space-model' }) @Entity({ name: 'space-model' })
@Unique(['modelName', 'project'])
export class SpaceModelEntity extends AbstractEntity<SpaceModelDto> { export class SpaceModelEntity extends AbstractEntity<SpaceModelDto> {
@Column({ @Column({
type: 'uuid', type: 'uuid',

View File

@ -1,12 +1,14 @@
import { ICommand } from '@nestjs/cqrs'; import { ICommand } from '@nestjs/cqrs';
import { SpaceModelEntity } from '@app/common/modules/space-model'; import { SpaceModelEntity } from '@app/common/modules/space-model';
import { ModifyspaceModelPayload } from '../interfaces'; import { ModifyspaceModelPayload } from '../interfaces';
import { QueryRunner } from 'typeorm';
export class PropogateUpdateSpaceModelCommand implements ICommand { export class PropogateUpdateSpaceModelCommand implements ICommand {
constructor( constructor(
public readonly param: { public readonly param: {
spaceModel: SpaceModelEntity; spaceModel: SpaceModelEntity;
modifiedSpaceModels: ModifyspaceModelPayload; modifiedSpaceModels: ModifyspaceModelPayload;
queryRunner: QueryRunner;
}, },
) {} ) {}
} }

View File

@ -29,36 +29,37 @@ export class PropogateUpdateSpaceModelHandler
) {} ) {}
async execute(command: PropogateUpdateSpaceModelCommand): Promise<void> { async execute(command: PropogateUpdateSpaceModelCommand): Promise<void> {
const { spaceModel, modifiedSpaceModels } = command.param; const { spaceModel, modifiedSpaceModels, queryRunner } = command.param;
const queryRunner = this.dataSource.createQueryRunner();
try { try {
await queryRunner.connect(); const spaces = await queryRunner.manager.find(SpaceEntity, {
await queryRunner.startTransaction();
const spaces = await this.spaceRepository.find({
where: { spaceModel }, where: { spaceModel },
}); });
if (
modifiedSpaceModels.modifiedSubspaceModels.addedSubspaceModels.length > const { modifiedSubspaceModels = {}, modifiedTags = {} } =
0 modifiedSpaceModels;
) {
const {
addedSubspaceModels = [],
updatedSubspaceModels = [],
deletedSubspaceModels = [],
} = modifiedSubspaceModels;
const { added = [], updated = [], deleted = [] } = modifiedTags;
if (addedSubspaceModels.length > 0) {
await this.addSubspaceModels( await this.addSubspaceModels(
modifiedSpaceModels.modifiedSubspaceModels.addedSubspaceModels, modifiedSpaceModels.modifiedSubspaceModels.addedSubspaceModels,
spaces, spaces,
queryRunner, queryRunner,
); );
} else if ( } else if (updatedSubspaceModels.length > 0) {
modifiedSpaceModels.modifiedSubspaceModels.updatedSubspaceModels
.length > 0
) {
await this.updateSubspaceModels( await this.updateSubspaceModels(
modifiedSpaceModels.modifiedSubspaceModels.updatedSubspaceModels, modifiedSpaceModels.modifiedSubspaceModels.updatedSubspaceModels,
queryRunner, queryRunner,
); );
} }
if ( if (deletedSubspaceModels.length > 0) {
modifiedSpaceModels.modifiedSubspaceModels.deletedSubspaceModels?.length
) {
const dtos: ModifySubspaceDto[] = const dtos: ModifySubspaceDto[] =
modifiedSpaceModels.modifiedSubspaceModels.deletedSubspaceModels.map( modifiedSpaceModels.modifiedSubspaceModels.deletedSubspaceModels.map(
(model) => ({ (model) => ({
@ -69,7 +70,7 @@ export class PropogateUpdateSpaceModelHandler
await this.subSpaceService.modifySubSpace(dtos, queryRunner); await this.subSpaceService.modifySubSpace(dtos, queryRunner);
} }
if (modifiedSpaceModels.modifiedTags.added.length > 0) { if (added.length > 0) {
await this.createTags( await this.createTags(
modifiedSpaceModels.modifiedTags.added, modifiedSpaceModels.modifiedTags.added,
queryRunner, queryRunner,
@ -78,26 +79,21 @@ export class PropogateUpdateSpaceModelHandler
); );
} }
if (modifiedSpaceModels.modifiedTags.updated.length > 0) { if (updated.length > 0) {
await this.updateTags( await this.updateTags(
modifiedSpaceModels.modifiedTags.updated, modifiedSpaceModels.modifiedTags.updated,
queryRunner, queryRunner,
); );
} }
if (modifiedSpaceModels.modifiedTags.deleted.length > 0) { if (deleted.length > 0) {
await this.deleteTags( await this.deleteTags(
modifiedSpaceModels.modifiedTags.deleted, modifiedSpaceModels.modifiedTags.deleted,
queryRunner, queryRunner,
); );
} }
await queryRunner.commitTransaction();
} catch (error) { } catch (error) {
await queryRunner.rollbackTransaction(); console.error(error);
throw error;
} finally {
await queryRunner.release();
} }
} }

View File

@ -7,7 +7,7 @@ import { CreateSpaceModelDto, UpdateSpaceModelDto } from '../dtos';
import { ProjectParam } from 'src/community/dtos'; import { ProjectParam } from 'src/community/dtos';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { SubSpaceModelService } from './subspace/subspace-model.service'; import { SubSpaceModelService } from './subspace/subspace-model.service';
import { DataSource } from 'typeorm'; import { DataSource, QueryRunner } from 'typeorm';
import { import {
TypeORMCustomModel, TypeORMCustomModel,
TypeORMCustomModelFindAllQuery, TypeORMCustomModelFindAllQuery,
@ -49,7 +49,11 @@ export class SpaceModelService {
try { try {
const project = await this.validateProject(params.projectUuid); const project = await this.validateProject(params.projectUuid);
await this.validateName(modelName, params.projectUuid); await this.validateNameUsingQueryRunner(
modelName,
params.projectUuid,
queryRunner,
);
const spaceModel = this.spaceModelRepository.create({ const spaceModel = this.spaceModelRepository.create({
modelName, modelName,
@ -140,20 +144,29 @@ export class SpaceModelService {
} }
async update(dto: UpdateSpaceModelDto, param: SpaceModelParam) { async update(dto: UpdateSpaceModelDto, param: SpaceModelParam) {
const queryRunner = this.dataSource.createQueryRunner();
await this.validateProject(param.projectUuid); await this.validateProject(param.projectUuid);
const spaceModel = await this.validateSpaceModel(param.spaceModelUuid); const spaceModel = await this.validateSpaceModel(param.spaceModelUuid);
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect(); await queryRunner.connect();
await queryRunner.startTransaction();
let modifiedSubspaceModels: ModifySubspaceModelPayload = {}; let modifiedSubspaceModels: ModifySubspaceModelPayload = {};
let modifiedTagsModelPayload: ModifiedTagsModelPayload = {}; let modifiedTagsModelPayload: ModifiedTagsModelPayload = {};
try { try {
await queryRunner.startTransaction();
const { modelName } = dto; const { modelName } = dto;
if (modelName) { if (modelName) {
await this.validateName(modelName, param.projectUuid); await this.validateNameUsingQueryRunner(
modelName,
param.projectUuid,
queryRunner,
);
spaceModel.modelName = modelName; spaceModel.modelName = modelName;
await queryRunner.manager.save(spaceModel); await queryRunner.manager.save(
this.spaceModelRepository.target,
spaceModel,
);
} }
if (dto.subspaceModels) { if (dto.subspaceModels) {
@ -182,6 +195,7 @@ export class SpaceModelService {
modifiedSubspaceModels, modifiedSubspaceModels,
modifiedTags: modifiedTagsModelPayload, modifiedTags: modifiedTagsModelPayload,
}, },
queryRunner,
}), }),
); );
@ -191,6 +205,11 @@ export class SpaceModelService {
}); });
} catch (error) { } catch (error) {
await queryRunner.rollbackTransaction(); await queryRunner.rollbackTransaction();
if (error instanceof HttpException) {
throw error;
}
throw new HttpException( throw new HttpException(
error.message || 'Failed to update SpaceModel', error.message || 'Failed to update SpaceModel',
HttpStatus.INTERNAL_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR,
@ -271,6 +290,23 @@ export class SpaceModelService {
} }
} }
async validateNameUsingQueryRunner(
modelName: string,
projectUuid: string,
queryRunner: QueryRunner,
): Promise<void> {
const isModelExist = await queryRunner.manager.findOne(SpaceModelEntity, {
where: { modelName, project: { uuid: projectUuid }, disabled: false },
});
if (isModelExist) {
throw new HttpException(
`Model name ${modelName} already exists in the project with UUID ${projectUuid}.`,
HttpStatus.CONFLICT,
);
}
}
async validateSpaceModel(uuid: string): Promise<SpaceModelEntity> { async validateSpaceModel(uuid: string): Promise<SpaceModelEntity> {
const spaceModel = await this.spaceModelRepository.findOne({ const spaceModel = await this.spaceModelRepository.findOne({
where: { where: {

View File

@ -31,38 +31,50 @@ export class SubSpaceModelService {
queryRunner: QueryRunner, queryRunner: QueryRunner,
otherTags?: CreateTagModelDto[], otherTags?: CreateTagModelDto[],
): Promise<SubspaceModelEntity[]> { ): Promise<SubspaceModelEntity[]> {
this.validateInputDtos(subSpaceModelDtos, spaceModel); try {
await this.validateInputDtos(subSpaceModelDtos, spaceModel);
const subspaces = subSpaceModelDtos.map((subspaceDto) => const subspaces = subSpaceModelDtos.map((subspaceDto) =>
queryRunner.manager.create(this.subspaceModelRepository.target, { queryRunner.manager.create(this.subspaceModelRepository.target, {
subspaceName: subspaceDto.subspaceName, subspaceName: subspaceDto.subspaceName,
spaceModel, spaceModel,
}), }),
); );
const savedSubspaces = await queryRunner.manager.save(subspaces); const savedSubspaces = await queryRunner.manager.save(subspaces);
await Promise.all( await Promise.all(
subSpaceModelDtos.map(async (dto, index) => { subSpaceModelDtos.map(async (dto, index) => {
const subspace = savedSubspaces[index]; const subspace = savedSubspaces[index];
const otherDtoTags = subSpaceModelDtos const otherDtoTags = subSpaceModelDtos
.filter((_, i) => i !== index) .filter((_, i) => i !== index)
.flatMap((otherDto) => otherDto.tags || []); .flatMap((otherDto) => otherDto.tags || []);
if (dto.tags?.length) { if (dto.tags?.length) {
subspace.tags = await this.tagModelService.createTags( subspace.tags = await this.tagModelService.createTags(
dto.tags, dto.tags,
queryRunner, queryRunner,
null, null,
subspace, subspace,
[...(otherTags || []), ...otherDtoTags], [...(otherTags || []), ...otherDtoTags],
); );
} }
}), }),
); );
return savedSubspaces; 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,
);
}
} }
async deleteSubspaceModels( async deleteSubspaceModels(
@ -104,38 +116,53 @@ export class SubSpaceModelService {
spaceModel: SpaceModelEntity, spaceModel: SpaceModelEntity,
queryRunner: QueryRunner, queryRunner: QueryRunner,
): Promise<ModifySubspaceModelPayload> { ): Promise<ModifySubspaceModelPayload> {
const modifiedSubspaceModels: ModifySubspaceModelPayload = {}; const modifiedSubspaceModels: ModifySubspaceModelPayload = {
for (const subspace of subspaceDtos) { addedSubspaceModels: [],
switch (subspace.action) { updatedSubspaceModels: [],
case ModifyAction.ADD: deletedSubspaceModels: [],
const subspaceModel = await this.handleAddAction( };
subspace, try {
spaceModel, for (const subspace of subspaceDtos) {
queryRunner, switch (subspace.action) {
); case ModifyAction.ADD:
modifiedSubspaceModels.addedSubspaceModels.push(subspaceModel); const subspaceModel = await this.handleAddAction(
break; subspace,
case ModifyAction.UPDATE: spaceModel,
const updatedSubspaceModel = await this.handleUpdateAction( queryRunner,
subspace, );
queryRunner, modifiedSubspaceModels.addedSubspaceModels.push(subspaceModel);
); break;
modifiedSubspaceModels.updatedSubspaceModels.push( case ModifyAction.UPDATE:
updatedSubspaceModel, const updatedSubspaceModel = await this.handleUpdateAction(
); subspace,
break; queryRunner,
case ModifyAction.DELETE: );
await this.handleDeleteAction(subspace, queryRunner); modifiedSubspaceModels.updatedSubspaceModels.push(
modifiedSubspaceModels.deletedSubspaceModels.push(subspace.uuid); updatedSubspaceModel,
break; );
default: break;
throw new HttpException( case ModifyAction.DELETE:
`Invalid action "${subspace.action}".`, await this.handleDeleteAction(subspace, queryRunner);
HttpStatus.BAD_REQUEST, modifiedSubspaceModels.deletedSubspaceModels.push(subspace.uuid);
); break;
default:
throw new HttpException(
`Invalid action "${subspace.action}".`,
HttpStatus.BAD_REQUEST,
);
}
} }
return modifiedSubspaceModels;
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
`An error occurred while modifying subspace models: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
} }
return modifiedSubspaceModels;
} }
private async handleAddAction( private async handleAddAction(
@ -143,24 +170,35 @@ export class SubSpaceModelService {
spaceModel: SpaceModelEntity, spaceModel: SpaceModelEntity,
queryRunner: QueryRunner, queryRunner: QueryRunner,
): Promise<SubspaceModelEntity> { ): Promise<SubspaceModelEntity> {
const createTagDtos: CreateTagModelDto[] = try {
subspace.tags?.map((tag) => ({ const createTagDtos: CreateTagModelDto[] =
tag: tag.tag, subspace.tags?.map((tag) => ({
productUuid: tag.productUuid, tag: tag.tag,
})) || []; productUuid: tag.productUuid,
})) || [];
const [createdSubspaceModel] = await this.createSubSpaceModels( const [createdSubspaceModel] = await this.createSubSpaceModels(
[ [
{ {
subspaceName: subspace.subspaceName, subspaceName: subspace.subspaceName,
tags: createTagDtos, tags: createTagDtos,
}, },
], ],
spaceModel, spaceModel,
queryRunner, queryRunner,
); );
return createdSubspaceModel; 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( private async handleUpdateAction(
@ -221,7 +259,7 @@ export class SubSpaceModelService {
private async findOne(subspaceUuid: string): Promise<SubspaceModelEntity> { private async findOne(subspaceUuid: string): Promise<SubspaceModelEntity> {
const subspace = await this.subspaceModelRepository.findOne({ const subspace = await this.subspaceModelRepository.findOne({
where: { uuid: subspaceUuid }, where: { uuid: subspaceUuid },
relations: ['tags'], relations: ['tags', 'spaceModel'],
}); });
if (!subspace) { if (!subspace) {
throw new HttpException( throw new HttpException(
@ -232,59 +270,86 @@ export class SubSpaceModelService {
return subspace; return subspace;
} }
private validateInputDtos( private async validateInputDtos(
subSpaceModelDtos: CreateSubspaceModelDto[], subSpaceModelDtos: CreateSubspaceModelDto[],
spaceModel: SpaceModelEntity, spaceModel: SpaceModelEntity,
): void { ): Promise<void> {
if (subSpaceModelDtos.length === 0) { try {
if (subSpaceModelDtos.length === 0) {
throw new HttpException(
'Subspace models cannot be empty.',
HttpStatus.BAD_REQUEST,
);
}
await this.validateName(
subSpaceModelDtos.map((dto) => dto.subspaceName),
spaceModel,
);
} catch (error) {
if (error instanceof HttpException) {
throw error; // Rethrow known HttpExceptions to preserve their message and status
}
// Wrap unexpected errors
throw new HttpException( throw new HttpException(
'Subspace models cannot be empty.', `An error occurred while validating subspace models: ${error.message}`,
HttpStatus.BAD_REQUEST, HttpStatus.INTERNAL_SERVER_ERROR,
); );
} }
this.validateName(
subSpaceModelDtos.map((dto) => dto.subspaceName),
spaceModel,
);
} }
private async validateName( private async validateName(
names: string[], names: string[],
spaceModel: SpaceModelEntity, spaceModel: SpaceModelEntity,
): Promise<void> { ): Promise<void> {
const seenNames = new Set<string>(); try {
const duplicateNames = new Set<string>(); const seenNames = new Set<string>();
const duplicateNames = new Set<string>();
for (const name of names) { // Check for duplicate names within the input array
if (!seenNames.add(name)) { for (const name of names) {
duplicateNames.add(name); if (!seenNames.add(name)) {
duplicateNames.add(name);
}
} }
}
if (duplicateNames.size > 0) { if (duplicateNames.size > 0) {
throw new HttpException( throw new HttpException(
`Duplicate subspace model names found: ${[...duplicateNames].join(', ')}`, `Duplicate subspace model names found: ${[...duplicateNames].join(', ')}`,
HttpStatus.CONFLICT, HttpStatus.CONFLICT,
); );
} }
const existingNames = await this.subspaceModelRepository.find({ // Check for existing names in the database
select: ['subspaceName'], const existingNames = await this.subspaceModelRepository.find({
where: { select: ['subspaceName'],
subspaceName: In([...seenNames]), where: {
spaceModel: { subspaceName: In([...seenNames]),
uuid: spaceModel.uuid, spaceModel: {
uuid: spaceModel.uuid,
},
}, },
}, });
});
if (existingNames.length > 0) { if (existingNames.length > 0) {
const existingNamesList = existingNames const existingNamesList = existingNames
.map((e) => e.subspaceName) .map((e) => e.subspaceName)
.join(', '); .join(', ');
throw new HttpException(
`Subspace model names already exist in the space: ${existingNamesList}`,
HttpStatus.BAD_REQUEST,
);
}
} catch (error) {
if (error instanceof HttpException) {
throw error; // Rethrow known HttpExceptions
}
// Handle unexpected errors
throw new HttpException( throw new HttpException(
`Subspace model names already exist in the space: ${existingNamesList}`, `An error occurred while validating subspace model names: ${error.message}`,
HttpStatus.BAD_REQUEST, HttpStatus.INTERNAL_SERVER_ERROR,
); );
} }
} }

View File

@ -134,7 +134,11 @@ export class TagModelService {
spaceModel?: SpaceModelEntity, spaceModel?: SpaceModelEntity,
subspaceModel?: SubspaceModelEntity, subspaceModel?: SubspaceModelEntity,
): Promise<ModifiedTagsModelPayload> { ): Promise<ModifiedTagsModelPayload> {
const modifiedTagModels: ModifiedTagsModelPayload = {}; const modifiedTagModels: ModifiedTagsModelPayload = {
added: [],
updated: [],
deleted: [],
};
try { try {
for (const tag of tags) { for (const tag of tags) {
if (tag.action === ModifyAction.ADD) { if (tag.action === ModifyAction.ADD) {
@ -190,25 +194,35 @@ export class TagModelService {
productUuid: string, productUuid: string,
spaceModel: SpaceModelEntity, spaceModel: SpaceModelEntity,
): Promise<void> { ): Promise<void> {
const tagExists = await this.tagModelRepository.exists({ try {
where: [ const tagExists = await this.tagModelRepository.exists({
{ where: [
tag, {
spaceModel: { uuid: spaceModel.uuid }, tag,
product: { uuid: productUuid }, spaceModel: { uuid: spaceModel.uuid },
disabled: false, product: { uuid: productUuid },
}, disabled: false,
{ },
tag, {
subspaceModel: { spaceModel: { uuid: spaceModel.uuid } }, tag,
product: { uuid: productUuid }, subspaceModel: { spaceModel: { uuid: spaceModel.uuid } },
disabled: false, product: { uuid: productUuid },
}, disabled: false,
], },
}); ],
});
if (tagExists) { if (tagExists) {
throw new HttpException(`Tag can't be reused`, HttpStatus.CONFLICT); throw new HttpException(`Tag can't be reused`, HttpStatus.CONFLICT);
}
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
`An error occurred while checking tag reuse: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
} }
} }
@ -218,31 +232,46 @@ export class TagModelService {
spaceModel?: SpaceModelEntity, spaceModel?: SpaceModelEntity,
subspaceModel?: SubspaceModelEntity, subspaceModel?: SubspaceModelEntity,
): Promise<TagModel> { ): Promise<TagModel> {
const product = await this.productService.findOne(tagDto.productUuid); try {
const product = await this.productService.findOne(tagDto.productUuid);
if (!product) { if (!product) {
throw new HttpException(
`Product with UUID ${tagDto.productUuid} not found.`,
HttpStatus.NOT_FOUND,
);
}
if (spaceModel) {
await this.checkTagReuse(tagDto.tag, tagDto.productUuid, spaceModel);
} else if (subspaceModel && subspaceModel.spaceModel) {
await this.checkTagReuse(
tagDto.tag,
tagDto.productUuid,
subspaceModel.spaceModel,
);
} else {
throw new HttpException(
`Invalid subspaceModel or spaceModel provided.`,
HttpStatus.BAD_REQUEST,
);
}
return queryRunner.manager.create(TagModel, {
tag: tagDto.tag,
product: product.data,
spaceModel,
subspaceModel,
});
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException( throw new HttpException(
`Product with UUID ${tagDto.productUuid} not found.`, `An error occurred while preparing the tag entity: ${error.message}`,
HttpStatus.NOT_FOUND, HttpStatus.INTERNAL_SERVER_ERROR,
); );
} }
if (spaceModel) {
await this.checkTagReuse(tagDto.tag, tagDto.productUuid, spaceModel);
} else {
await this.checkTagReuse(
tagDto.tag,
tagDto.productUuid,
subspaceModel.spaceModel,
);
}
return queryRunner.manager.create(TagModel, {
tag: tagDto.tag,
product: product.data,
spaceModel,
subspaceModel,
});
} }
async getTagByUuid(uuid: string): Promise<TagModel> { async getTagByUuid(uuid: string): Promise<TagModel> {