Merge pull request #178 from SyncrowIOT/feat/edit-space-model

Feat/edit space model
This commit is contained in:
hannathkadher
2024-12-30 14:44:15 +04:00
committed by GitHub
125 changed files with 3176 additions and 1932 deletions

View File

@ -41,7 +41,7 @@ import {
import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter';
import { DeviceRepository } from '@app/common/modules/device/repositories';
import { PermissionType } from '@app/common/constants/permission-type.enum';
import { In } from 'typeorm';
import { In, QueryRunner } from 'typeorm';
import { ProductType } from '@app/common/constants/product-type.enum';
import { SpaceRepository } from '@app/common/modules/space/repositories';
import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service';
@ -59,6 +59,7 @@ import { DeviceSceneParamDto } from '../dtos/device.param.dto';
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';
@Injectable()
export class DeviceService {
@ -83,6 +84,7 @@ export class DeviceService {
secretKey,
});
}
async getDeviceByDeviceUuid(
deviceUuid: string,
withProductDevice: boolean = true,
@ -98,6 +100,29 @@ export class DeviceService {
relations,
});
}
async deleteDevice(
devices: DeviceEntity[],
orphanSpace: SpaceEntity,
queryRunner: QueryRunner,
): Promise<void> {
try {
const deviceIds = devices.map((device) => device.uuid);
if (deviceIds.length > 0) {
await queryRunner.manager
.createQueryBuilder()
.update(DeviceEntity)
.set({ spaceDevice: orphanSpace })
.whereInIds(deviceIds)
.execute();
}
} catch (error) {
throw new HttpException(
`Failed to update devices to orphan space: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async getDeviceByDeviceTuyaUuid(deviceTuyaUuid: string) {
return await this.deviceRepository.findOne({

View File

@ -495,12 +495,16 @@ export class SceneService {
const space = await this.getSpaceByUuid(scene.space.uuid);
await this.delete(scene.sceneTuyaUuid, space.spaceTuyaUuid);
await this.sceneDeviceRepository.delete({
scene: { uuid: sceneUuid },
});
await this.sceneRepository.delete({
uuid: sceneUuid,
});
await this.sceneDeviceRepository.update(
{ uuid: sceneUuid },
{ disabled: true },
);
await this.sceneRepository.update(
{
uuid: sceneUuid,
},
{ disabled: true },
);
return new SuccessResponseDto({
message: `Scene with ID ${sceneUuid} deleted successfully`,
});

View File

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

View File

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

View File

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

View File

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

View File

@ -1,60 +0,0 @@
import { HttpException, HttpStatus } from '@nestjs/common';
import { QueryRunner } from 'typeorm';
import { SpaceModelEntity } from '@app/common/modules/space-model';
import { CreateProductItemModelDto } from 'src/space-model/dtos';
export abstract class BaseProductItemService {
async validateTags(
itemModelDtos: CreateProductItemModelDto[],
queryRunner: QueryRunner,
spaceModel: SpaceModelEntity,
): Promise<void> {
const incomingTags = new Set(
itemModelDtos.map((item) => item.tag).filter(Boolean),
);
const duplicateTags = itemModelDtos
.map((item) => item.tag)
.filter((tag, index, array) => array.indexOf(tag) !== index);
if (duplicateTags.length > 0) {
throw new HttpException(
`Duplicate tags found in the request: ${[...new Set(duplicateTags)].join(', ')}`,
HttpStatus.BAD_REQUEST,
);
}
const existingTagsQuery = `
SELECT DISTINCT tag
FROM (
SELECT spi.tag
FROM "subspace-product-item-model" spi
INNER JOIN "subspace-product-model" spm ON spi.subspace_product_model_uuid = spm.uuid
INNER JOIN "subspace-model" sm ON spm.subspace_model_uuid = sm.uuid
WHERE sm.space_model_uuid = $1
UNION
SELECT spi.tag
FROM "space-product-item-model" spi
INNER JOIN "space-product-model" spm ON spi.space_product_model_uuid = spm.uuid
WHERE spm.space_model_uuid = $1
) AS combined_tags;
`;
const existingTags = await queryRunner.manager.query(existingTagsQuery, [
spaceModel.uuid,
]);
const existingTagsSet = new Set(
existingTags.map((row: { tag: string }) => row.tag),
);
const conflictingTags = [...incomingTags].filter((tag) =>
existingTagsSet.has(tag),
);
if (conflictingTags.length > 0) {
throw new HttpException(
`Tags already exist in the model: ${conflictingTags.join(', ')}`,
HttpStatus.CONFLICT,
);
}
}
}

View File

@ -1,24 +0,0 @@
import { HttpException, HttpStatus } from '@nestjs/common';
import { CreateSpaceProductModelDto } from 'src/space-model/dtos';
import { ProductService } from '../../../product/services';
export abstract class BaseProductModelService {
constructor(private readonly productService: ProductService) {}
protected async validateProductCount(
dto: CreateSpaceProductModelDto,
): Promise<void> {
const productItemCount = dto.items.length;
if (dto.productCount !== productItemCount) {
throw new HttpException(
`Product count (${dto.productCount}) does not match the number of items (${productItemCount}) for product ID ${dto.productUuid}.`,
HttpStatus.BAD_REQUEST,
);
}
}
protected async getProduct(productId: string) {
const product = await this.productService.findOne(productId);
return product.data;
}
}

View File

@ -1,2 +0,0 @@
export * from './base-product-item-model.service';
export * from './base-product-model.service';

View File

@ -2,15 +2,21 @@ import { ControllerRoute } from '@app/common/constants/controller-route';
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
Query,
UseGuards,
} from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { SpaceModelService } from '../services';
import { CreateSpaceModelDto } from '../dtos';
import {
CreateSpaceModelDto,
SpaceModelParam,
UpdateSpaceModelDto,
} from '../dtos';
import { ProjectParam } from 'src/community/dtos';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { PermissionsGuard } from 'src/guards/permissions.guard';
@ -27,7 +33,7 @@ export class SpaceModelController {
@ApiBearerAuth()
@UseGuards(PermissionsGuard)
@Permissions('SPACE_MODULE_ADD')
@Permissions('SPACE_MODEL_ADD')
@ApiOperation({
summary: ControllerRoute.SPACE_MODEL.ACTIONS.CREATE_SPACE_MODEL_SUMMARY,
description:
@ -59,4 +65,33 @@ export class SpaceModelController {
): Promise<BaseResponseDto> {
return await this.spaceModelService.list(projectParam, query);
}
@ApiBearerAuth()
@UseGuards(PermissionsGuard)
@Permissions('SPACE_MODEL_UPDATE')
@ApiOperation({
summary: ControllerRoute.SPACE_MODEL.ACTIONS.UPDATE_SPACE_MODEL_SUMMARY,
description:
ControllerRoute.SPACE_MODEL.ACTIONS.UPDATE_SPACE_MODEL_DESCRIPTION,
})
@Put(':spaceModelUuid')
async update(
@Body() dto: UpdateSpaceModelDto,
@Param() param: SpaceModelParam,
): Promise<BaseResponseDto> {
return await this.spaceModelService.update(dto, param);
}
@ApiBearerAuth()
@UseGuards(PermissionsGuard)
@Permissions('SPACE_MODEL_DELETE')
@ApiOperation({
summary: ControllerRoute.SPACE_MODEL.ACTIONS.DELETE_SPACE_MODEL_SUMMARY,
description:
ControllerRoute.SPACE_MODEL.ACTIONS.DELETE_SPACE_MODEL_DESCRIPTION,
})
@Delete(':spaceModelUuid')
async delete(@Param() param: SpaceModelParam): Promise<BaseResponseDto> {
return await this.spaceModelService.deleteSpaceModel(param);
}
}

View File

@ -1,8 +1,8 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString, IsArray, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
import { CreateSubspaceModelDto } from './create-subspace-model.dto';
import { CreateSpaceProductModelDto } from './create-space-product-model.dto';
import { CreateSubspaceModelDto } from './subspaces-model-dtos/create-subspace-model.dto';
import { CreateTagModelDto } from './tag-model-dtos/create-tag-model.dto';
export class CreateSpaceModelDto {
@ApiProperty({
@ -23,11 +23,11 @@ export class CreateSpaceModelDto {
subspaceModels?: CreateSubspaceModelDto[];
@ApiProperty({
description: 'List of products included in the model',
type: [CreateSpaceProductModelDto],
description: 'List of tags associated with the space model',
type: [CreateTagModelDto],
})
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreateSpaceProductModelDto)
spaceProductModels?: CreateSpaceProductModelDto[];
@Type(() => CreateTagModelDto)
tags?: CreateTagModelDto[];
}

View File

@ -1,12 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class CreateProductItemModelDto {
@ApiProperty({
description: 'Specific name for the product item',
example: 'Light 1',
})
@IsNotEmpty()
@IsString()
tag: string;
}

View File

@ -1,39 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import {
IsNotEmpty,
IsString,
IsArray,
ValidateNested,
IsInt,
ArrayNotEmpty,
} from 'class-validator';
import { Type } from 'class-transformer';
import { CreateProductItemModelDto } from './create-space-product-item-model.dto';
export class CreateSpaceProductModelDto {
@ApiProperty({
description: 'ID of the product associated with the model',
example: 'product-uuid',
})
@IsNotEmpty()
@IsString()
productUuid: string;
@ApiProperty({
description: 'Number of products in the model',
example: 3,
})
@IsNotEmpty()
@IsInt()
productCount: number;
@ApiProperty({
description: 'Specific names for each product item',
type: [CreateProductItemModelDto],
})
@IsArray()
@ArrayNotEmpty()
@ValidateNested({ each: true })
@Type(() => CreateProductItemModelDto)
items: CreateProductItemModelDto[];
}

View File

@ -1,30 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import {
IsArray,
IsNotEmpty,
IsOptional,
IsString,
ValidateNested,
} from 'class-validator';
import { CreateSpaceProductModelDto } from './create-space-product-model.dto';
import { Type } from 'class-transformer';
export class CreateSubspaceModelDto {
@ApiProperty({
description: 'Name of the subspace',
example: 'Living Room',
})
@IsNotEmpty()
@IsString()
subspaceName: string;
@ApiProperty({
description: 'List of products included in the model',
type: [CreateSpaceProductModelDto],
})
@IsArray()
@IsOptional()
@ValidateNested({ each: true })
@Type(() => CreateSpaceProductModelDto)
spaceProductModels?: CreateSpaceProductModelDto[];
}

View File

@ -1,5 +1,6 @@
export * from './create-space-model.dto';
export * from './create-space-product-item-model.dto';
export * from './create-space-product-model.dto';
export * from './create-subspace-model.dto';
export * from './project-param.dto';
export * from './update-space-model.dto';
export * from './space-model-param';
export * from './subspaces-model-dtos';
export * from './tag-model-dtos';

View File

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

View File

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

View File

@ -0,0 +1,23 @@
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';
export class CreateSubspaceModelDto {
@ApiProperty({
description: 'Name of the subspace',
example: 'Living Room',
})
@IsNotEmpty()
@IsString()
subspaceName: string;
@ApiProperty({
description: 'List of tag models associated with the subspace',
type: [CreateTagModelDto],
})
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreateTagModelDto)
tags?: CreateTagModelDto[];
}

View File

@ -0,0 +1,12 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class DeleteSubspaceModelDto {
@ApiProperty({
description: 'Uuid of the subspace model need to be deleted',
example: '982fc3a3-64dc-4afb-a5b5-65ee8fef0424',
})
@IsNotEmpty()
@IsString()
subspaceUuid: string;
}

View File

@ -0,0 +1,4 @@
export * from './delete-subspace-model.dto';
export * from './create-subspace-model.dto';
export * from './update-subspace-model.dto';
export * from './modify-subspace-model.dto';

View File

@ -0,0 +1,47 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import {
IsString,
IsOptional,
IsArray,
ValidateNested,
IsEnum,
} from 'class-validator';
import { ModifyTagModelDto } from '../tag-model-dtos';
import { ModifyAction } from '@app/common/constants/modify-action.enum';
export class ModifySubspaceModelDto {
@ApiProperty({
description: 'Action to perform: add, update, or delete',
example: ModifyAction.ADD,
})
@IsEnum(ModifyAction)
action: ModifyAction;
@ApiPropertyOptional({
description: 'UUID of the subspace (required for update/delete)',
example: '123e4567-e89b-12d3-a456-426614174000',
})
@IsOptional()
@IsString()
uuid?: string;
@ApiPropertyOptional({
description: 'Name of the subspace (required for add/update)',
example: 'Living Room',
})
@IsOptional()
@IsString()
subspaceName?: string;
@ApiPropertyOptional({
description:
'List of tag modifications (add/update/delete) for the subspace',
type: [ModifyTagModelDto],
})
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => ModifyTagModelDto)
tags?: ModifyTagModelDto[];
}

View File

@ -0,0 +1,16 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class UpdateSubspaceModelDto {
@ApiProperty({
description: 'Name of the subspace',
example: 'Living Room',
})
@IsNotEmpty()
@IsString()
subspaceName?: string;
@IsNotEmpty()
@IsString()
subspaceUuid: string;
}

View File

@ -0,0 +1,20 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class CreateTagModelDto {
@ApiProperty({
description: 'Tag models associated with the space or subspace models',
example: 'Temperature Control',
})
@IsNotEmpty()
@IsString()
tag: string;
@ApiProperty({
description: 'ID of the product associated with the tag',
example: '123e4567-e89b-12d3-a456-426614174000',
})
@IsNotEmpty()
@IsString()
productUuid: string;
}

View File

@ -0,0 +1,3 @@
export * from './create-tag-model.dto';
export * from './update-tag-model.dto';
export * from './modify-tag-model.dto';

View File

@ -0,0 +1,37 @@
import { ModifyAction } from '@app/common/constants/modify-action.enum';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsString, IsOptional, IsEnum } from 'class-validator';
export class ModifyTagModelDto {
@ApiProperty({
description: 'Action to perform: add, update, or delete',
example: ModifyAction.ADD,
})
@IsEnum(ModifyAction)
action: ModifyAction;
@ApiPropertyOptional({
description: 'UUID of the tag model (required for update/delete)',
example: '123e4567-e89b-12d3-a456-426614174000',
})
@IsOptional()
@IsString()
uuid?: string;
@ApiPropertyOptional({
description: 'Name of the tag model (required for add/update)',
example: 'Temperature Sensor',
})
@IsOptional()
@IsString()
tag?: string;
@ApiPropertyOptional({
description:
'UUID of the product associated with the tag (required for add)',
example: 'c789a91e-549a-4753-9006-02f89e8170e0',
})
@IsOptional()
@IsString()
productUuid?: string;
}

View File

@ -0,0 +1,21 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
export class UpdateTagModelDto {
@ApiProperty({
description: 'UUID of the tag to be updated',
example: '123e4567-e89b-12d3-a456-426614174000',
})
@IsNotEmpty()
@IsUUID()
uuid: string;
@ApiProperty({
description: 'Updated name of the tag',
example: 'Updated Tag Name',
required: false,
})
@IsOptional()
@IsString()
tag?: string;
}

View File

@ -0,0 +1,76 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsArray, IsOptional, IsString, ValidateNested } from 'class-validator';
import { CreateSubspaceModelDto } from './subspaces-model-dtos/create-subspace-model.dto';
import { Type } from 'class-transformer';
import {
DeleteSubspaceModelDto,
ModifySubspaceModelDto,
UpdateSubspaceModelDto,
} from './subspaces-model-dtos';
import { ModifyTagModelDto } from './tag-model-dtos';
export class ModifySubspacesModelDto {
@ApiProperty({
description: 'List of subspaces to add',
type: [CreateSubspaceModelDto],
required: false,
})
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreateSubspaceModelDto)
add?: CreateSubspaceModelDto[];
@ApiProperty({
description: 'List of subspaces to add',
type: [CreateSubspaceModelDto],
required: false,
})
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => UpdateSubspaceModelDto)
update?: UpdateSubspaceModelDto[];
@ApiProperty({
description: 'List of subspaces to delete',
type: [DeleteSubspaceModelDto],
required: false,
})
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => DeleteSubspaceModelDto)
delete?: DeleteSubspaceModelDto[];
}
export class UpdateSpaceModelDto {
@ApiProperty({
description: 'Updated name of the space model',
example: 'New Space Model Name',
})
@IsOptional()
@IsString()
modelName?: string;
@ApiPropertyOptional({
description: 'List of subspace modifications (add/update/delete)',
type: [ModifySubspaceModelDto],
})
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => ModifySubspaceModelDto)
subspaceModels?: ModifySubspaceModelDto[];
@ApiPropertyOptional({
description:
'List of tag modifications (add/update/delete) for the space model',
type: [ModifyTagModelDto],
})
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => ModifyTagModelDto)
tags?: ModifyTagModelDto[];
}

View File

@ -0,0 +1,2 @@
export * from './propate-subspace-handler';
export * from './propogate-space-model-deletion.handler';

View File

@ -0,0 +1,250 @@
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 {
SpaceModelEntity,
SubspaceModelEntity,
TagModel,
} from '@app/common/modules/space-model';
import { DataSource, 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 { ModifyAction } from '@app/common/constants/modify-action.enum';
import { ModifySubspaceDto } from 'src/space/dtos';
@CommandHandler(PropogateUpdateSpaceModelCommand)
export class PropogateUpdateSpaceModelHandler
implements ICommandHandler<PropogateUpdateSpaceModelCommand>
{
constructor(
private readonly spaceRepository: SpaceRepository,
private readonly subspaceRepository: SubspaceRepository,
private readonly dataSource: DataSource,
private readonly subSpaceService: SubSpaceService,
private readonly tagService: TagService,
private readonly tagModelService: TagModelService,
) {}
async execute(command: PropogateUpdateSpaceModelCommand): Promise<void> {
const { spaceModel, modifiedSpaceModels } = command.param;
const queryRunner = this.dataSource.createQueryRunner();
try {
await queryRunner.connect();
await queryRunner.startTransaction();
const spaces = await this.spaceRepository.find({
where: { spaceModel },
});
if (
modifiedSpaceModels.modifiedSubspaceModels.addedSubspaceModels.length >
0
) {
await this.addSubspaceModels(
modifiedSpaceModels.modifiedSubspaceModels.addedSubspaceModels,
spaces,
queryRunner,
);
} else if (
modifiedSpaceModels.modifiedSubspaceModels.updatedSubspaceModels
.length > 0
) {
await this.updateSubspaceModels(
modifiedSpaceModels.modifiedSubspaceModels.updatedSubspaceModels,
queryRunner,
);
}
if (
modifiedSpaceModels.modifiedSubspaceModels.deletedSubspaceModels?.length
) {
const dtos: ModifySubspaceDto[] =
modifiedSpaceModels.modifiedSubspaceModels.deletedSubspaceModels.map(
(model) => ({
action: ModifyAction.DELETE,
uuid: model,
}),
);
await this.subSpaceService.modifySubSpace(dtos, queryRunner);
}
if (modifiedSpaceModels.modifiedTags.added.length > 0) {
await this.createTags(
modifiedSpaceModels.modifiedTags.added,
queryRunner,
null,
spaceModel,
);
}
if (modifiedSpaceModels.modifiedTags.updated.length > 0) {
await this.updateTags(
modifiedSpaceModels.modifiedTags.updated,
queryRunner,
);
}
if (modifiedSpaceModels.modifiedTags.deleted.length > 0) {
await this.deleteTags(
modifiedSpaceModels.modifiedTags.deleted,
queryRunner,
);
}
await queryRunner.commitTransaction();
} catch (error) {
await queryRunner.rollbackTransaction();
throw error;
} finally {
await queryRunner.release();
}
}
async addSubspaceModels(
subspaceModels: SubspaceModelEntity[],
spaces: SpaceEntity[],
queryRunner: QueryRunner,
) {
for (const space of spaces) {
await this.subSpaceService.createSubSpaceFromModel(
subspaceModels,
space,
queryRunner,
);
}
}
async updateSubspaceModels(
subspaceModels: UpdatedSubspaceModelPayload[],
queryRunner: QueryRunner,
): Promise<void> {
const subspaceUpdatePromises = subspaceModels.map(async (model) => {
const {
updated: tagsToUpdate,
deleted: tagsToDelete,
added: tagsToAdd,
} = model.modifiedTags;
// Perform tag operations concurrently
await Promise.all([
tagsToUpdate?.length && this.updateTags(tagsToUpdate, queryRunner),
tagsToDelete?.length && this.deleteTags(tagsToDelete, queryRunner),
tagsToAdd?.length &&
this.createTags(
tagsToAdd,
queryRunner,
model.subspaceModelUuid,
null,
),
]);
// Update subspace names
const subspaces = await queryRunner.manager.find(
this.subspaceRepository.target,
{
where: {
subSpaceModel: {
uuid: model.subspaceModelUuid,
},
},
},
);
if (subspaces.length > 0) {
const updateSubspacePromises = subspaces.map((subspace) =>
queryRunner.manager.update(
this.subspaceRepository.target,
{ uuid: subspace.uuid },
{ subspaceName: model.subspaceName },
),
);
await Promise.all(updateSubspacePromises);
}
});
// Wait for all subspace model updates to complete
await Promise.all(subspaceUpdatePromises);
}
async updateTags(models: TagModel[], queryRunner: QueryRunner) {
if (!models?.length) return;
const updatePromises = models.map((model) =>
this.tagService.updateTagsFromModel(model, queryRunner),
);
await Promise.all(updatePromises);
}
async deleteTags(uuids: string[], queryRunner: QueryRunner) {
const deletePromises = uuids.map((uuid) =>
this.tagService.deleteTagFromModel(uuid, queryRunner),
);
await Promise.all(deletePromises);
}
async createTags(
models: TagModel[],
queryRunner: QueryRunner,
subspaceModelUuid?: string,
spaceModel?: SpaceModelEntity,
): Promise<void> {
if (!models.length) {
return;
}
if (subspaceModelUuid) {
await this.processSubspaces(subspaceModelUuid, models, queryRunner);
}
if (spaceModel) {
await this.processSpaces(spaceModel.uuid, models, queryRunner);
}
}
private async processSubspaces(
subspaceModelUuid: string,
models: TagModel[],
queryRunner: QueryRunner,
): Promise<void> {
const subspaces = await this.subspaceRepository.find({
where: {
subSpaceModel: {
uuid: subspaceModelUuid,
},
},
});
if (subspaces.length > 0) {
const subspacePromises = subspaces.map((subspace) =>
this.tagService.createTagsFromModel(
queryRunner,
models,
null,
subspace,
),
);
await Promise.all(subspacePromises);
}
}
private async processSpaces(
spaceModelUuid: string,
models: TagModel[],
queryRunner: QueryRunner,
): Promise<void> {
const spaces = await this.spaceRepository.find({
where: {
spaceModel: {
uuid: spaceModelUuid,
},
},
});
if (spaces.length > 0) {
const spacePromises = spaces.map((space) =>
this.tagService.createTagsFromModel(queryRunner, models, space, null),
);
await Promise.all(spacePromises);
}
}
}

View File

@ -0,0 +1,57 @@
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
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
implements ICommandHandler<PropogateDeleteSpaceModelCommand>
{
private readonly logger = new Logger(PropogateDeleteSpaceModelHandler.name);
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();
try {
await queryRunner.connect();
await queryRunner.startTransaction();
const spaces = await this.spaceRepository.find({
where: {
spaceModel: {
uuid: spaceModel.uuid,
},
},
relations: ['subspaces', 'tags', 'subspaces.tags'],
});
for (const space of spaces) {
try {
await this.spaceService.unlinkSpaceFromModel(space, queryRunner);
} catch (innerError) {
this.logger.error(
`Error unlinking space model for space with UUID ${space.uuid}:`,
innerError.stack || innerError,
);
}
}
} catch (error) {
await queryRunner.rollbackTransaction();
this.logger.error(
'Error propagating delete space model:',
error.stack || error,
);
} finally {
await queryRunner.release();
}
}
}

View File

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

View File

@ -0,0 +1,24 @@
import { SubspaceModelEntity, TagModel } from '@app/common/modules/space-model';
export interface ModifyspaceModelPayload {
modifiedSubspaceModels?: ModifySubspaceModelPayload;
modifiedTags?: ModifiedTagsModelPayload;
}
export interface ModifySubspaceModelPayload {
addedSubspaceModels?: SubspaceModelEntity[];
updatedSubspaceModels?: UpdatedSubspaceModelPayload[];
deletedSubspaceModels?: string[];
}
export interface UpdatedSubspaceModelPayload {
subspaceName?: string;
modifiedTags?: ModifiedTagsModelPayload;
subspaceModelUuid: string;
}
export interface ModifiedTagsModelPayload {
added?: TagModel[];
updated?: TagModel[];
deleted?: string[];
}

View File

@ -0,0 +1,39 @@
import { SubspaceModelEntity } from '@app/common/modules/space-model';
export interface AddSubspaceModelInterface {
subspaceModel: SubspaceModelEntity;
}
export interface ProductModelInterface {}
export interface IModifySubspaceModelInterface {
spaceModelUuid: string;
new?: AddSubspaceModelInterface[];
update?: IUpdateSubspaceModelInterface[];
delete?: IDeletedSubsaceModelInterface[];
}
export interface IModifiedProductItemsModelsInterface {
delete?: string[];
}
export interface IUpdateSubspaceModelInterface {
subspaceName?: string;
uuid: string;
productModels?: IModifiedProductItemsModelsInterface[];
}
export interface IDeletedSubsaceModelInterface {
uuid: string;
}
export interface IUpdatedProductModelInterface {
productModelUuid: string;
productModifiedItemModel: IModifiedProductItemsModelsInterface;
}
export interface IModifiedProductModelsInterface {
add?: ProductModelInterface[];
update?: IUpdatedProductModelInterface[];
delete?: string[];
}

View File

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

View File

@ -1,11 +1,12 @@
import { SpaceModelRepository } from '@app/common/modules/space-model';
import {
SpaceModelEntity,
SpaceModelRepository,
} from '@app/common/modules/space-model';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { CreateSpaceModelDto } from '../dtos';
import { ProjectRepository } from '@app/common/modules/project/repositiories';
import { CreateSpaceModelDto, 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 { SpaceProductModelService } from './space-product-model.service';
import { DataSource } from 'typeorm';
import {
TypeORMCustomModel,
@ -13,63 +14,69 @@ import {
} from '@app/common/models/typeOrmCustom.model';
import { PageResponse } from '@app/common/dto/pagination.response.dto';
import { SpaceModelDto } from '@app/common/modules/space-model/dtos';
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 {
ModifiedTagsModelPayload,
ModifySubspaceModelPayload,
} from '../interfaces';
@Injectable()
export class SpaceModelService {
constructor(
private readonly dataSource: DataSource,
private readonly spaceModelRepository: SpaceModelRepository,
private readonly projectRepository: ProjectRepository,
private readonly projectService: ProjectService,
private readonly subSpaceModelService: SubSpaceModelService,
private readonly spaceProductModelService: SpaceProductModelService,
private readonly tagModelService: TagModelService,
private commandBus: CommandBus,
) {}
async createSpaceModel(
createSpaceModelDto: CreateSpaceModelDto,
params: ProjectParam,
) {
const { modelName, subspaceModels, spaceProductModels } =
createSpaceModelDto;
const project = await this.validateProject(params.projectUuid);
): Promise<BaseResponseDto> {
const { modelName, subspaceModels, tags } = createSpaceModelDto;
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const isModelExist = await this.validateName(
modelName,
params.projectUuid,
);
if (isModelExist) {
throw new HttpException(
`Model name "${modelName}" already exists in this project ${project.name}.`,
HttpStatus.CONFLICT,
);
}
const project = await this.validateProject(params.projectUuid);
await this.validateName(modelName, params.projectUuid);
const spaceModel = this.spaceModelRepository.create({
modelName,
project,
});
const savedSpaceModel = await queryRunner.manager.save(spaceModel);
if (subspaceModels) {
await this.subSpaceModelService.createSubSpaceModels(
subspaceModels,
savedSpaceModel,
if (subspaceModels?.length) {
savedSpaceModel.subspaceModels =
await this.subSpaceModelService.createSubSpaceModels(
subspaceModels,
savedSpaceModel,
queryRunner,
tags,
);
}
if (tags?.length) {
savedSpaceModel.tags = await this.tagModelService.createTags(
tags,
queryRunner,
savedSpaceModel,
null,
);
}
if (spaceProductModels) {
await this.spaceProductModelService.createSpaceProductModels(
spaceProductModels,
savedSpaceModel,
queryRunner,
);
}
await queryRunner.commitTransaction();
return new SuccessResponseDto({
@ -80,14 +87,16 @@ export class SpaceModelService {
} catch (error) {
await queryRunner.rollbackTransaction();
if (error instanceof HttpException) {
throw error;
}
const errorMessage =
error instanceof HttpException
? error.message
: 'An unexpected error occurred';
const statusCode =
error instanceof HttpException
? error.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
throw new HttpException(
error.message || `An unexpected error occurred`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
throw new HttpException(errorMessage, statusCode);
} finally {
await queryRunner.release();
}
@ -104,8 +113,7 @@ export class SpaceModelService {
pageable.where = {
project: { uuid: param.projectUuid },
};
pageable.include =
'subspaceModels,spaceProductModels,subspaceModels.productModels,subspaceModels.productModels.itemModels,spaceProductModels.items';
pageable.include = 'subspaceModels,tags,subspaceModels.tags';
const customModel = TypeORMCustomModel(this.spaceModelRepository);
@ -125,26 +133,159 @@ export class SpaceModelService {
}
}
async validateProject(projectUuid: string) {
const project = await this.projectRepository.findOne({
where: {
uuid: projectUuid,
},
async validateProject(projectUuid: string): Promise<ProjectEntity> {
return await this.projectService.findOne(projectUuid);
}
async update(dto: UpdateSpaceModelDto, param: SpaceModelParam) {
await this.validateProject(param.projectUuid);
const spaceModel = await this.validateSpaceModel(param.spaceModelUuid);
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
let modifiedSubspaceModels: ModifySubspaceModelPayload = {};
let modifiedTagsModelPayload: ModifiedTagsModelPayload = {};
try {
const { modelName } = dto;
if (modelName) {
await this.validateName(modelName, param.projectUuid);
spaceModel.modelName = modelName;
await queryRunner.manager.save(spaceModel);
}
if (dto.subspaceModels) {
modifiedSubspaceModels =
await this.subSpaceModelService.modifySubSpaceModels(
dto.subspaceModels,
spaceModel,
queryRunner,
);
}
if (dto.tags) {
modifiedTagsModelPayload = await this.tagModelService.modifyTags(
dto.tags,
queryRunner,
spaceModel,
);
}
await queryRunner.commitTransaction();
await this.commandBus.execute(
new PropogateUpdateSpaceModelCommand({
spaceModel: spaceModel,
modifiedSpaceModels: {
modifiedSubspaceModels,
modifiedTags: modifiedTagsModelPayload,
},
}),
);
return new SuccessResponseDto({
message: 'SpaceModel updated successfully',
data: spaceModel,
});
} catch (error) {
await queryRunner.rollbackTransaction();
throw new HttpException(
error.message || 'Failed to update SpaceModel',
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
await queryRunner.release();
}
}
async deleteSpaceModel(param: SpaceModelParam): Promise<BaseResponseDto> {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
await this.validateProject(param.projectUuid);
const spaceModel = await this.validateSpaceModel(param.spaceModelUuid);
if (spaceModel.subspaceModels?.length) {
const deleteSubspaceDtos = spaceModel.subspaceModels.map(
(subspace) => ({
subspaceUuid: subspace.uuid,
}),
);
await this.subSpaceModelService.deleteSubspaceModels(
deleteSubspaceDtos,
queryRunner,
);
}
if (spaceModel.tags?.length) {
const deleteSpaceTagsDtos = spaceModel.tags.map((tag) => tag.uuid);
await this.tagModelService.deleteTags(deleteSpaceTagsDtos, queryRunner);
}
await queryRunner.manager.update(
this.spaceModelRepository.target,
{ uuid: param.spaceModelUuid },
{ disabled: true },
);
await queryRunner.commitTransaction();
return new SuccessResponseDto({
message: `SpaceModel with UUID ${param.spaceModelUuid} deleted successfully.`,
statusCode: HttpStatus.OK,
});
} catch (error) {
await queryRunner.rollbackTransaction();
const errorMessage =
error instanceof HttpException
? error.message
: 'An unexpected error occurred while deleting the SpaceModel';
const statusCode =
error instanceof HttpException
? error.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
throw new HttpException(errorMessage, statusCode);
} finally {
await queryRunner.release();
}
}
async validateName(modelName: string, projectUuid: string): Promise<void> {
const isModelExist = await this.spaceModelRepository.findOne({
where: { modelName, project: { uuid: projectUuid }, disabled: false },
});
if (!project) {
if (isModelExist) {
throw new HttpException(
`Project with uuid ${projectUuid} not found`,
HttpStatus.NOT_FOUND,
`Model name ${modelName} already exists in the project with UUID ${projectUuid}.`,
HttpStatus.CONFLICT,
);
}
return project;
}
async validateName(modelName: string, projectUuid: string): Promise<boolean> {
const isModelExist = await this.spaceModelRepository.exists({
where: { modelName, project: { uuid: projectUuid } },
async validateSpaceModel(uuid: string): Promise<SpaceModelEntity> {
const spaceModel = await this.spaceModelRepository.findOne({
where: {
uuid,
disabled: false,
},
relations: [
'subspaceModels',
'tags',
'tags.product',
'subspaceModels.tags',
'subspaceModels.tags.product',
],
});
return isModelExist;
if (!spaceModel) {
throw new HttpException('space model not found', HttpStatus.NOT_FOUND);
}
return spaceModel;
}
}

View File

@ -1,47 +0,0 @@
import {
SpaceModelEntity,
SpaceProductItemModelRepository,
SpaceProductModelEntity,
} from '@app/common/modules/space-model';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { CreateProductItemModelDto } from '../dtos';
import { QueryRunner } from 'typeorm';
import { BaseProductItemService } from '../common';
@Injectable()
export class SpaceProductItemModelService extends BaseProductItemService {
constructor(
private readonly spaceProductItemRepository: SpaceProductItemModelRepository,
) {
super();
}
async createProdutItemModel(
itemModelDtos: CreateProductItemModelDto[],
spaceProductModel: SpaceProductModelEntity,
spaceModel: SpaceModelEntity,
queryRunner: QueryRunner,
) {
await this.validateTags(itemModelDtos, queryRunner, spaceModel);
try {
const productItems = itemModelDtos.map((dto) =>
queryRunner.manager.create(this.spaceProductItemRepository.target, {
tag: dto.tag,
spaceProductModel,
}),
);
await queryRunner.manager.save(productItems);
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
error.message ||
'An unexpected error occurred while creating product items.',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}

View File

@ -1,67 +0,0 @@
import { QueryRunner } from 'typeorm';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { CreateSpaceProductModelDto } from '../dtos';
import { SpaceProductItemModelService } from './space-product-item-model.service';
import { BaseProductModelService } from '../common';
import { ProductService } from 'src/product/services';
import {
SpaceModelEntity,
SpaceProductModelRepository,
} from '@app/common/modules/space-model';
@Injectable()
export class SpaceProductModelService extends BaseProductModelService {
constructor(
private readonly spaceProductModelRepository: SpaceProductModelRepository,
private readonly spaceProductItemModelService: SpaceProductItemModelService,
productService: ProductService,
) {
super(productService);
}
async createSpaceProductModels(
spaceProductModelDtos: CreateSpaceProductModelDto[],
spaceModel: SpaceModelEntity,
queryRunner: QueryRunner,
) {
try {
const productModels = await Promise.all(
spaceProductModelDtos.map(async (dto) => {
this.validateProductCount(dto);
const product = await this.getProduct(dto.productUuid);
return queryRunner.manager.create(
this.spaceProductModelRepository.target,
{
product,
productCount: dto.productCount,
spaceModel,
},
);
}),
);
const savedProductModels = await queryRunner.manager.save(productModels);
await Promise.all(
spaceProductModelDtos.map((dto, index) => {
const savedModel = savedProductModels[index];
return this.spaceProductItemModelService.createProdutItemModel(
dto.items,
savedModel, // Pass the saved model
spaceModel,
queryRunner,
);
}),
);
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
error.message ||
'An unexpected error occurred while creating product models.',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}

View File

@ -1,3 +1 @@
export * from './subspace-model.service';
export * from './subspace-product-item-model.service';
export * from './subspace-product-model.service';

View File

@ -1,88 +1,302 @@
import {
SpaceModelEntity,
SubspaceModelEntity,
SubspaceModelRepository,
} from '@app/common/modules/space-model';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { CreateSubspaceModelDto } from '../../dtos';
import { QueryRunner } from 'typeorm';
import { SubspaceProductModelService } from './subspace-product-model.service';
import { CreateSubspaceModelDto, CreateTagModelDto } from '../../dtos';
import { In, 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 { ModifyAction } from '@app/common/constants/modify-action.enum';
@Injectable()
export class SubSpaceModelService {
constructor(
private readonly subspaceModelRepository: SubspaceModelRepository,
private readonly subSpaceProducetModelService: SubspaceProductModelService,
private readonly tagModelService: TagModelService,
) {}
async createSubSpaceModels(
subSpaceModelDtos: CreateSubspaceModelDto[],
spaceModel: SpaceModelEntity,
queryRunner: QueryRunner,
) {
this.validateInputDtos(subSpaceModelDtos);
otherTags?: CreateTagModelDto[],
): Promise<SubspaceModelEntity[]> {
this.validateInputDtos(subSpaceModelDtos, spaceModel);
try {
const subspaces = subSpaceModelDtos.map((subspaceDto) =>
queryRunner.manager.create(this.subspaceModelRepository.target, {
subspaceName: subspaceDto.subspaceName,
spaceModel: spaceModel,
}),
);
const subspaces = subSpaceModelDtos.map((subspaceDto) =>
queryRunner.manager.create(this.subspaceModelRepository.target, {
subspaceName: subspaceDto.subspaceName,
spaceModel,
}),
);
await queryRunner.manager.save(subspaces);
const savedSubspaces = await queryRunner.manager.save(subspaces);
await Promise.all(
subSpaceModelDtos.map((dto, index) => {
const subspaceModel = subspaces[index];
return this.subSpaceProducetModelService.createSubspaceProductModels(
dto.spaceProductModels,
spaceModel,
subspaceModel,
await Promise.all(
subSpaceModelDtos.map(async (dto, index) => {
const subspace = savedSubspaces[index];
const otherDtoTags = subSpaceModelDtos
.filter((_, i) => i !== index)
.flatMap((otherDto) => otherDto.tags || []);
if (dto.tags?.length) {
subspace.tags = await this.tagModelService.createTags(
dto.tags,
queryRunner,
null,
subspace,
[...(otherTags || []), ...otherDtoTags],
);
}),
}
}),
);
return savedSubspaces;
}
async deleteSubspaceModels(
deleteDtos: DeleteSubspaceModelDto[],
queryRunner: QueryRunner,
): Promise<IDeletedSubsaceModelInterface[]> {
const deleteResults: IDeletedSubsaceModelInterface[] = [];
for (const dto of deleteDtos) {
const subspaceModel = await this.findOne(dto.subspaceUuid);
await queryRunner.manager.update(
this.subspaceModelRepository.target,
{ uuid: dto.subspaceUuid },
{ disabled: true },
);
} catch (error) {
if (error instanceof HttpException) {
throw error;
if (subspaceModel.tags?.length) {
const modifyTagDtos = subspaceModel.tags.map((tag) => ({
uuid: tag.uuid,
action: ModifyAction.DELETE,
}));
await this.tagModelService.modifyTags(
modifyTagDtos,
queryRunner,
null,
subspaceModel,
);
}
throw new HttpException(
error.message || `An unexpected error occurred`,
HttpStatus.INTERNAL_SERVER_ERROR,
deleteResults.push({ uuid: dto.subspaceUuid });
}
return deleteResults;
}
async modifySubSpaceModels(
subspaceDtos: ModifySubspaceModelDto[],
spaceModel: SpaceModelEntity,
queryRunner: QueryRunner,
): Promise<ModifySubspaceModelPayload> {
const modifiedSubspaceModels: ModifySubspaceModelPayload = {};
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;
}
private async handleAddAction(
subspace: ModifySubspaceModelDto,
spaceModel: SpaceModelEntity,
queryRunner: QueryRunner,
): Promise<SubspaceModelEntity> {
const createTagDtos: CreateTagModelDto[] =
subspace.tags?.map((tag) => ({
tag: tag.tag,
productUuid: tag.productUuid,
})) || [];
const [createdSubspaceModel] = await this.createSubSpaceModels(
[
{
subspaceName: subspace.subspaceName,
tags: createTagDtos,
},
],
spaceModel,
queryRunner,
);
return createdSubspaceModel;
}
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 = subspaceModel.tags.map((tag) => ({
uuid: tag.uuid,
action: ModifyAction.DELETE,
}));
await this.tagModelService.modifyTags(
modifyTagDtos,
queryRunner,
null,
subspaceModel,
);
}
}
private validateInputDtos(subSpaceModelDtos: CreateSubspaceModelDto[]) {
private async findOne(subspaceUuid: string): Promise<SubspaceModelEntity> {
const subspace = await this.subspaceModelRepository.findOne({
where: { uuid: subspaceUuid },
relations: ['tags'],
});
if (!subspace) {
throw new HttpException(
`SubspaceModel with UUID ${subspaceUuid} not found.`,
HttpStatus.NOT_FOUND,
);
}
return subspace;
}
private validateInputDtos(
subSpaceModelDtos: CreateSubspaceModelDto[],
spaceModel: SpaceModelEntity,
): void {
if (subSpaceModelDtos.length === 0) {
throw new HttpException(
'Subspace models cannot be empty.',
HttpStatus.BAD_REQUEST,
);
}
const incomingNames = subSpaceModelDtos.map((dto) => dto.subspaceName);
this.validateName(incomingNames);
this.validateName(
subSpaceModelDtos.map((dto) => dto.subspaceName),
spaceModel,
);
}
private validateName(names: string[]) {
private async validateName(
names: string[],
spaceModel: SpaceModelEntity,
): Promise<void> {
const seenNames = new Set<string>();
const duplicateNames = new Set<string>();
for (const name of names) {
if (seenNames.has(name)) {
if (!seenNames.add(name)) {
duplicateNames.add(name);
} else {
seenNames.add(name);
}
}
if (duplicateNames.size > 0) {
throw new HttpException(
`Duplicate subspace names found in request: ${[...duplicateNames].join(', ')}`,
`Duplicate subspace model names found: ${[...duplicateNames].join(', ')}`,
HttpStatus.CONFLICT,
);
}
const existingNames = await this.subspaceModelRepository.find({
select: ['subspaceName'],
where: {
subspaceName: In([...seenNames]),
spaceModel: {
uuid: spaceModel.uuid,
},
},
});
if (existingNames.length > 0) {
const existingNamesList = existingNames
.map((e) => e.subspaceName)
.join(', ');
throw new HttpException(
`Subspace model names already exist in the space: ${existingNamesList}`,
HttpStatus.BAD_REQUEST,
);
}
}
private async updateSubspaceName(
queryRunner: QueryRunner,
subSpaceModel: SubspaceModelEntity,
subspaceName?: string,
): Promise<void> {
if (subspaceName) {
subSpaceModel.subspaceName = subspaceName;
await queryRunner.manager.save(subSpaceModel);
}
}
}

View File

@ -1,53 +0,0 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { QueryRunner } from 'typeorm';
import {
SpaceModelEntity,
SubspaceProductItemModelRepository,
SubspaceProductModelEntity,
} from '@app/common/modules/space-model';
import { BaseProductItemService } from '../../common';
import { CreateProductItemModelDto } from '../../dtos';
@Injectable()
export class SubspaceProductItemModelService extends BaseProductItemService {
constructor(
private readonly subspaceProductItemRepository: SubspaceProductItemModelRepository,
) {
super();
}
async createProdutItemModel(
itemModelDtos: CreateProductItemModelDto[],
subspaceProductModel: SubspaceProductModelEntity,
spaceModel: SpaceModelEntity,
queryRunner: QueryRunner,
) {
if (!subspaceProductModel) {
throw new HttpException(
'The spaceProductModel parameter is required but was not provided.',
HttpStatus.BAD_REQUEST,
);
}
await this.validateTags(itemModelDtos, queryRunner, spaceModel);
try {
const productItems = itemModelDtos.map((dto) =>
queryRunner.manager.create(this.subspaceProductItemRepository.target, {
tag: dto.tag,
subspaceProductModel,
}),
);
await queryRunner.manager.save(productItems);
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
error.message ||
'An unexpected error occurred while creating product items.',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}

View File

@ -1,69 +0,0 @@
import {
SpaceModelEntity,
SubspaceModelEntity,
SubspaceProductModelRepository,
} from '@app/common/modules/space-model';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { SubspaceProductItemModelService } from './subspace-product-item-model.service';
import { CreateSpaceProductModelDto } from '../../dtos';
import { QueryRunner } from 'typeorm';
import { BaseProductModelService } from '../../common';
import { ProductService } from 'src/product/services';
@Injectable()
export class SubspaceProductModelService extends BaseProductModelService {
constructor(
private readonly subpaceProductModelRepository: SubspaceProductModelRepository,
productService: ProductService,
private readonly subspaceProductItemModelService: SubspaceProductItemModelService,
) {
super(productService);
}
async createSubspaceProductModels(
spaceProductModelDtos: CreateSpaceProductModelDto[],
spaceModel: SpaceModelEntity,
subspaceModel: SubspaceModelEntity,
queryRunner: QueryRunner,
) {
try {
const productModels = await Promise.all(
spaceProductModelDtos.map(async (dto) => {
this.validateProductCount(dto);
const product = await this.getProduct(dto.productUuid);
return queryRunner.manager.create(
this.subpaceProductModelRepository.target,
{
product,
productCount: dto.productCount,
subspaceModel,
},
);
}),
);
const savedProductModels = await queryRunner.manager.save(productModels);
await Promise.all(
spaceProductModelDtos.map((dto, index) => {
const savedModel = savedProductModels[index];
return this.subspaceProductItemModelService.createProdutItemModel(
dto.items,
savedModel, // Pass the saved model
spaceModel,
queryRunner,
);
}),
);
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
error.message ||
'An unexpected error occurred while creating product models.',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}

View File

@ -0,0 +1,295 @@
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, 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[],
): 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 tagEntities = await Promise.all(
tags.map(async (tagDto) =>
this.prepareTagEntity(tagDto, queryRunner, spaceModel, subspaceModel),
),
);
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.',
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 (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.softDelete(this.tagModelRepository.target, id),
);
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 = {};
try {
for (const tag of tags) {
if (tag.action === ModifyAction.ADD) {
const createTagDto: CreateTagModelDto = {
tag: tag.tag as string,
productUuid: tag.productUuid as string,
};
const newModel = await this.createTags(
[createTagDto],
queryRunner,
spaceModel,
subspaceModel,
);
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,
): Promise<void> {
const tagExists = await this.tagModelRepository.exists({
where: [
{
tag,
spaceModel: { uuid: spaceModel.uuid },
product: { uuid: productUuid },
disabled: false,
},
{
tag,
subspaceModel: { spaceModel: { uuid: spaceModel.uuid } },
product: { uuid: productUuid },
disabled: false,
},
],
});
if (tagExists) {
throw new HttpException(`Tag can't be reused`, HttpStatus.CONFLICT);
}
}
private async prepareTagEntity(
tagDto: CreateTagModelDto,
queryRunner: QueryRunner,
spaceModel?: SpaceModelEntity,
subspaceModel?: SubspaceModelEntity,
): Promise<TagModel> {
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);
} else {
await this.checkTagReuse(
tagDto.tag,
tagDto.productUuid,
subspaceModel.spaceModel,
);
}
return queryRunner.manager.create(TagModel, {
tag: tagDto.tag,
product: product.data,
spaceModel,
subspaceModel,
});
}
async getTagByUuid(uuid: string): Promise<TagModel> {
const tag = await this.tagModelRepository.findOne({
where: { uuid },
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;
}
}

View File

@ -4,42 +4,73 @@ import { ConfigModule } from '@nestjs/config';
import { SpaceModelController } from './controllers';
import {
SpaceModelService,
SpaceProductItemModelService,
SpaceProductModelService,
SubSpaceModelService,
SubspaceProductItemModelService,
TagModelService,
} from './services';
import {
SpaceModelRepository,
SpaceProductItemModelRepository,
SpaceProductModelRepository,
SubspaceModelRepository,
SubspaceProductItemModelRepository,
SubspaceProductModelRepository,
TagModelRepository,
} from '@app/common/modules/space-model';
import { ProjectRepository } from '@app/common/modules/project/repositiories';
import { ProductRepository } from '@app/common/modules/product/repositories';
import { SubspaceProductModelService } from './services/subspace/subspace-product-model.service';
import {
PropogateDeleteSpaceModelHandler,
PropogateUpdateSpaceModelHandler,
} from './handlers';
import { CqrsModule } from '@nestjs/cqrs';
import {
SpaceLinkRepository,
SpaceRepository,
TagRepository,
} from '@app/common/modules/space';
import { SubspaceRepository } from '@app/common/modules/space/repositories/subspace.repository';
import {
SpaceLinkService,
SpaceService,
SubspaceDeviceService,
SubSpaceService,
ValidationService,
} from 'src/space/services';
import { TagService } from 'src/space/services/tag';
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';
const CommandHandlers = [
PropogateUpdateSpaceModelHandler,
PropogateDeleteSpaceModelHandler,
];
@Module({
imports: [ConfigModule, SpaceRepositoryModule],
imports: [ConfigModule, SpaceRepositoryModule, CqrsModule],
controllers: [SpaceModelController],
providers: [
...CommandHandlers,
SpaceModelService,
SpaceService,
SpaceModelRepository,
SpaceRepository,
ProjectRepository,
SubSpaceModelService,
SpaceProductModelService,
SubspaceModelRepository,
SpaceProductModelRepository,
ProductRepository,
SpaceProductItemModelService,
SpaceProductItemModelRepository,
SubspaceProductItemModelService,
SubspaceProductItemModelRepository,
SubspaceProductModelService,
SubspaceProductModelRepository,
SubspaceRepository,
TagModelService,
TagModelRepository,
SubSpaceService,
ValidationService,
TagService,
SubspaceDeviceService,
CommunityService,
TagRepository,
DeviceRepository,
TuyaService,
CommunityRepository,
SpaceLinkService,
SpaceLinkRepository,
],
exports: [],
exports: [CqrsModule, SpaceModelService],
})
export class SpaceModelModule {}

View File

@ -0,0 +1,10 @@
import { SpaceEntity } from '@app/common/modules/space';
export class DisableSpaceCommand {
constructor(
public readonly param: {
spaceUuid: string;
orphanSpace: SpaceEntity;
},
) {}
}

View File

@ -0,0 +1 @@
export * from './disable-space.command';

View File

@ -67,8 +67,8 @@ export class SpaceController {
description: ControllerRoute.SPACE.ACTIONS.DELETE_SPACE_DESCRIPTION,
})
@Delete('/:spaceUuid')
async deleteSpace(@Param() params: GetSpaceParam): Promise<BaseResponseDto> {
return this.spaceService.delete(params);
async deleteSpace(@Param() params: GetSpaceParam) {
return await this.spaceService.delete(params);
}
@ApiBearerAuth()

View File

@ -65,7 +65,7 @@ export class SubSpaceController {
})
@Get(':subSpaceUuid')
async findOne(@Param() params: GetSubSpaceParam): Promise<BaseResponseDto> {
return this.subSpaceService.findOne(params);
return this.subSpaceService.getOne(params);
}
@ApiBearerAuth()

View File

@ -11,41 +11,7 @@ import {
ValidateNested,
} from 'class-validator';
import { AddSubspaceDto } from './subspace';
export class CreateSpaceProductItemDto {
@ApiProperty({
description: 'Specific name for the product item',
example: 'Light 1',
})
@IsNotEmpty()
@IsString()
tag: string;
}
export class ProductAssignmentDto {
@ApiProperty({
description: 'UUID of the product to be assigned',
example: 'prod-uuid-1234',
})
@IsNotEmpty()
productId: string;
@ApiProperty({
description: 'Number of items to assign for the product',
example: 3,
})
count: number;
@ApiProperty({
description: 'Specific names for each product item',
type: [CreateSpaceProductItemDto],
example: [{ tag: 'Light 1' }, { tag: 'Light 2' }, { tag: 'Light 3' }],
})
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreateSpaceProductItemDto)
items: CreateSpaceProductItemDto[];
}
import { CreateTagDto } from './tag';
export class AddSpaceDto {
@ApiProperty({
@ -103,26 +69,22 @@ export class AddSpaceDto {
@IsOptional()
direction?: string;
@ApiProperty({
description: 'List of products assigned to this space',
type: [ProductAssignmentDto],
required: false,
})
@IsArray()
@ValidateNested({ each: true })
@IsOptional()
@Type(() => ProductAssignmentDto)
products?: ProductAssignmentDto[];
@ApiProperty({
description: 'List of subspaces included in the model',
type: [AddSubspaceDto],
})
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => AddSubspaceDto)
subspaces?: AddSubspaceDto[];
@ApiProperty({
description: 'List of tags associated with the space model',
type: [CreateTagDto],
})
@ValidateNested({ each: true })
@Type(() => CreateTagDto)
tags?: CreateTagDto[];
}
export class AddUserSpaceDto {

View File

@ -5,3 +5,4 @@ export * from './user-space.param';
export * from './subspace';
export * from './project.param.dto';
export * from './update.space.dto';
export * from './tag';

View File

@ -1,13 +1,7 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator';
import { CreateTagDto } from '../tag';
import { Type } from 'class-transformer';
import {
IsArray,
IsNotEmpty,
IsOptional,
IsString,
ValidateNested,
} from 'class-validator';
import { ProductAssignmentDto } from '../add.space.dto';
export class AddSubspaceDto {
@ApiProperty({
@ -19,13 +13,11 @@ export class AddSubspaceDto {
subspaceName: string;
@ApiProperty({
description: 'List of products assigned to this space',
type: [ProductAssignmentDto],
required: false,
description: 'List of tags associated with the subspace',
type: [CreateTagDto],
})
@IsArray()
@ValidateNested({ each: true })
@IsOptional()
@Type(() => ProductAssignmentDto)
products?: ProductAssignmentDto[];
@Type(() => CreateTagDto)
tags?: CreateTagDto[];
}

View File

@ -0,0 +1,12 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class DeleteSubspaceDto {
@ApiProperty({
description: 'Uuid of the subspace model need to be deleted',
example: '982fc3a3-64dc-4afb-a5b5-65ee8fef0424',
})
@IsNotEmpty()
@IsString()
subspaceUuid: string;
}

View File

@ -1,3 +1,6 @@
export * from './add.subspace.dto';
export * from './get.subspace.param';
export * from './add.subspace-device.param';
export * from './update.subspace.dto';
export * from './delete.subspace.dto';
export * from './modify.subspace.dto';

View File

@ -0,0 +1,47 @@
import { ModifyAction } from '@app/common/constants/modify-action.enum';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import {
IsEnum,
IsOptional,
IsString,
IsArray,
ValidateNested,
} from 'class-validator';
import { ModifyTagDto } from '../tag/modify-tag.dto';
export class ModifySubspaceDto {
@ApiProperty({
description: 'Action to perform: add, update, or delete',
example: ModifyAction.ADD,
})
@IsEnum(ModifyAction)
action: ModifyAction;
@ApiPropertyOptional({
description: 'UUID of the subspace (required for update/delete)',
example: '123e4567-e89b-12d3-a456-426614174000',
})
@IsOptional()
@IsString()
uuid?: string;
@ApiPropertyOptional({
description: 'Name of the subspace (required for add/update)',
example: 'Living Room',
})
@IsOptional()
@IsString()
subspaceName?: string;
@ApiPropertyOptional({
description:
'List of tag modifications (add/update/delete) for the subspace',
type: [ModifyTagDto],
})
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => ModifyTagDto)
tags?: ModifyTagDto[];
}

View File

@ -0,0 +1,16 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class UpdateSubspaceDto {
@ApiProperty({
description: 'Name of the subspace',
example: 'Living Room',
})
@IsNotEmpty()
@IsString()
subspaceName?: string;
@IsNotEmpty()
@IsString()
subspaceUuid: string;
}

View File

@ -0,0 +1,20 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class CreateTagDto {
@ApiProperty({
description: 'Tag associated with the space or subspace',
example: 'Temperature Control',
})
@IsNotEmpty()
@IsString()
tag: string;
@ApiProperty({
description: 'ID of the product associated with the tag',
example: '123e4567-e89b-12d3-a456-426614174000',
})
@IsNotEmpty()
@IsString()
productUuid: string;
}

View File

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

View File

@ -0,0 +1,37 @@
import { ModifyAction } from '@app/common/constants/modify-action.enum';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsEnum, IsOptional, IsString } from 'class-validator';
export class ModifyTagDto {
@ApiProperty({
description: 'Action to perform: add, update, or delete',
example: ModifyAction.ADD,
})
@IsEnum(ModifyAction)
action: ModifyAction;
@ApiPropertyOptional({
description: 'UUID of the tag (required for update/delete)',
example: '123e4567-e89b-12d3-a456-426614174000',
})
@IsOptional()
@IsString()
uuid?: string;
@ApiPropertyOptional({
description: 'Name of the tag (required for add/update)',
example: 'Temperature Sensor',
})
@IsOptional()
@IsString()
tag?: string;
@ApiPropertyOptional({
description:
'UUID of the product associated with the tag (required for add)',
example: 'c789a91e-549a-4753-9006-02f89e8170e0',
})
@IsOptional()
@IsString()
productUuid?: string;
}

View File

@ -1,4 +1,61 @@
import { PartialType } from '@nestjs/swagger';
import { AddSpaceDto } from './add.space.dto';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import {
IsArray,
IsNumber,
IsOptional,
IsString,
ValidateNested,
} from 'class-validator';
import { ModifySubspaceDto } from './subspace';
import { Type } from 'class-transformer';
import { ModifyTagDto } from './tag/modify-tag.dto';
export class UpdateSpaceDto extends PartialType(AddSpaceDto) {}
export class UpdateSpaceDto {
@ApiProperty({
description: 'Updated name of the space ',
example: 'New Space Name',
})
@IsOptional()
@IsString()
spaceName?: string;
@ApiProperty({
description: 'Icon identifier for the space',
example: 'assets/location',
required: false,
})
@IsString()
@IsOptional()
public icon?: string;
@ApiProperty({ description: 'X position on canvas', example: 120 })
@IsNumber()
@IsOptional()
x?: number;
@ApiProperty({ description: 'Y position on canvas', example: 200 })
@IsNumber()
@IsOptional()
y?: number;
@ApiPropertyOptional({
description: 'List of subspace modifications (add/update/delete)',
type: [ModifySubspaceDto],
})
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => ModifySubspaceDto)
subspace?: ModifySubspaceDto[];
@ApiPropertyOptional({
description:
'List of tag modifications (add/update/delete) for the space model',
type: [ModifyTagDto],
})
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => ModifyTagDto)
tags?: ModifyTagDto[];
}

View File

@ -0,0 +1,100 @@
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';
import { UserSpaceService } from 'src/users/services';
import { DataSource } from 'typeorm';
import { DisableSpaceCommand } from '../commands';
import {
SubSpaceService,
SpaceLinkService,
SpaceSceneService,
} from '../services';
import { TagService } from '../services/tag';
@CommandHandler(DisableSpaceCommand)
export class DisableSpaceHandler
implements ICommandHandler<DisableSpaceCommand>
{
constructor(
private readonly subSpaceService: SubSpaceService,
private readonly userService: UserSpaceService,
private readonly tagService: TagService,
private readonly deviceService: DeviceService,
private readonly spaceLinkService: SpaceLinkService,
private readonly sceneService: SpaceSceneService,
private readonly dataSource: DataSource,
) {}
async execute(command: DisableSpaceCommand): Promise<void> {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const { spaceUuid, orphanSpace } = command.param;
const space = await queryRunner.manager.findOne(SpaceEntity, {
where: { uuid: spaceUuid, disabled: false },
relations: [
'subspaces',
'parent',
'tags',
'devices',
'outgoingConnections',
'incomingConnections',
'scenes',
'children',
'userSpaces',
],
});
if (!space) {
throw new HttpException(
`Space with UUID ${spaceUuid} not found`,
HttpStatus.NOT_FOUND,
);
}
if (space.children && space.children.length > 0) {
for (const child of space.children) {
await this.execute(
new DisableSpaceCommand({ spaceUuid: child.uuid, orphanSpace }),
);
}
}
const tagUuids = space.tags?.map((tag) => tag.uuid) || [];
const subspaceDtos =
space.subspaces?.map((subspace) => ({
subspaceUuid: subspace.uuid,
})) || [];
const deletionTasks = [
this.subSpaceService.deleteSubspaces(subspaceDtos, queryRunner),
this.userService.deleteUserSpace(space.uuid),
this.tagService.deleteTags(tagUuids, queryRunner),
this.deviceService.deleteDevice(
space.devices,
orphanSpace,
queryRunner,
),
this.spaceLinkService.deleteSpaceLink(space, queryRunner),
this.sceneService.deleteScenes(space, queryRunner),
];
await Promise.all(deletionTasks);
// Mark space as disabled
space.disabled = true;
await queryRunner.manager.save(space);
await queryRunner.commitTransaction();
} catch (error) {
await queryRunner.rollbackTransaction();
console.error(`Failed to disable space: ${error.message}`);
} finally {
await queryRunner.release();
}
}
}

View File

@ -0,0 +1 @@
export * from './disable-space.handler';

View File

@ -0,0 +1,5 @@
import { SubspaceEntity } from '@app/common/modules/space';
export interface ModifySubspacePayload {
addedSubspaces?: SubspaceEntity[];
}

View File

View File

@ -4,6 +4,4 @@ export * from './space-device.service';
export * from './subspace';
export * from './space-link';
export * from './space-scene.service';
export * from './space-products';
export * from './space-product-items';
export * from './space-validation.service';

View File

@ -1,40 +1,41 @@
import {
SpaceLinkRepository,
SpaceRepository,
} from '@app/common/modules/space/repositories';
import { SpaceEntity, SpaceLinkEntity } from '@app/common/modules/space';
import { SpaceLinkRepository } from '@app/common/modules/space/repositories';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { QueryRunner } from 'typeorm';
@Injectable()
export class SpaceLinkService {
constructor(
private readonly spaceRepository: SpaceRepository,
private readonly spaceLinkRepository: SpaceLinkRepository,
) {}
constructor(private readonly spaceLinkRepository: SpaceLinkRepository) {}
async saveSpaceLink(
startSpaceId: string,
endSpaceId: string,
direction: string,
queryRunner: QueryRunner,
): Promise<void> {
try {
// Check if a link between the startSpace and endSpace already exists
const existingLink = await this.spaceLinkRepository.findOne({
const existingLink = await queryRunner.manager.findOne(SpaceLinkEntity, {
where: {
startSpace: { uuid: startSpaceId },
endSpace: { uuid: endSpaceId },
disabled: false,
},
});
if (existingLink) {
// Update the direction if the link exists
existingLink.direction = direction;
await this.spaceLinkRepository.save(existingLink);
await queryRunner.manager.save(SpaceLinkEntity, existingLink);
return;
}
const existingEndSpaceLink = await this.spaceLinkRepository.findOne({
where: { endSpace: { uuid: endSpaceId } },
});
const existingEndSpaceLink = await queryRunner.manager.findOne(
SpaceLinkEntity,
{
where: { endSpace: { uuid: endSpaceId } },
},
);
if (
existingEndSpaceLink &&
@ -46,7 +47,7 @@ export class SpaceLinkService {
}
// Find start space
const startSpace = await this.spaceRepository.findOne({
const startSpace = await queryRunner.manager.findOne(SpaceEntity, {
where: { uuid: startSpaceId },
});
@ -58,7 +59,7 @@ export class SpaceLinkService {
}
// Find end space
const endSpace = await this.spaceRepository.findOne({
const endSpace = await queryRunner.manager.findOne(SpaceEntity, {
where: { uuid: endSpaceId },
});
@ -76,7 +77,7 @@ export class SpaceLinkService {
direction,
});
await this.spaceLinkRepository.save(spaceLink);
await queryRunner.manager.save(SpaceLinkEntity, spaceLink);
} catch (error) {
throw new HttpException(
error.message ||
@ -85,4 +86,35 @@ export class SpaceLinkService {
);
}
}
async deleteSpaceLink(
space: SpaceEntity,
queryRunner: QueryRunner,
): Promise<void> {
try {
const spaceLinks = await queryRunner.manager.find(SpaceLinkEntity, {
where: [
{ startSpace: space, disabled: false },
{ endSpace: space, disabled: false },
],
});
if (spaceLinks.length === 0) {
return;
}
const linkIds = spaceLinks.map((link) => link.uuid);
await queryRunner.manager
.createQueryBuilder()
.update(SpaceLinkEntity)
.set({ disabled: true })
.whereInIds(linkIds)
.execute();
} catch (error) {
throw new HttpException(
`Failed to disable space links for the given space: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}

View File

@ -1 +0,0 @@
export * from './space-product-items.service';

View File

@ -1,87 +0,0 @@
import {
SpaceEntity,
SpaceProductEntity,
SpaceProductItemRepository,
} from '@app/common/modules/space';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { CreateSpaceProductItemDto } from '../../dtos';
import { QueryRunner } from 'typeorm';
import { SpaceProductModelEntity } from '@app/common/modules/space-model';
import { BaseProductItemService } from '../../common';
@Injectable()
export class SpaceProductItemService extends BaseProductItemService {
constructor(
private readonly spaceProductItemRepository: SpaceProductItemRepository,
) {
super();
}
async createProductItem(
itemModelDtos: CreateSpaceProductItemDto[],
spaceProduct: SpaceProductEntity,
space: SpaceEntity,
queryRunner: QueryRunner,
) {
if (!itemModelDtos?.length) return;
const incomingTags = itemModelDtos.map((item) => item.tag);
await this.validateTags(incomingTags, queryRunner, space.uuid);
try {
const productItems = itemModelDtos.map((dto) =>
queryRunner.manager.create(this.spaceProductItemRepository.target, {
tag: dto.tag,
spaceProduct,
}),
);
await this.saveProductItems(
productItems,
this.spaceProductItemRepository.target,
queryRunner,
);
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
error.message ||
'An unexpected error occurred while creating product items.',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async createSpaceProductItemFromModel(
spaceProduct: SpaceProductEntity,
spaceProductModel: SpaceProductModelEntity,
queryRunner: QueryRunner,
) {
const spaceProductItemModels = spaceProductModel.items;
if (!spaceProductItemModels?.length) return;
try {
const productItems = spaceProductItemModels.map((model) =>
queryRunner.manager.create(this.spaceProductItemRepository.target, {
tag: model.tag,
spaceProduct,
}),
);
await queryRunner.manager.save(productItems);
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
error.message ||
'An unexpected error occurred while creating product items.',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}

View File

@ -1 +0,0 @@
export * from './space-products.service';

View File

@ -1,226 +0,0 @@
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
import { ProductRepository } from '@app/common/modules/product/repositories';
import { SpaceEntity } from '@app/common/modules/space/entities';
import { SpaceProductEntity } from '@app/common/modules/space/entities/space-product.entity';
import { In, QueryRunner } from 'typeorm';
import { ProductAssignmentDto } from '../../dtos';
import { SpaceProductItemService } from '../space-product-items';
import { SpaceModelEntity } from '@app/common/modules/space-model';
import { ProductEntity } from '@app/common/modules/product/entities';
import { ProductService } from 'src/product/services';
@Injectable()
export class SpaceProductService {
constructor(
private readonly productRepository: ProductRepository,
private readonly spaceProductItemService: SpaceProductItemService,
private readonly productService: ProductService,
) {}
async createFromModel(
spaceModel: SpaceModelEntity,
space: SpaceEntity,
queryRunner: QueryRunner,
) {
const spaceProductModels = spaceModel.spaceProductModels;
if (!spaceProductModels?.length) return;
const newSpaceProducts = [];
spaceProductModels.map((spaceProductModel) => {
newSpaceProducts.push(
queryRunner.manager.create(SpaceProductEntity, {
space: space,
product: spaceProductModel.product,
productCount: spaceProductModel.productCount,
spaceProductModel: spaceProductModel,
}),
);
});
if (newSpaceProducts.length > 0) {
await queryRunner.manager.save(SpaceProductEntity, newSpaceProducts);
await Promise.all(
newSpaceProducts.map((spaceProduct, index) => {
const spaceProductModel = spaceProductModels[index];
return this.spaceProductItemService.createSpaceProductItemFromModel(
spaceProduct,
spaceProductModel,
queryRunner,
);
}),
);
}
}
async assignProductsToSpace(
space: SpaceEntity,
products: ProductAssignmentDto[],
queryRunner: QueryRunner,
): Promise<SpaceProductEntity[]> {
let updatedProducts: SpaceProductEntity[] = [];
try {
const uniqueProducts = this.validateUniqueProducts(products);
const productEntities = await this.getProductEntities(uniqueProducts);
const existingSpaceProducts = await this.getExistingSpaceProducts(
space,
queryRunner,
);
if (existingSpaceProducts) {
updatedProducts = await this.updateExistingProducts(
existingSpaceProducts,
uniqueProducts,
productEntities,
queryRunner,
);
}
const newProducts = await this.createNewProducts(
uniqueProducts,
productEntities,
space,
queryRunner,
);
return [...updatedProducts, ...newProducts];
} catch (error) {
if (!(error instanceof HttpException)) {
throw new HttpException(
`An error occurred while assigning products to the space ${error}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
throw error;
}
}
private validateUniqueProducts(
products: ProductAssignmentDto[],
): ProductAssignmentDto[] {
const productIds = new Set();
const uniqueProducts = [];
for (const product of products) {
if (productIds.has(product.productId)) {
throw new HttpException(
`Duplicate product ID found: ${product.productId}`,
HttpStatus.BAD_REQUEST,
);
}
productIds.add(product.productId);
uniqueProducts.push(product);
}
return uniqueProducts;
}
private async getProductEntities(
products: ProductAssignmentDto[],
): Promise<Map<string, any>> {
try {
const productIds = products.map((p) => p.productId);
const productEntities = await this.productRepository.find({
where: { uuid: In(productIds) },
});
return new Map(productEntities.map((p) => [p.uuid, p]));
} catch (error) {
console.error('Error fetching product entities:', error);
throw new HttpException(
'Failed to fetch product entities',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
private async getExistingSpaceProducts(
space: SpaceEntity,
queryRunner: QueryRunner,
): Promise<SpaceProductEntity[]> {
return queryRunner.manager.find(SpaceProductEntity, {
where: { space: { uuid: space.uuid } },
relations: ['product'],
});
}
private async updateExistingProducts(
existingSpaceProducts: SpaceProductEntity[],
uniqueProducts: ProductAssignmentDto[],
productEntities: Map<string, any>,
queryRunner: QueryRunner,
): Promise<SpaceProductEntity[]> {
const updatedProducts = [];
for (const { productId, count } of uniqueProducts) {
productEntities.get(productId);
const existingProduct = existingSpaceProducts.find(
(spaceProduct) => spaceProduct.product.uuid === productId,
);
if (existingProduct && existingProduct.productCount !== count) {
existingProduct.productCount = count;
updatedProducts.push(existingProduct);
}
}
if (updatedProducts.length > 0) {
await queryRunner.manager.save(SpaceProductEntity, updatedProducts);
}
return updatedProducts;
}
private async createNewProducts(
uniqueSpaceProducts: ProductAssignmentDto[],
productEntities: Map<string, any>,
space: SpaceEntity,
queryRunner: QueryRunner,
): Promise<SpaceProductEntity[]> {
const newProducts = [];
for (const uniqueSpaceProduct of uniqueSpaceProducts) {
const product = productEntities.get(uniqueSpaceProduct.productId);
await this.getProduct(uniqueSpaceProduct.productId);
this.validateProductCount(uniqueSpaceProduct);
newProducts.push(
queryRunner.manager.create(SpaceProductEntity, {
space,
product,
productCount: uniqueSpaceProduct.count,
}),
);
}
if (newProducts.length > 0) {
await queryRunner.manager.save(SpaceProductEntity, newProducts);
await Promise.all(
uniqueSpaceProducts.map((dto, index) => {
const spaceProduct = newProducts[index];
return this.spaceProductItemService.createProductItem(
dto.items,
spaceProduct,
space,
queryRunner,
);
}),
);
}
return newProducts;
}
private validateProductCount(dto: ProductAssignmentDto) {
const productItemCount = dto.items.length;
if (dto.count !== productItemCount) {
throw new HttpException(
`Product count (${dto.count}) does not match the number of items (${productItemCount}) for product ID ${dto.productId}.`,
HttpStatus.BAD_REQUEST,
);
}
}
async getProduct(productId: string): Promise<ProductEntity> {
const product = await this.productService.findOne(productId);
return product.data;
}
}

View File

@ -5,6 +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';
@Injectable()
export class SpaceSceneService {
@ -48,4 +51,32 @@ export class SpaceSceneService {
}
}
}
async deleteScenes(
space: SpaceEntity,
queryRunner: QueryRunner,
): Promise<void> {
try {
const scenes = await queryRunner.manager.find(SceneEntity, {
where: { space },
});
if (scenes.length === 0) {
return;
}
const sceneUuids = scenes.map((scene) => scene.uuid);
await Promise.all(
sceneUuids.map((uuid) =>
this.sceneSevice.deleteScene({ sceneUuid: uuid }),
),
);
} catch (error) {
throw new HttpException(
`Failed to delete scenes: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}

View File

@ -1,4 +1,3 @@
import { CommunityEntity } from '@app/common/modules/community/entities';
import { SpaceEntity } from '@app/common/modules/space/entities';
import { SpaceRepository } from '@app/common/modules/space/repositories';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
@ -21,14 +20,14 @@ export class ValidationService {
async validateCommunityAndProject(
communityUuid: string,
projectUuid: string,
): Promise<CommunityEntity> {
await this.projectService.findOne(projectUuid);
) {
const project = await this.projectService.findOne(projectUuid);
const community = await this.communityService.getCommunityById({
communityUuid,
projectUuid,
});
return community.data;
return { community: community.data, project: project };
}
async validateSpaceWithinCommunityAndProject(
@ -43,7 +42,16 @@ export class ValidationService {
async validateSpace(spaceUuid: string): Promise<SpaceEntity> {
const space = await this.spaceRepository.findOne({
where: { uuid: spaceUuid },
where: { uuid: spaceUuid, disabled: false },
relations: [
'parent',
'children',
'subspaces',
'tags',
'subspaces.tags',
'subspaces.devices',
'devices',
],
});
if (!space) {
@ -61,11 +69,10 @@ export class ValidationService {
where: { uuid: spaceModelUuid },
relations: [
'subspaceModels',
'subspaceModels.productModels.product',
'subspaceModels.productModels',
'spaceProductModels',
'spaceProductModels.product',
'spaceProductModels.items',
'subspaceModels.tags',
'tags',
'subspaceModels.tags.product',
'tags.product',
],
});

View File

@ -7,9 +7,10 @@ import {
} from '@nestjs/common';
import {
AddSpaceDto,
AddSubspaceDto,
CommunitySpaceParam,
CreateTagDto,
GetSpaceParam,
ProductAssignmentDto,
UpdateSpaceDto,
} from '../dtos';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
@ -17,29 +18,35 @@ 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 { SpaceProductService } from './space-products';
import { CreateSubspaceModelDto } from 'src/space-model/dtos';
import { SubSpaceService } from './subspace';
import { DataSource, Not } from 'typeorm';
import { DataSource, Not, QueryRunner } from 'typeorm';
import { ValidationService } from './space-validation.service';
import { ORPHAN_SPACE_NAME } from '@app/common/constants/orphan-constant';
import {
ORPHAN_COMMUNITY_NAME,
ORPHAN_SPACE_NAME,
} from '@app/common/constants/orphan-constant';
import { CommandBus } from '@nestjs/cqrs';
import { TagService } from './tag';
import { SpaceModelService } from 'src/space-model/services';
import { DisableSpaceCommand } from '../commands';
@Injectable()
export class SpaceService {
constructor(
private readonly dataSource: DataSource,
private readonly spaceRepository: SpaceRepository,
private readonly spaceLinkService: SpaceLinkService,
private readonly spaceProductService: SpaceProductService,
private readonly subSpaceService: SubSpaceService,
private readonly validationService: ValidationService,
private readonly tagService: TagService,
private readonly spaceModelService: SpaceModelService,
private commandBus: CommandBus,
) {}
async createSpace(
addSpaceDto: AddSpaceDto,
params: CommunitySpaceParam,
): Promise<BaseResponseDto> {
const { parentUuid, direction, products, spaceModelUuid, subspaces } =
const { parentUuid, direction, spaceModelUuid, subspaces, tags } =
addSpaceDto;
const { communityUuid, projectUuid } = params;
@ -60,7 +67,7 @@ export class SpaceService {
projectUuid,
);
this.validateSpaceCreation(spaceModelUuid, products, subspaces);
this.validateSpaceCreation(addSpaceDto, spaceModelUuid);
const parent = parentUuid
? await this.validationService.validateSpace(parentUuid)
@ -71,50 +78,34 @@ export class SpaceService {
: null;
try {
const newSpace = queryRunner.manager.create(SpaceEntity, {
const space = queryRunner.manager.create(SpaceEntity, {
...addSpaceDto,
spaceModel,
parent: parentUuid ? parent : null,
community,
});
await queryRunner.manager.save(newSpace);
const newSpace = await queryRunner.manager.save(space);
if (direction && parent) {
await this.spaceLinkService.saveSpaceLink(
parent.uuid,
newSpace.uuid,
direction,
);
}
await Promise.all([
spaceModelUuid &&
this.createFromModel(spaceModelUuid, queryRunner, newSpace),
direction && parent
? this.spaceLinkService.saveSpaceLink(
parent.uuid,
newSpace.uuid,
direction,
queryRunner,
)
: Promise.resolve(),
subspaces?.length
? this.createSubspaces(subspaces, newSpace, queryRunner, tags)
: Promise.resolve(),
tags?.length
? this.createTags(tags, queryRunner, newSpace)
: Promise.resolve(),
]);
if (subspaces?.length) {
await this.subSpaceService.createSubspacesFromDto(
subspaces,
newSpace,
queryRunner,
);
} else if (spaceModel && spaceModel.subspaceModels.length) {
await this.subSpaceService.createSubSpaceFromModel(
spaceModel,
newSpace,
queryRunner,
);
}
if (products && products.length > 0) {
await this.spaceProductService.assignProductsToSpace(
newSpace,
products,
queryRunner,
);
} else if (spaceModel && spaceModel.spaceProductModels.length) {
await this.spaceProductService.createFromModel(
spaceModel,
newSpace,
queryRunner,
);
}
await queryRunner.commitTransaction();
return new SuccessResponseDto({
@ -134,6 +125,41 @@ export class SpaceService {
}
}
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;
}
throw new HttpException(
'An error occurred while creating the space from space model',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async getSpacesHierarchyForCommunity(
params: CommunitySpaceParam,
): Promise<BaseResponseDto> {
@ -204,21 +230,31 @@ export class SpaceService {
try {
const { communityUuid, spaceUuid, projectUuid } = params;
const space =
await this.validationService.validateSpaceWithinCommunityAndProject(
const { project } =
await this.validationService.validateCommunityAndProject(
communityUuid,
projectUuid,
spaceUuid,
);
const space = await this.validationService.validateSpace(spaceUuid);
if (space.spaceName === ORPHAN_SPACE_NAME) {
throw new HttpException(
`space ${ORPHAN_SPACE_NAME} cannot be deleted`,
HttpStatus.BAD_REQUEST,
);
}
// Delete the space
await this.spaceRepository.remove(space);
const orphanSpace = await this.spaceRepository.findOne({
where: {
community: {
uuid: `${ORPHAN_COMMUNITY_NAME}-${project.name}`,
},
spaceName: ORPHAN_SPACE_NAME,
},
});
await this.disableSpace(space, orphanSpace);
return new SuccessResponseDto({
message: `Space with ID ${spaceUuid} successfully deleted`,
@ -235,6 +271,12 @@ export class SpaceService {
}
}
async disableSpace(space: SpaceEntity, orphanSpace: SpaceEntity) {
await this.commandBus.execute(
new DisableSpaceCommand({ spaceUuid: space.uuid, orphanSpace }),
);
}
async updateSpace(
params: GetSpaceParam,
updateSpaceDto: UpdateSpaceDto,
@ -255,30 +297,39 @@ export class SpaceService {
if (space.spaceName === ORPHAN_SPACE_NAME) {
throw new HttpException(
`space ${ORPHAN_SPACE_NAME} cannot be updated`,
`Space "${ORPHAN_SPACE_NAME}" cannot be updated`,
HttpStatus.BAD_REQUEST,
);
}
// If a parentId is provided, check if the parent exists
const { parentUuid, products } = updateSpaceDto;
const parent = parentUuid
? await this.validationService.validateSpace(parentUuid)
: null;
this.updateSpaceProperties(space, updateSpaceDto);
// Update other space properties from updateSpaceDto
Object.assign(space, updateSpaceDto, { parent });
await queryRunner.manager.save(space);
// Save the updated space
const updatedSpace = await queryRunner.manager.save(space);
const hasSubspace = updateSpaceDto.subspace?.length > 0;
const hasTags = updateSpaceDto.tags?.length > 0;
if (products && products.length > 0) {
await this.spaceProductService.assignProductsToSpace(
updatedSpace,
products,
if (hasSubspace || hasTags) {
await this.tagService.unlinkModels(space.tags, queryRunner);
await this.subSpaceService.unlinkModels(space.subspaces, queryRunner);
}
if (hasSubspace) {
await this.subSpaceService.modifySubSpace(
updateSpaceDto.subspace,
queryRunner,
space,
);
}
if (hasTags) {
await this.tagService.modifyTags(
updateSpaceDto.tags,
queryRunner,
space,
);
}
await queryRunner.commitTransaction();
return new SuccessResponseDto({
@ -292,6 +343,7 @@ export class SpaceService {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
'An error occurred while updating the space',
HttpStatus.INTERNAL_SERVER_ERROR,
@ -301,6 +353,49 @@ export class SpaceService {
}
}
async unlinkSpaceFromModel(
space: SpaceEntity,
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);
}
if (space.subspaces) {
await this.subSpaceService.unlinkModels(space.subspaces, queryRunner);
}
}
} catch (error) {
throw new HttpException(
`Failed to unlink space model: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
private updateSpaceProperties(
space: SpaceEntity,
updateSpaceDto: UpdateSpaceDto,
): void {
const { spaceName, x, y, icon } = updateSpaceDto;
if (spaceName) space.spaceName = spaceName;
if (x) space.x = x;
if (y) space.y = y;
if (icon) space.icon = icon;
}
async getSpacesHierarchyForSpace(
params: GetSpaceParam,
): Promise<BaseResponseDto> {
@ -314,7 +409,7 @@ export class SpaceService {
try {
// Get all spaces that are children of the provided space, including the parent-child relations
const spaces = await this.spaceRepository.find({
where: { parent: { uuid: spaceUuid } },
where: { parent: { uuid: spaceUuid }, disabled: false },
relations: ['parent', 'children'], // Include parent and children relations
});
@ -391,15 +486,36 @@ export class SpaceService {
}
private validateSpaceCreation(
addSpaceDto: AddSpaceDto,
spaceModelUuid?: string,
products?: ProductAssignmentDto[],
subSpaces?: CreateSubspaceModelDto[],
) {
if (spaceModelUuid && (products?.length || subSpaces?.length)) {
if (spaceModelUuid && (addSpaceDto.tags || addSpaceDto.subspaces)) {
throw new HttpException(
'For space creation choose either space model or products and subspace',
HttpStatus.CONFLICT,
);
}
}
private async createSubspaces(
subspaces: AddSubspaceDto[],
space: SpaceEntity,
queryRunner: QueryRunner,
tags: CreateTagDto[],
): Promise<void> {
space.subspaces = await this.subSpaceService.createSubspacesFromDto(
subspaces,
space,
queryRunner,
tags,
);
}
private async createTags(
tags: CreateTagDto[],
queryRunner: QueryRunner,
space: SpaceEntity,
): Promise<void> {
space.tags = await this.tagService.createTags(tags, queryRunner, space);
}
}

View File

@ -1,4 +1,2 @@
export * from './subspace.service';
export * from './subspace-device.service';
export * from './subspace-product-item.service';
export * from './subspace-product.service';

View File

@ -9,6 +9,8 @@ import { ProductRepository } from '@app/common/modules/product/repositories';
import { GetDeviceDetailsInterface } from '../../../device/interfaces/get.device.interface';
import { ValidationService } from '../space-validation.service';
import { SubspaceRepository } from '@app/common/modules/space/repositories/subspace.repository';
import { In, QueryRunner } from 'typeorm';
import { DeviceEntity } from '@app/common/modules/device/entities';
@Injectable()
export class SubspaceDeviceService {
@ -173,6 +175,30 @@ export class SubspaceDeviceService {
return device;
}
async deleteSubspaceDevices(
devices: DeviceEntity[],
queryRunner: QueryRunner,
): Promise<void> {
const deviceUuids = devices.map((device) => device.uuid);
try {
if (deviceUuids.length === 0) {
return;
}
await queryRunner.manager.update(
this.deviceRepository.target,
{ uuid: In(deviceUuids) },
{ subspace: null },
);
} catch (error) {
throw new HttpException(
`Failed to delete devices with IDs ${deviceUuids.join(', ')}: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
private throwNotFound(entity: string, uuid: string) {
throw new HttpException(
`${entity} with ID ${uuid} not found`,

View File

@ -1,92 +0,0 @@
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
import { QueryRunner } from 'typeorm';
import {
SpaceEntity,
SubspaceProductEntity,
SubspaceProductItemEntity,
} from '@app/common/modules/space';
import {
SubspaceProductItemModelEntity,
SubspaceProductModelEntity,
} from '@app/common/modules/space-model';
import { SubspaceProductItemRepository } from '@app/common/modules/space/repositories/subspace.repository';
import { CreateSpaceProductItemDto } from '../../dtos';
import { BaseProductItemService } from '../../common';
@Injectable()
export class SubspaceProductItemService extends BaseProductItemService {
constructor(
private readonly productItemRepository: SubspaceProductItemRepository,
) {
super();
}
async createItemFromModel(
product: SubspaceProductEntity,
productModel: SubspaceProductModelEntity,
queryRunner: QueryRunner,
): Promise<void> {
const itemModels = productModel.itemModels;
if (!itemModels?.length) return;
try {
const productItems = itemModels.map((model) =>
this.createProductItem(product, model, queryRunner),
);
await queryRunner.manager.save(
this.productItemRepository.target,
productItems,
);
} catch (error) {
throw new HttpException(
error.message || 'An error occurred while creating product items.',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
private createProductItem(
product: SubspaceProductEntity,
model: SubspaceProductItemModelEntity,
queryRunner: QueryRunner,
): Partial<any> {
return queryRunner.manager.create(this.productItemRepository.target, {
tag: model.tag,
product,
model,
});
}
async createItemFromDtos(
product: SubspaceProductEntity,
itemDto: CreateSpaceProductItemDto[],
queryRunner: QueryRunner,
space: SpaceEntity,
) {
if (!itemDto?.length) return;
const incomingTags = itemDto.map((item) => item.tag);
await this.validateTags(incomingTags, queryRunner, space.uuid);
try {
const productItems = itemDto.map((dto) =>
queryRunner.manager.create(SubspaceProductItemEntity, {
tag: dto.tag,
subspaceProduct: product,
}),
);
await queryRunner.manager.save(
this.productItemRepository.target,
productItems,
);
} catch (error) {
throw new HttpException(
error.message || 'An error occurred while creating product items.',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}

View File

@ -1,128 +0,0 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { QueryRunner } from 'typeorm';
import {
SpaceEntity,
SubspaceEntity,
SubspaceProductEntity,
} from '@app/common/modules/space';
import {
SubspaceModelEntity,
SubspaceProductModelEntity,
} from '@app/common/modules/space-model';
import { SubspaceProductItemService } from './subspace-product-item.service';
import { ProductAssignmentDto } from 'src/space/dtos';
import { ProductService } from 'src/product/services';
import { ProductEntity } from '@app/common/modules/product/entities';
@Injectable()
export class SubspaceProductService {
constructor(
private readonly subspaceProductItemService: SubspaceProductItemService,
private readonly productService: ProductService,
) {}
async createFromModel(
subspaceModel: SubspaceModelEntity,
subspace: SubspaceEntity,
queryRunner: QueryRunner,
): Promise<void> {
const productModels = subspaceModel.productModels;
if (!productModels?.length) return;
try {
const newSpaceProducts = productModels.map((productModel) =>
this.createSubspaceProductEntity(subspace, productModel),
);
const subspaceProducts = await queryRunner.manager.save(
SubspaceProductEntity,
newSpaceProducts,
);
await Promise.all(
subspaceProducts.map((subspaceProduct, index) =>
this.subspaceProductItemService.createItemFromModel(
subspaceProduct,
productModels[index],
queryRunner,
),
),
);
} catch (error) {
throw new HttpException(
`Transaction failed: Unable to create subspace products ${error}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
private createSubspaceProductEntity(
subspace: SubspaceEntity,
productModel: SubspaceProductModelEntity,
): Partial<SubspaceProductEntity> {
return {
subspace,
product: productModel.product,
productCount: productModel.productCount,
model: productModel,
};
}
async createFromDto(
productDtos: ProductAssignmentDto[],
subspace: SubspaceEntity,
queryRunner: QueryRunner,
space: SpaceEntity,
): Promise<void> {
try {
const newSpaceProducts = await Promise.all(
productDtos.map(async (dto) => {
this.validateProductCount(dto);
const product = await this.getProduct(dto.productId);
return queryRunner.manager.create(SubspaceProductEntity, {
subspace,
product,
productCount: dto.count,
});
}),
);
const subspaceProducts = await queryRunner.manager.save(
SubspaceProductEntity,
newSpaceProducts,
);
await Promise.all(
productDtos.map((dto, index) =>
this.subspaceProductItemService.createItemFromDtos(
subspaceProducts[index],
dto.items,
queryRunner,
space,
),
),
);
} catch (error) {
throw new HttpException(
`Failed to create subspace products from DTOs. Error: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async getProduct(productId: string): Promise<ProductEntity> {
const product = await this.productService.findOne(productId);
return product.data;
}
async validateProductCount(dto: ProductAssignmentDto) {
if (dto.count !== dto.items.length) {
throw new HttpException(
'Producy item and count doesnot match',
HttpStatus.BAD_REQUEST,
);
}
}
}

View File

@ -1,6 +1,13 @@
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { AddSubspaceDto, GetSpaceParam, GetSubSpaceParam } from '../../dtos';
import {
AddSubspaceDto,
CreateTagDto,
DeleteSubspaceDto,
GetSpaceParam,
GetSubSpaceParam,
ModifySubspaceDto,
} from '../../dtos';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import {
TypeORMCustomModel,
@ -8,25 +15,26 @@ import {
} from '@app/common/models/typeOrmCustom.model';
import { PageResponse } from '@app/common/dto/pagination.response.dto';
import { SubspaceDto } from '@app/common/modules/space/dtos';
import { QueryRunner } from 'typeorm';
import { In, QueryRunner } from 'typeorm';
import {
SpaceEntity,
SubspaceEntity,
} from '@app/common/modules/space/entities';
import {
SpaceModelEntity,
SubspaceModelEntity,
} from '@app/common/modules/space-model';
import { SubspaceModelEntity } from '@app/common/modules/space-model';
import { ValidationService } from '../space-validation.service';
import { SubspaceRepository } from '@app/common/modules/space/repositories/subspace.repository';
import { SubspaceProductService } from './subspace-product.service';
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';
@Injectable()
export class SubSpaceService {
constructor(
private readonly subspaceRepository: SubspaceRepository,
private readonly validationService: ValidationService,
private readonly productService: SubspaceProductService,
private readonly tagService: TagService,
public readonly deviceService: SubspaceDeviceService,
) {}
async createSubspaces(
@ -38,10 +46,15 @@ export class SubSpaceService {
queryRunner: QueryRunner,
): Promise<SubspaceEntity[]> {
try {
const subspaceNames = subspaceData.map((data) => data.subspaceName);
await this.checkExistingNamesInSpace(
subspaceNames,
subspaceData[0].space,
);
const subspaces = subspaceData.map((data) =>
queryRunner.manager.create(this.subspaceRepository.target, data),
);
return await queryRunner.manager.save(subspaces);
} catch (error) {
throw new HttpException(
@ -52,15 +65,13 @@ export class SubSpaceService {
}
async createSubSpaceFromModel(
spaceModel: SpaceModelEntity,
subspaceModels: SubspaceModelEntity[],
space: SpaceEntity,
queryRunner: QueryRunner,
): Promise<void> {
const subSpaceModels = spaceModel.subspaceModels;
if (!subspaceModels?.length) return;
if (!subSpaceModels?.length) return;
const subspaceData = subSpaceModels.map((subSpaceModel) => ({
const subspaceData = subspaceModels.map((subSpaceModel) => ({
subspaceName: subSpaceModel.subspaceName,
space,
subSpaceModel,
@ -69,13 +80,14 @@ export class SubSpaceService {
const subspaces = await this.createSubspaces(subspaceData, queryRunner);
await Promise.all(
subSpaceModels.map((model, index) => {
this.productService.createFromModel(
model,
subspaces[index],
subspaceModels.map((model, index) =>
this.tagService.createTagsFromModel(
queryRunner,
);
}),
model.tags || [],
null,
subspaces[index],
),
),
);
}
@ -83,8 +95,14 @@ export class SubSpaceService {
addSubspaceDtos: AddSubspaceDto[],
space: SpaceEntity,
queryRunner: QueryRunner,
otherTags?: CreateTagDto[],
): Promise<SubspaceEntity[]> {
try {
await this.validateName(
addSubspaceDtos.map((dto) => dto.subspaceName),
space,
);
const subspaceData = addSubspaceDtos.map((dto) => ({
subspaceName: dto.subspaceName,
space,
@ -93,20 +111,31 @@ export class SubSpaceService {
const subspaces = await this.createSubspaces(subspaceData, queryRunner);
await Promise.all(
addSubspaceDtos.map((dto, index) =>
this.productService.createFromDto(
dto.products,
subspaces[index],
queryRunner,
space,
),
),
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,
queryRunner,
null,
subspace,
[...(otherTags || []), ...otherDtoTags],
);
}
}),
);
return subspaces;
} catch (error) {
throw new Error(
`Transaction failed: Unable to create subspaces and products. ${error.message}`,
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
'Failed to save subspaces due to an unexpected error.',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@ -123,6 +152,10 @@ export class SubSpaceService {
);
try {
await this.checkExistingNamesInSpace(
[addSubspaceDto.subspaceName],
space,
);
const newSubspace = this.subspaceRepository.create({
...addSubspaceDto,
space,
@ -167,43 +200,6 @@ export class SubSpaceService {
}
}
async findOne(params: GetSubSpaceParam): Promise<BaseResponseDto> {
const { communityUuid, subSpaceUuid, spaceUuid, projectUuid } = params;
await this.validationService.validateSpaceWithinCommunityAndProject(
communityUuid,
projectUuid,
spaceUuid,
);
try {
const subSpace = await this.subspaceRepository.findOne({
where: {
uuid: subSpaceUuid,
},
});
// If space is not found, throw a NotFoundException
if (!subSpace) {
throw new HttpException(
`Sub Space with UUID ${subSpaceUuid} not found`,
HttpStatus.NOT_FOUND,
);
}
return new SuccessResponseDto({
message: `Subspace with ID ${subSpaceUuid} successfully fetched`,
data: subSpace,
});
} catch (error) {
if (error instanceof HttpException) {
throw error; // If it's an HttpException, rethrow it
} else {
throw new HttpException(
'An error occurred while deleting the subspace',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
async updateSubSpace(
params: GetSubSpaceParam,
updateSubSpaceDto: AddSubspaceDto,
@ -279,4 +275,266 @@ export class SubSpaceService {
);
}
}
async deleteSubspaces(
deleteDtos: DeleteSubspaceDto[],
queryRunner: QueryRunner,
) {
const deleteResults: { uuid: string }[] = [];
for (const dto of deleteDtos) {
const subspace = await this.findOne(dto.subspaceUuid);
await queryRunner.manager.update(
this.subspaceRepository.target,
{ uuid: dto.subspaceUuid },
{ disabled: true },
);
if (subspace.tags?.length) {
const modifyTagDtos = subspace.tags.map((tag) => ({
uuid: tag.uuid,
action: ModifyAction.DELETE,
}));
await this.tagService.modifyTags(
modifyTagDtos,
queryRunner,
null,
subspace,
);
}
if (subspace.devices)
await this.deviceService.deleteSubspaceDevices(
subspace.devices,
queryRunner,
);
deleteResults.push({ uuid: dto.subspaceUuid });
}
return deleteResults;
}
async modifySubSpace(
subspaceDtos: ModifySubspaceDto[],
queryRunner: QueryRunner,
space?: SpaceEntity,
) {
for (const subspace of subspaceDtos) {
switch (subspace.action) {
case ModifyAction.ADD:
await this.handleAddAction(subspace, space, queryRunner);
break;
case ModifyAction.UPDATE:
await this.handleUpdateAction(subspace, queryRunner);
break;
case ModifyAction.DELETE:
await this.handleDeleteAction(subspace, queryRunner);
break;
default:
throw new HttpException(
`Invalid action "${subspace.action}".`,
HttpStatus.BAD_REQUEST,
);
}
}
}
async unlinkModels(
subspaces: SubspaceEntity[],
queryRunner: QueryRunner,
): Promise<void> {
if (!subspaces || subspaces.length === 0) {
return;
}
try {
const allTags = subspaces.flatMap((subSpace) => {
subSpace.subSpaceModel = null;
return subSpace.tags || [];
});
await this.tagService.unlinkModels(allTags, queryRunner);
await queryRunner.manager.save(subspaces);
} catch (error) {
if (error instanceof HttpException) throw error;
throw new HttpException(
`Failed to unlink subspace models: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async getOne(params: GetSubSpaceParam): Promise<BaseResponseDto> {
await this.validationService.validateSpaceWithinCommunityAndProject(
params.communityUuid,
params.projectUuid,
params.spaceUuid,
);
const subspace = await this.findOne(params.subSpaceUuid);
return new SuccessResponseDto({
message: `Successfully retrieved subspace`,
data: subspace,
});
}
private async handleAddAction(
subspace: ModifySubspaceDto,
space: SpaceEntity,
queryRunner: QueryRunner,
): Promise<SubspaceEntity> {
const createTagDtos: CreateTagDto[] =
subspace.tags?.map((tag) => ({
tag: tag.tag as string,
productUuid: tag.productUuid as string,
})) || [];
const subSpace = await this.createSubspacesFromDto(
[{ subspaceName: subspace.subspaceName, tags: createTagDtos }],
space,
queryRunner,
);
return subSpace[0];
}
private async handleUpdateAction(
modifyDto: ModifySubspaceDto,
queryRunner: QueryRunner,
): Promise<void> {
const subspace = await this.findOne(modifyDto.uuid);
await this.update(
queryRunner,
subspace,
modifyDto.subspaceName,
modifyDto.tags,
);
}
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,
);
}
}
async handleDeleteAction(
modifyDto: ModifySubspaceDto,
queryRunner: QueryRunner,
): Promise<void> {
const subspace = await this.findOne(modifyDto.uuid);
await queryRunner.manager.update(
this.subspaceRepository.target,
{ uuid: subspace.uuid },
{ disabled: true },
);
if (subspace.tags?.length) {
const modifyTagDtos = subspace.tags.map((tag) => ({
uuid: tag.uuid,
action: ModifyAction.DELETE,
}));
await this.tagService.modifyTags(
modifyTagDtos,
queryRunner,
null,
subspace,
);
}
if (subspace.devices.length > 0) {
await this.deviceService.deleteSubspaceDevices(
subspace.devices,
queryRunner,
);
}
}
private async findOne(subspaceUuid: string): Promise<SubspaceEntity> {
const subspace = await this.subspaceRepository.findOne({
where: { uuid: subspaceUuid },
relations: ['tags', 'space', 'devices', 'tags.product', 'tags.device'],
});
if (!subspace) {
throw new HttpException(
`SubspaceModel with UUID ${subspaceUuid} not found.`,
HttpStatus.NOT_FOUND,
);
}
return subspace;
}
async updateSubspaceName(
queryRunner: QueryRunner,
subSpace: SubspaceEntity,
subspaceName?: string,
): Promise<void> {
if (subspaceName) {
subSpace.subspaceName = subspaceName;
await queryRunner.manager.save(subSpace);
}
}
private async checkForDuplicateNames(names: string[]): Promise<void> {
const seenNames = new Set<string>();
const duplicateNames = new Set<string>();
for (const name of names) {
if (!seenNames.add(name)) {
duplicateNames.add(name);
}
}
if (duplicateNames.size > 0) {
throw new HttpException(
`Duplicate subspace names found: ${[...duplicateNames].join(', ')}`,
HttpStatus.CONFLICT,
);
}
}
private async checkExistingNamesInSpace(
names: string[],
space: SpaceEntity,
): Promise<void> {
const existingNames = await this.subspaceRepository.find({
select: ['subspaceName'],
where: {
subspaceName: In(names),
space: {
uuid: space.uuid,
},
disabled: false,
},
});
if (existingNames.length > 0) {
const existingNamesList = existingNames
.map((e) => e.subspaceName)
.join(', ');
throw new HttpException(
`Subspace names already exist in the space: ${existingNamesList}`,
HttpStatus.BAD_REQUEST,
);
}
}
private async validateName(
names: string[],
space: SpaceEntity,
): Promise<void> {
await this.checkForDuplicateNames(names);
await this.checkExistingNamesInSpace(names, space);
}
}

View File

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

View File

@ -0,0 +1,350 @@
import { ModifyAction } from '@app/common/constants/modify-action.enum';
import {
SpaceEntity,
SubspaceEntity,
TagEntity,
TagRepository,
} from '@app/common/modules/space';
import { TagModel } from '@app/common/modules/space-model';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { ProductService } from 'src/product/services';
import { CreateTagDto } from 'src/space/dtos';
import { ModifyTagDto } from 'src/space/dtos/tag/modify-tag.dto';
import { QueryRunner } from 'typeorm';
@Injectable()
export class TagService {
constructor(
private readonly tagRepository: TagRepository,
private readonly productService: ProductService,
) {}
async createTags(
tags: CreateTagDto[],
queryRunner: QueryRunner,
space?: SpaceEntity,
subspace?: SubspaceEntity,
additionalTags?: CreateTagDto[],
): Promise<TagEntity[]> {
this.validateTagsInput(tags);
const combinedTags = this.combineTags(tags, additionalTags);
this.ensureNoDuplicateTags(combinedTags);
try {
const tagEntities = await Promise.all(
tags.map(async (tagDto) =>
this.prepareTagEntity(tagDto, queryRunner, space, subspace),
),
);
return await queryRunner.manager.save(tagEntities);
} catch (error) {
throw this.handleUnexpectedError('Failed to save tags', error);
}
}
async createTagsFromModel(
queryRunner: QueryRunner,
tagModels: TagModel[],
space?: SpaceEntity,
subspace?: SubspaceEntity,
): Promise<void> {
if (!tagModels?.length) return;
const tags = tagModels.map((model) =>
queryRunner.manager.create(this.tagRepository.target, {
tag: model.tag,
space: space || undefined,
subspace: subspace || undefined,
product: model.product,
}),
);
await queryRunner.manager.save(tags);
}
async updateTag(
tag: ModifyTagDto,
queryRunner: QueryRunner,
space?: SpaceEntity,
subspace?: SubspaceEntity,
): Promise<TagEntity> {
try {
const existingTag = await this.getTagByUuid(tag.uuid);
const contextSpace = space ?? subspace?.space;
if (contextSpace) {
await this.checkTagReuse(
tag.tag,
existingTag.product.uuid,
contextSpace,
);
}
return await queryRunner.manager.save(
Object.assign(existingTag, { tag: tag.tag }),
);
} catch (error) {
throw this.handleUnexpectedError('Failed to update tags', error);
}
}
async updateTagsFromModel(
model: TagModel,
queryRunner: QueryRunner,
): Promise<void> {
try {
const tags = await this.tagRepository.find({
where: {
model: {
uuid: model.uuid,
},
},
});
if (!tags.length) return;
await queryRunner.manager.update(
this.tagRepository.target,
{ model: { uuid: model.uuid } },
{ tag: model.tag },
);
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
`Failed to update tags for model with UUID: ${model.uuid}. Reason: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async deleteTags(tagUuids: string[], queryRunner: QueryRunner) {
if (!tagUuids?.length) return;
try {
await Promise.all(
tagUuids.map((id) =>
queryRunner.manager.update(
this.tagRepository.target,
{ uuid: id },
{ disabled: true, device: null },
),
),
);
return { message: 'Tags deleted successfully', tagUuids };
} catch (error) {
throw this.handleUnexpectedError('Failed to update tags', error);
}
}
async deleteTagFromModel(modelUuid: string, queryRunner: QueryRunner) {
try {
const tags = await this.tagRepository.find({
where: {
model: {
uuid: modelUuid,
},
},
});
if (!tags.length) return;
await queryRunner.manager.update(
this.tagRepository.target,
{ model: { uuid: modelUuid } },
{ disabled: true, device: null },
);
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
`Failed to update tags for model with UUID: ${modelUuid}. Reason: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async modifyTags(
tags: ModifyTagDto[],
queryRunner: QueryRunner,
space?: SpaceEntity,
subspace?: SubspaceEntity,
): Promise<void> {
if (!tags?.length) return;
try {
await Promise.all(
tags.map(async (tag) => {
switch (tag.action) {
case ModifyAction.ADD:
await this.createTags(
[{ tag: tag.tag, productUuid: tag.productUuid }],
queryRunner,
space,
subspace,
);
break;
case ModifyAction.UPDATE:
await this.updateTag(tag, queryRunner, space, subspace);
break;
case ModifyAction.DELETE:
await this.deleteTags([tag.uuid], queryRunner);
break;
default:
throw new HttpException(
`Invalid action "${tag.action}" provided.`,
HttpStatus.BAD_REQUEST,
);
}
}),
);
} catch (error) {
throw this.handleUnexpectedError('Failed to modify tags', error);
}
}
async unlinkModels(tags: TagEntity[], queryRunner: QueryRunner) {
if (!tags?.length) return;
try {
tags.forEach((tag) => {
tag.model = null;
});
await queryRunner.manager.save(tags);
} catch (error) {
throw this.handleUnexpectedError('Failed to unlink tag models', error);
}
}
private findDuplicateTags(tags: CreateTagDto[]): 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;
}
private async checkTagReuse(
tag: string,
productUuid: string,
space: SpaceEntity,
): Promise<void> {
const { uuid: spaceUuid } = space;
const tagExists = await this.tagRepository.exists({
where: [
{
tag,
space: { uuid: spaceUuid },
product: { uuid: productUuid },
disabled: false,
},
{
tag,
subspace: { space: { uuid: spaceUuid } },
product: { uuid: productUuid },
disabled: false,
},
],
});
if (tagExists) {
throw new HttpException(`Tag can't be reused`, HttpStatus.CONFLICT);
}
}
private async prepareTagEntity(
tagDto: CreateTagDto,
queryRunner: QueryRunner,
space?: SpaceEntity,
subspace?: SubspaceEntity,
): Promise<TagEntity> {
const product = await this.productService.findOne(tagDto.productUuid);
if (!product) {
throw new HttpException(
`Product with UUID ${tagDto.productUuid} not found.`,
HttpStatus.NOT_FOUND,
);
}
await this.checkTagReuse(
tagDto.tag,
tagDto.productUuid,
space ?? subspace.space,
);
return queryRunner.manager.create(TagEntity, {
tag: tagDto.tag,
product: product.data,
space,
subspace,
});
}
private async getTagByUuid(uuid: string): Promise<TagEntity> {
const tag = await this.tagRepository.findOne({
where: { uuid },
relations: ['product'],
});
if (!tag) {
throw new HttpException(
`Tag with ID ${uuid} not found.`,
HttpStatus.NOT_FOUND,
);
}
return tag;
}
private handleUnexpectedError(
message: string,
error: unknown,
): HttpException {
if (error instanceof HttpException) throw error;
return new HttpException(
`${message}: ${(error as Error)?.message || 'Unknown error'}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
private combineTags(
primaryTags: CreateTagDto[],
additionalTags?: CreateTagDto[],
): CreateTagDto[] {
return additionalTags ? [...primaryTags, ...additionalTags] : primaryTags;
}
private ensureNoDuplicateTags(tags: CreateTagDto[]): void {
const duplicates = this.findDuplicateTags(tags);
if (duplicates.length > 0) {
throw new HttpException(
`Duplicate tags found: ${duplicates.join(', ')}`,
HttpStatus.BAD_REQUEST,
);
}
}
private validateTagsInput(tags: CreateTagDto[]): void {
if (!tags?.length) {
return;
}
}
}

View File

@ -12,27 +12,26 @@ import {
import {
SpaceDeviceService,
SpaceLinkService,
SpaceProductItemService,
SpaceProductService,
SpaceSceneService,
SpaceService,
SpaceUserService,
SubspaceDeviceService,
SubspaceProductItemService,
SubSpaceService,
} from './services';
import {
SpaceProductRepository,
SpaceRepository,
SpaceLinkRepository,
SpaceProductItemRepository,
TagRepository,
} from '@app/common/modules/space/repositories';
import { CommunityRepository } from '@app/common/modules/community/repositories';
import {
UserRepository,
UserSpaceRepository,
} from '@app/common/modules/user/repositories';
import { DeviceRepository } from '@app/common/modules/device/repositories';
import {
DeviceRepository,
DeviceUserPermissionRepository,
} from '@app/common/modules/device/repositories';
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
import { ProductRepository } from '@app/common/modules/product/repositories';
import { SceneService } from '../scene/services';
@ -45,18 +44,30 @@ import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status
import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories';
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
import { ProjectRepository } from '@app/common/modules/project/repositiories';
import { SpaceModelRepository } from '@app/common/modules/space-model';
import {
SpaceModelRepository,
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 { TagService } from './services/tag';
import {
SubspaceProductItemRepository,
SubspaceProductRepository,
SubspaceRepository,
} from '@app/common/modules/space/repositories/subspace.repository';
import { SubspaceProductService } from './services';
SpaceModelService,
SubSpaceModelService,
TagModelService,
} from 'src/space-model/services';
import { UserSpaceService } from 'src/users/services';
import { UserDevicePermissionService } from 'src/user-device-permission/services';
import { PermissionTypeRepository } from '@app/common/modules/permission/repositories';
import { CqrsModule } from '@nestjs/cqrs';
import { DisableSpaceHandler } from './handlers';
export const CommandHandlers = [DisableSpaceHandler];
@Module({
imports: [ConfigModule, SpaceRepositoryModule, CommunityModule],
imports: [ConfigModule, SpaceRepositoryModule, CommunityModule, CqrsModule],
controllers: [
SpaceController,
SpaceUserController,
@ -67,17 +78,20 @@ import { SubspaceProductService } from './services';
],
providers: [
ValidationService,
TagModelRepository,
TagRepository,
SpaceService,
TuyaService,
TagService,
ProductRepository,
SubSpaceService,
SpaceDeviceService,
SpaceLinkService,
SubspaceDeviceService,
SpaceRepository,
SubspaceRepository,
DeviceRepository,
CommunityRepository,
SubspaceRepository,
SpaceLinkRepository,
UserSpaceRepository,
UserRepository,
@ -88,19 +102,19 @@ import { SubspaceProductService } from './services';
SceneRepository,
DeviceService,
DeviceStatusFirebaseService,
SubspaceProductItemRepository,
DeviceStatusLogRepository,
SceneDeviceRepository,
SpaceProductService,
SpaceProductRepository,
SpaceModelService,
SubSpaceModelService,
TagModelService,
ProjectRepository,
SpaceModelRepository,
SubspaceRepository,
SpaceProductItemService,
SpaceProductItemRepository,
SubspaceProductService,
SubspaceProductItemService,
SubspaceProductRepository,
SubspaceModelRepository,
UserSpaceService,
UserDevicePermissionService,
DeviceUserPermissionRepository,
PermissionTypeRepository,
...CommandHandlers,
],
exports: [SpaceService],
})

View File

@ -150,4 +150,20 @@ export class UserSpaceService {
await Promise.all(permissionPromises);
}
async deleteUserSpace(spaceUuid: string) {
try {
await this.userSpaceRepository
.createQueryBuilder()
.delete()
.where('spaceUuid = :spaceUuid', { spaceUuid })
.execute();
} catch (error) {
console.error(`Error deleting user-space associations: ${error.message}`);
throw new HttpException(
`Failed to delete user-space associations: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}