From 7d1e252c0658623dd703c6ca9a0425322ff71fa2 Mon Sep 17 00:00:00 2001 From: Artem Niehrieiev Date: Wed, 18 Feb 2026 14:44:16 +0000 Subject: [PATCH 1/3] feat: Implement panel use cases and safety checks for database queries - Added FindAllSavedDbQueriesUseCase to retrieve all saved database queries. - Introduced FindSavedDbQueryUseCase for fetching a specific saved query by ID. - Created panel-use-cases.interface.ts to define interfaces for panel operations. - Developed TestDbQueryUseCase to test database queries with safety checks. - Implemented UpdateSavedDbQueryUseCase for updating existing saved queries. - Added utility functions for building DTOs and checking query safety. - Created migration to rename visualization entities in the database. - Added end-to-end tests for AI-generated dashboard widgets, including safety checks for queries. --- backend/src/app.module.ts | 8 +- .../global-database-context.interface.ts | 12 +- .../application/global-database-context.ts | 34 +- backend/src/common/data-injection.tokens.ts | 1 + .../entities/connection/connection.entity.ts | 8 +- ...oard-widget-custom-repository-extension.ts | 28 -- .../dashboard-widget.repository.interface.ts | 9 - .../dashboard-widget-use-cases.interface.ts | 17 - .../build-found-dashboard-widget-dto.util.ts | 14 - .../dashboard/dashboard.entity.ts | 6 +- .../dashboard/dto/found-dashboard.dto.ts | 6 +- .../utils/build-found-dashboard-dto.util.ts | 4 +- .../create-panel-position.ds.ts} | 2 +- .../delete-panel-position.ds.ts} | 2 +- .../generate-panel-position-with-ai.ds.ts | 13 + .../update-panel-position.ds.ts} | 2 +- .../dto/create-panel-position.dto.ts} | 2 +- .../dto/found-panel-position.dto.ts} | 2 +- .../generate-panel-position-with-ai.dto.ts | 23 + .../dto/update-panel-position.dto.ts} | 2 +- .../panel-position.controller.ts} | 96 +++- .../panel-position.entity.ts} | 10 +- .../panel-position.module.ts} | 23 +- ...el-position-custom-repository-extension.ts | 29 ++ .../panel-position.repository.interface.ts | 9 + .../create-panel-position.use.case.ts} | 42 +- .../delete-panel-position.use.case.ts} | 22 +- ...enerate-panel-position-with-ai.use.case.ts | 244 +++++++++ .../panel-position-use-cases.interface.ts | 22 + .../update-panel-position.use.case.ts} | 25 +- .../build-found-dashboard-widget-dto.util.ts | 14 + .../data-structures/create-panel.ds.ts} | 2 +- .../execute-saved-db-query.ds.ts | 0 .../data-structures/find-all-panels.ds.ts} | 2 +- .../data-structures/find-panel.ds.ts} | 0 .../data-structures/test-db-query.ds.ts | 0 .../data-structures/update-panel.ds.ts} | 2 +- .../dto/create-saved-db-query.dto.ts | 0 .../dto/execute-saved-db-query-result.dto.ts | 0 .../dto/found-saved-db-query.dto.ts | 2 +- .../dto/test-db-query-result.dto.ts | 0 .../dto/test-db-query.dto.ts | 0 .../dto/update-saved-db-query.dto.ts | 0 .../panel.controller.ts} | 58 +-- .../panel.entity.ts} | 30 +- .../panel.module.ts} | 18 +- ...ed-db-query-custom-repository-extension.ts | 24 + .../saved-db-query.repository.interface.ts | 7 + .../use-cases/create-panel.use.case.ts} | 27 +- .../use-cases/delete-panel.use.case.ts} | 20 +- .../use-cases/execute-panel.use.case.ts} | 4 +- .../use-cases/find-all-panels.use.case.ts} | 18 +- .../use-cases/find-panel.use.case.ts} | 19 +- .../use-cases/panel-use-cases.interface.ts | 38 ++ .../use-cases/test-db-query.use.case.ts | 2 +- .../use-cases/update-panel.use.case.ts} | 27 +- .../panel/utils/build-found-panel-dto.util.ts | 17 + .../utils/check-query-is-safe.util.ts | 0 ...ed-db-query-custom-repository-extension.ts | 24 - .../saved-db-query.repository.interface.ts | 7 - .../saved-db-query-use-cases.interface.ts | 38 -- .../build-found-saved-db-query-dto.util.ts | 17 - ...71344994001-RenameVisualisationEntities.ts | 48 ++ .../dashboard-ai-widget-e2e.test.ts | 474 ++++++++++++++++++ .../saved-database-queries-e2e.test.ts | 42 +- 65 files changed, 1269 insertions(+), 429 deletions(-) delete mode 100644 backend/src/entities/visualizations/dashboard-widget/repository/dashboard-widget-custom-repository-extension.ts delete mode 100644 backend/src/entities/visualizations/dashboard-widget/repository/dashboard-widget.repository.interface.ts delete mode 100644 backend/src/entities/visualizations/dashboard-widget/use-cases/dashboard-widget-use-cases.interface.ts delete mode 100644 backend/src/entities/visualizations/dashboard-widget/utils/build-found-dashboard-widget-dto.util.ts rename backend/src/entities/visualizations/{dashboard-widget/data-structures/create-dashboard-widget.ds.ts => panel-position/data-structures/create-panel-position.ds.ts} (82%) rename backend/src/entities/visualizations/{dashboard-widget/data-structures/delete-dashboard-widget.ds.ts => panel-position/data-structures/delete-panel-position.ds.ts} (73%) create mode 100644 backend/src/entities/visualizations/panel-position/data-structures/generate-panel-position-with-ai.ds.ts rename backend/src/entities/visualizations/{dashboard-widget/data-structures/update-dashboard-widget.ds.ts => panel-position/data-structures/update-panel-position.ds.ts} (84%) rename backend/src/entities/visualizations/{dashboard-widget/dto/create-dashboard-widget.dto.ts => panel-position/dto/create-panel-position.dto.ts} (95%) rename backend/src/entities/visualizations/{dashboard-widget/dto/found-dashboard-widget.dto.ts => panel-position/dto/found-panel-position.dto.ts} (93%) create mode 100644 backend/src/entities/visualizations/panel-position/dto/generate-panel-position-with-ai.dto.ts rename backend/src/entities/visualizations/{dashboard-widget/dto/update-dashboard-widget.dto.ts => panel-position/dto/update-panel-position.dto.ts} (94%) rename backend/src/entities/visualizations/{dashboard-widget/dashboard-widgets.controller.ts => panel-position/panel-position.controller.ts} (53%) rename backend/src/entities/visualizations/{dashboard-widget/dashboard-widget.entity.ts => panel-position/panel-position.entity.ts} (74%) rename backend/src/entities/visualizations/{dashboard-widget/dashboard-widget.module.ts => panel-position/panel-position.module.ts} (61%) create mode 100644 backend/src/entities/visualizations/panel-position/repository/panel-position-custom-repository-extension.ts create mode 100644 backend/src/entities/visualizations/panel-position/repository/panel-position.repository.interface.ts rename backend/src/entities/visualizations/{dashboard-widget/use-cases/create-dashboard-widget.use.case.ts => panel-position/use-cases/create-panel-position.use.case.ts} (50%) rename backend/src/entities/visualizations/{dashboard-widget/use-cases/delete-dashboard-widget.use.case.ts => panel-position/use-cases/delete-panel-position.use.case.ts} (59%) create mode 100644 backend/src/entities/visualizations/panel-position/use-cases/generate-panel-position-with-ai.use.case.ts create mode 100644 backend/src/entities/visualizations/panel-position/use-cases/panel-position-use-cases.interface.ts rename backend/src/entities/visualizations/{dashboard-widget/use-cases/update-dashboard-widget.use.case.ts => panel-position/use-cases/update-panel-position.use.case.ts} (66%) create mode 100644 backend/src/entities/visualizations/panel-position/utils/build-found-dashboard-widget-dto.util.ts rename backend/src/entities/visualizations/{saved-db-query/data-structures/create-saved-db-query.ds.ts => panel/data-structures/create-panel.ds.ts} (89%) rename backend/src/entities/visualizations/{saved-db-query => panel}/data-structures/execute-saved-db-query.ds.ts (100%) rename backend/src/entities/visualizations/{saved-db-query/data-structures/find-all-saved-db-queries.ds.ts => panel/data-structures/find-all-panels.ds.ts} (56%) rename backend/src/entities/visualizations/{saved-db-query/data-structures/find-saved-db-query.ds.ts => panel/data-structures/find-panel.ds.ts} (100%) rename backend/src/entities/visualizations/{saved-db-query => panel}/data-structures/test-db-query.ds.ts (100%) rename backend/src/entities/visualizations/{saved-db-query/data-structures/update-saved-db-query.ds.ts => panel/data-structures/update-panel.ds.ts} (90%) rename backend/src/entities/visualizations/{saved-db-query => panel}/dto/create-saved-db-query.dto.ts (100%) rename backend/src/entities/visualizations/{saved-db-query => panel}/dto/execute-saved-db-query-result.dto.ts (100%) rename backend/src/entities/visualizations/{saved-db-query => panel}/dto/found-saved-db-query.dto.ts (96%) rename backend/src/entities/visualizations/{saved-db-query => panel}/dto/test-db-query-result.dto.ts (100%) rename backend/src/entities/visualizations/{saved-db-query => panel}/dto/test-db-query.dto.ts (100%) rename backend/src/entities/visualizations/{saved-db-query => panel}/dto/update-saved-db-query.dto.ts (100%) rename backend/src/entities/visualizations/{saved-db-query/saved-db-query.controller.ts => panel/panel.controller.ts} (85%) rename backend/src/entities/visualizations/{saved-db-query/saved-db-query.entity.ts => panel/panel.entity.ts} (73%) rename backend/src/entities/visualizations/{saved-db-query/saved-db-query.module.ts => panel/panel.module.ts} (80%) create mode 100644 backend/src/entities/visualizations/panel/repository/saved-db-query-custom-repository-extension.ts create mode 100644 backend/src/entities/visualizations/panel/repository/saved-db-query.repository.interface.ts rename backend/src/entities/visualizations/{saved-db-query/use-cases/create-saved-db-query.use.case.ts => panel/use-cases/create-panel.use.case.ts} (60%) rename backend/src/entities/visualizations/{saved-db-query/use-cases/delete-saved-db-query.use.case.ts => panel/use-cases/delete-panel.use.case.ts} (60%) rename backend/src/entities/visualizations/{saved-db-query/use-cases/execute-saved-db-query.use.case.ts => panel/use-cases/execute-panel.use.case.ts} (94%) rename backend/src/entities/visualizations/{saved-db-query/use-cases/find-all-saved-db-queries.use.case.ts => panel/use-cases/find-all-panels.use.case.ts} (54%) rename backend/src/entities/visualizations/{saved-db-query/use-cases/find-saved-db-query.use.case.ts => panel/use-cases/find-panel.use.case.ts} (60%) create mode 100644 backend/src/entities/visualizations/panel/use-cases/panel-use-cases.interface.ts rename backend/src/entities/visualizations/{saved-db-query => panel}/use-cases/test-db-query.use.case.ts (97%) rename backend/src/entities/visualizations/{saved-db-query/use-cases/update-saved-db-query.use.case.ts => panel/use-cases/update-panel.use.case.ts} (63%) create mode 100644 backend/src/entities/visualizations/panel/utils/build-found-panel-dto.util.ts rename backend/src/entities/visualizations/{saved-db-query => panel}/utils/check-query-is-safe.util.ts (100%) delete mode 100644 backend/src/entities/visualizations/saved-db-query/repository/saved-db-query-custom-repository-extension.ts delete mode 100644 backend/src/entities/visualizations/saved-db-query/repository/saved-db-query.repository.interface.ts delete mode 100644 backend/src/entities/visualizations/saved-db-query/use-cases/saved-db-query-use-cases.interface.ts delete mode 100644 backend/src/entities/visualizations/saved-db-query/utils/build-found-saved-db-query-dto.util.ts create mode 100644 backend/src/migrations/1771344994001-RenameVisualisationEntities.ts create mode 100644 backend/test/ava-tests/saas-tests/dashboard-ai-widget-e2e.test.ts diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 754dd0d90..787057468 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -37,8 +37,8 @@ import { UserActionModule } from './entities/user-actions/user-action.module.js' import { UserSecretModule } from './entities/user-secret/user-secret.module.js'; import { SignInAuditModule } from './entities/user-sign-in-audit/sign-in-audit.module.js'; import { DashboardModule } from './entities/visualizations/dashboard/dashboards.module.js'; -import { DashboardWidgetModule } from './entities/visualizations/dashboard-widget/dashboard-widget.module.js'; -import { SavedDbQueryModule } from './entities/visualizations/saved-db-query/saved-db-query.module.js'; +import { PanelPositionModule } from './entities/visualizations/panel-position/panel-position.module.js'; +import { PanelModule } from './entities/visualizations/panel/panel.module.js'; import { TableWidgetModule } from './entities/widget/table-widget.module.js'; import { SaaSGatewayModule } from './microservices/gateways/saas-gateway.ts/saas-gateway.module.js'; import { SaasModule } from './microservices/saas-microservice/saas.module.js'; @@ -95,9 +95,9 @@ import { GetHelloUseCase } from './use-cases-app/get-hello.use.case.js'; SignInAuditModule, PersonalTableSettingsModule, S3WidgetModule, - SavedDbQueryModule, + PanelModule, DashboardModule, - DashboardWidgetModule, + PanelPositionModule, SelfHostedOperationsModule.register(), ], controllers: [AppController], diff --git a/backend/src/common/application/global-database-context.interface.ts b/backend/src/common/application/global-database-context.interface.ts index 8832f21aa..db6bab219 100644 --- a/backend/src/common/application/global-database-context.interface.ts +++ b/backend/src/common/application/global-database-context.interface.ts @@ -59,10 +59,10 @@ import { ISignInAuditRepository } from '../../entities/user-sign-in-audit/reposi import { SignInAuditEntity } from '../../entities/user-sign-in-audit/sign-in-audit.entity.js'; import { DashboardEntity } from '../../entities/visualizations/dashboard/dashboard.entity.js'; import { IDashboardRepository } from '../../entities/visualizations/dashboard/repository/dashboard.repository.interface.js'; -import { DashboardWidgetEntity } from '../../entities/visualizations/dashboard-widget/dashboard-widget.entity.js'; -import { IDashboardWidgetRepository } from '../../entities/visualizations/dashboard-widget/repository/dashboard-widget.repository.interface.js'; -import { ISavedDbQueryRepository } from '../../entities/visualizations/saved-db-query/repository/saved-db-query.repository.interface.js'; -import { SavedDbQueryEntity } from '../../entities/visualizations/saved-db-query/saved-db-query.entity.js'; +import { PanelPositionEntity } from '../../entities/visualizations/panel-position/panel-position.entity.js'; +import { IPanelPositionRepository } from '../../entities/visualizations/panel-position/repository/panel-position.repository.interface.js'; +import { IPanelRepository } from '../../entities/visualizations/panel/repository/saved-db-query.repository.interface.js'; +import { PanelEntity } from '../../entities/visualizations/panel/panel.entity.js'; import { ITableWidgetsRepository } from '../../entities/widget/repository/table-widgets-repository.interface.js'; import { TableWidgetEntity } from '../../entities/widget/table-widget.entity.js'; import { IDatabaseContext } from '../database-context.interface.js'; @@ -105,9 +105,9 @@ export interface IGlobalDatabaseContext extends IDatabaseContext { secretAccessLogRepository: Repository & ISecretAccessLogRepository; signInAuditRepository: Repository & ISignInAuditRepository; personalTableSettingsRepository: Repository & IPersonalTableSettingsRepository; - savedDbQueryRepository: Repository & ISavedDbQueryRepository; + panelRepository: Repository & IPanelRepository; dashboardRepository: Repository & IDashboardRepository; - dashboardWidgetRepository: Repository & IDashboardWidgetRepository; + panelPositionRepository: Repository & IPanelPositionRepository; userAiChatRepository: Repository & IUserAiChatRepository; aiChatMessageRepository: Repository & IAiChatMessageRepository; } diff --git a/backend/src/common/application/global-database-context.ts b/backend/src/common/application/global-database-context.ts index 286333140..fdbce430b 100644 --- a/backend/src/common/application/global-database-context.ts +++ b/backend/src/common/application/global-database-context.ts @@ -106,12 +106,12 @@ import { SignInAuditEntity } from '../../entities/user-sign-in-audit/sign-in-aud import { DashboardEntity } from '../../entities/visualizations/dashboard/dashboard.entity.js'; import { IDashboardRepository } from '../../entities/visualizations/dashboard/repository/dashboard.repository.interface.js'; import { dashboardCustomRepositoryExtension } from '../../entities/visualizations/dashboard/repository/dashboard-custom-repository-extension.js'; -import { DashboardWidgetEntity } from '../../entities/visualizations/dashboard-widget/dashboard-widget.entity.js'; -import { IDashboardWidgetRepository } from '../../entities/visualizations/dashboard-widget/repository/dashboard-widget.repository.interface.js'; -import { dashboardWidgetCustomRepositoryExtension } from '../../entities/visualizations/dashboard-widget/repository/dashboard-widget-custom-repository-extension.js'; -import { ISavedDbQueryRepository } from '../../entities/visualizations/saved-db-query/repository/saved-db-query.repository.interface.js'; -import { savedDbQueryCustomRepositoryExtension } from '../../entities/visualizations/saved-db-query/repository/saved-db-query-custom-repository-extension.js'; -import { SavedDbQueryEntity } from '../../entities/visualizations/saved-db-query/saved-db-query.entity.js'; +import { PanelPositionEntity } from '../../entities/visualizations/panel-position/panel-position.entity.js'; +import { IPanelPositionRepository } from '../../entities/visualizations/panel-position/repository/panel-position.repository.interface.js'; +import { panelPositionCustomRepositoryExtension } from '../../entities/visualizations/panel-position/repository/panel-position-custom-repository-extension.js'; +import { IPanelRepository } from '../../entities/visualizations/panel/repository/saved-db-query.repository.interface.js'; +import { panelCustomRepositoryExtension } from '../../entities/visualizations/panel/repository/saved-db-query-custom-repository-extension.js'; +import { PanelEntity } from '../../entities/visualizations/panel/panel.entity.js'; import { tableWidgetsCustomRepositoryExtension } from '../../entities/widget/repository/table-widgets-custom-repsitory-extension.js'; import { ITableWidgetsRepository } from '../../entities/widget/repository/table-widgets-repository.interface.js'; import { TableWidgetEntity } from '../../entities/widget/table-widget.entity.js'; @@ -159,9 +159,9 @@ export class GlobalDatabaseContext implements IGlobalDatabaseContext { private _secretAccessLogRepository: Repository & ISecretAccessLogRepository; private _signInAuditRepository: Repository & ISignInAuditRepository; private _personalTableSettingsRepository: Repository & IPersonalTableSettingsRepository; - private _savedDbQueryRepository: Repository & ISavedDbQueryRepository; + private _panelRepository: Repository & IPanelRepository; private _dashboardRepository: Repository & IDashboardRepository; - private _dashboardWidgetRepository: Repository & IDashboardWidgetRepository; + private _panelPositionRepository: Repository & IPanelPositionRepository; private _userAiChatRepository: Repository & IUserAiChatRepository; private _aiChatMessageRepository: Repository & IAiChatMessageRepository; @@ -264,15 +264,13 @@ export class GlobalDatabaseContext implements IGlobalDatabaseContext { this._personalTableSettingsRepository = this.appDataSource .getRepository(PersonalTableSettingsEntity) .extend(personalTableSettingsCustomRepositoryExtension); - this._savedDbQueryRepository = this.appDataSource - .getRepository(SavedDbQueryEntity) - .extend(savedDbQueryCustomRepositoryExtension); + this._panelRepository = this.appDataSource.getRepository(PanelEntity).extend(panelCustomRepositoryExtension); this._dashboardRepository = this.appDataSource .getRepository(DashboardEntity) .extend(dashboardCustomRepositoryExtension); - this._dashboardWidgetRepository = this.appDataSource - .getRepository(DashboardWidgetEntity) - .extend(dashboardWidgetCustomRepositoryExtension); + this._panelPositionRepository = this.appDataSource + .getRepository(PanelPositionEntity) + .extend(panelPositionCustomRepositoryExtension); this._userAiChatRepository = this.appDataSource .getRepository(UserAiChatEntity) .extend(userAiChatRepositoryExtension); @@ -431,16 +429,16 @@ export class GlobalDatabaseContext implements IGlobalDatabaseContext { return this._personalTableSettingsRepository; } - public get savedDbQueryRepository(): Repository & ISavedDbQueryRepository { - return this._savedDbQueryRepository; + public get panelRepository(): Repository & IPanelRepository { + return this._panelRepository; } public get dashboardRepository(): Repository & IDashboardRepository { return this._dashboardRepository; } - public get dashboardWidgetRepository(): Repository & IDashboardWidgetRepository { - return this._dashboardWidgetRepository; + public get panelPositionRepository(): Repository & IPanelPositionRepository { + return this._panelPositionRepository; } public get userAiChatRepository(): Repository & IUserAiChatRepository { diff --git a/backend/src/common/data-injection.tokens.ts b/backend/src/common/data-injection.tokens.ts index 92cf20ff6..90bfc194c 100644 --- a/backend/src/common/data-injection.tokens.ts +++ b/backend/src/common/data-injection.tokens.ts @@ -203,6 +203,7 @@ export enum UseCaseType { CREATE_DASHBOARD_WIDGET = 'CREATE_DASHBOARD_WIDGET', UPDATE_DASHBOARD_WIDGET = 'UPDATE_DASHBOARD_WIDGET', DELETE_DASHBOARD_WIDGET = 'DELETE_DASHBOARD_WIDGET', + GENERATE_WIDGET_WITH_AI = 'GENERATE_WIDGET_WITH_AI', IS_CONFIGURED = 'IS_CONFIGURED', CREATE_INITIAL_USER = 'CREATE_INITIAL_USER', diff --git a/backend/src/entities/connection/connection.entity.ts b/backend/src/entities/connection/connection.entity.ts index a6caaae77..8dfcb6429 100644 --- a/backend/src/entities/connection/connection.entity.ts +++ b/backend/src/entities/connection/connection.entity.ts @@ -28,7 +28,7 @@ import { TableSettingsEntity } from '../table-settings/common-table-settings/tab import { PersonalTableSettingsEntity } from '../table-settings/personal-table-settings/personal-table-settings.entity.js'; import { UserEntity } from '../user/user.entity.js'; import { DashboardEntity } from '../visualizations/dashboard/dashboard.entity.js'; -import { SavedDbQueryEntity } from '../visualizations/saved-db-query/saved-db-query.entity.js'; +import { PanelEntity } from '../visualizations/panel/panel.entity.js'; @Entity('connection') export class ConnectionEntity { @@ -284,10 +284,10 @@ export class ConnectionEntity { table_filters: Relation[]; @OneToMany( - () => SavedDbQueryEntity, - (saved_db_query) => saved_db_query.connection, + () => PanelEntity, + (panel) => panel.connection, ) - saved_db_queries: Relation[]; + panels: Relation[]; @OneToMany( () => DashboardEntity, diff --git a/backend/src/entities/visualizations/dashboard-widget/repository/dashboard-widget-custom-repository-extension.ts b/backend/src/entities/visualizations/dashboard-widget/repository/dashboard-widget-custom-repository-extension.ts deleted file mode 100644 index 9f4247521..000000000 --- a/backend/src/entities/visualizations/dashboard-widget/repository/dashboard-widget-custom-repository-extension.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { DashboardWidgetEntity } from '../dashboard-widget.entity.js'; - -export const dashboardWidgetCustomRepositoryExtension = { - async findWidgetById(widgetId: string): Promise { - return await this.findOne({ where: { id: widgetId } }); - }, - - async findWidgetByIdAndDashboardId(widgetId: string, dashboardId: string): Promise { - return await this.findOne({ - where: { id: widgetId, dashboard_id: dashboardId }, - }); - }, - - async findAllWidgetsByDashboardId(dashboardId: string): Promise { - return await this.find({ - where: { dashboard_id: dashboardId }, - order: { position_y: 'ASC', position_x: 'ASC' }, - }); - }, - - async saveWidget(widget: DashboardWidgetEntity): Promise { - return await this.save(widget); - }, - - async removeWidget(widget: DashboardWidgetEntity): Promise { - return await this.remove(widget); - }, -}; diff --git a/backend/src/entities/visualizations/dashboard-widget/repository/dashboard-widget.repository.interface.ts b/backend/src/entities/visualizations/dashboard-widget/repository/dashboard-widget.repository.interface.ts deleted file mode 100644 index a0a705877..000000000 --- a/backend/src/entities/visualizations/dashboard-widget/repository/dashboard-widget.repository.interface.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { DashboardWidgetEntity } from '../dashboard-widget.entity.js'; - -export interface IDashboardWidgetRepository { - findWidgetById(widgetId: string): Promise; - findWidgetByIdAndDashboardId(widgetId: string, dashboardId: string): Promise; - findAllWidgetsByDashboardId(dashboardId: string): Promise; - saveWidget(widget: DashboardWidgetEntity): Promise; - removeWidget(widget: DashboardWidgetEntity): Promise; -} diff --git a/backend/src/entities/visualizations/dashboard-widget/use-cases/dashboard-widget-use-cases.interface.ts b/backend/src/entities/visualizations/dashboard-widget/use-cases/dashboard-widget-use-cases.interface.ts deleted file mode 100644 index fc8f4ed23..000000000 --- a/backend/src/entities/visualizations/dashboard-widget/use-cases/dashboard-widget-use-cases.interface.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { InTransactionEnum } from '../../../../enums/in-transaction.enum.js'; -import { CreateDashboardWidgetDs } from '../data-structures/create-dashboard-widget.ds.js'; -import { DeleteDashboardWidgetDs } from '../data-structures/delete-dashboard-widget.ds.js'; -import { UpdateDashboardWidgetDs } from '../data-structures/update-dashboard-widget.ds.js'; -import { FoundDashboardWidgetDto } from '../dto/found-dashboard-widget.dto.js'; - -export interface ICreateDashboardWidget { - execute(inputData: CreateDashboardWidgetDs, inTransaction: InTransactionEnum): Promise; -} - -export interface IUpdateDashboardWidget { - execute(inputData: UpdateDashboardWidgetDs, inTransaction: InTransactionEnum): Promise; -} - -export interface IDeleteDashboardWidget { - execute(inputData: DeleteDashboardWidgetDs, inTransaction: InTransactionEnum): Promise; -} diff --git a/backend/src/entities/visualizations/dashboard-widget/utils/build-found-dashboard-widget-dto.util.ts b/backend/src/entities/visualizations/dashboard-widget/utils/build-found-dashboard-widget-dto.util.ts deleted file mode 100644 index ae6497adb..000000000 --- a/backend/src/entities/visualizations/dashboard-widget/utils/build-found-dashboard-widget-dto.util.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { DashboardWidgetEntity } from '../dashboard-widget.entity.js'; -import { FoundDashboardWidgetDto } from '../dto/found-dashboard-widget.dto.js'; - -export function buildFoundDashboardWidgetDto(widget: DashboardWidgetEntity): FoundDashboardWidgetDto { - return { - id: widget.id, - position_x: widget.position_x, - position_y: widget.position_y, - width: widget.width, - height: widget.height, - dashboard_id: widget.dashboard_id, - query_id: widget.query_id, - }; -} diff --git a/backend/src/entities/visualizations/dashboard/dashboard.entity.ts b/backend/src/entities/visualizations/dashboard/dashboard.entity.ts index ff93d80e5..c6f33a98b 100644 --- a/backend/src/entities/visualizations/dashboard/dashboard.entity.ts +++ b/backend/src/entities/visualizations/dashboard/dashboard.entity.ts @@ -1,6 +1,6 @@ import { Column, Entity, JoinColumn, ManyToOne, OneToMany, PrimaryGeneratedColumn, Relation } from 'typeorm'; import { ConnectionEntity } from '../../connection/connection.entity.js'; -import { DashboardWidgetEntity } from '../dashboard-widget/dashboard-widget.entity.js'; +import { PanelPositionEntity } from '../panel-position/panel-position.entity.js'; @Entity('dashboard') export class DashboardEntity { @@ -27,8 +27,8 @@ export class DashboardEntity { connection: Relation; @OneToMany( - () => DashboardWidgetEntity, + () => PanelPositionEntity, (widget) => widget.dashboard, ) - widgets: Relation[]; + widgets: Relation[]; } diff --git a/backend/src/entities/visualizations/dashboard/dto/found-dashboard.dto.ts b/backend/src/entities/visualizations/dashboard/dto/found-dashboard.dto.ts index ebf13f5e5..986a4220a 100644 --- a/backend/src/entities/visualizations/dashboard/dto/found-dashboard.dto.ts +++ b/backend/src/entities/visualizations/dashboard/dto/found-dashboard.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { FoundDashboardWidgetDto } from '../../dashboard-widget/dto/found-dashboard-widget.dto.js'; +import { FoundPanelPositionDto } from '../../panel-position/dto/found-panel-position.dto.js'; export class FoundDashboardDto { @ApiProperty({ description: 'Dashboard ID' }) @@ -20,6 +20,6 @@ export class FoundDashboardDto { @ApiProperty({ description: 'Last update timestamp' }) updated_at: Date; - @ApiPropertyOptional({ description: 'Dashboard widgets', type: FoundDashboardWidgetDto, isArray: true }) - widgets?: FoundDashboardWidgetDto[]; + @ApiPropertyOptional({ description: 'Dashboard widgets', type: FoundPanelPositionDto, isArray: true }) + widgets?: FoundPanelPositionDto[]; } diff --git a/backend/src/entities/visualizations/dashboard/utils/build-found-dashboard-dto.util.ts b/backend/src/entities/visualizations/dashboard/utils/build-found-dashboard-dto.util.ts index 08e6925ad..05f4f3287 100644 --- a/backend/src/entities/visualizations/dashboard/utils/build-found-dashboard-dto.util.ts +++ b/backend/src/entities/visualizations/dashboard/utils/build-found-dashboard-dto.util.ts @@ -1,4 +1,4 @@ -import { buildFoundDashboardWidgetDto } from '../../dashboard-widget/utils/build-found-dashboard-widget-dto.util.js'; +import { buildFoundPanelPositionDto } from '../../panel-position/utils/build-found-dashboard-widget-dto.util.js'; import { DashboardEntity } from '../dashboard.entity.js'; import { FoundDashboardDto } from '../dto/found-dashboard.dto.js'; @@ -10,6 +10,6 @@ export function buildFoundDashboardDto(dashboard: DashboardEntity): FoundDashboa connection_id: dashboard.connection_id, created_at: dashboard.created_at, updated_at: dashboard.updated_at, - widgets: dashboard.widgets?.map(buildFoundDashboardWidgetDto), + widgets: dashboard.widgets?.map(buildFoundPanelPositionDto), }; } diff --git a/backend/src/entities/visualizations/dashboard-widget/data-structures/create-dashboard-widget.ds.ts b/backend/src/entities/visualizations/panel-position/data-structures/create-panel-position.ds.ts similarity index 82% rename from backend/src/entities/visualizations/dashboard-widget/data-structures/create-dashboard-widget.ds.ts rename to backend/src/entities/visualizations/panel-position/data-structures/create-panel-position.ds.ts index 478ef4fd2..552f2de8f 100644 --- a/backend/src/entities/visualizations/dashboard-widget/data-structures/create-dashboard-widget.ds.ts +++ b/backend/src/entities/visualizations/panel-position/data-structures/create-panel-position.ds.ts @@ -1,4 +1,4 @@ -export class CreateDashboardWidgetDs { +export class CreatePanelPositionDs { dashboardId: string; connectionId: string; masterPassword: string; diff --git a/backend/src/entities/visualizations/dashboard-widget/data-structures/delete-dashboard-widget.ds.ts b/backend/src/entities/visualizations/panel-position/data-structures/delete-panel-position.ds.ts similarity index 73% rename from backend/src/entities/visualizations/dashboard-widget/data-structures/delete-dashboard-widget.ds.ts rename to backend/src/entities/visualizations/panel-position/data-structures/delete-panel-position.ds.ts index 1d9431f04..8dad26019 100644 --- a/backend/src/entities/visualizations/dashboard-widget/data-structures/delete-dashboard-widget.ds.ts +++ b/backend/src/entities/visualizations/panel-position/data-structures/delete-panel-position.ds.ts @@ -1,4 +1,4 @@ -export class DeleteDashboardWidgetDs { +export class DeletePanelPositionDs { widgetId: string; dashboardId: string; connectionId: string; diff --git a/backend/src/entities/visualizations/panel-position/data-structures/generate-panel-position-with-ai.ds.ts b/backend/src/entities/visualizations/panel-position/data-structures/generate-panel-position-with-ai.ds.ts new file mode 100644 index 000000000..64b49d504 --- /dev/null +++ b/backend/src/entities/visualizations/panel-position/data-structures/generate-panel-position-with-ai.ds.ts @@ -0,0 +1,13 @@ +export class GeneratePanelPositionWithAiDs { + dashboardId: string; + connectionId: string; + masterPassword: string; + userId: string; + chart_description: string; + table_name: string; + name?: string; + position_x?: number; + position_y?: number; + width?: number; + height?: number; +} diff --git a/backend/src/entities/visualizations/dashboard-widget/data-structures/update-dashboard-widget.ds.ts b/backend/src/entities/visualizations/panel-position/data-structures/update-panel-position.ds.ts similarity index 84% rename from backend/src/entities/visualizations/dashboard-widget/data-structures/update-dashboard-widget.ds.ts rename to backend/src/entities/visualizations/panel-position/data-structures/update-panel-position.ds.ts index 2ee62183d..0632013d5 100644 --- a/backend/src/entities/visualizations/dashboard-widget/data-structures/update-dashboard-widget.ds.ts +++ b/backend/src/entities/visualizations/panel-position/data-structures/update-panel-position.ds.ts @@ -1,4 +1,4 @@ -export class UpdateDashboardWidgetDs { +export class UpdatePanelPositionDs { widgetId: string; dashboardId: string; connectionId: string; diff --git a/backend/src/entities/visualizations/dashboard-widget/dto/create-dashboard-widget.dto.ts b/backend/src/entities/visualizations/panel-position/dto/create-panel-position.dto.ts similarity index 95% rename from backend/src/entities/visualizations/dashboard-widget/dto/create-dashboard-widget.dto.ts rename to backend/src/entities/visualizations/panel-position/dto/create-panel-position.dto.ts index 43d5323f5..aa8c019ba 100644 --- a/backend/src/entities/visualizations/dashboard-widget/dto/create-dashboard-widget.dto.ts +++ b/backend/src/entities/visualizations/panel-position/dto/create-panel-position.dto.ts @@ -1,7 +1,7 @@ import { ApiPropertyOptional } from '@nestjs/swagger'; import { IsInt, IsOptional, IsUUID, Max, Min } from 'class-validator'; -export class CreateDashboardWidgetDto { +export class CreatePanelPositionDto { @ApiPropertyOptional({ description: 'Associated saved query ID' }) @IsOptional() @IsUUID() diff --git a/backend/src/entities/visualizations/dashboard-widget/dto/found-dashboard-widget.dto.ts b/backend/src/entities/visualizations/panel-position/dto/found-panel-position.dto.ts similarity index 93% rename from backend/src/entities/visualizations/dashboard-widget/dto/found-dashboard-widget.dto.ts rename to backend/src/entities/visualizations/panel-position/dto/found-panel-position.dto.ts index ebfd78817..02c21aec8 100644 --- a/backend/src/entities/visualizations/dashboard-widget/dto/found-dashboard-widget.dto.ts +++ b/backend/src/entities/visualizations/panel-position/dto/found-panel-position.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -export class FoundDashboardWidgetDto { +export class FoundPanelPositionDto { @ApiProperty({ description: 'Widget ID' }) id: string; diff --git a/backend/src/entities/visualizations/panel-position/dto/generate-panel-position-with-ai.dto.ts b/backend/src/entities/visualizations/panel-position/dto/generate-panel-position-with-ai.dto.ts new file mode 100644 index 000000000..445c80841 --- /dev/null +++ b/backend/src/entities/visualizations/panel-position/dto/generate-panel-position-with-ai.dto.ts @@ -0,0 +1,23 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsNotEmpty, IsOptional, IsString, MaxLength } from 'class-validator'; + +export class GeneratePanelPositionWithAiDto { + @ApiProperty({ + type: String, + description: 'Natural language description of the chart you want to create', + example: 'Show monthly sales by product category as a bar chart', + }) + @IsNotEmpty() + @IsString() + @MaxLength(2000) + chart_description: string; + + @ApiPropertyOptional({ + type: String, + description: 'Optional name for the saved query. If not provided, AI will generate one.', + }) + @IsOptional() + @IsString() + @MaxLength(255) + name?: string; +} diff --git a/backend/src/entities/visualizations/dashboard-widget/dto/update-dashboard-widget.dto.ts b/backend/src/entities/visualizations/panel-position/dto/update-panel-position.dto.ts similarity index 94% rename from backend/src/entities/visualizations/dashboard-widget/dto/update-dashboard-widget.dto.ts rename to backend/src/entities/visualizations/panel-position/dto/update-panel-position.dto.ts index 4414a9262..f3029d1c4 100644 --- a/backend/src/entities/visualizations/dashboard-widget/dto/update-dashboard-widget.dto.ts +++ b/backend/src/entities/visualizations/panel-position/dto/update-panel-position.dto.ts @@ -1,7 +1,7 @@ import { ApiPropertyOptional } from '@nestjs/swagger'; import { IsInt, IsOptional, IsUUID, Max, Min } from 'class-validator'; -export class UpdateDashboardWidgetDto { +export class UpdatePanelPositionDto { @ApiPropertyOptional({ description: 'Associated saved query ID' }) @IsOptional() @IsUUID() diff --git a/backend/src/entities/visualizations/dashboard-widget/dashboard-widgets.controller.ts b/backend/src/entities/visualizations/panel-position/panel-position.controller.ts similarity index 53% rename from backend/src/entities/visualizations/dashboard-widget/dashboard-widgets.controller.ts rename to backend/src/entities/visualizations/panel-position/panel-position.controller.ts index 3a446503c..be24111f9 100644 --- a/backend/src/entities/visualizations/dashboard-widget/dashboard-widgets.controller.ts +++ b/backend/src/entities/visualizations/panel-position/panel-position.controller.ts @@ -7,10 +7,11 @@ import { Param, Post, Put, + Query, UseGuards, UseInterceptors, } from '@nestjs/common'; -import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger'; import { UseCaseType } from '../../../common/data-injection.tokens.js'; import { MasterPassword } from '../../../decorators/master-password.decorator.js'; import { SlugUuid } from '../../../decorators/slug-uuid.decorator.js'; @@ -19,17 +20,20 @@ import { UserId } from '../../../decorators/user-id.decorator.js'; import { InTransactionEnum } from '../../../enums/in-transaction.enum.js'; import { ConnectionEditGuard } from '../../../guards/connection-edit.guard.js'; import { SentryInterceptor } from '../../../interceptors/sentry.interceptor.js'; -import { CreateDashboardWidgetDs } from './data-structures/create-dashboard-widget.ds.js'; -import { DeleteDashboardWidgetDs } from './data-structures/delete-dashboard-widget.ds.js'; -import { UpdateDashboardWidgetDs } from './data-structures/update-dashboard-widget.ds.js'; -import { CreateDashboardWidgetDto } from './dto/create-dashboard-widget.dto.js'; -import { FoundDashboardWidgetDto } from './dto/found-dashboard-widget.dto.js'; -import { UpdateDashboardWidgetDto } from './dto/update-dashboard-widget.dto.js'; +import { CreatePanelPositionDs } from './data-structures/create-panel-position.ds.js'; +import { DeletePanelPositionDs } from './data-structures/delete-panel-position.ds.js'; +import { GeneratePanelPositionWithAiDs } from './data-structures/generate-panel-position-with-ai.ds.js'; +import { UpdatePanelPositionDs } from './data-structures/update-panel-position.ds.js'; +import { CreatePanelPositionDto } from './dto/create-panel-position.dto.js'; +import { FoundPanelPositionDto } from './dto/found-panel-position.dto.js'; +import { GeneratePanelPositionWithAiDto } from './dto/generate-panel-position-with-ai.dto.js'; +import { UpdatePanelPositionDto } from './dto/update-panel-position.dto.js'; import { - ICreateDashboardWidget, - IDeleteDashboardWidget, - IUpdateDashboardWidget, -} from './use-cases/dashboard-widget-use-cases.interface.js'; + ICreatePanelPositionWidget, + IDeletePanelPosition, + IGeneratePanelPositionWithAi, + IUpdatePanelPosition, +} from './use-cases/panel-position-use-cases.interface.js'; @UseInterceptors(SentryInterceptor) @Timeout() @@ -40,20 +44,22 @@ import { export class DashboardWidgetController { constructor( @Inject(UseCaseType.CREATE_DASHBOARD_WIDGET) - private readonly createDashboardWidgetUseCase: ICreateDashboardWidget, + private readonly createDashboardWidgetUseCase: ICreatePanelPositionWidget, @Inject(UseCaseType.UPDATE_DASHBOARD_WIDGET) - private readonly updateDashboardWidgetUseCase: IUpdateDashboardWidget, + private readonly updateDashboardWidgetUseCase: IUpdatePanelPosition, @Inject(UseCaseType.DELETE_DASHBOARD_WIDGET) - private readonly deleteDashboardWidgetUseCase: IDeleteDashboardWidget, + private readonly deleteDashboardWidgetUseCase: IDeletePanelPosition, + @Inject(UseCaseType.GENERATE_WIDGET_WITH_AI) + private readonly generateWidgetWithAiUseCase: IGeneratePanelPositionWithAi, ) {} @ApiOperation({ summary: 'Create a new widget in a dashboard' }) @ApiResponse({ status: 201, description: 'Widget created.', - type: FoundDashboardWidgetDto, + type: FoundPanelPositionDto, }) - @ApiBody({ type: CreateDashboardWidgetDto }) + @ApiBody({ type: CreatePanelPositionDto }) @ApiParam({ name: 'dashboardId', required: true }) @ApiParam({ name: 'connectionId', required: true }) @UseGuards(ConnectionEditGuard) @@ -63,9 +69,9 @@ export class DashboardWidgetController { @Param('dashboardId') dashboardId: string, @MasterPassword() masterPwd: string, @UserId() userId: string, - @Body() createDto: CreateDashboardWidgetDto, - ): Promise { - const inputData: CreateDashboardWidgetDs = { + @Body() createDto: CreatePanelPositionDto, + ): Promise { + const inputData: CreatePanelPositionDs = { dashboardId, connectionId, masterPassword: masterPwd, @@ -83,9 +89,9 @@ export class DashboardWidgetController { @ApiResponse({ status: 200, description: 'Widget updated.', - type: FoundDashboardWidgetDto, + type: FoundPanelPositionDto, }) - @ApiBody({ type: UpdateDashboardWidgetDto }) + @ApiBody({ type: UpdatePanelPositionDto }) @ApiParam({ name: 'dashboardId', required: true }) @ApiParam({ name: 'widgetId', required: true }) @ApiParam({ name: 'connectionId', required: true }) @@ -97,9 +103,9 @@ export class DashboardWidgetController { @Param('widgetId') widgetId: string, @MasterPassword() masterPwd: string, @UserId() userId: string, - @Body() updateDto: UpdateDashboardWidgetDto, - ): Promise { - const inputData: UpdateDashboardWidgetDs = { + @Body() updateDto: UpdatePanelPositionDto, + ): Promise { + const inputData: UpdatePanelPositionDs = { widgetId, dashboardId, connectionId, @@ -118,7 +124,7 @@ export class DashboardWidgetController { @ApiResponse({ status: 200, description: 'Widget deleted.', - type: FoundDashboardWidgetDto, + type: FoundPanelPositionDto, }) @ApiParam({ name: 'dashboardId', required: true }) @ApiParam({ name: 'widgetId', required: true }) @@ -131,8 +137,8 @@ export class DashboardWidgetController { @Param('widgetId') widgetId: string, @MasterPassword() masterPwd: string, @UserId() userId: string, - ): Promise { - const inputData: DeleteDashboardWidgetDs = { + ): Promise { + const inputData: DeletePanelPositionDs = { widgetId, dashboardId, connectionId, @@ -141,4 +147,40 @@ export class DashboardWidgetController { }; return await this.deleteDashboardWidgetUseCase.execute(inputData, InTransactionEnum.ON); } + + @ApiOperation({ + summary: 'Generate a widget using AI', + description: + 'Creates a new widget with SQL query and chart configuration generated by AI based on natural language description', + }) + @ApiResponse({ + status: 201, + description: 'Widget generated and created.', + type: FoundPanelPositionDto, + }) + @ApiBody({ type: GeneratePanelPositionWithAiDto }) + @ApiParam({ name: 'dashboardId', required: true }) + @ApiParam({ name: 'connectionId', required: true }) + @ApiQuery({ name: 'tableName', required: true, description: 'The table name to generate the widget for' }) + @UseGuards(ConnectionEditGuard) + @Post('/dashboard/:dashboardId/widget/generate/:connectionId') + async generateWidgetWithAi( + @SlugUuid('connectionId') connectionId: string, + @Param('dashboardId') dashboardId: string, + @Query('tableName') tableName: string, + @MasterPassword() masterPwd: string, + @UserId() userId: string, + @Body() generateDto: GeneratePanelPositionWithAiDto, + ): Promise { + const inputData: GeneratePanelPositionWithAiDs = { + dashboardId, + connectionId, + masterPassword: masterPwd, + userId, + chart_description: generateDto.chart_description, + table_name: tableName, + name: generateDto.name, + }; + return await this.generateWidgetWithAiUseCase.execute(inputData, InTransactionEnum.ON); + } } diff --git a/backend/src/entities/visualizations/dashboard-widget/dashboard-widget.entity.ts b/backend/src/entities/visualizations/panel-position/panel-position.entity.ts similarity index 74% rename from backend/src/entities/visualizations/dashboard-widget/dashboard-widget.entity.ts rename to backend/src/entities/visualizations/panel-position/panel-position.entity.ts index 6c1d75929..2c61bb1cf 100644 --- a/backend/src/entities/visualizations/dashboard-widget/dashboard-widget.entity.ts +++ b/backend/src/entities/visualizations/panel-position/panel-position.entity.ts @@ -1,9 +1,9 @@ import { Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn, Relation } from 'typeorm'; import { DashboardEntity } from '../dashboard/dashboard.entity.js'; -import { SavedDbQueryEntity } from '../saved-db-query/saved-db-query.entity.js'; +import { PanelEntity } from '../panel/panel.entity.js'; -@Entity('dashboard_widget') -export class DashboardWidgetEntity { +@Entity('panel_position') +export class PanelPositionEntity { @PrimaryGeneratedColumn('uuid') id: string; @@ -33,7 +33,7 @@ export class DashboardWidgetEntity { @Column({ type: 'uuid', nullable: true }) query_id: string | null; - @ManyToOne(() => SavedDbQueryEntity, { onDelete: 'SET NULL', nullable: true }) + @ManyToOne(() => PanelEntity, { onDelete: 'SET NULL', nullable: true }) @JoinColumn({ name: 'query_id' }) - query: Relation | null; + query: Relation | null; } diff --git a/backend/src/entities/visualizations/dashboard-widget/dashboard-widget.module.ts b/backend/src/entities/visualizations/panel-position/panel-position.module.ts similarity index 61% rename from backend/src/entities/visualizations/dashboard-widget/dashboard-widget.module.ts rename to backend/src/entities/visualizations/panel-position/panel-position.module.ts index 6f2970c90..6aff8301b 100644 --- a/backend/src/entities/visualizations/dashboard-widget/dashboard-widget.module.ts +++ b/backend/src/entities/visualizations/panel-position/panel-position.module.ts @@ -1,17 +1,19 @@ import { MiddlewareConsumer, Module, RequestMethod } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { AICoreModule } from '../../../ai-core/ai-core.module.js'; import { AuthMiddleware } from '../../../authorization/auth.middleware.js'; import { GlobalDatabaseContext } from '../../../common/application/global-database-context.js'; import { BaseType, UseCaseType } from '../../../common/data-injection.tokens.js'; import { LogOutEntity } from '../../log-out/log-out.entity.js'; import { UserEntity } from '../../user/user.entity.js'; -import { DashboardWidgetController } from './dashboard-widgets.controller.js'; -import { CreateDashboardWidgetUseCase } from './use-cases/create-dashboard-widget.use.case.js'; -import { DeleteDashboardWidgetUseCase } from './use-cases/delete-dashboard-widget.use.case.js'; -import { UpdateDashboardWidgetUseCase } from './use-cases/update-dashboard-widget.use.case.js'; +import { DashboardWidgetController } from './panel-position.controller.js'; +import { CreatePanelPositionUseCase } from './use-cases/create-panel-position.use.case.js'; +import { DeletePanelPositionUseCase } from './use-cases/delete-panel-position.use.case.js'; +import { GeneratePanelPositionWithAiUseCase } from './use-cases/generate-panel-position-with-ai.use.case.js'; +import { UpdateDashboardWidgetUseCase } from './use-cases/update-panel-position.use.case.js'; @Module({ - imports: [TypeOrmModule.forFeature([UserEntity, LogOutEntity])], + imports: [TypeOrmModule.forFeature([UserEntity, LogOutEntity]), AICoreModule], providers: [ { provide: BaseType.GLOBAL_DB_CONTEXT, @@ -19,7 +21,7 @@ import { UpdateDashboardWidgetUseCase } from './use-cases/update-dashboard-widge }, { provide: UseCaseType.CREATE_DASHBOARD_WIDGET, - useClass: CreateDashboardWidgetUseCase, + useClass: CreatePanelPositionUseCase, }, { provide: UseCaseType.UPDATE_DASHBOARD_WIDGET, @@ -27,18 +29,23 @@ import { UpdateDashboardWidgetUseCase } from './use-cases/update-dashboard-widge }, { provide: UseCaseType.DELETE_DASHBOARD_WIDGET, - useClass: DeleteDashboardWidgetUseCase, + useClass: DeletePanelPositionUseCase, + }, + { + provide: UseCaseType.GENERATE_WIDGET_WITH_AI, + useClass: GeneratePanelPositionWithAiUseCase, }, ], controllers: [DashboardWidgetController], exports: [], }) -export class DashboardWidgetModule { +export class PanelPositionModule { public configure(consumer: MiddlewareConsumer): void { consumer .apply(AuthMiddleware) .forRoutes( { path: '/dashboard/:dashboardId/widget/:connectionId', method: RequestMethod.POST }, + { path: '/dashboard/:dashboardId/widget/generate/:connectionId', method: RequestMethod.POST }, { path: '/dashboard/:dashboardId/widget/:widgetId/:connectionId', method: RequestMethod.PUT }, { path: '/dashboard/:dashboardId/widget/:widgetId/:connectionId', method: RequestMethod.DELETE }, ); diff --git a/backend/src/entities/visualizations/panel-position/repository/panel-position-custom-repository-extension.ts b/backend/src/entities/visualizations/panel-position/repository/panel-position-custom-repository-extension.ts new file mode 100644 index 000000000..19e9f0573 --- /dev/null +++ b/backend/src/entities/visualizations/panel-position/repository/panel-position-custom-repository-extension.ts @@ -0,0 +1,29 @@ +import { PanelPositionEntity } from '../panel-position.entity.js'; +import { IPanelPositionRepository } from './panel-position.repository.interface.js'; + +export const panelPositionCustomRepositoryExtension: IPanelPositionRepository = { + async findPanelPositionById(panelId: string): Promise { + return await this.findOne({ where: { id: panelId } }); + }, + + async findPanelPositionByIdAndDashboardId(panelId: string, dashboardId: string): Promise { + return await this.findOne({ + where: { id: panelId, dashboard_id: dashboardId }, + }); + }, + + async findAllPanelPositionsByDashboardId(dashboardId: string): Promise { + return await this.find({ + where: { dashboard_id: dashboardId }, + order: { position_y: 'ASC', position_x: 'ASC' }, + }); + }, + + async savePanelPosition(panelPosition: PanelPositionEntity): Promise { + return await this.save(panelPosition); + }, + + async removePanelPosition(panelPosition: PanelPositionEntity): Promise { + return await this.remove(panelPosition); + }, +}; diff --git a/backend/src/entities/visualizations/panel-position/repository/panel-position.repository.interface.ts b/backend/src/entities/visualizations/panel-position/repository/panel-position.repository.interface.ts new file mode 100644 index 000000000..a421b1aa3 --- /dev/null +++ b/backend/src/entities/visualizations/panel-position/repository/panel-position.repository.interface.ts @@ -0,0 +1,9 @@ +import { PanelPositionEntity } from '../panel-position.entity.js'; + +export interface IPanelPositionRepository { + findPanelPositionById(panelId: string): Promise; + findPanelPositionByIdAndDashboardId(panelId: string, dashboardId: string): Promise; + findAllPanelPositionsByDashboardId(dashboardId: string): Promise; + savePanelPosition(panel: PanelPositionEntity): Promise; + removePanelPosition(panel: PanelPositionEntity): Promise; +} diff --git a/backend/src/entities/visualizations/dashboard-widget/use-cases/create-dashboard-widget.use.case.ts b/backend/src/entities/visualizations/panel-position/use-cases/create-panel-position.use.case.ts similarity index 50% rename from backend/src/entities/visualizations/dashboard-widget/use-cases/create-dashboard-widget.use.case.ts rename to backend/src/entities/visualizations/panel-position/use-cases/create-panel-position.use.case.ts index 33d3ef91d..28facaf11 100644 --- a/backend/src/entities/visualizations/dashboard-widget/use-cases/create-dashboard-widget.use.case.ts +++ b/backend/src/entities/visualizations/panel-position/use-cases/create-panel-position.use.case.ts @@ -3,16 +3,16 @@ import AbstractUseCase from '../../../../common/abstract-use.case.js'; import { IGlobalDatabaseContext } from '../../../../common/application/global-database-context.interface.js'; import { BaseType } from '../../../../common/data-injection.tokens.js'; import { Messages } from '../../../../exceptions/text/messages.js'; -import { DashboardWidgetEntity } from '../dashboard-widget.entity.js'; -import { CreateDashboardWidgetDs } from '../data-structures/create-dashboard-widget.ds.js'; -import { FoundDashboardWidgetDto } from '../dto/found-dashboard-widget.dto.js'; -import { buildFoundDashboardWidgetDto } from '../utils/build-found-dashboard-widget-dto.util.js'; -import { ICreateDashboardWidget } from './dashboard-widget-use-cases.interface.js'; +import { PanelPositionEntity } from '../panel-position.entity.js'; +import { CreatePanelPositionDs } from '../data-structures/create-panel-position.ds.js'; +import { FoundPanelPositionDto } from '../dto/found-panel-position.dto.js'; +import { buildFoundPanelPositionDto } from '../utils/build-found-dashboard-widget-dto.util.js'; +import { ICreatePanelPositionWidget } from './panel-position-use-cases.interface.js'; @Injectable({ scope: Scope.REQUEST }) -export class CreateDashboardWidgetUseCase - extends AbstractUseCase - implements ICreateDashboardWidget +export class CreatePanelPositionUseCase + extends AbstractUseCase + implements ICreatePanelPositionWidget { constructor( @Inject(BaseType.GLOBAL_DB_CONTEXT) @@ -21,7 +21,7 @@ export class CreateDashboardWidgetUseCase super(); } - public async implementation(inputData: CreateDashboardWidgetDs): Promise { + public async implementation(inputData: CreatePanelPositionDs): Promise { const { dashboardId, connectionId, masterPassword, query_id, position_x, position_y, width, height } = inputData; const foundConnection = await this._dbContext.connectionRepository.findAndDecryptConnection( @@ -42,26 +42,22 @@ export class CreateDashboardWidgetUseCase throw new NotFoundException(Messages.DASHBOARD_NOT_FOUND); } - // Validate query_id if provided if (query_id) { - const foundQuery = await this._dbContext.savedDbQueryRepository.findQueryByIdAndConnectionId( - query_id, - connectionId, - ); + const foundQuery = await this._dbContext.panelRepository.findQueryByIdAndConnectionId(query_id, connectionId); if (!foundQuery) { throw new BadRequestException(Messages.SAVED_QUERY_NOT_FOUND); } } - const newWidget = new DashboardWidgetEntity(); - newWidget.position_x = position_x ?? 0; - newWidget.position_y = position_y ?? 0; - newWidget.width = width ?? 4; - newWidget.height = height ?? 3; - newWidget.dashboard_id = dashboardId; - newWidget.query_id = query_id || null; + const newPanelPosition = new PanelPositionEntity(); + newPanelPosition.position_x = position_x ?? 0; + newPanelPosition.position_y = position_y ?? 0; + newPanelPosition.width = width ?? 4; + newPanelPosition.height = height ?? 3; + newPanelPosition.dashboard_id = dashboardId; + newPanelPosition.query_id = query_id || null; - const savedWidget = await this._dbContext.dashboardWidgetRepository.saveWidget(newWidget); - return buildFoundDashboardWidgetDto(savedWidget); + const savedWidget = await this._dbContext.panelPositionRepository.savePanelPosition(newPanelPosition); + return buildFoundPanelPositionDto(savedWidget); } } diff --git a/backend/src/entities/visualizations/dashboard-widget/use-cases/delete-dashboard-widget.use.case.ts b/backend/src/entities/visualizations/panel-position/use-cases/delete-panel-position.use.case.ts similarity index 59% rename from backend/src/entities/visualizations/dashboard-widget/use-cases/delete-dashboard-widget.use.case.ts rename to backend/src/entities/visualizations/panel-position/use-cases/delete-panel-position.use.case.ts index 056c90f20..c6f5631b5 100644 --- a/backend/src/entities/visualizations/dashboard-widget/use-cases/delete-dashboard-widget.use.case.ts +++ b/backend/src/entities/visualizations/panel-position/use-cases/delete-panel-position.use.case.ts @@ -3,15 +3,15 @@ import AbstractUseCase from '../../../../common/abstract-use.case.js'; import { IGlobalDatabaseContext } from '../../../../common/application/global-database-context.interface.js'; import { BaseType } from '../../../../common/data-injection.tokens.js'; import { Messages } from '../../../../exceptions/text/messages.js'; -import { DeleteDashboardWidgetDs } from '../data-structures/delete-dashboard-widget.ds.js'; -import { FoundDashboardWidgetDto } from '../dto/found-dashboard-widget.dto.js'; -import { buildFoundDashboardWidgetDto } from '../utils/build-found-dashboard-widget-dto.util.js'; -import { IDeleteDashboardWidget } from './dashboard-widget-use-cases.interface.js'; +import { DeletePanelPositionDs } from '../data-structures/delete-panel-position.ds.js'; +import { FoundPanelPositionDto } from '../dto/found-panel-position.dto.js'; +import { buildFoundPanelPositionDto } from '../utils/build-found-dashboard-widget-dto.util.js'; +import { IDeletePanelPosition } from './panel-position-use-cases.interface.js'; @Injectable({ scope: Scope.REQUEST }) -export class DeleteDashboardWidgetUseCase - extends AbstractUseCase - implements IDeleteDashboardWidget +export class DeletePanelPositionUseCase + extends AbstractUseCase + implements IDeletePanelPosition { constructor( @Inject(BaseType.GLOBAL_DB_CONTEXT) @@ -20,7 +20,7 @@ export class DeleteDashboardWidgetUseCase super(); } - public async implementation(inputData: DeleteDashboardWidgetDs): Promise { + public async implementation(inputData: DeletePanelPositionDs): Promise { const { widgetId, dashboardId, connectionId, masterPassword } = inputData; const foundConnection = await this._dbContext.connectionRepository.findAndDecryptConnection( @@ -41,7 +41,7 @@ export class DeleteDashboardWidgetUseCase throw new NotFoundException(Messages.DASHBOARD_NOT_FOUND); } - const foundWidget = await this._dbContext.dashboardWidgetRepository.findWidgetByIdAndDashboardId( + const foundWidget = await this._dbContext.panelPositionRepository.findPanelPositionByIdAndDashboardId( widgetId, dashboardId, ); @@ -50,8 +50,8 @@ export class DeleteDashboardWidgetUseCase throw new NotFoundException(Messages.DASHBOARD_WIDGET_NOT_FOUND); } - const widgetDto = buildFoundDashboardWidgetDto(foundWidget); - await this._dbContext.dashboardWidgetRepository.removeWidget(foundWidget); + const widgetDto = buildFoundPanelPositionDto(foundWidget); + await this._dbContext.panelPositionRepository.removePanelPosition(foundWidget); return widgetDto; } diff --git a/backend/src/entities/visualizations/panel-position/use-cases/generate-panel-position-with-ai.use.case.ts b/backend/src/entities/visualizations/panel-position/use-cases/generate-panel-position-with-ai.use.case.ts new file mode 100644 index 000000000..895fba20a --- /dev/null +++ b/backend/src/entities/visualizations/panel-position/use-cases/generate-panel-position-with-ai.use.case.ts @@ -0,0 +1,244 @@ +import { BadRequestException, Inject, Injectable, NotFoundException, Scope } from '@nestjs/common'; +import { getDataAccessObject } from '@rocketadmin/shared-code/dist/src/data-access-layer/shared/create-data-access-object.js'; +import { ConnectionTypesEnum } from '@rocketadmin/shared-code/dist/src/shared/enums/connection-types-enum.js'; +import AbstractUseCase from '../../../../common/abstract-use.case.js'; +import { IGlobalDatabaseContext } from '../../../../common/application/global-database-context.interface.js'; +import { BaseType } from '../../../../common/data-injection.tokens.js'; +import { DashboardWidgetTypeEnum } from '../../../../enums/dashboard-widget-type.enum.js'; +import { Messages } from '../../../../exceptions/text/messages.js'; +import { AICoreService, AIProviderType, cleanAIJsonResponse } from '../../../../ai-core/index.js'; +import { PanelPositionEntity } from '../panel-position.entity.js'; +import { GeneratePanelPositionWithAiDs } from '../data-structures/generate-panel-position-with-ai.ds.js'; +import { FoundPanelPositionDto } from '../dto/found-panel-position.dto.js'; +import { buildFoundPanelPositionDto } from '../utils/build-found-dashboard-widget-dto.util.js'; +import { IGeneratePanelPositionWithAi } from './panel-position-use-cases.interface.js'; +import { PanelEntity } from '../../panel/panel.entity.js'; +import { validateQuerySafety } from '../../panel/utils/check-query-is-safe.util.js'; + +interface AIGeneratedWidgetResponse { + name: string; + description: string; + query_text: string; + widget_type: 'chart' | 'table' | 'counter' | 'text'; + chart_type?: 'bar' | 'line' | 'pie' | 'doughnut' | 'polarArea'; + widget_options?: { + label_column?: string; + value_column?: string; + series?: Array<{ + value_column: string; + label?: string; + color?: string; + }>; + stacked?: boolean; + horizontal?: boolean; + show_data_labels?: boolean; + legend?: { + show?: boolean; + position?: 'top' | 'bottom' | 'left' | 'right'; + }; + }; +} + +@Injectable({ scope: Scope.REQUEST }) +export class GeneratePanelPositionWithAiUseCase + extends AbstractUseCase + implements IGeneratePanelPositionWithAi +{ + constructor( + @Inject(BaseType.GLOBAL_DB_CONTEXT) + protected _dbContext: IGlobalDatabaseContext, + private readonly aiCoreService: AICoreService, + ) { + super(); + } + + public async implementation(inputData: GeneratePanelPositionWithAiDs): Promise { + const { + dashboardId, + connectionId, + masterPassword, + chart_description, + table_name, + name, + position_x, + position_y, + width, + height, + } = inputData; + + const foundConnection = await this._dbContext.connectionRepository.findAndDecryptConnection( + connectionId, + masterPassword, + ); + + if (!foundConnection) { + throw new NotFoundException(Messages.CONNECTION_NOT_FOUND); + } + + const foundDashboard = await this._dbContext.dashboardRepository.findDashboardByIdAndConnectionId( + dashboardId, + connectionId, + ); + + if (!foundDashboard) { + throw new NotFoundException(Messages.DASHBOARD_NOT_FOUND); + } + + const dao = getDataAccessObject(foundConnection); + + let tableInfo: { + table_name: string; + columns: Array<{ name: string; type: string; nullable: boolean }>; + }; + + try { + const structure = await dao.getTableStructure(table_name, null); + tableInfo = { + table_name: table_name, + columns: structure.map((col) => ({ + name: col.column_name, + type: col.data_type, + nullable: col.allow_null, + })), + }; + } catch (error) { + throw new BadRequestException(`Failed to get table structure for "${table_name}": ${error.message}`); + } + + if (tableInfo.columns.length === 0) { + throw new BadRequestException(`The specified table "${table_name}" does not have any columns or does not exist.`); + } + + const prompt = this.buildPrompt(chart_description, tableInfo, foundConnection.type); + + const aiResponse = await this.aiCoreService.completeWithProvider(AIProviderType.BEDROCK, prompt, { + temperature: 0.3, + }); + + const generatedWidget = this.parseAIResponse(aiResponse); + + validateQuerySafety(generatedWidget.query_text, foundConnection.type as ConnectionTypesEnum); + + const newPanel = new PanelEntity(); + newPanel.name = name || generatedWidget.name; + newPanel.description = generatedWidget.description || null; + newPanel.panel_type = this.mapWidgetType(generatedWidget.widget_type); + newPanel.chart_type = generatedWidget.chart_type || null; + newPanel.panel_options = generatedWidget.widget_options + ? (generatedWidget.widget_options as unknown as string) + : null; + newPanel.query_text = generatedWidget.query_text; + newPanel.connection_id = foundConnection.id; + + const savedQuery = await this._dbContext.panelRepository.save(newPanel); + + const newPanelPosition = new PanelPositionEntity(); + newPanelPosition.position_x = position_x ?? 0; + newPanelPosition.position_y = position_y ?? 0; + newPanelPosition.width = width ?? 6; + newPanelPosition.height = height ?? 4; + newPanelPosition.dashboard_id = dashboardId; + newPanelPosition.query_id = savedQuery.id; + + const savedWidget = await this._dbContext.panelPositionRepository.savePanelPosition(newPanelPosition); + return buildFoundPanelPositionDto(savedWidget); + } + + private buildPrompt( + chartDescription: string, + tableInfo: { table_name: string; columns: Array<{ name: string; type: string; nullable: boolean }> }, + databaseType: string, + ): string { + const schemaDescription = `Table: ${tableInfo.table_name}\n Columns:\n${tableInfo.columns.map((col) => ` - ${col.name}: ${col.type}${col.nullable ? ' (nullable)' : ''}`).join('\n')}`; + + return `You are a database analytics assistant. Based on the user's chart description and the database schema, generate the SQL query and chart configuration. + +DATABASE TYPE: ${databaseType} + +DATABASE SCHEMA: +${schemaDescription} + +USER'S CHART DESCRIPTION: +"${chartDescription}" + +Generate a JSON response with the following structure: +{ + "name": "Short descriptive name for the chart (max 50 chars)", + "description": "Brief description of what the chart shows", + "query_text": "SELECT query that returns data for the chart. The query should return columns that can be used for labels and values. Use appropriate aggregations (COUNT, SUM, AVG, etc.) and GROUP BY clauses as needed. Always use the table name '${tableInfo.table_name}'.", + "widget_type": "chart" | "table" | "counter" | "text", + "chart_type": "bar" | "line" | "pie" | "doughnut" | "polarArea", + "widget_options": { + "label_column": "column name for labels/categories (x-axis)", + "value_column": "column name for values (y-axis) - use this for single series", + "series": [ + { + "value_column": "column name", + "label": "Series label", + "color": "#hex_color" + } + ], + "stacked": false, + "horizontal": false, + "show_data_labels": true, + "legend": { + "show": true, + "position": "top" + } + } +} + +IMPORTANT GUIDELINES: +1. Write valid ${databaseType} SQL syntax +2. Use appropriate column aliases that are clear and descriptive +3. For charts, ensure the query returns data suitable for visualization: + - For bar/line charts: one column for labels, one or more for values + - For pie/doughnut: one column for labels, one for values + - For counter: single value result +4. Choose chart_type based on the data characteristics: + - bar: comparisons between categories + - line: trends over time + - pie/doughnut: parts of a whole (percentages) + - polarArea: similar to pie but with equal angles +5. Use meaningful colors from this palette: #3366CC, #DC3912, #FF9900, #109618, #990099, #0099C6, #DD4477, #66AA00 +6. Set widget_options.label_column to the column that should be used for labels +7. For single value series, use value_column directly +8. For multiple series, use the series array + +Respond ONLY with the JSON object, no additional text or explanation.`; + } + + private parseAIResponse(aiResponse: string): AIGeneratedWidgetResponse { + try { + const cleanedResponse = cleanAIJsonResponse(aiResponse); + const parsed = JSON.parse(cleanedResponse) as AIGeneratedWidgetResponse; + + if (!parsed.name || !parsed.query_text || !parsed.widget_type) { + throw new Error('Missing required fields in AI response'); + } + + return parsed; + } catch (error) { + console.error('Error parsing AI response:', error.message); + console.error('AI Response:', aiResponse); + throw new BadRequestException( + 'Failed to generate widget configuration from AI. Please try again with a different description.', + ); + } + } + + private mapWidgetType(type: string): DashboardWidgetTypeEnum { + switch (type) { + case 'chart': + return DashboardWidgetTypeEnum.Chart; + case 'table': + return DashboardWidgetTypeEnum.Table; + case 'counter': + return DashboardWidgetTypeEnum.Counter; + case 'text': + return DashboardWidgetTypeEnum.Text; + default: + return DashboardWidgetTypeEnum.Chart; + } + } +} diff --git a/backend/src/entities/visualizations/panel-position/use-cases/panel-position-use-cases.interface.ts b/backend/src/entities/visualizations/panel-position/use-cases/panel-position-use-cases.interface.ts new file mode 100644 index 000000000..1e878480e --- /dev/null +++ b/backend/src/entities/visualizations/panel-position/use-cases/panel-position-use-cases.interface.ts @@ -0,0 +1,22 @@ +import { InTransactionEnum } from '../../../../enums/in-transaction.enum.js'; +import { CreatePanelPositionDs } from '../data-structures/create-panel-position.ds.js'; +import { DeletePanelPositionDs } from '../data-structures/delete-panel-position.ds.js'; +import { GeneratePanelPositionWithAiDs } from '../data-structures/generate-panel-position-with-ai.ds.js'; +import { UpdatePanelPositionDs } from '../data-structures/update-panel-position.ds.js'; +import { FoundPanelPositionDto } from '../dto/found-panel-position.dto.js'; + +export interface ICreatePanelPositionWidget { + execute(inputData: CreatePanelPositionDs, inTransaction: InTransactionEnum): Promise; +} + +export interface IUpdatePanelPosition { + execute(inputData: UpdatePanelPositionDs, inTransaction: InTransactionEnum): Promise; +} + +export interface IDeletePanelPosition { + execute(inputData: DeletePanelPositionDs, inTransaction: InTransactionEnum): Promise; +} + +export interface IGeneratePanelPositionWithAi { + execute(inputData: GeneratePanelPositionWithAiDs, inTransaction: InTransactionEnum): Promise; +} diff --git a/backend/src/entities/visualizations/dashboard-widget/use-cases/update-dashboard-widget.use.case.ts b/backend/src/entities/visualizations/panel-position/use-cases/update-panel-position.use.case.ts similarity index 66% rename from backend/src/entities/visualizations/dashboard-widget/use-cases/update-dashboard-widget.use.case.ts rename to backend/src/entities/visualizations/panel-position/use-cases/update-panel-position.use.case.ts index 710495a85..ba0151fa9 100644 --- a/backend/src/entities/visualizations/dashboard-widget/use-cases/update-dashboard-widget.use.case.ts +++ b/backend/src/entities/visualizations/panel-position/use-cases/update-panel-position.use.case.ts @@ -3,15 +3,15 @@ import AbstractUseCase from '../../../../common/abstract-use.case.js'; import { IGlobalDatabaseContext } from '../../../../common/application/global-database-context.interface.js'; import { BaseType } from '../../../../common/data-injection.tokens.js'; import { Messages } from '../../../../exceptions/text/messages.js'; -import { UpdateDashboardWidgetDs } from '../data-structures/update-dashboard-widget.ds.js'; -import { FoundDashboardWidgetDto } from '../dto/found-dashboard-widget.dto.js'; -import { buildFoundDashboardWidgetDto } from '../utils/build-found-dashboard-widget-dto.util.js'; -import { IUpdateDashboardWidget } from './dashboard-widget-use-cases.interface.js'; +import { UpdatePanelPositionDs } from '../data-structures/update-panel-position.ds.js'; +import { FoundPanelPositionDto } from '../dto/found-panel-position.dto.js'; +import { buildFoundPanelPositionDto } from '../utils/build-found-dashboard-widget-dto.util.js'; +import { IUpdatePanelPosition } from './panel-position-use-cases.interface.js'; @Injectable({ scope: Scope.REQUEST }) export class UpdateDashboardWidgetUseCase - extends AbstractUseCase - implements IUpdateDashboardWidget + extends AbstractUseCase + implements IUpdatePanelPosition { constructor( @Inject(BaseType.GLOBAL_DB_CONTEXT) @@ -20,7 +20,7 @@ export class UpdateDashboardWidgetUseCase super(); } - public async implementation(inputData: UpdateDashboardWidgetDs): Promise { + public async implementation(inputData: UpdatePanelPositionDs): Promise { const { widgetId, dashboardId, connectionId, masterPassword, query_id, position_x, position_y, width, height } = inputData; @@ -42,7 +42,7 @@ export class UpdateDashboardWidgetUseCase throw new NotFoundException(Messages.DASHBOARD_NOT_FOUND); } - const foundWidget = await this._dbContext.dashboardWidgetRepository.findWidgetByIdAndDashboardId( + const foundWidget = await this._dbContext.panelPositionRepository.findPanelPositionByIdAndDashboardId( widgetId, dashboardId, ); @@ -53,10 +53,7 @@ export class UpdateDashboardWidgetUseCase // Validate query_id if provided if (query_id !== undefined && query_id !== null) { - const foundQuery = await this._dbContext.savedDbQueryRepository.findQueryByIdAndConnectionId( - query_id, - connectionId, - ); + const foundQuery = await this._dbContext.panelRepository.findQueryByIdAndConnectionId(query_id, connectionId); if (!foundQuery) { throw new BadRequestException(Messages.SAVED_QUERY_NOT_FOUND); } @@ -76,7 +73,7 @@ export class UpdateDashboardWidgetUseCase foundWidget.height = height; } - const savedWidget = await this._dbContext.dashboardWidgetRepository.saveWidget(foundWidget); - return buildFoundDashboardWidgetDto(savedWidget); + const savedWidget = await this._dbContext.panelPositionRepository.savePanelPosition(foundWidget); + return buildFoundPanelPositionDto(savedWidget); } } diff --git a/backend/src/entities/visualizations/panel-position/utils/build-found-dashboard-widget-dto.util.ts b/backend/src/entities/visualizations/panel-position/utils/build-found-dashboard-widget-dto.util.ts new file mode 100644 index 000000000..8c0ffe9e6 --- /dev/null +++ b/backend/src/entities/visualizations/panel-position/utils/build-found-dashboard-widget-dto.util.ts @@ -0,0 +1,14 @@ +import { PanelPositionEntity } from '../panel-position.entity.js'; +import { FoundPanelPositionDto } from '../dto/found-panel-position.dto.js'; + +export function buildFoundPanelPositionDto(panelPosition: PanelPositionEntity): FoundPanelPositionDto { + return { + id: panelPosition.id, + position_x: panelPosition.position_x, + position_y: panelPosition.position_y, + width: panelPosition.width, + height: panelPosition.height, + dashboard_id: panelPosition.dashboard_id, + query_id: panelPosition.query_id, + }; +} diff --git a/backend/src/entities/visualizations/saved-db-query/data-structures/create-saved-db-query.ds.ts b/backend/src/entities/visualizations/panel/data-structures/create-panel.ds.ts similarity index 89% rename from backend/src/entities/visualizations/saved-db-query/data-structures/create-saved-db-query.ds.ts rename to backend/src/entities/visualizations/panel/data-structures/create-panel.ds.ts index f9a3acdd5..af62dddb5 100644 --- a/backend/src/entities/visualizations/saved-db-query/data-structures/create-saved-db-query.ds.ts +++ b/backend/src/entities/visualizations/panel/data-structures/create-panel.ds.ts @@ -1,6 +1,6 @@ import { DashboardWidgetTypeEnum } from '../../../../enums/dashboard-widget-type.enum.js'; -export class CreateSavedDbQueryDs { +export class CreatePanelDs { connectionId: string; masterPassword: string; name: string; diff --git a/backend/src/entities/visualizations/saved-db-query/data-structures/execute-saved-db-query.ds.ts b/backend/src/entities/visualizations/panel/data-structures/execute-saved-db-query.ds.ts similarity index 100% rename from backend/src/entities/visualizations/saved-db-query/data-structures/execute-saved-db-query.ds.ts rename to backend/src/entities/visualizations/panel/data-structures/execute-saved-db-query.ds.ts diff --git a/backend/src/entities/visualizations/saved-db-query/data-structures/find-all-saved-db-queries.ds.ts b/backend/src/entities/visualizations/panel/data-structures/find-all-panels.ds.ts similarity index 56% rename from backend/src/entities/visualizations/saved-db-query/data-structures/find-all-saved-db-queries.ds.ts rename to backend/src/entities/visualizations/panel/data-structures/find-all-panels.ds.ts index a8f8b43e0..fd61907c4 100644 --- a/backend/src/entities/visualizations/saved-db-query/data-structures/find-all-saved-db-queries.ds.ts +++ b/backend/src/entities/visualizations/panel/data-structures/find-all-panels.ds.ts @@ -1,4 +1,4 @@ -export class FindAllSavedDbQueriesDs { +export class FindAllPanelsDs { connectionId: string; masterPassword: string; } diff --git a/backend/src/entities/visualizations/saved-db-query/data-structures/find-saved-db-query.ds.ts b/backend/src/entities/visualizations/panel/data-structures/find-panel.ds.ts similarity index 100% rename from backend/src/entities/visualizations/saved-db-query/data-structures/find-saved-db-query.ds.ts rename to backend/src/entities/visualizations/panel/data-structures/find-panel.ds.ts diff --git a/backend/src/entities/visualizations/saved-db-query/data-structures/test-db-query.ds.ts b/backend/src/entities/visualizations/panel/data-structures/test-db-query.ds.ts similarity index 100% rename from backend/src/entities/visualizations/saved-db-query/data-structures/test-db-query.ds.ts rename to backend/src/entities/visualizations/panel/data-structures/test-db-query.ds.ts diff --git a/backend/src/entities/visualizations/saved-db-query/data-structures/update-saved-db-query.ds.ts b/backend/src/entities/visualizations/panel/data-structures/update-panel.ds.ts similarity index 90% rename from backend/src/entities/visualizations/saved-db-query/data-structures/update-saved-db-query.ds.ts rename to backend/src/entities/visualizations/panel/data-structures/update-panel.ds.ts index 7dedb89ae..aa4e78000 100644 --- a/backend/src/entities/visualizations/saved-db-query/data-structures/update-saved-db-query.ds.ts +++ b/backend/src/entities/visualizations/panel/data-structures/update-panel.ds.ts @@ -1,6 +1,6 @@ import { DashboardWidgetTypeEnum } from '../../../../enums/dashboard-widget-type.enum.js'; -export class UpdateSavedDbQueryDs { +export class UpdatePanelDs { queryId: string; connectionId: string; masterPassword: string; diff --git a/backend/src/entities/visualizations/saved-db-query/dto/create-saved-db-query.dto.ts b/backend/src/entities/visualizations/panel/dto/create-saved-db-query.dto.ts similarity index 100% rename from backend/src/entities/visualizations/saved-db-query/dto/create-saved-db-query.dto.ts rename to backend/src/entities/visualizations/panel/dto/create-saved-db-query.dto.ts diff --git a/backend/src/entities/visualizations/saved-db-query/dto/execute-saved-db-query-result.dto.ts b/backend/src/entities/visualizations/panel/dto/execute-saved-db-query-result.dto.ts similarity index 100% rename from backend/src/entities/visualizations/saved-db-query/dto/execute-saved-db-query-result.dto.ts rename to backend/src/entities/visualizations/panel/dto/execute-saved-db-query-result.dto.ts diff --git a/backend/src/entities/visualizations/saved-db-query/dto/found-saved-db-query.dto.ts b/backend/src/entities/visualizations/panel/dto/found-saved-db-query.dto.ts similarity index 96% rename from backend/src/entities/visualizations/saved-db-query/dto/found-saved-db-query.dto.ts rename to backend/src/entities/visualizations/panel/dto/found-saved-db-query.dto.ts index 9b39bbe18..2565f5c90 100644 --- a/backend/src/entities/visualizations/saved-db-query/dto/found-saved-db-query.dto.ts +++ b/backend/src/entities/visualizations/panel/dto/found-saved-db-query.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { DashboardWidgetTypeEnum } from '../../../../enums/dashboard-widget-type.enum.js'; -export class FoundSavedDbQueryDto { +export class FoundPanelDto { @ApiProperty({ type: String, description: 'The entity id' }) id: string; diff --git a/backend/src/entities/visualizations/saved-db-query/dto/test-db-query-result.dto.ts b/backend/src/entities/visualizations/panel/dto/test-db-query-result.dto.ts similarity index 100% rename from backend/src/entities/visualizations/saved-db-query/dto/test-db-query-result.dto.ts rename to backend/src/entities/visualizations/panel/dto/test-db-query-result.dto.ts diff --git a/backend/src/entities/visualizations/saved-db-query/dto/test-db-query.dto.ts b/backend/src/entities/visualizations/panel/dto/test-db-query.dto.ts similarity index 100% rename from backend/src/entities/visualizations/saved-db-query/dto/test-db-query.dto.ts rename to backend/src/entities/visualizations/panel/dto/test-db-query.dto.ts diff --git a/backend/src/entities/visualizations/saved-db-query/dto/update-saved-db-query.dto.ts b/backend/src/entities/visualizations/panel/dto/update-saved-db-query.dto.ts similarity index 100% rename from backend/src/entities/visualizations/saved-db-query/dto/update-saved-db-query.dto.ts rename to backend/src/entities/visualizations/panel/dto/update-saved-db-query.dto.ts diff --git a/backend/src/entities/visualizations/saved-db-query/saved-db-query.controller.ts b/backend/src/entities/visualizations/panel/panel.controller.ts similarity index 85% rename from backend/src/entities/visualizations/saved-db-query/saved-db-query.controller.ts rename to backend/src/entities/visualizations/panel/panel.controller.ts index 19dccf049..67b177d7e 100644 --- a/backend/src/entities/visualizations/saved-db-query/saved-db-query.controller.ts +++ b/backend/src/entities/visualizations/panel/panel.controller.ts @@ -25,27 +25,27 @@ import { Messages } from '../../../exceptions/text/messages.js'; import { ConnectionEditGuard } from '../../../guards/connection-edit.guard.js'; import { ConnectionReadGuard } from '../../../guards/connection-read.guard.js'; import { SentryInterceptor } from '../../../interceptors/sentry.interceptor.js'; -import { CreateSavedDbQueryDs } from './data-structures/create-saved-db-query.ds.js'; +import { CreatePanelDs } from './data-structures/create-panel.ds.js'; import { ExecuteSavedDbQueryDs } from './data-structures/execute-saved-db-query.ds.js'; -import { FindAllSavedDbQueriesDs } from './data-structures/find-all-saved-db-queries.ds.js'; -import { FindSavedDbQueryDs } from './data-structures/find-saved-db-query.ds.js'; +import { FindAllPanelsDs } from './data-structures/find-all-panels.ds.js'; +import { FindSavedDbQueryDs } from './data-structures/find-panel.ds.js'; import { TestDbQueryDs } from './data-structures/test-db-query.ds.js'; -import { UpdateSavedDbQueryDs } from './data-structures/update-saved-db-query.ds.js'; +import { UpdatePanelDs } from './data-structures/update-panel.ds.js'; import { CreateSavedDbQueryDto } from './dto/create-saved-db-query.dto.js'; import { ExecuteSavedDbQueryResultDto } from './dto/execute-saved-db-query-result.dto.js'; -import { FoundSavedDbQueryDto } from './dto/found-saved-db-query.dto.js'; +import { FoundPanelDto } from './dto/found-saved-db-query.dto.js'; import { TestDbQueryDto } from './dto/test-db-query.dto.js'; import { TestDbQueryResultDto } from './dto/test-db-query-result.dto.js'; import { UpdateSavedDbQueryDto } from './dto/update-saved-db-query.dto.js'; import { - ICreateSavedDbQuery, - IDeleteSavedDbQuery, + ICreatePanel, + IDeletePanel, IExecuteSavedDbQuery, - IFindAllSavedDbQueries, - IFindSavedDbQuery, + IFindAllPanels, + IFindPanel, ITestDbQuery, - IUpdateSavedDbQuery, -} from './use-cases/saved-db-query-use-cases.interface.js'; + IUpdatePanel, +} from './use-cases/panel-use-cases.interface.js'; @UseInterceptors(SentryInterceptor) @Timeout() @@ -56,15 +56,15 @@ import { export class SavedDbQueryController { constructor( @Inject(UseCaseType.CREATE_SAVED_DB_QUERY) - private readonly createSavedDbQueryUseCase: ICreateSavedDbQuery, + private readonly createSavedDbQueryUseCase: ICreatePanel, @Inject(UseCaseType.UPDATE_SAVED_DB_QUERY) - private readonly updateSavedDbQueryUseCase: IUpdateSavedDbQuery, + private readonly updateSavedDbQueryUseCase: IUpdatePanel, @Inject(UseCaseType.FIND_SAVED_DB_QUERY) - private readonly findSavedDbQueryUseCase: IFindSavedDbQuery, + private readonly findSavedDbQueryUseCase: IFindPanel, @Inject(UseCaseType.FIND_ALL_SAVED_DB_QUERIES) - private readonly findAllSavedDbQueriesUseCase: IFindAllSavedDbQueries, + private readonly findAllSavedDbQueriesUseCase: IFindAllPanels, @Inject(UseCaseType.DELETE_SAVED_DB_QUERY) - private readonly deleteSavedDbQueryUseCase: IDeleteSavedDbQuery, + private readonly deleteSavedDbQueryUseCase: IDeletePanel, @Inject(UseCaseType.EXECUTE_SAVED_DB_QUERY) private readonly executeSavedDbQueryUseCase: IExecuteSavedDbQuery, @Inject(UseCaseType.TEST_DB_QUERY) @@ -75,7 +75,7 @@ export class SavedDbQueryController { @ApiResponse({ status: 200, description: 'Saved queries found.', - type: FoundSavedDbQueryDto, + type: FoundPanelDto, isArray: true, }) @ApiParam({ name: 'connectionId', required: true }) @@ -84,7 +84,7 @@ export class SavedDbQueryController { async findAll( @SlugUuid('connectionId') connectionId: string, @MasterPassword() masterPwd: string, - ): Promise { + ): Promise { if (!connectionId) { throw new HttpException( { @@ -93,7 +93,7 @@ export class SavedDbQueryController { HttpStatus.BAD_REQUEST, ); } - const inputData: FindAllSavedDbQueriesDs = { + const inputData: FindAllPanelsDs = { connectionId, masterPassword: masterPwd, }; @@ -104,7 +104,7 @@ export class SavedDbQueryController { @ApiResponse({ status: 200, description: 'Saved query found.', - type: FoundSavedDbQueryDto, + type: FoundPanelDto, }) @ApiParam({ name: 'connectionId', required: true }) @ApiParam({ name: 'queryId', required: true }) @@ -114,7 +114,7 @@ export class SavedDbQueryController { @SlugUuid('connectionId') connectionId: string, @Param('queryId') queryId: string, @MasterPassword() masterPwd: string, - ): Promise { + ): Promise { if (!connectionId) { throw new HttpException( { @@ -135,7 +135,7 @@ export class SavedDbQueryController { @ApiResponse({ status: 201, description: 'Saved query created.', - type: FoundSavedDbQueryDto, + type: FoundPanelDto, }) @ApiBody({ type: CreateSavedDbQueryDto }) @ApiParam({ name: 'connectionId', required: true }) @@ -145,7 +145,7 @@ export class SavedDbQueryController { @SlugUuid('connectionId') connectionId: string, @MasterPassword() masterPwd: string, @Body() createDto: CreateSavedDbQueryDto, - ): Promise { + ): Promise { if (!connectionId) { throw new HttpException( { @@ -154,7 +154,7 @@ export class SavedDbQueryController { HttpStatus.BAD_REQUEST, ); } - const inputData: CreateSavedDbQueryDs = { + const inputData: CreatePanelDs = { connectionId, masterPassword: masterPwd, name: createDto.name, @@ -171,7 +171,7 @@ export class SavedDbQueryController { @ApiResponse({ status: 200, description: 'Saved query updated.', - type: FoundSavedDbQueryDto, + type: FoundPanelDto, }) @ApiBody({ type: UpdateSavedDbQueryDto }) @ApiParam({ name: 'connectionId', required: true }) @@ -183,7 +183,7 @@ export class SavedDbQueryController { @Param('queryId') queryId: string, @MasterPassword() masterPwd: string, @Body() updateDto: UpdateSavedDbQueryDto, - ): Promise { + ): Promise { if (!connectionId) { throw new HttpException( { @@ -192,7 +192,7 @@ export class SavedDbQueryController { HttpStatus.BAD_REQUEST, ); } - const inputData: UpdateSavedDbQueryDs = { + const inputData: UpdatePanelDs = { queryId, connectionId, masterPassword: masterPwd, @@ -210,7 +210,7 @@ export class SavedDbQueryController { @ApiResponse({ status: 200, description: 'Saved query deleted.', - type: FoundSavedDbQueryDto, + type: FoundPanelDto, }) @ApiParam({ name: 'connectionId', required: true }) @ApiParam({ name: 'queryId', required: true }) @@ -220,7 +220,7 @@ export class SavedDbQueryController { @SlugUuid('connectionId') connectionId: string, @Param('queryId') queryId: string, @MasterPassword() masterPwd: string, - ): Promise { + ): Promise { if (!connectionId) { throw new HttpException( { diff --git a/backend/src/entities/visualizations/saved-db-query/saved-db-query.entity.ts b/backend/src/entities/visualizations/panel/panel.entity.ts similarity index 73% rename from backend/src/entities/visualizations/saved-db-query/saved-db-query.entity.ts rename to backend/src/entities/visualizations/panel/panel.entity.ts index 6d8da4232..e5c9fc4eb 100644 --- a/backend/src/entities/visualizations/saved-db-query/saved-db-query.entity.ts +++ b/backend/src/entities/visualizations/panel/panel.entity.ts @@ -14,10 +14,10 @@ import { import { DashboardWidgetTypeEnum } from '../../../enums/dashboard-widget-type.enum.js'; import { Encryptor } from '../../../helpers/encryption/encryptor.js'; import { ConnectionEntity } from '../../connection/connection.entity.js'; -import { DashboardWidgetEntity } from '../dashboard-widget/dashboard-widget.entity.js'; +import { PanelPositionEntity } from '../panel-position/panel-position.entity.js'; -@Entity('saved_db_query') -export class SavedDbQueryEntity { +@Entity('panel') +export class PanelEntity { @PrimaryGeneratedColumn('uuid') id: string; @@ -28,13 +28,13 @@ export class SavedDbQueryEntity { description: string | null; @Column({ type: 'varchar', nullable: true, default: DashboardWidgetTypeEnum.Chart }) - widget_type: DashboardWidgetTypeEnum; + panel_type: DashboardWidgetTypeEnum; @Column({ type: 'varchar', default: null, nullable: true }) chart_type: string | null; @Column('json', { default: null, nullable: true }) - widget_options: string | null; + panel_options: string | null; @Column({ type: 'text', nullable: true, default: null }) query_text: string; @@ -51,8 +51,8 @@ export class SavedDbQueryEntity { this.query_text = Encryptor.encryptData(this.query_text); } try { - if (this.widget_options && typeof this.widget_options === 'object') { - this.widget_options = JSON.stringify(this.widget_options); + if (this.panel_options && typeof this.panel_options === 'object') { + this.panel_options = JSON.stringify(this.panel_options); } } catch (e) { console.error('-> Error widget options stringify ' + e.message); @@ -66,8 +66,8 @@ export class SavedDbQueryEntity { this.query_text = Encryptor.encryptData(this.query_text); } try { - if (this.widget_options && typeof this.widget_options === 'object') { - this.widget_options = JSON.stringify(this.widget_options); + if (this.panel_options && typeof this.panel_options === 'object') { + this.panel_options = JSON.stringify(this.panel_options); } } catch (e) { console.error('-> Error widget options stringify ' + e.message); @@ -80,8 +80,8 @@ export class SavedDbQueryEntity { this.query_text = Encryptor.decryptData(this.query_text); } try { - if (this.widget_options && typeof this.widget_options === 'string') { - this.widget_options = sjson.parse(this.widget_options, null, { + if (this.panel_options && typeof this.panel_options === 'string') { + this.panel_options = sjson.parse(this.panel_options, null, { protoAction: 'remove', constructorAction: 'remove', }); @@ -93,7 +93,7 @@ export class SavedDbQueryEntity { @ManyToOne( () => ConnectionEntity, - (connection) => connection.saved_db_queries, + (connection) => connection.panels, { onDelete: 'CASCADE' }, ) @JoinColumn({ name: 'connection_id' }) @@ -103,8 +103,8 @@ export class SavedDbQueryEntity { connection_id: string; @OneToMany( - () => DashboardWidgetEntity, - (widget) => widget.query, + () => PanelPositionEntity, + (panel_position) => panel_position.query, ) - widgets: Relation[]; + panels: Relation[]; } diff --git a/backend/src/entities/visualizations/saved-db-query/saved-db-query.module.ts b/backend/src/entities/visualizations/panel/panel.module.ts similarity index 80% rename from backend/src/entities/visualizations/saved-db-query/saved-db-query.module.ts rename to backend/src/entities/visualizations/panel/panel.module.ts index 779985e4a..67ada6b9b 100644 --- a/backend/src/entities/visualizations/saved-db-query/saved-db-query.module.ts +++ b/backend/src/entities/visualizations/panel/panel.module.ts @@ -5,14 +5,14 @@ import { GlobalDatabaseContext } from '../../../common/application/global-databa import { BaseType, UseCaseType } from '../../../common/data-injection.tokens.js'; import { LogOutEntity } from '../../log-out/log-out.entity.js'; import { UserEntity } from '../../user/user.entity.js'; -import { SavedDbQueryController } from './saved-db-query.controller.js'; -import { CreateSavedDbQueryUseCase } from './use-cases/create-saved-db-query.use.case.js'; -import { DeleteSavedDbQueryUseCase } from './use-cases/delete-saved-db-query.use.case.js'; -import { ExecuteSavedDbQueryUseCase } from './use-cases/execute-saved-db-query.use.case.js'; -import { FindAllSavedDbQueriesUseCase } from './use-cases/find-all-saved-db-queries.use.case.js'; -import { FindSavedDbQueryUseCase } from './use-cases/find-saved-db-query.use.case.js'; +import { SavedDbQueryController } from './panel.controller.js'; +import { CreatePanelUseCase } from './use-cases/create-panel.use.case.js'; +import { DeleteSavedDbQueryUseCase } from './use-cases/delete-panel.use.case.js'; +import { ExecuteSavedDbQueryUseCase } from './use-cases/execute-panel.use.case.js'; +import { FindAllSavedDbQueriesUseCase } from './use-cases/find-all-panels.use.case.js'; +import { FindSavedDbQueryUseCase } from './use-cases/find-panel.use.case.js'; import { TestDbQueryUseCase } from './use-cases/test-db-query.use.case.js'; -import { UpdateSavedDbQueryUseCase } from './use-cases/update-saved-db-query.use.case.js'; +import { UpdateSavedDbQueryUseCase } from './use-cases/update-panel.use.case.js'; @Module({ imports: [TypeOrmModule.forFeature([UserEntity, LogOutEntity])], @@ -23,7 +23,7 @@ import { UpdateSavedDbQueryUseCase } from './use-cases/update-saved-db-query.use }, { provide: UseCaseType.CREATE_SAVED_DB_QUERY, - useClass: CreateSavedDbQueryUseCase, + useClass: CreatePanelUseCase, }, { provide: UseCaseType.UPDATE_SAVED_DB_QUERY, @@ -53,7 +53,7 @@ import { UpdateSavedDbQueryUseCase } from './use-cases/update-saved-db-query.use controllers: [SavedDbQueryController], exports: [], }) -export class SavedDbQueryModule { +export class PanelModule { public configure(consumer: MiddlewareConsumer): void { consumer .apply(AuthMiddleware) diff --git a/backend/src/entities/visualizations/panel/repository/saved-db-query-custom-repository-extension.ts b/backend/src/entities/visualizations/panel/repository/saved-db-query-custom-repository-extension.ts new file mode 100644 index 000000000..02063c285 --- /dev/null +++ b/backend/src/entities/visualizations/panel/repository/saved-db-query-custom-repository-extension.ts @@ -0,0 +1,24 @@ +import { PanelEntity } from '../panel.entity.js'; +import { IPanelRepository } from './saved-db-query.repository.interface.js'; + +export const panelCustomRepositoryExtension: IPanelRepository = { + async findAllQueriesByConnectionId(connectionId: string): Promise { + const qb = this.createQueryBuilder('panel'); + qb.where('panel.connection_id = :connectionId', { connectionId }); + qb.orderBy('panel.created_at', 'DESC'); + return await qb.getMany(); + }, + + async findQueryById(queryId: string): Promise { + const qb = this.createQueryBuilder('panel'); + qb.where('panel.id = :queryId', { queryId }); + return await qb.getOne(); + }, + + async findQueryByIdAndConnectionId(queryId: string, connectionId: string): Promise { + const qb = this.createQueryBuilder('panel'); + qb.where('panel.id = :queryId', { queryId }); + qb.andWhere('panel.connection_id = :connectionId', { connectionId }); + return await qb.getOne(); + }, +}; diff --git a/backend/src/entities/visualizations/panel/repository/saved-db-query.repository.interface.ts b/backend/src/entities/visualizations/panel/repository/saved-db-query.repository.interface.ts new file mode 100644 index 000000000..8bee4656c --- /dev/null +++ b/backend/src/entities/visualizations/panel/repository/saved-db-query.repository.interface.ts @@ -0,0 +1,7 @@ +import { PanelEntity } from '../panel.entity.js'; + +export interface IPanelRepository { + findAllQueriesByConnectionId(connectionId: string): Promise; + findQueryById(queryId: string): Promise; + findQueryByIdAndConnectionId(queryId: string, connectionId: string): Promise; +} diff --git a/backend/src/entities/visualizations/saved-db-query/use-cases/create-saved-db-query.use.case.ts b/backend/src/entities/visualizations/panel/use-cases/create-panel.use.case.ts similarity index 60% rename from backend/src/entities/visualizations/saved-db-query/use-cases/create-saved-db-query.use.case.ts rename to backend/src/entities/visualizations/panel/use-cases/create-panel.use.case.ts index 6ef72f488..7154a40f0 100644 --- a/backend/src/entities/visualizations/saved-db-query/use-cases/create-saved-db-query.use.case.ts +++ b/backend/src/entities/visualizations/panel/use-cases/create-panel.use.case.ts @@ -4,18 +4,15 @@ import AbstractUseCase from '../../../../common/abstract-use.case.js'; import { IGlobalDatabaseContext } from '../../../../common/application/global-database-context.interface.js'; import { BaseType } from '../../../../common/data-injection.tokens.js'; import { Messages } from '../../../../exceptions/text/messages.js'; -import { CreateSavedDbQueryDs } from '../data-structures/create-saved-db-query.ds.js'; -import { FoundSavedDbQueryDto } from '../dto/found-saved-db-query.dto.js'; -import { SavedDbQueryEntity } from '../saved-db-query.entity.js'; -import { buildFoundSavedDbQueryDto } from '../utils/build-found-saved-db-query-dto.util.js'; +import { CreatePanelDs } from '../data-structures/create-panel.ds.js'; +import { FoundPanelDto } from '../dto/found-saved-db-query.dto.js'; +import { PanelEntity } from '../panel.entity.js'; +import { buildFoundPanelDto } from '../utils/build-found-panel-dto.util.js'; import { validateQuerySafety } from '../utils/check-query-is-safe.util.js'; -import { ICreateSavedDbQuery } from './saved-db-query-use-cases.interface.js'; +import { ICreatePanel } from './panel-use-cases.interface.js'; @Injectable({ scope: Scope.REQUEST }) -export class CreateSavedDbQueryUseCase - extends AbstractUseCase - implements ICreateSavedDbQuery -{ +export class CreatePanelUseCase extends AbstractUseCase implements ICreatePanel { constructor( @Inject(BaseType.GLOBAL_DB_CONTEXT) protected _dbContext: IGlobalDatabaseContext, @@ -23,7 +20,7 @@ export class CreateSavedDbQueryUseCase super(); } - public async implementation(inputData: CreateSavedDbQueryDs): Promise { + public async implementation(inputData: CreatePanelDs): Promise { const { connectionId, masterPassword, name, description, widget_type, chart_type, widget_options, query_text } = inputData; @@ -38,18 +35,18 @@ export class CreateSavedDbQueryUseCase validateQuerySafety(query_text, foundConnection.type as ConnectionTypesEnum); - const newQuery = new SavedDbQueryEntity(); + const newQuery = new PanelEntity(); newQuery.name = name; newQuery.description = description || null; - newQuery.widget_type = widget_type; + newQuery.panel_type = widget_type; newQuery.chart_type = chart_type || null; - newQuery.widget_options = widget_options ? (widget_options as unknown as string) : null; + newQuery.panel_options = widget_options ? (widget_options as unknown as string) : null; newQuery.query_text = query_text; newQuery.connection_id = foundConnection.id; - const savedQuery = await this._dbContext.savedDbQueryRepository.save(newQuery); + const savedQuery = await this._dbContext.panelRepository.save(newQuery); const savedQueryCopy = { ...savedQuery }; savedQueryCopy.query_text = query_text; - return buildFoundSavedDbQueryDto(savedQueryCopy as SavedDbQueryEntity); + return buildFoundPanelDto(savedQueryCopy as PanelEntity); } } diff --git a/backend/src/entities/visualizations/saved-db-query/use-cases/delete-saved-db-query.use.case.ts b/backend/src/entities/visualizations/panel/use-cases/delete-panel.use.case.ts similarity index 60% rename from backend/src/entities/visualizations/saved-db-query/use-cases/delete-saved-db-query.use.case.ts rename to backend/src/entities/visualizations/panel/use-cases/delete-panel.use.case.ts index 260122cbe..79c888e6d 100644 --- a/backend/src/entities/visualizations/saved-db-query/use-cases/delete-saved-db-query.use.case.ts +++ b/backend/src/entities/visualizations/panel/use-cases/delete-panel.use.case.ts @@ -3,15 +3,15 @@ import AbstractUseCase from '../../../../common/abstract-use.case.js'; import { IGlobalDatabaseContext } from '../../../../common/application/global-database-context.interface.js'; import { BaseType } from '../../../../common/data-injection.tokens.js'; import { Messages } from '../../../../exceptions/text/messages.js'; -import { FindSavedDbQueryDs } from '../data-structures/find-saved-db-query.ds.js'; -import { FoundSavedDbQueryDto } from '../dto/found-saved-db-query.dto.js'; -import { buildFoundSavedDbQueryDto } from '../utils/build-found-saved-db-query-dto.util.js'; -import { IDeleteSavedDbQuery } from './saved-db-query-use-cases.interface.js'; +import { FindSavedDbQueryDs } from '../data-structures/find-panel.ds.js'; +import { FoundPanelDto } from '../dto/found-saved-db-query.dto.js'; +import { buildFoundPanelDto } from '../utils/build-found-panel-dto.util.js'; +import { IDeletePanel } from './panel-use-cases.interface.js'; @Injectable({ scope: Scope.REQUEST }) export class DeleteSavedDbQueryUseCase - extends AbstractUseCase - implements IDeleteSavedDbQuery + extends AbstractUseCase + implements IDeletePanel { constructor( @Inject(BaseType.GLOBAL_DB_CONTEXT) @@ -20,7 +20,7 @@ export class DeleteSavedDbQueryUseCase super(); } - public async implementation(inputData: FindSavedDbQueryDs): Promise { + public async implementation(inputData: FindSavedDbQueryDs): Promise { const { queryId, connectionId, masterPassword } = inputData; const foundConnection = await this._dbContext.connectionRepository.findAndDecryptConnection( @@ -32,14 +32,14 @@ export class DeleteSavedDbQueryUseCase throw new NotFoundException(Messages.CONNECTION_NOT_FOUND); } - const foundQuery = await this._dbContext.savedDbQueryRepository.findQueryByIdAndConnectionId(queryId, connectionId); + const foundQuery = await this._dbContext.panelRepository.findQueryByIdAndConnectionId(queryId, connectionId); if (!foundQuery) { throw new NotFoundException(Messages.SAVED_QUERY_NOT_FOUND); } - const deletedQueryDto = buildFoundSavedDbQueryDto(foundQuery); - await this._dbContext.savedDbQueryRepository.remove(foundQuery); + const deletedQueryDto = buildFoundPanelDto(foundQuery); + await this._dbContext.panelRepository.remove(foundQuery); return deletedQueryDto; } } diff --git a/backend/src/entities/visualizations/saved-db-query/use-cases/execute-saved-db-query.use.case.ts b/backend/src/entities/visualizations/panel/use-cases/execute-panel.use.case.ts similarity index 94% rename from backend/src/entities/visualizations/saved-db-query/use-cases/execute-saved-db-query.use.case.ts rename to backend/src/entities/visualizations/panel/use-cases/execute-panel.use.case.ts index 0c4d36311..446481896 100644 --- a/backend/src/entities/visualizations/saved-db-query/use-cases/execute-saved-db-query.use.case.ts +++ b/backend/src/entities/visualizations/panel/use-cases/execute-panel.use.case.ts @@ -9,7 +9,7 @@ import { isConnectionTypeAgent } from '../../../../helpers/is-connection-entity- import { ExecuteSavedDbQueryDs } from '../data-structures/execute-saved-db-query.ds.js'; import { ExecuteSavedDbQueryResultDto } from '../dto/execute-saved-db-query-result.dto.js'; import { validateQuerySafety } from '../utils/check-query-is-safe.util.js'; -import { IExecuteSavedDbQuery } from './saved-db-query-use-cases.interface.js'; +import { IExecuteSavedDbQuery } from './panel-use-cases.interface.js'; @Injectable({ scope: Scope.REQUEST }) export class ExecuteSavedDbQueryUseCase @@ -35,7 +35,7 @@ export class ExecuteSavedDbQueryUseCase throw new NotFoundException(Messages.CONNECTION_NOT_FOUND); } - const foundQuery = await this._dbContext.savedDbQueryRepository.findQueryByIdAndConnectionId(queryId, connectionId); + const foundQuery = await this._dbContext.panelRepository.findQueryByIdAndConnectionId(queryId, connectionId); if (!foundQuery) { throw new NotFoundException(Messages.SAVED_QUERY_NOT_FOUND); diff --git a/backend/src/entities/visualizations/saved-db-query/use-cases/find-all-saved-db-queries.use.case.ts b/backend/src/entities/visualizations/panel/use-cases/find-all-panels.use.case.ts similarity index 54% rename from backend/src/entities/visualizations/saved-db-query/use-cases/find-all-saved-db-queries.use.case.ts rename to backend/src/entities/visualizations/panel/use-cases/find-all-panels.use.case.ts index 8239c195b..969a1de49 100644 --- a/backend/src/entities/visualizations/saved-db-query/use-cases/find-all-saved-db-queries.use.case.ts +++ b/backend/src/entities/visualizations/panel/use-cases/find-all-panels.use.case.ts @@ -3,15 +3,15 @@ import AbstractUseCase from '../../../../common/abstract-use.case.js'; import { IGlobalDatabaseContext } from '../../../../common/application/global-database-context.interface.js'; import { BaseType } from '../../../../common/data-injection.tokens.js'; import { Messages } from '../../../../exceptions/text/messages.js'; -import { FindAllSavedDbQueriesDs } from '../data-structures/find-all-saved-db-queries.ds.js'; -import { FoundSavedDbQueryDto } from '../dto/found-saved-db-query.dto.js'; -import { buildFoundSavedDbQueryDto } from '../utils/build-found-saved-db-query-dto.util.js'; -import { IFindAllSavedDbQueries } from './saved-db-query-use-cases.interface.js'; +import { FindAllPanelsDs } from '../data-structures/find-all-panels.ds.js'; +import { FoundPanelDto } from '../dto/found-saved-db-query.dto.js'; +import { buildFoundPanelDto } from '../utils/build-found-panel-dto.util.js'; +import { IFindAllPanels } from './panel-use-cases.interface.js'; @Injectable({ scope: Scope.REQUEST }) export class FindAllSavedDbQueriesUseCase - extends AbstractUseCase - implements IFindAllSavedDbQueries + extends AbstractUseCase + implements IFindAllPanels { constructor( @Inject(BaseType.GLOBAL_DB_CONTEXT) @@ -20,7 +20,7 @@ export class FindAllSavedDbQueriesUseCase super(); } - public async implementation(inputData: FindAllSavedDbQueriesDs): Promise { + public async implementation(inputData: FindAllPanelsDs): Promise { const { connectionId, masterPassword } = inputData; const foundConnection = await this._dbContext.connectionRepository.findAndDecryptConnection( @@ -32,8 +32,8 @@ export class FindAllSavedDbQueriesUseCase throw new NotFoundException(Messages.CONNECTION_NOT_FOUND); } - const foundQueries = await this._dbContext.savedDbQueryRepository.findAllQueriesByConnectionId(connectionId); + const foundQueries = await this._dbContext.panelRepository.findAllQueriesByConnectionId(connectionId); - return foundQueries.map((query) => buildFoundSavedDbQueryDto(query)); + return foundQueries.map((query) => buildFoundPanelDto(query)); } } diff --git a/backend/src/entities/visualizations/saved-db-query/use-cases/find-saved-db-query.use.case.ts b/backend/src/entities/visualizations/panel/use-cases/find-panel.use.case.ts similarity index 60% rename from backend/src/entities/visualizations/saved-db-query/use-cases/find-saved-db-query.use.case.ts rename to backend/src/entities/visualizations/panel/use-cases/find-panel.use.case.ts index 8b1a8327f..1e464adbe 100644 --- a/backend/src/entities/visualizations/saved-db-query/use-cases/find-saved-db-query.use.case.ts +++ b/backend/src/entities/visualizations/panel/use-cases/find-panel.use.case.ts @@ -3,16 +3,13 @@ import AbstractUseCase from '../../../../common/abstract-use.case.js'; import { IGlobalDatabaseContext } from '../../../../common/application/global-database-context.interface.js'; import { BaseType } from '../../../../common/data-injection.tokens.js'; import { Messages } from '../../../../exceptions/text/messages.js'; -import { FindSavedDbQueryDs } from '../data-structures/find-saved-db-query.ds.js'; -import { FoundSavedDbQueryDto } from '../dto/found-saved-db-query.dto.js'; -import { buildFoundSavedDbQueryDto } from '../utils/build-found-saved-db-query-dto.util.js'; -import { IFindSavedDbQuery } from './saved-db-query-use-cases.interface.js'; +import { FindSavedDbQueryDs } from '../data-structures/find-panel.ds.js'; +import { FoundPanelDto } from '../dto/found-saved-db-query.dto.js'; +import { buildFoundPanelDto } from '../utils/build-found-panel-dto.util.js'; +import { IFindPanel } from './panel-use-cases.interface.js'; @Injectable({ scope: Scope.REQUEST }) -export class FindSavedDbQueryUseCase - extends AbstractUseCase - implements IFindSavedDbQuery -{ +export class FindSavedDbQueryUseCase extends AbstractUseCase implements IFindPanel { constructor( @Inject(BaseType.GLOBAL_DB_CONTEXT) protected _dbContext: IGlobalDatabaseContext, @@ -20,7 +17,7 @@ export class FindSavedDbQueryUseCase super(); } - public async implementation(inputData: FindSavedDbQueryDs): Promise { + public async implementation(inputData: FindSavedDbQueryDs): Promise { const { queryId, connectionId, masterPassword } = inputData; const foundConnection = await this._dbContext.connectionRepository.findAndDecryptConnection( @@ -32,12 +29,12 @@ export class FindSavedDbQueryUseCase throw new NotFoundException(Messages.CONNECTION_NOT_FOUND); } - const foundQuery = await this._dbContext.savedDbQueryRepository.findQueryByIdAndConnectionId(queryId, connectionId); + const foundQuery = await this._dbContext.panelRepository.findQueryByIdAndConnectionId(queryId, connectionId); if (!foundQuery) { throw new NotFoundException(Messages.SAVED_QUERY_NOT_FOUND); } - return buildFoundSavedDbQueryDto(foundQuery); + return buildFoundPanelDto(foundQuery); } } diff --git a/backend/src/entities/visualizations/panel/use-cases/panel-use-cases.interface.ts b/backend/src/entities/visualizations/panel/use-cases/panel-use-cases.interface.ts new file mode 100644 index 000000000..fc36de2c1 --- /dev/null +++ b/backend/src/entities/visualizations/panel/use-cases/panel-use-cases.interface.ts @@ -0,0 +1,38 @@ +import { InTransactionEnum } from '../../../../enums/in-transaction.enum.js'; +import { CreatePanelDs } from '../data-structures/create-panel.ds.js'; +import { ExecuteSavedDbQueryDs } from '../data-structures/execute-saved-db-query.ds.js'; +import { FindAllPanelsDs } from '../data-structures/find-all-panels.ds.js'; +import { FindSavedDbQueryDs as FindPanelDs } from '../data-structures/find-panel.ds.js'; +import { TestDbQueryDs } from '../data-structures/test-db-query.ds.js'; +import { UpdatePanelDs } from '../data-structures/update-panel.ds.js'; +import { ExecuteSavedDbQueryResultDto } from '../dto/execute-saved-db-query-result.dto.js'; +import { FoundPanelDto } from '../dto/found-saved-db-query.dto.js'; +import { TestDbQueryResultDto } from '../dto/test-db-query-result.dto.js'; + +export interface ICreatePanel { + execute(inputData: CreatePanelDs, inTransaction: InTransactionEnum): Promise; +} + +export interface IUpdatePanel { + execute(inputData: UpdatePanelDs, inTransaction: InTransactionEnum): Promise; +} + +export interface IFindPanel { + execute(inputData: FindPanelDs, inTransaction: InTransactionEnum): Promise; +} + +export interface IFindAllPanels { + execute(inputData: FindAllPanelsDs, inTransaction: InTransactionEnum): Promise; +} + +export interface IDeletePanel { + execute(inputData: FindPanelDs, inTransaction: InTransactionEnum): Promise; +} + +export interface IExecuteSavedDbQuery { + execute(inputData: ExecuteSavedDbQueryDs, inTransaction: InTransactionEnum): Promise; +} + +export interface ITestDbQuery { + execute(inputData: TestDbQueryDs, inTransaction: InTransactionEnum): Promise; +} diff --git a/backend/src/entities/visualizations/saved-db-query/use-cases/test-db-query.use.case.ts b/backend/src/entities/visualizations/panel/use-cases/test-db-query.use.case.ts similarity index 97% rename from backend/src/entities/visualizations/saved-db-query/use-cases/test-db-query.use.case.ts rename to backend/src/entities/visualizations/panel/use-cases/test-db-query.use.case.ts index 1011ef850..26e5674fb 100644 --- a/backend/src/entities/visualizations/saved-db-query/use-cases/test-db-query.use.case.ts +++ b/backend/src/entities/visualizations/panel/use-cases/test-db-query.use.case.ts @@ -9,7 +9,7 @@ import { isConnectionTypeAgent } from '../../../../helpers/is-connection-entity- import { TestDbQueryDs } from '../data-structures/test-db-query.ds.js'; import { TestDbQueryResultDto } from '../dto/test-db-query-result.dto.js'; import { validateQuerySafety } from '../utils/check-query-is-safe.util.js'; -import { ITestDbQuery } from './saved-db-query-use-cases.interface.js'; +import { ITestDbQuery } from './panel-use-cases.interface.js'; @Injectable({ scope: Scope.REQUEST }) export class TestDbQueryUseCase extends AbstractUseCase implements ITestDbQuery { diff --git a/backend/src/entities/visualizations/saved-db-query/use-cases/update-saved-db-query.use.case.ts b/backend/src/entities/visualizations/panel/use-cases/update-panel.use.case.ts similarity index 63% rename from backend/src/entities/visualizations/saved-db-query/use-cases/update-saved-db-query.use.case.ts rename to backend/src/entities/visualizations/panel/use-cases/update-panel.use.case.ts index 7eb25b5d5..d0d938c5c 100644 --- a/backend/src/entities/visualizations/saved-db-query/use-cases/update-saved-db-query.use.case.ts +++ b/backend/src/entities/visualizations/panel/use-cases/update-panel.use.case.ts @@ -4,18 +4,15 @@ import AbstractUseCase from '../../../../common/abstract-use.case.js'; import { IGlobalDatabaseContext } from '../../../../common/application/global-database-context.interface.js'; import { BaseType } from '../../../../common/data-injection.tokens.js'; import { Messages } from '../../../../exceptions/text/messages.js'; -import { UpdateSavedDbQueryDs } from '../data-structures/update-saved-db-query.ds.js'; -import { FoundSavedDbQueryDto } from '../dto/found-saved-db-query.dto.js'; -import { SavedDbQueryEntity } from '../saved-db-query.entity.js'; -import { buildFoundSavedDbQueryDto } from '../utils/build-found-saved-db-query-dto.util.js'; +import { UpdatePanelDs } from '../data-structures/update-panel.ds.js'; +import { FoundPanelDto } from '../dto/found-saved-db-query.dto.js'; +import { PanelEntity } from '../panel.entity.js'; +import { buildFoundPanelDto } from '../utils/build-found-panel-dto.util.js'; import { validateQuerySafety } from '../utils/check-query-is-safe.util.js'; -import { IUpdateSavedDbQuery } from './saved-db-query-use-cases.interface.js'; +import { IUpdatePanel } from './panel-use-cases.interface.js'; @Injectable({ scope: Scope.REQUEST }) -export class UpdateSavedDbQueryUseCase - extends AbstractUseCase - implements IUpdateSavedDbQuery -{ +export class UpdateSavedDbQueryUseCase extends AbstractUseCase implements IUpdatePanel { constructor( @Inject(BaseType.GLOBAL_DB_CONTEXT) protected _dbContext: IGlobalDatabaseContext, @@ -23,7 +20,7 @@ export class UpdateSavedDbQueryUseCase super(); } - public async implementation(inputData: UpdateSavedDbQueryDs): Promise { + public async implementation(inputData: UpdatePanelDs): Promise { const { queryId, connectionId, @@ -45,7 +42,7 @@ export class UpdateSavedDbQueryUseCase throw new NotFoundException(Messages.CONNECTION_NOT_FOUND); } - const foundQuery = await this._dbContext.savedDbQueryRepository.findQueryByIdAndConnectionId(queryId, connectionId); + const foundQuery = await this._dbContext.panelRepository.findQueryByIdAndConnectionId(queryId, connectionId); if (!foundQuery) { throw new NotFoundException(Messages.SAVED_QUERY_NOT_FOUND); @@ -58,22 +55,22 @@ export class UpdateSavedDbQueryUseCase foundQuery.description = description; } if (widget_type !== undefined) { - foundQuery.widget_type = widget_type; + foundQuery.panel_type = widget_type; } if (chart_type !== undefined) { foundQuery.chart_type = chart_type; } if (widget_options !== undefined) { - foundQuery.widget_options = widget_options ? (widget_options as unknown as string) : null; + foundQuery.panel_options = widget_options ? (widget_options as unknown as string) : null; } if (query_text !== undefined) { validateQuerySafety(query_text, foundConnection.type as ConnectionTypesEnum); foundQuery.query_text = query_text; } const resultQueryText = foundQuery.query_text; - const savedQuery = await this._dbContext.savedDbQueryRepository.save(foundQuery); + const savedQuery = await this._dbContext.panelRepository.save(foundQuery); const savedQueryCopy = { ...savedQuery }; savedQueryCopy.query_text = resultQueryText; - return buildFoundSavedDbQueryDto(savedQueryCopy as SavedDbQueryEntity); + return buildFoundPanelDto(savedQueryCopy as PanelEntity); } } diff --git a/backend/src/entities/visualizations/panel/utils/build-found-panel-dto.util.ts b/backend/src/entities/visualizations/panel/utils/build-found-panel-dto.util.ts new file mode 100644 index 000000000..10d4e5fed --- /dev/null +++ b/backend/src/entities/visualizations/panel/utils/build-found-panel-dto.util.ts @@ -0,0 +1,17 @@ +import { FoundPanelDto } from '../dto/found-saved-db-query.dto.js'; +import { PanelEntity } from '../panel.entity.js'; + +export function buildFoundPanelDto(panel: PanelEntity): FoundPanelDto { + return { + id: panel.id, + name: panel.name, + description: panel.description, + widget_type: panel.panel_type, + chart_type: panel.chart_type, + widget_options: panel.panel_options as unknown as Record | null, + query_text: panel.query_text, + connection_id: panel.connection_id, + created_at: panel.created_at, + updated_at: panel.updated_at, + }; +} diff --git a/backend/src/entities/visualizations/saved-db-query/utils/check-query-is-safe.util.ts b/backend/src/entities/visualizations/panel/utils/check-query-is-safe.util.ts similarity index 100% rename from backend/src/entities/visualizations/saved-db-query/utils/check-query-is-safe.util.ts rename to backend/src/entities/visualizations/panel/utils/check-query-is-safe.util.ts diff --git a/backend/src/entities/visualizations/saved-db-query/repository/saved-db-query-custom-repository-extension.ts b/backend/src/entities/visualizations/saved-db-query/repository/saved-db-query-custom-repository-extension.ts deleted file mode 100644 index c41bb6de6..000000000 --- a/backend/src/entities/visualizations/saved-db-query/repository/saved-db-query-custom-repository-extension.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { SavedDbQueryEntity } from '../saved-db-query.entity.js'; -import { ISavedDbQueryRepository } from './saved-db-query.repository.interface.js'; - -export const savedDbQueryCustomRepositoryExtension: ISavedDbQueryRepository = { - async findAllQueriesByConnectionId(connectionId: string): Promise { - const qb = this.createQueryBuilder('saved_db_query'); - qb.where('saved_db_query.connection_id = :connectionId', { connectionId }); - qb.orderBy('saved_db_query.created_at', 'DESC'); - return await qb.getMany(); - }, - - async findQueryById(queryId: string): Promise { - const qb = this.createQueryBuilder('saved_db_query'); - qb.where('saved_db_query.id = :queryId', { queryId }); - return await qb.getOne(); - }, - - async findQueryByIdAndConnectionId(queryId: string, connectionId: string): Promise { - const qb = this.createQueryBuilder('saved_db_query'); - qb.where('saved_db_query.id = :queryId', { queryId }); - qb.andWhere('saved_db_query.connection_id = :connectionId', { connectionId }); - return await qb.getOne(); - }, -}; diff --git a/backend/src/entities/visualizations/saved-db-query/repository/saved-db-query.repository.interface.ts b/backend/src/entities/visualizations/saved-db-query/repository/saved-db-query.repository.interface.ts deleted file mode 100644 index 94e466d55..000000000 --- a/backend/src/entities/visualizations/saved-db-query/repository/saved-db-query.repository.interface.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { SavedDbQueryEntity } from '../saved-db-query.entity.js'; - -export interface ISavedDbQueryRepository { - findAllQueriesByConnectionId(connectionId: string): Promise; - findQueryById(queryId: string): Promise; - findQueryByIdAndConnectionId(queryId: string, connectionId: string): Promise; -} diff --git a/backend/src/entities/visualizations/saved-db-query/use-cases/saved-db-query-use-cases.interface.ts b/backend/src/entities/visualizations/saved-db-query/use-cases/saved-db-query-use-cases.interface.ts deleted file mode 100644 index a17f57012..000000000 --- a/backend/src/entities/visualizations/saved-db-query/use-cases/saved-db-query-use-cases.interface.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { InTransactionEnum } from '../../../../enums/in-transaction.enum.js'; -import { CreateSavedDbQueryDs } from '../data-structures/create-saved-db-query.ds.js'; -import { ExecuteSavedDbQueryDs } from '../data-structures/execute-saved-db-query.ds.js'; -import { FindAllSavedDbQueriesDs } from '../data-structures/find-all-saved-db-queries.ds.js'; -import { FindSavedDbQueryDs } from '../data-structures/find-saved-db-query.ds.js'; -import { TestDbQueryDs } from '../data-structures/test-db-query.ds.js'; -import { UpdateSavedDbQueryDs } from '../data-structures/update-saved-db-query.ds.js'; -import { ExecuteSavedDbQueryResultDto } from '../dto/execute-saved-db-query-result.dto.js'; -import { FoundSavedDbQueryDto } from '../dto/found-saved-db-query.dto.js'; -import { TestDbQueryResultDto } from '../dto/test-db-query-result.dto.js'; - -export interface ICreateSavedDbQuery { - execute(inputData: CreateSavedDbQueryDs, inTransaction: InTransactionEnum): Promise; -} - -export interface IUpdateSavedDbQuery { - execute(inputData: UpdateSavedDbQueryDs, inTransaction: InTransactionEnum): Promise; -} - -export interface IFindSavedDbQuery { - execute(inputData: FindSavedDbQueryDs, inTransaction: InTransactionEnum): Promise; -} - -export interface IFindAllSavedDbQueries { - execute(inputData: FindAllSavedDbQueriesDs, inTransaction: InTransactionEnum): Promise; -} - -export interface IDeleteSavedDbQuery { - execute(inputData: FindSavedDbQueryDs, inTransaction: InTransactionEnum): Promise; -} - -export interface IExecuteSavedDbQuery { - execute(inputData: ExecuteSavedDbQueryDs, inTransaction: InTransactionEnum): Promise; -} - -export interface ITestDbQuery { - execute(inputData: TestDbQueryDs, inTransaction: InTransactionEnum): Promise; -} diff --git a/backend/src/entities/visualizations/saved-db-query/utils/build-found-saved-db-query-dto.util.ts b/backend/src/entities/visualizations/saved-db-query/utils/build-found-saved-db-query-dto.util.ts deleted file mode 100644 index c4ed67613..000000000 --- a/backend/src/entities/visualizations/saved-db-query/utils/build-found-saved-db-query-dto.util.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { FoundSavedDbQueryDto } from '../dto/found-saved-db-query.dto.js'; -import { SavedDbQueryEntity } from '../saved-db-query.entity.js'; - -export function buildFoundSavedDbQueryDto(entity: SavedDbQueryEntity): FoundSavedDbQueryDto { - return { - id: entity.id, - name: entity.name, - description: entity.description, - widget_type: entity.widget_type, - chart_type: entity.chart_type, - widget_options: entity.widget_options as unknown as Record | null, - query_text: entity.query_text, - connection_id: entity.connection_id, - created_at: entity.created_at, - updated_at: entity.updated_at, - }; -} diff --git a/backend/src/migrations/1771344994001-RenameVisualisationEntities.ts b/backend/src/migrations/1771344994001-RenameVisualisationEntities.ts new file mode 100644 index 000000000..d43874358 --- /dev/null +++ b/backend/src/migrations/1771344994001-RenameVisualisationEntities.ts @@ -0,0 +1,48 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RenameVisualisationEntities1771344994001 implements MigrationInterface { + name = 'RenameVisualisationEntities1771344994001'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "dashboard_widget" DROP CONSTRAINT IF EXISTS "FK_2d30b309abbaf0e051fd89560b9"`, + ); + await queryRunner.query( + `ALTER TABLE "dashboard_widget" DROP CONSTRAINT IF EXISTS "FK_1d4cbbe2829d760116ce4472bd5"`, + ); + await queryRunner.query(`ALTER TABLE "saved_db_query" DROP CONSTRAINT IF EXISTS "FK_11269686996b3f9dadfc831428a"`); + await queryRunner.query(`ALTER TABLE "saved_db_query" RENAME TO "panel"`); + + await queryRunner.query(`ALTER TABLE "panel" RENAME COLUMN "widget_type" TO "panel_type"`); + await queryRunner.query(`ALTER TABLE "panel" RENAME COLUMN "widget_options" TO "panel_options"`); + await queryRunner.query(`ALTER TABLE "dashboard_widget" RENAME TO "panel_position"`); + await queryRunner.query( + `ALTER TABLE "panel" ADD CONSTRAINT "FK_panel_connection_id" FOREIGN KEY ("connection_id") REFERENCES "connection"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "panel_position" ADD CONSTRAINT "FK_panel_position_dashboard_id" FOREIGN KEY ("dashboard_id") REFERENCES "dashboard"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "panel_position" ADD CONSTRAINT "FK_panel_position_query_id" FOREIGN KEY ("query_id") REFERENCES "panel"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "panel_position" DROP CONSTRAINT IF EXISTS "FK_panel_position_query_id"`); + await queryRunner.query(`ALTER TABLE "panel_position" DROP CONSTRAINT IF EXISTS "FK_panel_position_dashboard_id"`); + await queryRunner.query(`ALTER TABLE "panel" DROP CONSTRAINT IF EXISTS "FK_panel_connection_id"`); + await queryRunner.query(`ALTER TABLE "panel_position" RENAME TO "dashboard_widget"`); + await queryRunner.query(`ALTER TABLE "panel" RENAME COLUMN "panel_type" TO "widget_type"`); + await queryRunner.query(`ALTER TABLE "panel" RENAME COLUMN "panel_options" TO "widget_options"`); + await queryRunner.query(`ALTER TABLE "panel" RENAME TO "saved_db_query"`); + await queryRunner.query( + `ALTER TABLE "saved_db_query" ADD CONSTRAINT "FK_11269686996b3f9dadfc831428a" FOREIGN KEY ("connection_id") REFERENCES "connection"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "dashboard_widget" ADD CONSTRAINT "FK_1d4cbbe2829d760116ce4472bd5" FOREIGN KEY ("dashboard_id") REFERENCES "dashboard"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "dashboard_widget" ADD CONSTRAINT "FK_2d30b309abbaf0e051fd89560b9" FOREIGN KEY ("query_id") REFERENCES "saved_db_query"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + } +} diff --git a/backend/test/ava-tests/saas-tests/dashboard-ai-widget-e2e.test.ts b/backend/test/ava-tests/saas-tests/dashboard-ai-widget-e2e.test.ts new file mode 100644 index 000000000..9e0a911d5 --- /dev/null +++ b/backend/test/ava-tests/saas-tests/dashboard-ai-widget-e2e.test.ts @@ -0,0 +1,474 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { faker } from '@faker-js/faker'; +import { INestApplication, ValidationPipe } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import test from 'ava'; +import { ValidationError } from 'class-validator'; +import cookieParser from 'cookie-parser'; +import request from 'supertest'; +import { AICoreService } from '../../../src/ai-core/index.js'; +import { ApplicationModule } from '../../../src/app.module.js'; +import { WinstonLogger } from '../../../src/entities/logging/winston-logger.js'; +import { DashboardWidgetTypeEnum } from '../../../src/enums/dashboard-widget-type.enum.js'; +import { AllExceptionsFilter } from '../../../src/exceptions/all-exceptions.filter.js'; +import { ValidationException } from '../../../src/exceptions/custom-exceptions/validation-exception.js'; +import { Cacher } from '../../../src/helpers/cache/cacher.js'; +import { DatabaseModule } from '../../../src/shared/database/database.module.js'; +import { DatabaseService } from '../../../src/shared/database/database.service.js'; +import { MockFactory } from '../../mock.factory.js'; +import { createTestTable } from '../../utils/create-test-table.js'; +import { getTestData } from '../../utils/get-test-data.js'; +import { registerUserAndReturnUserInfo } from '../../utils/register-user-and-return-user-info.js'; +import { TestUtils } from '../../utils/test.utils.js'; + +const mockFactory = new MockFactory(); +let app: INestApplication; +let testUtils: TestUtils; +let currentTest: string; + +const MOCK_AI_RESPONSE_CHART = JSON.stringify({ + name: 'Sales by Category', + description: 'Bar chart showing total sales grouped by category', + query_text: 'SELECT category, SUM(amount) as total FROM orders GROUP BY category', + widget_type: 'chart', + chart_type: 'bar', + widget_options: { + label_column: 'category', + value_column: 'total', + show_data_labels: true, + legend: { + show: true, + position: 'top', + }, + }, +}); + +const MOCK_AI_RESPONSE_COUNTER = JSON.stringify({ + name: 'Total Orders', + description: 'Counter showing total number of orders', + query_text: 'SELECT COUNT(*) as total FROM orders', + widget_type: 'counter', + widget_options: { + value_column: 'total', + }, +}); + +const MOCK_AI_RESPONSE_UNSAFE_QUERY = JSON.stringify({ + name: 'Delete All', + description: 'Unsafe query', + query_text: 'DELETE FROM orders', + widget_type: 'table', +}); + +let mockResponse = MOCK_AI_RESPONSE_CHART; + +const mockAICoreService = { + completeWithProvider: async () => mockResponse, + complete: async () => mockResponse, + chat: async () => ({ content: mockResponse, responseId: faker.string.uuid() }), + streamChat: async () => ({ + *[Symbol.asyncIterator]() { + yield { type: 'text', content: mockResponse, responseId: faker.string.uuid() }; + }, + }), + chatWithTools: async () => ({ content: mockResponse, responseId: faker.string.uuid() }), + streamChatWithTools: async () => ({ + *[Symbol.asyncIterator]() { + yield { type: 'text', content: mockResponse, responseId: faker.string.uuid() }; + }, + }), + streamChatWithToolsAndProvider: async () => ({ + *[Symbol.asyncIterator]() { + yield { type: 'text', content: mockResponse, responseId: faker.string.uuid() }; + }, + }), + getDefaultProvider: () => 'bedrock', + setDefaultProvider: () => {}, + getAvailableProviders: () => [], +}; + +test.before(async () => { + const moduleFixture = await Test.createTestingModule({ + imports: [ApplicationModule, DatabaseModule], + providers: [DatabaseService, TestUtils], + }) + .overrideProvider(AICoreService) + .useValue(mockAICoreService) + .compile(); + + testUtils = moduleFixture.get(TestUtils); + + app = moduleFixture.createNestApplication(); + app.use(cookieParser()); + app.useGlobalFilters(new AllExceptionsFilter(app.get(WinstonLogger))); + app.useGlobalPipes( + new ValidationPipe({ + exceptionFactory(validationErrors: ValidationError[] = []) { + return new ValidationException(validationErrors); + }, + }), + ); + await app.init(); + app.getHttpServer().listen(0); +}); + +test.after(async () => { + try { + await Cacher.clearAllCache(); + await app.close(); + } catch (e) { + console.error('After tests error ' + e); + } +}); + +currentTest = 'POST /dashboard/:dashboardId/widget/generate/:connectionId'; + +test.serial(`${currentTest} should generate a widget with AI for chart type`, async (t) => { + mockResponse = MOCK_AI_RESPONSE_CHART; + + const connectionToTestDB = getTestData(mockFactory).connectionToPostgres; + const { token } = await registerUserAndReturnUserInfo(app); + const { testTableName } = await createTestTable(connectionToTestDB); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', token) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const connectionId = JSON.parse(createConnectionResponse.text).id; + t.is(createConnectionResponse.status, 201); + + const createDashboard = await request(app.getHttpServer()) + .post(`/dashboards/${connectionId}`) + .send({ name: 'AI Generated Dashboard' }) + .set('Cookie', token) + .set('masterpwd', 'ahalaimahalai') + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const dashboardId = JSON.parse(createDashboard.text).id; + t.is(createDashboard.status, 201); + + const generateWidget = await request(app.getHttpServer()) + .post(`/dashboard/${dashboardId}/widget/generate/${connectionId}?tableName=${testTableName}`) + .send({ + chart_description: 'Show total sales by category as a bar chart', + }) + .set('Cookie', token) + .set('masterpwd', 'ahalaimahalai') + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const generateWidgetRO = JSON.parse(generateWidget.text); + t.is(generateWidget.status, 201); + t.truthy(generateWidgetRO.id); + t.truthy(generateWidgetRO.query_id); + t.is(generateWidgetRO.dashboard_id, dashboardId); + t.is(generateWidgetRO.width, 6); + t.is(generateWidgetRO.height, 4); + + const getSavedQueries = await request(app.getHttpServer()) + .get(`/connection/${connectionId}/saved-queries`) + .set('Cookie', token) + .set('masterpwd', 'ahalaimahalai') + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const queries = JSON.parse(getSavedQueries.text); + t.is(getSavedQueries.status, 200); + t.true(queries.length >= 1); + + const createdQuery = queries.find((q: { id: string }) => q.id === generateWidgetRO.query_id); + t.truthy(createdQuery); + t.is(createdQuery.name, 'Sales by Category'); + t.is(createdQuery.widget_type, DashboardWidgetTypeEnum.Chart); + t.is(createdQuery.chart_type, 'bar'); +}); + +test.serial(`${currentTest} should generate a counter widget with AI`, async (t) => { + mockResponse = MOCK_AI_RESPONSE_COUNTER; + + const connectionToTestDB = getTestData(mockFactory).connectionToPostgres; + const { token } = await registerUserAndReturnUserInfo(app); + const { testTableName } = await createTestTable(connectionToTestDB); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', token) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const connectionId = JSON.parse(createConnectionResponse.text).id; + t.is(createConnectionResponse.status, 201); + + const createDashboard = await request(app.getHttpServer()) + .post(`/dashboards/${connectionId}`) + .send({ name: 'Counter Dashboard' }) + .set('Cookie', token) + .set('masterpwd', 'ahalaimahalai') + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const dashboardId = JSON.parse(createDashboard.text).id; + + const generateWidget = await request(app.getHttpServer()) + .post(`/dashboard/${dashboardId}/widget/generate/${connectionId}?tableName=${testTableName}`) + .send({ + chart_description: 'Show total count of orders', + }) + .set('Cookie', token) + .set('masterpwd', 'ahalaimahalai') + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const generateWidgetRO = JSON.parse(generateWidget.text); + t.is(generateWidget.status, 201); + t.truthy(generateWidgetRO.id); + t.truthy(generateWidgetRO.query_id); + + const getQuery = await request(app.getHttpServer()) + .get(`/connection/${connectionId}/saved-query/${generateWidgetRO.query_id}`) + .set('Cookie', token) + .set('masterpwd', 'ahalaimahalai') + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const query = JSON.parse(getQuery.text); + t.is(getQuery.status, 200); + t.is(query.name, 'Total Orders'); + t.is(query.widget_type, DashboardWidgetTypeEnum.Counter); +}); + +test.serial(`${currentTest} should reject AI-generated unsafe query`, async (t) => { + mockResponse = MOCK_AI_RESPONSE_UNSAFE_QUERY; + + const connectionToTestDB = getTestData(mockFactory).connectionToPostgres; + const { token } = await registerUserAndReturnUserInfo(app); + const { testTableName } = await createTestTable(connectionToTestDB); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', token) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const connectionId = JSON.parse(createConnectionResponse.text).id; + + const createDashboard = await request(app.getHttpServer()) + .post(`/dashboards/${connectionId}`) + .send({ name: 'Unsafe Query Dashboard' }) + .set('Cookie', token) + .set('masterpwd', 'ahalaimahalai') + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const dashboardId = JSON.parse(createDashboard.text).id; + + const generateWidget = await request(app.getHttpServer()) + .post(`/dashboard/${dashboardId}/widget/generate/${connectionId}?tableName=${testTableName}`) + .send({ + chart_description: 'Delete all data', + }) + .set('Cookie', token) + .set('masterpwd', 'ahalaimahalai') + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(generateWidget.status, 400); + const errorResponse = JSON.parse(generateWidget.text); + t.truthy(errorResponse.message); + t.true(errorResponse.message.includes('Unsafe query') || errorResponse.message.includes('DELETE')); +}); + +test.serial(`${currentTest} should generate widget with custom name`, async (t) => { + mockResponse = MOCK_AI_RESPONSE_CHART; + + const connectionToTestDB = getTestData(mockFactory).connectionToPostgres; + const { token } = await registerUserAndReturnUserInfo(app); + const { testTableName } = await createTestTable(connectionToTestDB); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', token) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const connectionId = JSON.parse(createConnectionResponse.text).id; + + const createDashboard = await request(app.getHttpServer()) + .post(`/dashboards/${connectionId}`) + .send({ name: 'Custom Name Dashboard' }) + .set('Cookie', token) + .set('masterpwd', 'ahalaimahalai') + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const dashboardId = JSON.parse(createDashboard.text).id; + + const customName = 'My Custom Widget Name'; + const generateWidget = await request(app.getHttpServer()) + .post(`/dashboard/${dashboardId}/widget/generate/${connectionId}?tableName=${testTableName}`) + .send({ + chart_description: 'Show sales data', + name: customName, + }) + .set('Cookie', token) + .set('masterpwd', 'ahalaimahalai') + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const generateWidgetRO = JSON.parse(generateWidget.text); + t.is(generateWidget.status, 201); + + const getQuery = await request(app.getHttpServer()) + .get(`/connection/${connectionId}/saved-query/${generateWidgetRO.query_id}`) + .set('Cookie', token) + .set('masterpwd', 'ahalaimahalai') + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const query = JSON.parse(getQuery.text); + t.is(query.name, customName); +}); + +test.serial(`${currentTest} should fail without tableName query parameter`, async (t) => { + mockResponse = MOCK_AI_RESPONSE_CHART; + + const { token } = await registerUserAndReturnUserInfo(app); + const newConnection = getTestData(mockFactory).newEncryptedConnection; + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(newConnection) + .set('Cookie', token) + .set('masterpwd', 'ahalaimahalai') + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const connectionId = JSON.parse(createConnectionResponse.text).id; + + const createDashboard = await request(app.getHttpServer()) + .post(`/dashboards/${connectionId}`) + .send({ name: 'No Table Dashboard' }) + .set('Cookie', token) + .set('masterpwd', 'ahalaimahalai') + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const dashboardId = JSON.parse(createDashboard.text).id; + + const generateWidget = await request(app.getHttpServer()) + .post(`/dashboard/${dashboardId}/widget/generate/${connectionId}`) + .send({ + chart_description: 'Show some data', + }) + .set('Cookie', token) + .set('masterpwd', 'ahalaimahalai') + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(generateWidget.status, 400); +}); + +test.serial(`${currentTest} should fail without chart_description`, async (t) => { + mockResponse = MOCK_AI_RESPONSE_CHART; + + const connectionToTestDB = getTestData(mockFactory).connectionToPostgres; + const { token } = await registerUserAndReturnUserInfo(app); + const { testTableName } = await createTestTable(connectionToTestDB); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', token) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const connectionId = JSON.parse(createConnectionResponse.text).id; + + const createDashboard = await request(app.getHttpServer()) + .post(`/dashboards/${connectionId}`) + .send({ name: 'No Description Dashboard' }) + .set('Cookie', token) + .set('masterpwd', 'ahalaimahalai') + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const dashboardId = JSON.parse(createDashboard.text).id; + + const generateWidget = await request(app.getHttpServer()) + .post(`/dashboard/${dashboardId}/widget/generate/${connectionId}?tableName=${testTableName}`) + .send({}) + .set('Cookie', token) + .set('masterpwd', 'ahalaimahalai') + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(generateWidget.status, 400); +}); + +test.serial(`${currentTest} should fail for non-existent dashboard`, async (t) => { + mockResponse = MOCK_AI_RESPONSE_CHART; + + const connectionToTestDB = getTestData(mockFactory).connectionToPostgres; + const { token } = await registerUserAndReturnUserInfo(app); + const { testTableName } = await createTestTable(connectionToTestDB); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', token) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const connectionId = JSON.parse(createConnectionResponse.text).id; + + const fakeDashboardId = faker.string.uuid(); + + const generateWidget = await request(app.getHttpServer()) + .post(`/dashboard/${fakeDashboardId}/widget/generate/${connectionId}?tableName=${testTableName}`) + .send({ + chart_description: 'Show some data', + }) + .set('Cookie', token) + .set('masterpwd', 'ahalaimahalai') + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(generateWidget.status, 404); +}); + +test.serial(`${currentTest} should fail for non-existent table`, async (t) => { + mockResponse = MOCK_AI_RESPONSE_CHART; + + const connectionToTestDB = getTestData(mockFactory).connectionToPostgres; + const { token } = await registerUserAndReturnUserInfo(app); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', token) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const connectionId = JSON.parse(createConnectionResponse.text).id; + + const createDashboard = await request(app.getHttpServer()) + .post(`/dashboards/${connectionId}`) + .send({ name: 'Non-existent Table Dashboard' }) + .set('Cookie', token) + .set('masterpwd', 'ahalaimahalai') + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const dashboardId = JSON.parse(createDashboard.text).id; + + const generateWidget = await request(app.getHttpServer()) + .post(`/dashboard/${dashboardId}/widget/generate/${connectionId}?tableName=non_existent_table_xyz`) + .send({ + chart_description: 'Show some data', + }) + .set('Cookie', token) + .set('masterpwd', 'ahalaimahalai') + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(generateWidget.status, 400); +}); diff --git a/backend/test/ava-tests/saas-tests/saved-database-queries-e2e.test.ts b/backend/test/ava-tests/saas-tests/saved-database-queries-e2e.test.ts index c9f09cd26..1beb68904 100644 --- a/backend/test/ava-tests/saas-tests/saved-database-queries-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/saved-database-queries-e2e.test.ts @@ -8,12 +8,12 @@ import cookieParser from 'cookie-parser'; import request from 'supertest'; import { ApplicationModule } from '../../../src/app.module.js'; import { WinstonLogger } from '../../../src/entities/logging/winston-logger.js'; -import { CreateSavedDbQueryDto } from '../../../src/entities/visualizations/saved-db-query/dto/create-saved-db-query.dto.js'; -import { ExecuteSavedDbQueryResultDto } from '../../../src/entities/visualizations/saved-db-query/dto/execute-saved-db-query-result.dto.js'; -import { FoundSavedDbQueryDto } from '../../../src/entities/visualizations/saved-db-query/dto/found-saved-db-query.dto.js'; -import { TestDbQueryDto } from '../../../src/entities/visualizations/saved-db-query/dto/test-db-query.dto.js'; -import { TestDbQueryResultDto } from '../../../src/entities/visualizations/saved-db-query/dto/test-db-query-result.dto.js'; -import { UpdateSavedDbQueryDto } from '../../../src/entities/visualizations/saved-db-query/dto/update-saved-db-query.dto.js'; +import { CreateSavedDbQueryDto } from '../../../src/entities/visualizations/panel/dto/create-saved-db-query.dto.js'; +import { ExecuteSavedDbQueryResultDto } from '../../../src/entities/visualizations/panel/dto/execute-saved-db-query-result.dto.js'; +import { FoundPanelDto } from '../../../src/entities/visualizations/panel/dto/found-saved-db-query.dto.js'; +import { TestDbQueryDto } from '../../../src/entities/visualizations/panel/dto/test-db-query.dto.js'; +import { TestDbQueryResultDto } from '../../../src/entities/visualizations/panel/dto/test-db-query-result.dto.js'; +import { UpdateSavedDbQueryDto } from '../../../src/entities/visualizations/panel/dto/update-saved-db-query.dto.js'; import { DashboardWidgetTypeEnum } from '../../../src/enums/dashboard-widget-type.enum.js'; import { AllExceptionsFilter } from '../../../src/exceptions/all-exceptions.filter.js'; import { ValidationException } from '../../../src/exceptions/custom-exceptions/validation-exception.js'; @@ -92,7 +92,7 @@ test.serial(`${currentTest} should create a new saved query`, async (t) => { .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - const createSavedQueryRO: FoundSavedDbQueryDto = JSON.parse(createSavedQueryResponse.text); + const createSavedQueryRO: FoundPanelDto = JSON.parse(createSavedQueryResponse.text); t.is(createSavedQueryResponse.status, 201); t.truthy(createSavedQueryRO.id); t.is(createSavedQueryRO.name, createSavedQueryDTO.name); @@ -213,7 +213,7 @@ test.serial(`${currentTest} should return all saved queries for a connection`, a .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - const getAllSavedQueriesRO: FoundSavedDbQueryDto[] = JSON.parse(getAllSavedQueriesResponse.text); + const getAllSavedQueriesRO: FoundPanelDto[] = JSON.parse(getAllSavedQueriesResponse.text); t.is(getAllSavedQueriesResponse.status, 200); t.true(Array.isArray(getAllSavedQueriesRO)); t.is(getAllSavedQueriesRO.length, 2); @@ -240,7 +240,7 @@ test.serial(`${currentTest} should return empty array when no saved queries exis .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - const getAllSavedQueriesRO: FoundSavedDbQueryDto[] = JSON.parse(getAllSavedQueriesResponse.text); + const getAllSavedQueriesRO: FoundPanelDto[] = JSON.parse(getAllSavedQueriesResponse.text); t.is(getAllSavedQueriesResponse.status, 200); t.true(Array.isArray(getAllSavedQueriesRO)); t.is(getAllSavedQueriesRO.length, 0); @@ -276,7 +276,7 @@ test.serial(`${currentTest} should return a saved query by id`, async (t) => { .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - const createSavedQueryRO: FoundSavedDbQueryDto = JSON.parse(createSavedQueryResponse.text); + const createSavedQueryRO: FoundPanelDto = JSON.parse(createSavedQueryResponse.text); t.is(createSavedQueryResponse.status, 201); // Get saved query by id @@ -286,7 +286,7 @@ test.serial(`${currentTest} should return a saved query by id`, async (t) => { .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - const getSavedQueryRO: FoundSavedDbQueryDto = JSON.parse(getSavedQueryResponse.text); + const getSavedQueryRO: FoundPanelDto = JSON.parse(getSavedQueryResponse.text); t.is(getSavedQueryResponse.status, 200); t.is(getSavedQueryRO.id, createSavedQueryRO.id); t.is(getSavedQueryRO.name, createSavedQueryDTO.name); @@ -347,7 +347,7 @@ test.serial(`${currentTest} should update a saved query`, async (t) => { .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - const createSavedQueryRO: FoundSavedDbQueryDto = JSON.parse(createSavedQueryResponse.text); + const createSavedQueryRO: FoundPanelDto = JSON.parse(createSavedQueryResponse.text); t.is(createSavedQueryResponse.status, 201); // Update saved query @@ -364,7 +364,7 @@ test.serial(`${currentTest} should update a saved query`, async (t) => { .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - const updateSavedQueryRO: FoundSavedDbQueryDto = JSON.parse(updateSavedQueryResponse.text); + const updateSavedQueryRO: FoundPanelDto = JSON.parse(updateSavedQueryResponse.text); t.is(updateSavedQueryResponse.status, 200); t.is(updateSavedQueryRO.id, createSavedQueryRO.id); t.is(updateSavedQueryRO.name, updateSavedQueryDTO.name); @@ -400,7 +400,7 @@ test.serial(`${currentTest} should update only provided fields`, async (t) => { .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - const createSavedQueryRO: FoundSavedDbQueryDto = JSON.parse(createSavedQueryResponse.text); + const createSavedQueryRO: FoundPanelDto = JSON.parse(createSavedQueryResponse.text); t.is(createSavedQueryResponse.status, 201); const updateSavedQueryDTO: UpdateSavedDbQueryDto = { @@ -414,7 +414,7 @@ test.serial(`${currentTest} should update only provided fields`, async (t) => { .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - const updateSavedQueryRO: FoundSavedDbQueryDto = JSON.parse(updateSavedQueryResponse.text); + const updateSavedQueryRO: FoundPanelDto = JSON.parse(updateSavedQueryResponse.text); t.is(updateSavedQueryResponse.status, 200); t.is(updateSavedQueryRO.name, updateSavedQueryDTO.name); t.is(updateSavedQueryRO.description, createSavedQueryDTO.description); @@ -479,7 +479,7 @@ test.serial(`${currentTest} should delete a saved query`, async (t) => { .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - const createSavedQueryRO: FoundSavedDbQueryDto = JSON.parse(createSavedQueryResponse.text); + const createSavedQueryRO: FoundPanelDto = JSON.parse(createSavedQueryResponse.text); t.is(createSavedQueryResponse.status, 201); // Delete saved query @@ -489,7 +489,7 @@ test.serial(`${currentTest} should delete a saved query`, async (t) => { .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - const deleteSavedQueryRO: FoundSavedDbQueryDto = JSON.parse(deleteSavedQueryResponse.text); + const deleteSavedQueryRO: FoundPanelDto = JSON.parse(deleteSavedQueryResponse.text); t.is(deleteSavedQueryResponse.status, 200); t.is(deleteSavedQueryRO.id, createSavedQueryRO.id); @@ -557,7 +557,7 @@ test.serial(`${currentTest} should execute a saved query and return results`, as .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - const createSavedQueryRO: FoundSavedDbQueryDto = JSON.parse(createSavedQueryResponse.text); + const createSavedQueryRO: FoundPanelDto = JSON.parse(createSavedQueryResponse.text); t.is(createSavedQueryResponse.status, 201); // Execute saved query @@ -647,7 +647,7 @@ test.serial(`${currentTest} should not return queries from other connections`, a .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - const getQueriesFromConnection2RO: FoundSavedDbQueryDto[] = JSON.parse(getQueriesFromConnection2Response.text); + const getQueriesFromConnection2RO: FoundPanelDto[] = JSON.parse(getQueriesFromConnection2Response.text); t.is(getQueriesFromConnection2Response.status, 200); t.is(getQueriesFromConnection2RO.length, 0); @@ -658,7 +658,7 @@ test.serial(`${currentTest} should not return queries from other connections`, a .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - const getQueriesFromConnection1RO: FoundSavedDbQueryDto[] = JSON.parse(getQueriesFromConnection1Response.text); + const getQueriesFromConnection1RO: FoundPanelDto[] = JSON.parse(getQueriesFromConnection1Response.text); t.is(getQueriesFromConnection1Response.status, 200); t.is(getQueriesFromConnection1RO.length, 1); t.is(getQueriesFromConnection1RO[0].name, 'Query in Connection 1'); @@ -959,7 +959,7 @@ test.serial(`${currentTest} should reject unsafe query when updating saved query .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - const createSavedQueryRO: FoundSavedDbQueryDto = JSON.parse(createSavedQueryResponse.text); + const createSavedQueryRO: FoundPanelDto = JSON.parse(createSavedQueryResponse.text); t.is(createSavedQueryResponse.status, 201); const updateSavedQueryDTO: UpdateSavedDbQueryDto = { From 737d300052a477f83ac2e4c5cdb64608c1c77c2b Mon Sep 17 00:00:00 2001 From: Artem Niehrieiev Date: Thu, 19 Feb 2026 09:24:55 +0000 Subject: [PATCH 2/3] added generate panel with ai endpoint and use case --- .../dto/generated-panel-with-position.dto.ts | 45 ++++++++++++++ .../panel-position.controller.ts | 9 +-- ...enerate-panel-position-with-ai.use.case.ts | 50 +++++++--------- .../panel-position-use-cases.interface.ts | 3 +- .../dashboard-ai-widget-e2e.test.ts | 60 ++++++++----------- 5 files changed, 99 insertions(+), 68 deletions(-) create mode 100644 backend/src/entities/visualizations/panel-position/dto/generated-panel-with-position.dto.ts diff --git a/backend/src/entities/visualizations/panel-position/dto/generated-panel-with-position.dto.ts b/backend/src/entities/visualizations/panel-position/dto/generated-panel-with-position.dto.ts new file mode 100644 index 000000000..a016c1dcc --- /dev/null +++ b/backend/src/entities/visualizations/panel-position/dto/generated-panel-with-position.dto.ts @@ -0,0 +1,45 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { DashboardWidgetTypeEnum } from '../../../../enums/dashboard-widget-type.enum.js'; + +export class GeneratedPanelPositionDto { + @ApiProperty({ description: 'Position X in grid' }) + position_x: number; + + @ApiProperty({ description: 'Position Y in grid' }) + position_y: number; + + @ApiProperty({ description: 'Widget width in grid units' }) + width: number; + + @ApiProperty({ description: 'Widget height in grid units' }) + height: number; + + @ApiProperty({ description: 'Dashboard ID' }) + dashboard_id: string; +} + +export class GeneratedPanelWithPositionDto { + @ApiProperty({ description: 'Panel name' }) + name: string; + + @ApiPropertyOptional({ description: 'Panel description' }) + description: string | null; + + @ApiProperty({ description: 'Widget type', enum: DashboardWidgetTypeEnum }) + widget_type: DashboardWidgetTypeEnum; + + @ApiPropertyOptional({ description: 'Chart type for chart widgets' }) + chart_type: string | null; + + @ApiPropertyOptional({ description: 'Visualization options' }) + widget_options: Record | null; + + @ApiProperty({ description: 'AI-generated SQL query text' }) + query_text: string; + + @ApiProperty({ description: 'Connection ID' }) + connection_id: string; + + @ApiProperty({ description: 'Panel position configuration', type: GeneratedPanelPositionDto }) + panel_position: GeneratedPanelPositionDto; +} diff --git a/backend/src/entities/visualizations/panel-position/panel-position.controller.ts b/backend/src/entities/visualizations/panel-position/panel-position.controller.ts index be24111f9..c46fe16fb 100644 --- a/backend/src/entities/visualizations/panel-position/panel-position.controller.ts +++ b/backend/src/entities/visualizations/panel-position/panel-position.controller.ts @@ -26,6 +26,7 @@ import { GeneratePanelPositionWithAiDs } from './data-structures/generate-panel- import { UpdatePanelPositionDs } from './data-structures/update-panel-position.ds.js'; import { CreatePanelPositionDto } from './dto/create-panel-position.dto.js'; import { FoundPanelPositionDto } from './dto/found-panel-position.dto.js'; +import { GeneratedPanelWithPositionDto } from './dto/generated-panel-with-position.dto.js'; import { GeneratePanelPositionWithAiDto } from './dto/generate-panel-position-with-ai.dto.js'; import { UpdatePanelPositionDto } from './dto/update-panel-position.dto.js'; import { @@ -155,8 +156,8 @@ export class DashboardWidgetController { }) @ApiResponse({ status: 201, - description: 'Widget generated and created.', - type: FoundPanelPositionDto, + description: 'Widget configuration generated by AI (not yet saved).', + type: GeneratedPanelWithPositionDto, }) @ApiBody({ type: GeneratePanelPositionWithAiDto }) @ApiParam({ name: 'dashboardId', required: true }) @@ -171,7 +172,7 @@ export class DashboardWidgetController { @MasterPassword() masterPwd: string, @UserId() userId: string, @Body() generateDto: GeneratePanelPositionWithAiDto, - ): Promise { + ): Promise { const inputData: GeneratePanelPositionWithAiDs = { dashboardId, connectionId, @@ -181,6 +182,6 @@ export class DashboardWidgetController { table_name: tableName, name: generateDto.name, }; - return await this.generateWidgetWithAiUseCase.execute(inputData, InTransactionEnum.ON); + return await this.generateWidgetWithAiUseCase.execute(inputData, InTransactionEnum.OFF); } } diff --git a/backend/src/entities/visualizations/panel-position/use-cases/generate-panel-position-with-ai.use.case.ts b/backend/src/entities/visualizations/panel-position/use-cases/generate-panel-position-with-ai.use.case.ts index 895fba20a..16ba5e489 100644 --- a/backend/src/entities/visualizations/panel-position/use-cases/generate-panel-position-with-ai.use.case.ts +++ b/backend/src/entities/visualizations/panel-position/use-cases/generate-panel-position-with-ai.use.case.ts @@ -7,12 +7,9 @@ import { BaseType } from '../../../../common/data-injection.tokens.js'; import { DashboardWidgetTypeEnum } from '../../../../enums/dashboard-widget-type.enum.js'; import { Messages } from '../../../../exceptions/text/messages.js'; import { AICoreService, AIProviderType, cleanAIJsonResponse } from '../../../../ai-core/index.js'; -import { PanelPositionEntity } from '../panel-position.entity.js'; import { GeneratePanelPositionWithAiDs } from '../data-structures/generate-panel-position-with-ai.ds.js'; -import { FoundPanelPositionDto } from '../dto/found-panel-position.dto.js'; -import { buildFoundPanelPositionDto } from '../utils/build-found-dashboard-widget-dto.util.js'; +import { GeneratedPanelWithPositionDto } from '../dto/generated-panel-with-position.dto.js'; import { IGeneratePanelPositionWithAi } from './panel-position-use-cases.interface.js'; -import { PanelEntity } from '../../panel/panel.entity.js'; import { validateQuerySafety } from '../../panel/utils/check-query-is-safe.util.js'; interface AIGeneratedWidgetResponse { @@ -41,7 +38,7 @@ interface AIGeneratedWidgetResponse { @Injectable({ scope: Scope.REQUEST }) export class GeneratePanelPositionWithAiUseCase - extends AbstractUseCase + extends AbstractUseCase implements IGeneratePanelPositionWithAi { constructor( @@ -52,7 +49,7 @@ export class GeneratePanelPositionWithAiUseCase super(); } - public async implementation(inputData: GeneratePanelPositionWithAiDs): Promise { + public async implementation(inputData: GeneratePanelPositionWithAiDs): Promise { const { dashboardId, connectionId, @@ -119,29 +116,24 @@ export class GeneratePanelPositionWithAiUseCase validateQuerySafety(generatedWidget.query_text, foundConnection.type as ConnectionTypesEnum); - const newPanel = new PanelEntity(); - newPanel.name = name || generatedWidget.name; - newPanel.description = generatedWidget.description || null; - newPanel.panel_type = this.mapWidgetType(generatedWidget.widget_type); - newPanel.chart_type = generatedWidget.chart_type || null; - newPanel.panel_options = generatedWidget.widget_options - ? (generatedWidget.widget_options as unknown as string) - : null; - newPanel.query_text = generatedWidget.query_text; - newPanel.connection_id = foundConnection.id; - - const savedQuery = await this._dbContext.panelRepository.save(newPanel); - - const newPanelPosition = new PanelPositionEntity(); - newPanelPosition.position_x = position_x ?? 0; - newPanelPosition.position_y = position_y ?? 0; - newPanelPosition.width = width ?? 6; - newPanelPosition.height = height ?? 4; - newPanelPosition.dashboard_id = dashboardId; - newPanelPosition.query_id = savedQuery.id; - - const savedWidget = await this._dbContext.panelPositionRepository.savePanelPosition(newPanelPosition); - return buildFoundPanelPositionDto(savedWidget); + return { + name: name || generatedWidget.name, + description: generatedWidget.description || null, + widget_type: this.mapWidgetType(generatedWidget.widget_type), + chart_type: generatedWidget.chart_type || null, + widget_options: generatedWidget.widget_options + ? (generatedWidget.widget_options as unknown as Record) + : null, + query_text: generatedWidget.query_text, + connection_id: connectionId, + panel_position: { + position_x: position_x ?? 0, + position_y: position_y ?? 0, + width: width ?? 6, + height: height ?? 4, + dashboard_id: dashboardId, + }, + }; } private buildPrompt( diff --git a/backend/src/entities/visualizations/panel-position/use-cases/panel-position-use-cases.interface.ts b/backend/src/entities/visualizations/panel-position/use-cases/panel-position-use-cases.interface.ts index 1e878480e..e17943c9f 100644 --- a/backend/src/entities/visualizations/panel-position/use-cases/panel-position-use-cases.interface.ts +++ b/backend/src/entities/visualizations/panel-position/use-cases/panel-position-use-cases.interface.ts @@ -4,6 +4,7 @@ import { DeletePanelPositionDs } from '../data-structures/delete-panel-position. import { GeneratePanelPositionWithAiDs } from '../data-structures/generate-panel-position-with-ai.ds.js'; import { UpdatePanelPositionDs } from '../data-structures/update-panel-position.ds.js'; import { FoundPanelPositionDto } from '../dto/found-panel-position.dto.js'; +import { GeneratedPanelWithPositionDto } from '../dto/generated-panel-with-position.dto.js'; export interface ICreatePanelPositionWidget { execute(inputData: CreatePanelPositionDs, inTransaction: InTransactionEnum): Promise; @@ -18,5 +19,5 @@ export interface IDeletePanelPosition { } export interface IGeneratePanelPositionWithAi { - execute(inputData: GeneratePanelPositionWithAiDs, inTransaction: InTransactionEnum): Promise; + execute(inputData: GeneratePanelPositionWithAiDs, inTransaction: InTransactionEnum): Promise; } diff --git a/backend/test/ava-tests/saas-tests/dashboard-ai-widget-e2e.test.ts b/backend/test/ava-tests/saas-tests/dashboard-ai-widget-e2e.test.ts index 9e0a911d5..f231d8a4b 100644 --- a/backend/test/ava-tests/saas-tests/dashboard-ai-widget-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/dashboard-ai-widget-e2e.test.ts @@ -162,11 +162,23 @@ test.serial(`${currentTest} should generate a widget with AI for chart type`, as const generateWidgetRO = JSON.parse(generateWidget.text); t.is(generateWidget.status, 201); - t.truthy(generateWidgetRO.id); - t.truthy(generateWidgetRO.query_id); - t.is(generateWidgetRO.dashboard_id, dashboardId); - t.is(generateWidgetRO.width, 6); - t.is(generateWidgetRO.height, 4); + + t.is(generateWidgetRO.name, 'Sales by Category'); + t.is(generateWidgetRO.description, 'Bar chart showing total sales grouped by category'); + t.is(generateWidgetRO.widget_type, DashboardWidgetTypeEnum.Chart); + t.is(generateWidgetRO.chart_type, 'bar'); + t.is(generateWidgetRO.query_text, 'SELECT category, SUM(amount) as total FROM orders GROUP BY category'); + t.is(generateWidgetRO.connection_id, connectionId); + t.truthy(generateWidgetRO.widget_options); + t.is(generateWidgetRO.widget_options.label_column, 'category'); + t.is(generateWidgetRO.widget_options.value_column, 'total'); + + t.truthy(generateWidgetRO.panel_position); + t.is(generateWidgetRO.panel_position.position_x, 0); + t.is(generateWidgetRO.panel_position.position_y, 0); + t.is(generateWidgetRO.panel_position.width, 6); + t.is(generateWidgetRO.panel_position.height, 4); + t.is(generateWidgetRO.panel_position.dashboard_id, dashboardId); const getSavedQueries = await request(app.getHttpServer()) .get(`/connection/${connectionId}/saved-queries`) @@ -177,13 +189,7 @@ test.serial(`${currentTest} should generate a widget with AI for chart type`, as const queries = JSON.parse(getSavedQueries.text); t.is(getSavedQueries.status, 200); - t.true(queries.length >= 1); - - const createdQuery = queries.find((q: { id: string }) => q.id === generateWidgetRO.query_id); - t.truthy(createdQuery); - t.is(createdQuery.name, 'Sales by Category'); - t.is(createdQuery.widget_type, DashboardWidgetTypeEnum.Chart); - t.is(createdQuery.chart_type, 'bar'); + t.is(queries.length, 0); }); test.serial(`${currentTest} should generate a counter widget with AI`, async (t) => { @@ -224,20 +230,15 @@ test.serial(`${currentTest} should generate a counter widget with AI`, async (t) const generateWidgetRO = JSON.parse(generateWidget.text); t.is(generateWidget.status, 201); - t.truthy(generateWidgetRO.id); - t.truthy(generateWidgetRO.query_id); - const getQuery = await request(app.getHttpServer()) - .get(`/connection/${connectionId}/saved-query/${generateWidgetRO.query_id}`) - .set('Cookie', token) - .set('masterpwd', 'ahalaimahalai') - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + t.is(generateWidgetRO.name, 'Total Orders'); + t.is(generateWidgetRO.description, 'Counter showing total number of orders'); + t.is(generateWidgetRO.widget_type, DashboardWidgetTypeEnum.Counter); + t.is(generateWidgetRO.query_text, 'SELECT COUNT(*) as total FROM orders'); + t.is(generateWidgetRO.connection_id, connectionId); - const query = JSON.parse(getQuery.text); - t.is(getQuery.status, 200); - t.is(query.name, 'Total Orders'); - t.is(query.widget_type, DashboardWidgetTypeEnum.Counter); + t.truthy(generateWidgetRO.panel_position); + t.is(generateWidgetRO.panel_position.dashboard_id, dashboardId); }); test.serial(`${currentTest} should reject AI-generated unsafe query`, async (t) => { @@ -320,16 +321,7 @@ test.serial(`${currentTest} should generate widget with custom name`, async (t) const generateWidgetRO = JSON.parse(generateWidget.text); t.is(generateWidget.status, 201); - - const getQuery = await request(app.getHttpServer()) - .get(`/connection/${connectionId}/saved-query/${generateWidgetRO.query_id}`) - .set('Cookie', token) - .set('masterpwd', 'ahalaimahalai') - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - const query = JSON.parse(getQuery.text); - t.is(query.name, customName); + t.is(generateWidgetRO.name, customName); }); test.serial(`${currentTest} should fail without tableName query parameter`, async (t) => { From 8703e11040b886fc1b4185f07c4b158018355b2b Mon Sep 17 00:00:00 2001 From: Artem Niehrieiev Date: Thu, 19 Feb 2026 10:27:14 +0000 Subject: [PATCH 3/3] disable tables in categories --- .../find-table-categories-with-tables.use.case.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/src/entities/table-categories/use-cases/find-table-categories-with-tables.use.case.ts b/backend/src/entities/table-categories/use-cases/find-table-categories-with-tables.use.case.ts index d19954dbf..e3ff9694f 100644 --- a/backend/src/entities/table-categories/use-cases/find-table-categories-with-tables.use.case.ts +++ b/backend/src/entities/table-categories/use-cases/find-table-categories-with-tables.use.case.ts @@ -90,20 +90,20 @@ export class FindTableCategoriesWithTablesUseCase const foundTableCategories = await this._dbContext.tableCategoriesRepository.findTableCategoriesForConnection(connectionId); - const sortedTables = tablesRO.sort((tableRO1, tableRO2) => { - const name1 = tableRO1.display_name || tableRO1.table; - const name2 = tableRO2.display_name || tableRO2.table; - return name1.localeCompare(name2); - }); + // const sortedTables = tablesRO.sort((tableRO1, tableRO2) => { + // const name1 = tableRO1.display_name || tableRO1.table; + // const name2 = tableRO2.display_name || tableRO2.table; + // return name1.localeCompare(name2); + // }); const allTableCategory: FoundTableCategoriesWithTablesRo = { category_id: null, category_color: null, category_name: 'All tables', - tables: sortedTables, + tables: tablesRO, }; const foundTableCategoriesRO = foundTableCategories.map((category) => { - const tablesInCategory = sortedTables.filter((tableRO) => { + const tablesInCategory = tablesRO.filter((tableRO) => { return category.tables.includes(tableRO.table); }); return {