diff --git a/libs/common/src/constants/controller-route.ts b/libs/common/src/constants/controller-route.ts index bd37317..10e20d9 100644 --- a/libs/common/src/constants/controller-route.ts +++ b/libs/common/src/constants/controller-route.ts @@ -266,6 +266,16 @@ export class ControllerRoute { }; }; + static SPACE_MODEL = class { + public static readonly ROUTE = '/projects/:projectUuid/space-models'; + static ACTIONS = class { + public static readonly CREATE_SPACE_MODEL_SUMMARY = + 'Create a New Space Model'; + public static readonly CREATE_SPACE_MODEL_DESCRIPTION = + 'This endpoint allows you to create a new space model within a specified project. A space model defines the structure of spaces, including subspaces, products, and product items, and is uniquely identifiable within the project.'; + }; + }; + static PRODUCT = class { public static readonly ROUTE = 'products'; static ACTIONS = class { diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index 34e4bb6..44fd854 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -28,6 +28,12 @@ import { SceneEntity, SceneIconEntity } from '../modules/scene/entities'; import { SceneDeviceEntity } from '../modules/scene-device/entities'; import { SpaceProductEntity } from '../modules/space/entities/space-product.entity'; import { ProjectEntity } from '../modules/project/entities'; +import { + SpaceModelEntity, + SpaceProductItemModelEntity, + SpaceProductModelEntity, + SubspaceModelEntity, +} from '../modules/space-model/entities'; @Module({ imports: [ TypeOrmModule.forRootAsync({ @@ -68,6 +74,10 @@ import { ProjectEntity } from '../modules/project/entities'; SceneEntity, SceneIconEntity, SceneDeviceEntity, + SpaceModelEntity, + SpaceProductModelEntity, + SpaceProductItemModelEntity, + SubspaceModelEntity, ], namingStrategy: new SnakeNamingStrategy(), synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))), diff --git a/libs/common/src/modules/product/entities/product.entity.ts b/libs/common/src/modules/product/entities/product.entity.ts index 2c731a0..7b25470 100644 --- a/libs/common/src/modules/product/entities/product.entity.ts +++ b/libs/common/src/modules/product/entities/product.entity.ts @@ -3,6 +3,7 @@ import { ProductDto } from '../dtos'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { DeviceEntity } from '../../device/entities'; import { SpaceProductEntity } from '../../space/entities/space-product.entity'; +import { SpaceProductModelEntity } from '../../space-model/entities'; @Entity({ name: 'product' }) export class ProductEntity extends AbstractEntity { @@ -30,6 +31,12 @@ export class ProductEntity extends AbstractEntity { @OneToMany(() => SpaceProductEntity, (spaceProduct) => spaceProduct.product) spaceProducts: SpaceProductEntity[]; + @OneToMany( + () => SpaceProductModelEntity, + (spaceProductModel) => spaceProductModel.product, + ) + spaceProductModels: SpaceProductModelEntity[]; + @OneToMany( () => DeviceEntity, (devicesProductEntity) => devicesProductEntity.productDevice, diff --git a/libs/common/src/modules/project/entities/project.entity.ts b/libs/common/src/modules/project/entities/project.entity.ts index 04e3f11..f7213a2 100644 --- a/libs/common/src/modules/project/entities/project.entity.ts +++ b/libs/common/src/modules/project/entities/project.entity.ts @@ -2,6 +2,7 @@ import { Entity, Column, Unique, OneToMany } from 'typeorm'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { ProjectDto } from '../dtos'; import { CommunityEntity } from '../../community/entities'; +import { SpaceModelEntity } from '../../space-model'; @Entity({ name: 'project' }) @Unique(['name']) @@ -20,7 +21,10 @@ export class ProjectEntity extends AbstractEntity { @Column({ length: 255, nullable: true }) description: string; - + + @OneToMany(() => SpaceModelEntity, (spaceModel) => spaceModel.project) + public spaceModels: SpaceModelEntity[]; + @OneToMany(() => CommunityEntity, (community) => community.project) communities: CommunityEntity[]; diff --git a/libs/common/src/modules/space-model/dtos/index.ts b/libs/common/src/modules/space-model/dtos/index.ts new file mode 100644 index 0000000..f775b4a --- /dev/null +++ b/libs/common/src/modules/space-model/dtos/index.ts @@ -0,0 +1,4 @@ +export * from './subspace-model.dto'; +export * from './space-model.dto'; +export * from './space-product-item.dto'; +export * from './space-product-model.dto'; diff --git a/libs/common/src/modules/space-model/dtos/space-model.dto.ts b/libs/common/src/modules/space-model/dtos/space-model.dto.ts new file mode 100644 index 0000000..f4d7cbf --- /dev/null +++ b/libs/common/src/modules/space-model/dtos/space-model.dto.ts @@ -0,0 +1,15 @@ +import { IsString, IsNotEmpty } from 'class-validator'; + +export class SpaceModelDto { + @IsString() + @IsNotEmpty() + public uuid: string; + + @IsString() + @IsNotEmpty() + public spaceModelName: string; + + @IsString() + @IsNotEmpty() + projectUuid: string; +} diff --git a/libs/common/src/modules/space-model/dtos/space-product-item.dto.ts b/libs/common/src/modules/space-model/dtos/space-product-item.dto.ts new file mode 100644 index 0000000..fa68825 --- /dev/null +++ b/libs/common/src/modules/space-model/dtos/space-product-item.dto.ts @@ -0,0 +1,15 @@ +import { IsString, IsNotEmpty } from 'class-validator'; + +export class SpaceProductItemDto { + @IsString() + @IsNotEmpty() + uuid: string; + + @IsString() + @IsNotEmpty() + tag: string; + + @IsString() + @IsNotEmpty() + spaceProductModelUuid: string; +} diff --git a/libs/common/src/modules/space-model/dtos/space-product-model.dto.ts b/libs/common/src/modules/space-model/dtos/space-product-model.dto.ts new file mode 100644 index 0000000..eae8088 --- /dev/null +++ b/libs/common/src/modules/space-model/dtos/space-product-model.dto.ts @@ -0,0 +1,23 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsNumber, IsString } from 'class-validator'; +import { SpaceProductItemDto } from './space-product-item.dto'; + +export class SpaceProductModelDto { + @IsString() + @IsNotEmpty() + uuid: string; + + @IsNumber() + @IsNotEmpty() + productCount: number; + + @IsString() + @IsNotEmpty() + productUuid: string; + + @ApiProperty({ + description: 'List of individual items with specific names for the product', + type: [SpaceProductItemDto], + }) + items: SpaceProductItemDto[]; +} diff --git a/libs/common/src/modules/space-model/dtos/subspace-model.dto.ts b/libs/common/src/modules/space-model/dtos/subspace-model.dto.ts new file mode 100644 index 0000000..8c9a64c --- /dev/null +++ b/libs/common/src/modules/space-model/dtos/subspace-model.dto.ts @@ -0,0 +1,15 @@ +import { IsString, IsNotEmpty } from 'class-validator'; + +export class SubSpaceModelDto { + @IsString() + @IsNotEmpty() + public uuid: string; + + @IsString() + @IsNotEmpty() + public subSpaceModelName: string; + + @IsString() + @IsNotEmpty() + spaceModelUuid: string; +} diff --git a/libs/common/src/modules/space-model/entities/index.ts b/libs/common/src/modules/space-model/entities/index.ts new file mode 100644 index 0000000..3bf3f69 --- /dev/null +++ b/libs/common/src/modules/space-model/entities/index.ts @@ -0,0 +1,4 @@ +export * from './space-model.entity'; +export * from './space-product-item.entity'; +export * from './space-product-model.entity'; +export * from './subspace-model.entity'; \ No newline at end of file diff --git a/libs/common/src/modules/space-model/entities/space-model.entity.ts b/libs/common/src/modules/space-model/entities/space-model.entity.ts new file mode 100644 index 0000000..9937a01 --- /dev/null +++ b/libs/common/src/modules/space-model/entities/space-model.entity.ts @@ -0,0 +1,56 @@ +import { + Entity, + Column, + OneToMany, + ManyToOne, + JoinColumn, + Unique, +} from 'typeorm'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { SpaceModelDto } from '../dtos'; +import { SubspaceModelEntity } from './subspace-model.entity'; +import { SpaceProductModelEntity } from './space-product-model.entity'; +import { ProjectEntity } from '../../project/entities'; + +@Entity({ name: 'space-model' }) +@Unique(['modelName', 'project']) +export class SpaceModelEntity extends AbstractEntity { + @Column({ + type: 'uuid', + default: () => 'gen_random_uuid()', + nullable: false, + }) + public uuid: string; + + @Column({ + nullable: false, + }) + public modelName: string; + + @ManyToOne(() => ProjectEntity, (project) => project.spaceModels, { + nullable: false, + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'project_uuid' }) + public project: ProjectEntity; + + @OneToMany( + () => SubspaceModelEntity, + (subspaceModel) => subspaceModel.spaceModel, + { + cascade: true, + nullable: true, + }, + ) + public subspaceModels: SubspaceModelEntity[]; + + @OneToMany( + () => SpaceProductModelEntity, + (productModel) => productModel.spaceModel, + { + cascade: true, + nullable: true, + }, + ) + public spaceProductModels: SpaceProductModelEntity[]; +} diff --git a/libs/common/src/modules/space-model/entities/space-product-item.entity.ts b/libs/common/src/modules/space-model/entities/space-product-item.entity.ts new file mode 100644 index 0000000..3695831 --- /dev/null +++ b/libs/common/src/modules/space-model/entities/space-product-item.entity.ts @@ -0,0 +1,32 @@ +import { Entity, Column, ManyToOne, Unique } from 'typeorm'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { SpaceProductItemDto } from '../dtos'; +import { SpaceProductModelEntity } from './space-product-model.entity'; +import { SpaceModelEntity } from './space-model.entity'; + +@Entity({ name: 'space-product-item-model' }) +@Unique(['tag', 'spaceProductModel', 'spaceModel']) +export class SpaceProductItemModelEntity extends AbstractEntity { + @Column({ + nullable: false, + }) + public tag: string; + + @ManyToOne( + () => SpaceProductModelEntity, + (spaceProductModel) => spaceProductModel.items, + { + nullable: false, + }, + ) + public spaceProductModel: SpaceProductModelEntity; + + @ManyToOne( + () => SpaceModelEntity, + (spaceModel) => spaceModel.spaceProductModels, + { + nullable: false, + }, + ) + public spaceModel: SpaceModelEntity; +} diff --git a/libs/common/src/modules/space-model/entities/space-product-model.entity.ts b/libs/common/src/modules/space-model/entities/space-product-model.entity.ts new file mode 100644 index 0000000..d843d07 --- /dev/null +++ b/libs/common/src/modules/space-model/entities/space-product-model.entity.ts @@ -0,0 +1,40 @@ +import { Entity, Column, ManyToOne, OneToMany } from 'typeorm'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { ProductEntity } from '../../product/entities'; +import { SpaceModelEntity } from './space-model.entity'; +import { SpaceProductItemModelEntity } from './space-product-item.entity'; +import { SpaceProductModelDto } from '../dtos'; + +@Entity({ name: 'space-product-model' }) +export class SpaceProductModelEntity extends AbstractEntity { + @Column({ + nullable: false, + type: 'int', + }) + productCount: number; + + @ManyToOne( + () => SpaceModelEntity, + (spaceModel) => spaceModel.spaceProductModels, + { + nullable: false, + onDelete: 'CASCADE', + }, + ) + public spaceModel: SpaceModelEntity; + + @ManyToOne(() => ProductEntity, (product) => product.spaceProductModels, { + nullable: false, + onDelete: 'CASCADE', + }) + public product: ProductEntity; + + @OneToMany( + () => SpaceProductItemModelEntity, + (item) => item.spaceProductModel, + { + cascade: true, + }, + ) + public items: SpaceProductItemModelEntity[]; +} diff --git a/libs/common/src/modules/space-model/entities/subspace-model.entity.ts b/libs/common/src/modules/space-model/entities/subspace-model.entity.ts new file mode 100644 index 0000000..776f831 --- /dev/null +++ b/libs/common/src/modules/space-model/entities/subspace-model.entity.ts @@ -0,0 +1,30 @@ +import { Column, Entity, ManyToOne, Unique } from 'typeorm'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { SpaceModelEntity } from './space-model.entity'; +import { SubSpaceModelDto } from '../dtos'; + +@Entity({ name: 'subspace-model' }) +@Unique(['subspaceName', 'spaceModel']) +export class SubspaceModelEntity extends AbstractEntity { + @Column({ + type: 'uuid', + default: () => 'gen_random_uuid()', + nullable: false, + }) + public uuid: string; + + @Column({ + nullable: false, + }) + public subspaceName: string; + + @ManyToOne( + () => SpaceModelEntity, + (spaceModel) => spaceModel.subspaceModels, + { + nullable: false, + onDelete: 'CASCADE', + }, + ) + public spaceModel: SpaceModelEntity; +} diff --git a/libs/common/src/modules/space-model/index.ts b/libs/common/src/modules/space-model/index.ts new file mode 100644 index 0000000..9d32775 --- /dev/null +++ b/libs/common/src/modules/space-model/index.ts @@ -0,0 +1,3 @@ +export * from './space-model.repository.module'; +export * from './entities'; +export * from './repositories'; diff --git a/libs/common/src/modules/space-model/repositories/index.ts b/libs/common/src/modules/space-model/repositories/index.ts new file mode 100644 index 0000000..d8fcff4 --- /dev/null +++ b/libs/common/src/modules/space-model/repositories/index.ts @@ -0,0 +1 @@ +export * from './space-model.repository'; diff --git a/libs/common/src/modules/space-model/repositories/space-model.repository.ts b/libs/common/src/modules/space-model/repositories/space-model.repository.ts new file mode 100644 index 0000000..0e37479 --- /dev/null +++ b/libs/common/src/modules/space-model/repositories/space-model.repository.ts @@ -0,0 +1,34 @@ +import { DataSource, Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { + SpaceModelEntity, + SpaceProductItemModelEntity, + SpaceProductModelEntity, + SubspaceModelEntity, +} from '../entities'; + +@Injectable() +export class SpaceModelRepository extends Repository { + constructor(private dataSource: DataSource) { + super(SpaceModelEntity, dataSource.createEntityManager()); + } +} +@Injectable() +export class SubspaceModelRepository extends Repository { + constructor(private dataSource: DataSource) { + super(SubspaceModelEntity, dataSource.createEntityManager()); + } +} + +@Injectable() +export class SpaceProductModelRepository extends Repository { + constructor(private dataSource: DataSource) { + super(SpaceProductModelEntity, dataSource.createEntityManager()); + } +} +@Injectable() +export class SpaceProductItemModelRepository extends Repository { + constructor(private dataSource: DataSource) { + super(SpaceProductItemModelEntity, dataSource.createEntityManager()); + } +} diff --git a/libs/common/src/modules/space-model/space-model.repository.module.ts b/libs/common/src/modules/space-model/space-model.repository.module.ts new file mode 100644 index 0000000..573bba0 --- /dev/null +++ b/libs/common/src/modules/space-model/space-model.repository.module.ts @@ -0,0 +1,23 @@ +import { TypeOrmModule } from '@nestjs/typeorm'; +import { + SpaceModelEntity, + SpaceProductItemModelEntity, + SpaceProductModelEntity, + SubspaceModelEntity, +} from './entities'; +import { Module } from '@nestjs/common'; + +@Module({ + providers: [], + exports: [], + controllers: [], + imports: [ + TypeOrmModule.forFeature([ + SpaceModelEntity, + SubspaceModelEntity, + SpaceProductModelEntity, + SpaceProductItemModelEntity, + ]), + ], +}) +export class SpaceModelRepositoryModule {} diff --git a/src/app.module.ts b/src/app.module.ts index bb5986b..ac06b84 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -23,6 +23,7 @@ import { ScheduleModule } from './schedule/schedule.module'; import { SpaceModule } from './space/space.module'; import { ProductModule } from './product'; import { ProjectModule } from './project'; +import { SpaceModelModule } from './space-model'; @Module({ imports: [ ConfigModule.forRoot({ @@ -34,7 +35,7 @@ import { ProjectModule } from './project'; CommunityModule, SpaceModule, - + SpaceModelModule, GroupModule, DeviceModule, DeviceMessagesSubscriptionModule, diff --git a/src/space-model/controllers/index.ts b/src/space-model/controllers/index.ts new file mode 100644 index 0000000..c12699e --- /dev/null +++ b/src/space-model/controllers/index.ts @@ -0,0 +1 @@ +export * from './space-model.controller'; diff --git a/src/space-model/controllers/space-model.controller.ts b/src/space-model/controllers/space-model.controller.ts new file mode 100644 index 0000000..8475b6c --- /dev/null +++ b/src/space-model/controllers/space-model.controller.ts @@ -0,0 +1,35 @@ +import { ControllerRoute } from '@app/common/constants/controller-route'; +import { Body, Controller, Param, Post, UseGuards } from '@nestjs/common'; +import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { SpaceModelService } from '../services'; +import { CreateSpaceModelDto } from '../dtos'; +import { ProjectParam } from 'src/community/dtos'; +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; + +@ApiTags('Space Model Module') +@Controller({ + version: '1', + path: ControllerRoute.SPACE_MODEL.ROUTE, +}) +export class SpaceModelController { + constructor(private readonly spaceModelService: SpaceModelService) {} + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @ApiOperation({ + summary: ControllerRoute.SPACE_MODEL.ACTIONS.CREATE_SPACE_MODEL_SUMMARY, + description: + ControllerRoute.SPACE_MODEL.ACTIONS.CREATE_SPACE_MODEL_DESCRIPTION, + }) + @Post() + async createSpaceModel( + @Body() createSpaceModelDto: CreateSpaceModelDto, + @Param() projectParam: ProjectParam, + ): Promise { + return await this.spaceModelService.createSpaceModel( + createSpaceModelDto, + projectParam, + ); + } +} diff --git a/src/space-model/dtos/create-space-model.dto.ts b/src/space-model/dtos/create-space-model.dto.ts new file mode 100644 index 0000000..9506728 --- /dev/null +++ b/src/space-model/dtos/create-space-model.dto.ts @@ -0,0 +1,33 @@ +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'; + +export class CreateSpaceModelDto { + @ApiProperty({ + description: 'Name of the space model', + example: 'Apartment Model', + }) + @IsNotEmpty() + @IsString() + modelName: string; + + @ApiProperty({ + description: 'List of subspaces included in the model', + type: [CreateSubspaceModelDto], + }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => CreateSubspaceModelDto) + subspaceModels?: CreateSubspaceModelDto[]; + + @ApiProperty({ + description: 'List of products included in the model', + type: [CreateSpaceProductModelDto], + }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => CreateSpaceProductModelDto) + spaceProductModels?: CreateSpaceProductModelDto[]; +} diff --git a/src/space-model/dtos/create-space-product-item-model.dto.ts b/src/space-model/dtos/create-space-product-item-model.dto.ts new file mode 100644 index 0000000..3825219 --- /dev/null +++ b/src/space-model/dtos/create-space-product-item-model.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class CreateSpaceProductItemModelDto { + @ApiProperty({ + description: 'Specific name for the product item', + example: 'Light 1', + }) + @IsNotEmpty() + @IsString() + tag: string; +} diff --git a/src/space-model/dtos/create-space-product-model.dto.ts b/src/space-model/dtos/create-space-product-model.dto.ts new file mode 100644 index 0000000..11d8bb5 --- /dev/null +++ b/src/space-model/dtos/create-space-product-model.dto.ts @@ -0,0 +1,39 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsNotEmpty, + IsString, + IsArray, + ValidateNested, + IsInt, + ArrayNotEmpty, +} from 'class-validator'; +import { Type } from 'class-transformer'; +import { CreateSpaceProductItemModelDto } 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() + productId: string; + + @ApiProperty({ + description: 'Number of products in the model', + example: 3, + }) + @IsNotEmpty() + @IsInt() + productCount: number; + + @ApiProperty({ + description: 'Specific names for each product item', + type: [CreateSpaceProductItemModelDto], + }) + @IsArray() + @ArrayNotEmpty() + @ValidateNested({ each: true }) + @Type(() => CreateSpaceProductItemModelDto) + items: CreateSpaceProductItemModelDto[]; +} diff --git a/src/space-model/dtos/create-subspace-model.dto.ts b/src/space-model/dtos/create-subspace-model.dto.ts new file mode 100644 index 0000000..a27ad3b --- /dev/null +++ b/src/space-model/dtos/create-subspace-model.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class CreateSubspaceModelDto { + @ApiProperty({ + description: 'Name of the subspace', + example: 'Living Room', + }) + @IsNotEmpty() + @IsString() + subspaceName: string; +} diff --git a/src/space-model/dtos/index.ts b/src/space-model/dtos/index.ts new file mode 100644 index 0000000..b4b1e07 --- /dev/null +++ b/src/space-model/dtos/index.ts @@ -0,0 +1,5 @@ +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'; diff --git a/src/space-model/dtos/project-param.dto.ts b/src/space-model/dtos/project-param.dto.ts new file mode 100644 index 0000000..e7d9e97 --- /dev/null +++ b/src/space-model/dtos/project-param.dto.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsUUID } from 'class-validator'; + +export class projectParam { + @ApiProperty({ + description: 'UUID of the Project', + example: 'd290f1ee-6c54-4b01-90e6-d701748f0851', + }) + @IsUUID() + projectUuid: string; +} diff --git a/src/space-model/index.ts b/src/space-model/index.ts new file mode 100644 index 0000000..8885fc3 --- /dev/null +++ b/src/space-model/index.ts @@ -0,0 +1 @@ +export * from './space-model.module'; diff --git a/src/space-model/services/index.ts b/src/space-model/services/index.ts new file mode 100644 index 0000000..88e2d41 --- /dev/null +++ b/src/space-model/services/index.ts @@ -0,0 +1,4 @@ +export * from './space-model.service'; +export * from './space-product-item-model.service'; +export * from './space-product-model.service'; +export * from './subspace-model.service'; diff --git a/src/space-model/services/space-model.service.ts b/src/space-model/services/space-model.service.ts new file mode 100644 index 0000000..7359e25 --- /dev/null +++ b/src/space-model/services/space-model.service.ts @@ -0,0 +1,112 @@ +import { 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 { ProjectParam } from 'src/community/dtos'; +import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; +import { SubSpaceModelService } from './subspace-model.service'; +import { SpaceProductModelService } from './space-product-model.service'; +import { DataSource } from 'typeorm'; + +@Injectable() +export class SpaceModelService { + constructor( + private readonly dataSource: DataSource, + private readonly spaceModelRepository: SpaceModelRepository, + private readonly projectRepository: ProjectRepository, + private readonly subSpaceModelService: SubSpaceModelService, + private readonly spaceProductModelService: SpaceProductModelService, + ) {} + + async createSpaceModel( + createSpaceModelDto: CreateSpaceModelDto, + params: ProjectParam, + ) { + const { modelName, subspaceModels, spaceProductModels } = + createSpaceModelDto; + const project = await this.validateProject(params.projectUuid); + + 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 spaceModel = this.spaceModelRepository.create({ + modelName, + project, + }); + const savedSpaceModel = await queryRunner.manager.save(spaceModel); + + if (subspaceModels) { + await this.subSpaceModelService.createSubSpaceModels( + subspaceModels, + savedSpaceModel, + queryRunner, + ); + } + + if (spaceProductModels) { + await this.spaceProductModelService.createSpaceProductModels( + spaceProductModels, + savedSpaceModel, + queryRunner, + ); + } + await queryRunner.commitTransaction(); + + return new SuccessResponseDto({ + message: `Successfully created new space model with uuid ${savedSpaceModel.uuid}`, + data: savedSpaceModel, + statusCode: HttpStatus.CREATED, + }); + } catch (error) { + await queryRunner.rollbackTransaction(); + + if (error instanceof HttpException) { + throw error; + } + + throw new HttpException( + error.message || `An unexpected error occurred`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } finally { + await queryRunner.release(); + } + } + + async validateProject(projectUuid: string) { + const project = await this.projectRepository.findOne({ + where: { + uuid: projectUuid, + }, + }); + + if (!project) { + throw new HttpException( + `Project with uuid ${projectUuid} not found`, + HttpStatus.NOT_FOUND, + ); + } + return project; + } + + async validateName(modelName: string, projectUuid: string): Promise { + const isModelExist = await this.spaceModelRepository.exists({ + where: { modelName, project: { uuid: projectUuid } }, + }); + return isModelExist; + } +} diff --git a/src/space-model/services/space-product-item-model.service.ts b/src/space-model/services/space-product-item-model.service.ts new file mode 100644 index 0000000..b2f856c --- /dev/null +++ b/src/space-model/services/space-product-item-model.service.ts @@ -0,0 +1,83 @@ +import { + SpaceModelEntity, + SpaceProductItemModelRepository, + SpaceProductModelEntity, +} from '@app/common/modules/space-model'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { CreateSpaceProductItemModelDto } from '../dtos'; +import { QueryRunner } from 'typeorm'; + +@Injectable() +export class SpaceProductItemModelService { + constructor( + private readonly spaceProductItemRepository: SpaceProductItemModelRepository, + ) {} + + async createProdutItemModel( + itemModelDtos: CreateSpaceProductItemModelDto[], + spaceProductModel: SpaceProductModelEntity, + spaceModel: SpaceModelEntity, + queryRunner: QueryRunner, + ) { + await this.validateTags(itemModelDtos, spaceModel, queryRunner); + + try { + const productItems = itemModelDtos.map((dto) => + queryRunner.manager.create(this.spaceProductItemRepository.target, { + tag: dto.tag, + spaceProductModel, + spaceModel, + }), + ); + + 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, + ); + } + } + + private async validateTags( + itemModelDtos: CreateSpaceProductItemModelDto[], + spaceModel: SpaceModelEntity, + queryRunner: QueryRunner, + ) { + const incomingTags = itemModelDtos.map((item) => item.tag); + + const duplicateTags = incomingTags.filter( + (tag, index) => incomingTags.indexOf(tag) !== index, + ); + if (duplicateTags.length > 0) { + throw new HttpException( + `Duplicate tags found in the request: ${[...new Set(duplicateTags)].join(', ')}`, + HttpStatus.BAD_REQUEST, + ); + } + + const existingTags = await queryRunner.manager.find( + this.spaceProductItemRepository.target, + { + where: { spaceModel }, + select: ['tag'], + }, + ); + const existingTagSet = new Set(existingTags.map((item) => item.tag)); + + const conflictingTags = incomingTags.filter((tag) => + existingTagSet.has(tag), + ); + if (conflictingTags.length > 0) { + throw new HttpException( + `Tags already exist in the model: ${conflictingTags.join(', ')}`, + HttpStatus.CONFLICT, + ); + } + } +} diff --git a/src/space-model/services/space-product-model.service.ts b/src/space-model/services/space-product-model.service.ts new file mode 100644 index 0000000..aa08a16 --- /dev/null +++ b/src/space-model/services/space-product-model.service.ts @@ -0,0 +1,84 @@ +import { + SpaceModelEntity, + SpaceProductModelRepository, +} from '@app/common/modules/space-model'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { CreateSpaceProductModelDto } from '../dtos'; +import { ProductRepository } from '@app/common/modules/product/repositories'; +import { SpaceProductItemModelService } from './space-product-item-model.service'; +import { QueryRunner } from 'typeorm'; + +@Injectable() +export class SpaceProductModelService { + constructor( + private readonly spaceProductModelRepository: SpaceProductModelRepository, + private readonly productRepository: ProductRepository, + private readonly spaceProductItemModelService: SpaceProductItemModelService, + ) {} + + 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.productId); + 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) => + this.spaceProductItemModelService.createProdutItemModel( + dto.items, + savedProductModels[index], + 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, + ); + } + } + + private validateProductCount(dto: CreateSpaceProductModelDto) { + 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.productId}.`, + HttpStatus.BAD_REQUEST, + ); + } + } + + private async getProduct(productId: string) { + const product = await this.productRepository.findOneBy({ uuid: productId }); + if (!product) { + throw new HttpException( + `Product with ID ${productId} not found.`, + HttpStatus.NOT_FOUND, + ); + } + return product; + } +} diff --git a/src/space-model/services/subspace-model.service.ts b/src/space-model/services/subspace-model.service.ts new file mode 100644 index 0000000..a6a75aa --- /dev/null +++ b/src/space-model/services/subspace-model.service.ts @@ -0,0 +1,74 @@ +import { + SpaceModelEntity, + SubspaceModelRepository, +} from '@app/common/modules/space-model'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { CreateSubspaceModelDto } from '../dtos'; +import { QueryRunner } from 'typeorm'; + +@Injectable() +export class SubSpaceModelService { + constructor( + private readonly subspaceModelRepository: SubspaceModelRepository, + ) {} + + async createSubSpaceModels( + subSpaceModelDtos: CreateSubspaceModelDto[], + spaceModel: SpaceModelEntity, + queryRunner: QueryRunner, + ) { + this.validateInputDtos(subSpaceModelDtos); + + try { + const subspaces = subSpaceModelDtos.map((subspaceDto) => + queryRunner.manager.create(this.subspaceModelRepository.target, { + subspaceName: subspaceDto.subspaceName, + spaceModel: spaceModel, + }), + ); + + await queryRunner.manager.save(subspaces); + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + + throw new HttpException( + error.message || `An unexpected error occurred`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + private validateInputDtos(subSpaceModelDtos: CreateSubspaceModelDto[]) { + 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); + } + + private validateName(names: string[]) { + const seenNames = new Set(); + const duplicateNames = new Set(); + + for (const name of names) { + if (seenNames.has(name)) { + duplicateNames.add(name); + } else { + seenNames.add(name); + } + } + + if (duplicateNames.size > 0) { + throw new HttpException( + `Duplicate subspace names found in request: ${[...duplicateNames].join(', ')}`, + HttpStatus.CONFLICT, + ); + } + } +} diff --git a/src/space-model/space-model.module.ts b/src/space-model/space-model.module.ts new file mode 100644 index 0000000..bbc6245 --- /dev/null +++ b/src/space-model/space-model.module.ts @@ -0,0 +1,37 @@ +import { SpaceRepositoryModule } from '@app/common/modules/space/space.repository.module'; +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { SpaceModelController } from './controllers'; +import { + SpaceModelService, + SpaceProductItemModelService, + SpaceProductModelService, + SubSpaceModelService, +} from './services'; +import { + SpaceModelRepository, + SpaceProductItemModelRepository, + SpaceProductModelRepository, + SubspaceModelRepository, +} from '@app/common/modules/space-model'; +import { ProjectRepository } from '@app/common/modules/project/repositiories'; +import { ProductRepository } from '@app/common/modules/product/repositories'; + +@Module({ + imports: [ConfigModule, SpaceRepositoryModule], + controllers: [SpaceModelController], + providers: [ + SpaceModelService, + SpaceModelRepository, + ProjectRepository, + SubSpaceModelService, + SpaceProductModelService, + SubspaceModelRepository, + SpaceProductModelRepository, + ProductRepository, + SpaceProductItemModelService, + SpaceProductItemModelRepository, + ], + exports: [], +}) +export class SpaceModelModule {} diff --git a/src/space/services/space.service.ts b/src/space/services/space.service.ts index 0055ddf..8189f8c 100644 --- a/src/space/services/space.service.ts +++ b/src/space/services/space.service.ts @@ -5,7 +5,12 @@ import { HttpStatus, Injectable, } from '@nestjs/common'; -import { AddSpaceDto, CommunitySpaceParam, GetSpaceParam, UpdateSpaceDto } from '../dtos'; +import { + AddSpaceDto, + CommunitySpaceParam, + GetSpaceParam, + UpdateSpaceDto, +} from '../dtos'; import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; import { BaseResponseDto } from '@app/common/dto/base.response.dto'; import { CommunityRepository } from '@app/common/modules/community/repositories';