diff --git a/.eslintrc.js b/.eslintrc.js index 34440e3..60ac5b0 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -21,6 +21,7 @@ module.exports = { '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-explicit-any': 'off', + "@typescript-eslint/no-unused-vars": 'warn', }, settings: { 'import/resolver': { diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index 2196901..373041c 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -25,6 +25,7 @@ import { InviteUserEntity, InviteUserSpaceEntity, } from '../modules/Invite-user/entities'; +import { SpaceDailyOccupancyDurationEntity } from '../modules/occupancy/entities'; import { PowerClampDailyEntity, PowerClampHourlyEntity, @@ -46,7 +47,6 @@ import { SubspaceModelProductAllocationEntity, } from '../modules/space-model/entities'; import { InviteSpaceEntity } from '../modules/space/entities/invite-space.entity'; -import { SpaceLinkEntity } from '../modules/space/entities/space-link.entity'; import { SpaceProductAllocationEntity } from '../modules/space/entities/space-product-allocation.entity'; import { SpaceEntity } from '../modules/space/entities/space.entity'; import { SubspaceProductAllocationEntity } from '../modules/space/entities/subspace/subspace-product-allocation.entity'; @@ -58,7 +58,6 @@ import { UserSpaceEntity, } from '../modules/user/entities'; import { VisitorPasswordEntity } from '../modules/visitor-password/entities'; -import { SpaceDailyOccupancyDurationEntity } from '../modules/occupancy/entities'; @Module({ imports: [ TypeOrmModule.forRootAsync({ @@ -87,7 +86,6 @@ import { SpaceDailyOccupancyDurationEntity } from '../modules/occupancy/entities PermissionTypeEntity, CommunityEntity, SpaceEntity, - SpaceLinkEntity, SubspaceEntity, UserSpaceEntity, DeviceUserPermissionEntity, diff --git a/libs/common/src/dto/pagination.request.dto.ts b/libs/common/src/dto/pagination.request.dto.ts index e20501a..467fb61 100644 --- a/libs/common/src/dto/pagination.request.dto.ts +++ b/libs/common/src/dto/pagination.request.dto.ts @@ -1,10 +1,9 @@ -import { IsBoolean, IsDate, IsOptional } from 'class-validator'; -import { IsPageRequestParam } from '../validators/is-page-request-param.validator'; import { ApiProperty } from '@nestjs/swagger'; -import { IsSizeRequestParam } from '../validators/is-size-request-param.validator'; import { Transform } from 'class-transformer'; -import { parseToDate } from '../util/parseToDate'; +import { IsBoolean, IsOptional } from 'class-validator'; import { BooleanValues } from '../constants/boolean-values.enum'; +import { IsPageRequestParam } from '../validators/is-page-request-param.validator'; +import { IsSizeRequestParam } from '../validators/is-size-request-param.validator'; export class PaginationRequestGetListDto { @ApiProperty({ @@ -19,6 +18,7 @@ export class PaginationRequestGetListDto { return value.obj.includeSpaces === BooleanValues.TRUE; }) public includeSpaces?: boolean = false; + @IsOptional() @IsPageRequestParam({ message: 'Page must be bigger than 0', @@ -40,40 +40,4 @@ export class PaginationRequestGetListDto { description: 'Size request', }) size?: number; - - @IsOptional() - @ApiProperty({ - name: 'name', - required: false, - description: 'Name to be filtered', - }) - name?: string; - - @ApiProperty({ - name: 'from', - required: false, - type: Number, - description: `Start time in UNIX timestamp format to filter`, - example: 1674172800000, - }) - @IsOptional() - @Transform(({ value }) => parseToDate(value)) - @IsDate({ - message: `From must be in UNIX timestamp format in order to parse to Date instance`, - }) - from?: Date; - - @ApiProperty({ - name: 'to', - required: false, - type: Number, - description: `End time in UNIX timestamp format to filter`, - example: 1674259200000, - }) - @IsOptional() - @Transform(({ value }) => parseToDate(value)) - @IsDate({ - message: `To must be in UNIX timestamp format in order to parse to Date instance`, - }) - to?: Date; } diff --git a/libs/common/src/modules/space/entities/space-link.entity.ts b/libs/common/src/modules/space/entities/space-link.entity.ts index da11eb7..7223591 100644 --- a/libs/common/src/modules/space/entities/space-link.entity.ts +++ b/libs/common/src/modules/space/entities/space-link.entity.ts @@ -1,32 +1,3 @@ -import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; -import { SpaceEntity } from './space.entity'; -import { Direction } from '@app/common/constants/direction.enum'; -@Entity({ name: 'space-link' }) -export class SpaceLinkEntity extends AbstractEntity { - @ManyToOne(() => SpaceEntity, { nullable: false, onDelete: 'CASCADE' }) - @JoinColumn({ name: 'start_space_id' }) - public startSpace: SpaceEntity; - - @ManyToOne(() => SpaceEntity, { nullable: false, onDelete: 'CASCADE' }) - @JoinColumn({ name: 'end_space_id' }) - public endSpace: SpaceEntity; - - @Column({ - nullable: false, - default: false, - }) - public disabled: boolean; - - @Column({ - nullable: false, - enum: Object.values(Direction), - }) - direction: string; - - constructor(partial: Partial) { - super(); - Object.assign(this, partial); - } -} +export class SpaceLinkEntity extends AbstractEntity {} diff --git a/libs/common/src/modules/space/entities/space.entity.ts b/libs/common/src/modules/space/entities/space.entity.ts index 56b5d4f..5341613 100644 --- a/libs/common/src/modules/space/entities/space.entity.ts +++ b/libs/common/src/modules/space/entities/space.entity.ts @@ -1,18 +1,17 @@ import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm'; -import { SpaceDto } from '../dtos'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; -import { UserSpaceEntity } from '../../user/entities'; -import { DeviceEntity } from '../../device/entities'; +import { AqiSpaceDailyPollutantStatsEntity } from '../../aqi/entities'; import { CommunityEntity } from '../../community/entities'; -import { SpaceLinkEntity } from './space-link.entity'; +import { DeviceEntity } from '../../device/entities'; +import { InviteUserSpaceEntity } from '../../Invite-user/entities'; +import { SpaceDailyOccupancyDurationEntity } from '../../occupancy/entities'; +import { PresenceSensorDailySpaceEntity } from '../../presence-sensor/entities'; import { SceneEntity } from '../../scene/entities'; import { SpaceModelEntity } from '../../space-model'; -import { InviteUserSpaceEntity } from '../../Invite-user/entities'; +import { UserSpaceEntity } from '../../user/entities'; +import { SpaceDto } from '../dtos'; import { SpaceProductAllocationEntity } from './space-product-allocation.entity'; import { SubspaceEntity } from './subspace/subspace.entity'; -import { PresenceSensorDailySpaceEntity } from '../../presence-sensor/entities'; -import { AqiSpaceDailyPollutantStatsEntity } from '../../aqi/entities'; -import { SpaceDailyOccupancyDurationEntity } from '../../occupancy/entities'; @Entity({ name: 'space' }) export class SpaceEntity extends AbstractEntity { @@ -75,16 +74,6 @@ export class SpaceEntity extends AbstractEntity { ) devices: DeviceEntity[]; - @OneToMany(() => SpaceLinkEntity, (connection) => connection.startSpace, { - nullable: true, - }) - public outgoingConnections: SpaceLinkEntity[]; - - @OneToMany(() => SpaceLinkEntity, (connection) => connection.endSpace, { - nullable: true, - }) - public incomingConnections: SpaceLinkEntity[]; - @Column({ nullable: true, type: 'text', diff --git a/libs/common/src/modules/space/repositories/space.repository.ts b/libs/common/src/modules/space/repositories/space.repository.ts index a89a314..36cc548 100644 --- a/libs/common/src/modules/space/repositories/space.repository.ts +++ b/libs/common/src/modules/space/repositories/space.repository.ts @@ -1,7 +1,6 @@ import { Injectable } from '@nestjs/common'; import { DataSource, Repository } from 'typeorm'; import { InviteSpaceEntity } from '../entities/invite-space.entity'; -import { SpaceLinkEntity } from '../entities/space-link.entity'; import { SpaceProductAllocationEntity } from '../entities/space-product-allocation.entity'; import { SpaceEntity } from '../entities/space.entity'; @@ -13,11 +12,7 @@ export class SpaceRepository extends Repository { } @Injectable() -export class SpaceLinkRepository extends Repository { - constructor(private dataSource: DataSource) { - super(SpaceLinkEntity, dataSource.createEntityManager()); - } -} +export class SpaceLinkRepository {} @Injectable() export class InviteSpaceRepository extends Repository { diff --git a/src/community/community.module.ts b/src/community/community.module.ts index ea69401..0567ffb 100644 --- a/src/community/community.module.ts +++ b/src/community/community.module.ts @@ -78,6 +78,7 @@ import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; ProjectRepository, SpaceService, InviteSpaceRepository, + // Todo: find out why this is needed SpaceLinkService, SubSpaceService, ValidationService, diff --git a/src/community/controllers/community.controller.ts b/src/community/controllers/community.controller.ts index a6b269f..a2b0ef2 100644 --- a/src/community/controllers/community.controller.ts +++ b/src/community/controllers/community.controller.ts @@ -1,4 +1,3 @@ -import { CommunityService } from '../services/community.service'; import { Body, Controller, @@ -10,17 +9,18 @@ import { Query, UseGuards, } from '@nestjs/common'; -import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; import { AddCommunityDto } from '../dtos/add.community.dto'; import { GetCommunityParams } from '../dtos/get.community.dto'; import { UpdateCommunityNameDto } from '../dtos/update.community.dto'; +import { CommunityService } from '../services/community.service'; // import { CheckUserCommunityGuard } from 'src/guards/user.community.guard'; import { ControllerRoute } from '@app/common/constants/controller-route'; import { BaseResponseDto } from '@app/common/dto/base.response.dto'; -import { ProjectParam } from '../dtos'; -import { PermissionsGuard } from 'src/guards/permissions.guard'; -import { Permissions } from 'src/decorators/permissions.decorator'; import { PaginationRequestWithSearchGetListDto } from '@app/common/dto/pagination-with-search.request.dto'; +import { Permissions } from 'src/decorators/permissions.decorator'; +import { PermissionsGuard } from 'src/guards/permissions.guard'; +import { ProjectParam } from '../dtos'; @ApiTags('Community Module') @Controller({ @@ -45,6 +45,21 @@ export class CommunityController { return await this.communityService.createCommunity(param, addCommunityDto); } + @ApiBearerAuth() + @UseGuards(PermissionsGuard) + @Permissions('COMMUNITY_VIEW') + @ApiOperation({ + summary: ControllerRoute.COMMUNITY.ACTIONS.LIST_COMMUNITY_SUMMARY, + description: ControllerRoute.COMMUNITY.ACTIONS.LIST_COMMUNITY_DESCRIPTION, + }) + @Get('v2') + async getCommunitiesV2( + @Param() param: ProjectParam, + @Query() query: PaginationRequestWithSearchGetListDto, + ): Promise { + return this.communityService.getCommunitiesV2(param, query); + } + @ApiBearerAuth() @UseGuards(PermissionsGuard) @Permissions('COMMUNITY_VIEW') diff --git a/src/community/services/community.service.ts b/src/community/services/community.service.ts index fc33011..5de34fa 100644 --- a/src/community/services/community.service.ts +++ b/src/community/services/community.service.ts @@ -1,4 +1,7 @@ -import { ORPHAN_COMMUNITY_NAME } from '@app/common/constants/orphan-constant'; +import { + ORPHAN_COMMUNITY_NAME, + ORPHAN_SPACE_NAME, +} from '@app/common/constants/orphan-constant'; import { BaseResponseDto } from '@app/common/dto/base.response.dto'; import { PageResponse } from '@app/common/dto/pagination.response.dto'; import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; @@ -22,7 +25,7 @@ import { NotFoundException, } from '@nestjs/common'; import { SpaceService } from 'src/space/services'; -import { SelectQueryBuilder } from 'typeorm'; +import { QueryRunner, SelectQueryBuilder } from 'typeorm'; import { AddCommunityDto, GetCommunityParams, ProjectParam } from '../dtos'; import { UpdateCommunityNameDto } from '../dtos/update.community.dto'; @@ -69,12 +72,18 @@ export class CommunityService { } } - async getCommunityById(params: GetCommunityParams): Promise { + async getCommunityById( + params: GetCommunityParams, + queryRunner?: QueryRunner, + ): Promise { const { communityUuid, projectUuid } = params; await this.validateProject(projectUuid); - const community = await this.communityRepository.findOneBy({ + const communityRepository = + queryRunner?.manager.getRepository(CommunityEntity) || + this.communityRepository; + const community = await communityRepository.findOneBy({ uuid: communityUuid, }); @@ -161,6 +170,64 @@ export class CommunityService { } } + async getCommunitiesV2( + { projectUuid }: ProjectParam, + { + search, + includeSpaces, + ...pageable + }: Partial, + ) { + try { + const project = await this.validateProject(projectUuid); + + let qb: undefined | SelectQueryBuilder = undefined; + + qb = this.communityRepository + .createQueryBuilder('c') + .where('c.project = :projectUuid', { projectUuid }) + .andWhere(`c.name != '${ORPHAN_COMMUNITY_NAME}-${project.name}'`) + .distinct(true); + + if (includeSpaces) { + qb.leftJoinAndSelect('c.spaces', 'space', 'space.disabled = false') + .leftJoinAndSelect('space.parent', 'parent') + .leftJoinAndSelect( + 'space.children', + 'children', + 'children.disabled = :disabled', + { disabled: false }, + ) + // .leftJoinAndSelect('space.spaceModel', 'spaceModel') + .andWhere('space.spaceName != :orphanSpaceName', { + orphanSpaceName: ORPHAN_SPACE_NAME, + }) + .andWhere('space.disabled = :disabled', { disabled: false }); + } + + if (search) { + qb.andWhere( + `c.name ILIKE '%${search}%' ${includeSpaces ? "OR space.space_name ILIKE '%" + search + "%'" : ''}`, + ); + } + + const customModel = TypeORMCustomModel(this.communityRepository); + + const { baseResponseDto, paginationResponseDto } = + await customModel.findAll({ ...pageable, modelName: 'community' }, qb); + return new PageResponse( + baseResponseDto, + paginationResponseDto, + ); + } catch (error) { + // Generic error handling + throw new HttpException( + error.message || 'An error occurred while fetching communities.', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async updateCommunity( params: GetCommunityParams, updateCommunityDto: UpdateCommunityNameDto, diff --git a/src/invite-user/invite-user.module.ts b/src/invite-user/invite-user.module.ts index d28ff10..31e3a9b 100644 --- a/src/invite-user/invite-user.module.ts +++ b/src/invite-user/invite-user.module.ts @@ -71,7 +71,6 @@ import { 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 { - SpaceLinkService, SpaceService, SpaceUserService, SubspaceDeviceService, @@ -115,7 +114,6 @@ import { UserService, UserSpaceService } from 'src/users/services'; TimeZoneRepository, SpaceService, InviteSpaceRepository, - SpaceLinkService, SubSpaceService, ValidationService, NewTagService, diff --git a/src/power-clamp/power-clamp.module.ts b/src/power-clamp/power-clamp.module.ts index 6a36393..120e368 100644 --- a/src/power-clamp/power-clamp.module.ts +++ b/src/power-clamp/power-clamp.module.ts @@ -1,4 +1,5 @@ import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service'; +import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; import { OccupancyService } from '@app/common/helper/services/occupancy.service'; import { PowerClampService } from '@app/common/helper/services/power.clamp.service'; import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; @@ -49,7 +50,6 @@ import { SpaceModelProductAllocationService } from 'src/space-model/services/spa import { SubspaceModelProductAllocationService } from 'src/space-model/services/subspace/subspace-model-product-allocation.service'; import { SpaceDeviceService, - SpaceLinkService, SpaceService, SubspaceDeviceService, SubSpaceService, @@ -60,7 +60,6 @@ import { SubspaceProductAllocationService } from 'src/space/services/subspace/su import { TagService } from 'src/tags/services'; import { PowerClampController } from './controllers'; import { PowerClampService as PowerClamp } from './services/power-clamp.service'; -import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; @Module({ imports: [ConfigModule], controllers: [PowerClampController], @@ -90,7 +89,6 @@ import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; SceneRepository, AutomationRepository, InviteSpaceRepository, - SpaceLinkService, SubSpaceService, TagService, SpaceModelService, diff --git a/src/project/project.module.ts b/src/project/project.module.ts index 2434fde..7c7f7d3 100644 --- a/src/project/project.module.ts +++ b/src/project/project.module.ts @@ -54,7 +54,6 @@ import { 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 { - SpaceLinkService, SpaceService, SubspaceDeviceService, SubSpaceService, @@ -87,7 +86,6 @@ const CommandHandlers = [CreateOrphanSpaceHandler]; UserRepository, SpaceService, InviteSpaceRepository, - SpaceLinkService, SubSpaceService, ValidationService, TagService, diff --git a/src/project/services/project.service.ts b/src/project/services/project.service.ts index 645554e..6aed066 100644 --- a/src/project/services/project.service.ts +++ b/src/project/services/project.service.ts @@ -24,6 +24,7 @@ import { SpaceService } from 'src/space/services'; import { PassThrough } from 'stream'; import { CreateOrphanSpaceCommand } from '../command/create-orphan-space-command'; import { CreateProjectDto, GetProjectParam } from '../dto'; +import { QueryRunner } from 'typeorm'; @Injectable() export class ProjectService { @@ -212,8 +213,14 @@ export class ProjectService { } } - async findOne(uuid: string): Promise { - const project = await this.projectRepository.findOne({ where: { uuid } }); + async findOne( + uuid: string, + queryRunner?: QueryRunner, + ): Promise { + const projectRepository = queryRunner + ? queryRunner.manager.getRepository(ProjectEntity) + : this.projectRepository; + const project = await projectRepository.findOne({ where: { uuid } }); if (!project) { throw new HttpException( `Invalid project with uuid ${uuid}`, diff --git a/src/space-model/services/subspace/subspace-model.service.ts b/src/space-model/services/subspace/subspace-model.service.ts index 4001040..011b5dc 100644 --- a/src/space-model/services/subspace/subspace-model.service.ts +++ b/src/space-model/services/subspace/subspace-model.service.ts @@ -51,8 +51,12 @@ export class SubSpaceModelService { for (const [index, dto] of dtos.entries()) { const subspaceModel = savedSubspaces[index]; - const processedTags = await this.tagService.processTags( - dto.tags, + const processedTags = await this.tagService.upsertTags( + dto.tags.map((tag) => ({ + tagName: tag.name, + productUuid: tag.productUuid, + tagUuid: tag.uuid, + })), spaceModel.project.uuid, queryRunner, ); diff --git a/src/space-model/space-model.module.ts b/src/space-model/space-model.module.ts index 3d868c6..4cd6435 100644 --- a/src/space-model/space-model.module.ts +++ b/src/space-model/space-model.module.ts @@ -46,7 +46,6 @@ import { CommunityService } from 'src/community/services'; import { DeviceService } from 'src/device/services'; import { SceneService } from 'src/scene/services'; import { - SpaceLinkService, SpaceService, SubspaceDeviceService, SubSpaceService, @@ -92,7 +91,6 @@ const CommandHandlers = [ DeviceRepository, TuyaService, CommunityRepository, - SpaceLinkService, SpaceLinkRepository, InviteSpaceRepository, NewTagService, diff --git a/src/space/dtos/add.space.dto.ts b/src/space/dtos/add.space.dto.ts index c40eef7..b3b4fcc 100644 --- a/src/space/dtos/add.space.dto.ts +++ b/src/space/dtos/add.space.dto.ts @@ -3,7 +3,8 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { ArrayUnique, - IsBoolean, + IsArray, + IsMongoId, IsNotEmpty, IsNumber, IsOptional, @@ -12,7 +13,7 @@ import { NotEquals, ValidateNested, } from 'class-validator'; -import { ProcessTagDto } from 'src/tags/dtos'; +import { CreateProductAllocationDto } from './create-product-allocation.dto'; import { AddSubspaceDto } from './subspace'; export class AddSpaceDto { @@ -47,14 +48,6 @@ export class AddSpaceDto { @IsOptional() public icon?: string; - @ApiProperty({ - description: 'Indicates whether the space is private or public', - example: false, - default: false, - }) - @IsBoolean() - isPrivate: boolean; - @ApiProperty({ description: 'X position on canvas', example: 120 }) @IsNumber() x: number; @@ -64,23 +57,19 @@ export class AddSpaceDto { y: number; @ApiProperty({ - description: 'UUID of the Space', + description: 'UUID of the Space Model', example: 'd290f1ee-6c54-4b01-90e6-d701748f0851', }) - @IsString() + @IsMongoId() @IsOptional() spaceModelUuid?: string; - @ApiProperty({ description: 'Y position on canvas', example: 200 }) - @IsString() - @IsOptional() - direction?: string; - @ApiProperty({ description: 'List of subspaces included in the model', type: [AddSubspaceDto], }) @IsOptional() + @IsArray() @ValidateNested({ each: true }) @ArrayUnique((subspace) => subspace.subspaceName, { message(validationArguments) { @@ -100,51 +89,21 @@ export class AddSpaceDto { subspaces?: AddSubspaceDto[]; @ApiProperty({ - description: 'List of tags associated with the space model', - type: [ProcessTagDto], + description: 'List of allocations associated with the space', + type: [CreateProductAllocationDto], }) + @IsOptional() + @IsArray() @ValidateNested({ each: true }) - @Type(() => ProcessTagDto) - tags?: ProcessTagDto[]; -} + @Type(() => CreateProductAllocationDto) + productAllocations?: CreateProductAllocationDto[]; -export class AddUserSpaceDto { @ApiProperty({ - description: 'spaceUuid', - required: true, + description: 'List of children spaces associated with the space', + type: [AddSpaceDto], }) - @IsString() - @IsNotEmpty() - public spaceUuid: string; - @ApiProperty({ - description: 'userUuid', - required: true, - }) - @IsString() - @IsNotEmpty() - public userUuid: string; - constructor(dto: Partial) { - Object.assign(this, dto); - } -} - -export class AddUserSpaceUsingCodeDto { - @ApiProperty({ - description: 'userUuid', - required: true, - }) - @IsString() - @IsNotEmpty() - public userUuid: string; - @ApiProperty({ - description: 'inviteCode', - required: true, - }) - @IsString() - @IsNotEmpty() - public inviteCode: string; - - constructor(dto: Partial) { - Object.assign(this, dto); - } + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => AddSpaceDto) + children?: AddSpaceDto[]; } diff --git a/src/space/dtos/create-allocations.dto.ts b/src/space/dtos/create-allocations.dto.ts index 4fb7a2a..ed86b5d 100644 --- a/src/space/dtos/create-allocations.dto.ts +++ b/src/space/dtos/create-allocations.dto.ts @@ -1,14 +1,14 @@ import { SpaceEntity } from '@app/common/modules/space/entities/space.entity'; import { SubspaceEntity } from '@app/common/modules/space/entities/subspace/subspace.entity'; -import { ProcessTagDto } from 'src/tags/dtos'; import { QueryRunner } from 'typeorm'; +import { CreateProductAllocationDto } from './create-product-allocation.dto'; export enum AllocationsOwnerType { SPACE = 'space', SUBSPACE = 'subspace', } export class BaseCreateAllocationsDto { - tags: ProcessTagDto[]; + productAllocations: CreateProductAllocationDto[]; projectUuid: string; queryRunner: QueryRunner; type: AllocationsOwnerType; diff --git a/src/space/dtos/create-product-allocation.dto.ts b/src/space/dtos/create-product-allocation.dto.ts new file mode 100644 index 0000000..f0472cf --- /dev/null +++ b/src/space/dtos/create-product-allocation.dto.ts @@ -0,0 +1,36 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsNotEmpty, + IsOptional, + IsString, + IsUUID, + ValidateIf, +} from 'class-validator'; + +export class CreateProductAllocationDto { + @ApiProperty({ + description: 'The name of the tag (if creating a new tag)', + example: 'New Tag', + }) + @IsString() + @IsNotEmpty() + @ValidateIf((o) => !o.tagUuid) + tagName: string; + + @ApiProperty({ + description: 'UUID of the tag (if selecting an existing tag)', + example: '123e4567-e89b-12d3-a456-426614174000', + }) + @IsUUID() + @IsNotEmpty() + @ValidateIf((o) => !o.tagName) + tagUuid: string; + + @ApiProperty({ + description: 'UUID of the product', + example: '550e8400-e29b-41d4-a716-446655440000', + }) + @IsUUID() + @IsOptional() + productUuid: string; +} diff --git a/src/space/dtos/index.ts b/src/space/dtos/index.ts index 506efa9..c72e2b0 100644 --- a/src/space/dtos/index.ts +++ b/src/space/dtos/index.ts @@ -1,8 +1,10 @@ export * from './add.space.dto'; export * from './community-space.param'; +export * from './create-allocations.dto'; +export * from './create-product-allocation.dto'; export * from './get.space.param'; -export * from './user-space.param'; -export * from './subspace'; export * from './project.param.dto'; -export * from './update.space.dto'; +export * from './subspace'; export * from './tag'; +export * from './update.space.dto'; +export * from './user-space.param'; diff --git a/src/space/dtos/subspace/add.subspace.dto.ts b/src/space/dtos/subspace/add.subspace.dto.ts index 79dc395..d015a7c 100644 --- a/src/space/dtos/subspace/add.subspace.dto.ts +++ b/src/space/dtos/subspace/add.subspace.dto.ts @@ -1,4 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; import { IsArray, IsNotEmpty, @@ -6,8 +7,8 @@ import { IsString, ValidateNested, } from 'class-validator'; -import { Type } from 'class-transformer'; import { ProcessTagDto } from 'src/tags/dtos'; +import { CreateProductAllocationDto } from '../create-product-allocation.dto'; export class AddSubspaceDto { @ApiProperty({ @@ -24,7 +25,7 @@ export class AddSubspaceDto { }) @IsArray() @ValidateNested({ each: true }) - @Type(() => ProcessTagDto) + @Type(() => CreateProductAllocationDto) @IsOptional() - tags?: ProcessTagDto[]; + productAllocations?: CreateProductAllocationDto[]; } diff --git a/src/space/dtos/subspace/index.ts b/src/space/dtos/subspace/index.ts index 0071b44..8ce87d0 100644 --- a/src/space/dtos/subspace/index.ts +++ b/src/space/dtos/subspace/index.ts @@ -1,6 +1,5 @@ -export * from './add.subspace.dto'; -export * from './get.subspace.param'; export * from './add.subspace-device.param'; -export * from './update.subspace.dto'; +export * from './add.subspace.dto'; export * from './delete.subspace.dto'; -export * from './modify.subspace.dto'; +export * from './get.subspace.param'; +export * from './update.subspace.dto'; diff --git a/src/space/dtos/subspace/modify.subspace.dto.ts b/src/space/dtos/subspace/modify.subspace.dto.ts deleted file mode 100644 index 7f32aca..0000000 --- a/src/space/dtos/subspace/modify.subspace.dto.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ApiPropertyOptional, PartialType } from '@nestjs/swagger'; -import { IsOptional, IsUUID } from 'class-validator'; -import { AddSubspaceDto } from './add.subspace.dto'; - -export class ModifySubspaceDto extends PartialType(AddSubspaceDto) { - @ApiPropertyOptional({ - description: - 'UUID of the subspace (will present if updating an existing subspace)', - example: '123e4567-e89b-12d3-a456-426614174000', - }) - @IsOptional() - @IsUUID() - uuid?: string; -} diff --git a/src/space/dtos/subspace/update.subspace.dto.ts b/src/space/dtos/subspace/update.subspace.dto.ts index 0931d9e..4ff2397 100644 --- a/src/space/dtos/subspace/update.subspace.dto.ts +++ b/src/space/dtos/subspace/update.subspace.dto.ts @@ -1,16 +1,14 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; +import { ApiPropertyOptional, PartialType } from '@nestjs/swagger'; +import { IsOptional, IsUUID } from 'class-validator'; +import { AddSubspaceDto } from './add.subspace.dto'; -export class UpdateSubspaceDto { - @ApiProperty({ - description: 'Name of the subspace', - example: 'Living Room', +export class UpdateSubspaceDto extends PartialType(AddSubspaceDto) { + @ApiPropertyOptional({ + description: + 'UUID of the subspace (will present if updating an existing subspace)', + example: '123e4567-e89b-12d3-a456-426614174000', }) - @IsNotEmpty() - @IsString() - subspaceName?: string; - - @IsNotEmpty() - @IsString() - subspaceUuid: string; + @IsOptional() + @IsUUID() + uuid?: string; } diff --git a/src/space/dtos/update.space.dto.ts b/src/space/dtos/update.space.dto.ts index 310f695..93ff377 100644 --- a/src/space/dtos/update.space.dto.ts +++ b/src/space/dtos/update.space.dto.ts @@ -9,8 +9,8 @@ import { NotEquals, ValidateNested, } from 'class-validator'; -import { ModifySubspaceDto } from './subspace'; -import { ModifyTagDto } from './tag/modify-tag.dto'; +import { CreateProductAllocationDto } from './create-product-allocation.dto'; +import { UpdateSubspaceDto } from './subspace'; export class UpdateSpaceDto { @ApiProperty({ @@ -46,25 +46,24 @@ export class UpdateSpaceDto { y?: number; @ApiPropertyOptional({ - description: 'List of subspace modifications (add/update/delete)', - type: [ModifySubspaceDto], + description: 'List of subspace modifications', + type: [UpdateSubspaceDto], }) @IsOptional() @IsArray() @ValidateNested({ each: true }) - @Type(() => ModifySubspaceDto) - subspaces?: ModifySubspaceDto[]; + @Type(() => UpdateSubspaceDto) + subspaces?: UpdateSubspaceDto[]; @ApiPropertyOptional({ - description: - 'List of tag modifications (add/update/delete) for the space model', - type: [ModifyTagDto], + description: 'List of allocations modifications', + type: [CreateProductAllocationDto], }) @IsOptional() @IsArray() @ValidateNested({ each: true }) - @Type(() => ModifyTagDto) - tags?: ModifyTagDto[]; + @Type(() => CreateProductAllocationDto) + productAllocations?: CreateProductAllocationDto[]; @ApiProperty({ description: 'UUID of the Space', diff --git a/src/space/handlers/disable-space.handler.ts b/src/space/handlers/disable-space.handler.ts index 089ab76..45d3f96 100644 --- a/src/space/handlers/disable-space.handler.ts +++ b/src/space/handlers/disable-space.handler.ts @@ -5,11 +5,7 @@ import { DeviceService } from 'src/device/services'; import { UserSpaceService } from 'src/users/services'; import { DataSource } from 'typeorm'; import { DisableSpaceCommand } from '../commands'; -import { - SpaceLinkService, - SpaceSceneService, - SubSpaceService, -} from '../services'; +import { SpaceSceneService, SubSpaceService } from '../services'; @CommandHandler(DisableSpaceCommand) export class DisableSpaceHandler @@ -19,7 +15,6 @@ export class DisableSpaceHandler private readonly subSpaceService: SubSpaceService, private readonly userService: UserSpaceService, private readonly deviceService: DeviceService, - private readonly spaceLinkService: SpaceLinkService, private readonly sceneService: SpaceSceneService, private readonly dataSource: DataSource, ) {} @@ -39,8 +34,6 @@ export class DisableSpaceHandler 'subspaces', 'parent', 'devices', - 'outgoingConnections', - 'incomingConnections', 'scenes', 'children', 'userSpaces', @@ -79,7 +72,6 @@ export class DisableSpaceHandler orphanSpace, queryRunner, ), - this.spaceLinkService.deleteSpaceLink(space, queryRunner), this.sceneService.deleteScenes(space, queryRunner), ]; diff --git a/src/space/services/product-allocation/product-allocation.service.ts b/src/space/services/product-allocation/product-allocation.service.ts index 7950f9f..87190cb 100644 --- a/src/space/services/product-allocation/product-allocation.service.ts +++ b/src/space/services/product-allocation/product-allocation.service.ts @@ -16,10 +16,10 @@ export class ProductAllocationService { ) {} async createAllocations(dto: CreateAllocationsDto): Promise { - const { projectUuid, queryRunner, tags, type } = dto; + const { projectUuid, queryRunner, productAllocations, type } = dto; - const allocationsData = await this.tagService.processTags( - tags, + const allocationsData = await this.tagService.upsertTags( + productAllocations, projectUuid, queryRunner, ); @@ -29,15 +29,17 @@ export class ProductAllocationService { const createdTagsByName = new Map(allocationsData.map((t) => [t.name, t])); // Create the product-tag mapping based on the processed tags - const productTagMapping = tags.map(({ uuid, name, productUuid }) => { - const inputTag = uuid - ? createdTagsByUUID.get(uuid) - : createdTagsByName.get(name); - return { - tag: inputTag?.uuid, - product: productUuid, - }; - }); + const productTagMapping = productAllocations.map( + ({ tagUuid, tagName, productUuid }) => { + const inputTag = tagUuid + ? createdTagsByUUID.get(tagUuid) + : createdTagsByName.get(tagName); + return { + tag: inputTag?.uuid, + product: productUuid, + }; + }, + ); switch (type) { case AllocationsOwnerType.SPACE: { diff --git a/src/space/services/space-link/space-link.service.ts b/src/space/services/space-link/space-link.service.ts index 4801fe6..af84c2b 100644 --- a/src/space/services/space-link/space-link.service.ts +++ b/src/space/services/space-link/space-link.service.ts @@ -1,121 +1,6 @@ -import { SpaceLinkEntity } from '@app/common/modules/space/entities/space-link.entity'; -import { SpaceEntity } from '@app/common/modules/space/entities/space.entity'; -import { SpaceLinkRepository } from '@app/common/modules/space/repositories'; -import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; -import { QueryRunner } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +// todo: find out why we need to import this +// in community module in order for the whole system to work @Injectable() -export class SpaceLinkService { - constructor(private readonly spaceLinkRepository: SpaceLinkRepository) {} - - async saveSpaceLink( - startSpaceId: string, - endSpaceId: string, - direction: string, - queryRunner: QueryRunner, - ): Promise { - try { - // Check if a link between the startSpace and endSpace already exists - const existingLink = await queryRunner.manager.findOne(SpaceLinkEntity, { - where: { - startSpace: { uuid: startSpaceId }, - endSpace: { uuid: endSpaceId }, - disabled: false, - }, - }); - - if (existingLink) { - // Update the direction if the link exists - existingLink.direction = direction; - await queryRunner.manager.save(SpaceLinkEntity, existingLink); - return; - } - - const existingEndSpaceLink = await queryRunner.manager.findOne( - SpaceLinkEntity, - { - where: { endSpace: { uuid: endSpaceId } }, - }, - ); - - if ( - existingEndSpaceLink && - existingEndSpaceLink.startSpace.uuid !== startSpaceId - ) { - throw new Error( - `Space with ID ${endSpaceId} is already an endSpace in another link and cannot be reused.`, - ); - } - - // Find start space - const startSpace = await queryRunner.manager.findOne(SpaceEntity, { - where: { uuid: startSpaceId }, - }); - - if (!startSpace) { - throw new HttpException( - `Start space with ID ${startSpaceId} not found.`, - HttpStatus.NOT_FOUND, - ); - } - - // Find end space - const endSpace = await queryRunner.manager.findOne(SpaceEntity, { - where: { uuid: endSpaceId }, - }); - - if (!endSpace) { - throw new HttpException( - `End space with ID ${endSpaceId} not found.`, - HttpStatus.NOT_FOUND, - ); - } - - // Create and save the space link - const spaceLink = this.spaceLinkRepository.create({ - startSpace, - endSpace, - direction, - }); - - await queryRunner.manager.save(SpaceLinkEntity, spaceLink); - } catch (error) { - throw new HttpException( - error.message || - `Failed to save space link. Internal Server Error: ${error.message}`, - error.status || HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - async deleteSpaceLink( - space: SpaceEntity, - queryRunner: QueryRunner, - ): Promise { - try { - const spaceLinks = await queryRunner.manager.find(SpaceLinkEntity, { - where: [ - { startSpace: space, disabled: false }, - { endSpace: space, disabled: false }, - ], - }); - - if (spaceLinks.length === 0) { - return; - } - - const linkIds = spaceLinks.map((link) => link.uuid); - - await queryRunner.manager - .createQueryBuilder() - .update(SpaceLinkEntity) - .set({ disabled: true }) - .whereInIds(linkIds) - .execute(); - } catch (error) { - throw new HttpException( - `Failed to disable space links for the given space: ${error.message}`, - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } -} +export class SpaceLinkService {} diff --git a/src/space/services/space-validation.service.ts b/src/space/services/space-validation.service.ts index 0eac0d2..efbddeb 100644 --- a/src/space/services/space-validation.service.ts +++ b/src/space/services/space-validation.service.ts @@ -16,7 +16,7 @@ import { Inject, Injectable, } from '@nestjs/common'; -import { In } from 'typeorm'; +import { In, QueryRunner } from 'typeorm'; import { CommunityService } from '../../community/services'; import { ProjectService } from '../../project/services'; import { ProjectParam } from '../dtos'; @@ -69,12 +69,17 @@ export class ValidationService { async validateCommunityAndProject( communityUuid: string, projectUuid: string, + queryRunner?: QueryRunner, ) { - const project = await this.projectService.findOne(projectUuid); - const community = await this.communityService.getCommunityById({ - communityUuid, - projectUuid, - }); + const project = await this.projectService.findOne(projectUuid, queryRunner); + + const community = await this.communityService.getCommunityById( + { + communityUuid, + projectUuid, + }, + queryRunner, + ); return { community: community.data, project: project }; } @@ -170,8 +175,14 @@ export class ValidationService { return space; } - async validateSpaceModel(spaceModelUuid: string): Promise { - const queryBuilder = this.spaceModelRepository + async validateSpaceModel( + spaceModelUuid: string, + queryRunner?: QueryRunner, + ): Promise { + const queryBuilder = ( + queryRunner.manager.getRepository(SpaceModelEntity) || + this.spaceModelRepository + ) .createQueryBuilder('spaceModel') .leftJoinAndSelect( 'spaceModel.subspaceModels', diff --git a/src/space/services/space.service.ts b/src/space/services/space.service.ts index 1312030..fb08b59 100644 --- a/src/space/services/space.service.ts +++ b/src/space/services/space.service.ts @@ -22,7 +22,6 @@ import { import { CommandBus } from '@nestjs/cqrs'; import { DeviceService } from 'src/device/services'; import { SpaceModelService } from 'src/space-model/services'; -import { ProcessTagDto } from 'src/tags/dtos'; import { TagService } from 'src/tags/services/tags.service'; import { DataSource, In, Not, QueryRunner } from 'typeorm'; import { DisableSpaceCommand } from '../commands'; @@ -32,9 +31,9 @@ import { GetSpaceParam, UpdateSpaceDto, } from '../dtos'; +import { CreateProductAllocationDto } from '../dtos/create-product-allocation.dto'; import { GetSpaceDto } from '../dtos/get.space.dto'; import { SpaceWithParentsDto } from '../dtos/space.parents.dto'; -import { SpaceLinkService } from './space-link'; import { SpaceProductAllocationService } from './space-product-allocation.service'; import { ValidationService } from './space-validation.service'; import { SubSpaceService } from './subspace'; @@ -44,7 +43,6 @@ export class SpaceService { private readonly dataSource: DataSource, private readonly spaceRepository: SpaceRepository, private readonly inviteSpaceRepository: InviteSpaceRepository, - private readonly spaceLinkService: SpaceLinkService, private readonly subSpaceService: SubSpaceService, private readonly validationService: ValidationService, private readonly tagService: TagService, @@ -57,50 +55,72 @@ export class SpaceService { async createSpace( addSpaceDto: AddSpaceDto, params: CommunitySpaceParam, + queryRunner?: QueryRunner, + recursiveCallParentEntity?: SpaceEntity, ): Promise { - const { parentUuid, direction, spaceModelUuid, subspaces, tags } = - addSpaceDto; + const isRecursiveCall = !!queryRunner; + + const { + parentUuid, + spaceModelUuid, + subspaces, + productAllocations, + children, + } = addSpaceDto; const { communityUuid, projectUuid } = params; - const queryRunner = this.dataSource.createQueryRunner(); - - await queryRunner.connect(); - await queryRunner.startTransaction(); + if (!queryRunner) { + queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + } const { community } = await this.validationService.validateCommunityAndProject( communityUuid, projectUuid, + queryRunner, ); - this.validateSpaceCreationCriteria({ spaceModelUuid, subspaces, tags }); + this.validateSpaceCreationCriteria({ + spaceModelUuid, + subspaces, + productAllocations, + }); - const parent = parentUuid - ? await this.validationService.validateSpace(parentUuid) - : null; + const parent = + parentUuid && !isRecursiveCall + ? await this.validationService.validateSpace(parentUuid) + : null; const spaceModel = spaceModelUuid ? await this.validationService.validateSpaceModel(spaceModelUuid) : null; - try { const space = queryRunner.manager.create(SpaceEntity, { - ...addSpaceDto, + // todo: find a better way to handle this instead of naming every key + spaceName: addSpaceDto.spaceName, + icon: addSpaceDto.icon, + x: addSpaceDto.x, + y: addSpaceDto.y, spaceModel, - parent: parentUuid ? parent : null, + parent: isRecursiveCall + ? recursiveCallParentEntity + : parentUuid + ? parent + : null, community, }); const newSpace = await queryRunner.manager.save(space); - - const subspaceTags = - subspaces?.flatMap((subspace) => subspace.tags || []) || []; - - this.checkDuplicateTags([...tags, ...subspaceTags]); + this.checkDuplicateTags([ + ...(productAllocations || []), + ...(subspaces?.flatMap( + (subspace) => subspace.productAllocations || [], + ) || []), + ]); if (spaceModelUuid) { - // no need to check for existing dependencies here as validateSpaceCreationCriteria - // ensures no tags or subspaces are present along with spaceModelUuid await this.spaceModelService.linkToSpace( newSpace, spaceModel, @@ -109,15 +129,6 @@ export class SpaceService { } await Promise.all([ - // todo: remove this logic as we are not using space links anymore - direction && parent - ? this.spaceLinkService.saveSpaceLink( - parent.uuid, - newSpace.uuid, - direction, - queryRunner, - ) - : Promise.resolve(), subspaces?.length ? this.subSpaceService.createSubspacesFromDto( subspaces, @@ -126,12 +137,32 @@ export class SpaceService { projectUuid, ) : Promise.resolve(), - tags?.length - ? this.createAllocations(tags, projectUuid, queryRunner, newSpace) + productAllocations?.length + ? this.createAllocations( + productAllocations, + projectUuid, + queryRunner, + newSpace, + ) : Promise.resolve(), ]); - await queryRunner.commitTransaction(); + if (children?.length) { + await Promise.all( + children.map((child) => + this.createSpace( + { ...child, parentUuid: newSpace.uuid }, + { communityUuid, projectUuid }, + queryRunner, + newSpace, + ), + ), + ); + } + + if (!isRecursiveCall) { + await queryRunner.commitTransaction(); + } return new SuccessResponseDto({ statusCode: HttpStatus.CREATED, @@ -139,34 +170,34 @@ export class SpaceService { message: 'Space created successfully', }); } catch (error) { - await queryRunner.rollbackTransaction(); + !isRecursiveCall ? await queryRunner.rollbackTransaction() : null; if (error instanceof HttpException) { throw error; } throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR); } finally { - await queryRunner.release(); + !isRecursiveCall ? await queryRunner.release() : null; } } - private checkDuplicateTags(allTags: ProcessTagDto[]) { + private checkDuplicateTags(allocations: CreateProductAllocationDto[]) { const tagUuidSet = new Set(); const tagNameProductSet = new Set(); - for (const tag of allTags) { - if (tag.uuid) { - if (tagUuidSet.has(tag.uuid)) { + for (const allocation of allocations) { + if (allocation.tagUuid) { + if (tagUuidSet.has(allocation.tagUuid)) { throw new HttpException( - `Duplicate tag UUID found: ${tag.uuid}`, + `Duplicate tag UUID found: ${allocation.tagUuid}`, HttpStatus.BAD_REQUEST, ); } - tagUuidSet.add(tag.uuid); + tagUuidSet.add(allocation.tagUuid); } else { - const tagKey = `${tag.name}-${tag.productUuid}`; + const tagKey = `${allocation.tagName}-${allocation.productUuid}`; if (tagNameProductSet.has(tagKey)) { throw new HttpException( - `Duplicate tag found with name "${tag.name}" and product "${tag.productUuid}".`, + `Duplicate tag found with name "${allocation.tagName}" and product "${allocation.productUuid}".`, HttpStatus.BAD_REQUEST, ); } @@ -195,12 +226,7 @@ export class SpaceService { 'children.disabled = :disabled', { disabled: false }, ) - .leftJoinAndSelect( - 'space.incomingConnections', - 'incomingConnections', - 'incomingConnections.disabled = :incomingConnectionDisabled', - { incomingConnectionDisabled: false }, - ) + .leftJoinAndSelect('space.productAllocations', 'productAllocations') .leftJoinAndSelect('productAllocations.tag', 'tag') .leftJoinAndSelect('productAllocations.product', 'product') @@ -271,7 +297,6 @@ export class SpaceService { } } - // todo refactor this method to eliminate wrong use of tags async findOne(params: GetSpaceParam): Promise { const { communityUuid, spaceUuid, projectUuid } = params; try { @@ -282,19 +307,6 @@ export class SpaceService { const queryBuilder = this.spaceRepository .createQueryBuilder('space') - .leftJoinAndSelect('space.parent', 'parent') - .leftJoinAndSelect( - 'space.children', - 'children', - 'children.disabled = :disabled', - { disabled: false }, - ) - .leftJoinAndSelect( - 'space.incomingConnections', - 'incomingConnections', - 'incomingConnections.disabled = :incomingConnectionDisabled', - { incomingConnectionDisabled: false }, - ) .leftJoinAndSelect('space.productAllocations', 'productAllocations') .leftJoinAndSelect('productAllocations.tag', 'spaceTag') .leftJoinAndSelect('productAllocations.product', 'spaceProduct') @@ -423,7 +435,7 @@ export class SpaceService { const queryRunner = this.dataSource.createQueryRunner(); const hasSubspace = updateSpaceDto.subspaces?.length > 0; - const hasTags = updateSpaceDto.tags?.length > 0; + const hasAllocations = updateSpaceDto.productAllocations?.length > 0; try { await queryRunner.connect(); @@ -448,7 +460,7 @@ export class SpaceService { await this.updateSpaceProperties(space, updateSpaceDto, queryRunner); - if (hasSubspace || hasTags) { + if (hasSubspace || hasAllocations) { await queryRunner.manager.update(SpaceEntity, space.uuid, { spaceModel: null, }); @@ -492,7 +504,7 @@ export class SpaceService { await this.subSpaceService.unlinkModels(space.subspaces, queryRunner); } - if (hasTags && space.productAllocations && space.spaceModel) { + if (hasAllocations && space.productAllocations && space.spaceModel) { await this.spaceProductAllocationService.unlinkModels( space, queryRunner, @@ -508,13 +520,13 @@ export class SpaceService { ); } - if (updateSpaceDto.tags) { + if (updateSpaceDto.productAllocations) { await queryRunner.manager.delete(SpaceProductAllocationEntity, { space: { uuid: space.uuid }, tag: { uuid: Not( In( - updateSpaceDto.tags + updateSpaceDto.productAllocations .filter((tag) => tag.tagUuid) .map((tag) => tag.tagUuid), ), @@ -522,11 +534,7 @@ export class SpaceService { }, }); await this.createAllocations( - updateSpaceDto.tags.map((tag) => ({ - name: tag.name, - uuid: tag.tagUuid, - productUuid: tag.productUuid, - })), + updateSpaceDto.productAllocations, projectUuid, queryRunner, space, @@ -702,13 +710,17 @@ export class SpaceService { private validateSpaceCreationCriteria({ spaceModelUuid, - tags, + productAllocations, subspaces, - }: Pick): void { - const hasTagsOrSubspaces = - (tags && tags.length > 0) || (subspaces && subspaces.length > 0); + }: Pick< + AddSpaceDto, + 'spaceModelUuid' | 'productAllocations' | 'subspaces' + >): void { + const hasProductsOrSubspaces = + (productAllocations && productAllocations.length > 0) || + (subspaces && subspaces.length > 0); - if (spaceModelUuid && hasTagsOrSubspaces) { + if (spaceModelUuid && hasProductsOrSubspaces) { throw new HttpException( 'For space creation choose either space model or products and subspace', HttpStatus.CONFLICT, @@ -717,13 +729,13 @@ export class SpaceService { } private async createAllocations( - tags: ProcessTagDto[], + productAllocations: CreateProductAllocationDto[], projectUuid: string, queryRunner: QueryRunner, space: SpaceEntity, ): Promise { - const allocationsData = await this.tagService.processTags( - tags, + const allocationsData = await this.tagService.upsertTags( + productAllocations, projectUuid, queryRunner, ); @@ -733,15 +745,17 @@ export class SpaceService { const createdTagsByName = new Map(allocationsData.map((t) => [t.name, t])); // Create the product-tag mapping based on the processed tags - const productTagMapping = tags.map(({ uuid, name, productUuid }) => { - const inputTag = uuid - ? createdTagsByUUID.get(uuid) - : createdTagsByName.get(name); - return { - tag: inputTag?.uuid, - product: productUuid, - }; - }); + const productTagMapping = productAllocations.map( + ({ tagUuid, tagName, productUuid }) => { + const inputTag = tagUuid + ? createdTagsByUUID.get(tagUuid) + : createdTagsByName.get(tagName); + return { + tag: inputTag?.uuid, + product: productUuid, + }; + }, + ); await this.spaceProductAllocationService.createProductAllocations( space, diff --git a/src/space/services/subspace/subspace-product-allocation.service.ts b/src/space/services/subspace/subspace-product-allocation.service.ts index b976ce6..a74823c 100644 --- a/src/space/services/subspace/subspace-product-allocation.service.ts +++ b/src/space/services/subspace/subspace-product-allocation.service.ts @@ -3,7 +3,7 @@ import { SubspaceProductAllocationEntity } from '@app/common/modules/space/entit import { SubspaceEntity } from '@app/common/modules/space/entities/subspace/subspace.entity'; import { SubspaceProductAllocationRepository } from '@app/common/modules/space/repositories/subspace.repository'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; -import { UpdateSpaceAllocationDto } from 'src/space/interfaces/update-subspace-allocation.dto'; +import { UpdateSubspaceDto } from 'src/space/dtos'; import { TagService as NewTagService } from 'src/tags/services'; import { In, Not, QueryRunner } from 'typeorm'; @@ -60,31 +60,46 @@ export class SubspaceProductAllocationService { } async updateSubspaceProductAllocationsV2( - subSpaces: UpdateSpaceAllocationDto[], + subSpaces: UpdateSubspaceDto[], projectUuid: string, queryRunner: QueryRunner, ) { await Promise.all( subSpaces.map(async (subspace) => { await queryRunner.manager.delete(SubspaceProductAllocationEntity, { - subspace: { uuid: subspace.uuid }, - tag: { - uuid: Not( - In( - subspace.tags.filter((tag) => tag.uuid).map((tag) => tag.uuid), - ), - ), - }, + subspace: subspace.uuid ? { uuid: subspace.uuid } : undefined, + tag: subspace.productAllocations + ? { + uuid: Not( + In( + subspace.productAllocations + .filter((allocation) => allocation.tagUuid) + .map((allocation) => allocation.tagUuid), + ), + ), + } + : undefined, + product: subspace.productAllocations + ? { + uuid: Not( + In( + subspace.productAllocations + .filter((allocation) => allocation.productUuid) + .map((allocation) => allocation.productUuid), + ), + ), + } + : undefined, }); + const subspaceEntity = await queryRunner.manager.findOne( SubspaceEntity, { where: { uuid: subspace.uuid }, }, ); - - const processedTags = await this.tagService.processTags( - subspace.tags, + const processedTags = await this.tagService.upsertTags( + subspace.productAllocations, projectUuid, queryRunner, ); @@ -97,11 +112,11 @@ export class SubspaceProductAllocationService { ); // Create the product-tag mapping based on the processed tags - const productTagMapping = subspace.tags.map( - ({ uuid, name, productUuid }) => { - const inputTag = uuid - ? createdTagsByUUID.get(uuid) - : createdTagsByName.get(name); + const productTagMapping = subspace.productAllocations.map( + ({ tagUuid, tagName, productUuid }) => { + const inputTag = tagUuid + ? createdTagsByUUID.get(tagUuid) + : createdTagsByName.get(tagName); return { tag: inputTag?.uuid, product: productUuid, @@ -118,71 +133,6 @@ export class SubspaceProductAllocationService { ); } - // async processDeleteActions(dtos: ModifyTagDto[], queryRunner: QueryRunner) { - // // : Promise - // 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( - // // SubspaceProductAllocationEntity, - // // { - // // where: { tag: In(tagUuidsToDelete) }, - // // }, - // // ); - // // if (!allocationsToUpdate || allocationsToUpdate.length === 0) return []; - // // const deletedAllocations: SubspaceProductAllocationEntity[] = []; - // // const allocationUpdates: SubspaceProductAllocationEntity[] = []; - // // 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( - // // SubspaceProductAllocationEntity, - // // allocationUpdates, - // // ); - // // } - // // if (deletedAllocations.length > 0) { - // // await queryRunner.manager.remove( - // // SubspaceProductAllocationEntity, - // // deletedAllocations, - // // ); - // // } - // // await queryRunner.manager - // // .createQueryBuilder() - // // .delete() - // // .from('subspace_product_tags') - // // .where( - // // 'subspace_product_allocation_uuid NOT IN ' + - // // queryRunner.manager - // // .createQueryBuilder() - // // .select('allocation.uuid') - // // .from(SubspaceProductAllocationEntity, 'allocation') - // // .getQuery() + - // // ')', - // // ) - // // .execute(); - // // return deletedAllocations; - // } catch (error) { - // throw this.handleError(error, `Failed to delete tags in subspace`); - // } - // } - async unlinkModels( allocations: SubspaceProductAllocationEntity[], queryRunner: QueryRunner, @@ -205,67 +155,6 @@ export class SubspaceProductAllocationService { } } - // private async validateTagWithinSubspace( - // queryRunner: QueryRunner | undefined, - // tag: NewTagEntity & { product: string }, - // subspace: SubspaceEntity, - // spaceAllocationsToExclude?: SpaceProductAllocationEntity[], - // ): Promise { - // // const existingTagInSpace = await (queryRunner - // // ? queryRunner.manager.findOne(SpaceProductAllocationEntity, { - // // where: { - // // product: { uuid: tag.product }, - // // space: { uuid: subspace.space.uuid }, - // // tag: { uuid: tag.uuid }, - // // }, - // // }) - // // : this.spaceProductAllocationRepository.findOne({ - // // where: { - // // product: { uuid: tag.product }, - // // space: { uuid: subspace.space.uuid }, - // // tag: { uuid: tag.uuid }, - // // }, - // // })); - // // const isExcluded = spaceAllocationsToExclude?.some( - // // (excludedAllocation) => - // // excludedAllocation.product.uuid === tag.product && - // // excludedAllocation.tags.some((t) => t.uuid === tag.uuid), - // // ); - // // if (!isExcluded && existingTagInSpace) { - // // throw new HttpException( - // // `Tag ${tag.uuid} (Product: ${tag.product}) is already allocated at the space level (${subspace.space.uuid}). Cannot allocate the same tag in a subspace.`, - // // HttpStatus.BAD_REQUEST, - // // ); - // // } - // // // ?: Check if the tag is already allocated in another "subspace" within the same space - // // const existingTagInSameSpace = await (queryRunner - // // ? queryRunner.manager.findOne(SubspaceProductAllocationEntity, { - // // where: { - // // product: { uuid: tag.product }, - // // subspace: { space: subspace.space }, - // // tag: { uuid: tag.uuid }, - // // }, - // // relations: ['subspace'], - // // }) - // // : this.subspaceProductAllocationRepository.findOne({ - // // where: { - // // product: { uuid: tag.product }, - // // subspace: { space: subspace.space }, - // // tag: { uuid: tag.uuid }, - // // }, - // // relations: ['subspace'], - // // })); - // // if ( - // // existingTagInSameSpace && - // // existingTagInSameSpace.subspace.uuid !== subspace.uuid - // // ) { - // // throw new HttpException( - // // `Tag ${tag.uuid} (Product: ${tag.product}) is already allocated in another subspace (${existingTagInSameSpace.subspace.uuid}) within the same space (${subspace.space.uuid}).`, - // // HttpStatus.BAD_REQUEST, - // // ); - // // } - // } - private createNewSubspaceAllocation( subspace: SubspaceEntity, allocationData: { product: string; tag: string }, diff --git a/src/space/services/subspace/subspace.service.ts b/src/space/services/subspace/subspace.service.ts index d9bcd3d..d1bf83f 100644 --- a/src/space/services/subspace/subspace.service.ts +++ b/src/space/services/subspace/subspace.service.ts @@ -12,7 +12,7 @@ import { AddSubspaceDto, GetSpaceParam, GetSubSpaceParam, - ModifySubspaceDto, + UpdateSubspaceDto, } from '../../dtos'; import { SubspaceModelEntity } from '@app/common/modules/space-model'; @@ -103,13 +103,13 @@ export class SubSpaceService { queryRunner, ); await Promise.all( - addSubspaceDtos.map(async ({ tags }, index) => { + addSubspaceDtos.map(async ({ productAllocations }, index) => { // map the dto to the corresponding subspace const subspace = createdSubspaces[index]; await this.createAllocations({ projectUuid, queryRunner, - tags, + productAllocations, type: AllocationsOwnerType.SUBSPACE, subspace, }); @@ -145,7 +145,7 @@ export class SubSpaceService { space, ); const newSubspace = this.subspaceRepository.create({ - ...addSubspaceDto, + subspaceName: addSubspaceDto.subspaceName, space, }); @@ -305,7 +305,7 @@ export class SubSpaceService { } */ async updateSubspaceInSpace( - subspaceDtos: ModifySubspaceDto[], + subspaceDtos: UpdateSubspaceDto[], queryRunner: QueryRunner, space: SpaceEntity, projectUuid: string, @@ -324,42 +324,52 @@ export class SubSpaceService { disabled: true, }, ); - await queryRunner.manager.delete(SubspaceProductAllocationEntity, { - subspace: { uuid: Not(In(subspaceDtos.map((dto) => dto.uuid))) }, + subspace: { + uuid: Not( + In(subspaceDtos.filter(({ uuid }) => uuid).map(({ uuid }) => uuid)), + ), + }, }); // create or update subspaces provided in the list const newSubspaces = this.subspaceRepository.create( - subspaceDtos.filter((dto) => !dto.uuid), + subspaceDtos + .filter((dto) => !dto.uuid) + .map((dto) => ({ + subspaceName: dto.subspaceName, + space, + })), ); const updatedSubspaces: SubspaceEntity[] = await queryRunner.manager.save( SubspaceEntity, - [...newSubspaces, ...subspaceDtos.filter((dto) => dto.uuid)].map( - (subspace) => ({ ...subspace, space }), - ), + [ + ...newSubspaces, + ...subspaceDtos + .filter((dto) => dto.uuid) + .map((dto) => ({ + subspaceName: dto.subspaceName, + space, + })), + ], ); - // create or update allocations for the subspaces if (updatedSubspaces.length > 0) { await this.subspaceProductAllocationService.updateSubspaceProductAllocationsV2( - subspaceDtos.map((dto) => { - if (!dto.uuid) { - dto.uuid = updatedSubspaces.find( - (subspace) => subspace.subspaceName === dto.subspaceName, - )?.uuid; - } - return { - tags: dto.tags || [], - uuid: dto.uuid, - }; - }), + subspaceDtos.map((dto) => ({ + ...dto, + uuid: + dto.uuid || + updatedSubspaces.find((s) => s.subspaceName === dto.subspaceName) + ?.uuid, + })), projectUuid, queryRunner, ); } } catch (error) { + console.log(error); throw new HttpException( `An error occurred while modifying subspaces: ${error.message}`, HttpStatus.INTERNAL_SERVER_ERROR, @@ -478,10 +488,10 @@ export class SubSpaceService { } async createAllocations(dto: CreateAllocationsDto): Promise { - const { projectUuid, queryRunner, tags, type } = dto; - - const allocationsData = await this.newTagService.processTags( - tags, + const { projectUuid, queryRunner, productAllocations, type } = dto; + if (!productAllocations) return; + const allocationsData = await this.newTagService.upsertTags( + productAllocations, projectUuid, queryRunner, ); @@ -491,15 +501,17 @@ export class SubSpaceService { const createdTagsByName = new Map(allocationsData.map((t) => [t.name, t])); // Create the product-tag mapping based on the processed tags - const productTagMapping = tags.map(({ uuid, name, productUuid }) => { - const inputTag = uuid - ? createdTagsByUUID.get(uuid) - : createdTagsByName.get(name); - return { - tag: inputTag?.uuid, - product: productUuid, - }; - }); + const productTagMapping = productAllocations.map( + ({ tagUuid, tagName, productUuid }) => { + const inputTag = tagUuid + ? createdTagsByUUID.get(tagUuid) + : createdTagsByName.get(tagName); + return { + tag: inputTag?.uuid, + product: productUuid, + }; + }, + ); switch (type) { case AllocationsOwnerType.SUBSPACE: { diff --git a/src/space/space.module.ts b/src/space/space.module.ts index 58f54a5..8229879 100644 --- a/src/space/space.module.ts +++ b/src/space/space.module.ts @@ -79,7 +79,6 @@ import { SpaceValidationController } from './controllers/space-validation.contro import { DisableSpaceHandler } from './handlers'; import { SpaceDeviceService, - SpaceLinkService, SpaceSceneService, SpaceService, SpaceUserService, @@ -110,7 +109,6 @@ export const CommandHandlers = [DisableSpaceHandler]; ProductRepository, SubSpaceService, SpaceDeviceService, - SpaceLinkService, SubspaceDeviceService, SpaceRepository, SubspaceRepository, diff --git a/src/tags/services/tags.service.ts b/src/tags/services/tags.service.ts index b82ebc2..e43438d 100644 --- a/src/tags/services/tags.service.ts +++ b/src/tags/services/tags.service.ts @@ -14,8 +14,8 @@ import { Injectable, NotFoundException, } from '@nestjs/common'; +import { CreateProductAllocationDto } from 'src/space/dtos'; import { In, QueryRunner } from 'typeorm'; -import { ProcessTagDto } from '../dtos'; import { CreateTagDto } from '../dtos/tags.dto'; @Injectable() @@ -68,13 +68,13 @@ export class TagService { /** * Processes an array of tag DTOs, creating or updating tags in the database. - * @param tagDtos - The array of tag DTOs to process. + * @param allocationDtos - The array of allocations DTOs to process. * @param projectUuid - The UUID of the project to associate the tags with. * @param queryRunner - Optional TypeORM query runner for transaction management. * @returns An array of the processed tag entities. */ - async processTags( - tagDtos: ProcessTagDto[], + async upsertTags( + allocationDtos: CreateProductAllocationDto[], projectUuid: string, queryRunner?: QueryRunner, ): Promise { @@ -82,20 +82,22 @@ export class TagService { const dbManager = queryRunner ? queryRunner.manager : this.tagRepository.manager; - if (!tagDtos || tagDtos.length === 0) { + if (!allocationDtos || allocationDtos.length === 0) { return []; } - const [tagsWithUuid, tagsWithoutUuid]: [ - Pick[], - Omit[], - ] = this.splitTagsByUuid(tagDtos); + const [allocationsWithTagUuid, allocationsWithoutTagUuid]: [ + Pick[], + Omit[], + ] = this.splitTagsByUuid(allocationDtos); // create a set of unique existing tag names for the project const upsertedTagsByNameResult = await dbManager.upsert( NewTagEntity, Array.from( - new Set(tagsWithoutUuid.map((tag) => tag.name)).values(), + new Set( + allocationsWithoutTagUuid.map((allocation) => allocation.tagName), + ).values(), ).map((name) => ({ name, project: { uuid: projectUuid }, @@ -111,20 +113,22 @@ export class TagService { let foundByUuidTags: NewTagEntity[] = []; // Fetch existing tags using UUIDs - if (tagsWithUuid.length) { + if (allocationsWithTagUuid.length) { foundByUuidTags = await dbManager.find(NewTagEntity, { where: { - uuid: In([...tagsWithUuid.map((tag) => tag.uuid)]), + uuid: In([ + ...allocationsWithTagUuid.map((allocation) => allocation.tagUuid), + ]), project: { uuid: projectUuid }, }, }); } // Ensure all provided UUIDs exist in the database - if (foundByUuidTags.length !== tagsWithUuid.length) { + if (foundByUuidTags.length !== allocationsWithTagUuid.length) { const foundUuids = new Set(foundByUuidTags.map((tag) => tag.uuid)); - const missingUuids = tagsWithUuid.filter( - ({ uuid }) => !foundUuids.has(uuid), + const missingUuids = allocationsWithTagUuid.filter( + ({ tagUuid }) => !foundUuids.has(tagUuid), ); throw new HttpException( @@ -179,20 +183,22 @@ export class TagService { } private splitTagsByUuid( - tagDtos: ProcessTagDto[], - ): [ProcessTagDto[], ProcessTagDto[]] { - return tagDtos.reduce<[ProcessTagDto[], ProcessTagDto[]]>( - ([withUuid, withoutUuid], tag) => { - if (tag.uuid) { - withUuid.push(tag); + allocationsDtos: CreateProductAllocationDto[], + ): [CreateProductAllocationDto[], CreateProductAllocationDto[]] { + return allocationsDtos.reduce< + [CreateProductAllocationDto[], CreateProductAllocationDto[]] + >( + ([withUuid, withoutUuid], allocation) => { + if (allocation.tagUuid) { + withUuid.push(allocation); } else { - if (!tag.name || !tag.productUuid) { + if (!allocation.tagName || !allocation.productUuid) { throw new HttpException( `Tag name or product UUID is missing`, HttpStatus.BAD_REQUEST, ); } - withoutUuid.push(tag); + withoutUuid.push(allocation); } return [withUuid, withoutUuid]; }, diff --git a/src/users/dtos/add.space.dto.ts b/src/users/dtos/add.space.dto.ts index 635cec9..1bd87d7 100644 --- a/src/users/dtos/add.space.dto.ts +++ b/src/users/dtos/add.space.dto.ts @@ -1,38 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { - IsBoolean, - IsNotEmpty, - IsOptional, - IsString, - IsUUID, -} from 'class-validator'; - -export class AddSpaceDto { - @ApiProperty({ - description: 'Name of the space (e.g., Floor 1, Unit 101)', - example: 'Unit 101', - }) - @IsString() - @IsNotEmpty() - spaceName: string; - - @ApiProperty({ - description: 'UUID of the parent space (if any, for hierarchical spaces)', - example: 'f5d7e9c3-44bc-4b12-88f1-1b3cda84752e', - required: false, - }) - @IsUUID() - @IsOptional() - parentUuid?: string; - - @ApiProperty({ - description: 'Indicates whether the space is private or public', - example: false, - default: false, - }) - @IsBoolean() - isPrivate: boolean; -} +import { IsNotEmpty, IsString } from 'class-validator'; export class AddUserSpaceDto { @ApiProperty({