diff --git a/libs/common/src/constants/controller-route.ts b/libs/common/src/constants/controller-route.ts index 7f84bcc..fdf0435 100644 --- a/libs/common/src/constants/controller-route.ts +++ b/libs/common/src/constants/controller-route.ts @@ -235,4 +235,13 @@ export class ControllerRoute { 'Removes the association of a device from a specific subspace, making it no longer managed within that subspace.'; }; }; + + static PRODUCT = class { + public static readonly ROUTE = 'products'; + static ACTIONS = class { + public static readonly LIST_PRODUCT_SUMMARY = 'Retrieve all products'; + public static readonly LIST_PRODUCT_DESCRIPTION = + 'Fetches a list of all products along with their associated device details'; + }; + }; } diff --git a/libs/common/src/constants/direction.enum.ts b/libs/common/src/constants/direction.enum.ts new file mode 100644 index 0000000..5791b57 --- /dev/null +++ b/libs/common/src/constants/direction.enum.ts @@ -0,0 +1,5 @@ +export enum Direction { + LEFT = 'left', + RIGHT = 'right', + DOWN = 'down', +} diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index 868ef5a..8b4acf4 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -8,8 +8,11 @@ import { UserOtpEntity } from '../modules/user/entities'; import { ProductEntity } from '../modules/product/entities'; import { DeviceEntity } from '../modules/device/entities'; import { PermissionTypeEntity } from '../modules/permission/entities'; -import { SpaceEntity, SubspaceEntity } from '../modules/space/entities'; -import { SpaceTypeEntity } from '../modules/space/entities'; +import { + SpaceEntity, + SpaceLinkEntity, + SubspaceEntity, +} from '../modules/space/entities'; import { UserSpaceEntity } from '../modules/user/entities'; import { DeviceUserPermissionEntity } from '../modules/device/entities'; import { UserRoleEntity } from '../modules/user/entities'; @@ -23,6 +26,7 @@ import { CommunityEntity } from '../modules/community/entities'; import { DeviceStatusLogEntity } from '../modules/device-status-log/entities'; import { SceneEntity, SceneIconEntity } from '../modules/scene/entities'; import { SceneDeviceEntity } from '../modules/scene-device/entities'; +import { SpaceProductEntity } from '../modules/space/entities/space-product.entity'; @Module({ imports: [ @@ -47,8 +51,9 @@ import { SceneDeviceEntity } from '../modules/scene-device/entities'; PermissionTypeEntity, CommunityEntity, SpaceEntity, + SpaceLinkEntity, SubspaceEntity, - SpaceTypeEntity, + SpaceProductEntity, UserSpaceEntity, DeviceUserPermissionEntity, UserRoleEntity, diff --git a/libs/common/src/modules/product/entities/product.entity.ts b/libs/common/src/modules/product/entities/product.entity.ts index 6553dc1..2c731a0 100644 --- a/libs/common/src/modules/product/entities/product.entity.ts +++ b/libs/common/src/modules/product/entities/product.entity.ts @@ -2,6 +2,7 @@ import { Column, Entity, OneToMany } from 'typeorm'; import { ProductDto } from '../dtos'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { DeviceEntity } from '../../device/entities'; +import { SpaceProductEntity } from '../../space/entities/space-product.entity'; @Entity({ name: 'product' }) export class ProductEntity extends AbstractEntity { @@ -16,11 +17,19 @@ export class ProductEntity extends AbstractEntity { }) public prodId: string; + @Column({ + nullable: true, + }) + public name: string; + @Column({ nullable: false, }) public prodType: string; + @OneToMany(() => SpaceProductEntity, (spaceProduct) => spaceProduct.product) + spaceProducts: SpaceProductEntity[]; + @OneToMany( () => DeviceEntity, (devicesProductEntity) => devicesProductEntity.productDevice, diff --git a/libs/common/src/modules/space/dtos/space.dto.ts b/libs/common/src/modules/space/dtos/space.dto.ts index 04072fd..98706d0 100644 --- a/libs/common/src/modules/space/dtos/space.dto.ts +++ b/libs/common/src/modules/space/dtos/space.dto.ts @@ -21,13 +21,3 @@ export class SpaceDto { @IsNotEmpty() public invitationCode: string; } - -export class SpaceTypeDto { - @IsString() - @IsNotEmpty() - public uuid: string; - - @IsString() - @IsNotEmpty() - public type: string; -} diff --git a/libs/common/src/modules/space/entities/index.ts b/libs/common/src/modules/space/entities/index.ts index e551c0a..f720cf4 100644 --- a/libs/common/src/modules/space/entities/index.ts +++ b/libs/common/src/modules/space/entities/index.ts @@ -1,2 +1,3 @@ export * from './space.entity'; export * from './subspace.entity'; +export * from './space-link.entity'; diff --git a/libs/common/src/modules/space/entities/space-link.entity.ts b/libs/common/src/modules/space/entities/space-link.entity.ts new file mode 100644 index 0000000..a62ce4f --- /dev/null +++ b/libs/common/src/modules/space/entities/space-link.entity.ts @@ -0,0 +1,26 @@ +import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { SpaceEntity } from './space.entity'; +import { Direction } from '@app/common/constants/direction.enum'; + +@Entity({ name: 'space-link' }) +export class SpaceLinkEntity extends AbstractEntity { + @ManyToOne(() => SpaceEntity, { nullable: false, onDelete: 'CASCADE' }) + @JoinColumn({ name: 'start_space_id' }) + public startSpace: SpaceEntity; + + @ManyToOne(() => SpaceEntity, { nullable: false, onDelete: 'CASCADE' }) + @JoinColumn({ name: 'end_space_id' }) + public endSpace: SpaceEntity; + + @Column({ + nullable: false, + enum: Object.values(Direction), + }) + direction: string; + + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} diff --git a/libs/common/src/modules/space/entities/space-product.entity.ts b/libs/common/src/modules/space/entities/space-product.entity.ts new file mode 100644 index 0000000..92ad411 --- /dev/null +++ b/libs/common/src/modules/space/entities/space-product.entity.ts @@ -0,0 +1,32 @@ +import { Column, Entity, ManyToOne, JoinColumn } from 'typeorm'; +import { SpaceEntity } from './space.entity'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { ProductEntity } from '../../product/entities'; + +@Entity({ name: 'space-product' }) +export class SpaceProductEntity extends AbstractEntity { + @ManyToOne(() => SpaceEntity, (space) => space.spaceProducts, { + nullable: false, + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'space_uuid' }) + space: SpaceEntity; + + @ManyToOne(() => ProductEntity, (product) => product.spaceProducts, { + nullable: false, + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'product_uuid' }) + product: ProductEntity; + + @Column({ + nullable: false, + type: 'int', + }) + productCount: number; + + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} diff --git a/libs/common/src/modules/space/entities/space.entity.ts b/libs/common/src/modules/space/entities/space.entity.ts index 196a522..38d4c5e 100644 --- a/libs/common/src/modules/space/entities/space.entity.ts +++ b/libs/common/src/modules/space/entities/space.entity.ts @@ -6,32 +6,14 @@ import { OneToMany, Unique, } from 'typeorm'; -import { SpaceDto, SpaceTypeDto } from '../dtos'; +import { SpaceDto } from '../dtos'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { UserSpaceEntity } from '../../user/entities'; import { DeviceEntity } from '../../device/entities'; import { CommunityEntity } from '../../community/entities'; import { SubspaceEntity } from './subspace.entity'; - -@Entity({ name: 'space-type' }) -export class SpaceTypeEntity extends AbstractEntity { - @Column({ - type: 'uuid', - default: () => 'gen_random_uuid()', - nullable: false, - }) - public uuid: string; - - @Column({ - nullable: false, - }) - type: string; - - constructor(partial: Partial) { - super(); - Object.assign(this, partial); - } -} +import { SpaceLinkEntity } from './space-link.entity'; +import { SpaceProductEntity } from './space-product.entity'; @Entity({ name: 'space' }) @Unique(['invitationCode']) @@ -77,12 +59,38 @@ export class SpaceEntity extends AbstractEntity { }) subspaces?: SubspaceEntity[]; + // Position columns + @Column({ type: 'float', nullable: false, default: 0 }) + public x: number; // X coordinate for position + + @Column({ type: 'float', nullable: false, default: 0 }) + public y: number; // Y coordinate for position + @OneToMany( () => DeviceEntity, (devicesSpaceEntity) => devicesSpaceEntity.spaceDevice, ) devices: DeviceEntity[]; + @OneToMany(() => SpaceLinkEntity, (connection) => connection.startSpace, { + nullable: true, + }) + public outgoingConnections: SpaceLinkEntity[]; + + @OneToMany(() => SpaceLinkEntity, (connection) => connection.endSpace, { + nullable: true, + }) + public incomingConnections: SpaceLinkEntity[]; + + @Column({ + nullable: true, + type: 'text', + }) + public icon: string; + + @OneToMany(() => SpaceProductEntity, (spaceProduct) => spaceProduct.space) + spaceProducts: SpaceProductEntity[]; + constructor(partial: Partial) { super(); Object.assign(this, partial); diff --git a/libs/common/src/modules/space/repositories/space.repository.ts b/libs/common/src/modules/space/repositories/space.repository.ts index 5e0962c..43ce45e 100644 --- a/libs/common/src/modules/space/repositories/space.repository.ts +++ b/libs/common/src/modules/space/repositories/space.repository.ts @@ -1,6 +1,7 @@ import { DataSource, Repository } from 'typeorm'; import { Injectable } from '@nestjs/common'; -import { SpaceEntity, SpaceTypeEntity, SubspaceEntity } from '../entities'; +import { SpaceProductEntity } from '../entities/space-product.entity'; +import { SpaceEntity, SpaceLinkEntity, SubspaceEntity } from '../entities'; @Injectable() export class SpaceRepository extends Repository { @@ -8,17 +9,22 @@ export class SpaceRepository extends Repository { super(SpaceEntity, dataSource.createEntityManager()); } } - -@Injectable() -export class SpaceTypeRepository extends Repository { - constructor(private dataSource: DataSource) { - super(SpaceTypeEntity, dataSource.createEntityManager()); - } -} - @Injectable() export class SubspaceRepository extends Repository { constructor(private dataSource: DataSource) { super(SubspaceEntity, dataSource.createEntityManager()); } } + +@Injectable() +export class SpaceLinkRepository extends Repository { + constructor(private dataSource: DataSource) { + super(SpaceLinkEntity, dataSource.createEntityManager()); + } +} +@Injectable() +export class SpaceProductRepository extends Repository { + constructor(private dataSource: DataSource) { + super(SpaceProductEntity, dataSource.createEntityManager()); + } +} diff --git a/libs/common/src/modules/space/space.repository.module.ts b/libs/common/src/modules/space/space.repository.module.ts index ea3c606..90916c2 100644 --- a/libs/common/src/modules/space/space.repository.module.ts +++ b/libs/common/src/modules/space/space.repository.module.ts @@ -1,13 +1,11 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { SpaceEntity, SpaceTypeEntity, SubspaceEntity } from './entities'; +import { SpaceEntity, SubspaceEntity } from './entities'; @Module({ providers: [], exports: [], controllers: [], - imports: [ - TypeOrmModule.forFeature([SpaceEntity, SpaceTypeEntity, SubspaceEntity]), - ], + imports: [TypeOrmModule.forFeature([SpaceEntity, SubspaceEntity])], }) export class SpaceRepositoryModule {} diff --git a/libs/common/src/seed/seeder.module.ts b/libs/common/src/seed/seeder.module.ts index 0c89c5c..eeb245b 100644 --- a/libs/common/src/seed/seeder.module.ts +++ b/libs/common/src/seed/seeder.module.ts @@ -7,8 +7,6 @@ import { ConfigModule } from '@nestjs/config'; import { RoleTypeRepositoryModule } from '../modules/role-type/role.type.repository.module'; import { RoleTypeRepository } from '../modules/role-type/repositories'; import { RoleTypeSeeder } from './services/role.type.seeder'; -import { SpaceTypeRepository } from '../modules/space/repositories'; -import { SpaceTypeSeeder } from './services/space.type.seeder'; import { SpaceRepositoryModule } from '../modules/space/space.repository.module'; import { SuperAdminSeeder } from './services/supper.admin.seeder'; import { UserRepository } from '../modules/user/repositories'; @@ -25,11 +23,9 @@ import { SceneIconRepository } from '../modules/scene/repositories'; providers: [ PermissionTypeSeeder, RoleTypeSeeder, - SpaceTypeSeeder, SeederService, PermissionTypeRepository, RoleTypeRepository, - SpaceTypeRepository, SuperAdminSeeder, UserRepository, UserRoleRepository, diff --git a/libs/common/src/seed/services/seeder.service.ts b/libs/common/src/seed/services/seeder.service.ts index e627e44..09fb91d 100644 --- a/libs/common/src/seed/services/seeder.service.ts +++ b/libs/common/src/seed/services/seeder.service.ts @@ -1,7 +1,6 @@ import { Injectable } from '@nestjs/common'; import { PermissionTypeSeeder } from './permission.type.seeder'; import { RoleTypeSeeder } from './role.type.seeder'; -import { SpaceTypeSeeder } from './space.type.seeder'; import { SuperAdminSeeder } from './supper.admin.seeder'; import { RegionSeeder } from './regions.seeder'; import { TimeZoneSeeder } from './timezone.seeder'; @@ -11,7 +10,6 @@ export class SeederService { constructor( private readonly permissionTypeSeeder: PermissionTypeSeeder, private readonly roleTypeSeeder: RoleTypeSeeder, - private readonly spaceTypeSeeder: SpaceTypeSeeder, private readonly regionSeeder: RegionSeeder, private readonly timeZoneSeeder: TimeZoneSeeder, private readonly superAdminSeeder: SuperAdminSeeder, @@ -21,7 +19,6 @@ export class SeederService { async seed() { await this.permissionTypeSeeder.addPermissionTypeDataIfNotFound(); await this.roleTypeSeeder.addRoleTypeDataIfNotFound(); - await this.spaceTypeSeeder.addSpaceTypeDataIfNotFound(); await this.regionSeeder.addRegionDataIfNotFound(); await this.timeZoneSeeder.addTimeZoneDataIfNotFound(); await this.superAdminSeeder.createSuperAdminIfNotFound(); diff --git a/libs/common/src/seed/services/space.type.seeder.ts b/libs/common/src/seed/services/space.type.seeder.ts deleted file mode 100644 index 56c2ee8..0000000 --- a/libs/common/src/seed/services/space.type.seeder.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { SpaceType } from '../../constants/space-type.enum'; -import { SpaceTypeRepository } from '../../modules/space/repositories'; - -@Injectable() -export class SpaceTypeSeeder { - constructor(private readonly spaceTypeRepository: SpaceTypeRepository) {} - - async addSpaceTypeDataIfNotFound(): Promise { - try { - const existingSpaceTypes = await this.spaceTypeRepository.find(); - - const spaceTypeNames = existingSpaceTypes.map((pt) => pt.type); - - const missingSpaceTypes = []; - if (!spaceTypeNames.includes(SpaceType.COMMUNITY)) { - missingSpaceTypes.push(SpaceType.COMMUNITY); - } - if (!spaceTypeNames.includes(SpaceType.BUILDING)) { - missingSpaceTypes.push(SpaceType.BUILDING); - } - if (!spaceTypeNames.includes(SpaceType.FLOOR)) { - missingSpaceTypes.push(SpaceType.FLOOR); - } - if (!spaceTypeNames.includes(SpaceType.UNIT)) { - missingSpaceTypes.push(SpaceType.UNIT); - } - if (!spaceTypeNames.includes(SpaceType.ROOM)) { - missingSpaceTypes.push(SpaceType.ROOM); - } - if (missingSpaceTypes.length > 0) { - await this.addSpaceTypeData(missingSpaceTypes); - } - } catch (err) { - console.error('Error while checking space type data:', err); - throw err; - } - } - - private async addSpaceTypeData(spaceTypes: string[]): Promise { - try { - const spaceTypeEntities = spaceTypes.map((type) => ({ - type, - })); - - await this.spaceTypeRepository.save(spaceTypeEntities); - } catch (err) { - console.error('Error while adding space type data:', err); - throw err; - } - } -} diff --git a/src/app.module.ts b/src/app.module.ts index 820602b..609147c 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -21,6 +21,7 @@ import { TimeZoneModule } from './timezone/timezone.module'; import { VisitorPasswordModule } from './vistor-password/visitor-password.module'; import { ScheduleModule } from './schedule/schedule.module'; import { SpaceModule } from './space/space.module'; +import { ProductModule } from './product'; @Module({ imports: [ ConfigModule.forRoot({ @@ -46,6 +47,7 @@ import { SpaceModule } from './space/space.module'; TimeZoneModule, VisitorPasswordModule, ScheduleModule, + ProductModule, ], providers: [ { diff --git a/src/product/controllers/index.ts b/src/product/controllers/index.ts new file mode 100644 index 0000000..eead553 --- /dev/null +++ b/src/product/controllers/index.ts @@ -0,0 +1 @@ +export * from './product.controller'; diff --git a/src/product/controllers/product.controller.ts b/src/product/controllers/product.controller.ts new file mode 100644 index 0000000..f5fdf53 --- /dev/null +++ b/src/product/controllers/product.controller.ts @@ -0,0 +1,27 @@ +import { Controller, Get, UseGuards } from '@nestjs/common'; +import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; +import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { ControllerRoute } from '@app/common/constants/controller-route'; +import { ProductService } from '../services'; + +@ApiTags('Product Module') +@Controller({ + version: EnableDisableStatusEnum.ENABLED, + path: ControllerRoute.PRODUCT.ROUTE, +}) +export class ProductController { + constructor(private readonly productService: ProductService) {} + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get() + @ApiOperation({ + summary: ControllerRoute.PRODUCT.ACTIONS.LIST_PRODUCT_SUMMARY, + description: ControllerRoute.PRODUCT.ACTIONS.LIST_PRODUCT_DESCRIPTION, + }) + async getProducts(): Promise { + return await this.productService.list(); + } +} diff --git a/src/product/index.ts b/src/product/index.ts new file mode 100644 index 0000000..8b3162c --- /dev/null +++ b/src/product/index.ts @@ -0,0 +1 @@ +export * from './product.module'; diff --git a/src/product/product.module.ts b/src/product/product.module.ts new file mode 100644 index 0000000..e5ffd25 --- /dev/null +++ b/src/product/product.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { ProductService } from './services'; +import { ProductRepository } from '@app/common/modules/product/repositories'; +import { ProductController } from './controllers'; + +@Module({ + controllers: [ProductController], + providers: [ProductService, ProductRepository], +}) +export class ProductModule {} diff --git a/src/product/services/index.ts b/src/product/services/index.ts new file mode 100644 index 0000000..59832ef --- /dev/null +++ b/src/product/services/index.ts @@ -0,0 +1 @@ +export * from './product.service'; diff --git a/src/product/services/product.service.ts b/src/product/services/product.service.ts new file mode 100644 index 0000000..cebf9ba --- /dev/null +++ b/src/product/services/product.service.ts @@ -0,0 +1,24 @@ +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; +import { ProductRepository } from '@app/common/modules/product/repositories'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; + +@Injectable() +export class ProductService { + constructor(private readonly productRepository: ProductRepository) {} + + async list(): Promise { + const products = await this.productRepository.find(); + + if (!products) { + throw new HttpException( + `No products found in the system`, + HttpStatus.NOT_FOUND, + ); + } + return new SuccessResponseDto({ + data: products, + message: 'List of products retrieved successfully', + }); + } +} diff --git a/src/space/dtos/add.space.dto.ts b/src/space/dtos/add.space.dto.ts index a33bb34..1f369b1 100644 --- a/src/space/dtos/add.space.dto.ts +++ b/src/space/dtos/add.space.dto.ts @@ -1,10 +1,14 @@ import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; import { + IsArray, IsBoolean, IsNotEmpty, + IsNumber, IsOptional, IsString, IsUUID, + ValidateNested, } from 'class-validator'; export class AddSpaceDto { @@ -25,6 +29,10 @@ export class AddSpaceDto { @IsOptional() parentUuid?: string; + @IsString() + @IsNotEmpty() + public icon: string; + @ApiProperty({ description: 'Indicates whether the space is private or public', example: false, @@ -32,6 +40,24 @@ export class AddSpaceDto { }) @IsBoolean() isPrivate: boolean; + + @ApiProperty({ description: 'X position on canvas', example: 120 }) + @IsNumber() + x: number; + + @ApiProperty({ description: 'Y position on canvas', example: 200 }) + @IsNumber() + y: number; + + @ApiProperty({ description: 'Y position on canvas', example: 200 }) + @IsString() + @IsOptional() + direction: string; + + @IsArray() + @ValidateNested({ each: true }) + @Type(() => ProductAssignmentDto) + products: ProductAssignmentDto[]; } export class AddUserSpaceDto { @@ -69,7 +95,16 @@ export class AddUserSpaceUsingCodeDto { @IsString() @IsNotEmpty() public inviteCode: string; + constructor(dto: Partial) { Object.assign(this, dto); } } + +class ProductAssignmentDto { + @IsNotEmpty() + productId: string; + + @IsNotEmpty() + count: number; +} diff --git a/src/space/services/index.ts b/src/space/services/index.ts index c4e9405..79eb32f 100644 --- a/src/space/services/index.ts +++ b/src/space/services/index.ts @@ -2,4 +2,6 @@ export * from './space.service'; export * from './space-user.service'; export * from './space-device.service'; export * from './subspace'; +export * from './space-link'; export * from './space-scene.service'; +export * from './space-products'; diff --git a/src/space/services/space-link/index.ts b/src/space/services/space-link/index.ts new file mode 100644 index 0000000..ad41b9a --- /dev/null +++ b/src/space/services/space-link/index.ts @@ -0,0 +1 @@ +export * from './space-link.service'; diff --git a/src/space/services/space-link/space-link.service.ts b/src/space/services/space-link/space-link.service.ts new file mode 100644 index 0000000..0acece0 --- /dev/null +++ b/src/space/services/space-link/space-link.service.ts @@ -0,0 +1,88 @@ +import { + SpaceLinkRepository, + SpaceRepository, +} from '@app/common/modules/space/repositories'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; + +@Injectable() +export class SpaceLinkService { + constructor( + private readonly spaceRepository: SpaceRepository, + private readonly spaceLinkRepository: SpaceLinkRepository, + ) {} + + async saveSpaceLink( + startSpaceId: string, + endSpaceId: string, + direction: string, + ): Promise { + try { + // Check if a link between the startSpace and endSpace already exists + const existingLink = await this.spaceLinkRepository.findOne({ + where: { + startSpace: { uuid: startSpaceId }, + endSpace: { uuid: endSpaceId }, + }, + }); + + if (existingLink) { + // Update the direction if the link exists + existingLink.direction = direction; + await this.spaceLinkRepository.save(existingLink); + return; + } + + const existingEndSpaceLink = await this.spaceLinkRepository.findOne({ + where: { endSpace: { uuid: endSpaceId } }, + }); + + if ( + existingEndSpaceLink && + existingEndSpaceLink.startSpace.uuid !== startSpaceId + ) { + throw new Error( + `Space with ID ${endSpaceId} is already an endSpace in another link and cannot be reused.`, + ); + } + + // Find start space + const startSpace = await this.spaceRepository.findOne({ + where: { uuid: startSpaceId }, + }); + + if (!startSpace) { + throw new HttpException( + `Start space with ID ${startSpaceId} not found.`, + HttpStatus.NOT_FOUND, + ); + } + + // Find end space + const endSpace = await this.spaceRepository.findOne({ + where: { uuid: endSpaceId }, + }); + + if (!endSpace) { + throw new HttpException( + `End space with ID ${endSpaceId} not found.`, + HttpStatus.NOT_FOUND, + ); + } + + // Create and save the space link + const spaceLink = this.spaceLinkRepository.create({ + startSpace, + endSpace, + direction, + }); + + await this.spaceLinkRepository.save(spaceLink); + } catch (error) { + throw new HttpException( + error.message || + `Failed to save space link. Internal Server Error: ${error.message}`, + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/space/services/space-products/index.ts b/src/space/services/space-products/index.ts new file mode 100644 index 0000000..d0b92d2 --- /dev/null +++ b/src/space/services/space-products/index.ts @@ -0,0 +1 @@ +export * from './space-products.service'; diff --git a/src/space/services/space-products/space-products.service.ts b/src/space/services/space-products/space-products.service.ts new file mode 100644 index 0000000..5b218e4 --- /dev/null +++ b/src/space/services/space-products/space-products.service.ts @@ -0,0 +1,131 @@ +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 { SpaceProductRepository } from '@app/common/modules/space/repositories'; +import { In } from 'typeorm'; + +@Injectable() +export class SpaceProductService { + constructor( + private readonly productRepository: ProductRepository, + private readonly spaceProductRepository: SpaceProductRepository, + ) {} + + async assignProductsToSpace( + space: SpaceEntity, + products: { productId: string; count: number }[], + ): Promise { + try { + const uniqueProducts = this.validateUniqueProducts(products); + const productEntities = await this.getProductEntities(uniqueProducts); + + // Fetch existing space products + const existingSpaceProducts = await this.spaceProductRepository.find({ + where: { + space: { + uuid: space.uuid, + }, + }, + relations: ['product'], + }); + + const updatedProducts = []; + const newProducts = []; + + for (const { productId, count } of uniqueProducts) { + const product = productEntities.get(productId); + if (!product) { + throw new HttpException( + `Product with ID ${productId} not found`, + HttpStatus.NOT_FOUND, + ); + } + + // Check if product already exists in the space + const existingProduct = existingSpaceProducts.find( + (spaceProduct) => spaceProduct.product.uuid === productId, + ); + + if (existingProduct) { + // If count is different, update the existing record + if (existingProduct.productCount !== count) { + existingProduct.productCount = count; + updatedProducts.push(existingProduct); + } + } else { + // Add new product if it doesn't exist + newProducts.push( + this.spaceProductRepository.create({ + space, + product, + productCount: count, + }), + ); + } + } + + // Save updates and new records + if (updatedProducts.length > 0) { + await this.spaceProductRepository.save(updatedProducts); + } + + if (newProducts.length > 0) { + await this.spaceProductRepository.save(newProducts); + } + + return [...updatedProducts, ...newProducts]; + } catch (error) { + console.error('Error assigning products to space:', error); + + if (!(error instanceof HttpException)) { + throw new HttpException( + 'An error occurred while assigning products to the space', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + throw error; + } + } + + private validateUniqueProducts( + products: { productId: string; count: number }[], + ): { productId: string; count: number }[] { + 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: { productId: string; count: number }[], + ): Promise> { + 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, + ); + } + } +} diff --git a/src/space/services/space.service.ts b/src/space/services/space.service.ts index 78d45d7..44d27ba 100644 --- a/src/space/services/space.service.ts +++ b/src/space/services/space.service.ts @@ -11,46 +11,27 @@ import { BaseResponseDto } from '@app/common/dto/base.response.dto'; import { CommunityRepository } from '@app/common/modules/community/repositories'; 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'; @Injectable() export class SpaceService { constructor( private readonly spaceRepository: SpaceRepository, private readonly communityRepository: CommunityRepository, + private readonly spaceLinkService: SpaceLinkService, + private readonly spaceProductService: SpaceProductService, ) {} async createSpace( addSpaceDto: AddSpaceDto, communityId: string, ): Promise { - let parent: SpaceEntity | null = null; + const { parentUuid, direction, products } = addSpaceDto; - const { parentUuid } = addSpaceDto; - const community = await this.communityRepository.findOne({ - where: { uuid: communityId }, - }); + const community = await this.validateCommunity(communityId); - // If the community doesn't exist, throw a 404 error - if (!community) { - throw new HttpException( - `Community with ID ${communityId} not found`, - HttpStatus.NOT_FOUND, - ); - } - - if (parentUuid) { - parent = await this.spaceRepository.findOne({ - where: { uuid: parentUuid }, - }); - - // If the community doesn't exist, throw a 404 error - if (!parent) { - throw new HttpException( - `Parent with ID ${parentUuid} not found`, - HttpStatus.NOT_FOUND, - ); - } - } + const parent = parentUuid ? await this.validateSpace(parentUuid) : null; try { const newSpace = this.spaceRepository.create({ ...addSpaceDto, @@ -60,6 +41,21 @@ export class SpaceService { await this.spaceRepository.save(newSpace); + if (direction && parent) { + await this.spaceLinkService.saveSpaceLink( + parent.uuid, + newSpace.uuid, + direction, + ); + } + + if (products && products.length > 0) { + await this.spaceProductService.assignProductsToSpace( + newSpace, + products, + ); + } + return new SuccessResponseDto({ statusCode: HttpStatus.CREATED, data: newSpace, @@ -73,22 +69,18 @@ export class SpaceService { async getSpacesHierarchyForCommunity( communityUuid: string, ): Promise { - const community = await this.communityRepository.findOne({ - where: { uuid: communityUuid }, - }); - - // If the community doesn't exist, throw a 404 error - if (!community) { - throw new HttpException( - `Community with ID ${communityUuid} not found`, - HttpStatus.NOT_FOUND, - ); - } + await this.validateCommunity(communityUuid); try { // Get all spaces related to the community, including the parent-child relations const spaces = await this.spaceRepository.find({ where: { community: { uuid: communityUuid } }, - relations: ['parent', 'children'], // Include parent and children relations + relations: [ + 'parent', + 'children', + 'incomingConnections', + 'spaceProducts', + 'spaceProducts.product', + ], // Include parent and children relations }); // Organize spaces into a hierarchical structure @@ -109,19 +101,8 @@ export class SpaceService { async findOne(spaceUuid: string): Promise { try { - const space = await this.spaceRepository.findOne({ - where: { - uuid: spaceUuid, - }, - }); + const space = await this.validateSpace(spaceUuid); - // If space is not found, throw a NotFoundException - if (!space) { - throw new HttpException( - `Space with UUID ${spaceUuid} not found`, - HttpStatus.NOT_FOUND, - ); - } return new SuccessResponseDto({ message: `Space with ID ${spaceUuid} successfully fetched`, data: space, @@ -144,28 +125,10 @@ export class SpaceService { ): Promise { try { // First, check if the community exists - const community = await this.communityRepository.findOne({ - where: { uuid: communityUuid }, - }); - - if (!community) { - throw new HttpException( - `Community with ID ${communityUuid} not found`, - HttpStatus.NOT_FOUND, - ); - } - - // Check if the space exists - const space = await this.spaceRepository.findOne({ - where: { uuid: spaceUuid, community: { uuid: communityUuid } }, - }); - - if (!space) { - throw new HttpException( - `Space with ID ${spaceUuid} not found`, - HttpStatus.NOT_FOUND, - ); - } + const space = await this.validateCommunityAndSpace( + spaceUuid, + communityUuid, + ); // Delete the space await this.spaceRepository.remove(space); @@ -191,57 +154,27 @@ export class SpaceService { updateSpaceDto: AddSpaceDto, ): Promise { try { - // First, check if the community exists - const community = await this.communityRepository.findOne({ - where: { uuid: communityId }, - }); - - if (!community) { - throw new HttpException( - `Community with ID ${communityId} not found`, - HttpStatus.NOT_FOUND, - ); - } - - // Check if the space exists - const space = await this.spaceRepository.findOne({ - where: { uuid: spaceUuid, community: { uuid: communityId } }, - }); - - if (!space) { - throw new HttpException( - `Space with ID ${spaceUuid} not found`, - HttpStatus.NOT_FOUND, - ); - } + const space = await this.validateCommunityAndSpace( + communityId, + spaceUuid, + ); // If a parentId is provided, check if the parent exists - const { parentUuid } = updateSpaceDto; - let parent: SpaceEntity | null = null; - if (parentUuid) { - parent = await this.spaceRepository.findOne({ - where: { uuid: parentUuid, community: { uuid: communityId } }, - }); - - // If the parent doesn't exist, throw a 404 error - if (!parent) { - throw new HttpException( - `Parent space with ID ${parentUuid} not found`, - HttpStatus.NOT_FOUND, - ); - } - - // Set the parent of the current space - space.parent = parent; - } else { - space.parent = null; // If no parent is provided, clear the parent - } + const { parentUuid, products } = updateSpaceDto; + const parent = parentUuid ? await this.validateSpace(parentUuid) : null; // Update other space properties from updateSpaceDto - Object.assign(space, updateSpaceDto); + Object.assign(space, updateSpaceDto, { parent }); // Save the updated space - await this.spaceRepository.save(space); + const updatedSpace = await this.spaceRepository.save(space); + + if (products && products.length > 0) { + await this.spaceProductService.assignProductsToSpace( + updatedSpace, + products, + ); + } return new SuccessResponseDto({ message: `Space with ID ${spaceUuid} successfully updated`, @@ -262,17 +195,7 @@ export class SpaceService { async getSpacesHierarchyForSpace( spaceUuid: string, ): Promise { - const space = await this.spaceRepository.findOne({ - where: { uuid: spaceUuid }, - }); - - // If the space doesn't exist, throw a 404 error - if (!space) { - throw new HttpException( - `Space with ID ${spaceUuid} not found`, - HttpStatus.NOT_FOUND, - ); - } + await this.validateSpace(spaceUuid); try { // Get all spaces that are children of the provided space, including the parent-child relations @@ -301,18 +224,7 @@ export class SpaceService { try { const invitationCode = generateRandomString(6); - const space = await this.spaceRepository.findOne({ - where: { - uuid: spaceUuid, - }, - }); - - if (!space) { - throw new HttpException( - `Space with ID ${spaceUuid} not found`, - HttpStatus.NOT_FOUND, - ); - } + const space = await this.validateSpace(spaceUuid); space.invitationCode = invitationCode; await this.spaceRepository.save(space); @@ -355,24 +267,37 @@ export class SpaceService { } }); - return rootSpaces; // Return the root spaces with children nested within them + return rootSpaces; + } + + private async validateCommunity(communityId: string) { + const community = await this.communityRepository.findOne({ + where: { uuid: communityId }, + }); + if (!community) { + throw new HttpException( + `Community with ID ${communityId} not found`, + HttpStatus.NOT_FOUND, + ); + } + return community; } async validateCommunityAndSpace(communityUuid: string, spaceUuid: string) { - const community = await this.communityRepository.findOne({ - where: { uuid: communityUuid }, - }); + const community = await this.validateCommunity(communityUuid); if (!community) { this.throwNotFound('Community', communityUuid); } + const space = await this.validateSpace(spaceUuid); + return space; + } + + private async validateSpace(spaceUuid: string) { const space = await this.spaceRepository.findOne({ - where: { uuid: spaceUuid, community: { uuid: communityUuid } }, - relations: ['devices', 'devices.productDevice'], + where: { uuid: spaceUuid }, }); - if (!space) { - this.throwNotFound('Space', spaceUuid); - } + if (!space) this.throwNotFound('Space', spaceUuid); return space; } diff --git a/src/space/space.module.ts b/src/space/space.module.ts index c840bd9..9c80fa1 100644 --- a/src/space/space.module.ts +++ b/src/space/space.module.ts @@ -11,6 +11,8 @@ import { } from './controllers'; import { SpaceDeviceService, + SpaceLinkService, + SpaceProductService, SpaceSceneService, SpaceService, SpaceUserService, @@ -18,8 +20,10 @@ import { SubSpaceService, } from './services'; import { + SpaceProductRepository, SpaceRepository, SubspaceRepository, + SpaceLinkRepository, } from '@app/common/modules/space/repositories'; import { CommunityRepository } from '@app/common/modules/community/repositories'; import { @@ -55,11 +59,13 @@ import { SceneDeviceRepository } from '@app/common/modules/scene-device/reposito ProductRepository, SubSpaceService, SpaceDeviceService, + SpaceLinkService, SubspaceDeviceService, SpaceRepository, DeviceRepository, CommunityRepository, SubspaceRepository, + SpaceLinkRepository, UserSpaceRepository, UserRepository, SpaceUserService, @@ -71,6 +77,8 @@ import { SceneDeviceRepository } from '@app/common/modules/scene-device/reposito DeviceStatusFirebaseService, DeviceStatusLogRepository, SceneDeviceRepository, + SpaceProductService, + SpaceProductRepository, ], exports: [SpaceService], })