Files
backend/src/space-model/services/subspace/subspace-model.service.ts
ZaydSkaff 689a38ee0c Revamp/space management (#409)
* task: add getCommunitiesV2

* task: update getOneSpace API to match revamp structure

* refactor: implement modifications to pace management APIs

* refactor: remove space link
2025-06-18 10:34:29 +03:00

521 lines
16 KiB
TypeScript

import { ModifyAction } from '@app/common/constants/modify-action.enum';
import {
SpaceModelEntity,
SpaceModelProductAllocationEntity,
SubspaceModelEntity,
SubspaceModelProductAllocationEntity,
SubspaceModelRepository,
} from '@app/common/modules/space-model';
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { ModifySubspaceModelDto } from 'src/space-model/dtos/subspaces-model-dtos';
import {
IRelocatedAllocation,
ISingleSubspaceModel,
ISubspaceModelUpdates,
} from 'src/space-model/interfaces';
import { ModifyTagDto } from 'src/space/dtos/tag/modify-tag.dto';
import { SubSpaceService } from 'src/space/services/subspace';
import { ProcessTagDto } from 'src/tags/dtos';
import { TagService } from 'src/tags/services';
import { In, Not, QueryFailedError, QueryRunner } from 'typeorm';
import { CreateSubspaceModelDto } from '../../dtos';
import { SubspaceModelProductAllocationService } from './subspace-model-product-allocation.service';
@Injectable()
export class SubSpaceModelService {
constructor(
private readonly subspaceModelRepository: SubspaceModelRepository,
private readonly tagService: TagService,
private readonly productAllocationService: SubspaceModelProductAllocationService,
private readonly subspaceService: SubSpaceService,
) {}
async createModels(
spaceModel: SpaceModelEntity,
dtos: CreateSubspaceModelDto[],
queryRunner: QueryRunner,
) {
try {
this.validateNamesInDTO(dtos.map((dto) => dto.subspaceName));
const subspaceEntities: SubspaceModelEntity[] = dtos.map((dto) =>
queryRunner.manager.create(this.subspaceModelRepository.target, {
subspaceName: dto.subspaceName,
spaceModel,
}),
);
const savedSubspaces = await queryRunner.manager.save(subspaceEntities);
for (const [index, dto] of dtos.entries()) {
const subspaceModel = savedSubspaces[index];
const processedTags = await this.tagService.upsertTags(
dto.tags.map((tag) => ({
tagName: tag.name,
productUuid: tag.productUuid,
tagUuid: tag.uuid,
})),
spaceModel.project.uuid,
queryRunner,
);
await this.productAllocationService.createProductAllocations(
subspaceModel,
spaceModel,
processedTags,
queryRunner,
);
}
return savedSubspaces;
} catch (error) {
throw new HttpException(
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,
spaces?: SpaceEntity[],
): 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);
if (spaces) {
await this.subspaceService.createSubSpaceFromModel(
savedSubspaces,
spaces,
queryRunner,
);
}
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?: ModifyTagDto[],
spaces?: SpaceEntity[],
): Promise<ISubspaceModelUpdates> {
try {
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 updatedSubspaces = await this.updateSubspaceModel(
combinedDtos,
spaceModel,
queryRunner,
);
const createdSubspaces = await this.handleAddAction(
addDtos,
spaceModel,
queryRunner,
spaces,
);
const combineModels = [...createdSubspaces, ...updatedSubspaces];
const updatedAllocations =
await this.productAllocationService.updateAllocations(
combineModels,
projectUuid,
queryRunner,
spaceModel,
// spaceTagUpdateDtos,
);
const deletedSubspaces = await this.deleteSubspaceModels(
deleteDtos,
queryRunner,
spaceModel,
spaceTagUpdateDtos,
);
return {
subspaceModels: [
createdSubspaces ?? [],
updatedSubspaces ?? [],
deletedSubspaces ?? [],
]
.filter((arr) => arr.length > 0)
.flat(),
updatedAllocations: updatedAllocations,
};
} catch (error) {
console.error('Error in modifySubspaceModels:', error);
throw new HttpException(
{
status: HttpStatus.INTERNAL_SERVER_ERROR,
message: 'An error occurred while modifying subspace models',
error: error.message,
},
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async deleteSubspaceModels(
deleteDtos: ModifySubspaceModelDto[],
queryRunner: QueryRunner,
spaceModel: SpaceModelEntity,
spaceTagUpdateDtos?: ModifyTagDto[],
): Promise<ISingleSubspaceModel[]> {
try {
if (!deleteDtos || deleteDtos.length === 0) {
return [];
}
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(
SubspaceModelEntity,
{ uuid: In(subspaceUuids) },
{ disabled: true },
);
const allocationsToRemove = subspaces.flatMap((subspace) =>
(subspace.productAllocations || []).map((allocation) => ({
...allocation,
subspaceModel: subspace,
})),
);
const relocatedAllocationsMap = new Map<string, IRelocatedAllocation[]>();
if (allocationsToRemove.length > 0) {
const spaceAllocationsMap = new Map<
string,
SpaceModelProductAllocationEntity
>();
for (const allocation of allocationsToRemove) {
// const product = allocation.product;
// const tags = allocation.tags;
// const subspaceUuid = allocation.subspaceModel.uuid;
// 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 movedToAlreadyExistingSpaceAllocations: IRelocatedAllocation[] =
// [];
// 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) {
// movedToAlreadyExistingSpaceAllocations.push({
// tags: newTags,
// allocation: spaceAllocation,
// });
// spaceAllocation.tags.push(...newTags);
// await queryRunner.manager.save(spaceAllocation);
// }
// } else {
// let tagsToAdd = [...tags];
// if (spaceTagUpdateDtos && spaceTagUpdateDtos.length > 0) {
// const spaceTagDtosToAdd = spaceTagUpdateDtos.filter(
// (dto) => dto.action === ModifyAction.ADD,
// );
// tagsToAdd = tagsToAdd.filter(
// (tag) =>
// !spaceTagDtosToAdd.some(
// (addDto) =>
// (addDto.name && addDto.name === tag.name) ||
// (addDto.newTagUuid && addDto.newTagUuid === tag.uuid),
// ),
// );
// }
// if (tagsToAdd.length > 0) {
// const newSpaceAllocation = queryRunner.manager.create(
// SpaceModelProductAllocationEntity,
// {
// spaceModel: spaceModel,
// product: product,
// tags: tags,
// },
// );
// movedToAlreadyExistingSpaceAllocations.push({
// allocation: newSpaceAllocation,
// tags: tags,
// });
// await queryRunner.manager.save(newSpaceAllocation);
// }
// }
// if (movedToAlreadyExistingSpaceAllocations.length > 0) {
// if (!relocatedAllocationsMap.has(subspaceUuid)) {
// relocatedAllocationsMap.set(subspaceUuid, []);
// }
// relocatedAllocationsMap
// .get(subspaceUuid)
// .push(...movedToAlreadyExistingSpaceAllocations);
// }
}
await queryRunner.manager.remove(
SubspaceModelProductAllocationEntity,
allocationsToRemove,
);
}
await queryRunner.manager
.createQueryBuilder()
.delete()
.from('subspace_model_product_tags')
.where(
'subspace_model_product_allocation_uuid NOT IN (' +
queryRunner.manager
.createQueryBuilder()
.select('allocation.uuid')
.from(SubspaceModelProductAllocationEntity, 'allocation')
.getQuery() +
')',
)
.execute();
deleteResults.push(...subspaceUuids.map((uuid) => ({ uuid })));
return subspaces.map((subspace) => ({
subspaceModel: subspace,
action: ModifyAction.DELETE,
relocatedAllocations: relocatedAllocationsMap.get(subspace.uuid) || [],
}));
} catch (error) {
if (error instanceof QueryFailedError) {
throw new HttpException(
`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,
);
}
}
}
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) {
throw new HttpException(
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: ModifyTagDto[];
action: ModifyAction.UPDATE;
}[] = [];
for (const dto of dtos) {
if (!dto.subspaceName) continue;
const existingSubspace = await queryRunner.manager.findOne(
this.subspaceModelRepository.target,
{ where: { uuid: dto.uuid } },
);
if (existingSubspace) {
if (existingSubspace.subspaceName !== dto.subspaceName) {
await this.checkDuplicateNames(dto.subspaceName, spaceModel.uuid);
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,
excludeUuid?: string,
): Promise<void> {
const duplicateSubspace = await this.subspaceModelRepository.findOne({
where: {
subspaceName,
spaceModel: {
uuid: spaceModelUuid,
},
disabled: false,
...(excludeUuid && { uuid: Not(excludeUuid) }),
},
});
if (duplicateSubspace) {
throw new HttpException(
`A subspace with the name '${subspaceName}' already exists within the same space model. ${spaceModelUuid}`,
HttpStatus.CONFLICT,
);
}
}
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>();
for (const name of names) {
if (!seenNames.add(name)) {
duplicateNames.add(name);
}
}
if (duplicateNames.size > 0) {
throw new HttpException(
`Duplicate subspace model names found: ${[...duplicateNames].join(', ')}`,
HttpStatus.CONFLICT,
);
}
}
extractTagsFromSubspaceModels(
subspaceModels: CreateSubspaceModelDto[],
): ProcessTagDto[] {
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.product',
'productAllocations.tags',
'spaceModel',
],
});
}
}