This commit is contained in:
hannathkadher
2025-04-15 20:01:42 +04:00
parent 9f3dce535d
commit 5d379e0c36
5 changed files with 134 additions and 2 deletions

View File

@ -43,6 +43,11 @@ export class ControllerRoute {
'Get user by uuid in project'; 'Get user by uuid in project';
public static readonly GET_USER_BY_UUID_IN_PROJECT_DESCRIPTION = 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.'; '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 { static PROJECT_USER = class {

39
package-lock.json generated
View File

@ -9,6 +9,7 @@
"version": "0.0.1", "version": "0.0.1",
"license": "UNLICENSED", "license": "UNLICENSED",
"dependencies": { "dependencies": {
"@fast-csv/format": "^5.0.2",
"@nestjs/common": "^10.0.0", "@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.2.0", "@nestjs/config": "^3.2.0",
"@nestjs/core": "^10.0.0", "@nestjs/core": "^10.0.0",
@ -900,6 +901,19 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0" "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": { "node_modules/@firebase/analytics": {
"version": "0.10.8", "version": "0.10.8",
"resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.8.tgz", "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.8.tgz",
@ -9354,6 +9368,12 @@
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
"license": "MIT" "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": { "node_modules/lodash.includes": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
@ -9372,12 +9392,31 @@
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
"license": "MIT" "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": { "node_modules/lodash.isinteger": {
"version": "4.0.4", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
"license": "MIT" "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": { "node_modules/lodash.isnumber": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",

View File

@ -20,6 +20,7 @@
"test:e2e": "jest --config ./apps/backend/test/jest-e2e.json" "test:e2e": "jest --config ./apps/backend/test/jest-e2e.json"
}, },
"dependencies": { "dependencies": {
"@fast-csv/format": "^5.0.2",
"@nestjs/common": "^10.0.0", "@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.2.0", "@nestjs/config": "^3.2.0",
"@nestjs/core": "^10.0.0", "@nestjs/core": "^10.0.0",

View File

@ -4,13 +4,20 @@ import {
Controller, Controller,
Delete, Delete,
Get, Get,
Header,
Param, Param,
Post, Post,
Put, Put,
Query, Query,
UseGuards, UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; import {
ApiBearerAuth,
ApiOperation,
ApiProduces,
ApiResponse,
ApiTags,
} from '@nestjs/swagger';
import { ProjectService } from '../services'; import { ProjectService } from '../services';
import { CreateProjectDto, GetProjectParam } from '../dto'; import { CreateProjectDto, GetProjectParam } from '../dto';
import { BaseResponseDto } from '@app/common/dto/base.response.dto'; import { BaseResponseDto } from '@app/common/dto/base.response.dto';
@ -86,4 +93,24 @@ export class ProjectController {
async findOne(@Param() params: GetProjectParam): Promise<BaseResponseDto> { async findOne(@Param() params: GetProjectParam): Promise<BaseResponseDto> {
return this.projectService.getProject(params.projectUuid); 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);
}
} }

View File

@ -1,6 +1,6 @@
import { ProjectRepository } from '@app/common/modules/project/repositiories'; import { ProjectRepository } from '@app/common/modules/project/repositiories';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; 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 { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { ProjectEntity } from '@app/common/modules/project/entities'; 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 { CommandBus } from '@nestjs/cqrs';
import { CreateOrphanSpaceCommand } from '../command/create-orphan-space-command'; import { CreateOrphanSpaceCommand } from '../command/create-orphan-space-command';
import { UserRepository } from '@app/common/modules/user/repositories'; import { UserRepository } from '@app/common/modules/user/repositories';
import { format } from '@fast-csv/format';
import { PassThrough } from 'stream';
@Injectable() @Injectable()
export class ProjectService { export class ProjectService {
@ -213,4 +215,62 @@ export class ProjectService {
async validate(name: string): Promise<boolean> { async validate(name: string): Promise<boolean> {
return await this.projectRepository.exists({ where: { name } }); return await this.projectRepository.exists({ where: { name } });
} }
async exportToCsv(param: GetProjectParam): Promise<PassThrough> {
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,
);
}
}
} }