mirror of
https://github.com/SyncrowIOT/backend.git
synced 2025-08-25 04:42:26 +00:00
implement duplicate space API
This commit is contained in:
@ -223,6 +223,10 @@ export class ControllerRoute {
|
||||
public static readonly CREATE_SPACE_DESCRIPTION =
|
||||
'This endpoint allows you to create a space in a specified community. Optionally, you can specify a parent space to nest the new space under it.';
|
||||
|
||||
public static readonly DUPLICATE_SPACE_SUMMARY = 'Duplicate a space';
|
||||
public static readonly DUPLICATE_SPACE_DESCRIPTION =
|
||||
'This endpoint allows you to create a copy of an existing space in a specified community.';
|
||||
|
||||
public static readonly LIST_SPACE_SUMMARY = 'List spaces in community';
|
||||
public static readonly LIST_SPACE_DESCRIPTION =
|
||||
'List spaces in specified community by community id';
|
||||
|
@ -22,6 +22,11 @@ export class SpaceProductAllocationEntity extends AbstractEntity<SpaceProductAll
|
||||
})
|
||||
public space: SpaceEntity;
|
||||
|
||||
@Column({
|
||||
name: 'space_uuid',
|
||||
})
|
||||
public spaceUuid: string;
|
||||
|
||||
@ManyToOne(() => SpaceModelProductAllocationEntity, {
|
||||
nullable: true,
|
||||
onDelete: 'SET NULL',
|
||||
@ -31,9 +36,19 @@ export class SpaceProductAllocationEntity extends AbstractEntity<SpaceProductAll
|
||||
@ManyToOne(() => ProductEntity, { nullable: false, onDelete: 'CASCADE' })
|
||||
public product: ProductEntity;
|
||||
|
||||
@Column({
|
||||
name: 'product_uuid',
|
||||
})
|
||||
public productUuid: string;
|
||||
|
||||
@ManyToOne(() => NewTagEntity, { nullable: true, onDelete: 'CASCADE' })
|
||||
public tag: NewTagEntity;
|
||||
|
||||
@Column({
|
||||
name: 'tag_uuid',
|
||||
})
|
||||
public tagUuid: string;
|
||||
|
||||
constructor(partial: Partial<SpaceProductAllocationEntity>) {
|
||||
super();
|
||||
Object.assign(this, partial);
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
|
||||
import { AqiSpaceDailyPollutantStatsEntity } from '../../aqi/entities';
|
||||
import { BookableSpaceEntity } from '../../booking/entities/bookable-space.entity';
|
||||
import { BookingEntity } from '../../booking/entities/booking.entity';
|
||||
import { CommunityEntity } from '../../community/entities';
|
||||
import { DeviceEntity } from '../../device/entities';
|
||||
import { InviteUserSpaceEntity } from '../../Invite-user/entities';
|
||||
@ -20,7 +21,6 @@ import { UserSpaceEntity } from '../../user/entities';
|
||||
import { SpaceDto } from '../dtos';
|
||||
import { SpaceProductAllocationEntity } from './space-product-allocation.entity';
|
||||
import { SubspaceEntity } from './subspace/subspace.entity';
|
||||
import { BookingEntity } from '../../booking/entities/booking.entity';
|
||||
|
||||
@Entity({ name: 'space' })
|
||||
export class SpaceEntity extends AbstractEntity<SpaceDto> {
|
||||
@ -47,12 +47,26 @@ export class SpaceEntity extends AbstractEntity<SpaceDto> {
|
||||
@JoinColumn({ name: 'community_id' })
|
||||
community: CommunityEntity;
|
||||
|
||||
@ManyToOne(() => SpaceEntity, (space) => space.children, { nullable: true })
|
||||
@Column({
|
||||
name: 'community_id',
|
||||
})
|
||||
communityId: string;
|
||||
|
||||
@ManyToOne(() => SpaceEntity, (space) => space.children, {
|
||||
nullable: true,
|
||||
})
|
||||
parent: SpaceEntity;
|
||||
|
||||
@Column({
|
||||
name: 'parent_uuid',
|
||||
nullable: true,
|
||||
})
|
||||
public parentUuid: string;
|
||||
|
||||
@OneToMany(() => SpaceEntity, (space) => space.parent, {
|
||||
nullable: false,
|
||||
onDelete: 'CASCADE',
|
||||
cascade: true,
|
||||
})
|
||||
children: SpaceEntity[];
|
||||
|
||||
@ -73,6 +87,7 @@ export class SpaceEntity extends AbstractEntity<SpaceDto> {
|
||||
|
||||
@OneToMany(() => SubspaceEntity, (subspace) => subspace.space, {
|
||||
nullable: true,
|
||||
cascade: true,
|
||||
})
|
||||
subspaces?: SubspaceEntity[];
|
||||
|
||||
|
@ -22,6 +22,11 @@ export class SubspaceProductAllocationEntity extends AbstractEntity<SubspaceProd
|
||||
})
|
||||
public subspace: SubspaceEntity;
|
||||
|
||||
@Column({
|
||||
name: 'subspace_uuid',
|
||||
})
|
||||
public subspaceUuid: string;
|
||||
|
||||
@ManyToOne(() => SubspaceModelProductAllocationEntity, {
|
||||
nullable: true,
|
||||
onDelete: 'SET NULL',
|
||||
@ -31,9 +36,19 @@ export class SubspaceProductAllocationEntity extends AbstractEntity<SubspaceProd
|
||||
@ManyToOne(() => ProductEntity, { nullable: false, onDelete: 'CASCADE' })
|
||||
public product: ProductEntity;
|
||||
|
||||
@Column({
|
||||
name: 'product_uuid',
|
||||
})
|
||||
public productUuid: string;
|
||||
|
||||
@ManyToOne(() => NewTagEntity, { nullable: true, onDelete: 'CASCADE' })
|
||||
public tag: NewTagEntity;
|
||||
|
||||
@Column({
|
||||
name: 'tag_uuid',
|
||||
})
|
||||
public tagUuid: string;
|
||||
|
||||
constructor(partial: Partial<SubspaceProductAllocationEntity>) {
|
||||
super();
|
||||
Object.assign(this, partial);
|
||||
|
@ -26,6 +26,11 @@ export class SubspaceEntity extends AbstractEntity<SubspaceDto> {
|
||||
@JoinColumn({ name: 'space_uuid' })
|
||||
space: SpaceEntity;
|
||||
|
||||
@Column({
|
||||
name: 'space_uuid',
|
||||
})
|
||||
public spaceUuid: string;
|
||||
|
||||
@Column({
|
||||
nullable: false,
|
||||
default: false,
|
||||
|
31
package-lock.json
generated
31
package-lock.json
generated
@ -48,6 +48,7 @@
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"typeorm": "^0.3.20",
|
||||
"uuid": "^11.1.0",
|
||||
"winston": "^3.17.0",
|
||||
"ws": "^8.17.0"
|
||||
},
|
||||
@ -2365,6 +2366,18 @@
|
||||
"rxjs": "^7.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/cqrs/node_modules/uuid": {
|
||||
"version": "11.0.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.2.tgz",
|
||||
"integrity": "sha512-14FfcOJmqdjbBPdDjFQyk/SdT4NySW4eM0zcG+HqbHP5jzuH56xO3J1DGhgs/cEMCfwYi3HQI1gnTO62iaG+tQ==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"bin": {
|
||||
"uuid": "dist/esm/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/jwt": {
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-10.2.0.tgz",
|
||||
@ -12749,18 +12762,6 @@
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/typeorm/node_modules/uuid": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
||||
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"bin": {
|
||||
"uuid": "dist/esm/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
@ -12896,9 +12897,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "11.0.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.2.tgz",
|
||||
"integrity": "sha512-14FfcOJmqdjbBPdDjFQyk/SdT4NySW4eM0zcG+HqbHP5jzuH56xO3J1DGhgs/cEMCfwYi3HQI1gnTO62iaG+tQ==",
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
||||
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
|
@ -60,6 +60,7 @@
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"typeorm": "^0.3.20",
|
||||
"uuid": "^11.1.0",
|
||||
"winston": "^3.17.0",
|
||||
"ws": "^8.17.0"
|
||||
},
|
||||
|
@ -17,6 +17,7 @@ import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { Permissions } from 'src/decorators/permissions.decorator';
|
||||
import { PermissionsGuard } from 'src/guards/permissions.guard';
|
||||
import { AddSpaceDto, CommunitySpaceParam, UpdateSpaceDto } from '../dtos';
|
||||
import { DuplicateSpaceDto } from '../dtos/duplicate-space.dto';
|
||||
import { GetSpaceDto } from '../dtos/get.space.dto';
|
||||
import { GetSpaceParam } from '../dtos/get.space.param';
|
||||
import { OrderSpacesDto } from '../dtos/order.spaces.dto';
|
||||
@ -48,6 +49,26 @@ export class SpaceController {
|
||||
);
|
||||
}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(PermissionsGuard)
|
||||
@Permissions('SPACE_ADD')
|
||||
@ApiOperation({
|
||||
summary: ControllerRoute.SPACE.ACTIONS.DUPLICATE_SPACE_SUMMARY,
|
||||
description: ControllerRoute.SPACE.ACTIONS.DUPLICATE_SPACE_DESCRIPTION,
|
||||
})
|
||||
@Post(':spaceUuid/duplicate')
|
||||
async duplicateSpace(
|
||||
@Param('spaceUuid', ParseUUIDPipe) spaceUuid: string,
|
||||
@Body() dto: DuplicateSpaceDto,
|
||||
@Param() communitySpaceParam: CommunitySpaceParam,
|
||||
): Promise<BaseResponseDto> {
|
||||
return await this.spaceService.duplicateSpace(
|
||||
spaceUuid,
|
||||
communitySpaceParam,
|
||||
dto,
|
||||
);
|
||||
}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(PermissionsGuard)
|
||||
@Permissions('SPACE_VIEW')
|
||||
|
18
src/space/dtos/duplicate-space.dto.ts
Normal file
18
src/space/dtos/duplicate-space.dto.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { ORPHAN_SPACE_NAME } from '@app/common/constants/orphan-constant';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsString, NotEquals } from 'class-validator';
|
||||
|
||||
export class DuplicateSpaceDto {
|
||||
@ApiProperty({
|
||||
description: 'Name of the space (e.g., Floor 1, Unit 101)',
|
||||
example: 'Unit 101',
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@NotEquals(ORPHAN_SPACE_NAME, {
|
||||
message() {
|
||||
return `Space name cannot be "${ORPHAN_SPACE_NAME}". Please choose a different name.`;
|
||||
},
|
||||
})
|
||||
spaceName: string;
|
||||
}
|
@ -133,7 +133,7 @@ export class ValidationService {
|
||||
'subspaces.productAllocations',
|
||||
'subspaces.productAllocations.product',
|
||||
'subspaces.devices',
|
||||
'spaceModel',
|
||||
// 'spaceModel',
|
||||
],
|
||||
});
|
||||
|
||||
|
@ -8,6 +8,7 @@ import { generateRandomString } from '@app/common/helper/randomString';
|
||||
import { removeCircularReferences } from '@app/common/helper/removeCircularReferences';
|
||||
import { SpaceProductAllocationEntity } from '@app/common/modules/space/entities/space-product-allocation.entity';
|
||||
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
|
||||
import { SubspaceProductAllocationEntity } from '@app/common/modules/space/entities/subspace/subspace-product-allocation.entity';
|
||||
import { SubspaceEntity } from '@app/common/modules/space/entities/subspace/subspace.entity';
|
||||
import {
|
||||
InviteSpaceRepository,
|
||||
@ -24,6 +25,7 @@ import { DeviceService } from 'src/device/services';
|
||||
import { SpaceModelService } from 'src/space-model/services';
|
||||
import { TagService } from 'src/tags/services/tags.service';
|
||||
import { DataSource, In, Not, QueryRunner } from 'typeorm';
|
||||
import { v4 as uuidV4 } from 'uuid';
|
||||
import { DisableSpaceCommand } from '../commands';
|
||||
import {
|
||||
AddSpaceDto,
|
||||
@ -32,6 +34,7 @@ import {
|
||||
UpdateSpaceDto,
|
||||
} from '../dtos';
|
||||
import { CreateProductAllocationDto } from '../dtos/create-product-allocation.dto';
|
||||
import { DuplicateSpaceDto } from '../dtos/duplicate-space.dto';
|
||||
import { GetSpaceDto } from '../dtos/get.space.dto';
|
||||
import { OrderSpacesDto } from '../dtos/order.spaces.dto';
|
||||
import { SpaceWithParentsDto } from '../dtos/space.parents.dto';
|
||||
@ -181,6 +184,155 @@ export class SpaceService {
|
||||
!isRecursiveCall ? await queryRunner.release() : null;
|
||||
}
|
||||
}
|
||||
|
||||
async duplicateSpace(
|
||||
spaceUuid: string,
|
||||
{ communityUuid, projectUuid }: CommunitySpaceParam,
|
||||
dto: DuplicateSpaceDto,
|
||||
queryRunner?: QueryRunner,
|
||||
): Promise<BaseResponseDto> {
|
||||
if (!queryRunner) {
|
||||
queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
}
|
||||
|
||||
try {
|
||||
await this.validationService.validateCommunityAndProject(
|
||||
communityUuid,
|
||||
projectUuid,
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
const result = await this.handleSpaceDuplication(
|
||||
spaceUuid,
|
||||
dto.spaceName,
|
||||
queryRunner,
|
||||
);
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
return new SuccessResponseDto({
|
||||
message: `Space with ID ${spaceUuid} successfully duplicated`,
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
private async handleSpaceDuplication(
|
||||
spaceUuid: string,
|
||||
newSpaceName: string | null,
|
||||
queryRunner: QueryRunner,
|
||||
parent?: SpaceEntity,
|
||||
) {
|
||||
const space = await this.spaceRepository.findOne({
|
||||
where: { uuid: spaceUuid },
|
||||
relations: [
|
||||
'children',
|
||||
'productAllocations',
|
||||
'subspaces',
|
||||
'subspaces.productAllocations',
|
||||
],
|
||||
});
|
||||
const clonedSpace = structuredClone(space);
|
||||
console.log(`creating duplicate for space ${clonedSpace.spaceName}`);
|
||||
const newSpace = queryRunner.manager.create(SpaceEntity, {
|
||||
...clonedSpace,
|
||||
spaceName: newSpaceName || clonedSpace.spaceName,
|
||||
parent,
|
||||
children: undefined,
|
||||
subspaces: undefined,
|
||||
productAllocations: undefined,
|
||||
uuid: uuidV4(),
|
||||
});
|
||||
|
||||
console.log(`handling space ${newSpace.spaceName} allocations`);
|
||||
if (clonedSpace.productAllocations?.length) {
|
||||
newSpace.productAllocations = this.copySpaceAllocations(
|
||||
newSpace,
|
||||
clonedSpace.productAllocations,
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
console.log(`handling space ${newSpace.spaceName} subspaces`);
|
||||
if (clonedSpace.subspaces?.length) {
|
||||
newSpace.subspaces = this.copySpaceSubspaces(
|
||||
newSpace,
|
||||
clonedSpace.subspaces,
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
const savedSpace = await queryRunner.manager.save(newSpace);
|
||||
|
||||
console.log(`handling space ${newSpace.spaceName} children`);
|
||||
if (clonedSpace.children?.length) {
|
||||
for (const child of clonedSpace.children) {
|
||||
if (child.disabled) continue;
|
||||
await this.handleSpaceDuplication(
|
||||
child.uuid,
|
||||
child.spaceName,
|
||||
queryRunner,
|
||||
savedSpace,
|
||||
);
|
||||
}
|
||||
}
|
||||
return savedSpace;
|
||||
}
|
||||
private copySpaceSubspaces(
|
||||
newSpace: SpaceEntity,
|
||||
subspaces: SubspaceEntity[],
|
||||
queryRunner: QueryRunner,
|
||||
) {
|
||||
const newSubspaces = [];
|
||||
for (const sub of subspaces) {
|
||||
if (sub.disabled) continue;
|
||||
const clonedSub = structuredClone(sub);
|
||||
delete clonedSub.uuid;
|
||||
const newSubspace = queryRunner.manager.create(SubspaceEntity, {
|
||||
...clonedSub,
|
||||
space: newSpace,
|
||||
productAllocations: [],
|
||||
uuid: uuidV4(),
|
||||
});
|
||||
if (sub.productAllocations?.length) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
for (const { uuid, ...allocation } of sub.productAllocations) {
|
||||
newSubspace.productAllocations.push(
|
||||
queryRunner.manager.create(SubspaceProductAllocationEntity, {
|
||||
...allocation,
|
||||
subspace: newSubspace,
|
||||
uuid: uuidV4(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
newSubspaces.push(newSubspace);
|
||||
}
|
||||
return newSubspaces;
|
||||
}
|
||||
private copySpaceAllocations(
|
||||
newSpace: SpaceEntity,
|
||||
allocations: SpaceProductAllocationEntity[],
|
||||
queryRunner: QueryRunner,
|
||||
) {
|
||||
const newAllocations = [];
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
for (const { uuid, ...allocation } of allocations) {
|
||||
newAllocations.push(
|
||||
queryRunner.manager.create(SpaceProductAllocationEntity, {
|
||||
...allocation,
|
||||
space: newSpace,
|
||||
uuid: uuidV4(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
return newAllocations;
|
||||
}
|
||||
|
||||
private checkDuplicateTags(allocations: CreateProductAllocationDto[]) {
|
||||
const tagUuidSet = new Set<string>();
|
||||
const tagNameProductSet = new Set<string>();
|
||||
|
Reference in New Issue
Block a user