From 5d379e0c366026a7fd7d724a3de9d1788004bb91 Mon Sep 17 00:00:00 2001 From: hannathkadher Date: Tue, 15 Apr 2025 20:01:42 +0400 Subject: [PATCH] export --- libs/common/src/constants/controller-route.ts | 5 ++ package-lock.json | 39 ++++++++++++ package.json | 1 + src/project/controllers/project.controller.ts | 29 ++++++++- src/project/services/project.service.ts | 62 ++++++++++++++++++- 5 files changed, 134 insertions(+), 2 deletions(-) diff --git a/libs/common/src/constants/controller-route.ts b/libs/common/src/constants/controller-route.ts index 1c8685d..c78ff60 100644 --- a/libs/common/src/constants/controller-route.ts +++ b/libs/common/src/constants/controller-route.ts @@ -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 { diff --git a/package-lock.json b/package-lock.json index b0d9130..8ef39df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", @@ -900,6 +901,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", @@ -9354,6 +9368,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", @@ -9372,12 +9392,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", diff --git a/package.json b/package.json index a44084a..a58a02f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/project/controllers/project.controller.ts b/src/project/controllers/project.controller.ts index a39ced1..b43647f 100644 --- a/src/project/controllers/project.controller.ts +++ b/src/project/controllers/project.controller.ts @@ -4,13 +4,20 @@ import { Controller, Delete, Get, + Header, Param, Post, Put, Query, UseGuards, } from '@nestjs/common'; -import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; +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 +93,24 @@ export class ProjectController { async findOne(@Param() params: GetProjectParam): Promise { 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) { + return this.projectService.exportToCsv(params); + } } diff --git a/src/project/services/project.service.ts b/src/project/services/project.service.ts index ad9cdbf..4617726 100644 --- a/src/project/services/project.service.ts +++ b/src/project/services/project.service.ts @@ -1,6 +1,6 @@ import { ProjectRepository } from '@app/common/modules/project/repositiories'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; -import { CreateProjectDto } from '../dto'; +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 +13,8 @@ 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'; @Injectable() export class ProjectService { @@ -213,4 +215,62 @@ export class ProjectService { async validate(name: string): Promise { return await this.projectRepository.exists({ where: { name } }); } + + async exportToCsv(param: GetProjectParam): Promise { + try { + const project = await this.projectRepository.findOne({ + where: { uuid: param.projectUuid }, + relations: [ + 'communities', + 'communities.spaces', + 'communities.spaces.parent', + 'communities.spaces.tags', + ], + }); + + if (!project) { + throw new HttpException( + `Project with UUID ${param.projectUuid} not found`, + HttpStatus.NOT_FOUND, + ); + } + + const stream = new PassThrough(); + const csvStream = format({ headers: true }); + + csvStream.pipe(stream); + + for (const community of project.communities || []) { + for (const space of community.spaces || []) { + const tagNames = space.tags?.map((tag) => tag.tag).join(', ') || ''; + + csvStream.write({ + 'Project Name': project.name, + 'Project UUID': project.uuid, + 'Community Name': community.name, + 'Community UUID': community.uuid, + 'Space Name': space.spaceName, + 'Space UUID': space.uuid, + 'Parent Space UUID': space.parent?.uuid || '', + 'Space Tuya UUID': space.spaceTuyaUuid || '', + X: space.x, + Y: space.y, + Tags: tagNames, + }); + } + } + + csvStream.end(); + return stream; + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + + throw new HttpException( + `Failed to export project structure for UUID ${param.projectUuid}: ${error.message}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } }