update space service

This commit is contained in:
faris Aljohari
2025-02-27 13:36:08 +03:00
parent 7ddc39a7eb
commit 902ddbab50
6 changed files with 439 additions and 13 deletions

View File

@ -4,6 +4,7 @@ import { InviteSpaceEntity } from '../entities/invite-space.entity';
import { SpaceLinkEntity } from '../entities/space-link.entity';
import { SpaceEntity } from '../entities/space.entity';
import { TagEntity } from '../entities/tag.entity';
import { SpaceProductAllocationEntity } from '../entities/space-product-allocation.entity';
@Injectable()
export class SpaceRepository extends Repository<SpaceEntity> {
@ -32,3 +33,9 @@ export class InviteSpaceRepository extends Repository<InviteSpaceEntity> {
super(InviteSpaceEntity, dataSource.createEntityManager());
}
}
@Injectable()
export class SpaceProductAllocationRepository extends Repository<SpaceProductAllocationEntity> {
constructor(private dataSource: DataSource) {
super(SpaceProductAllocationEntity, dataSource.createEntityManager());
}
}

View File

@ -20,10 +20,14 @@ import { CqrsModule } from '@nestjs/cqrs';
import {
InviteSpaceRepository,
SpaceLinkRepository,
SpaceProductAllocationRepository,
SpaceRepository,
TagRepository,
} from '@app/common/modules/space';
import { SubspaceRepository } from '@app/common/modules/space/repositories/subspace.repository';
import {
SubspaceProductAllocationRepository,
SubspaceRepository,
} from '@app/common/modules/space/repositories/subspace.repository';
import {
SpaceLinkService,
SpaceService,
@ -40,6 +44,8 @@ import { TagService as NewTagService } from 'src/tags/services/tags.service';
import { NewTagRepository } from '@app/common/modules/tag/repositories/tag-repository';
import { SpaceModelProductAllocationService } from './services/space-model-product-allocation.service';
import { SubspaceModelProductAllocationService } from './services/subspace/subspace-model-product-allocation.service';
import { SpaceProductAllocationService } from 'src/space/services/space-product-allocation.service';
import { SubspaceProductAllocationService } from 'src/space/services/subspace/subspace-product-allocation.service';
const CommandHandlers = [
PropogateUpdateSpaceModelHandler,
@ -80,6 +86,10 @@ const CommandHandlers = [
SpaceModelProductAllocationService,
SubspaceModelProductAllocationRepoitory,
SubspaceModelProductAllocationService,
SpaceProductAllocationService,
SubspaceProductAllocationService,
SpaceProductAllocationRepository,
SubspaceProductAllocationRepository,
],
exports: [CqrsModule, SpaceModelService],
})

View File

@ -10,7 +10,7 @@ import {
ValidateNested,
} from 'class-validator';
import { AddSubspaceDto } from './subspace';
import { CreateTagDto } from './tag';
import { ProcessTagDto } from 'src/tags/dtos';
export class AddSpaceDto {
@ApiProperty({
@ -79,11 +79,11 @@ export class AddSpaceDto {
@ApiProperty({
description: 'List of tags associated with the space model',
type: [CreateTagDto],
type: [ProcessTagDto],
})
@ValidateNested({ each: true })
@Type(() => CreateTagDto)
tags?: CreateTagDto[];
@Type(() => ProcessTagDto)
tags?: ProcessTagDto[];
}
export class AddUserSpaceDto {

View File

@ -0,0 +1,334 @@
import { ModifyAction } from '@app/common/constants/modify-action.enum';
import { ProductEntity } from '@app/common/modules/product/entities';
import { SpaceProductAllocationRepository } from '@app/common/modules/space';
import { SpaceProductAllocationEntity } from '@app/common/modules/space/entities/space-product-allocation.entity';
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
import { NewTagEntity } from '@app/common/modules/tag';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { In, QueryRunner } from 'typeorm';
import { SubspaceProductAllocationEntity } from '@app/common/modules/space/entities/subspace/subspace-product-allocation.entity';
import { ModifyTagDto } from '../dtos/tag/modify-tag.dto';
import { ModifySubspaceDto } from '../dtos';
import { ProcessTagDto } from 'src/tags/dtos';
import { TagService as NewTagService } from 'src/tags/services';
@Injectable()
export class SpaceProductAllocationService {
constructor(
private readonly tagService: NewTagService,
private readonly spaceProductAllocationRepository: SpaceProductAllocationRepository,
) {}
async createSpaceProductAllocations(
space: SpaceEntity,
processedTags: NewTagEntity[],
queryRunner: QueryRunner,
modifySubspaces?: ModifySubspaceDto[],
): Promise<void> {
try {
if (!processedTags.length) return;
const productAllocations: SpaceProductAllocationEntity[] = [];
const existingAllocations = new Map<
string,
SpaceProductAllocationEntity
>();
for (const tag of processedTags) {
let isTagNeeded = true;
if (modifySubspaces) {
const relatedSubspaces = await queryRunner.manager.find(
SubspaceProductAllocationEntity,
{
where: {
product: tag.product,
subspace: { space: { uuid: space.uuid } },
tags: { uuid: tag.uuid },
},
relations: ['subspace', 'tags'],
},
);
for (const subspaceWithTag of relatedSubspaces) {
const modifyingSubspace = modifySubspaces.find(
(subspace) =>
subspace.action === ModifyAction.UPDATE &&
subspace.uuid === subspaceWithTag.subspace.uuid,
);
if (
modifyingSubspace &&
modifyingSubspace.tags &&
modifyingSubspace.tags.some(
(subspaceTag) =>
subspaceTag.action === ModifyAction.DELETE &&
subspaceTag.tagUuid === tag.uuid,
)
) {
isTagNeeded = true;
break;
}
}
}
if (isTagNeeded) {
await this.validateTagWithinSpace(queryRunner, tag, space);
let allocation = existingAllocations.get(tag.product.uuid);
if (!allocation) {
allocation = await this.getAllocationByProduct(
tag.product,
space,
queryRunner,
);
if (allocation) {
existingAllocations.set(tag.product.uuid, allocation);
}
}
if (!allocation) {
allocation = this.createNewAllocation(space, 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);
}
} catch (error) {
throw this.handleError(
error,
'Failed to create space product allocations',
);
}
}
async updateSpaceProductAllocations(
dtos: ModifyTagDto[],
projectUuid: string,
space: SpaceEntity,
queryRunner: QueryRunner,
modifySubspace?: ModifySubspaceDto[],
): Promise<void> {
try {
await Promise.all([
this.processAddActions(
dtos,
projectUuid,
space,
queryRunner,
modifySubspace,
),
this.processDeleteActions(dtos, queryRunner),
]);
} catch (error) {
throw this.handleError(error, 'Error while updating product allocations');
}
}
private async processDeleteActions(
dtos: ModifyTagDto[],
queryRunner: QueryRunner,
): Promise<SpaceProductAllocationEntity[]> {
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(
SpaceProductAllocationEntity,
{
where: { tags: { uuid: In(tagUuidsToDelete) } },
relations: ['tags'],
},
);
if (!allocationsToUpdate || allocationsToUpdate.length === 0) return [];
const deletedAllocations: SpaceProductAllocationEntity[] = [];
const allocationUpdates: SpaceProductAllocationEntity[] = [];
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(
SpaceProductAllocationEntity,
allocationUpdates,
);
}
if (deletedAllocations.length > 0) {
await queryRunner.manager.remove(
SpaceProductAllocationEntity,
deletedAllocations,
);
}
await queryRunner.manager
.createQueryBuilder()
.delete()
.from('space_product_tags')
.where(
'space_product_allocation_id NOT IN ' +
queryRunner.manager
.createQueryBuilder()
.select('uuid')
.from(SpaceProductAllocationEntity, 'allocation')
.getQuery(),
)
.execute();
return deletedAllocations;
} catch (error) {
throw this.handleError(error, `Failed to delete tags in space`);
}
}
private async processAddActions(
dtos: ModifyTagDto[],
projectUuid: string,
space: SpaceEntity,
queryRunner: QueryRunner,
modifySubspace?: ModifySubspaceDto[],
): 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) {
const processedTags = await this.tagService.processTags(
addDtos,
projectUuid,
queryRunner,
);
await this.createSpaceProductAllocations(
space,
processedTags,
queryRunner,
modifySubspace,
);
}
}
private async validateTagWithinSpace(
queryRunner: QueryRunner,
tag: NewTagEntity,
space: SpaceEntity,
) {
const existingAllocationsForProduct = await queryRunner.manager.find(
SpaceProductAllocationEntity,
{
where: { space, product: tag.product },
relations: ['tags'],
},
);
const existingTagsForProduct = existingAllocationsForProduct.flatMap(
(allocation) => allocation.tags,
);
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 (${space.uuid}).`,
HttpStatus.BAD_REQUEST,
);
}
}
private async getAllocationByProduct(
product: ProductEntity,
space: SpaceEntity,
queryRunner?: QueryRunner,
): Promise<SpaceProductAllocationEntity | null> {
return queryRunner
? queryRunner.manager.findOne(SpaceProductAllocationEntity, {
where: { space, product: product },
relations: ['tags'],
})
: this.spaceProductAllocationRepository.findOne({
where: { space, product: product },
relations: ['tags'],
});
}
private createNewAllocation(
space: SpaceEntity,
tag: NewTagEntity,
queryRunner?: QueryRunner,
): SpaceProductAllocationEntity {
return queryRunner
? queryRunner.manager.create(SpaceProductAllocationEntity, {
space,
product: tag.product,
tags: [tag],
})
: this.spaceProductAllocationRepository.create({
space,
product: tag.product,
tags: [tag],
});
}
private async saveAllocation(
allocation: SpaceProductAllocationEntity,
queryRunner?: QueryRunner,
) {
queryRunner
? await queryRunner.manager.save(SpaceProductAllocationEntity, allocation)
: await this.spaceProductAllocationRepository.save(allocation);
}
private async saveAllocations(
allocations: SpaceProductAllocationEntity[],
queryRunner?: QueryRunner,
) {
queryRunner
? await queryRunner.manager.save(
SpaceProductAllocationEntity,
allocations,
)
: await this.spaceProductAllocationRepository.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,
);
}
}

View File

@ -12,7 +12,6 @@ import {
AddSpaceDto,
AddSubspaceDto,
CommunitySpaceParam,
CreateTagDto,
GetSpaceParam,
UpdateSpaceDto,
} from '../dtos';
@ -29,11 +28,15 @@ import {
} from '@app/common/constants/orphan-constant';
import { CommandBus } from '@nestjs/cqrs';
import { TagService } from './tag';
import { TagService as NewTagService } from 'src/tags/services/tags.service';
import { SpaceModelService } from 'src/space-model/services';
import { DisableSpaceCommand } from '../commands';
import { GetSpaceDto } from '../dtos/get.space.dto';
import { removeCircularReferences } from '@app/common/helper/removeCircularReferences';
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
import { ProcessTagDto } from 'src/tags/dtos';
import { SpaceProductAllocationService } from './space-product-allocation.service';
import { SubspaceProductAllocationService } from './subspace/subspace-product-allocation.service';
@Injectable()
export class SpaceService {
constructor(
@ -44,8 +47,11 @@ export class SpaceService {
private readonly subSpaceService: SubSpaceService,
private readonly validationService: ValidationService,
private readonly tagService: TagService,
private readonly newTagService: NewTagService,
private readonly spaceModelService: SpaceModelService,
private commandBus: CommandBus,
private readonly spaceProductAllocationService: SpaceProductAllocationService,
private readonly subspaceProductAllocationService: SubspaceProductAllocationService,
) {}
async createSpace(
@ -93,6 +99,10 @@ export class SpaceService {
});
const newSpace = await queryRunner.manager.save(space);
const subspaceTags =
this.subSpaceService.extractTagsFromSubspace(subspaces);
const allTags = [...tags, ...subspaceTags];
this.validateUniqueTags(allTags);
await Promise.all([
spaceModelUuid &&
@ -106,10 +116,17 @@ export class SpaceService {
)
: Promise.resolve(),
subspaces?.length
? this.createSubspaces(subspaces, newSpace, queryRunner, tags)
? this.createSubspaces(
subspaces,
newSpace,
queryRunner,
tags,
projectUuid,
)
: Promise.resolve(),
tags?.length
? this.createTags(tags, queryRunner, newSpace)
? this.createTags(tags, projectUuid, queryRunner, space)
: Promise.resolve(),
]);
@ -121,6 +138,8 @@ export class SpaceService {
message: 'Space created successfully',
});
} catch (error) {
console.log('error', error);
await queryRunner.rollbackTransaction();
if (error instanceof HttpException) {
@ -131,7 +150,31 @@ export class SpaceService {
await queryRunner.release();
}
}
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);
}
}
}
async createFromModel(
spaceModelUuid: string,
queryRunner: QueryRunner,
@ -408,6 +451,8 @@ export class SpaceService {
modifiedSubspaces,
queryRunner,
space,
projectUuid,
updateSpaceDto.tags,
);
}
@ -423,7 +468,15 @@ export class SpaceService {
space,
);
}
if (updateSpaceDto.tags) {
await this.spaceProductAllocationService.updateSpaceProductAllocations(
updateSpaceDto.tags,
projectUuid,
space,
queryRunner,
updateSpaceDto.subspace,
);
}
await queryRunner.commitTransaction();
return new SuccessResponseDto({
@ -603,21 +656,33 @@ export class SpaceService {
subspaces: AddSubspaceDto[],
space: SpaceEntity,
queryRunner: QueryRunner,
tags: CreateTagDto[],
tags: ProcessTagDto[],
projectUuid: string,
): Promise<void> {
space.subspaces = await this.subSpaceService.createSubspacesFromDto(
subspaces,
space,
queryRunner,
tags,
projectUuid,
);
}
private async createTags(
tags: CreateTagDto[],
tags: ProcessTagDto[],
projectUuid: string,
queryRunner: QueryRunner,
space: SpaceEntity,
): Promise<void> {
space.tags = await this.tagService.createTags(tags, queryRunner, space);
const processedTags = await this.newTagService.processTags(
tags,
projectUuid,
queryRunner,
);
await this.spaceProductAllocationService.createSpaceProductAllocations(
space,
processedTags,
queryRunner,
);
}
}

View File

@ -23,6 +23,7 @@ import {
SpaceLinkRepository,
TagRepository,
InviteSpaceRepository,
SpaceProductAllocationRepository,
} from '@app/common/modules/space/repositories';
import { CommunityRepository } from '@app/common/modules/community/repositories';
import {
@ -54,7 +55,10 @@ import {
} from '@app/common/modules/space-model';
import { CommunityModule } from 'src/community/community.module';
import { ValidationService } from './services';
import { SubspaceRepository } from '@app/common/modules/space/repositories/subspace.repository';
import {
SubspaceProductAllocationRepository,
SubspaceRepository,
} from '@app/common/modules/space/repositories/subspace.repository';
import { TagService } from './services/tag';
import {
SpaceModelService,
@ -76,6 +80,8 @@ import { TagService as NewTagService } from 'src/tags/services/tags.service';
import { NewTagRepository } from '@app/common/modules/tag/repositories/tag-repository';
import { SpaceModelProductAllocationService } from 'src/space-model/services/space-model-product-allocation.service';
import { SubspaceModelProductAllocationService } from 'src/space-model/services/subspace/subspace-model-product-allocation.service';
import { SpaceProductAllocationService } from './services/space-product-allocation.service';
import { SubspaceProductAllocationService } from './services/subspace/subspace-product-allocation.service';
export const CommandHandlers = [DisableSpaceHandler];
@ -141,6 +147,10 @@ export const CommandHandlers = [DisableSpaceHandler];
NewTagRepository,
SpaceModelProductAllocationService,
SubspaceModelProductAllocationService,
SpaceProductAllocationService,
SubspaceProductAllocationService,
SpaceProductAllocationRepository,
SubspaceProductAllocationRepository,
],
exports: [SpaceService],
})