mirror of
https://github.com/SyncrowIOT/backend.git
synced 2025-07-15 18:27:05 +00:00
Merge pull request #339 from SyncrowIOT/feat/commission-device
Feat/commission device
This commit is contained in:
@ -43,6 +43,11 @@ export class ControllerRoute {
|
||||
'Get user by uuid in project';
|
||||
public static readonly GET_USER_BY_UUID_IN_PROJECT_DESCRIPTION =
|
||||
'This endpoint retrieves a user by their unique identifier (UUID) associated with a specific project.';
|
||||
|
||||
public static readonly EXPORT_STRUCTURE_CSV_SUMMARY =
|
||||
'Export project with their full structure to a CSV file';
|
||||
public static readonly EXPORT_STRUCTURE_CSV_DESCRIPTION =
|
||||
'This endpoint exports project along with their associated communities, spaces, and nested space hierarchy into a downloadable CSV file. Useful for backups, reports, or audits';
|
||||
};
|
||||
};
|
||||
static PROJECT_USER = class {
|
||||
|
@ -3,6 +3,7 @@ import { RoleType } from './role.type.enum';
|
||||
export const RolePermissions = {
|
||||
[RoleType.SUPER_ADMIN]: [
|
||||
'DEVICE_SINGLE_CONTROL',
|
||||
'COMMISSION_DEVICE',
|
||||
'DEVICE_VIEW',
|
||||
'DEVICE_DELETE',
|
||||
'DEVICE_UPDATE',
|
||||
@ -58,6 +59,7 @@ export const RolePermissions = {
|
||||
'PRODUCT_ADD',
|
||||
],
|
||||
[RoleType.ADMIN]: [
|
||||
'COMMISSION_DEVICE',
|
||||
'DEVICE_SINGLE_CONTROL',
|
||||
'DEVICE_VIEW',
|
||||
'DEVICE_DELETE',
|
||||
@ -127,6 +129,7 @@ export const RolePermissions = {
|
||||
'SCENES_CONTROL',
|
||||
],
|
||||
[RoleType.SPACE_OWNER]: [
|
||||
'COMMISSION_DEVICE',
|
||||
'DEVICE_SINGLE_CONTROL',
|
||||
'DEVICE_VIEW',
|
||||
'DEVICE_DELETE',
|
||||
|
@ -78,7 +78,7 @@ export class DeviceEntity extends AbstractEntity<DeviceDto> {
|
||||
|
||||
@OneToMany(() => NewTagEntity, (tag) => tag.devices)
|
||||
// @JoinTable({ name: 'device_tags' })
|
||||
public tag: NewTagEntity[];
|
||||
public tag: NewTagEntity;
|
||||
|
||||
constructor(partial: Partial<DeviceEntity>) {
|
||||
super();
|
||||
|
@ -8,7 +8,6 @@ import { SpaceLinkEntity } from './space-link.entity';
|
||||
import { SceneEntity } from '../../scene/entities';
|
||||
import { SpaceModelEntity } from '../../space-model';
|
||||
import { InviteUserSpaceEntity } from '../../Invite-user/entities';
|
||||
import { TagEntity } from './tag.entity';
|
||||
import { SpaceProductAllocationEntity } from './space-product-allocation.entity';
|
||||
import { SubspaceEntity } from './subspace/subspace.entity';
|
||||
|
||||
@ -103,9 +102,6 @@ export class SpaceEntity extends AbstractEntity<SpaceDto> {
|
||||
)
|
||||
invitedUsers: InviteUserSpaceEntity[];
|
||||
|
||||
@OneToMany(() => TagEntity, (tag) => tag.space)
|
||||
tags: TagEntity[];
|
||||
|
||||
@OneToMany(
|
||||
() => SpaceProductAllocationEntity,
|
||||
(allocation) => allocation.space,
|
||||
|
@ -3,7 +3,6 @@ import { AbstractEntity } from '../../abstract/entities/abstract.entity';
|
||||
import { ProductEntity } from '../../product/entities';
|
||||
import { TagDto } from '../dtos';
|
||||
import { TagModel } from '../../space-model/entities/tag-model.entity';
|
||||
import { SpaceEntity } from './space.entity';
|
||||
import { DeviceEntity } from '../../device/entities';
|
||||
import { SubspaceEntity } from './subspace/subspace.entity';
|
||||
|
||||
@ -22,9 +21,6 @@ export class TagEntity extends AbstractEntity<TagDto> {
|
||||
})
|
||||
product: ProductEntity;
|
||||
|
||||
@ManyToOne(() => SpaceEntity, (space) => space.tags, { nullable: true })
|
||||
space: SpaceEntity;
|
||||
|
||||
@ManyToOne(() => SubspaceEntity, (subspace) => subspace.tags, {
|
||||
nullable: true,
|
||||
})
|
||||
|
39
package-lock.json
generated
39
package-lock.json
generated
@ -9,6 +9,7 @@
|
||||
"version": "0.0.1",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@fast-csv/format": "^5.0.2",
|
||||
"@nestjs/common": "^10.0.0",
|
||||
"@nestjs/config": "^3.2.0",
|
||||
"@nestjs/core": "^10.0.0",
|
||||
@ -913,6 +914,19 @@
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fast-csv/format": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-5.0.2.tgz",
|
||||
"integrity": "sha512-fRYcWvI8vs0Zxa/8fXd/QlmQYWWkJqKZPAXM+vksnplb3owQFKTPPh9JqOtD0L3flQw/AZjjXdPkD7Kp/uHm8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash.escaperegexp": "^4.1.2",
|
||||
"lodash.isboolean": "^3.0.3",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"lodash.isfunction": "^3.0.9",
|
||||
"lodash.isnil": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@firebase/analytics": {
|
||||
"version": "0.10.8",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.8.tgz",
|
||||
@ -9441,6 +9455,12 @@
|
||||
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.escaperegexp": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
|
||||
"integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.includes": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||
@ -9459,12 +9479,31 @@
|
||||
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isequal": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
|
||||
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isfunction": {
|
||||
"version": "3.0.9",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz",
|
||||
"integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isinteger": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isnil": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz",
|
||||
"integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isnumber": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||
|
@ -20,6 +20,7 @@
|
||||
"test:e2e": "jest --config ./apps/backend/test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fast-csv/format": "^5.0.2",
|
||||
"@nestjs/common": "^10.0.0",
|
||||
"@nestjs/config": "^3.2.0",
|
||||
"@nestjs/core": "^10.0.0",
|
||||
|
@ -19,6 +19,8 @@ import {
|
||||
SceneRepository,
|
||||
} from '@app/common/modules/scene/repositories';
|
||||
import { AutomationRepository } from '@app/common/modules/automation/repositories';
|
||||
import { CommunityRepository } from '@app/common/modules/community/repositories';
|
||||
import { SubspaceRepository } from '@app/common/modules/space/repositories/subspace.repository';
|
||||
|
||||
@Module({
|
||||
imports: [ConfigModule, SpaceRepositoryModule],
|
||||
@ -39,6 +41,8 @@ import { AutomationRepository } from '@app/common/modules/automation/repositorie
|
||||
SceneIconRepository,
|
||||
SceneRepository,
|
||||
AutomationRepository,
|
||||
CommunityRepository,
|
||||
SubspaceRepository,
|
||||
],
|
||||
exports: [],
|
||||
})
|
||||
|
@ -22,9 +22,10 @@ import { ControllerRoute } from '@app/common/constants/controller-route';
|
||||
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
|
||||
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
|
||||
import { CommissionDeviceCsvDto } from '../dto';
|
||||
import { CommunityParam } from '@app/common/dto/community-space.param';
|
||||
import { DeviceCommissionService } from '../services';
|
||||
import { ProjectParam } from '@app/common/dto/project-param.dto';
|
||||
import { Permissions } from 'src/decorators/permissions.decorator';
|
||||
import { PermissionsGuard } from 'src/guards/permissions.guard';
|
||||
|
||||
@ApiTags('Commission Devices Module')
|
||||
@Controller({
|
||||
@ -34,6 +35,8 @@ import { ProjectParam } from '@app/common/dto/project-param.dto';
|
||||
export class DeviceCommissionController {
|
||||
constructor(private readonly commissionService: DeviceCommissionService) {}
|
||||
|
||||
@UseGuards(PermissionsGuard)
|
||||
@Permissions('COMMISSION_DEVICE')
|
||||
@ApiBearerAuth()
|
||||
@Post()
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@ -65,7 +68,7 @@ export class DeviceCommissionController {
|
||||
@Param() param: ProjectParam,
|
||||
@Req() req: any,
|
||||
): Promise<BaseResponseDto> {
|
||||
await this.commissionService.processCsv(file.path);
|
||||
await this.commissionService.processCsv(param, file.path);
|
||||
return {
|
||||
message: 'CSV file received and processing started',
|
||||
success: true,
|
||||
|
@ -2,44 +2,185 @@ import * as fs from 'fs';
|
||||
import * as csv from 'csv-parser';
|
||||
|
||||
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { DeviceService } from 'src/device/services';
|
||||
import { CommunityRepository } from '@app/common/modules/community/repositories';
|
||||
import { SpaceRepository } from '@app/common/modules/space';
|
||||
import { SubspaceRepository } from '@app/common/modules/space/repositories/subspace.repository';
|
||||
import { SubspaceEntity } from '@app/common/modules/space/entities/subspace/subspace.entity';
|
||||
import { DeviceRepository } from '@app/common/modules/device/repositories';
|
||||
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
|
||||
import { ProjectParam } from '@app/common/dto/project-param.dto';
|
||||
import { ProjectRepository } from '@app/common/modules/project/repositiories';
|
||||
|
||||
@Injectable()
|
||||
export class DeviceCommissionService {
|
||||
constructor(
|
||||
private readonly tuyaService: TuyaService,
|
||||
private readonly deviceService: DeviceService,
|
||||
private readonly communityRepository: CommunityRepository,
|
||||
private readonly spaceRepository: SpaceRepository,
|
||||
private readonly subspaceRepository: SubspaceRepository,
|
||||
private readonly deviceRepository: DeviceRepository,
|
||||
private readonly projectRepository: ProjectRepository,
|
||||
) {}
|
||||
|
||||
async processCsv(filePath: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const results = [];
|
||||
async processCsv(param: ProjectParam, filePath: string) {
|
||||
const successCount = { value: 0 };
|
||||
const failureCount = { value: 0 };
|
||||
|
||||
const projectId = param.projectUuid;
|
||||
|
||||
const project = await this.projectRepository.findOne({
|
||||
where: { uuid: projectId },
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
throw new HttpException('Project not found', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
const rows: any[] = [];
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
fs.createReadStream(filePath)
|
||||
.pipe(csv())
|
||||
.on('data', async (row) => {
|
||||
console.log(`Device: ${JSON.stringify(row)}`);
|
||||
const deviceId = row.deviceId?.trim();
|
||||
.on('data', (row) => rows.push(row))
|
||||
.on('end', () => resolve())
|
||||
.on('error', (error) => reject(error));
|
||||
});
|
||||
|
||||
if (!deviceId) {
|
||||
console.error('Missing deviceId or deviceName in row:', row);
|
||||
return;
|
||||
} else {
|
||||
const device = await this.tuyaService.getDeviceDetails(
|
||||
row.deviceId,
|
||||
for (const row of rows) {
|
||||
await this.processCsvRow(param, row, successCount, failureCount);
|
||||
}
|
||||
|
||||
return new SuccessResponseDto({
|
||||
message: `Successfully processed CSV file`,
|
||||
data: {
|
||||
successCount: successCount.value,
|
||||
failureCount: failureCount.value,
|
||||
},
|
||||
statusCode: HttpStatus.ACCEPTED,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new HttpException(
|
||||
'Failed to process CSV file',
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
console.log(device);
|
||||
}
|
||||
})
|
||||
.on('end', () => {
|
||||
console.log(`Finished processing ${results.length} devices.`);
|
||||
resolve();
|
||||
})
|
||||
.on('error', (error) => {
|
||||
console.error('Error reading CSV', error);
|
||||
reject(error);
|
||||
}
|
||||
|
||||
private async processCsvRow(
|
||||
param: ProjectParam,
|
||||
row: any,
|
||||
successCount: { value: number },
|
||||
failureCount: { value: number },
|
||||
) {
|
||||
try {
|
||||
const rawDeviceId = row['Device ID']?.trim();
|
||||
const communityId = row['Community UUID']?.trim();
|
||||
const spaceId = row['Space UUID']?.trim();
|
||||
const subspaceId = row['Subspace UUID']?.trim();
|
||||
const tagName = row['Tag']?.trim();
|
||||
const productName = row['Product Name']?.trim();
|
||||
const projectId = param.projectUuid;
|
||||
|
||||
if (!rawDeviceId) {
|
||||
console.error('Missing Device ID in row:', row);
|
||||
failureCount.value++;
|
||||
return;
|
||||
}
|
||||
|
||||
const device = await this.tuyaService.getDeviceDetails(rawDeviceId);
|
||||
if (!device) {
|
||||
console.error(`Device not found for Device ID: ${rawDeviceId}`);
|
||||
failureCount.value++;
|
||||
return;
|
||||
}
|
||||
|
||||
const community = await this.communityRepository.findOne({
|
||||
where: { uuid: communityId, project: { uuid: projectId } },
|
||||
});
|
||||
|
||||
if (!community) {
|
||||
console.error(`Community not found: ${communityId}`);
|
||||
failureCount.value++;
|
||||
return;
|
||||
}
|
||||
|
||||
const tuyaSpaceId = community.externalId;
|
||||
|
||||
const space = await this.spaceRepository.findOne({
|
||||
where: { uuid: spaceId },
|
||||
relations: [
|
||||
'productAllocations',
|
||||
'productAllocations.tags',
|
||||
'productAllocations.product',
|
||||
],
|
||||
});
|
||||
|
||||
if (!space) {
|
||||
console.error(`Space not found: ${spaceId}`);
|
||||
failureCount.value++;
|
||||
return;
|
||||
}
|
||||
|
||||
let subspace: SubspaceEntity | null = null;
|
||||
if (subspaceId?.trim()) {
|
||||
subspace = await this.subspaceRepository.findOne({
|
||||
where: { uuid: subspaceId },
|
||||
relations: [
|
||||
'productAllocations',
|
||||
'productAllocations.tags',
|
||||
'productAllocations.product',
|
||||
],
|
||||
});
|
||||
|
||||
if (!subspace) {
|
||||
console.error(`Subspace not found: ${subspaceId}`);
|
||||
failureCount.value++;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const allocations =
|
||||
subspace?.productAllocations || space.productAllocations;
|
||||
|
||||
const match = allocations
|
||||
.flatMap((pa) =>
|
||||
(pa.tags || []).map((tag) => ({ product: pa.product, tag })),
|
||||
)
|
||||
.find(({ tag }) => tag.name === tagName);
|
||||
|
||||
if (!match) {
|
||||
console.error(`No matching tag found for Device ID: ${rawDeviceId}`);
|
||||
failureCount.value++;
|
||||
return;
|
||||
}
|
||||
|
||||
if (match.product.name !== productName) {
|
||||
console.error(`Product name mismatch for Device ID: ${rawDeviceId}`);
|
||||
failureCount.value++;
|
||||
return;
|
||||
}
|
||||
|
||||
const middlewareDevice = this.deviceRepository.create({
|
||||
deviceTuyaUuid: rawDeviceId,
|
||||
isActive: true,
|
||||
spaceDevice: space,
|
||||
subspace: subspace || null,
|
||||
productDevice: match.product,
|
||||
tag: match.tag,
|
||||
});
|
||||
|
||||
await this.deviceRepository.save(middlewareDevice);
|
||||
|
||||
await this.deviceService.transferDeviceInSpacesTuya(
|
||||
rawDeviceId,
|
||||
tuyaSpaceId,
|
||||
);
|
||||
successCount.value++;
|
||||
} catch (err) {
|
||||
failureCount.value++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,13 +4,23 @@ import {
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Header,
|
||||
HttpStatus,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
Res,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { Response } from 'express';
|
||||
import {
|
||||
ApiBearerAuth,
|
||||
ApiOperation,
|
||||
ApiProduces,
|
||||
ApiResponse,
|
||||
ApiTags,
|
||||
} from '@nestjs/swagger';
|
||||
import { ProjectService } from '../services';
|
||||
import { CreateProjectDto, GetProjectParam } from '../dto';
|
||||
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
|
||||
@ -86,4 +96,34 @@ export class ProjectController {
|
||||
async findOne(@Param() params: GetProjectParam): Promise<BaseResponseDto> {
|
||||
return this.projectService.getProject(params.projectUuid);
|
||||
}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiOperation({
|
||||
summary: ControllerRoute.PROJECT.ACTIONS.EXPORT_STRUCTURE_CSV_SUMMARY,
|
||||
description:
|
||||
ControllerRoute.PROJECT.ACTIONS.EXPORT_STRUCTURE_CSV_DESCRIPTION,
|
||||
})
|
||||
@Get(':projectUuid/structure/export-csv')
|
||||
@ApiProduces('text/csv')
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description:
|
||||
'A CSV file containing project details and their structural hierarchy (Project, Community, Space, Parent Space).',
|
||||
})
|
||||
@Header('Content-Type', 'text/csv')
|
||||
@Header('Content-Disposition', 'attachment; filename=project-structure.csv')
|
||||
async exportStructures(
|
||||
@Param() params: GetProjectParam,
|
||||
@Res() res: Response,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const csvStream = await this.projectService.exportToCsv(params);
|
||||
csvStream.pipe(res as unknown as NodeJS.WritableStream);
|
||||
} catch (error) {
|
||||
res
|
||||
.status(error.status || HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.json({ message: error.message || 'Failed to generate CSV file.' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,13 @@ import { ProjectController } from './controllers';
|
||||
import { ProjectService } from './services';
|
||||
import { ProjectRepository } from '@app/common/modules/project/repositiories';
|
||||
import { CreateOrphanSpaceHandler } from './handler';
|
||||
import { SpaceRepository } from '@app/common/modules/space';
|
||||
import {
|
||||
InviteSpaceRepository,
|
||||
SpaceLinkRepository,
|
||||
SpaceProductAllocationRepository,
|
||||
SpaceRepository,
|
||||
TagRepository,
|
||||
} from '@app/common/modules/space';
|
||||
import { CommunityRepository } from '@app/common/modules/community/repositories';
|
||||
import { InviteUserRepository } from '@app/common/modules/Invite-user/repositiories';
|
||||
import { ProjectUserController } from './controllers/project-user.controller';
|
||||
@ -13,6 +19,47 @@ import {
|
||||
UserRepository,
|
||||
UserSpaceRepository,
|
||||
} from '@app/common/modules/user/repositories';
|
||||
import {
|
||||
SpaceLinkService,
|
||||
SpaceService,
|
||||
SubspaceDeviceService,
|
||||
SubSpaceService,
|
||||
ValidationService,
|
||||
} from 'src/space/services';
|
||||
import { TagService } from 'src/tags/services';
|
||||
import {
|
||||
SpaceModelService,
|
||||
SubSpaceModelService,
|
||||
} from 'src/space-model/services';
|
||||
import { DeviceService } from 'src/device/services';
|
||||
import { SpaceProductAllocationService } from 'src/space/services/space-product-allocation.service';
|
||||
import {
|
||||
SubspaceProductAllocationRepository,
|
||||
SubspaceRepository,
|
||||
} from '@app/common/modules/space/repositories/subspace.repository';
|
||||
import { SubspaceProductAllocationService } from 'src/space/services/subspace/subspace-product-allocation.service';
|
||||
import { CommunityService } from 'src/community/services';
|
||||
import {
|
||||
SpaceModelProductAllocationRepoitory,
|
||||
SpaceModelRepository,
|
||||
SubspaceModelProductAllocationRepoitory,
|
||||
SubspaceModelRepository,
|
||||
} from '@app/common/modules/space-model';
|
||||
import { DeviceRepository } from '@app/common/modules/device/repositories';
|
||||
import { NewTagRepository } from '@app/common/modules/tag/repositories/tag-repository';
|
||||
import { ProductRepository } from '@app/common/modules/product/repositories';
|
||||
import { SpaceModelProductAllocationService } from 'src/space-model/services/space-model-product-allocation.service';
|
||||
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
|
||||
import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service';
|
||||
import { SceneService } from 'src/scene/services';
|
||||
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
|
||||
import { SubspaceModelProductAllocationService } from 'src/space-model/services/subspace/subspace-model-product-allocation.service';
|
||||
import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories';
|
||||
import {
|
||||
SceneIconRepository,
|
||||
SceneRepository,
|
||||
} from '@app/common/modules/scene/repositories';
|
||||
import { AutomationRepository } from '@app/common/modules/automation/repositories';
|
||||
|
||||
const CommandHandlers = [CreateOrphanSpaceHandler];
|
||||
|
||||
@ -30,6 +77,41 @@ const CommandHandlers = [CreateOrphanSpaceHandler];
|
||||
InviteUserRepository,
|
||||
UserSpaceRepository,
|
||||
UserRepository,
|
||||
SpaceService,
|
||||
InviteSpaceRepository,
|
||||
SpaceLinkService,
|
||||
SubSpaceService,
|
||||
ValidationService,
|
||||
TagService,
|
||||
SpaceModelService,
|
||||
DeviceService,
|
||||
SpaceProductAllocationService,
|
||||
SpaceLinkRepository,
|
||||
SubspaceRepository,
|
||||
SubspaceDeviceService,
|
||||
SubspaceProductAllocationService,
|
||||
CommunityService,
|
||||
SpaceModelRepository,
|
||||
DeviceRepository,
|
||||
NewTagRepository,
|
||||
ProductRepository,
|
||||
SubSpaceModelService,
|
||||
SpaceModelProductAllocationService,
|
||||
SpaceProductAllocationRepository,
|
||||
SubspaceProductAllocationRepository,
|
||||
SceneDeviceRepository,
|
||||
DeviceStatusFirebaseService,
|
||||
SceneService,
|
||||
TuyaService,
|
||||
TagRepository,
|
||||
SubspaceModelRepository,
|
||||
SubspaceModelProductAllocationService,
|
||||
SpaceModelProductAllocationRepoitory,
|
||||
DeviceStatusLogRepository,
|
||||
SceneIconRepository,
|
||||
SceneRepository,
|
||||
AutomationRepository,
|
||||
SubspaceModelProductAllocationRepoitory,
|
||||
],
|
||||
exports: [ProjectService, CqrsModule],
|
||||
})
|
||||
|
@ -1,6 +1,12 @@
|
||||
import { ProjectRepository } from '@app/common/modules/project/repositiories';
|
||||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { CreateProjectDto } from '../dto';
|
||||
import {
|
||||
forwardRef,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Inject,
|
||||
Injectable,
|
||||
} from '@nestjs/common';
|
||||
import { CreateProjectDto, GetProjectParam } from '../dto';
|
||||
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
|
||||
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
|
||||
import { ProjectEntity } from '@app/common/modules/project/entities';
|
||||
@ -13,6 +19,11 @@ import { PageResponse } from '@app/common/dto/pagination.response.dto';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import { CreateOrphanSpaceCommand } from '../command/create-orphan-space-command';
|
||||
import { UserRepository } from '@app/common/modules/user/repositories';
|
||||
import { format } from '@fast-csv/format';
|
||||
import { PassThrough } from 'stream';
|
||||
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
|
||||
import { SpaceService } from 'src/space/services';
|
||||
import { ORPHAN_COMMUNITY_NAME } from '@app/common/constants/orphan-constant';
|
||||
|
||||
@Injectable()
|
||||
export class ProjectService {
|
||||
@ -20,6 +31,8 @@ export class ProjectService {
|
||||
private readonly projectRepository: ProjectRepository,
|
||||
private readonly userRepository: UserRepository,
|
||||
private commandBus: CommandBus,
|
||||
@Inject(forwardRef(() => SpaceService))
|
||||
private readonly spaceService: SpaceService,
|
||||
) {}
|
||||
|
||||
async createProject(
|
||||
@ -213,4 +226,129 @@ export class ProjectService {
|
||||
async validate(name: string): Promise<boolean> {
|
||||
return await this.projectRepository.exists({ where: { name } });
|
||||
}
|
||||
|
||||
async exportToCsv(param: GetProjectParam): Promise<PassThrough> {
|
||||
const project = await this.projectRepository.findOne({
|
||||
where: { uuid: param.projectUuid },
|
||||
relations: [
|
||||
'communities',
|
||||
'communities.spaces',
|
||||
'communities.spaces.parent',
|
||||
'communities.spaces.productAllocations',
|
||||
'communities.spaces.productAllocations.product',
|
||||
'communities.spaces.productAllocations.tags',
|
||||
'communities.spaces.subspaces',
|
||||
'communities.spaces.subspaces.productAllocations',
|
||||
'communities.spaces.subspaces.productAllocations.product',
|
||||
'communities.spaces.subspaces.productAllocations.tags',
|
||||
],
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
throw new HttpException(
|
||||
`Project with UUID ${param.projectUuid} not found`,
|
||||
HttpStatus.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const stream = new PassThrough();
|
||||
const csvStream = format({
|
||||
headers: [
|
||||
'Device ID',
|
||||
'Community Name',
|
||||
'Space Name',
|
||||
'Space Location',
|
||||
'Subspace Name',
|
||||
'Tag',
|
||||
'Product Name',
|
||||
'Community UUID',
|
||||
'Space UUID',
|
||||
'Subspace UUID',
|
||||
],
|
||||
});
|
||||
|
||||
csvStream.pipe(stream);
|
||||
|
||||
const allSpaces: SpaceEntity[] = [];
|
||||
for (const community of project.communities) {
|
||||
if (community.name === `${ORPHAN_COMMUNITY_NAME}-${project.name}`)
|
||||
continue;
|
||||
for (const space of community.spaces) {
|
||||
if (!space.disabled) {
|
||||
space.community = community;
|
||||
allSpaces.push(space);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const spaceMap = new Map<string, SpaceEntity>();
|
||||
for (const space of allSpaces) {
|
||||
spaceMap.set(space.uuid, space);
|
||||
}
|
||||
|
||||
function buildSpaceLocation(space: SpaceEntity): string {
|
||||
const path: string[] = [];
|
||||
let current = space.parent;
|
||||
while (current) {
|
||||
path.unshift(current.spaceName);
|
||||
current = current.parent ?? spaceMap.get(current.uuid)?.parent ?? null;
|
||||
}
|
||||
return path.join(' > ');
|
||||
}
|
||||
|
||||
for (const space of allSpaces) {
|
||||
const spaceLocation = buildSpaceLocation(space);
|
||||
|
||||
for (const subspace of space.subspaces || []) {
|
||||
if (subspace.disabled) continue;
|
||||
|
||||
for (const productAllocation of subspace.productAllocations || []) {
|
||||
for (const tag of productAllocation.tags || []) {
|
||||
csvStream.write({
|
||||
'Device ID': '',
|
||||
'Community Name': space.community?.name || '',
|
||||
'Space Name': space.spaceName,
|
||||
'Space Location': spaceLocation,
|
||||
'Subspace Name': subspace.subspaceName || '',
|
||||
Tag: tag.name,
|
||||
'Product Name': productAllocation.product.name || '',
|
||||
'Community UUID': space.community?.uuid || '',
|
||||
'Space UUID': space.uuid,
|
||||
'Subspace UUID': subspace.uuid,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const productAllocation of space.productAllocations || []) {
|
||||
for (const tag of productAllocation.tags || []) {
|
||||
csvStream.write({
|
||||
'Device ID': '',
|
||||
'Community Name': space.community?.name || '',
|
||||
'Space Name': space.spaceName,
|
||||
'Space Location': spaceLocation,
|
||||
'Subspace Name': '',
|
||||
Tag: tag.name,
|
||||
'Product Name': productAllocation.product.name || '',
|
||||
'Community UUID': space.community?.uuid || '',
|
||||
'Space UUID': space.uuid,
|
||||
'Subspace UUID': '',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
csvStream.end();
|
||||
return stream;
|
||||
}
|
||||
|
||||
getSpaceLocation(space: SpaceEntity): string {
|
||||
const names = [];
|
||||
let current = space.parent;
|
||||
while (current) {
|
||||
names.unshift(current.spaceName);
|
||||
current = current.parent;
|
||||
}
|
||||
return names.join(' > ');
|
||||
}
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ export class DisableSpaceHandler
|
||||
}
|
||||
}
|
||||
|
||||
const tagUuids = space.tags?.map((tag) => tag.uuid) || [];
|
||||
const tagUuids = space.productAllocations?.map((tag) => tag.uuid) || [];
|
||||
/* const subspaceDtos =
|
||||
space.subspaces?.map((subspace) => ({
|
||||
subspaceUuid: subspace.uuid,
|
||||
|
@ -582,8 +582,8 @@ export class SpaceService {
|
||||
queryRunner: QueryRunner,
|
||||
): Promise<void> {
|
||||
try {
|
||||
if (space.subspaces || space.tags) {
|
||||
if (space.tags) {
|
||||
if (space.subspaces || space.productAllocations) {
|
||||
if (space.productAllocations) {
|
||||
await this.spaceProductAllocationService.unlinkModels(
|
||||
space,
|
||||
queryRunner,
|
||||
|
@ -18,7 +18,6 @@ import { In, QueryRunner } from 'typeorm';
|
||||
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 { 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';
|
||||
@ -34,7 +33,6 @@ export class SubSpaceService {
|
||||
constructor(
|
||||
private readonly subspaceRepository: SubspaceRepository,
|
||||
private readonly validationService: ValidationService,
|
||||
private readonly tagService: TagService,
|
||||
private readonly newTagService: NewTagService,
|
||||
public readonly deviceService: SubspaceDeviceService,
|
||||
private readonly subspaceProductAllocationService: SubspaceProductAllocationService,
|
||||
@ -481,21 +479,6 @@ export class SubSpaceService {
|
||||
{ disabled: true },
|
||||
);
|
||||
|
||||
if (subspace.tags?.length) {
|
||||
const modifyTagDtos: ProcessTagDto[] = subspace.tags.map((tag) => ({
|
||||
uuid: tag.uuid,
|
||||
action: ModifyAction.ADD,
|
||||
name: tag.tag,
|
||||
productUuid: tag.product.uuid,
|
||||
}));
|
||||
await this.tagService.moveTags(
|
||||
modifyTagDtos,
|
||||
queryRunner,
|
||||
subspace.space,
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
if (subspace.devices.length > 0) {
|
||||
await this.deviceService.deleteSubspaceDevices(
|
||||
subspace.devices,
|
||||
|
@ -118,7 +118,7 @@ export class TagService {
|
||||
await queryRunner.manager.update(
|
||||
this.tagRepository.target,
|
||||
{ uuid: tag.uuid },
|
||||
{ subspace, space: null },
|
||||
{ subspace },
|
||||
);
|
||||
tag.subspace = subspace;
|
||||
}
|
||||
@ -127,10 +127,9 @@ export class TagService {
|
||||
await queryRunner.manager.update(
|
||||
this.tagRepository.target,
|
||||
{ uuid: tag.uuid },
|
||||
{ subspace: null, space: space },
|
||||
{ subspace: null },
|
||||
);
|
||||
tag.subspace = null;
|
||||
tag.space = space;
|
||||
}
|
||||
|
||||
return tag;
|
||||
@ -367,7 +366,6 @@ export class TagService {
|
||||
where: [
|
||||
{
|
||||
tag,
|
||||
space: { uuid: spaceUuid },
|
||||
product: { uuid: productUuid },
|
||||
disabled: false,
|
||||
},
|
||||
|
Reference in New Issue
Block a user