From 2c924bc79dea0726f02606d00fe336dd27ebbd01 Mon Sep 17 00:00:00 2001 From: Stefanos Hadjipetrou Date: Sun, 10 May 2026 23:11:17 +0300 Subject: [PATCH 1/2] feat: initial folder system implementation --- .../report-builder/folder.controller.ts | 114 +++++++++++++++++- .../report-builder/report.controller.ts | 88 +++++++------- 2 files changed, 157 insertions(+), 45 deletions(-) diff --git a/src/controllers/report-builder/folder.controller.ts b/src/controllers/report-builder/folder.controller.ts index 8c66203..f2451d7 100644 --- a/src/controllers/report-builder/folder.controller.ts +++ b/src/controllers/report-builder/folder.controller.ts @@ -67,12 +67,40 @@ export class FolderController { // @authenticate({strategy: 'auth0-jwt', options: {scopes: ['greet']}}) async find( @param.filter(FolderModel) filter?: Filter, + @param.query.string('includeSubFolders') includeSubFolders?: string, ): Promise { const userId = _.get(this.req, 'user.sub', 'anonymous'); this.logger.info( `FolderController - find - Fetching folders for user ${userId}`, ); - return this.folderService.find(userId, filter); + if (Boolean(includeSubFolders)) { + return this.folderService.getAllFoldersWithSubfolders(userId, filter); + } else { + return this.folderService.find(userId, filter); + } + } + + @get('/folders-structure') + @response(200, { + description: 'Array of FolderModel instances', + content: { + 'application/json': { + schema: { + type: 'array', + items: getModelSchemaRef(FolderModel, {includeRelations: true}), + }, + }, + }, + }) + // @authenticate({strategy: 'auth0-jwt', options: {scopes: ['greet']}}) + async getFoldersStructure( + @param.filter(FolderModel) filter?: Filter, + ): Promise { + const userId = _.get(this.req, 'user.sub', 'anonymous'); + this.logger.info( + `FolderController - getFoldersStructure - Fetching folder structure for user ${userId}`, + ); + return this.folderService.getFolderTree(userId, filter); } @get('/folder/{id}') @@ -173,4 +201,88 @@ export class FolderController { ); return this.folderService.duplicate(userId, id); } + + @get('/folder/add-asset/{id}') + @response(200, { + description: 'FolderModel instance', + content: { + 'application/json': { + schema: getModelSchemaRef(FolderModel, {includeRelations: true}), + }, + }, + }) + // @authenticate({strategy: 'auth0-jwt', options: {scopes: ['greet']}}) + async addAssetToFolder( + @param.path.string('id') id: string, + @param.query.string('assetId') assetId: string, + ): Promise { + const userId = _.get(this.req, 'user.sub', 'anonymous'); + this.logger.info( + `FolderController - addAssetToFolder - Adding asset ${assetId} to folder ${id} for user ${userId}`, + ); + return this.folderService.addAsset(userId, id, assetId); + } + + @get('/folder/remove-asset/{id}') + @response(200, { + description: 'FolderModel instance', + content: { + 'application/json': { + schema: getModelSchemaRef(FolderModel, {includeRelations: true}), + }, + }, + }) + // @authenticate({strategy: 'auth0-jwt', options: {scopes: ['greet']}}) + async removeAssetFromFolder( + @param.path.string('id') id: string, + @param.query.string('assetId') assetId: string, + ): Promise { + const userId = _.get(this.req, 'user.sub', 'anonymous'); + this.logger.info( + `FolderController - removeAssetFromFolder - Removing asset ${assetId} from folder ${id} for user ${userId}`, + ); + return this.folderService.removeAsset(userId, id, assetId); + } + + @get('/folder/add-report/{id}') + @response(200, { + description: 'FolderModel instance', + content: { + 'application/json': { + schema: getModelSchemaRef(FolderModel, {includeRelations: true}), + }, + }, + }) + // @authenticate({strategy: 'auth0-jwt', options: {scopes: ['greet']}}) + async addReportToFolder( + @param.path.string('id') id: string, + @param.query.string('reportId') reportId: string, + ): Promise { + const userId = _.get(this.req, 'user.sub', 'anonymous'); + this.logger.info( + `FolderController - addReportToFolder - Adding report ${reportId} to folder ${id} for user ${userId}`, + ); + return this.folderService.addReport(userId, id, reportId); + } + + @get('/folder/remove-report/{id}') + @response(200, { + description: 'FolderModel instance', + content: { + 'application/json': { + schema: getModelSchemaRef(FolderModel, {includeRelations: true}), + }, + }, + }) + // @authenticate({strategy: 'auth0-jwt', options: {scopes: ['greet']}}) + async removeReportFromFolder( + @param.path.string('id') id: string, + @param.query.string('reportId') reportId: string, + ): Promise { + const userId = _.get(this.req, 'user.sub', 'anonymous'); + this.logger.info( + `FolderController - removeReportFromFolder - Removing report ${reportId} from folder ${id} for user ${userId}`, + ); + return this.folderService.removeReport(userId, id, reportId); + } } diff --git a/src/controllers/report-builder/report.controller.ts b/src/controllers/report-builder/report.controller.ts index 6cea378..0a301e2 100644 --- a/src/controllers/report-builder/report.controller.ts +++ b/src/controllers/report-builder/report.controller.ts @@ -17,8 +17,8 @@ import { import axios, {AxiosResponse} from 'axios'; import fs from 'fs/promises'; import _ from 'lodash'; -import {ReportModel} from 'rb-core-middleware/dist/models'; -import {ReportService} from 'rb-core-middleware/dist/services'; +import {FolderModel, ReportModel} from 'rb-core-middleware/dist/models'; +import {FolderService, ReportService} from 'rb-core-middleware/dist/services'; import {Logger} from 'winston'; import {handleDataApiError} from '../../utils/dataApiError'; import {renderChartData} from '../../utils/renderChart'; @@ -28,6 +28,7 @@ export class ReportController { @inject(RestBindings.Http.REQUEST) private req: Request, @inject('services.logger') private logger: Logger, @inject('services.ReportService') private reportService: ReportService, + @inject('services.FolderService') private folderService: FolderService, ) {} @post('/report/render-chart-data') @@ -40,47 +41,6 @@ export class ReportController { } } - @get('/report/dummy') - @response(200) - async dummy() { - this.logger.info('ReportController - dummy - Dummy endpoint called'); - return this.reportService.create('dummy-user', { - name: 'Dummy Report', - nameLower: 'dummy report', - description: 'This is a dummy report', - items: [], - public: false, - baseline: false, - owner: 'dummy-user', - updatedDate: new Date().toISOString(), - createdDate: new Date().toISOString(), - settings: { - width: 800, - height: 600, - paddingLeft: 10, - paddingTop: 10, - paddingRight: 10, - paddingBottom: 10, - stroke: 0, - strokeColor: '#000000', - backgroundColor: '#FFFFFF', - borderRadius: 0, - }, - getId: function () { - return ''; - }, - getIdObject: function () { - return {}; - }, - toJSON: function () { - return {}; - }, - toObject: function () { - return {}; - }, - }); - } - @post('/report') @response(200, { description: 'ReportModel instance', @@ -136,11 +96,51 @@ export class ReportController { // @authenticate({strategy: 'auth0-jwt', options: {scopes: ['greet']}}) async find( @param.filter(ReportModel) filter?: Filter, - ): Promise { + @param.query.string('includeFolders') includeFolders?: boolean, + @param.filter(FolderModel) folderFilter?: Filter, + ): Promise< + { + id: string; + name: string; + owner: string; + public: boolean; + description: string; + createdDate: string; + updatedDate: string; + isFolder?: boolean; + assetCount?: number; + reportCount?: number; + }[] + > { const userId = _.get(this.req, 'user.sub', 'anonymous'); this.logger.info( `ReportController - find - Fetching reports for user ${userId}`, ); + if (includeFolders) { + const reports = await this.reportService.find(userId, filter); + const folders = await this.folderService.find(userId, folderFilter); + const orderFilter = _.get(filter, 'order[0]', 'createdDate DESC'); + const [orderByField, orderByDirection] = orderFilter.split(' '); + return _.orderBy( + [ + ...reports, + ...folders.map(folder => ({ + id: folder.id, + name: folder.name, + public: false, + owner: folder.owner, + createdDate: folder.createdDate, + updatedDate: folder.updatedDate, + description: '', + isFolder: true, + assetCount: folder.assets ? folder.assets.length : 0, + reportCount: folder.reports ? folder.reports.length : 0, + })), + ], + [orderByField], + [orderByDirection.toLowerCase() as 'asc' | 'desc'], + ); + } return this.reportService.find(userId, filter); } From 7d0f9b934b3fd504376ddb52d2ce9b055c57cb50 Mon Sep 17 00:00:00 2001 From: Stefanos Hadjipetrou Date: Thu, 21 May 2026 11:00:55 +0300 Subject: [PATCH 2/2] feat: move to folder feature implementation --- .../report-builder/folder.controller.ts | 81 +++++++++++++++++-- .../report-builder/report.controller.ts | 58 +++++++++++-- 2 files changed, 128 insertions(+), 11 deletions(-) diff --git a/src/controllers/report-builder/folder.controller.ts b/src/controllers/report-builder/folder.controller.ts index f2451d7..c8e569a 100644 --- a/src/controllers/report-builder/folder.controller.ts +++ b/src/controllers/report-builder/folder.controller.ts @@ -16,7 +16,7 @@ import { } from '@loopback/rest'; import _ from 'lodash'; import {FolderModel} from 'rb-core-middleware/dist/models'; -import {FolderService} from 'rb-core-middleware/dist/services'; +import {FolderService, ReportService} from 'rb-core-middleware/dist/services'; import {Logger} from 'winston'; export class FolderController { @@ -24,6 +24,7 @@ export class FolderController { @inject(RestBindings.Http.REQUEST) private req: Request, @inject('services.logger') private logger: Logger, @inject('services.FolderService') private folderService: FolderService, + @inject('services.ReportService') private reportService: ReportService, ) {} @post('/folder') @@ -68,7 +69,7 @@ export class FolderController { async find( @param.filter(FolderModel) filter?: Filter, @param.query.string('includeSubFolders') includeSubFolders?: string, - ): Promise { + ): Promise { const userId = _.get(this.req, 'user.sub', 'anonymous'); this.logger.info( `FolderController - find - Fetching folders for user ${userId}`, @@ -76,7 +77,36 @@ export class FolderController { if (Boolean(includeSubFolders)) { return this.folderService.getAllFoldersWithSubfolders(userId, filter); } else { - return this.folderService.find(userId, filter); + const folders = await this.folderService.find(userId, filter); + const folderById = new Map( + folders.map(f => [f.id, f]), + ); + const pathCache = new Map(); + const buildFolderPath = (folderId: string | undefined): string => { + if (!folderId) return 'My Workspace'; + const cached = pathCache.get(folderId); + if (cached !== undefined) return cached; + const folder = folderById.get(folderId); + if (!folder) return 'My Workspace'; + const parentPath = buildFolderPath(folder.parentId); + const path = + parentPath === 'My Workspace' + ? `My Workspace > ${folder.name}` + : `${parentPath} > ${folder.name}`; + pathCache.set(folderId, path); + return path; + }; + + const foldersWithPath = _.filter(folders, folder => !folder.parentId).map( + folder => { + const locationPath = buildFolderPath(folder.parentId); + return { + ...folder, + locationPath, + }; + }, + ); + return foldersWithPath; } } @@ -100,7 +130,7 @@ export class FolderController { this.logger.info( `FolderController - getFoldersStructure - Fetching folder structure for user ${userId}`, ); - return this.folderService.getFolderTree(userId, filter); + return this.folderService.getFolderTree(userId); } @get('/folder/{id}') @@ -262,7 +292,17 @@ export class FolderController { this.logger.info( `FolderController - addReportToFolder - Adding report ${reportId} to folder ${id} for user ${userId}`, ); - return this.folderService.addReport(userId, id, reportId); + const reportPrevFolder = await this.reportService.findById( + [userId], + reportId, + ); + return this.folderService.addReport( + userId, + id, + reportId, + // @ts-ignore + reportPrevFolder?.folderId ?? undefined, + ); } @get('/folder/remove-report/{id}') @@ -285,4 +325,35 @@ export class FolderController { ); return this.folderService.removeReport(userId, id, reportId); } + + @get('/folder/add-folder/{id}') + @response(200, { + description: 'FolderModel instance', + content: { + 'application/json': { + schema: getModelSchemaRef(FolderModel, {includeRelations: true}), + }, + }, + }) + // @authenticate({strategy: 'auth0-jwt', options: {scopes: ['greet']}}) + async addFolderToFolder( + @param.path.string('id') id: string, + @param.query.string('folderId') folderId: string, + ): Promise { + const userId = _.get(this.req, 'user.sub', 'anonymous'); + this.logger.info( + `FolderController - addFolderToFolder - Adding folder ${folderId} to folder ${id} for user ${userId}`, + ); + const folderPrevFolder = await this.folderService.findById( + [userId], + folderId, + ); + return this.folderService.addFolder( + userId, + id, + folderId, + // @ts-ignore + folderPrevFolder?.folderId ?? undefined, + ); + } } diff --git a/src/controllers/report-builder/report.controller.ts b/src/controllers/report-builder/report.controller.ts index 15eb89d..aa8fbbb 100644 --- a/src/controllers/report-builder/report.controller.ts +++ b/src/controllers/report-builder/report.controller.ts @@ -100,8 +100,9 @@ export class ReportController { // @authenticate({strategy: 'auth0-jwt', options: {scopes: ['greet']}}) async find( @param.filter(ReportModel) filter?: Filter, - @param.query.string('includeFolders') includeFolders?: boolean, + @param.query.string('onlyRootLevel') onlyRootLevel?: boolean, @param.filter(FolderModel) folderFilter?: Filter, + @param.query.string('includeFolders') includeFolders?: boolean, ): Promise< { id: string; @@ -114,21 +115,64 @@ export class ReportController { isFolder?: boolean; assetCount?: number; reportCount?: number; + locationPath: string; }[] > { const userId = _.get(this.req, 'user.sub', 'anonymous'); this.logger.info( `ReportController - find - Fetching reports for user ${userId}`, ); + const reports = await this.reportService.find(userId, filter); + const allFolders = await this.folderService.find(userId, folderFilter); + + const folderById = new Map( + allFolders.map(f => [f.id, f]), + ); + const pathCache = new Map(); + const buildFolderPath = (folderId: string | undefined): string => { + if (!folderId) return 'My Workspace'; + const cached = pathCache.get(folderId); + if (cached !== undefined) return cached; + const folder = folderById.get(folderId); + if (!folder) return 'My Workspace'; + const parentPath = buildFolderPath(folder.parentId); + const path = + parentPath === 'My Workspace' + ? `My Workspace > ${folder.name}` + : `${parentPath} > ${folder.name}`; + pathCache.set(folderId, path); + return path; + }; + + const reportsWithPath = _.filter( + reports, + report => !onlyRootLevel || !report.folderId, + ).map(report => { + const locationPath = buildFolderPath(report.folderId); + return { + ..._.omit(report, ['folderId']), + locationPath, + }; + }); + + const foldersWithPath = _.filter( + allFolders, + folder => !onlyRootLevel || !folder.parentId, + ).map(folder => { + const locationPath = buildFolderPath(folder.parentId); + return { + ..._.omit(folder, ['parentId']), + locationPath, + }; + }); + if (includeFolders) { - const reports = await this.reportService.find(userId, filter); - const folders = await this.folderService.find(userId, folderFilter); const orderFilter = _.get(filter, 'order[0]', 'createdDate DESC'); const [orderByField, orderByDirection] = orderFilter.split(' '); return _.orderBy( [ - ...reports, - ...folders.map(folder => ({ + ...reportsWithPath, + ...foldersWithPath.map(folder => ({ id: folder.id, name: folder.name, public: false, @@ -139,13 +183,15 @@ export class ReportController { isFolder: true, assetCount: folder.assets ? folder.assets.length : 0, reportCount: folder.reports ? folder.reports.length : 0, + folderCount: folder.children ? folder.children.length : 0, + locationPath: folder.locationPath, })), ], [orderByField], [orderByDirection.toLowerCase() as 'asc' | 'desc'], ); } - return this.reportService.find(userId, filter); + return reportsWithPath; } @get('/report/{id}')