Merge branch 'dev'

This commit is contained in:
faris Aljohari
2025-03-11 15:33:52 +03:00
107 changed files with 7758 additions and 3568 deletions

View File

@ -11,7 +11,7 @@ on:
jobs:
build:
runs-on: "ubuntu-latest"
runs-on: 'ubuntu-latest'
steps:
- uses: actions/checkout@v2
@ -37,7 +37,7 @@ jobs:
runs-on: ubuntu-latest
needs: build
environment:
name: "staging"
name: 'staging'
url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}
steps:
@ -45,7 +45,7 @@ jobs:
id: deploy-to-webapp
uses: azure/webapps-deploy@v2
with:
app-name: "syncrow"
slot-name: "staging"
app-name: 'syncrow'
slot-name: 'staging'
publish-profile: ${{ secrets.AzureAppService_PublishProfile_44f7766441ec4796b74789e9761ef589 }}
images: "syncrow.azurecr.io/${{ secrets.AzureAppService_ContainerUsername_47395803300340b49931ea82f6d80be3 }}/syncrow/backend:${{ github.sha }}"
images: 'syncrow.azurecr.io/${{ secrets.AzureAppService_ContainerUsername_47395803300340b49931ea82f6d80be3 }}/syncrow/backend:${{ github.sha }}'

3
.gitignore vendored
View File

@ -3,6 +3,9 @@
/node_modules
/build
#github
/.github
# Logs
logs
*.log

View File

@ -197,7 +197,16 @@ export class ControllerRoute {
'retrieves all the spaces associated with a given community, organized into a hierarchical structure.';
};
};
static SPACE_VALIDATION = class {
public static readonly ROUTE = '/projects/:projectUuid/spaces';
static ACTIONS = class {
public static readonly VALIDATE_SPACE_WITH_DEVICES_OR_SUBSPACES_SUMMARY =
'Check if a space has devices or sub-spaces';
public static readonly VALIDATE_SPACE_WITH_DEVICES_OR_SUBSPACES_DESCRIPTION =
'Checks if a space has any devices or sub-spaces associated with it.';
};
};
static SPACE_SCENE = class {
public static readonly ROUTE =
'/projects/:projectUuid/communities/:communityUuid/spaces/:spaceUuid/scenes';
@ -292,7 +301,7 @@ export class ControllerRoute {
public static readonly CREATE_SPACE_MODEL_DESCRIPTION =
'This endpoint allows you to create a new space model within a specified project. A space model defines the structure of spaces, including subspaces, products, and product items, and is uniquely identifiable within the project.';
public static readonly GET_SPACE_MODEL_SUMMARY = 'Get a New Space Model';
public static readonly GET_SPACE_MODEL_SUMMARY = 'Get a Space Model';
public static readonly GET_SPACE_MODEL_DESCRIPTION =
'Fetch a space model details';
@ -307,6 +316,11 @@ export class ControllerRoute {
public static readonly DELETE_SPACE_MODEL_SUMMARY = 'Delete Space Model';
public static readonly DELETE_SPACE_MODEL_DESCRIPTION =
'This endpoint allows you to delete a specified Space Model within a project. Deleting a Space Model disables the model and all its associated subspaces and tags, ensuring they are no longer active but remain in the system for auditing.';
public static readonly LINK_SPACE_MODEL_SUMMARY =
'Link a Space Model to spaces';
public static readonly LINK_SPACE_MODEL_DESCRIPTION =
'This endpoint allows you to link a specified Space Model within a project to multiple spaces. Linking a Space Model applies the model and all its associated subspaces and tags, to the spaces.';
};
};
@ -318,6 +332,19 @@ export class ControllerRoute {
'Fetches a list of all products along with their associated device details';
};
};
static TAG = class {
public static readonly ROUTE = '/projects/:projectUuid/tags';
static ACTIONS = class {
public static readonly CREATE_TAG_SUMMARY = 'Create a new tag';
public static readonly CREATE_TAG_DESCRIPTION =
'Creates a new tag and assigns it to a specific project and product.';
public static readonly GET_TAGS_BY_PROJECT_SUMMARY =
'Get tags by project';
public static readonly GET_TAGS_BY_PROJECT_DESCRIPTION =
'Retrieves a list of tags associated with a specific project.';
};
};
static USER = class {
public static readonly ROUTE = '/user';

View File

@ -23,6 +23,7 @@ export const RolePermissions = {
'SPACE_MODEL_VIEW',
'SPACE_MODEL_UPDATE',
'SPACE_MODEL_DELETE',
'SPACE_MODEL_LINK',
'SPACE_ASSIGN_USER_TO_SPACE',
'SPACE_DELETE_USER_FROM_SPACE',
'SUBSPACE_VIEW',
@ -75,6 +76,7 @@ export const RolePermissions = {
'SPACE_MODEL_VIEW',
'SPACE_MODEL_UPDATE',
'SPACE_MODEL_DELETE',
'SPACE_MODEL_LINK',
'SPACE_ASSIGN_USER_TO_SPACE',
'SPACE_DELETE_USER_FROM_SPACE',
'SUBSPACE_VIEW',

View File

@ -8,12 +8,7 @@ import { UserOtpEntity } from '../modules/user/entities';
import { ProductEntity } from '../modules/product/entities';
import { DeviceEntity } from '../modules/device/entities';
import { PermissionTypeEntity } from '../modules/permission/entities';
import {
SpaceEntity,
SpaceLinkEntity,
SubspaceEntity,
TagEntity,
} from '../modules/space/entities';
import { UserSpaceEntity } from '../modules/user/entities';
import { DeviceUserPermissionEntity } from '../modules/device/entities';
import { RoleTypeEntity } from '../modules/role-type/entities';
@ -31,6 +26,8 @@ import {
SpaceModelEntity,
SubspaceModelEntity,
TagModel,
SpaceModelProductAllocationEntity,
SubspaceModelProductAllocationEntity,
} from '../modules/space-model/entities';
import {
InviteUserEntity,
@ -38,6 +35,13 @@ import {
} from '../modules/Invite-user/entities';
import { InviteSpaceEntity } from '../modules/space/entities/invite-space.entity';
import { AutomationEntity } from '../modules/automation/entities';
import { SpaceProductAllocationEntity } from '../modules/space/entities/space-product-allocation.entity';
import { NewTagEntity } from '../modules/tag/entities/tag.entity';
import { SpaceEntity } from '../modules/space/entities/space.entity';
import { SpaceLinkEntity } from '../modules/space/entities/space-link.entity';
import { SubspaceProductAllocationEntity } from '../modules/space/entities/subspace/subspace-product-allocation.entity';
import { SubspaceEntity } from '../modules/space/entities/subspace/subspace.entity';
import { TagEntity } from '../modules/space/entities/tag.entity';
@Module({
imports: [
TypeOrmModule.forRootAsync({
@ -52,6 +56,7 @@ import { AutomationEntity } from '../modules/automation/entities';
password: configService.get('DB_PASSWORD'),
database: configService.get('DB_NAME'),
entities: [
NewTagEntity,
ProjectEntity,
UserEntity,
UserSessionEntity,
@ -84,6 +89,10 @@ import { AutomationEntity } from '../modules/automation/entities';
InviteUserSpaceEntity,
InviteSpaceEntity,
AutomationEntity,
SpaceModelProductAllocationEntity,
SubspaceModelProductAllocationEntity,
SpaceProductAllocationEntity,
SubspaceProductAllocationEntity,
],
namingStrategy: new SnakeNamingStrategy(),
synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))),

View File

@ -0,0 +1,11 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsUUID } from 'class-validator';
export class ProjectParam {
@ApiProperty({
description: 'UUID of the Project',
example: 'd290f1ee-6c54-4b01-90e6-d701748f0851',
})
@IsUUID()
projectUuid: string;
}

View File

@ -10,7 +10,13 @@ import { GetDeviceDetailsFunctionsStatusInterface } from 'src/device/interfaces/
import { TuyaContext } from '@tuya/tuya-connector-nodejs';
import { ConfigService } from '@nestjs/config';
import { firebaseDataBase } from '../../firebase.config';
import { Database, DataSnapshot, get, ref, set } from 'firebase/database';
import {
Database,
DataSnapshot,
get,
ref,
runTransaction,
} from 'firebase/database';
import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories';
@Injectable()
export class DeviceStatusFirebaseService {
@ -154,39 +160,48 @@ export class DeviceStatusFirebaseService {
this.firebaseDb,
`device-status/${addDeviceStatusDto.deviceUuid}`,
);
const snapshot: DataSnapshot = await get(dataRef);
const existingData = snapshot.val() || {};
// Assign default values if fields are not present
if (!existingData.deviceTuyaUuid) {
existingData.deviceTuyaUuid = addDeviceStatusDto.deviceTuyaUuid;
}
if (!existingData.productUuid) {
existingData.productUuid = addDeviceStatusDto.productUuid;
}
if (!existingData.productType) {
existingData.productType = addDeviceStatusDto.productType;
}
if (!existingData.status) {
existingData.status = [];
}
// Use a transaction to handle concurrent updates
await runTransaction(dataRef, (existingData) => {
if (!existingData) {
existingData = {};
}
// Create a map to track existing status codes
const statusMap = new Map(
existingData.status.map((item) => [item.code, item.value]),
);
// Assign default values if fields are not present
if (!existingData.deviceTuyaUuid) {
existingData.deviceTuyaUuid = addDeviceStatusDto.deviceTuyaUuid;
}
if (!existingData.productUuid) {
existingData.productUuid = addDeviceStatusDto.productUuid;
}
if (!existingData.productType) {
existingData.productType = addDeviceStatusDto.productType;
}
if (!existingData.status) {
existingData.status = [];
}
// Update or add status codes
// Create a map to track existing status codes
const statusMap = new Map(
existingData.status.map((item) => [item.code, item.value]),
);
for (const statusItem of addDeviceStatusDto.status) {
statusMap.set(statusItem.code, statusItem.value);
}
// Update or add status codes
// Convert the map back to an array format
existingData.status = Array.from(statusMap, ([code, value]) => ({
code,
value,
}));
for (const statusItem of addDeviceStatusDto.status) {
statusMap.set(statusItem.code, statusItem.value);
}
// Convert the map back to an array format
existingData.status = Array.from(statusMap, ([code, value]) => ({
code,
value,
}));
return existingData;
});
// Save logs to your repository
const newLogs = addDeviceStatusDto.log.properties.map((property) => {
return this.deviceStatusLogRepository.create({
deviceId: addDeviceStatusDto.deviceUuid,
@ -200,10 +215,9 @@ export class DeviceStatusFirebaseService {
});
});
await this.deviceStatusLogRepository.save(newLogs);
// Save the updated data to Firebase
await set(dataRef, existingData);
// Return the updated data
return existingData;
const snapshot: DataSnapshot = await get(dataRef);
return snapshot.val();
}
}

View File

@ -12,10 +12,10 @@ import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { RoleTypeEntity } from '../../role-type/entities';
import { UserStatusEnum } from '@app/common/constants/user-status.enum';
import { UserEntity } from '../../user/entities';
import { SpaceEntity } from '../../space/entities';
import { RoleType } from '@app/common/constants/role.type.enum';
import { InviteUserDto, InviteUserSpaceDto } from '../dtos';
import { ProjectEntity } from '../../project/entities';
import { SpaceEntity } from '../../space/entities/space.entity';
@Entity({ name: 'invite-user' })
@Unique(['email', 'project'])

View File

@ -1,7 +1,7 @@
import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm';
import { AutomationDto } from '../dtos';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { SpaceEntity } from '../../space/entities';
import { SpaceEntity } from '../../space/entities/space.entity';
@Entity({ name: 'automation' })
export class AutomationEntity extends AbstractEntity<AutomationDto> {

View File

@ -1,8 +1,8 @@
import { Column, Entity, ManyToOne, OneToMany, Unique } from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { CommunityDto } from '../dtos';
import { SpaceEntity } from '../../space/entities';
import { ProjectEntity } from '../../project/entities';
import { SpaceEntity } from '../../space/entities/space.entity';
@Entity({ name: 'community' })
@Unique(['name'])

View File

@ -6,16 +6,17 @@ import {
Unique,
Index,
JoinColumn,
OneToOne,
} from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { DeviceDto, DeviceUserPermissionDto } from '../dtos/device.dto';
import { SpaceEntity, SubspaceEntity, TagEntity } from '../../space/entities';
import { ProductEntity } from '../../product/entities';
import { UserEntity } from '../../user/entities';
import { DeviceNotificationDto } from '../dtos';
import { PermissionTypeEntity } from '../../permission/entities';
import { SceneDeviceEntity } from '../../scene-device/entities';
import { SpaceEntity } from '../../space/entities/space.entity';
import { SubspaceEntity } from '../../space/entities/subspace/subspace.entity';
import { NewTagEntity } from '../../tag';
@Entity({ name: 'device' })
@Unique(['deviceTuyaUuid'])
@ -75,10 +76,9 @@ export class DeviceEntity extends AbstractEntity<DeviceDto> {
@OneToMany(() => SceneDeviceEntity, (sceneDevice) => sceneDevice.device, {})
sceneDevices: SceneDeviceEntity[];
@OneToOne(() => TagEntity, (tag) => tag.device, {
nullable: true,
})
tag: TagEntity;
@OneToMany(() => NewTagEntity, (tag) => tag.devices)
// @JoinTable({ name: 'device_tags' })
public tag: NewTagEntity[];
constructor(partial: Partial<DeviceEntity>) {
super();

View File

@ -4,6 +4,7 @@ import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { DeviceEntity } from '../../device/entities';
import { TagModel } from '../../space-model';
import { TagEntity } from '../../space/entities/tag.entity';
import { NewTagEntity } from '../../tag/entities';
@Entity({ name: 'product' })
export class ProductEntity extends AbstractEntity<ProductDto> {
@Column({
@ -27,6 +28,9 @@ export class ProductEntity extends AbstractEntity<ProductDto> {
})
public prodType: string;
@OneToMany(() => NewTagEntity, (tag) => tag.product, { cascade: true })
public newTags: NewTagEntity[];
@OneToMany(() => TagModel, (tag) => tag.product)
tagModels: TagModel[];

View File

@ -5,6 +5,7 @@ import { CommunityEntity } from '../../community/entities';
import { SpaceModelEntity } from '../../space-model';
import { UserEntity } from '../../user/entities';
import { InviteUserEntity } from '../../Invite-user/entities';
import { NewTagEntity } from '../../tag/entities';
@Entity({ name: 'project' })
@Unique(['name'])
@ -36,6 +37,9 @@ export class ProjectEntity extends AbstractEntity<ProjectDto> {
@OneToMany(() => InviteUserEntity, (inviteUser) => inviteUser.project)
public invitedUsers: InviteUserEntity[];
@OneToMany(() => NewTagEntity, (tag) => tag.project, { cascade: true })
public tags: NewTagEntity[];
constructor(partial: Partial<ProjectEntity>) {
super();
Object.assign(this, partial);

View File

@ -3,7 +3,7 @@ import { SceneDto, SceneIconDto } from '../dtos';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { SceneIconType } from '@app/common/constants/secne-icon-type.enum';
import { SceneDeviceEntity } from '../../scene-device/entities';
import { SpaceEntity } from '../../space/entities';
import { SpaceEntity } from '../../space/entities/space.entity';
// Define SceneIconEntity before SceneEntity
@Entity({ name: 'scene-icon' })

View File

@ -0,0 +1,30 @@
import { IsUUID, IsOptional, ValidateNested, IsArray } from 'class-validator';
import { Type } from 'class-transformer';
import { SpaceModelDto } from '../../space-model/dtos/space-model.dto';
import { ProductDto } from '../../product/dtos/product.dto';
import { NewTagDto } from '../../tag/dtos/tag.dto';
import { SpaceProductAllocationDto } from '../../space/dtos/space-product-allocation.dto';
export class SpaceModelProductAllocationDto {
@IsUUID()
uuid: string;
@ValidateNested()
@Type(() => SpaceModelDto)
spaceModel: SpaceModelDto;
@ValidateNested()
@Type(() => ProductDto)
product: ProductDto;
@IsArray()
@ValidateNested({ each: true })
@Type(() => NewTagDto)
tags: NewTagDto[];
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => SpaceProductAllocationDto)
inheritedSpaceAllocations?: SpaceProductAllocationDto[];
}

View File

@ -0,0 +1,23 @@
import { IsUUID, ValidateNested, IsArray } from 'class-validator';
import { Type } from 'class-transformer';
import { SubSpaceModelDto } from './subspace-model.dto';
import { ProductDto } from '@app/common/modules/product/dtos';
import { NewTagDto } from '@app/common/modules/tag/dtos';
export class SubspaceModelProductAllocationDto {
@IsUUID()
uuid: string;
@ValidateNested()
@Type(() => SubSpaceModelDto)
subspaceModel: SubSpaceModelDto;
@ValidateNested()
@Type(() => ProductDto)
product: ProductDto;
@IsArray()
@ValidateNested({ each: true })
@Type(() => NewTagDto)
tags: NewTagDto[];
}

View File

@ -1,3 +1,4 @@
export * from './space-model.entity';
export * from './subspace-model';
export * from './tag-model.entity';
export * from './space-model-product-allocation.entity';

View File

@ -0,0 +1,51 @@
import {
Entity,
Column,
ManyToOne,
ManyToMany,
JoinTable,
OneToMany,
} from 'typeorm';
import { SpaceModelEntity } from './space-model.entity';
import { NewTagEntity } from '../../tag/entities/tag.entity';
import { ProductEntity } from '../../product/entities/product.entity';
import { SpaceProductAllocationEntity } from '../../space/entities/space-product-allocation.entity';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
@Entity({ name: 'space_model_product_allocation' })
export class SpaceModelProductAllocationEntity extends AbstractEntity<SpaceModelProductAllocationEntity> {
@Column({
type: 'uuid',
default: () => 'gen_random_uuid()',
nullable: false,
})
public uuid: string;
@ManyToOne(
() => SpaceModelEntity,
(spaceModel) => spaceModel.productAllocations,
{ nullable: false, onDelete: 'CASCADE' },
)
public spaceModel: SpaceModelEntity;
@ManyToOne(() => ProductEntity, { nullable: false, onDelete: 'CASCADE' })
public product: ProductEntity;
@ManyToMany(() => NewTagEntity, { cascade: true, onDelete: 'CASCADE' })
@JoinTable({ name: 'space_model_product_tags' })
public tags: NewTagEntity[];
@OneToMany(
() => SpaceProductAllocationEntity,
(allocation) => allocation.inheritedFromModel,
{
cascade: true,
},
)
public inheritedSpaceAllocations: SpaceProductAllocationEntity[];
constructor(partial: Partial<SpaceModelProductAllocationEntity>) {
super();
Object.assign(this, partial);
}
}

View File

@ -3,8 +3,9 @@ import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { SpaceModelDto } from '../dtos';
import { SubspaceModelEntity } from './subspace-model';
import { ProjectEntity } from '../../project/entities';
import { SpaceEntity } from '../../space/entities';
import { TagModel } from './tag-model.entity';
import { SpaceModelProductAllocationEntity } from './space-model-product-allocation.entity';
import { SpaceEntity } from '../../space/entities/space.entity';
@Entity({ name: 'space-model' })
export class SpaceModelEntity extends AbstractEntity<SpaceModelDto> {
@ -51,6 +52,13 @@ export class SpaceModelEntity extends AbstractEntity<SpaceModelDto> {
@OneToMany(() => TagModel, (tag) => tag.spaceModel)
tags: TagModel[];
@OneToMany(
() => SpaceModelProductAllocationEntity,
(allocation) => allocation.spaceModel,
{ cascade: true },
)
public productAllocations: SpaceModelProductAllocationEntity[];
constructor(partial: Partial<SpaceModelEntity>) {
super();
Object.assign(this, partial);

View File

@ -1,2 +1,2 @@
export * from './subspace-model.entity';
export * from './subspace-model-product-allocation.entity';

View File

@ -0,0 +1,41 @@
import { Entity, Column, ManyToOne, ManyToMany, JoinTable } from 'typeorm';
import { SubspaceModelEntity } from './subspace-model.entity';
import { ProductEntity } from '@app/common/modules/product/entities/product.entity';
import { NewTagEntity } from '@app/common/modules/tag/entities/tag.entity';
import { SubspaceModelProductAllocationDto } from '../../dtos/subspace-model/subspace-model-product-allocation.dto';
import { AbstractEntity } from '@app/common/modules/abstract/entities/abstract.entity';
@Entity({ name: 'subspace_model_product_allocation' })
export class SubspaceModelProductAllocationEntity extends AbstractEntity<SubspaceModelProductAllocationDto> {
@Column({
type: 'uuid',
default: () => 'gen_random_uuid()',
nullable: false,
})
public uuid: string;
@ManyToOne(
() => SubspaceModelEntity,
(subspaceModel) => subspaceModel.productAllocations,
{
nullable: false,
onDelete: 'CASCADE',
},
)
public subspaceModel: SubspaceModelEntity;
@ManyToOne(() => ProductEntity, { nullable: false, onDelete: 'CASCADE' })
public product: ProductEntity;
@ManyToMany(() => NewTagEntity, (tag) => tag.subspaceModelAllocations, {
cascade: true,
onDelete: 'CASCADE',
})
@JoinTable({ name: 'subspace_model_product_tags' })
public tags: NewTagEntity[];
constructor(partial: Partial<SubspaceModelProductAllocationEntity>) {
super();
Object.assign(this, partial);
}
}

View File

@ -1,9 +1,10 @@
import { AbstractEntity } from '@app/common/modules/abstract/entities/abstract.entity';
import { Column, Entity, ManyToOne, OneToMany, Unique } from 'typeorm';
import { Column, Entity, ManyToOne, OneToMany } from 'typeorm';
import { SubSpaceModelDto } from '../../dtos';
import { SpaceModelEntity } from '../space-model.entity';
import { SubspaceEntity } from '@app/common/modules/space/entities';
import { TagModel } from '../tag-model.entity';
import { SubspaceModelProductAllocationEntity } from './subspace-model-product-allocation.entity';
import { SubspaceEntity } from '@app/common/modules/space/entities/subspace/subspace.entity';
@Entity({ name: 'subspace-model' })
export class SubspaceModelEntity extends AbstractEntity<SubSpaceModelDto> {
@ -32,7 +33,7 @@ export class SubspaceModelEntity extends AbstractEntity<SubSpaceModelDto> {
@OneToMany(() => SubspaceEntity, (subspace) => subspace.subSpaceModel, {
cascade: true,
})
public subspaceModel: SubspaceEntity[];
public subspace: SubspaceEntity[];
@Column({
nullable: false,
@ -42,4 +43,11 @@ export class SubspaceModelEntity extends AbstractEntity<SubSpaceModelDto> {
@OneToMany(() => TagModel, (tag) => tag.subspaceModel)
tags: TagModel[];
@OneToMany(
() => SubspaceModelProductAllocationEntity,
(allocation) => allocation.subspaceModel,
{ cascade: true },
)
public productAllocations: SubspaceModelProductAllocationEntity[];
}

View File

@ -1,6 +1,12 @@
import { DataSource, Repository } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { SpaceModelEntity, SubspaceModelEntity, TagModel } from '../entities';
import {
SpaceModelEntity,
SpaceModelProductAllocationEntity,
SubspaceModelEntity,
SubspaceModelProductAllocationEntity,
TagModel,
} from '../entities';
@Injectable()
export class SpaceModelRepository extends Repository<SpaceModelEntity> {
@ -21,3 +27,20 @@ export class TagModelRepository extends Repository<TagModel> {
super(TagModel, dataSource.createEntityManager());
}
}
@Injectable()
export class SpaceModelProductAllocationRepoitory extends Repository<SpaceModelProductAllocationEntity> {
constructor(private dataSource: DataSource) {
super(SpaceModelProductAllocationEntity, dataSource.createEntityManager());
}
}
@Injectable()
export class SubspaceModelProductAllocationRepoitory extends Repository<SubspaceModelProductAllocationEntity> {
constructor(private dataSource: DataSource) {
super(
SubspaceModelProductAllocationEntity,
dataSource.createEntityManager(),
);
}
}

View File

@ -1,3 +1,4 @@
export * from './space.dto';
export * from './subspace.dto';
export * from './tag.dto';
export * from './space-product-allocation.dto';

View File

@ -0,0 +1,24 @@
import { IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator';
import { SpaceDto } from './space.dto';
import { Type } from 'class-transformer';
import { ProductDto } from '../../product/dtos';
import { NewTagDto } from '../../tag/dtos';
export class SpaceProductAllocationDto {
@IsString()
@IsNotEmpty()
public uuid: string;
@ValidateNested()
@Type(() => SpaceDto)
public space: SpaceDto;
@ValidateNested()
@Type(() => ProductDto)
product: ProductDto;
@IsArray()
@ValidateNested({ each: true })
@Type(() => NewTagDto)
tags: NewTagDto[];
}

View File

@ -0,0 +1,24 @@
import { IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
import { ProductDto } from '../../product/dtos';
import { NewTagDto } from '../../tag/dtos';
import { SubspaceDto } from './subspace.dto';
export class SubspaceProductAllocationDto {
@IsString()
@IsNotEmpty()
public uuid: string;
@ValidateNested()
@Type(() => SubspaceDto)
public subspace: SubspaceDto;
@ValidateNested()
@Type(() => ProductDto)
product: ProductDto;
@IsArray()
@ValidateNested({ each: true })
@Type(() => NewTagDto)
tags: NewTagDto[];
}

View File

@ -1,4 +0,0 @@
export * from './space.entity';
export * from './subspace';
export * from './space-link.entity';
export * from './tag.entity';

View File

@ -0,0 +1,41 @@
import { Entity, Column, ManyToOne, ManyToMany, JoinTable } from 'typeorm';
import { SpaceEntity } from './space.entity';
import { SpaceModelProductAllocationEntity } from '../../space-model/entities/space-model-product-allocation.entity';
import { ProductEntity } from '../../product/entities/product.entity';
import { NewTagEntity } from '../../tag/entities/tag.entity';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { SpaceProductAllocationDto } from '../dtos/space-product-allocation.dto';
@Entity({ name: 'space_product_allocation' })
export class SpaceProductAllocationEntity extends AbstractEntity<SpaceProductAllocationDto> {
@Column({
type: 'uuid',
default: () => 'gen_random_uuid()',
nullable: false,
})
public uuid: string;
@ManyToOne(() => SpaceEntity, (space) => space.productAllocations, {
nullable: false,
onDelete: 'CASCADE',
})
public space: SpaceEntity;
@ManyToOne(() => SpaceModelProductAllocationEntity, {
nullable: true,
onDelete: 'SET NULL',
})
public inheritedFromModel?: SpaceModelProductAllocationEntity;
@ManyToOne(() => ProductEntity, { nullable: false, onDelete: 'CASCADE' })
public product: ProductEntity;
@ManyToMany(() => NewTagEntity)
@JoinTable({ name: 'space_product_tags' })
public tags: NewTagEntity[];
constructor(partial: Partial<SpaceProductAllocationEntity>) {
super();
Object.assign(this, partial);
}
}

View File

@ -4,12 +4,13 @@ import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { UserSpaceEntity } from '../../user/entities';
import { DeviceEntity } from '../../device/entities';
import { CommunityEntity } from '../../community/entities';
import { SubspaceEntity } from './subspace';
import { SpaceLinkEntity } from './space-link.entity';
import { SceneEntity } from '../../scene/entities';
import { SpaceModelEntity } from '../../space-model';
import { InviteUserSpaceEntity } from '../../Invite-user/entities';
import { TagEntity } from './tag.entity';
import { SpaceProductAllocationEntity } from './space-product-allocation.entity';
import { SubspaceEntity } from './subspace/subspace.entity';
@Entity({ name: 'space' })
export class SpaceEntity extends AbstractEntity<SpaceDto> {
@ -105,6 +106,15 @@ export class SpaceEntity extends AbstractEntity<SpaceDto> {
@OneToMany(() => TagEntity, (tag) => tag.space)
tags: TagEntity[];
@OneToMany(
() => SpaceProductAllocationEntity,
(allocation) => allocation.space,
{
cascade: true,
},
)
public productAllocations: SpaceProductAllocationEntity[];
constructor(partial: Partial<SpaceEntity>) {
super();
Object.assign(this, partial);

View File

@ -1 +0,0 @@
export * from './subspace.entity';

View File

@ -0,0 +1,49 @@
import {
Entity,
Column,
ManyToOne,
ManyToMany,
JoinTable,
Unique,
} from 'typeorm';
import { SubspaceEntity } from './subspace.entity';
import { ProductEntity } from '@app/common/modules/product/entities';
import { SubspaceModelProductAllocationEntity } from '@app/common/modules/space-model';
import { NewTagEntity } from '@app/common/modules/tag/entities/tag.entity';
import { AbstractEntity } from '@app/common/modules/abstract/entities/abstract.entity';
import { SubspaceProductAllocationDto } from '../../dtos/subspace-product-allocation.dto';
@Entity({ name: 'subspace_product_allocation' })
@Unique(['subspace', 'product'])
export class SubspaceProductAllocationEntity extends AbstractEntity<SubspaceProductAllocationDto> {
@Column({
type: 'uuid',
default: () => 'gen_random_uuid()',
nullable: false,
})
public uuid: string;
@ManyToOne(() => SubspaceEntity, (subspace) => subspace.productAllocations, {
nullable: false,
onDelete: 'CASCADE',
})
public subspace: SubspaceEntity;
@ManyToOne(() => SubspaceModelProductAllocationEntity, {
nullable: true,
onDelete: 'SET NULL',
})
public inheritedFromModel?: SubspaceModelProductAllocationEntity;
@ManyToOne(() => ProductEntity, { nullable: false, onDelete: 'CASCADE' })
public product: ProductEntity;
@ManyToMany(() => NewTagEntity)
@JoinTable({ name: 'subspace_product_tags' })
public tags: NewTagEntity[];
constructor(partial: Partial<SubspaceProductAllocationEntity>) {
super();
Object.assign(this, partial);
}
}

View File

@ -5,6 +5,7 @@ import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm';
import { SubspaceDto } from '../../dtos';
import { SpaceEntity } from '../space.entity';
import { TagEntity } from '../tag.entity';
import { SubspaceProductAllocationEntity } from './subspace-product-allocation.entity';
@Entity({ name: 'subspace' })
export class SubspaceEntity extends AbstractEntity<SubspaceDto> {
@ -37,7 +38,7 @@ export class SubspaceEntity extends AbstractEntity<SubspaceDto> {
})
devices: DeviceEntity[];
@ManyToOne(() => SubspaceModelEntity, (subspace) => subspace.subspaceModel, {
@ManyToOne(() => SubspaceModelEntity, (model) => model.subspace, {
nullable: true,
})
subSpaceModel?: SubspaceModelEntity;
@ -45,6 +46,13 @@ export class SubspaceEntity extends AbstractEntity<SubspaceDto> {
@OneToMany(() => TagEntity, (tag) => tag.subspace)
tags: TagEntity[];
@OneToMany(
() => SubspaceProductAllocationEntity,
(allocation) => allocation.subspace,
{ cascade: true },
)
public productAllocations: SubspaceProductAllocationEntity[];
constructor(partial: Partial<SubspaceEntity>) {
super();
Object.assign(this, partial);

View File

@ -4,12 +4,12 @@ import { ProductEntity } from '../../product/entities';
import { TagDto } from '../dtos';
import { TagModel } from '../../space-model/entities/tag-model.entity';
import { SpaceEntity } from './space.entity';
import { SubspaceEntity } from './subspace';
import { DeviceEntity } from '../../device/entities';
import { SubspaceEntity } from './subspace/subspace.entity';
@Entity({ name: 'tag' })
export class TagEntity extends AbstractEntity<TagDto> {
@Column({ type: 'varchar', length: 255 })
@Column({ type: 'varchar', length: 255, nullable: true })
tag: string;
@ManyToOne(() => TagModel, (model) => model.tags, {

View File

@ -1,4 +1,3 @@
export * from './dtos';
export * from './entities';
export * from './repositories';
export * from './space.repository.module';

View File

@ -1,7 +1,10 @@
import { DataSource, Repository } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { SpaceEntity, SpaceLinkEntity, TagEntity } from '../entities';
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> {
@ -30,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

@ -1,6 +1,7 @@
import { DataSource, Repository } from 'typeorm';
import { SubspaceEntity } from '../entities';
import { Injectable } from '@nestjs/common';
import { SubspaceEntity } from '../entities/subspace/subspace.entity';
import { SubspaceProductAllocationEntity } from '../entities/subspace/subspace-product-allocation.entity';
@Injectable()
export class SubspaceRepository extends Repository<SubspaceEntity> {
@ -8,3 +9,9 @@ export class SubspaceRepository extends Repository<SubspaceEntity> {
super(SubspaceEntity, dataSource.createEntityManager());
}
}
@Injectable()
export class SubspaceProductAllocationRepository extends Repository<SubspaceProductAllocationEntity> {
constructor(private dataSource: DataSource) {
super(SubspaceProductAllocationEntity, dataSource.createEntityManager());
}
}

View File

@ -1,7 +1,12 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { SpaceEntity, SubspaceEntity, TagEntity } from './entities';
import { InviteSpaceEntity } from './entities/invite-space.entity';
import { SpaceProductAllocationEntity } from './entities/space-product-allocation.entity';
import { SpaceEntity } from './entities/space.entity';
import { SubspaceProductAllocationEntity } from './entities/subspace/subspace-product-allocation.entity';
import { SubspaceEntity } from './entities/subspace/subspace.entity';
import { TagEntity } from './entities/tag.entity';
@Module({
providers: [],
@ -13,6 +18,8 @@ import { InviteSpaceEntity } from './entities/invite-space.entity';
SubspaceEntity,
TagEntity,
InviteSpaceEntity,
SpaceProductAllocationEntity,
SubspaceProductAllocationEntity,
]),
],
})

View File

@ -0,0 +1 @@
export * from './tag.dto';

View File

@ -0,0 +1,19 @@
import { IsNotEmpty, IsUUID, IsString } from 'class-validator';
export class NewTagDto {
@IsString()
@IsNotEmpty()
public uuid: string;
@IsString()
@IsNotEmpty()
name: string;
@IsUUID()
@IsNotEmpty()
productId: string;
@IsUUID()
@IsNotEmpty()
projectId: string;
}

View File

@ -0,0 +1 @@
export * from './tag.entity';

View File

@ -0,0 +1,58 @@
import { Entity, Column, ManyToOne, Unique, ManyToMany } from 'typeorm';
import { ProductEntity } from '../../product/entities';
import { ProjectEntity } from '../../project/entities';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { NewTagDto } from '../dtos/tag.dto';
import { SpaceModelProductAllocationEntity } from '../../space-model/entities/space-model-product-allocation.entity';
import { SubspaceModelProductAllocationEntity } from '../../space-model/entities/subspace-model/subspace-model-product-allocation.entity';
import { DeviceEntity } from '../../device/entities/device.entity';
@Entity({ name: 'new_tag' })
@Unique(['name', 'project'])
export class NewTagEntity extends AbstractEntity<NewTagDto> {
@Column({
type: 'uuid',
default: () => 'gen_random_uuid()',
nullable: false,
})
public uuid: string;
@Column({
type: 'varchar',
length: 255,
nullable: true,
})
name: string;
@ManyToOne(() => ProductEntity, (product) => product.newTags, {
nullable: false,
onDelete: 'CASCADE',
})
public product: ProductEntity;
@ManyToOne(() => ProjectEntity, (project) => project.tags, {
nullable: false,
onDelete: 'CASCADE',
})
public project: ProjectEntity;
@ManyToMany(
() => SpaceModelProductAllocationEntity,
(allocation) => allocation.tags,
)
public spaceModelAllocations: SpaceModelProductAllocationEntity[];
@ManyToMany(
() => SubspaceModelProductAllocationEntity,
(allocation) => allocation.tags,
)
public subspaceModelAllocations: SubspaceModelProductAllocationEntity[];
@ManyToOne(() => DeviceEntity, (device) => device.tag)
public devices: DeviceEntity[];
constructor(partial: Partial<NewTagEntity>) {
super();
Object.assign(this, partial);
}
}

View File

@ -0,0 +1 @@
export * from './entities';

View File

@ -0,0 +1,10 @@
import { Injectable } from '@nestjs/common';
import { NewTagEntity } from '../entities';
import { DataSource, Repository } from 'typeorm';
@Injectable()
export class NewTagRepository extends Repository<NewTagEntity> {
constructor(private dataSource: DataSource) {
super(NewTagEntity, dataSource.createEntityManager());
}
}

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { NewTagEntity } from './entities/tag.entity';
@Module({
providers: [],
exports: [],
controllers: [],
imports: [TypeOrmModule.forFeature([NewTagEntity])],
})
export class NewTagRepositoryModule {}

View File

@ -25,10 +25,10 @@ import { RegionEntity } from '../../region/entities';
import { TimeZoneEntity } from '../../timezone/entities';
import { OtpType } from '../../../../src/constants/otp-type.enum';
import { RoleTypeEntity } from '../../role-type/entities';
import { SpaceEntity } from '../../space/entities';
import { VisitorPasswordEntity } from '../../visitor-password/entities';
import { InviteUserEntity } from '../../Invite-user/entities';
import { ProjectEntity } from '../../project/entities';
import { SpaceEntity } from '../../space/entities/space.entity';
@Entity({ name: 'user' })
export class UserEntity extends AbstractEntity<UserDto> {

4850
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -28,6 +28,7 @@ import { PermissionModule } from './permission/permission.module';
import { RoleModule } from './role/role.module';
import { TermsConditionsModule } from './terms-conditions/terms-conditions.module';
import { PrivacyPolicyModule } from './privacy-policy/privacy-policy.module';
import { TagModule } from './tags/tags.module';
@Module({
imports: [
ConfigModule.forRoot({
@ -59,6 +60,7 @@ import { PrivacyPolicyModule } from './privacy-policy/privacy-policy.module';
RoleModule,
TermsConditionsModule,
PrivacyPolicyModule,
TagModule,
],
providers: [
{

View File

@ -218,7 +218,7 @@ export class CommunityService {
}
}
private async validateProject(uuid: string) {
async validateProject(uuid: string) {
const project = await this.projectRepository.findOne({
where: { uuid },
});

View File

@ -48,7 +48,6 @@ import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status
import { DeviceStatuses } from '@app/common/constants/device-status.enum';
import { CommonErrorCodes } from '@app/common/constants/error-codes.enum';
import { BatteryStatus } from '@app/common/constants/battery-status.enum';
import { SpaceEntity } from '@app/common/modules/space/entities';
import { SceneService } from 'src/scene/services';
import { AddAutomationDto } from 'src/automation/dtos';
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
@ -60,6 +59,7 @@ import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { DeleteSceneFromSceneDeviceDto } from '../dtos/delete.device.dto';
import { DeviceEntity } from '@app/common/modules/device/entities';
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
import { ProjectRepository } from '@app/common/modules/project/repositiories';
import { ProjectParam } from '../dtos';
@ -1546,4 +1546,21 @@ export class DeviceService {
);
}
}
async moveDevicesToSpace(
targetSpace: SpaceEntity,
deviceIds: string[],
): Promise<void> {
if (!deviceIds || deviceIds.length === 0) {
throw new HttpException(
'No device IDs provided for transfer',
HttpStatus.BAD_REQUEST,
);
}
await this.deviceRepository.update(
{ uuid: In(deviceIds) },
{ spaceDevice: targetSpace },
);
}
}

View File

@ -20,7 +20,7 @@ import {
import { CheckEmailDto } from '../dtos/check-email.dto';
import { UserRepository } from '@app/common/modules/user/repositories';
import { EmailService } from '@app/common/util/email.service';
import { SpaceEntity, SpaceRepository } from '@app/common/modules/space';
import { SpaceRepository } from '@app/common/modules/space';
import { ActivateCodeDto } from '../dtos/active-code.dto';
import { UserSpaceService } from 'src/users/services';
import { SpaceUserService } from 'src/space/services';
@ -30,6 +30,7 @@ import {
} from '../dtos/update.invite-user.dto';
import { RoleTypeRepository } from '@app/common/modules/role-type/repositories';
import { InviteUserEntity } from '@app/common/modules/Invite-user/entities';
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
@Injectable()
export class InviteUserService {

View File

@ -26,7 +26,7 @@ export class CreateOrphanSpaceHandler
const orphanCommunityName = `${ORPHAN_COMMUNITY_NAME}-${project.name}`;
let orphanCommunity = await this.communityRepository.findOne({
where: { name: orphanCommunityName, project },
where: { name: orphanCommunityName, project: { uuid: project.uuid } },
});
if (!orphanCommunity) {

View File

@ -94,7 +94,7 @@ export class ProjectUserService {
'invitedBy',
'isEnabled',
],
relations: ['roleType', 'spaces.space'],
relations: ['roleType', 'spaces.space', 'spaces.space.community'],
});
if (!user) {
@ -114,7 +114,14 @@ export class ProjectUserService {
roleType: user.roleType.type,
createdDate,
createdTime,
spaces: user.spaces.map((space) => space.space),
spaces: user.spaces.map(({ space }) => {
const { community, ...spaceWithoutCommunity } = space;
return {
...spaceWithoutCommunity,
communityUuid: community.uuid,
communityName: community.name,
};
}),
},
statusCode: HttpStatus.OK,
});

View File

@ -183,6 +183,12 @@ export class ProjectService {
async findOne(uuid: string): Promise<ProjectEntity> {
const project = await this.projectRepository.findOne({ where: { uuid } });
if (!project) {
throw new HttpException(
`Invalid project with uuid ${uuid}`,
HttpStatus.NOT_FOUND,
);
}
return project;
}

View File

@ -1,2 +1,2 @@
export * from './propogate-subspace-update-command';
export * from './propagate-space-model-deletion.command';
export * from './propagate-subspace-model-update-command';

View File

@ -1,9 +1,11 @@
import { SpaceModelEntity } from '@app/common/modules/space-model';
import { QueryRunner } from 'typeorm';
export class PropogateDeleteSpaceModelCommand {
constructor(
public readonly param: {
spaceModel: SpaceModelEntity;
queryRunner: QueryRunner;
},
) {}
}

View File

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

View File

@ -14,6 +14,7 @@ import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { SpaceModelService } from '../services';
import {
CreateSpaceModelDto,
LinkSpacesToModelDto,
SpaceModelParam,
UpdateSpaceModelDto,
} from '../dtos';
@ -70,9 +71,9 @@ export class SpaceModelController {
@UseGuards(PermissionsGuard)
@Permissions('SPACE_MODEL_VIEW')
@ApiOperation({
summary: ControllerRoute.SPACE_MODEL.ACTIONS.LIST_SPACE_MODEL_SUMMARY,
summary: ControllerRoute.SPACE_MODEL.ACTIONS.GET_SPACE_MODEL_SUMMARY,
description:
ControllerRoute.SPACE_MODEL.ACTIONS.LIST_SPACE_MODEL_DESCRIPTION,
ControllerRoute.SPACE_MODEL.ACTIONS.GET_SPACE_MODEL_DESCRIPTION,
})
@Get(':spaceModelUuid')
async get(@Param() param: SpaceModelParam): Promise<BaseResponseDto> {
@ -107,4 +108,20 @@ export class SpaceModelController {
async delete(@Param() param: SpaceModelParam): Promise<BaseResponseDto> {
return await this.spaceModelService.deleteSpaceModel(param);
}
@ApiBearerAuth()
@UseGuards(PermissionsGuard)
@Permissions('SPACE_MODEL_LINK')
@ApiOperation({
summary: ControllerRoute.SPACE_MODEL.ACTIONS.LINK_SPACE_MODEL_SUMMARY,
description:
ControllerRoute.SPACE_MODEL.ACTIONS.LINK_SPACE_MODEL_DESCRIPTION,
})
@Post(':spaceModelUuid/spaces/link')
async link(
@Param() params: SpaceModelParam,
@Body() dto: LinkSpacesToModelDto,
): Promise<BaseResponseDto> {
return await this.spaceModelService.linkSpaceModel(params, dto);
}
}

View File

@ -2,7 +2,7 @@ import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString, IsArray, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
import { CreateSubspaceModelDto } from './subspaces-model-dtos/create-subspace-model.dto';
import { CreateTagModelDto } from './tag-model-dtos/create-tag-model.dto';
import { ProcessTagDto } from 'src/tags/dtos';
export class CreateSpaceModelDto {
@ApiProperty({
@ -24,10 +24,10 @@ export class CreateSpaceModelDto {
@ApiProperty({
description: 'List of tags associated with the space model',
type: [CreateTagModelDto],
type: [ProcessTagDto],
})
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreateTagModelDto)
tags?: CreateTagModelDto[];
@Type(() => ProcessTagDto)
tags?: ProcessTagDto[];
}

View File

@ -4,3 +4,4 @@ export * from './update-space-model.dto';
export * from './space-model-param';
export * from './subspaces-model-dtos';
export * from './tag-model-dtos';
export * from './link-space-model.dto';

View File

@ -0,0 +1,25 @@
import { IsArray, ArrayNotEmpty, IsUUID, IsBoolean } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class LinkSpacesToModelDto {
@ApiProperty({
description: 'List of space UUIDs to be linked to the space model',
type: [String],
example: [
'550e8400-e29b-41d4-a716-446655440000',
'f47ac10b-58cc-4372-a567-0e02b2c3d479',
],
})
@IsArray()
@ArrayNotEmpty()
@IsUUID('4', { each: true })
spaceUuids: string[];
@ApiProperty({
description: 'Whether to overwrite existing space model links',
type: Boolean,
example: false,
})
@IsBoolean()
overwrite: boolean;
}

View File

@ -3,7 +3,7 @@ import { IsUUID } from 'class-validator';
import { ProjectParam } from './project-param.dto';
export class SpaceModelParam extends ProjectParam {
@ApiProperty({
description: 'UUID of the Space',
description: 'UUID of the Space model',
example: 'd290f1ee-6c54-4b01-90e6-d701748f0851',
})
@IsUUID()

View File

@ -1,7 +1,7 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator';
import { CreateTagModelDto } from '../tag-model-dtos/create-tag-model.dto';
import { Type } from 'class-transformer';
import { ProcessTagDto } from 'src/tags/dtos';
export class CreateSubspaceModelDto {
@ApiProperty({
@ -14,10 +14,10 @@ export class CreateSubspaceModelDto {
@ApiProperty({
description: 'List of tag models associated with the subspace',
type: [CreateTagModelDto],
type: [ProcessTagDto],
})
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreateTagModelDto)
tags?: CreateTagModelDto[];
@Type(() => ProcessTagDto)
tags?: ProcessTagDto[];
}

View File

@ -3,7 +3,7 @@ import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
export class CreateTagModelDto {
@ApiProperty({
description: 'Tag models associated with the space or subspace models',
description: 'Tag associated with the space or subspace models',
example: 'Temperature Control',
})
@IsNotEmpty()

View File

@ -1,6 +1,6 @@
import { ModifyAction } from '@app/common/constants/modify-action.enum';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsString, IsOptional, IsEnum } from 'class-validator';
import { IsString, IsOptional, IsEnum, IsUUID } from 'class-validator';
export class ModifyTagModelDto {
@ApiProperty({
@ -11,20 +11,29 @@ export class ModifyTagModelDto {
action: ModifyAction;
@ApiPropertyOptional({
description: 'UUID of the tag model (required for update/delete)',
description: 'UUID of the new tag',
example: '123e4567-e89b-12d3-a456-426614174000',
})
@IsOptional()
@IsString()
uuid?: string;
@IsUUID()
newTagUuid: string;
@ApiPropertyOptional({
description: 'Name of the tag model (required for add/update)',
description:
'UUID of an existing tag (required for update/delete, optional for add)',
example: 'a1b2c3d4-5678-90ef-abcd-1234567890ef',
})
@IsOptional()
@IsUUID()
tagUuid?: string;
@ApiPropertyOptional({
description: 'Name of the tag (required for add/update)',
example: 'Temperature Sensor',
})
@IsOptional()
@IsString()
tag?: string;
name?: string;
@ApiPropertyOptional({
description:
@ -32,6 +41,6 @@ export class ModifyTagModelDto {
example: 'c789a91e-549a-4753-9006-02f89e8170e0',
})
@IsOptional()
@IsString()
@IsUUID()
productUuid?: string;
}

View File

@ -1,19 +1,27 @@
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { PropogateUpdateSpaceModelCommand } from '../commands';
import { SpaceEntity, SpaceRepository } from '@app/common/modules/space';
import { SubspaceRepository } from '@app/common/modules/space/repositories/subspace.repository';
import {
SpaceProductAllocationRepository,
SpaceRepository,
} from '@app/common/modules/space';
import {
SubspaceProductAllocationRepository,
SubspaceRepository,
} from '@app/common/modules/space/repositories/subspace.repository';
import {
SpaceModelEntity,
SubspaceModelEntity,
SubspaceModelProductAllocationRepoitory,
TagModel,
} from '@app/common/modules/space-model';
import { DataSource, QueryRunner } from 'typeorm';
import { DataSource, In, QueryRunner } from 'typeorm';
import { SubSpaceService } from 'src/space/services';
import { TagService } from 'src/space/services/tag';
import { TagModelService } from '../services';
import { UpdatedSubspaceModelPayload } from '../interfaces';
import {
ISingleSubspaceModel,
UpdatedSubspaceModelPayload,
} from '../interfaces';
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
import { ModifyAction } from '@app/common/constants/modify-action.enum';
import { ModifySubspaceDto } from 'src/space/dtos';
@CommandHandler(PropogateUpdateSpaceModelCommand)
export class PropogateUpdateSpaceModelHandler
@ -25,89 +33,122 @@ export class PropogateUpdateSpaceModelHandler
private readonly dataSource: DataSource,
private readonly subSpaceService: SubSpaceService,
private readonly tagService: TagService,
private readonly tagModelService: TagModelService,
private readonly subspaceModelProductRepository: SubspaceModelProductAllocationRepoitory,
private readonly subspaceProductRepository: SubspaceProductAllocationRepository,
private readonly spaceProductRepository: SpaceProductAllocationRepository,
) {}
async execute(command: PropogateUpdateSpaceModelCommand): Promise<void> {
const { spaceModel, modifiedSpaceModels, queryRunner } = command.param;
const { subspaceModels, spaces } = command.param;
try {
const spaces = await queryRunner.manager.find(SpaceEntity, {
where: { spaceModel },
});
if (!subspaceModels || subspaceModels.length === 0) return;
if (!spaces || spaces.length === 0) return;
const { modifiedSubspaceModels = {}, modifiedTags = {} } =
modifiedSpaceModels;
const {
addedSubspaceModels = [],
updatedSubspaceModels = [],
deletedSubspaceModels = [],
} = modifiedSubspaceModels;
const { added = [], updated = [], deleted = [] } = modifiedTags;
if (addedSubspaceModels.length > 0) {
await this.addSubspaceModels(
modifiedSpaceModels.modifiedSubspaceModels.addedSubspaceModels,
spaces,
queryRunner,
);
} else if (updatedSubspaceModels.length > 0) {
await this.updateSubspaceModels(
modifiedSpaceModels.modifiedSubspaceModels.updatedSubspaceModels,
queryRunner,
);
for (const subspaceModel of subspaceModels) {
if (subspaceModel.action === ModifyAction.ADD) {
await this.addSubspaceModel(subspaceModel, spaces);
} else if (subspaceModel.action === ModifyAction.DELETE) {
await this.deleteSubspaceModel(subspaceModel, spaces);
}
if (deletedSubspaceModels.length > 0) {
const dtos: ModifySubspaceDto[] =
modifiedSpaceModels.modifiedSubspaceModels.deletedSubspaceModels.map(
(model) => ({
action: ModifyAction.DELETE,
uuid: model,
}),
);
await this.subSpaceService.modifySubSpace(dtos, queryRunner);
}
if (added.length > 0) {
await this.createTags(
modifiedSpaceModels.modifiedTags.added,
queryRunner,
null,
spaceModel,
);
}
if (updated.length > 0) {
await this.updateTags(
modifiedSpaceModels.modifiedTags.updated,
queryRunner,
);
}
if (deleted.length > 0) {
await this.deleteTags(
modifiedSpaceModels.modifiedTags.deleted,
queryRunner,
);
}
} catch (error) {
console.error(error);
}
}
async addSubspaceModels(
subspaceModels: SubspaceModelEntity[],
async addSubspaceModel(
subspaceModel: ISingleSubspaceModel,
spaces: SpaceEntity[],
queryRunner: QueryRunner,
) {
const subspaceModelAllocations =
await this.subspaceModelProductRepository.find({
where: {
subspaceModel: {
uuid: subspaceModel.subspaceModel.uuid,
},
},
relations: ['tags', 'product'],
});
for (const space of spaces) {
await this.subSpaceService.createSubSpaceFromModel(
subspaceModels,
space,
queryRunner,
);
const subspace = this.subspaceRepository.create({
subspaceName: subspaceModel.subspaceModel.subspaceName,
space: space,
});
subspace.subSpaceModel = subspaceModel.subspaceModel;
await this.subspaceRepository.save(subspace);
if (subspaceModelAllocations?.length > 0) {
for (const allocation of subspaceModelAllocations) {
const subspaceAllocation = this.subspaceProductRepository.create({
subspace: subspace,
product: allocation.product,
tags: allocation.tags,
inheritedFromModel: allocation,
});
await this.subspaceProductRepository.save(subspaceAllocation);
}
}
}
}
async deleteSubspaceModel(
subspaceModel: ISingleSubspaceModel,
spaces: SpaceEntity[],
) {
const subspaces = await this.subspaceRepository.find({
where: {
subSpaceModel: { uuid: subspaceModel.subspaceModel.uuid },
disabled: false,
},
relations: [
'productAllocations',
'productAllocations.product',
'productAllocations.tags',
],
});
if (!subspaces.length) return;
const allocationUuidsToRemove = subspaces.flatMap((subspace) =>
subspace.productAllocations.map((allocation) => allocation.uuid),
);
if (allocationUuidsToRemove.length) {
await this.subspaceProductRepository.delete(allocationUuidsToRemove);
}
await this.subspaceRepository.update(
{ uuid: In(subspaces.map((s) => s.uuid)) },
{ disabled: true },
);
const relocatedAllocations = subspaceModel.relocatedAllocations || [];
if (!relocatedAllocations.length) return;
for (const space of spaces) {
for (const { allocation, tags = [] } of relocatedAllocations) {
const spaceAllocation = await this.spaceProductRepository.findOne({
where: {
inheritedFromModel: { uuid: allocation.uuid },
space: { uuid: space.uuid },
},
relations: ['tags'],
});
if (spaceAllocation) {
if (tags.length) {
spaceAllocation.tags.push(...tags);
await this.spaceProductRepository.save(spaceAllocation);
}
} else {
const newSpaceAllocation = this.spaceProductRepository.create({
space,
inheritedFromModel: allocation,
tags: allocation.tags,
product: allocation.product,
});
await this.spaceProductRepository.save(newSpaceAllocation);
}
}
}
}

View File

@ -4,7 +4,6 @@ import { Logger } from '@nestjs/common';
import { PropogateDeleteSpaceModelCommand } from '../commands';
import { SpaceRepository } from '@app/common/modules/space';
import { SpaceService } from '../../space/services/space.service';
import { DataSource } from 'typeorm';
@CommandHandler(PropogateDeleteSpaceModelCommand)
export class PropogateDeleteSpaceModelHandler
@ -15,15 +14,12 @@ export class PropogateDeleteSpaceModelHandler
constructor(
private readonly spaceRepository: SpaceRepository,
private readonly spaceService: SpaceService,
private readonly dataSource: DataSource,
) {}
async execute(command: PropogateDeleteSpaceModelCommand): Promise<void> {
const { spaceModel } = command.param;
const queryRunner = this.dataSource.createQueryRunner();
const { spaceModel, queryRunner } = command.param;
try {
await queryRunner.connect();
await queryRunner.startTransaction();
const spaces = await this.spaceRepository.find({
where: {
@ -31,7 +27,11 @@ export class PropogateDeleteSpaceModelHandler
uuid: spaceModel.uuid,
},
},
relations: ['subspaces', 'tags', 'subspaces.tags'],
relations: [
'subspaces',
'productAllocations',
'subspaces.productAllocations',
],
});
for (const space of spaces) {
@ -44,6 +44,7 @@ export class PropogateDeleteSpaceModelHandler
);
}
}
await queryRunner.commitTransaction();
} catch (error) {
await queryRunner.rollbackTransaction();
this.logger.error(

View File

@ -1,2 +1,3 @@
export * from './update-subspace.interface';
export * from './modify-subspace.interface';
export * from './single-subspace.interface';

View File

@ -0,0 +1,18 @@
import {
SpaceModelProductAllocationEntity,
SubspaceModelEntity,
} from '@app/common/modules/space-model';
import { ModifyTagModelDto } from '../dtos';
import { ModifyAction } from '@app/common/constants/modify-action.enum';
import { NewTagEntity } from '@app/common/modules/tag';
export interface IRelocatedAllocation {
allocation: SpaceModelProductAllocationEntity;
tags?: NewTagEntity[];
}
export interface ISingleSubspaceModel {
subspaceModel: SubspaceModelEntity;
action: ModifyAction;
tags?: ModifyTagModelDto[];
relocatedAllocations?: IRelocatedAllocation[];
}

View File

@ -0,0 +1,32 @@
import { SubspaceModelProductAllocationEntity } from '@app/common/modules/space-model';
import { NewTagEntity } from '@app/common/modules/tag';
export type IUpdatedAllocations = {
allocation?: SubspaceModelProductAllocationEntity;
tagsRemoved?: NewTagEntity[];
tagsAdded?: NewTagEntity[];
newAllocation?: SubspaceModelProductAllocationEntity;
deletedAllocation?: SubspaceModelProductAllocationEntity;
};
export interface ISubspaceProductAllocationUpdateResult {
createdAllocations: ICreatedAllocation[];
updatedAllocations: IUpdatedAllocation[];
deletedAllocations: IDeletedAllocation[];
}
export interface ICreatedAllocation {
allocation: SubspaceModelProductAllocationEntity;
tags: NewTagEntity[];
}
export interface IUpdatedAllocation {
allocation: SubspaceModelProductAllocationEntity;
tagsAdded: NewTagEntity[];
tagsRemoved: NewTagEntity[];
}
export interface IDeletedAllocation {
allocation: SubspaceModelProductAllocationEntity;
tagsRemoved: NewTagEntity[];
}

View File

@ -1,3 +1,2 @@
export * from './space-model.service';
export * from './subspace';
export * from './tag-model.service';

View File

@ -0,0 +1,430 @@
import { In, QueryRunner } from 'typeorm';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import {
SpaceModelEntity,
SpaceModelProductAllocationEntity,
SpaceModelProductAllocationRepoitory,
SubspaceModelProductAllocationEntity,
} from '@app/common/modules/space-model';
import { TagService as NewTagService } from 'src/tags/services';
import { ProcessTagDto } from 'src/tags/dtos';
import { ModifySubspaceModelDto, ModifyTagModelDto } from '../dtos';
import { ModifyAction } from '@app/common/constants/modify-action.enum';
import { NewTagEntity } from '@app/common/modules/tag';
import { ProductEntity } from '@app/common/modules/product/entities';
import { SpaceRepository } from '@app/common/modules/space';
import { ProjectEntity } from '@app/common/modules/project/entities';
@Injectable()
export class SpaceModelProductAllocationService {
constructor(
private readonly tagService: NewTagService,
private readonly spaceModelProductAllocationRepository: SpaceModelProductAllocationRepoitory,
private readonly spaceRepository: SpaceRepository,
) {}
async createProductAllocations(
projectUuid: string,
spaceModel: SpaceModelEntity,
tags: ProcessTagDto[],
queryRunner?: QueryRunner,
modifySubspaceModels?: ModifySubspaceModelDto[],
): Promise<SpaceModelProductAllocationEntity[]> {
try {
if (!tags.length) return [];
const processedTags = await this.tagService.processTags(
tags,
projectUuid,
queryRunner,
);
const productAllocations: SpaceModelProductAllocationEntity[] = [];
const existingAllocations = new Map<
string,
SpaceModelProductAllocationEntity
>();
for (const tag of processedTags) {
let isTagNeeded = true;
if (modifySubspaceModels) {
const relatedSubspaces = await queryRunner.manager.find(
SubspaceModelProductAllocationEntity,
{
where: {
product: { uuid: tag.product.uuid },
subspaceModel: { spaceModel: { uuid: spaceModel.uuid } },
tags: { uuid: tag.uuid },
},
relations: ['subspaceModel', 'tags'],
},
);
for (const subspaceWithTag of relatedSubspaces) {
const modifyingSubspace = modifySubspaceModels.find(
(subspace) =>
subspace.action === ModifyAction.UPDATE &&
subspace.uuid === subspaceWithTag.subspaceModel.uuid,
);
if (
modifyingSubspace &&
modifyingSubspace.tags &&
modifyingSubspace.tags.some(
(subspaceTag) =>
subspaceTag.action === ModifyAction.DELETE &&
subspaceTag.tagUuid === tag.uuid,
)
) {
isTagNeeded = true;
break;
}
}
}
if (isTagNeeded) {
const hasTags = await this.validateTagWithinSpaceModel(
queryRunner,
tag,
spaceModel,
);
if (hasTags) continue;
let allocation = existingAllocations.get(tag.product.uuid);
if (!allocation) {
allocation = await this.getAllocationByProduct(
tag.product,
spaceModel,
queryRunner,
);
if (allocation) {
existingAllocations.set(tag.product.uuid, allocation);
}
}
if (!allocation) {
allocation = this.createNewAllocation(spaceModel, 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);
}
return productAllocations;
} catch (error) {
throw this.handleError(error, 'Failed to create product allocations');
}
}
async updateProductAllocations(
dtos: ModifyTagModelDto[],
project: ProjectEntity,
spaceModel: SpaceModelEntity,
queryRunner: QueryRunner,
modifySubspaceModels?: ModifySubspaceModelDto[],
): Promise<void> {
try {
const addDtos = dtos.filter((dto) => dto.action === ModifyAction.ADD);
const deleteDtos = dtos.filter(
(dto) => dto.action === ModifyAction.DELETE,
);
const addTagDtos: ProcessTagDto[] = addDtos.map((dto) => ({
name: dto.name,
productUuid: dto.productUuid,
uuid: dto.newTagUuid,
}));
const processedTags = await this.tagService.processTags(
addTagDtos,
project.uuid,
queryRunner,
);
const addTagUuidMap = new Map<string, ModifyTagModelDto>();
processedTags.forEach((tag, index) => {
addTagUuidMap.set(tag.uuid, addDtos[index]);
});
const addTagUuids = new Set(processedTags.map((tag) => tag.uuid));
const deleteTagUuids = new Set(deleteDtos.map((dto) => dto.tagUuid));
const tagsToIgnore = new Set(
[...addTagUuids].filter((uuid) => deleteTagUuids.has(uuid)),
);
const filteredDtos = dtos.filter(
(dto) =>
!(
tagsToIgnore.has(dto.tagUuid) ||
(dto.action === ModifyAction.ADD &&
tagsToIgnore.has(
[...addTagUuidMap.keys()].find(
(uuid) => addTagUuidMap.get(uuid) === dto,
),
))
),
);
await Promise.all([
this.processAddActions(
filteredDtos,
project.uuid,
spaceModel,
queryRunner,
modifySubspaceModels,
),
this.processDeleteActions(filteredDtos, queryRunner, spaceModel),
]);
} catch (error) {
throw this.handleError(error, 'Error while updating product allocations');
}
}
private async processAddActions(
dtos: ModifyTagModelDto[],
projectUuid: string,
spaceModel: SpaceModelEntity,
queryRunner: QueryRunner,
modifySubspaceModels?: ModifySubspaceModelDto[],
): 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) {
await this.createProductAllocations(
projectUuid,
spaceModel,
addDtos,
queryRunner,
modifySubspaceModels,
);
}
}
private createNewAllocation(
spaceModel: SpaceModelEntity,
tag: NewTagEntity,
queryRunner?: QueryRunner,
): SpaceModelProductAllocationEntity {
return queryRunner
? queryRunner.manager.create(SpaceModelProductAllocationEntity, {
spaceModel,
product: tag.product,
tags: [tag],
})
: this.spaceModelProductAllocationRepository.create({
spaceModel,
product: tag.product,
tags: [tag],
});
}
private async getAllocationByProduct(
product: ProductEntity,
spaceModel: SpaceModelEntity,
queryRunner?: QueryRunner,
): Promise<SpaceModelProductAllocationEntity | null> {
return queryRunner
? queryRunner.manager.findOne(SpaceModelProductAllocationEntity, {
where: {
spaceModel: { uuid: spaceModel.uuid },
product: { uuid: product.uuid },
},
relations: ['tags'],
})
: this.spaceModelProductAllocationRepository.findOne({
where: {
spaceModel: { uuid: spaceModel.uuid },
product: { uuid: product.uuid },
},
relations: ['tags'],
});
}
private async saveAllocation(
allocation: SpaceModelProductAllocationEntity,
queryRunner?: QueryRunner,
) {
queryRunner
? await queryRunner.manager.save(
SpaceModelProductAllocationEntity,
allocation,
)
: await this.spaceModelProductAllocationRepository.save(allocation);
}
private async saveAllocations(
allocations: SpaceModelProductAllocationEntity[],
queryRunner?: QueryRunner,
) {
queryRunner
? await queryRunner.manager.save(
SpaceModelProductAllocationEntity,
allocations,
)
: await this.spaceModelProductAllocationRepository.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,
);
}
private async processDeleteActions(
dtos: ModifyTagModelDto[],
queryRunner: QueryRunner,
spaceModel: SpaceModelEntity,
): Promise<SpaceModelProductAllocationEntity[]> {
try {
if (!dtos || dtos.length === 0) {
return;
}
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(
SpaceModelProductAllocationEntity,
{
where: {
tags: { uuid: In(tagUuidsToDelete) },
spaceModel: {
uuid: spaceModel.uuid,
},
},
relations: [
'tags',
'inheritedSpaceAllocations',
'inheritedSpaceAllocations.tags',
],
},
);
if (!allocationsToUpdate || allocationsToUpdate.length === 0) return [];
const deletedAllocations: SpaceModelProductAllocationEntity[] = [];
const allocationUpdates: SpaceModelProductAllocationEntity[] = [];
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(
SpaceModelProductAllocationEntity,
allocationUpdates,
);
}
if (deletedAllocations.length > 0) {
await queryRunner.manager.remove(
SpaceModelProductAllocationEntity,
deletedAllocations,
);
}
await queryRunner.manager
.createQueryBuilder()
.delete()
.from('space_model_product_tags')
.where(
'space_model_product_allocation_uuid NOT IN (' +
queryRunner.manager
.createQueryBuilder()
.select('allocation.uuid')
.from(SpaceModelProductAllocationEntity, 'allocation')
.getQuery() +
')',
)
.execute();
return deletedAllocations;
} catch (error) {
throw this.handleError(error, `Failed to delete tags in space model`);
}
}
private async validateTagWithinSpaceModel(
queryRunner: QueryRunner,
tag: NewTagEntity,
spaceModel: SpaceModelEntity,
): Promise<boolean> {
const existingAllocationsForProduct = await queryRunner.manager.find(
SpaceModelProductAllocationEntity,
{
where: {
spaceModel: {
uuid: spaceModel.uuid,
},
product: {
uuid: tag.product.uuid,
},
},
relations: ['tags'],
},
);
const existingTagsForProduct = existingAllocationsForProduct.flatMap(
(allocation) => allocation.tags,
);
const isDuplicateTag = existingTagsForProduct.some(
(existingTag) => existingTag.uuid === tag.uuid,
);
if (isDuplicateTag) {
return true;
}
return false;
}
async clearAllAllocations(spaceModelUuid: string, queryRunner: QueryRunner) {
try {
await queryRunner.manager
.createQueryBuilder()
.delete()
.from(SpaceModelProductAllocationEntity)
.where('space_model_uuid = :spaceModelUuid', { spaceModelUuid })
.execute();
} catch (error) {
throw this.handleError(
error,
`Failed to clear all allocations in the space model product allocation`,
);
}
}
}

View File

@ -1,30 +1,53 @@
import {
SpaceModelEntity,
SpaceModelProductAllocationEntity,
SpaceModelRepository,
SubspaceModelProductAllocationEntity,
} from '@app/common/modules/space-model';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { CreateSpaceModelDto, UpdateSpaceModelDto } from '../dtos';
import {
CreateSpaceModelDto,
LinkSpacesToModelDto,
UpdateSpaceModelDto,
} from '../dtos';
import { ProjectParam } from 'src/community/dtos';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { SubSpaceModelService } from './subspace/subspace-model.service';
import { DataSource, QueryRunner } from 'typeorm';
import { DataSource, In, QueryRunner, SelectQueryBuilder } from 'typeorm';
import {
TypeORMCustomModel,
TypeORMCustomModelFindAllQuery,
} from '@app/common/models/typeOrmCustom.model';
import { PageResponse } from '@app/common/dto/pagination.response.dto';
import { SpaceModelParam } from '../dtos/space-model-param';
import { ProjectService } from 'src/project/services';
import { ProjectEntity } from '@app/common/modules/project/entities';
import { TagModelService } from './tag-model.service';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { CommandBus } from '@nestjs/cqrs';
import { PropogateUpdateSpaceModelCommand } from '../commands';
import { ProcessTagDto } from 'src/tags/dtos';
import { SpaceModelProductAllocationService } from './space-model-product-allocation.service';
import {
ModifiedTagsModelPayload,
ModifySubspaceModelPayload,
} from '../interfaces';
import { SpaceModelDto } from '@app/common/modules/space-model/dtos';
PropogateDeleteSpaceModelCommand,
PropogateUpdateSpaceModelCommand,
} from '../commands';
import {
SpaceProductAllocationRepository,
SpaceRepository,
} from '@app/common/modules/space';
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
import {
SubspaceProductAllocationRepository,
SubspaceRepository,
} from '@app/common/modules/space/repositories/subspace.repository';
import {
ORPHAN_COMMUNITY_NAME,
ORPHAN_SPACE_NAME,
} from '@app/common/constants/orphan-constant';
import { DeviceRepository } from '@app/common/modules/device/repositories';
import { SpaceProductAllocationEntity } from '@app/common/modules/space/entities/space-product-allocation.entity';
import { SubspaceEntity } from '@app/common/modules/space/entities/subspace/subspace.entity';
import { SubspaceProductAllocationEntity } from '@app/common/modules/space/entities/subspace/subspace-product-allocation.entity';
import { DeviceEntity } from '@app/common/modules/device/entities';
import { ISingleSubspaceModel } from '../interfaces';
@Injectable()
export class SpaceModelService {
@ -33,8 +56,13 @@ export class SpaceModelService {
private readonly spaceModelRepository: SpaceModelRepository,
private readonly projectService: ProjectService,
private readonly subSpaceModelService: SubSpaceModelService,
private readonly tagModelService: TagModelService,
private commandBus: CommandBus,
private readonly spaceModelProductAllocationService: SpaceModelProductAllocationService,
private readonly spaceRepository: SpaceRepository,
private readonly spaceProductAllocationRepository: SpaceProductAllocationRepository,
private readonly subspaceRepository: SubspaceRepository,
private readonly subspaceProductAllocationRepository: SubspaceProductAllocationRepository,
private readonly deviceRepository: DeviceRepository,
) {}
async createSpaceModel(
@ -43,11 +71,15 @@ export class SpaceModelService {
): Promise<BaseResponseDto> {
const { modelName, subspaceModels, tags } = createSpaceModelDto;
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const project = await this.validateProject(params.projectUuid);
const project = await this.projectService.findOne(params.projectUuid);
if (!project) {
throw new HttpException('Project not found', HttpStatus.NOT_FOUND);
}
await this.validateNameUsingQueryRunner(
modelName,
@ -62,22 +94,27 @@ export class SpaceModelService {
const savedSpaceModel = await queryRunner.manager.save(spaceModel);
const subspaceTags =
this.subSpaceModelService.extractTagsFromSubspaceModels(subspaceModels);
const allTags = [...tags, ...subspaceTags];
this.validateUniqueTags(allTags);
if (subspaceModels?.length) {
savedSpaceModel.subspaceModels =
await this.subSpaceModelService.createSubSpaceModels(
subspaceModels,
await this.subSpaceModelService.createModels(
savedSpaceModel,
subspaceModels,
queryRunner,
tags,
);
}
if (tags?.length) {
savedSpaceModel.tags = await this.tagModelService.createTags(
await this.spaceModelProductAllocationService.createProductAllocations(
params.projectUuid,
spaceModel,
tags,
queryRunner,
savedSpaceModel,
null,
);
}
@ -94,7 +131,7 @@ export class SpaceModelService {
const errorMessage =
error instanceof HttpException
? error.message
: 'An unexpected error occurred';
: `An unexpected error occurred: ${error.message}`;
const statusCode =
error instanceof HttpException
? error.getStatus()
@ -119,34 +156,9 @@ export class SpaceModelService {
disabled: false,
};
pageable.include =
'subspaceModels,tags,subspaceModels.tags,tags.product,subspaceModels.tags.product';
'subspaceModels.productAllocations,subspaceModelProductAllocations.tags,subspaceModels, productAllocations, productAllocations.tags';
const queryBuilder = await this.spaceModelRepository
.createQueryBuilder('spaceModel')
.leftJoinAndSelect(
'spaceModel.subspaceModels',
'subspaceModels',
'subspaceModels.disabled = :subspaceDisabled',
{ subspaceDisabled: false },
)
.leftJoinAndSelect(
'spaceModel.tags',
'tags',
'tags.disabled = :tagsDisabled',
{ tagsDisabled: false },
)
.leftJoinAndSelect('tags.product', 'spaceTagproduct')
.leftJoinAndSelect(
'subspaceModels.tags',
'subspaceModelTags',
'subspaceModelTags.disabled = :subspaceModelTagsDisabled',
{ subspaceModelTagsDisabled: false },
)
.leftJoinAndSelect('subspaceModelTags.product', 'subspaceTagproduct')
.where('spaceModel.disabled = :disabled', { disabled: false })
.andWhere('spaceModel.project = :projectUuid', {
projectUuid: param.projectUuid,
});
const queryBuilder = this.buildSpaceModelQuery(param.projectUuid);
const customModel = TypeORMCustomModel(this.spaceModelRepository);
const { baseResponseDto, paginationResponseDto } =
@ -159,10 +171,14 @@ export class SpaceModelService {
queryBuilder,
);
return new PageResponse<SpaceModelDto>(
baseResponseDto,
paginationResponseDto,
);
const formattedData = this.transformSpaceModelData(baseResponseDto.data);
return {
code: 200,
data: formattedData,
message: 'Success get list spaceModel',
...paginationResponseDto,
};
} catch (error) {
throw new HttpException(
`Error fetching paginated list: ${error.message}`,
@ -177,14 +193,16 @@ export class SpaceModelService {
async update(dto: UpdateSpaceModelDto, param: SpaceModelParam) {
const queryRunner = this.dataSource.createQueryRunner();
await this.validateProject(param.projectUuid);
const spaceModel = await this.validateSpaceModel(param.spaceModelUuid);
const project = await this.validateProject(param.projectUuid);
const spaceModel = await this.validateSpaceModel(
param.spaceModelUuid,
param.projectUuid,
);
await queryRunner.connect();
let modifiedSubspaceModels: ModifySubspaceModelPayload = {};
let modifiedTagsModelPayload: ModifiedTagsModelPayload = {};
let modifiedSubspaces: ISingleSubspaceModel[] = [];
try {
await queryRunner.startTransaction();
const spaces = await this.fetchModelSpaces(spaceModel);
const { modelName } = dto;
if (modelName) {
@ -200,44 +218,33 @@ export class SpaceModelService {
);
}
const spaceTagsAfterMove = this.tagModelService.getSubspaceTagsToBeAdded(
dto.tags,
dto.subspaceModels,
);
const modifiedSubspaces = this.tagModelService.getModifiedSubspaces(
dto.tags,
dto.subspaceModels,
);
if (dto.subspaceModels) {
modifiedSubspaceModels =
await this.subSpaceModelService.modifySubSpaceModels(
modifiedSubspaces,
modifiedSubspaces =
await this.subSpaceModelService.modifySubspaceModels(
dto.subspaceModels,
spaceModel,
queryRunner,
param.projectUuid,
dto.tags,
);
}
if (dto.tags) {
modifiedTagsModelPayload = await this.tagModelService.modifyTags(
spaceTagsAfterMove,
queryRunner,
await this.spaceModelProductAllocationService.updateProductAllocations(
dto.tags,
project,
spaceModel,
null,
queryRunner,
dto.subspaceModels,
);
}
await queryRunner.commitTransaction();
await this.commandBus.execute(
new PropogateUpdateSpaceModelCommand({
spaceModel: spaceModel,
modifiedSpaceModels: {
modifiedSubspaceModels,
modifiedTags: modifiedTagsModelPayload,
},
queryRunner,
subspaceModels: modifiedSubspaces,
spaces: spaces,
}),
);
@ -266,25 +273,27 @@ export class SpaceModelService {
try {
await this.validateProject(param.projectUuid);
const spaceModel = await this.validateSpaceModel(param.spaceModelUuid);
const spaceModel = await this.validateSpaceModel(
param.spaceModelUuid,
param.projectUuid,
);
if (spaceModel.subspaceModels?.length) {
const deleteSubspaceDtos = spaceModel.subspaceModels.map(
(subspace) => ({
subspaceUuid: subspace.uuid,
}),
const deleteSubspaceUuids = spaceModel.subspaceModels.map(
(subspace) => subspace.uuid,
);
await this.subSpaceModelService.deleteSubspaceModels(
deleteSubspaceDtos,
await this.subSpaceModelService.clearModels(
deleteSubspaceUuids,
queryRunner,
);
}
if (spaceModel.tags?.length) {
const deleteSpaceTagsDtos = spaceModel.tags.map((tag) => tag.uuid);
await this.tagModelService.deleteTags(deleteSpaceTagsDtos, queryRunner);
if (spaceModel.productAllocations?.length) {
await this.spaceModelProductAllocationService.clearAllAllocations(
spaceModel.uuid,
queryRunner,
);
}
await queryRunner.manager.update(
@ -292,9 +301,15 @@ export class SpaceModelService {
{ uuid: param.spaceModelUuid },
{ disabled: true },
);
await queryRunner.commitTransaction();
await this.commandBus.execute(
new PropogateDeleteSpaceModelCommand({
spaceModel: spaceModel,
queryRunner,
}),
);
return new SuccessResponseDto({
message: `SpaceModel with UUID ${param.spaceModelUuid} deleted successfully.`,
statusCode: HttpStatus.OK,
@ -317,6 +332,268 @@ export class SpaceModelService {
}
}
async fetchModelSpaces(
spaceModel: SpaceModelEntity,
queryRunner?: QueryRunner,
) {
const spaces = await (queryRunner
? queryRunner.manager.find(SpaceEntity, {
where: {
spaceModel: {
uuid: spaceModel.uuid,
},
disabled: false,
},
})
: this.spaceRepository.find({
where: {
spaceModel: {
uuid: spaceModel.uuid,
},
disabled: false,
},
}));
return spaces;
}
async linkSpaceModel(
params: SpaceModelParam,
dto: LinkSpacesToModelDto,
): Promise<BaseResponseDto> {
const project = await this.validateProject(params.projectUuid);
try {
const spaceModel = await this.spaceModelRepository.findOne({
where: { uuid: params.spaceModelUuid },
relations: [
'productAllocations',
'subspaceModels',
'productAllocations.product',
'productAllocations.tags',
'subspaceModels.productAllocations',
'subspaceModels.productAllocations.product',
'subspaceModels.productAllocations.tags',
],
});
if (!spaceModel) {
throw new HttpException(
`Space Model with UUID ${params.spaceModelUuid} not found`,
HttpStatus.NOT_FOUND,
);
}
const spaces = await this.spaceRepository.find({
where: { uuid: In(dto.spaceUuids), disabled: false },
relations: [
'spaceModel',
'devices',
'subspaces',
'productAllocations',
'productAllocations.product',
'productAllocations.tags',
'subspaces.productAllocations',
'subspaces.productAllocations.product',
'subspaces.productAllocations.product.tags',
'community',
],
});
if (!spaces.length) {
throw new HttpException(
`No spaces found for the given UUIDs`,
HttpStatus.NOT_FOUND,
);
}
await Promise.all(
spaces.map(async (space) => {
const hasDependencies =
space.devices.length > 0 ||
space.subspaces.length > 0 ||
space.productAllocations.length > 0;
if (!hasDependencies && !space.spaceModel) {
await this.linkToSpace(space, spaceModel);
} else if (dto.overwrite) {
await this.overwriteSpace(space, project);
await this.linkToSpace(space, spaceModel);
}
}),
);
return new SuccessResponseDto({
message: 'Spaces linked successfully',
data: dto.spaceUuids,
});
} catch (error) {
throw new HttpException(
`Failed to link space model: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async linkToSpace(
space: SpaceEntity,
spaceModel: SpaceModelEntity,
queryRunner?: QueryRunner, // Make queryRunner optional
): Promise<void> {
try {
space.spaceModel = spaceModel;
if (queryRunner) {
await queryRunner.manager.save(SpaceEntity, space);
} else {
await this.spaceRepository.save(space);
}
const spaceProductAllocations = spaceModel.productAllocations.map(
(modelAllocation) =>
this.spaceProductAllocationRepository.create({
space,
inheritedFromModel: modelAllocation,
product: modelAllocation.product,
tags: modelAllocation.tags,
}),
);
if (queryRunner) {
await queryRunner.manager.save(
SpaceProductAllocationEntity,
spaceProductAllocations,
);
} else {
await this.spaceProductAllocationRepository.save(
spaceProductAllocations,
);
}
await Promise.all(
spaceModel.subspaceModels.map(async (subspaceModel) => {
const subspace = this.subspaceRepository.create({
subspaceName: subspaceModel.subspaceName,
subSpaceModel: subspaceModel,
space: space,
});
if (queryRunner) {
await queryRunner.manager.save(SubspaceEntity, subspace);
} else {
await this.subspaceRepository.save(subspace);
}
const subspaceAllocations = subspaceModel.productAllocations.map(
(modelAllocation) =>
this.subspaceProductAllocationRepository.create({
subspace,
inheritedFromModel: modelAllocation,
product: modelAllocation.product,
tags: modelAllocation.tags,
}),
);
if (subspaceAllocations.length) {
if (queryRunner) {
await queryRunner.manager.save(
SubspaceProductAllocationEntity,
subspaceAllocations,
);
} else {
await this.subspaceProductAllocationRepository.save(
subspaceAllocations,
);
}
}
}),
);
} catch (error) {
throw new HttpException(
`Failed to link space ${space.uuid} to space model ${spaceModel.uuid}: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async overwriteSpace(
space: SpaceEntity,
project: ProjectEntity,
queryRunner?: QueryRunner,
): Promise<void> {
try {
const spaceProductAllocationRepository = queryRunner
? queryRunner.manager.getRepository(SpaceProductAllocationEntity)
: this.spaceProductAllocationRepository;
const subspaceRepository = queryRunner
? queryRunner.manager.getRepository(SubspaceEntity)
: this.subspaceRepository;
const subspaceProductAllocationRepository = queryRunner
? queryRunner.manager.getRepository(SubspaceProductAllocationEntity)
: this.subspaceProductAllocationRepository;
const spaceRepository = queryRunner
? queryRunner.manager.getRepository(SpaceEntity)
: this.spaceRepository;
const deviceRepository = queryRunner
? queryRunner.manager.getRepository(DeviceEntity)
: this.deviceRepository;
if (space.productAllocations.length) {
await spaceProductAllocationRepository.delete({
uuid: In(
space.productAllocations.map((allocation) => allocation.uuid),
),
});
}
await Promise.all(
space.subspaces.map(async (subspace) => {
await subspaceRepository.update(
{ uuid: subspace.uuid },
{ disabled: true },
);
if (subspace.productAllocations.length) {
await subspaceProductAllocationRepository.delete({
uuid: In(
subspace.productAllocations.map(
(allocation) => allocation.uuid,
),
),
});
}
}),
);
if (space.devices.length > 0) {
const orphanSpace = await spaceRepository.findOne({
where: {
community: {
name: `${ORPHAN_COMMUNITY_NAME}-${project.name}`,
},
spaceName: ORPHAN_SPACE_NAME,
},
});
if (!orphanSpace) {
throw new HttpException(
`Orphan space not found in community ${project.name}`,
HttpStatus.NOT_FOUND,
);
}
await deviceRepository.update(
{ uuid: In(space.devices.map((device) => device.uuid)) },
{ spaceDevice: orphanSpace },
);
}
} catch (error) {
throw new Error(
`Failed to overwrite space ${space.uuid}: ${error.message}`,
);
}
}
async validateName(modelName: string, projectUuid: string): Promise<void> {
const isModelExist = await this.spaceModelRepository.findOne({
where: { modelName, project: { uuid: projectUuid }, disabled: false },
@ -350,11 +627,15 @@ export class SpaceModelService {
async findOne(params: SpaceModelParam): Promise<BaseResponseDto> {
try {
await this.validateProject(params.projectUuid);
const spaceModel = await this.validateSpaceModel(params.spaceModelUuid);
const spaceModel = await this.validateSpaceModel(
params.spaceModelUuid,
params.projectUuid,
);
const response = this.formatSpaceModelResponse(spaceModel);
return new SuccessResponseDto({
message: 'SpaceModel retrieved successfully',
data: spaceModel,
data: response,
});
} catch (error) {
throw new HttpException(
@ -364,8 +645,57 @@ export class SpaceModelService {
}
}
async validateSpaceModel(uuid: string): Promise<SpaceModelEntity> {
const spaceModel = await this.spaceModelRepository
async validateSpaceModel(
uuid: string,
projectUuid?: string,
): Promise<SpaceModelEntity> {
const query = this.buildSpaceModelQuery(projectUuid);
const result = await query
.andWhere('spaceModel.uuid = :uuid', { uuid })
.getOne();
if (!result) {
throw new HttpException('space model not found', HttpStatus.NOT_FOUND);
}
return result;
}
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 {
if (!tag.name || !tag.productUuid) {
throw new HttpException(
`Tag name and product should not be null.`,
HttpStatus.BAD_REQUEST,
);
}
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);
}
}
}
private buildSpaceModelQuery(
projectUuid: string,
): SelectQueryBuilder<SpaceModelEntity> {
return this.spaceModelRepository
.createQueryBuilder('spaceModel')
.leftJoinAndSelect(
'spaceModel.subspaceModels',
@ -374,27 +704,90 @@ export class SpaceModelService {
{ subspaceDisabled: false },
)
.leftJoinAndSelect(
'spaceModel.tags',
'tags',
'tags.disabled = :tagsDisabled',
{ tagsDisabled: false },
'subspaceModels.productAllocations',
'subspaceModelProductAllocations',
)
.leftJoinAndSelect('tags.product', 'spaceTagproduct')
.leftJoinAndSelect(
'subspaceModels.tags',
'subspaceModelProductAllocations.tags',
'subspaceModelTags',
'subspaceModelTags.disabled = :subspaceModelTagsDisabled',
{ subspaceModelTagsDisabled: false },
)
.leftJoinAndSelect('subspaceModelTags.product', 'subspaceTagproduct')
.where('spaceModel.disabled = :disabled', { disabled: false })
.where('spaceModel.disabled = :disabled', { disabled: false })
.andWhere('spaceModel.uuid = :uuid', { uuid })
.getOne();
.leftJoinAndSelect('subspaceModelTags.product', 'subspaceModelTagProduct')
.leftJoinAndSelect('spaceModel.productAllocations', 'productAllocations')
.leftJoinAndSelect('productAllocations.product', 'allocatedProduct')
.leftJoinAndSelect('productAllocations.tags', 'productTags')
.leftJoinAndSelect('productTags.product', 'productTagProduct')
.where('spaceModel.disabled = false')
.andWhere('spaceModel.project = :projectUuid', { projectUuid });
}
if (!spaceModel) {
throw new HttpException('space model not found', HttpStatus.NOT_FOUND);
}
return spaceModel;
private transformSpaceModelData(spaceModelsArray: SpaceModelEntity[]): any[] {
if (!Array.isArray(spaceModelsArray)) return [];
return spaceModelsArray.map((spaceModel) => ({
uuid: spaceModel.uuid,
createdAt: spaceModel.createdAt,
updatedAt: spaceModel.updatedAt,
modelName: spaceModel.modelName,
disabled: spaceModel.disabled,
subspaceModels: (spaceModel.subspaceModels ?? []).map((subspace) => ({
uuid: subspace.uuid,
createdAt: subspace.createdAt,
updatedAt: subspace.updatedAt,
subspaceName: subspace.subspaceName,
disabled: subspace.disabled,
tags: this.extractTags(subspace.productAllocations),
})),
tags: this.extractTags(spaceModel.productAllocations),
}));
}
private extractTags(
productAllocations:
| SpaceModelProductAllocationEntity[]
| SubspaceModelProductAllocationEntity[]
| undefined,
): any[] {
if (!productAllocations) return [];
return productAllocations
.flatMap((allocation) => allocation.tags ?? [])
.map((tag) => ({
uuid: tag.uuid,
createdAt: tag.createdAt,
updatedAt: tag.updatedAt,
name: tag.name,
disabled: tag.disabled,
product: tag.product
? {
uuid: tag.product.uuid,
createdAt: tag.product.createdAt,
updatedAt: tag.product.updatedAt,
catName: tag.product.catName,
prodId: tag.product.prodId,
name: tag.product.name,
prodType: tag.product.prodType,
}
: null,
}));
}
private formatSpaceModelResponse(spaceModel: SpaceModelEntity): any {
return {
uuid: spaceModel.uuid,
createdAt: spaceModel.createdAt,
updatedAt: spaceModel.updatedAt,
modelName: spaceModel.modelName,
disabled: spaceModel.disabled,
subspaceModels:
spaceModel.subspaceModels?.map((subspace) => ({
uuid: subspace.uuid,
createdAt: subspace.createdAt,
updatedAt: subspace.updatedAt,
subspaceName: subspace.subspaceName,
disabled: subspace.disabled,
tags: this.extractTags(subspace.productAllocations),
})) ?? [],
tags: this.extractTags(spaceModel.productAllocations),
};
}
}

View File

@ -0,0 +1,563 @@
import { ModifyAction } from '@app/common/constants/modify-action.enum';
import {
SpaceModelEntity,
SpaceModelProductAllocationEntity,
SpaceModelProductAllocationRepoitory,
SubspaceModelEntity,
SubspaceModelProductAllocationEntity,
SubspaceModelProductAllocationRepoitory,
} from '@app/common/modules/space-model';
import { NewTagEntity } from '@app/common/modules/tag';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { ModifyTagModelDto } from 'src/space-model/dtos';
import { ISingleSubspaceModel } from 'src/space-model/interfaces';
import { IUpdatedAllocations } from 'src/space-model/interfaces/subspace-product-allocation-update-result.interface';
import { ProcessTagDto } from 'src/tags/dtos';
import { TagService as NewTagService } from 'src/tags/services';
import { In, QueryRunner } from 'typeorm';
@Injectable()
export class SubspaceModelProductAllocationService {
constructor(
private readonly tagService: NewTagService,
private readonly subspaceModelProductAllocationRepository: SubspaceModelProductAllocationRepoitory,
private readonly spaceModelAllocationRepository: SpaceModelProductAllocationRepoitory,
) {}
async createProductAllocations(
subspaceModel: SubspaceModelEntity,
spaceModel: SpaceModelEntity,
tags: NewTagEntity[],
queryRunner?: QueryRunner,
spaceAllocationsToExclude?: SpaceModelProductAllocationEntity[],
): Promise<IUpdatedAllocations[]> {
try {
const updatedAllocations: IUpdatedAllocations[] = [];
const allocations: SubspaceModelProductAllocationEntity[] = [];
for (const tag of tags) {
// Step 1: Check if this specific tag is already allocated at the space level
const existingTagInSpaceModel = await (queryRunner
? queryRunner.manager.findOne(SpaceModelProductAllocationEntity, {
where: {
spaceModel: {
uuid: spaceModel.uuid,
}, // Check at the space level
tags: { uuid: tag.uuid }, // Check for the specific tag
},
relations: ['tags'],
})
: this.spaceModelAllocationRepository.findOne({
where: {
spaceModel: {
uuid: spaceModel.uuid,
},
tags: { uuid: tag.uuid },
},
relations: ['tags', 'product'],
}));
const isExcluded = spaceAllocationsToExclude?.some(
(excludedAllocation) =>
excludedAllocation.product.uuid === tag.product.uuid &&
excludedAllocation.tags.some((t) => t.uuid === tag.uuid),
);
// If tag is found at the space level, prevent allocation at the subspace level
if (!isExcluded && existingTagInSpaceModel) {
throw new HttpException(
`Tag ${tag.uuid} (Product: ${tag.product.uuid}) is already allocated at the space level (${subspaceModel.spaceModel.uuid}). Cannot allocate the same tag in a subspace.`,
HttpStatus.BAD_REQUEST,
);
}
// Check if this specific tag is already allocated within another subspace of the same space
const existingTagInSameSpace = await (queryRunner
? queryRunner.manager.findOne(SubspaceModelProductAllocationEntity, {
where: {
product: { uuid: tag.product.uuid },
subspaceModel: { spaceModel: subspaceModel.spaceModel },
tags: { uuid: tag.uuid }, // Ensure the exact tag is checked
},
relations: ['subspaceModel', 'tags'],
})
: this.subspaceModelProductAllocationRepository.findOne({
where: {
product: { uuid: tag.product.uuid },
subspaceModel: { spaceModel: subspaceModel.spaceModel },
tags: { uuid: tag.uuid },
},
relations: ['subspaceModel', 'tags'],
}));
// Prevent duplicate allocation if tag exists in another subspace of the same space
if (
existingTagInSameSpace &&
existingTagInSameSpace.subspaceModel.uuid !== subspaceModel.uuid
) {
throw new HttpException(
`Tag ${tag.uuid} (Product: ${tag.product.uuid}) is already allocated in another subspace (${existingTagInSameSpace.subspaceModel.uuid}) within the same space (${subspaceModel.spaceModel.uuid}).`,
HttpStatus.BAD_REQUEST,
);
}
//Check if there are existing allocations for this product in the subspace
const existingAllocationsForProduct = await (queryRunner
? queryRunner.manager.find(SubspaceModelProductAllocationEntity, {
where: {
subspaceModel: { uuid: subspaceModel.uuid },
product: { uuid: tag.product.uuid },
},
relations: ['tags'],
})
: this.subspaceModelProductAllocationRepository.find({
where: {
subspaceModel: { uuid: subspaceModel.uuid },
product: { uuid: tag.product.uuid },
},
relations: ['tags'],
}));
//Flatten all existing tags for this product in the subspace
const existingTagsForProduct = existingAllocationsForProduct.flatMap(
(allocation) => allocation.tags,
);
// Check if the tag is already assigned to the same product in this subspace
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 subspace (${subspaceModel.uuid}).`,
HttpStatus.BAD_REQUEST,
);
}
// If no existing allocation, create a new one
if (existingAllocationsForProduct.length === 0) {
const allocation = queryRunner
? queryRunner.manager.create(SubspaceModelProductAllocationEntity, {
subspaceModel,
product: tag.product,
tags: [tag],
})
: this.subspaceModelProductAllocationRepository.create({
subspaceModel,
product: tag.product,
tags: [tag],
});
allocations.push(allocation);
} else {
//If allocation exists, add the tag to it
existingAllocationsForProduct[0].tags.push(tag);
if (queryRunner) {
await queryRunner.manager.save(
SubspaceModelProductAllocationEntity,
existingAllocationsForProduct[0],
);
} else {
await this.subspaceModelProductAllocationRepository.save(
existingAllocationsForProduct[0],
);
}
updatedAllocations.push({
allocation: existingAllocationsForProduct[0],
tagsAdded: [tag],
});
}
}
// Save newly created allocations
if (allocations.length > 0) {
if (queryRunner) {
await queryRunner.manager.save(
SubspaceModelProductAllocationEntity,
allocations,
);
} else {
await this.subspaceModelProductAllocationRepository.save(allocations);
}
allocations.forEach((allocation) => {
updatedAllocations.push({ newAllocation: allocation });
});
}
return updatedAllocations;
} catch (error) {
throw new HttpException(
`An unexpected error occurred while creating subspace product allocations ${error}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async processDeleteActions(
dtos: ModifyTagModelDto[],
queryRunner: QueryRunner,
): Promise<SubspaceModelProductAllocationEntity[]> {
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(
SubspaceModelProductAllocationEntity,
{
where: { tags: { uuid: In(tagUuidsToDelete) } },
relations: ['tags'],
},
);
if (!allocationsToUpdate || allocationsToUpdate.length === 0) return [];
const deletedAllocations: SubspaceModelProductAllocationEntity[] = [];
const allocationUpdates: SubspaceModelProductAllocationEntity[] = [];
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(
SubspaceModelProductAllocationEntity,
allocationUpdates,
);
}
if (deletedAllocations.length > 0) {
await queryRunner.manager.remove(
SubspaceModelProductAllocationEntity,
deletedAllocations,
);
}
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();
return deletedAllocations;
} catch (error) {
throw this.handleError(error, `Failed to delete tags in subspace model`);
}
}
async updateAllocations(
subspaceModels: ISingleSubspaceModel[],
projectUuid: string,
queryRunner: QueryRunner,
spaceModel: SpaceModelEntity,
spaceTagUpdateDtos?: ModifyTagModelDto[],
) {
const spaceAllocationToExclude: SpaceModelProductAllocationEntity[] = [];
const updatedAllocations: IUpdatedAllocations[] = [];
for (const subspaceModel of subspaceModels) {
const tagDtos = subspaceModel.tags;
if (tagDtos.length > 0) {
const tagsToAddDto: ProcessTagDto[] = tagDtos
.filter((dto) => dto.action === ModifyAction.ADD)
.map((dto) => ({
name: dto.name,
productUuid: dto.productUuid,
uuid: dto.newTagUuid,
}));
const tagsToDeleteDto = tagDtos.filter(
(dto) => dto.action === ModifyAction.DELETE,
);
if (tagsToAddDto.length > 0) {
let processedTags = await this.tagService.processTags(
tagsToAddDto,
projectUuid,
queryRunner,
);
for (const subspaceDto of subspaceModels) {
if (
subspaceDto !== subspaceModel &&
subspaceDto.action === ModifyAction.UPDATE &&
subspaceDto.tags
) {
// Tag is deleted from one subspace and added in another subspace
const deletedTags = subspaceDto.tags.filter(
(tagDto) =>
tagDto.action === ModifyAction.DELETE &&
processedTags.some((tag) => tag.uuid === tagDto.tagUuid),
);
for (const deletedTag of deletedTags) {
const allocation = await queryRunner.manager.findOne(
SubspaceModelProductAllocationEntity,
{
where: {
tags: {
uuid: deletedTag.tagUuid,
},
subspaceModel: {
uuid: subspaceDto.subspaceModel.uuid,
},
},
relations: ['tags', 'product', 'subspaceModel'],
},
);
if (allocation) {
const isCommonTag = allocation.tags.some(
(tag) => tag.uuid === deletedTag.tagUuid,
);
if (allocation && isCommonTag) {
const tagEntity = allocation.tags.find(
(tag) => tag.uuid === deletedTag.tagUuid,
);
allocation.tags = allocation.tags.filter(
(tag) => tag.uuid !== deletedTag.tagUuid,
);
updatedAllocations.push({
allocation,
tagsRemoved: [tagEntity],
});
await queryRunner.manager.save(allocation);
const productAllocationExistInSubspace =
await queryRunner.manager.findOne(
SubspaceModelProductAllocationEntity,
{
where: {
subspaceModel: {
uuid: subspaceDto.subspaceModel.uuid,
},
product: { uuid: allocation.product.uuid },
},
relations: ['tags'],
},
);
if (productAllocationExistInSubspace) {
productAllocationExistInSubspace.tags.push(tagEntity);
updatedAllocations.push({
allocation: productAllocationExistInSubspace,
tagsAdded: [tagEntity],
});
await queryRunner.manager.save(
productAllocationExistInSubspace,
);
} else {
const newProductAllocation = queryRunner.manager.create(
SubspaceModelProductAllocationEntity,
{
subspaceModel: subspaceModel.subspaceModel,
product: allocation.product,
tags: [tagEntity],
},
);
updatedAllocations.push({
allocation: newProductAllocation,
});
await queryRunner.manager.save(newProductAllocation);
}
// Remove the tag from processedTags to prevent duplication
processedTags = processedTags.filter(
(tag) => tag.uuid !== deletedTag.tagUuid,
);
// Remove the tag from subspaceDto.tags to ensure it's not processed again while processing other dtos.
subspaceDto.tags = subspaceDto.tags.filter(
(tagDto) => tagDto.tagUuid !== deletedTag.tagUuid,
);
}
}
}
}
if (
subspaceDto !== subspaceModel &&
subspaceDto.action === ModifyAction.DELETE
) {
const allocation = await queryRunner.manager.findOne(
SubspaceModelProductAllocationEntity,
{
where: {
subspaceModel: { uuid: subspaceDto.subspaceModel.uuid },
},
relations: ['tags'],
},
);
const repeatedTags = allocation?.tags.filter((tag) =>
processedTags.some(
(processedTag) => processedTag.uuid === tag.uuid,
),
);
if (repeatedTags.length > 0) {
allocation.tags = allocation.tags.filter(
(tag) =>
!repeatedTags.some(
(repeatedTag) => repeatedTag.uuid === tag.uuid,
),
);
updatedAllocations.push({
allocation: allocation,
tagsRemoved: repeatedTags,
});
await queryRunner.manager.save(allocation);
const productAllocationExistInSubspace =
await queryRunner.manager.findOne(
SubspaceModelProductAllocationEntity,
{
where: {
subspaceModel: { uuid: subspaceDto.subspaceModel.uuid },
product: { uuid: allocation.product.uuid },
},
relations: ['tags'],
},
);
if (productAllocationExistInSubspace) {
updatedAllocations.push({
allocation: productAllocationExistInSubspace,
tagsAdded: repeatedTags,
});
productAllocationExistInSubspace.tags.push(...repeatedTags);
await queryRunner.manager.save(
productAllocationExistInSubspace,
);
} else {
const newProductAllocation = queryRunner.manager.create(
SubspaceModelProductAllocationEntity,
{
subspaceModel: subspaceModel.subspaceModel,
product: allocation.product,
tags: repeatedTags,
},
);
updatedAllocations.push({
newAllocation: newProductAllocation,
});
await queryRunner.manager.save(newProductAllocation);
}
}
}
}
if (spaceTagUpdateDtos) {
const deletedSpaceTags = spaceTagUpdateDtos.filter(
(tagDto) =>
tagDto.action === ModifyAction.DELETE &&
processedTags.some((tag) => tag.uuid === tagDto.tagUuid),
);
for (const deletedTag of deletedSpaceTags) {
const allocation = await queryRunner.manager.findOne(
SpaceModelProductAllocationEntity,
{
where: {
spaceModel: { uuid: spaceModel.uuid },
tags: { uuid: deletedTag.tagUuid },
},
relations: ['tags', 'product'],
},
);
if (
allocation &&
allocation.tags.some((tag) => tag.uuid === deletedTag.tagUuid)
) {
spaceAllocationToExclude.push(allocation);
}
}
}
// Create new product allocations
const newAllocations = await this.createProductAllocations(
subspaceModel.subspaceModel,
spaceModel,
processedTags,
queryRunner,
spaceAllocationToExclude,
);
return [...updatedAllocations, ...newAllocations];
}
if (tagsToDeleteDto.length > 0) {
await this.processDeleteActions(tagsToDeleteDto, queryRunner);
}
}
}
}
async clearAllAllocations(subspaceIds: string[], queryRunner: QueryRunner) {
try {
await queryRunner.manager.delete(SubspaceModelProductAllocationEntity, {
subspaceModel: In(subspaceIds),
});
await queryRunner.manager
.createQueryBuilder()
.delete()
.from(SubspaceModelProductAllocationEntity)
.where('"subspace_model_uuid" IN (:...subspaceIds)', {
subspaceIds,
})
.execute();
} catch (error) {
throw this.handleError(
error,
`Failed to clear all allocations subspace model product allocation`,
);
}
}
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

@ -1,308 +1,434 @@
import {
SpaceModelEntity,
SpaceModelProductAllocationEntity,
SubspaceModelEntity,
SubspaceModelProductAllocationEntity,
SubspaceModelRepository,
} from '@app/common/modules/space-model';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { CreateSubspaceModelDto, CreateTagModelDto } from '../../dtos';
import { Not, QueryRunner } from 'typeorm';
import {
IDeletedSubsaceModelInterface,
ModifySubspaceModelPayload,
UpdatedSubspaceModelPayload,
} from 'src/space-model/interfaces';
import {
DeleteSubspaceModelDto,
ModifySubspaceModelDto,
} from 'src/space-model/dtos/subspaces-model-dtos';
import { TagModelService } from '../tag-model.service';
import { CreateSubspaceModelDto, ModifyTagModelDto } from '../../dtos';
import { In, Not, QueryFailedError, QueryRunner } from 'typeorm';
import { ModifySubspaceModelDto } from 'src/space-model/dtos/subspaces-model-dtos';
import { ModifyAction } from '@app/common/constants/modify-action.enum';
import { ProcessTagDto } from 'src/tags/dtos';
import { TagService } from 'src/tags/services';
import { SubspaceModelProductAllocationService } from './subspace-model-product-allocation.service';
import {
IRelocatedAllocation,
ISingleSubspaceModel,
} from 'src/space-model/interfaces';
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
import { SubSpaceService } from 'src/space/services/subspace';
@Injectable()
export class SubSpaceModelService {
constructor(
private readonly subspaceModelRepository: SubspaceModelRepository,
private readonly tagModelService: TagModelService,
private readonly tagService: TagService,
private readonly productAllocationService: SubspaceModelProductAllocationService,
private readonly subspaceService: SubSpaceService,
) {}
async createSubSpaceModels(
subSpaceModelDtos: CreateSubspaceModelDto[],
async createModels(
spaceModel: SpaceModelEntity,
dtos: CreateSubspaceModelDto[],
queryRunner: QueryRunner,
otherTags?: CreateTagModelDto[],
): Promise<SubspaceModelEntity[]> {
) {
try {
await this.validateInputDtos(subSpaceModelDtos, spaceModel);
this.validateNamesInDTO(dtos.map((dto) => dto.subspaceName));
const subspaces = subSpaceModelDtos.map((subspaceDto) =>
const subspaceEntities: SubspaceModelEntity[] = dtos.map((dto) =>
queryRunner.manager.create(this.subspaceModelRepository.target, {
subspaceName: subspaceDto.subspaceName,
subspaceName: dto.subspaceName,
spaceModel,
}),
);
const savedSubspaces = await queryRunner.manager.save(subspaces);
const savedSubspaces = await queryRunner.manager.save(subspaceEntities);
await Promise.all(
subSpaceModelDtos.map(async (dto, index) => {
const subspace = savedSubspaces[index];
for (const [index, dto] of dtos.entries()) {
const subspaceModel = savedSubspaces[index];
const otherDtoTags = subSpaceModelDtos
.filter((_, i) => i !== index)
.flatMap((otherDto) => otherDto.tags || []);
if (dto.tags && dto.tags.length > 0) {
subspace.tags = await this.tagModelService.createTags(
dto.tags,
queryRunner,
null,
subspace,
[...(otherTags || []), ...otherDtoTags],
);
}
}),
);
const processedTags = await this.tagService.processTags(
dto.tags,
spaceModel.project.uuid,
queryRunner,
);
await this.productAllocationService.createProductAllocations(
subspaceModel,
spaceModel,
processedTags,
queryRunner,
);
}
return savedSubspaces;
} catch (error) {
if (error instanceof HttpException) {
throw error; // Rethrow known HttpExceptions
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?: ModifyTagModelDto[],
spaces?: SpaceEntity[],
): Promise<ISingleSubspaceModel[]> {
try {
if (!dtos || dtos.length === 0) {
return;
}
// Handle unexpected errors
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];
await this.productAllocationService.updateAllocations(
combineModels,
projectUuid,
queryRunner,
spaceModel,
spaceTagUpdateDtos,
);
const deletedSubspaces = await this.deleteSubspaceModels(
deleteDtos,
queryRunner,
spaceModel,
spaceTagUpdateDtos,
);
return [
createdSubspaces ?? [],
updatedSubspaces ?? [],
deletedSubspaces ?? [],
]
.filter((arr) => arr.length > 0)
.flat();
} catch (error) {
console.error('Error in modifySubspaceModels:', error);
throw new HttpException(
`An error occurred while creating subspace models: ${error.message}`,
{
status: HttpStatus.INTERNAL_SERVER_ERROR,
message: 'An error occurred while modifying subspace models',
error: error.message,
},
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async deleteSubspaceModels(
deleteDtos: DeleteSubspaceModelDto[],
deleteDtos: ModifySubspaceModelDto[],
queryRunner: QueryRunner,
): Promise<IDeletedSubsaceModelInterface[]> {
const deleteResults: IDeletedSubsaceModelInterface[] = [];
spaceModel: SpaceModelEntity,
spaceTagUpdateDtos?: ModifyTagModelDto[],
): Promise<ISingleSubspaceModel[]> {
try {
if (!deleteDtos || deleteDtos.length === 0) {
return [];
}
for (const dto of deleteDtos) {
const subspaceModel = await this.findOne(dto.subspaceUuid);
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(
this.subspaceModelRepository.target,
{ uuid: dto.subspaceUuid },
SubspaceModelEntity,
{ uuid: In(subspaceUuids) },
{ disabled: true },
);
if (subspaceModel.tags?.length) {
const modifyTagDtos = subspaceModel.tags.map((tag) => ({
uuid: tag.uuid,
action: ModifyAction.DELETE,
}));
await this.tagModelService.modifyTags(
modifyTagDtos,
queryRunner,
null,
subspaceModel,
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,
);
}
deleteResults.push({ uuid: dto.subspaceUuid });
}
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();
return deleteResults;
}
deleteResults.push(...subspaceUuids.map((uuid) => ({ uuid })));
async modifySubSpaceModels(
subspaceDtos: ModifySubspaceModelDto[],
spaceModel: SpaceModelEntity,
queryRunner: QueryRunner,
): Promise<ModifySubspaceModelPayload> {
const modifiedSubspaceModels: ModifySubspaceModelPayload = {
addedSubspaceModels: [],
updatedSubspaceModels: [],
deletedSubspaceModels: [],
};
try {
for (const subspace of subspaceDtos) {
switch (subspace.action) {
case ModifyAction.ADD:
const subspaceModel = await this.handleAddAction(
subspace,
spaceModel,
queryRunner,
);
modifiedSubspaceModels.addedSubspaceModels.push(subspaceModel);
break;
case ModifyAction.UPDATE:
const updatedSubspaceModel = await this.handleUpdateAction(
subspace,
queryRunner,
);
modifiedSubspaceModels.updatedSubspaceModels.push(
updatedSubspaceModel,
);
break;
case ModifyAction.DELETE:
await this.handleDeleteAction(subspace, queryRunner);
modifiedSubspaceModels.deletedSubspaceModels.push(subspace.uuid);
break;
default:
throw new HttpException(
`Invalid action "${subspace.action}".`,
HttpStatus.BAD_REQUEST,
);
}
}
return modifiedSubspaceModels;
return subspaces.map((subspace) => ({
subspaceModel: subspace,
action: ModifyAction.DELETE,
relocatedAllocations: relocatedAllocationsMap.get(subspace.uuid) || [],
}));
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
`An error occurred while modifying subspace models: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
private async handleAddAction(
subspace: ModifySubspaceModelDto,
spaceModel: SpaceModelEntity,
queryRunner: QueryRunner,
): Promise<SubspaceModelEntity> {
try {
const createTagDtos: CreateTagModelDto[] =
subspace.tags?.map((tag) => ({
tag: tag.tag,
uuid: tag.uuid,
productUuid: tag.productUuid,
})) || [];
const [createdSubspaceModel] = await this.createSubSpaceModels(
[
{
subspaceName: subspace.subspaceName,
tags: createTagDtos,
},
],
spaceModel,
queryRunner,
);
return createdSubspaceModel;
} catch (error) {
if (error instanceof HttpException) {
throw error; // Rethrow known HttpExceptions
}
throw new HttpException(
`An error occurred while adding subspace: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
private async handleUpdateAction(
modifyDto: ModifySubspaceModelDto,
queryRunner: QueryRunner,
): Promise<UpdatedSubspaceModelPayload> {
const updatePayload: UpdatedSubspaceModelPayload = {
subspaceModelUuid: modifyDto.uuid,
};
const subspace = await this.findOne(modifyDto.uuid);
await this.updateSubspaceName(
queryRunner,
subspace,
modifyDto.subspaceName,
);
updatePayload.subspaceName = modifyDto.subspaceName;
if (modifyDto.tags?.length) {
updatePayload.modifiedTags = await this.tagModelService.modifyTags(
modifyDto.tags,
queryRunner,
null,
subspace,
);
}
return updatePayload;
}
private async handleDeleteAction(
subspace: ModifySubspaceModelDto,
queryRunner: QueryRunner,
) {
const subspaceModel = await this.findOne(subspace.uuid);
await queryRunner.manager.update(
this.subspaceModelRepository.target,
{ uuid: subspace.uuid },
{ disabled: true },
);
if (subspaceModel.tags?.length) {
const modifyTagDtos: CreateTagModelDto[] = subspaceModel.tags.map(
(tag) => ({
uuid: tag.uuid,
action: ModifyAction.ADD,
tag: tag.tag,
productUuid: tag.product.uuid,
}),
);
await this.tagModelService.moveTags(
modifyTagDtos,
queryRunner,
subspaceModel.spaceModel,
null,
);
}
}
private async findOne(subspaceUuid: string): Promise<SubspaceModelEntity> {
const subspace = await this.subspaceModelRepository.findOne({
where: { uuid: subspaceUuid, disabled: false },
relations: ['tags', 'spaceModel', 'tags.product'],
});
if (!subspace) {
throw new HttpException(
`SubspaceModel with UUID ${subspaceUuid} not found.`,
HttpStatus.NOT_FOUND,
);
}
return subspace;
}
private async validateInputDtos(
subSpaceModelDtos: CreateSubspaceModelDto[],
spaceModel: SpaceModelEntity,
): Promise<void> {
try {
if (subSpaceModelDtos.length === 0) {
if (error instanceof QueryFailedError) {
throw new HttpException(
'Subspace models cannot be empty.',
`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,
);
}
}
}
await this.validateName(
subSpaceModelDtos.map((dto) => dto.subspaceName),
spaceModel,
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) {
if (error instanceof HttpException) {
throw error; // Rethrow known HttpExceptions to preserve their message and status
}
// Wrap unexpected errors
throw new HttpException(
`An error occurred while validating subspace models: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
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: ModifyTagModelDto[];
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,
@ -327,10 +453,33 @@ export class SubSpaceModelService {
}
}
private async validateName(
names: string[],
spaceModel: SpaceModelEntity,
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>();
@ -346,26 +495,32 @@ export class SubSpaceModelService {
HttpStatus.CONFLICT,
);
}
for (const name of names) {
await this.checkDuplicateNames(name, spaceModel.uuid);
}
}
private async updateSubspaceName(
queryRunner: QueryRunner,
subSpaceModel: SubspaceModelEntity,
subspaceName?: string,
): Promise<void> {
if (subspaceName) {
await this.checkDuplicateNames(
subspaceName,
subSpaceModel.spaceModel.uuid,
subSpaceModel.uuid,
);
extractTagsFromSubspaceModels(
subspaceModels: CreateSubspaceModelDto[],
): ProcessTagDto[] {
return subspaceModels.flatMap((subspace) => subspace.tags || []);
}
subSpaceModel.subspaceName = subspaceName;
await queryRunner.manager.save(subSpaceModel);
}
extractTagsFromModifiedSubspaceModels(
subspaceModels: ModifySubspaceModelDto[],
): ModifyTagModelDto[] {
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',
],
});
}
}

View File

@ -1,577 +0,0 @@
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
import { QueryRunner } from 'typeorm';
import {
SpaceModelEntity,
TagModel,
} from '@app/common/modules/space-model/entities';
import { SubspaceModelEntity } from '@app/common/modules/space-model/entities';
import { TagModelRepository } from '@app/common/modules/space-model';
import {
CreateTagModelDto,
ModifySubspaceModelDto,
ModifyTagModelDto,
} from '../dtos';
import { ProductService } from 'src/product/services';
import { ModifyAction } from '@app/common/constants/modify-action.enum';
import { ModifiedTagsModelPayload } from '../interfaces';
@Injectable()
export class TagModelService {
constructor(
private readonly tagModelRepository: TagModelRepository,
private readonly productService: ProductService,
) {}
async createTags(
tags: CreateTagModelDto[],
queryRunner: QueryRunner,
spaceModel?: SpaceModelEntity,
subspaceModel?: SubspaceModelEntity,
additionalTags?: CreateTagModelDto[],
tagsToDelete?: ModifyTagModelDto[],
): Promise<TagModel[]> {
if (!tags.length) {
throw new HttpException('Tags cannot be empty.', HttpStatus.BAD_REQUEST);
}
const combinedTags = additionalTags ? [...tags, ...additionalTags] : tags;
const duplicateTags = this.findDuplicateTags(combinedTags);
if (duplicateTags.length > 0) {
throw new HttpException(
`Duplicate tags found for the same product: ${duplicateTags.join(', ')}`,
HttpStatus.BAD_REQUEST,
);
}
const tagEntitiesToCreate = tags.filter((tagDto) => !tagDto.uuid);
const tagEntitiesToUpdate = tags.filter((tagDto) => !!tagDto.uuid);
try {
const createdTags = await this.bulkSaveTags(
tagEntitiesToCreate,
queryRunner,
spaceModel,
subspaceModel,
tagsToDelete,
);
// Update existing tags
const updatedTags = await this.moveTags(
tagEntitiesToUpdate,
queryRunner,
spaceModel,
subspaceModel,
);
// Combine created and updated tags
return [...createdTags, ...updatedTags];
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
`Failed to create tag models due to an unexpected error.: ${error}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async bulkSaveTags(
tags: CreateTagModelDto[],
queryRunner: QueryRunner,
spaceModel?: SpaceModelEntity,
subspaceModel?: SubspaceModelEntity,
tagsToDelete?: ModifyTagModelDto[],
): Promise<TagModel[]> {
if (!tags.length) {
return [];
}
const tagEntities = await Promise.all(
tags.map((tagDto) =>
this.prepareTagEntity(
tagDto,
queryRunner,
spaceModel,
subspaceModel,
tagsToDelete,
),
),
);
try {
return await queryRunner.manager.save(tagEntities);
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
`Failed to save tag models due to an unexpected error: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async moveTags(
tags: CreateTagModelDto[],
queryRunner: QueryRunner,
spaceModel?: SpaceModelEntity,
subspaceModel?: SubspaceModelEntity,
): Promise<TagModel[]> {
if (!tags.length) {
return [];
}
try {
return await Promise.all(
tags.map(async (tagDto) => {
try {
const tag = await this.getTagByUuid(tagDto.uuid);
if (!tag) {
throw new HttpException(
`Tag with UUID ${tagDto.uuid} not found.`,
HttpStatus.NOT_FOUND,
);
}
if (subspaceModel && subspaceModel.spaceModel) {
await queryRunner.manager.update(
this.tagModelRepository.target,
{ uuid: tag.uuid },
{ subspaceModel, spaceModel: null },
);
tag.subspaceModel = subspaceModel;
}
if (!subspaceModel && spaceModel) {
await queryRunner.manager.update(
this.tagModelRepository.target,
{ uuid: tag.uuid },
{ subspaceModel: null, spaceModel: spaceModel },
);
tag.subspaceModel = null;
tag.spaceModel = spaceModel;
}
return tag;
} catch (error) {
console.error(
`Error moving tag with UUID ${tagDto.uuid}: ${error.message}`,
);
throw error; // Re-throw the error to propagate it to the parent Promise.all
}
}),
);
} catch (error) {
console.error(`Error in moveTags: ${error.message}`);
throw new HttpException(
`Failed to move tags due to an unexpected error: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async updateTag(
tag: ModifyTagModelDto,
queryRunner: QueryRunner,
spaceModel?: SpaceModelEntity,
subspaceModel?: SubspaceModelEntity,
): Promise<TagModel> {
try {
const existingTag = await this.getTagByUuid(tag.uuid);
if (tag.tag !== existingTag.tag) {
if (spaceModel) {
await this.checkTagReuse(
tag.tag,
existingTag.product.uuid,
spaceModel,
);
} else {
await this.checkTagReuse(
tag.tag,
existingTag.product.uuid,
subspaceModel.spaceModel,
);
}
if (tag.tag) {
existingTag.tag = tag.tag;
}
}
return await queryRunner.manager.save(existingTag);
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
error.message || 'Failed to update tags',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async deleteTags(tagUuids: string[], queryRunner: QueryRunner) {
try {
const deletePromises = tagUuids.map((id) =>
queryRunner.manager.update(
this.tagModelRepository.target,
{ uuid: id },
{ disabled: true },
),
);
await Promise.all(deletePromises);
return { message: 'Tags deleted successfully', tagUuids };
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
error.message || 'Failed to delete tags',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
private findDuplicateTags(tags: CreateTagModelDto[]): string[] {
const seen = new Map<string, boolean>();
const duplicates: string[] = [];
tags.forEach((tagDto) => {
const key = `${tagDto.productUuid}-${tagDto.tag}`;
if (seen.has(key)) {
duplicates.push(`${tagDto.tag} for Product: ${tagDto.productUuid}`);
} else {
seen.set(key, true);
}
});
return duplicates;
}
async modifyTags(
tags: ModifyTagModelDto[],
queryRunner: QueryRunner,
spaceModel?: SpaceModelEntity,
subspaceModel?: SubspaceModelEntity,
): Promise<ModifiedTagsModelPayload> {
const modifiedTagModels: ModifiedTagsModelPayload = {
added: [],
updated: [],
deleted: [],
};
try {
const tagsToDelete = tags.filter(
(tag) => tag.action === ModifyAction.DELETE,
);
for (const tag of tags) {
if (tag.action === ModifyAction.ADD) {
const createTagDto: CreateTagModelDto = {
tag: tag.tag as string,
uuid: tag.uuid,
productUuid: tag.productUuid as string,
};
const newModel = await this.createTags(
[createTagDto],
queryRunner,
spaceModel,
subspaceModel,
null,
tagsToDelete,
);
modifiedTagModels.added.push(...newModel);
} else if (tag.action === ModifyAction.UPDATE) {
const updatedModel = await this.updateTag(
tag,
queryRunner,
spaceModel,
subspaceModel,
);
modifiedTagModels.updated.push(updatedModel);
} else if (tag.action === ModifyAction.DELETE) {
await queryRunner.manager.update(
this.tagModelRepository.target,
{ uuid: tag.uuid },
{ disabled: true },
);
modifiedTagModels.deleted.push(tag.uuid);
} else {
throw new HttpException(
`Invalid action "${tag.action}" provided.`,
HttpStatus.BAD_REQUEST,
);
}
}
return modifiedTagModels;
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
`An error occurred while modifying tag models: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
private async checkTagReuse(
tag: string,
productUuid: string,
spaceModel: SpaceModelEntity,
tagsToDelete?: ModifyTagModelDto[],
): Promise<void> {
try {
// Query to find existing tags
const tagExists = await this.tagModelRepository.find({
where: [
{
tag,
spaceModel: { uuid: spaceModel.uuid },
product: { uuid: productUuid },
disabled: false,
},
{
tag,
subspaceModel: { spaceModel: { uuid: spaceModel.uuid } },
product: { uuid: productUuid },
disabled: false,
},
],
});
// Remove tags that are marked for deletion
const filteredTagExists = tagExists.filter(
(existingTag) =>
!tagsToDelete?.some(
(deleteTag) => deleteTag.uuid === existingTag.uuid,
),
);
// If any tags remain, throw an exception
if (filteredTagExists.length > 0) {
throw new HttpException(
`Tag ${tag} can't be reused`,
HttpStatus.CONFLICT,
);
}
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
console.error(`Error while checking tag reuse: ${error.message}`);
throw new HttpException(
`An error occurred while checking tag reuse: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
private async prepareTagEntity(
tagDto: CreateTagModelDto,
queryRunner: QueryRunner,
spaceModel?: SpaceModelEntity,
subspaceModel?: SubspaceModelEntity,
tagsToDelete?: ModifyTagModelDto[],
): Promise<TagModel> {
try {
const product = await this.productService.findOne(tagDto.productUuid);
if (!product) {
throw new HttpException(
`Product with UUID ${tagDto.productUuid} not found.`,
HttpStatus.NOT_FOUND,
);
}
if (spaceModel) {
await this.checkTagReuse(
tagDto.tag,
tagDto.productUuid,
spaceModel,
tagsToDelete,
);
} else if (subspaceModel && subspaceModel.spaceModel) {
await this.checkTagReuse(
tagDto.tag,
tagDto.productUuid,
subspaceModel.spaceModel,
);
} else {
throw new HttpException(
`Invalid subspaceModel or spaceModel provided.`,
HttpStatus.BAD_REQUEST,
);
}
return queryRunner.manager.create(TagModel, {
tag: tagDto.tag,
product: product.data,
spaceModel: spaceModel,
subspaceModel: subspaceModel,
});
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
`An error occurred while preparing the tag entity: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async getTagByUuid(uuid: string): Promise<TagModel> {
const tag = await this.tagModelRepository.findOne({
where: { uuid, disabled: false },
relations: ['product'],
});
if (!tag) {
throw new HttpException(
`Tag model with ID ${uuid} not found.`,
HttpStatus.NOT_FOUND,
);
}
return tag;
}
async getTagByName(
tag: string,
subspaceUuid?: string,
spaceUuid?: string,
): Promise<TagModel> {
const queryConditions: any = { tag };
if (spaceUuid) {
queryConditions.spaceModel = { uuid: spaceUuid };
} else if (subspaceUuid) {
queryConditions.subspaceModel = { uuid: subspaceUuid };
} else {
throw new HttpException(
'Either spaceUuid or subspaceUuid must be provided.',
HttpStatus.BAD_REQUEST,
);
}
queryConditions.disabled = false;
const existingTag = await this.tagModelRepository.findOne({
where: queryConditions,
relations: ['product'],
});
if (!existingTag) {
throw new HttpException(
`Tag model with tag "${tag}" not found.`,
HttpStatus.NOT_FOUND,
);
}
return existingTag;
}
getSubspaceTagsToBeAdded(
spaceTags?: ModifyTagModelDto[],
subspaceModels?: ModifySubspaceModelDto[],
): ModifyTagModelDto[] {
if (!subspaceModels || subspaceModels.length === 0) {
return spaceTags;
}
const spaceTagsToDelete = spaceTags?.filter(
(tag) => tag.action === 'delete',
);
const tagsToAdd = subspaceModels.flatMap(
(subspace) => subspace.tags?.filter((tag) => tag.action === 'add') || [],
);
const commonTagUuids = new Set(
tagsToAdd
.filter((tagToAdd) =>
spaceTagsToDelete.some(
(tagToDelete) => tagToAdd.uuid === tagToDelete.uuid,
),
)
.map((tag) => tag.uuid),
);
const remainingTags = spaceTags.filter(
(tag) => !commonTagUuids.has(tag.uuid), // Exclude tags in commonTagUuids
);
return remainingTags;
}
getModifiedSubspaces(
spaceTags: ModifyTagModelDto[],
subspaceModels: ModifySubspaceModelDto[],
): ModifySubspaceModelDto[] {
if (!subspaceModels || subspaceModels.length === 0) {
return [];
}
// Extract tags marked for addition in spaceTags
const spaceTagsToAdd = spaceTags.filter((tag) => tag.action === 'add');
const subspaceTagsToAdd = subspaceModels.flatMap(
(subspace) => subspace.tags?.filter((tag) => tag.action === 'add') || [],
);
const subspaceTagsToDelete = subspaceModels.flatMap(
(subspace) =>
subspace.tags?.filter((tag) => tag.action === 'delete') || [],
);
const subspaceTagsToDeleteUuids = new Set(
subspaceTagsToDelete.map((tag) => tag.uuid),
);
const commonTagsInSubspaces = subspaceTagsToAdd.filter((tag) =>
subspaceTagsToDeleteUuids.has(tag.uuid),
);
// Find UUIDs of tags that are common between spaceTagsToAdd and subspace tags marked for deletion
const commonTagUuids = new Set(
spaceTagsToAdd
.flatMap((tagToAdd) =>
subspaceModels.flatMap(
(subspace) =>
subspace.tags?.filter(
(tagToDelete) =>
tagToDelete.action === 'delete' &&
tagToAdd.uuid === tagToDelete.uuid,
) || [],
),
)
.map((tag) => tag.uuid),
);
// Modify subspaceModels by removing tags with UUIDs present in commonTagUuids
let modifiedSubspaces = subspaceModels.map((subspace) => ({
...subspace,
tags: subspace.tags?.filter((tag) => !commonTagUuids.has(tag.uuid)) || [],
}));
modifiedSubspaces = modifiedSubspaces.map((subspace) => ({
...subspace,
tags:
subspace.tags?.filter(
(tag) =>
!(
tag.action === 'delete' &&
commonTagsInSubspaces.some(
(commonTag) => commonTag.uuid === tag.uuid,
)
),
) || [],
}));
return modifiedSubspaces;
}
}

View File

@ -2,13 +2,11 @@ import { SpaceRepositoryModule } from '@app/common/modules/space/space.repositor
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { SpaceModelController } from './controllers';
import { SpaceModelService, SubSpaceModelService } from './services';
import {
SpaceModelService,
SubSpaceModelService,
TagModelService,
} from './services';
import {
SpaceModelProductAllocationRepoitory,
SpaceModelRepository,
SubspaceModelProductAllocationRepoitory,
SubspaceModelRepository,
TagModelRepository,
} from '@app/common/modules/space-model';
@ -22,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,
@ -38,6 +40,22 @@ import { CommunityService } from 'src/community/services';
import { DeviceRepository } from '@app/common/modules/device/repositories';
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
import { CommunityRepository } from '@app/common/modules/community/repositories';
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';
import { DeviceService } from 'src/device/services';
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service';
import { SceneService } from 'src/scene/services';
import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories';
import {
SceneIconRepository,
SceneRepository,
} from '@app/common/modules/scene/repositories';
import { AutomationRepository } from '@app/common/modules/automation/repositories';
const CommandHandlers = [
PropogateUpdateSpaceModelHandler,
@ -58,7 +76,6 @@ const CommandHandlers = [
SubspaceModelRepository,
ProductRepository,
SubspaceRepository,
TagModelService,
TagModelRepository,
SubSpaceService,
ValidationService,
@ -72,6 +89,26 @@ const CommandHandlers = [
SpaceLinkService,
SpaceLinkRepository,
InviteSpaceRepository,
NewTagService,
DeviceService,
DeviceStatusFirebaseService,
DeviceStatusLogRepository,
SceneService,
SceneIconRepository,
SceneDeviceRepository,
SceneRepository,
AutomationRepository,
SpaceModelProductAllocationRepoitory,
SubspaceModelProductAllocationRepoitory,
NewTagRepository,
SpaceModelProductAllocationService,
SubspaceModelProductAllocationRepoitory,
SubspaceModelProductAllocationService,
SpaceProductAllocationService,
SubspaceProductAllocationService,
SpaceProductAllocationRepository,
SubspaceProductAllocationRepository,
SubSpaceService,
],
exports: [CqrsModule, SpaceModelService],
})

View File

@ -1,4 +1,4 @@
import { SpaceEntity } from '@app/common/modules/space';
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
export class DisableSpaceCommand {
constructor(

View File

@ -0,0 +1,38 @@
import { ControllerRoute } from '@app/common/constants/controller-route';
import { Body, Controller, Param, Post, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { ValidationService } from '../services';
import { ValidateSpacesDto } from '../dtos/validation.space.dto';
import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard';
import { ProjectParam } from '../dtos';
@ApiTags('Space Module')
@Controller({
version: '1',
path: ControllerRoute.SPACE_VALIDATION.ROUTE,
})
export class SpaceValidationController {
constructor(private readonly validationService: ValidationService) {}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@ApiOperation({
summary:
ControllerRoute.SPACE_VALIDATION.ACTIONS
.VALIDATE_SPACE_WITH_DEVICES_OR_SUBSPACES_SUMMARY,
description:
ControllerRoute.SPACE_VALIDATION.ACTIONS
.VALIDATE_SPACE_WITH_DEVICES_OR_SUBSPACES_DESCRIPTION,
})
@Post('validate')
async validateSpaces(
@Body() validateSpacesDto: ValidateSpacesDto,
@Param() projectParam: ProjectParam,
): Promise<BaseResponseDto> {
return this.validationService.validateSpacesWithDevicesOrSubspaces(
validateSpacesDto,
projectParam,
);
}
}

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

@ -6,8 +6,8 @@ import {
IsString,
ValidateNested,
} from 'class-validator';
import { CreateTagDto } from '../tag';
import { Type } from 'class-transformer';
import { ProcessTagDto } from 'src/tags/dtos';
export class AddSubspaceDto {
@ApiProperty({
@ -20,11 +20,11 @@ export class AddSubspaceDto {
@ApiProperty({
description: 'List of tags associated with the subspace',
type: [CreateTagDto],
type: [ProcessTagDto],
})
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreateTagDto)
@Type(() => ProcessTagDto)
@IsOptional()
tags?: CreateTagDto[];
tags?: ProcessTagDto[];
}

View File

@ -1,6 +1,6 @@
import { ModifyAction } from '@app/common/constants/modify-action.enum';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsEnum, IsOptional, IsString } from 'class-validator';
import { IsEnum, IsOptional, IsString, IsUUID } from 'class-validator';
export class ModifyTagDto {
@ApiProperty({
@ -11,12 +11,21 @@ export class ModifyTagDto {
action: ModifyAction;
@ApiPropertyOptional({
description: 'UUID of the tag (required for update/delete)',
description: 'UUID of the new tag',
example: '123e4567-e89b-12d3-a456-426614174000',
})
@IsOptional()
@IsString()
uuid?: string;
@IsUUID()
newTagUuid: string;
@ApiPropertyOptional({
description:
'UUID of an existing tag (required for update/delete, optional for add)',
example: 'a1b2c3d4-5678-90ef-abcd-1234567890ef',
})
@IsOptional()
@IsUUID()
tagUuid?: string;
@ApiPropertyOptional({
description: 'Name of the tag (required for add/update)',
@ -24,7 +33,7 @@ export class ModifyTagDto {
})
@IsOptional()
@IsString()
tag?: string;
name?: string;
@ApiPropertyOptional({
description:
@ -32,6 +41,6 @@ export class ModifyTagDto {
example: 'c789a91e-549a-4753-9006-02f89e8170e0',
})
@IsOptional()
@IsString()
@IsUUID()
productUuid?: string;
}

View File

@ -58,4 +58,11 @@ export class UpdateSpaceDto {
@ValidateNested({ each: true })
@Type(() => ModifyTagDto)
tags?: ModifyTagDto[];
@ApiProperty({
description: 'UUID of the Space',
example: 'd290f1ee-6c54-4b01-90e6-d701748f0851',
})
@IsString()
@IsOptional()
spaceModelUuid?: string;
}

View File

@ -0,0 +1,14 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsArray, IsNotEmpty, IsString } from 'class-validator';
export class ValidateSpacesDto {
@ApiProperty({
description: 'Array of space UUIDs to be validated',
type: [String],
example: ['space-1', 'space-2', 'space-3'],
})
@IsArray()
@IsNotEmpty()
@IsString({ each: true })
spacesUuids: string[];
}

View File

@ -1,4 +1,3 @@
import { SpaceEntity } from '@app/common/modules/space';
import { HttpException, HttpStatus } from '@nestjs/common';
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { DeviceService } from 'src/device/services';
@ -11,6 +10,7 @@ import {
SpaceSceneService,
} from '../services';
import { TagService } from '../services/tag';
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
@CommandHandler(DisableSpaceCommand)
export class DisableSpaceHandler
@ -66,12 +66,12 @@ export class DisableSpaceHandler
}
const tagUuids = space.tags?.map((tag) => tag.uuid) || [];
const subspaceDtos =
/* const subspaceDtos =
space.subspaces?.map((subspace) => ({
subspaceUuid: subspace.uuid,
})) || [];
})) || []; */
const deletionTasks = [
this.subSpaceService.deleteSubspaces(subspaceDtos, queryRunner),
// this.subSpaceService.deleteSubspaces(subspaceDtos, queryRunner),
this.userService.deleteUserSpace(space.uuid),
this.tagService.deleteTags(tagUuids, queryRunner),
this.deviceService.deleteDevice(

View File

@ -1,4 +1,4 @@
import { SubspaceEntity } from '@app/common/modules/space';
import { SubspaceEntity } from '@app/common/modules/space/entities/subspace/subspace.entity';
export interface ModifySubspacePayload {
addedSubspaces?: SubspaceEntity[];

View File

@ -0,0 +1,9 @@
import { ModifyAction } from '@app/common/constants/modify-action.enum';
import { SubspaceEntity } from '@app/common/modules/space/entities/subspace/subspace.entity';
import { ModifyTagDto } from '../dtos/tag/modify-tag.dto';
export interface ISingleSubspace {
subspace: SubspaceEntity;
action: ModifyAction;
tags: ModifyTagDto[];
}

View File

@ -1,4 +1,5 @@
import { SpaceEntity, SpaceLinkEntity } from '@app/common/modules/space';
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';

View File

@ -0,0 +1,589 @@
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';
import { SpaceModelProductAllocationEntity } from '@app/common/modules/space-model';
import { DeviceEntity } from '@app/common/modules/device/entities';
import { ProjectEntity } from '@app/common/modules/project/entities';
import { ValidationService } from './space-validation.service';
@Injectable()
export class SpaceProductAllocationService {
constructor(
private readonly tagService: NewTagService,
private readonly spaceProductAllocationRepository: SpaceProductAllocationRepository,
private readonly spaceService: ValidationService,
) {}
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) {
const isDuplicated = await this.validateTagWithinSpace(
queryRunner,
tag,
space,
);
if (isDuplicated) continue;
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 createAllocationFromModel(
modelAllocations: SpaceModelProductAllocationEntity[],
queryRunner: QueryRunner,
spaces?: SpaceEntity[],
) {
if (!spaces || spaces.length === 0 || !modelAllocations.length) return;
const allocations: SpaceProductAllocationEntity[] = [];
for (const space of spaces) {
for (const modelAllocation of modelAllocations) {
const allocation = queryRunner.manager.create(
SpaceProductAllocationEntity,
{
space,
product: modelAllocation.product,
tags: modelAllocation.tags,
inheritedFromModel: modelAllocation,
},
);
allocations.push(allocation);
}
}
if (allocations.length > 0) {
await queryRunner.manager.save(SpaceProductAllocationEntity, allocations);
}
}
async addTagToAllocationFromModel(
modelAllocation: SpaceModelProductAllocationEntity,
queryRunner: QueryRunner,
tag: NewTagEntity,
spaces?: SpaceEntity[],
) {
try {
if (!spaces || spaces.length === 0 || !modelAllocation) return;
const spaceAllocations = await queryRunner.manager.find(
SpaceProductAllocationEntity,
{
where: { inheritedFromModel: { uuid: modelAllocation.uuid } },
relations: ['tags'],
},
);
if (spaceAllocations.length === 0) return;
for (const allocation of spaceAllocations) {
allocation.tags.push(tag);
}
await queryRunner.manager.save(
SpaceProductAllocationEntity,
spaceAllocations,
);
} catch (error) {
throw new HttpException(
'Failed to add tag to allocation from model',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async updateSpaceProductAllocations(
dtos: ModifyTagDto[],
projectUuid: string,
space: SpaceEntity,
queryRunner: QueryRunner,
modifySubspace?: ModifySubspaceDto[],
): Promise<void> {
if (!dtos || dtos.length === 0) return;
try {
await Promise.all([
this.processAddActions(
dtos,
projectUuid,
space,
queryRunner,
modifySubspace,
),
this.processDeleteActions(dtos, queryRunner, space),
]);
} catch (error) {
throw this.handleError(error, 'Error while updating product allocations');
}
}
async unlinkModels(space: SpaceEntity, queryRunner: QueryRunner) {
try {
if (!space.productAllocations || space.productAllocations.length === 0)
return;
const allocationUuids = space.productAllocations.map(
(allocation) => allocation.uuid,
);
await queryRunner.manager.update(
SpaceProductAllocationEntity,
{ uuid: In(allocationUuids) },
{ inheritedFromModel: null },
);
} catch (error) {
throw new HttpException(
'Failed to unlink models',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async propagateDeleteToInheritedAllocations(
queryRunner: QueryRunner,
allocationsToUpdate: SpaceModelProductAllocationEntity[],
tagUuidsToDelete: string[],
project: ProjectEntity,
spaces?: SpaceEntity[],
): Promise<void> {
try {
const inheritedAllocationUpdates: SpaceProductAllocationEntity[] = [];
const inheritedAllocationsToDelete: SpaceProductAllocationEntity[] = [];
for (const allocation of allocationsToUpdate) {
for (const inheritedAllocation of allocation.inheritedSpaceAllocations) {
const updatedInheritedTags = inheritedAllocation.tags.filter(
(tag) => !tagUuidsToDelete.includes(tag.uuid),
);
if (updatedInheritedTags.length === inheritedAllocation.tags.length) {
continue;
}
if (updatedInheritedTags.length === 0) {
inheritedAllocationsToDelete.push(inheritedAllocation);
} else {
inheritedAllocation.tags = updatedInheritedTags;
inheritedAllocationUpdates.push(inheritedAllocation);
}
}
}
if (inheritedAllocationUpdates.length > 0) {
await queryRunner.manager.save(
SpaceProductAllocationEntity,
inheritedAllocationUpdates,
);
}
if (inheritedAllocationsToDelete.length > 0) {
await queryRunner.manager.remove(
SpaceProductAllocationEntity,
inheritedAllocationsToDelete,
);
}
if (spaces && spaces.length > 0) {
await this.moveDevicesToOrphanSpace(
queryRunner,
spaces,
tagUuidsToDelete,
project,
);
}
await queryRunner.manager
.createQueryBuilder()
.delete()
.from('space_product_tags')
.where(
'space_product_allocation_uuid NOT IN (' +
queryRunner.manager
.createQueryBuilder()
.select('allocation.uuid')
.from(SpaceProductAllocationEntity, 'allocation')
.getQuery() +
')',
)
.execute();
} catch (error) {
throw this.handleError(
error,
`Failed to propagate tag deletion to inherited allocations`,
);
}
}
async moveDevicesToOrphanSpace(
queryRunner: QueryRunner,
spaces: SpaceEntity[],
tagUuidsToDelete: string[],
project: ProjectEntity,
): Promise<void> {
try {
const orphanSpace = await this.spaceService.getOrphanSpace(project);
const devicesToMove = await queryRunner.manager
.createQueryBuilder(DeviceEntity, 'device')
.leftJoinAndSelect('device.tag', 'tag')
.where('device.spaceDevice IN (:...spaceUuids)', {
spaceUuids: spaces.map((space) => space.uuid),
})
.andWhere('tag.uuid IN (:...tagUuidsToDelete)', { tagUuidsToDelete })
.getMany();
if (devicesToMove.length === 0) return;
await queryRunner.manager
.createQueryBuilder()
.update(DeviceEntity)
.set({ spaceDevice: orphanSpace })
.where('uuid IN (:...deviceUuids)', {
deviceUuids: devicesToMove.map((device) => device.uuid),
})
.execute();
} catch (error) {
throw this.handleError(error, `Failed to move devices to orphan space`);
}
}
private async processDeleteActions(
dtos: ModifyTagDto[],
queryRunner: QueryRunner,
space: SpaceEntity,
): Promise<SpaceProductAllocationEntity[]> {
try {
if (!dtos || dtos.length === 0) return;
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) },
space: { uuid: space.uuid },
},
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_uuid NOT IN (' +
queryRunner.manager
.createQueryBuilder()
.select('allocation.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,
): Promise<boolean> {
const existingAllocationsForProduct = await queryRunner.manager.find(
SpaceProductAllocationEntity,
{
where: {
space: {
uuid: space.uuid,
},
product: {
uuid: tag.product.uuid,
},
},
relations: ['tags'],
},
);
if (
!existingAllocationsForProduct ||
existingAllocationsForProduct.length === 0
) {
return false;
}
const existingTagsForProduct = existingAllocationsForProduct.flatMap(
(allocation) => allocation.tags || [],
);
return existingTagsForProduct.some(
(existingTag) => existingTag.uuid === tag.uuid,
);
}
private async getAllocationByProduct(
product: ProductEntity,
space: SpaceEntity,
queryRunner?: QueryRunner,
): Promise<SpaceProductAllocationEntity | null> {
return queryRunner
? queryRunner.manager.findOne(SpaceProductAllocationEntity, {
where: {
space: { uuid: space.uuid },
product: { uuid: product.uuid },
},
relations: ['tags'],
})
: this.spaceProductAllocationRepository.findOne({
where: {
space: { uuid: space.uuid },
product: { uuid: product.uuid },
},
relations: ['tags'],
});
}
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);
}
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,
);
}
async clearAllAllocations(spaceUuid: string, queryRunner: QueryRunner) {
try {
const allocationUuids = await queryRunner.manager
.createQueryBuilder(SpaceProductAllocationEntity, 'allocation')
.select('allocation.uuid')
.where('allocation.space_uuid = :spaceUuid', { spaceUuid })
.getRawMany()
.then((results) => results.map((r) => r.allocation_uuid));
if (allocationUuids.length === 0) {
return;
}
await queryRunner.manager
.createQueryBuilder()
.delete()
.from('space_product_tags')
.where('space_product_allocation_uuid IN (:...allocationUuids)', {
allocationUuids,
})
.execute();
await queryRunner.manager
.createQueryBuilder()
.delete()
.from(SpaceProductAllocationEntity)
.where('space_uuid = :spaceUuid', { spaceUuid })
.execute();
} catch (error) {
throw this.handleError(error, 'Failed to clear all allocations');
}
}
}

View File

@ -5,9 +5,9 @@ import { SceneService } from '../../scene/services';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { GetSceneDto } from '../../scene/dtos';
import { ValidationService } from './space-validation.service';
import { SpaceEntity } from '@app/common/modules/space';
import { QueryRunner } from 'typeorm';
import { SceneEntity } from '@app/common/modules/scene/entities';
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
@Injectable()
export class SpaceSceneService {

View File

@ -1,6 +1,10 @@
import { SpaceEntity } from '@app/common/modules/space/entities';
import { SpaceRepository } from '@app/common/modules/space/repositories';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import {
BadRequestException,
HttpException,
HttpStatus,
Injectable,
} from '@nestjs/common';
import { CommunityService } from '../../community/services';
import { ProjectService } from '../../project/services';
import {
@ -10,6 +14,16 @@ import {
import { ProjectRepository } from '@app/common/modules/project/repositiories';
import { CommunityRepository } from '@app/common/modules/community/repositories';
import { DeviceRepository } from '@app/common/modules/device/repositories';
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
import { ValidateSpacesDto } from '../dtos/validation.space.dto';
import { ProjectParam } from '../dtos';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { In } from 'typeorm';
import {
ORPHAN_COMMUNITY_NAME,
ORPHAN_SPACE_NAME,
} from '@app/common/constants/orphan-constant';
import { ProjectEntity } from '@app/common/modules/project/entities';
@Injectable()
export class ValidationService {
@ -22,7 +36,37 @@ export class ValidationService {
private readonly spaceModelRepository: SpaceModelRepository,
private readonly deviceRepository: DeviceRepository,
) {}
async validateSpacesWithDevicesOrSubspaces(
validateSpacesDto: ValidateSpacesDto,
projectParam: ProjectParam,
) {
const { spacesUuids } = validateSpacesDto;
const { projectUuid } = projectParam;
await this.communityService.validateProject(projectUuid);
const spaces = await this.spaceRepository.find({
where: { uuid: In(spacesUuids), disabled: false },
relations: ['devices', 'subspaces', 'productAllocations'],
});
const hasInvalidSpaces = spaces.some(
(space) =>
space.devices.length > 0 ||
space.subspaces.length > 0 ||
space.productAllocations.length > 0,
);
if (hasInvalidSpaces) {
throw new BadRequestException(
'Selected spaces already have linked space model / sub-spaces and devices',
);
}
return new SuccessResponseDto({
statusCode: HttpStatus.OK,
message:
'Validation completed successfully. No spaces have linked devices or sub-spaces.',
});
}
async validateCommunityAndProject(
communityUuid: string,
projectUuid: string,
@ -81,8 +125,15 @@ export class ValidationService {
'children',
'subspaces',
'tags',
'productAllocations',
'productAllocations.product',
'productAllocations.tags',
'subspaces.productAllocations',
'subspaces.productAllocations.tags',
'subspaces.productAllocations.product',
'subspaces.tags',
'subspaces.devices',
'spaceModel',
],
});
@ -96,7 +147,7 @@ export class ValidationService {
const devices = await this.deviceRepository.find({
where: { spaceDevice: { uuid: spaceUuid } },
select: ['uuid', 'deviceTuyaUuid', 'isActive', 'createdAt', 'updatedAt'],
relations: ['productDevice', 'tag', 'subspace'],
relations: ['productDevice', 'subspace'],
});
space.devices = devices;
@ -129,10 +180,11 @@ export class ValidationService {
where: { uuid: spaceModelUuid },
relations: [
'subspaceModels',
'subspaceModels.tags',
'tags',
'subspaceModels.tags.product',
'tags.product',
'subspaceModels.productAllocations',
'subspaceModels.productAllocations.tags',
'subspaceModels.productAllocations.product',
'productAllocations.product',
'productAllocations.tags',
],
});
@ -250,4 +302,24 @@ export class ValidationService {
);
}
}
async getOrphanSpace(project: ProjectEntity): Promise<SpaceEntity> {
const orphanSpace = await this.spaceRepository.findOne({
where: {
community: {
name: `${ORPHAN_COMMUNITY_NAME}-${project.name}`,
},
spaceName: ORPHAN_SPACE_NAME,
},
});
if (!orphanSpace) {
throw new HttpException(
`Orphan space not found in community ${project.name}`,
HttpStatus.NOT_FOUND,
);
}
return orphanSpace;
}
}

View File

@ -12,13 +12,11 @@ import {
AddSpaceDto,
AddSubspaceDto,
CommunitySpaceParam,
CreateTagDto,
GetSpaceParam,
UpdateSpaceDto,
} from '../dtos';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { SpaceEntity } from '@app/common/modules/space/entities';
import { generateRandomString } from '@app/common/helper/randomString';
import { SpaceLinkService } from './space-link';
import { SubSpaceService } from './subspace';
@ -29,11 +27,15 @@ import {
ORPHAN_SPACE_NAME,
} 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 { SubspaceEntity } from '@app/common/modules/space/entities/subspace/subspace.entity';
@Injectable()
export class SpaceService {
constructor(
@ -43,9 +45,10 @@ export class SpaceService {
private readonly spaceLinkService: SpaceLinkService,
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,
) {}
async createSpace(
@ -93,10 +96,27 @@ export class SpaceService {
});
const newSpace = await queryRunner.manager.save(space);
const subspaceTags =
this.subSpaceService.extractTagsFromSubspace(subspaces);
const allTags = [...tags, ...subspaceTags];
this.validateUniqueTags(allTags);
if (spaceModelUuid) {
const hasDependencies = subspaces?.length > 0 || tags?.length > 0;
if (!hasDependencies) {
await this.spaceModelService.linkToSpace(
newSpace,
spaceModel,
queryRunner,
);
} else if (hasDependencies) {
throw new HttpException(
`Space cannot be linked to a model because it has existing dependencies (subspaces or tags).`,
HttpStatus.BAD_REQUEST,
);
}
}
await Promise.all([
spaceModelUuid &&
this.createFromModel(spaceModelUuid, queryRunner, newSpace),
direction && parent
? this.spaceLinkService.saveSpaceLink(
parent.uuid,
@ -106,10 +126,16 @@ export class SpaceService {
)
: Promise.resolve(),
subspaces?.length
? this.createSubspaces(subspaces, newSpace, queryRunner, tags)
? this.createSubspaces(
subspaces,
newSpace,
queryRunner,
null,
projectUuid,
)
: Promise.resolve(),
tags?.length
? this.createTags(tags, queryRunner, newSpace)
? this.createTags(tags, projectUuid, queryRunner, newSpace)
: Promise.resolve(),
]);
@ -131,39 +157,29 @@ export class SpaceService {
await queryRunner.release();
}
}
private validateUniqueTags(allTags: ProcessTagDto[]) {
const tagUuidSet = new Set<string>();
const tagNameProductSet = new Set<string>();
async createFromModel(
spaceModelUuid: string,
queryRunner: QueryRunner,
space: SpaceEntity,
) {
try {
const spaceModel =
await this.spaceModelService.validateSpaceModel(spaceModelUuid);
space.spaceModel = spaceModel;
await queryRunner.manager.save(SpaceEntity, space);
await this.subSpaceService.createSubSpaceFromModel(
spaceModel.subspaceModels,
space,
queryRunner,
);
await this.tagService.createTagsFromModel(
queryRunner,
spaceModel.tags,
space,
null,
);
} catch (error) {
if (error instanceof HttpException) {
throw error;
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);
}
throw new HttpException(
'An error occurred while creating the space from space model',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@ -193,12 +209,8 @@ export class SpaceService {
'incomingConnections.disabled = :incomingConnectionDisabled',
{ incomingConnectionDisabled: false },
)
.leftJoinAndSelect(
'space.tags',
'tags',
'tags.disabled = :tagDisabled',
{ tagDisabled: false },
)
.leftJoinAndSelect('space.productAllocations', 'productAllocations')
.leftJoinAndSelect('productAllocations.tags', 'tags')
.leftJoinAndSelect('tags.product', 'tagProduct')
.leftJoinAndSelect(
'space.subspaces',
@ -207,11 +219,10 @@ export class SpaceService {
{ subspaceDisabled: false },
)
.leftJoinAndSelect(
'subspaces.tags',
'subspaceTags',
'subspaceTags.disabled = :subspaceTagsDisabled',
{ subspaceTagsDisabled: false },
'subspaces.productAllocations',
'subspaceProductAllocations',
)
.leftJoinAndSelect('subspaceProductAllocations.tags', 'subspaceTags')
.leftJoinAndSelect('subspaceTags.product', 'subspaceTagProduct')
.leftJoinAndSelect('space.spaceModel', 'spaceModel')
.where('space.community_id = :communityUuid', { communityUuid })
@ -226,7 +237,8 @@ export class SpaceService {
const spaces = await queryBuilder.getMany();
const spaceHierarchy = this.buildSpaceHierarchy(spaces);
const transformedSpaces = spaces.map(this.transformSpace);
const spaceHierarchy = this.buildSpaceHierarchy(transformedSpaces);
return new SuccessResponseDto({
message: `Spaces in community ${communityUuid} successfully fetched in hierarchy`,
@ -241,6 +253,28 @@ export class SpaceService {
}
}
private transformSpace(space) {
const { productAllocations, subspaces, ...restSpace } = space;
const tags = productAllocations.flatMap((pa) => pa.tags);
const transformedSubspaces = subspaces.map((subspace) => {
const {
productAllocations: subspaceProductAllocations,
...restSubspace
} = subspace;
const subspaceTags = subspaceProductAllocations.flatMap((pa) => pa.tags);
return {
...restSubspace,
tags: subspaceTags,
};
});
return {
...restSpace,
tags,
subspaces: transformedSubspaces,
};
}
async findOne(params: GetSpaceParam): Promise<BaseResponseDto> {
const { communityUuid, spaceUuid, projectUuid } = params;
try {
@ -310,6 +344,10 @@ export class SpaceService {
}
async delete(params: GetSpaceParam): Promise<BaseResponseDto> {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const { communityUuid, spaceUuid, projectUuid } = params;
@ -337,20 +375,42 @@ export class SpaceService {
},
});
await this.spaceProductAllocationService.clearAllAllocations(
spaceUuid,
queryRunner,
);
const subspaces = await queryRunner.manager.find(SubspaceEntity, {
where: { space: { uuid: spaceUuid } },
});
const subspaceUuids = subspaces.map((subspace) => subspace.uuid);
if (subspaceUuids.length > 0) {
await this.subSpaceService.clearSubspaces(subspaceUuids, queryRunner);
}
await this.disableSpace(space, orphanSpace);
await queryRunner.commitTransaction();
return new SuccessResponseDto({
message: `Space with ID ${spaceUuid} successfully deleted`,
statusCode: HttpStatus.OK,
});
} catch (error) {
await queryRunner.rollbackTransaction();
console.error('Error deleting space:', error);
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
`An error occurred while deleting the space ${error.message}`,
`An error occurred while deleting the space: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
await queryRunner.release();
}
}
@ -371,6 +431,9 @@ export class SpaceService {
try {
await queryRunner.connect();
await queryRunner.startTransaction();
const project = await this.spaceModelService.validateProject(
params.projectUuid,
);
const space =
await this.validationService.validateSpaceWithinCommunityAndProject(
@ -386,44 +449,70 @@ export class SpaceService {
);
}
if (space.spaceModel && !updateSpaceDto.spaceModelUuid) {
space.spaceModel = null;
await this.unlinkSpaceFromModel(space, queryRunner);
}
this.updateSpaceProperties(space, updateSpaceDto);
const hasSubspace = updateSpaceDto.subspace?.length > 0;
const hasTags = updateSpaceDto.tags?.length > 0;
if (updateSpaceDto.spaceModelUuid) {
const spaceModel = await this.validationService.validateSpaceModel(
updateSpaceDto.spaceModelUuid,
);
if (hasSubspace || hasTags) {
const hasDependencies =
space.devices?.length > 0 ||
space.subspaces?.length > 0 ||
space.productAllocations?.length > 0;
if (!hasDependencies && !space.spaceModel) {
await this.spaceModelService.linkToSpace(
space,
spaceModel,
queryRunner,
);
} else if (hasDependencies) {
await this.spaceModelService.overwriteSpace(
space,
project,
queryRunner,
);
await this.spaceModelService.linkToSpace(
space,
spaceModel,
queryRunner,
);
}
}
const hasSubspace = updateSpaceDto.subspace?.length > 0;
if (hasSubspace) {
space.spaceModel = null;
await this.tagService.unlinkModels(space.tags, queryRunner);
await this.subSpaceService.unlinkModels(space.subspaces, queryRunner);
}
await queryRunner.manager.save(space);
if (hasSubspace) {
const modifiedSubspaces = this.tagService.getModifiedSubspaces(
updateSpaceDto.tags,
updateSpaceDto.subspace,
);
await this.subSpaceService.modifySubSpace(
modifiedSubspaces,
updateSpaceDto.subspace,
queryRunner,
space,
projectUuid,
updateSpaceDto.tags,
);
}
if (hasTags) {
const spaceTagsAfterMove = this.tagService.getSubspaceTagsToBeAdded(
if (updateSpaceDto.tags) {
await this.spaceProductAllocationService.updateSpaceProductAllocations(
updateSpaceDto.tags,
projectUuid,
space,
queryRunner,
updateSpaceDto.subspace,
);
await this.tagService.modifyTags(
spaceTagsAfterMove,
queryRunner,
space,
);
}
await queryRunner.commitTransaction();
return new SuccessResponseDto({
@ -438,7 +527,7 @@ export class SpaceService {
}
throw new HttpException(
'An error occurred while updating the space',
`An error occurred while updating the space: error ${error}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
@ -451,18 +540,12 @@ export class SpaceService {
queryRunner: QueryRunner,
): Promise<void> {
try {
await queryRunner.manager.update(
this.spaceRepository.target,
{ uuid: space.uuid },
{
spaceModel: null,
},
);
// Unlink subspaces and tags if they exist
if (space.subspaces || space.tags) {
if (space.tags) {
await this.tagService.unlinkModels(space.tags, queryRunner);
await this.spaceProductAllocationService.unlinkModels(
space,
queryRunner,
);
}
if (space.subspaces) {
@ -559,14 +642,8 @@ export class SpaceService {
const map = new Map<string, SpaceEntity>();
// Step 1: Create a map of spaces by UUID
spaces.forEach((space) => {
map.set(
space.uuid,
this.spaceRepository.create({
...space,
children: [], // Add children if needed
}),
);
spaces.forEach((space: any) => {
map.set(space.uuid, { ...space, children: [] }); // Ensure children are reset
});
// Step 2: Organize the hierarchy
@ -574,9 +651,14 @@ export class SpaceService {
spaces.forEach((space) => {
if (space.parent && space.parent.uuid) {
const parent = map.get(space.parent.uuid);
parent?.children?.push(map.get(space.uuid));
if (parent) {
const child = map.get(space.uuid);
if (child && !parent.children.some((c) => c.uuid === child.uuid)) {
parent.children.push(child);
}
}
} else {
rootSpaces.push(map.get(space.uuid));
rootSpaces.push(map.get(space.uuid)!); // Push only root spaces
}
});
@ -603,21 +685,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,
null,
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

@ -81,24 +81,6 @@ export class SubspaceDeviceService {
const subspace = await this.findSubspace(subSpaceUuid);
const device = await this.findDevice(deviceUuid);
if (device.tag?.subspace?.uuid !== subspace.uuid) {
await this.tagRepository.update(
{ uuid: device.tag.uuid },
{ subspace, space: null },
);
}
if (!device.tag) {
const tag = this.tagRepository.create({
tag: `Tag ${this.findNextTag()}`,
product: device.productDevice,
subspace: subspace,
device: device,
});
await this.tagRepository.save(tag);
device.tag = tag;
}
device.subspace = subspace;
const newDevice = await this.deviceRepository.save(device);
@ -140,26 +122,6 @@ export class SubspaceDeviceService {
);
}
if (device.tag?.subspace !== null) {
await this.tagRepository.update(
{ uuid: device.tag.uuid },
{ subspace: null, space: device.spaceDevice },
);
}
if (!device.tag) {
const tag = this.tagRepository.create({
tag: `Tag ${this.findNextTag()}`,
product: device.productDevice,
subspace: null,
space: device.spaceDevice,
device: device,
});
await this.tagRepository.save(tag);
device.tag = tag;
}
device.subspace = null;
const updatedDevice = await this.deviceRepository.save(device);

View File

@ -0,0 +1,577 @@
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 { SubspaceProductAllocationEntity } from '@app/common/modules/space/entities/subspace/subspace-product-allocation.entity';
import { SubspaceEntity } from '@app/common/modules/space/entities/subspace/subspace.entity';
import { SubspaceProductAllocationRepository } from '@app/common/modules/space/repositories/subspace.repository';
import { NewTagEntity } from '@app/common/modules/tag';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { ModifyTagDto } from 'src/space/dtos/tag/modify-tag.dto';
import { ISingleSubspace } from 'src/space/interfaces/single-subspace.interface';
import { ProcessTagDto } from 'src/tags/dtos';
import { TagService as NewTagService } from 'src/tags/services';
import { In, QueryRunner } from 'typeorm';
@Injectable()
export class SubspaceProductAllocationService {
constructor(
private readonly tagService: NewTagService,
private readonly spaceProductAllocationRepository: SpaceProductAllocationRepository,
private readonly subspaceProductAllocationRepository: SubspaceProductAllocationRepository,
) {}
async createSubspaceProductAllocations(
subspace: SubspaceEntity,
processedTags: NewTagEntity[],
queryRunner?: QueryRunner,
spaceAllocationsToExclude?: SpaceProductAllocationEntity[],
): Promise<void> {
try {
if (!processedTags.length) return;
const allocations: SubspaceProductAllocationEntity[] = [];
for (const tag of processedTags) {
await this.validateTagWithinSubspace(
queryRunner,
tag,
subspace,
spaceAllocationsToExclude,
);
let allocation = await this.getAllocationByProduct(
tag.product,
subspace,
queryRunner,
);
if (!allocation) {
allocation = this.createNewSubspaceAllocation(
subspace,
tag,
queryRunner,
);
allocations.push(allocation);
} else if (!allocation.tags.some((t) => t.uuid === tag.uuid)) {
allocation.tags.push(tag);
await this.saveAllocation(allocation, queryRunner);
}
}
if (allocations.length > 0) {
await this.saveAllocations(allocations, queryRunner);
}
} catch (error) {
throw this.handleError(
error,
'Failed to create subspace product allocations',
);
}
}
async updateSubspaceProductAllocations(
subspaces: ISingleSubspace[],
projectUuid: string,
queryRunner: QueryRunner,
space: SpaceEntity,
spaceTagUpdateDtos?: ModifyTagDto[],
) {
const spaceAllocationToExclude: SpaceProductAllocationEntity[] = [];
for (const subspace of subspaces) {
if (!subspace.tags || subspace.tags.length === 0) continue;
const tagDtos = subspace.tags;
const tagsToAddDto: ProcessTagDto[] = tagDtos
.filter((dto) => dto.action === ModifyAction.ADD)
.map((dto) => ({
name: dto.name,
productUuid: dto.productUuid,
uuid: dto.newTagUuid,
}));
const tagsToDeleteDto = tagDtos.filter(
(dto) => dto.action === ModifyAction.DELETE,
);
if (tagsToAddDto.length > 0) {
let processedTags = await this.tagService.processTags(
tagsToAddDto,
projectUuid,
queryRunner,
);
for (const subspaceDto of subspaces) {
if (
subspaceDto !== subspace &&
subspaceDto.action === ModifyAction.UPDATE &&
subspaceDto.tags
) {
const deletedTags = subspaceDto.tags.filter(
(tagDto) =>
tagDto.action === ModifyAction.DELETE &&
processedTags.some((tag) => tag.uuid === tagDto.tagUuid),
);
for (const deletedTag of deletedTags) {
const allocation = await queryRunner.manager.findOne(
SubspaceProductAllocationEntity,
{
where: {
subspace: { uuid: subspaceDto.subspace.uuid },
},
relations: ['tags', 'product', 'subspace'],
},
);
const isCommonTag = allocation.tags.some(
(tag) => tag.uuid === deletedTag.tagUuid,
);
if (allocation && isCommonTag) {
const tagEntity = allocation.tags.find(
(tag) => tag.uuid === deletedTag.tagUuid,
);
allocation.tags = allocation.tags.filter(
(tag) => tag.uuid !== deletedTag.tagUuid,
);
await queryRunner.manager.save(allocation);
const productAllocationExistInSubspace =
await queryRunner.manager.findOne(
SubspaceProductAllocationEntity,
{
where: {
subspace: {
uuid: subspaceDto.subspace.uuid,
},
product: { uuid: allocation.product.uuid },
},
relations: ['tags'],
},
);
if (productAllocationExistInSubspace) {
productAllocationExistInSubspace.tags.push(tagEntity);
await queryRunner.manager.save(
productAllocationExistInSubspace,
);
} else {
const newProductAllocation = queryRunner.manager.create(
SubspaceProductAllocationEntity,
{
subspace: subspace.subspace,
product: allocation.product,
tags: [tagEntity],
},
);
await queryRunner.manager.save(newProductAllocation);
}
processedTags = processedTags.filter(
(tag) => tag.uuid !== deletedTag.tagUuid,
);
subspaceDto.tags = subspaceDto.tags.filter(
(tagDto) => tagDto.tagUuid !== deletedTag.tagUuid,
);
}
}
}
if (
subspaceDto !== subspace &&
subspaceDto.action === ModifyAction.DELETE
) {
const allocation = await queryRunner.manager.findOne(
SubspaceProductAllocationEntity,
{
where: {
subspace: { uuid: subspaceDto.subspace.uuid },
},
relations: ['tags'],
},
);
const repeatedTags = allocation?.tags.filter((tag) =>
processedTags.some(
(processedTag) => processedTag.uuid === tag.uuid,
),
);
if (repeatedTags.length > 0) {
allocation.tags = allocation.tags.filter(
(tag) =>
!repeatedTags.some(
(repeatedTag) => repeatedTag.uuid === tag.uuid,
),
);
await queryRunner.manager.save(allocation);
const productAllocationExistInSubspace =
await queryRunner.manager.findOne(
SubspaceProductAllocationEntity,
{
where: {
subspace: { uuid: subspaceDto.subspace.uuid },
product: { uuid: allocation.product.uuid },
},
relations: ['tags'],
},
);
if (productAllocationExistInSubspace) {
productAllocationExistInSubspace.tags.push(...repeatedTags);
await queryRunner.manager.save(
productAllocationExistInSubspace,
);
} else {
const newProductAllocation = queryRunner.manager.create(
SubspaceProductAllocationEntity,
{
subspace: subspace.subspace,
product: allocation.product,
tags: repeatedTags,
},
);
await queryRunner.manager.save(newProductAllocation);
}
const newAllocation = queryRunner.manager.create(
SubspaceProductAllocationEntity,
{
subspace: subspace.subspace,
product: allocation.product,
tags: repeatedTags,
},
);
await queryRunner.manager.save(newAllocation);
}
}
}
if (spaceTagUpdateDtos) {
const deletedSpaceTags = spaceTagUpdateDtos.filter(
(tagDto) =>
tagDto.action === ModifyAction.DELETE &&
processedTags.some((tag) => tag.uuid === tagDto.tagUuid),
);
for (const deletedTag of deletedSpaceTags) {
const allocation = await queryRunner.manager.findOne(
SpaceProductAllocationEntity,
{
where: {
space: { uuid: space.uuid },
tags: { uuid: deletedTag.tagUuid },
},
relations: ['tags', 'subspace'],
},
);
if (
allocation &&
allocation.tags.some((tag) => tag.uuid === deletedTag.tagUuid)
) {
spaceAllocationToExclude.push(allocation);
}
}
}
await this.createSubspaceProductAllocations(
subspace.subspace,
processedTags,
queryRunner,
spaceAllocationToExclude,
);
}
if (tagsToDeleteDto.length > 0) {
await this.processDeleteActions(tagsToDeleteDto, queryRunner);
}
}
}
async processDeleteActions(
dtos: ModifyTagDto[],
queryRunner: QueryRunner,
): Promise<SubspaceProductAllocationEntity[]> {
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: { tags: { uuid: In(tagUuidsToDelete) } },
relations: ['tags'],
},
);
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,
) {
try {
if (allocations.length === 0) return;
const allocationUuids = allocations.map((allocation) => allocation.uuid);
await queryRunner.manager.update(
SubspaceProductAllocationEntity,
{ uuid: In(allocationUuids) },
{ inheritedFromModel: null },
);
} catch (error) {
throw new HttpException(
'Failed to unlink models',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
private async validateTagWithinSubspace(
queryRunner: QueryRunner | undefined,
tag: NewTagEntity,
subspace: SubspaceEntity,
spaceAllocationsToExclude?: SpaceProductAllocationEntity[],
): Promise<void> {
const existingTagInSpace = await (queryRunner
? queryRunner.manager.findOne(SpaceProductAllocationEntity, {
where: {
product: { uuid: tag.product.uuid },
space: { uuid: subspace.space.uuid },
tags: { uuid: tag.uuid },
},
relations: ['tags'],
})
: this.spaceProductAllocationRepository.findOne({
where: {
product: { uuid: tag.product.uuid },
space: { uuid: subspace.space.uuid },
tags: { uuid: tag.uuid },
},
relations: ['tags'],
}));
const isExcluded = spaceAllocationsToExclude?.some(
(excludedAllocation) =>
excludedAllocation.product.uuid === tag.product.uuid &&
excludedAllocation.tags.some((t) => t.uuid === tag.uuid),
);
if (!isExcluded && existingTagInSpace) {
throw new HttpException(
`Tag ${tag.uuid} (Product: ${tag.product.uuid}) is already allocated at the space level (${subspace.space.uuid}). Cannot allocate the same tag in a subspace.`,
HttpStatus.BAD_REQUEST,
);
}
const existingTagInSameSpace = await (queryRunner
? queryRunner.manager.findOne(SubspaceProductAllocationEntity, {
where: {
product: { uuid: tag.product.uuid },
subspace: { space: subspace.space },
tags: { uuid: tag.uuid },
},
relations: ['subspace', 'tags'],
})
: this.subspaceProductAllocationRepository.findOne({
where: {
product: { uuid: tag.product.uuid },
subspace: { space: subspace.space },
tags: { uuid: tag.uuid },
},
relations: ['subspace', 'tags'],
}));
if (
existingTagInSameSpace &&
existingTagInSameSpace.subspace.uuid !== subspace.uuid
) {
throw new HttpException(
`Tag ${tag.uuid} (Product: ${tag.product.uuid}) is already allocated in another subspace (${existingTagInSameSpace.subspace.uuid}) within the same space (${subspace.space.uuid}).`,
HttpStatus.BAD_REQUEST,
);
}
}
private createNewSubspaceAllocation(
subspace: SubspaceEntity,
tag: NewTagEntity,
queryRunner?: QueryRunner,
): SubspaceProductAllocationEntity {
return queryRunner
? queryRunner.manager.create(SubspaceProductAllocationEntity, {
subspace,
product: tag.product,
tags: [tag],
})
: this.subspaceProductAllocationRepository.create({
subspace,
product: tag.product,
tags: [tag],
});
}
private async getAllocationByProduct(
product: ProductEntity,
subspace: SubspaceEntity,
queryRunner?: QueryRunner,
): Promise<SubspaceProductAllocationEntity | null> {
return queryRunner
? queryRunner.manager.findOne(SubspaceProductAllocationEntity, {
where: {
subspace: { uuid: subspace.uuid },
product: { uuid: product.uuid },
},
relations: ['tags'],
})
: this.subspaceProductAllocationRepository.findOne({
where: {
subspace: { uuid: subspace.uuid },
product: { uuid: product.uuid },
},
relations: ['tags'],
});
}
private async saveAllocation(
allocation: SubspaceProductAllocationEntity,
queryRunner?: QueryRunner,
): Promise<void> {
if (queryRunner) {
await queryRunner.manager.save(
SubspaceProductAllocationEntity,
allocation,
);
} else {
await this.subspaceProductAllocationRepository.save(allocation);
}
}
private async saveAllocations(
allocations: SubspaceProductAllocationEntity[],
queryRunner?: QueryRunner,
): Promise<void> {
if (queryRunner) {
await queryRunner.manager.save(
SubspaceProductAllocationEntity,
allocations,
);
} else {
await this.subspaceProductAllocationRepository.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,
);
}
async clearAllAllocations(subspaceUuids: string[], queryRunner: QueryRunner) {
try {
const allocationUuids = await queryRunner.manager
.createQueryBuilder(SubspaceProductAllocationEntity, 'allocation')
.select('allocation.uuid')
.where('allocation.subspace_uuid IN (:...subspaceUuids)', {
subspaceUuids,
})
.getRawMany()
.then((results) => results.map((r) => r.allocation_uuid));
if (allocationUuids.length === 0) {
return;
}
await queryRunner.manager
.createQueryBuilder()
.delete()
.from('subspace_product_tags')
.where('subspace_product_allocation_uuid IN (:...allocationUuids)', {
allocationUuids,
})
.execute();
await queryRunner.manager
.createQueryBuilder()
.delete()
.from(SubspaceProductAllocationEntity)
.where('subspace_uuid IN (:...subspaceUuids)', { subspaceUuids })
.execute();
} catch (error) {
throw new HttpException(
error instanceof HttpException
? error.message
: 'An unexpected error occurred while clearing allocations',
error instanceof HttpException
? error.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}

View File

@ -2,8 +2,6 @@ import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import {
AddSubspaceDto,
CreateTagDto,
DeleteSubspaceDto,
GetSpaceParam,
GetSubSpaceParam,
ModifySubspaceDto,
@ -16,10 +14,7 @@ import {
import { PageResponse } from '@app/common/dto/pagination.response.dto';
import { SubspaceDto } from '@app/common/modules/space/dtos';
import { In, QueryRunner } from 'typeorm';
import {
SpaceEntity,
SubspaceEntity,
} from '@app/common/modules/space/entities';
import { SubspaceModelEntity } from '@app/common/modules/space-model';
import { ValidationService } from '../space-validation.service';
import { SubspaceRepository } from '@app/common/modules/space/repositories/subspace.repository';
@ -27,6 +22,12 @@ import { TagService } from '../tag';
import { ModifyAction } from '@app/common/constants/modify-action.enum';
import { SubspaceDeviceService } from './subspace-device.service';
import { ModifyTagDto } from 'src/space/dtos/tag/modify-tag.dto';
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 { TagService as NewTagService } from 'src/tags/services/tags.service';
import { SubspaceProductAllocationService } from './subspace-product-allocation.service';
import { SubspaceProductAllocationEntity } from '@app/common/modules/space/entities/subspace/subspace-product-allocation.entity';
@Injectable()
export class SubSpaceService {
@ -34,7 +35,9 @@ export class SubSpaceService {
private readonly subspaceRepository: SubspaceRepository,
private readonly validationService: ValidationService,
private readonly tagService: TagService,
private readonly newTagService: NewTagService,
public readonly deviceService: SubspaceDeviceService,
private readonly subspaceProductAllocationService: SubspaceProductAllocationService,
) {}
async createSubspaces(
@ -52,6 +55,7 @@ export class SubSpaceService {
subspaceNames,
subspaceData[0].space,
);
const subspaces = subspaceData.map((data) =>
queryRunner.manager.create(this.subspaceRepository.target, data),
);
@ -66,36 +70,31 @@ export class SubSpaceService {
async createSubSpaceFromModel(
subspaceModels: SubspaceModelEntity[],
space: SpaceEntity,
spaces: SpaceEntity[],
queryRunner: QueryRunner,
): Promise<void> {
if (!subspaceModels?.length) return;
if (!subspaceModels?.length || !spaces?.length) return;
const subspaceData = subspaceModels.map((subSpaceModel) => ({
subspaceName: subSpaceModel.subspaceName,
space,
subSpaceModel,
}));
const subspaceData = [];
const subspaces = await this.createSubspaces(subspaceData, queryRunner);
await Promise.all(
subspaceModels.map((model, index) =>
this.tagService.createTagsFromModel(
queryRunner,
model.tags || [],
null,
subspaces[index],
),
),
);
for (const space of spaces) {
for (const subSpaceModel of subspaceModels) {
subspaceData.push({
subspaceName: subSpaceModel.subspaceName,
space,
subSpaceModel,
});
}
}
await this.createSubspaces(subspaceData, queryRunner);
}
async createSubspacesFromDto(
addSubspaceDtos: AddSubspaceDto[],
space: SpaceEntity,
queryRunner: QueryRunner,
otherTags?: CreateTagDto[],
otherTags?: ProcessTagDto[],
projectUuid?: string,
): Promise<SubspaceEntity[]> {
try {
this.checkForDuplicateNames(
@ -108,20 +107,23 @@ export class SubSpaceService {
}));
const subspaces = await this.createSubspaces(subspaceData, queryRunner);
await Promise.all(
addSubspaceDtos.map(async (dto, index) => {
const otherDtoTags = addSubspaceDtos
.filter((_, i) => i !== index)
.flatMap((otherDto) => otherDto.tags || []);
const subspace = subspaces[index];
if (dto.tags?.length) {
subspace.tags = await this.tagService.createTags(
dto.tags,
const allTags = [...(dto.tags || []), ...(otherTags || [])];
if (allTags.length) {
const processedTags = await this.newTagService.processTags(
allTags,
projectUuid,
queryRunner,
null,
);
await this.subspaceProductAllocationService.createSubspaceProductAllocations(
subspace,
[...(otherTags || []), ...otherDtoTags],
processedTags,
queryRunner,
);
}
}),
@ -275,11 +277,11 @@ export class SubSpaceService {
}
}
async deleteSubspaces(
/* async deleteSubspaces(
deleteDtos: DeleteSubspaceDto[],
queryRunner: QueryRunner,
) {
const deleteResults: { uuid: string }[] = [];
/* const deleteResults: { uuid: string }[] = [];
for (const dto of deleteDtos) {
const subspace = await this.findOne(dto.subspaceUuid);
@ -312,21 +314,41 @@ export class SubSpaceService {
deleteResults.push({ uuid: dto.subspaceUuid });
}
return deleteResults;
}
return deleteResults;
} */
async modifySubSpace(
subspaceDtos: ModifySubspaceDto[],
queryRunner: QueryRunner,
space?: SpaceEntity,
projectUuid?: string,
spaceTagUpdateDtos?: ModifyTagDto[],
) {
if (!subspaceDtos || subspaceDtos.length === 0) {
return;
}
const addedSubspaces = [];
const updatedSubspaces = [];
for (const subspace of subspaceDtos) {
switch (subspace.action) {
case ModifyAction.ADD:
await this.handleAddAction(subspace, space, queryRunner);
const addedSubspace = await this.handleAddAction(
subspace,
space,
queryRunner,
);
if (addedSubspace) addedSubspaces.push(addedSubspace);
break;
case ModifyAction.UPDATE:
await this.handleUpdateAction(subspace, queryRunner);
const updatedSubspace = await this.handleUpdateAction(
subspace,
queryRunner,
);
if (updatedSubspace) {
updatedSubspaces.push(updatedSubspace);
}
break;
case ModifyAction.DELETE:
await this.handleDeleteAction(subspace, queryRunner);
@ -338,28 +360,50 @@ export class SubSpaceService {
);
}
}
const combinedSubspaces = [...addedSubspaces, ...updatedSubspaces].filter(
(subspace) => subspace !== undefined,
);
if (combinedSubspaces.length > 0) {
await this.subspaceProductAllocationService.updateSubspaceProductAllocations(
combinedSubspaces,
projectUuid,
queryRunner,
space,
spaceTagUpdateDtos,
);
}
}
async unlinkModels(
subspaces: SubspaceEntity[],
queryRunner: QueryRunner,
): Promise<void> {
if (!subspaces || subspaces.length === 0) {
return;
}
if (!subspaces || subspaces.length === 0) return;
try {
const allTags = subspaces.flatMap((subSpace) => {
subSpace.subSpaceModel = null;
return subSpace.tags || [];
});
const subspaceUuids = subspaces.map((subSpace) => subSpace.uuid);
await this.tagService.unlinkModels(allTags, queryRunner);
const allocations: SubspaceProductAllocationEntity[] = subspaces.flatMap(
(subspace) => subspace.productAllocations || [],
);
await queryRunner.manager.save(subspaces);
if (allocations.length > 0) {
await this.subspaceProductAllocationService.unlinkModels(
allocations,
queryRunner,
);
}
await queryRunner.manager.update(
SubspaceEntity,
{ uuid: In(subspaceUuids) },
{ subSpaceModel: null },
);
} catch (error) {
if (error instanceof HttpException) throw error;
throw new HttpException(
`Failed to unlink subspace models: ${error.message}`,
`Failed to unlink subspace models: ${error instanceof Error ? error.message : 'Unknown error'}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
@ -383,10 +427,10 @@ export class SubSpaceService {
space: SpaceEntity,
queryRunner: QueryRunner,
): Promise<SubspaceEntity> {
const createTagDtos: CreateTagDto[] =
const createTagDtos: ProcessTagDto[] =
subspace.tags?.map((tag) => ({
tag: tag.tag as string,
uuid: tag.uuid,
name: tag.name as string,
uuid: tag.tagUuid,
productUuid: tag.productUuid as string,
})) || [];
const subSpace = await this.createSubspacesFromDto(
@ -400,32 +444,22 @@ export class SubSpaceService {
private async handleUpdateAction(
modifyDto: ModifySubspaceDto,
queryRunner: QueryRunner,
): Promise<void> {
): Promise<SubspaceEntity> {
const subspace = await this.findOne(modifyDto.uuid);
await this.update(
const updatedSubspace = await this.update(
queryRunner,
subspace,
modifyDto.subspaceName,
modifyDto.tags,
);
return updatedSubspace;
}
async update(
queryRunner: QueryRunner,
subspace: SubspaceEntity,
subspaceName?: string,
modifyTagDto?: ModifyTagDto[],
) {
await this.updateSubspaceName(queryRunner, subspace, subspaceName);
if (modifyTagDto?.length) {
await this.tagService.modifyTags(
modifyTagDto,
queryRunner,
null,
subspace,
);
}
return await this.updateSubspaceName(queryRunner, subspace, subspaceName);
}
async handleDeleteAction(
@ -441,10 +475,10 @@ export class SubSpaceService {
);
if (subspace.tags?.length) {
const modifyTagDtos: CreateTagDto[] = subspace.tags.map((tag) => ({
const modifyTagDtos: ProcessTagDto[] = subspace.tags.map((tag) => ({
uuid: tag.uuid,
action: ModifyAction.ADD,
tag: tag.tag,
name: tag.tag,
productUuid: tag.product.uuid,
}));
await this.tagService.moveTags(
@ -481,11 +515,12 @@ export class SubSpaceService {
queryRunner: QueryRunner,
subSpace: SubspaceEntity,
subspaceName?: string,
): Promise<void> {
): Promise<SubspaceEntity> {
if (subspaceName) {
subSpace.subspaceName = subspaceName;
await queryRunner.manager.save(subSpace);
return await queryRunner.manager.save(subSpace);
}
return subSpace;
}
private async checkForDuplicateNames(names: string[]): Promise<void> {
@ -539,4 +574,30 @@ export class SubSpaceService {
await this.checkForDuplicateNames(names);
await this.checkExistingNamesInSpace(names, space);
}
extractTagsFromSubspace(addSubspaceDto: AddSubspaceDto[]): ProcessTagDto[] {
return addSubspaceDto.flatMap((subspace) => subspace.tags || []);
}
async clearSubspaces(subspaceUuids: string[], queryRunner: QueryRunner) {
try {
await queryRunner.manager.update(
SubspaceEntity,
{ uuid: In(subspaceUuids) },
{ disabled: true },
);
await this.subspaceProductAllocationService.clearAllAllocations(
subspaceUuids,
queryRunner,
);
} catch (error) {
throw new HttpException(
error instanceof HttpException
? error.message
: 'An unexpected error occurred while clearing subspaces',
error instanceof HttpException
? error.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}

View File

@ -1,15 +1,14 @@
import { ModifyAction } from '@app/common/constants/modify-action.enum';
import {
SpaceEntity,
SubspaceEntity,
TagEntity,
TagRepository,
} from '@app/common/modules/space';
import { TagRepository } from '@app/common/modules/space';
import { TagModel } from '@app/common/modules/space-model';
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
import { SubspaceEntity } from '@app/common/modules/space/entities/subspace/subspace.entity';
import { TagEntity } from '@app/common/modules/space/entities/tag.entity';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { ProductService } from 'src/product/services';
import { CreateTagDto, ModifySubspaceDto } from 'src/space/dtos';
import { ModifySubspaceDto } from 'src/space/dtos';
import { ModifyTagDto } from 'src/space/dtos/tag/modify-tag.dto';
import { ProcessTagDto } from 'src/tags/dtos';
import { QueryRunner } from 'typeorm';
@Injectable()
@ -20,11 +19,11 @@ export class TagService {
) {}
async createTags(
tags: CreateTagDto[],
tags: ProcessTagDto[],
queryRunner: QueryRunner,
space?: SpaceEntity,
subspace?: SubspaceEntity,
additionalTags?: CreateTagDto[],
additionalTags?: ProcessTagDto[],
tagsToDelete?: ModifyTagDto[],
): Promise<TagEntity[]> {
this.validateTagsInput(tags);
@ -57,7 +56,7 @@ export class TagService {
}
async bulkSaveTags(
tags: CreateTagDto[],
tags: ProcessTagDto[],
queryRunner: QueryRunner,
space?: SpaceEntity,
subspace?: SubspaceEntity,
@ -94,7 +93,7 @@ export class TagService {
}
async moveTags(
tags: CreateTagDto[],
tags: ProcessTagDto[],
queryRunner: QueryRunner,
space?: SpaceEntity,
subspace?: SubspaceEntity,
@ -136,15 +135,11 @@ export class TagService {
return tag;
} catch (error) {
console.error(
`Error moving tag with UUID ${tagDto.uuid}: ${error.message}`,
);
throw error; // Re-throw the error to propagate it to the parent Promise.all
throw error;
}
}),
);
} catch (error) {
console.error(`Error in moveTags: ${error.message}`);
throw new HttpException(
`Failed to move tags due to an unexpected error: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
@ -179,20 +174,20 @@ export class TagService {
subspace?: SubspaceEntity,
): Promise<TagEntity> {
try {
const existingTag = await this.getTagByUuid(tag.uuid);
const existingTag = await this.getTagByUuid(tag.tagUuid);
const contextSpace = space ?? subspace?.space;
if (contextSpace && tag.tag !== existingTag.tag) {
if (contextSpace && tag.name !== existingTag.tag) {
await this.checkTagReuse(
tag.tag,
tag.name,
existingTag.product.uuid,
contextSpace,
);
}
return await queryRunner.manager.save(
Object.assign(existingTag, { tag: tag.tag }),
Object.assign(existingTag, { tag: tag.name }),
);
} catch (error) {
throw this.handleUnexpectedError('Failed to update tags', error);
@ -299,9 +294,9 @@ export class TagService {
await this.createTags(
[
{
tag: tag.tag,
name: tag.name,
productUuid: tag.productUuid,
uuid: tag.uuid,
uuid: tag.tagUuid,
},
],
queryRunner,
@ -315,7 +310,7 @@ export class TagService {
await this.updateTag(tag, queryRunner, space, subspace);
break;
case ModifyAction.DELETE:
await this.deleteTags([tag.uuid], queryRunner);
await this.deleteTags([tag.tagUuid], queryRunner);
break;
default:
throw new HttpException(
@ -344,14 +339,14 @@ export class TagService {
}
}
private findDuplicateTags(tags: CreateTagDto[]): string[] {
private findDuplicateTags(tags: ProcessTagDto[]): string[] {
const seen = new Map<string, boolean>();
const duplicates: string[] = [];
tags.forEach((tagDto) => {
const key = `${tagDto.productUuid}-${tagDto.tag}`;
const key = `${tagDto.productUuid}-${tagDto.name}`;
if (seen.has(key)) {
duplicates.push(`${tagDto.tag} for Product: ${tagDto.productUuid}`);
duplicates.push(`${tagDto.name} for Product: ${tagDto.productUuid}`);
} else {
seen.set(key, true);
}
@ -387,7 +382,9 @@ export class TagService {
const filteredTagExists = tagExists.filter(
(existingTag) =>
!tagsToDelete?.some((deleteTag) => deleteTag.uuid === existingTag.uuid),
!tagsToDelete?.some(
(deleteTag) => deleteTag.tagUuid === existingTag.uuid,
),
);
if (filteredTagExists.length > 0) {
@ -396,7 +393,7 @@ export class TagService {
}
private async prepareTagEntity(
tagDto: CreateTagDto,
tagDto: ProcessTagDto,
queryRunner: QueryRunner,
space?: SpaceEntity,
subspace?: SubspaceEntity,
@ -414,14 +411,14 @@ export class TagService {
if (space) {
await this.checkTagReuse(
tagDto.tag,
tagDto.name,
tagDto.productUuid,
space,
tagsToDelete,
);
} else if (subspace && subspace.space) {
await this.checkTagReuse(
tagDto.tag,
tagDto.name,
tagDto.productUuid,
subspace.space,
);
@ -433,7 +430,7 @@ export class TagService {
}
return queryRunner.manager.create(TagEntity, {
tag: tagDto.tag,
tag: tagDto.name,
product: product.data,
space: space,
subspace: subspace,
@ -476,13 +473,13 @@ export class TagService {
}
private combineTags(
primaryTags: CreateTagDto[],
additionalTags?: CreateTagDto[],
): CreateTagDto[] {
primaryTags: ProcessTagDto[],
additionalTags?: ProcessTagDto[],
): ProcessTagDto[] {
return additionalTags ? [...primaryTags, ...additionalTags] : primaryTags;
}
private ensureNoDuplicateTags(tags: CreateTagDto[]): void {
private ensureNoDuplicateTags(tags: ProcessTagDto[]): void {
const duplicates = this.findDuplicateTags(tags);
if (duplicates.length > 0) {
@ -493,7 +490,7 @@ export class TagService {
}
}
private validateTagsInput(tags: CreateTagDto[]): void {
private validateTagsInput(tags: ProcessTagDto[]): void {
if (!tags?.length) {
return;
}
@ -519,14 +516,14 @@ export class TagService {
tagsToAdd
.filter((tagToAdd) =>
spaceTagsToDelete.some(
(tagToDelete) => tagToAdd.uuid === tagToDelete.uuid,
(tagToDelete) => tagToAdd.tagUuid === tagToDelete.tagUuid,
),
)
.map((tag) => tag.uuid),
.map((tag) => tag.tagUuid),
);
const remainingTags = spaceTags.filter(
(tag) => !commonTagUuids.has(tag.uuid), // Exclude tags in commonTagUuids
(tag) => !commonTagUuids.has(tag.tagUuid), // Exclude tags in commonTagUuids
);
return remainingTags;
@ -553,11 +550,11 @@ export class TagService {
);
const subspaceTagsToDeleteUuids = new Set(
subspaceTagsToDelete.map((tag) => tag.uuid),
subspaceTagsToDelete.map((tag) => tag.tagUuid),
);
const commonTagsInSubspaces = subspaceTagsToAdd.filter((tag) =>
subspaceTagsToDeleteUuids.has(tag.uuid),
subspaceTagsToDeleteUuids.has(tag.tagUuid),
);
// Find UUIDs of tags that are common between spaceTagsToAdd and subspace tags marked for deletion
@ -569,17 +566,18 @@ export class TagService {
subspace.tags?.filter(
(tagToDelete) =>
tagToDelete.action === 'delete' &&
tagToAdd.uuid === tagToDelete.uuid,
tagToAdd.tagUuid === tagToDelete.tagUuid,
) || [],
),
)
.map((tag) => tag.uuid),
.map((tag) => tag.tagUuid),
);
// Modify subspaceModels by removing tags with UUIDs present in commonTagUuids
let modifiedSubspaces = subspaceModels.map((subspace) => ({
...subspace,
tags: subspace.tags?.filter((tag) => !commonTagUuids.has(tag.uuid)) || [],
tags:
subspace.tags?.filter((tag) => !commonTagUuids.has(tag.tagUuid)) || [],
}));
modifiedSubspaces = modifiedSubspaces.map((subspace) => ({
@ -590,7 +588,7 @@ export class TagService {
!(
tag.action === 'delete' &&
commonTagsInSubspaces.some(
(commonTag) => commonTag.uuid === tag.uuid,
(commonTag) => commonTag.tagUuid === tag.tagUuid,
)
),
) || [],

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 {
@ -46,18 +47,22 @@ import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
import { ProjectRepository } from '@app/common/modules/project/repositiories';
import {
SpaceModelProductAllocationRepoitory,
SpaceModelRepository,
SubspaceModelProductAllocationRepoitory,
SubspaceModelRepository,
TagModelRepository,
} 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,
SubSpaceModelService,
TagModelService,
} from 'src/space-model/services';
import { UserService, UserSpaceService } from 'src/users/services';
import { UserDevicePermissionService } from 'src/user-device-permission/services';
@ -71,6 +76,13 @@ import {
InviteUserSpaceRepository,
} from '@app/common/modules/Invite-user/repositiories';
import { AutomationRepository } from '@app/common/modules/automation/repositories';
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';
import { SpaceValidationController } from './controllers/space-validation.controller';
export const CommandHandlers = [DisableSpaceHandler];
@ -83,6 +95,7 @@ export const CommandHandlers = [DisableSpaceHandler];
SubSpaceController,
SubSpaceDeviceController,
SpaceSceneController,
SpaceValidationController,
],
providers: [
ValidationService,
@ -105,6 +118,7 @@ export const CommandHandlers = [DisableSpaceHandler];
UserRepository,
SpaceUserService,
SpaceSceneService,
DeviceService,
SceneService,
SceneIconRepository,
SceneRepository,
@ -114,7 +128,6 @@ export const CommandHandlers = [DisableSpaceHandler];
SceneDeviceRepository,
SpaceModelService,
SubSpaceModelService,
TagModelService,
ProjectRepository,
SpaceModelRepository,
SubspaceModelRepository,
@ -130,6 +143,17 @@ export const CommandHandlers = [DisableSpaceHandler];
InviteUserRepository,
InviteUserSpaceRepository,
AutomationRepository,
TagService,
NewTagService,
SpaceModelProductAllocationRepoitory,
SubspaceModelProductAllocationRepoitory,
NewTagRepository,
SpaceModelProductAllocationService,
SubspaceModelProductAllocationService,
SpaceProductAllocationService,
SubspaceProductAllocationService,
SpaceProductAllocationRepository,
SubspaceProductAllocationRepository,
],
exports: [SpaceService],
})

View File

@ -0,0 +1 @@
export * from './tags.controller';

View File

@ -0,0 +1,45 @@
import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
import { TagService } from '../services/tags.service';
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard';
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
import { ControllerRoute } from '@app/common/constants/controller-route';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { CreateTagDto } from '../dtos/tags.dto';
import { ProjectParam } from '@app/common/dto/project-param.dto';
@ApiTags('Tag Module')
@Controller({
version: EnableDisableStatusEnum.ENABLED,
path: ControllerRoute.TAG.ROUTE,
})
export class TagController {
constructor(private readonly tagService: TagService) {}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Post()
@ApiOperation({
summary: ControllerRoute.TAG.ACTIONS.CREATE_TAG_SUMMARY,
description: ControllerRoute.TAG.ACTIONS.CREATE_TAG_DESCRIPTION,
})
async createTag(
@Body() dto: CreateTagDto,
@Param() param: ProjectParam,
): Promise<BaseResponseDto> {
return this.tagService.createTag(dto, param);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get()
@ApiOperation({
summary: ControllerRoute.TAG.ACTIONS.GET_TAGS_BY_PROJECT_SUMMARY,
description: ControllerRoute.TAG.ACTIONS.GET_TAGS_BY_PROJECT_DESCRIPTION,
})
async getTagsByProject(
@Param() params: ProjectParam,
): Promise<BaseResponseDto> {
return this.tagService.getTagsByProjectUuid(params);
}
}

View File

@ -0,0 +1,23 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator';
import { CreateTagDto } from './tags.dto';
export class BulkCreateTagsDto {
@ApiProperty({
description: 'Project UUID for which the tags are being created',
example: '760e8400-e29b-41d4-a716-446655440001',
})
@IsNotEmpty()
@IsString()
projectUuid: string;
@ApiProperty({
description: 'List of tags to be created',
type: [CreateTagDto],
})
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreateTagDto)
tags: CreateTagDto[];
}

Some files were not shown because too many files have changed in this diff Show More