diff --git a/backend/src/entities/visualizations/dashboard-widget/dashboard-widget.entity.ts b/backend/src/entities/visualizations/dashboard-widget/dashboard-widget.entity.ts index 708bec7ff..6c1d75929 100644 --- a/backend/src/entities/visualizations/dashboard-widget/dashboard-widget.entity.ts +++ b/backend/src/entities/visualizations/dashboard-widget/dashboard-widget.entity.ts @@ -1,16 +1,4 @@ -import sjson from 'secure-json-parse'; -import { - AfterLoad, - BeforeInsert, - BeforeUpdate, - Column, - Entity, - JoinColumn, - ManyToOne, - PrimaryGeneratedColumn, - Relation, -} from 'typeorm'; -import { DashboardWidgetTypeEnum } from '../../../enums/dashboard-widget-type.enum.js'; +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'; @@ -19,18 +7,6 @@ export class DashboardWidgetEntity { @PrimaryGeneratedColumn('uuid') id: string; - @Column({ type: 'varchar' }) - widget_type: DashboardWidgetTypeEnum; - - @Column({ type: 'varchar', default: null, nullable: true }) - chart_type: string | null; - - @Column({ default: null, nullable: true }) - name: string | null; - - @Column({ type: 'text', default: null, nullable: true }) - description: string | null; - @Column({ type: 'int', default: 0 }) position_x: number; @@ -43,9 +19,6 @@ export class DashboardWidgetEntity { @Column({ type: 'int', default: 3 }) height: number; - @Column('json', { default: null, nullable: true }) - widget_options: string | null; - @Column({ type: 'uuid' }) dashboard_id: string; @@ -63,40 +36,4 @@ export class DashboardWidgetEntity { @ManyToOne(() => SavedDbQueryEntity, { onDelete: 'SET NULL', nullable: true }) @JoinColumn({ name: 'query_id' }) query: Relation | null; - - @BeforeUpdate() - stringifyOptionsOnUpdate(): void { - try { - if (this.widget_options && typeof this.widget_options === 'object') { - this.widget_options = JSON.stringify(this.widget_options); - } - } catch (e) { - console.error('-> Error widget options stringify ' + e.message); - } - } - - @BeforeInsert() - stringifyOptions(): void { - try { - if (this.widget_options && typeof this.widget_options === 'object') { - this.widget_options = JSON.stringify(this.widget_options); - } - } catch (e) { - console.error('-> Error widget options stringify ' + e.message); - } - } - - @AfterLoad() - parseOptions(): void { - try { - if (this.widget_options && typeof this.widget_options === 'string') { - this.widget_options = sjson.parse(this.widget_options, null, { - protoAction: 'remove', - constructorAction: 'remove', - }); - } - } catch (e) { - console.error('-> Error widget options parse ' + e.message); - } - } } diff --git a/backend/src/entities/visualizations/dashboard-widget/dashboard-widgets.controller.ts b/backend/src/entities/visualizations/dashboard-widget/dashboard-widgets.controller.ts index 807efd4e5..fe3a9a80e 100644 --- a/backend/src/entities/visualizations/dashboard-widget/dashboard-widgets.controller.ts +++ b/backend/src/entities/visualizations/dashboard-widget/dashboard-widgets.controller.ts @@ -70,16 +70,11 @@ export class DashboardWidgetController { connectionId, masterPassword: masterPwd, userId, - widget_type: createDto.widget_type, - chart_type: createDto.chart_type, - name: createDto.name, - description: createDto.description, + query_id: createDto.query_id, position_x: createDto.position_x, position_y: createDto.position_y, width: createDto.width, height: createDto.height, - widget_options: createDto.widget_options, - query_id: createDto.query_id, }; return await this.createDashboardWidgetUseCase.execute(inputData, InTransactionEnum.ON); } @@ -110,16 +105,11 @@ export class DashboardWidgetController { connectionId, masterPassword: masterPwd, userId, - widget_type: updateDto.widget_type, - chart_type: updateDto.chart_type, - name: updateDto.name, - description: updateDto.description, + query_id: updateDto.query_id, position_x: updateDto.position_x, position_y: updateDto.position_y, width: updateDto.width, height: updateDto.height, - widget_options: updateDto.widget_options, - query_id: updateDto.query_id, }; return await this.updateDashboardWidgetUseCase.execute(inputData, InTransactionEnum.ON); } diff --git a/backend/src/entities/visualizations/dashboard-widget/data-structures/create-dashboard-widget.ds.ts b/backend/src/entities/visualizations/dashboard-widget/data-structures/create-dashboard-widget.ds.ts index 9e801b3e2..478ef4fd2 100644 --- a/backend/src/entities/visualizations/dashboard-widget/data-structures/create-dashboard-widget.ds.ts +++ b/backend/src/entities/visualizations/dashboard-widget/data-structures/create-dashboard-widget.ds.ts @@ -1,18 +1,11 @@ -import { DashboardWidgetTypeEnum } from '../../../../enums/dashboard-widget-type.enum.js'; - export class CreateDashboardWidgetDs { dashboardId: string; connectionId: string; masterPassword: string; userId: string; - widget_type: DashboardWidgetTypeEnum; - chart_type?: string; - name?: string; - description?: string; + query_id?: string; position_x?: number; position_y?: number; width?: number; height?: number; - widget_options?: Record; - query_id?: string; } diff --git a/backend/src/entities/visualizations/dashboard-widget/data-structures/update-dashboard-widget.ds.ts b/backend/src/entities/visualizations/dashboard-widget/data-structures/update-dashboard-widget.ds.ts index da77edd04..2ee62183d 100644 --- a/backend/src/entities/visualizations/dashboard-widget/data-structures/update-dashboard-widget.ds.ts +++ b/backend/src/entities/visualizations/dashboard-widget/data-structures/update-dashboard-widget.ds.ts @@ -1,19 +1,12 @@ -import { DashboardWidgetTypeEnum } from '../../../../enums/dashboard-widget-type.enum.js'; - export class UpdateDashboardWidgetDs { widgetId: string; dashboardId: string; connectionId: string; masterPassword: string; userId: string; - widget_type?: DashboardWidgetTypeEnum; - chart_type?: string; - name?: string; - description?: string; + query_id?: string; position_x?: number; position_y?: number; width?: number; height?: number; - widget_options?: Record; - query_id?: string; } diff --git a/backend/src/entities/visualizations/dashboard-widget/dto/create-dashboard-widget.dto.ts b/backend/src/entities/visualizations/dashboard-widget/dto/create-dashboard-widget.dto.ts index 79894362f..43d5323f5 100644 --- a/backend/src/entities/visualizations/dashboard-widget/dto/create-dashboard-widget.dto.ts +++ b/backend/src/entities/visualizations/dashboard-widget/dto/create-dashboard-widget.dto.ts @@ -1,27 +1,11 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsEnum, IsInt, IsNotEmpty, IsOptional, IsString, IsUUID, Max, Min } from 'class-validator'; -import { DashboardWidgetTypeEnum } from '../../../../enums/dashboard-widget-type.enum.js'; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsInt, IsOptional, IsUUID, Max, Min } from 'class-validator'; export class CreateDashboardWidgetDto { - @ApiProperty({ description: 'Widget type', enum: DashboardWidgetTypeEnum }) - @IsNotEmpty() - @IsEnum(DashboardWidgetTypeEnum) - widget_type: DashboardWidgetTypeEnum; - - @ApiPropertyOptional({ description: 'Chart type for chart widgets' }) - @IsOptional() - @IsString() - chart_type?: string; - - @ApiPropertyOptional({ description: 'Widget name' }) - @IsOptional() - @IsString() - name?: string; - - @ApiPropertyOptional({ description: 'Widget description' }) + @ApiPropertyOptional({ description: 'Associated saved query ID' }) @IsOptional() - @IsString() - description?: string; + @IsUUID() + query_id?: string; @ApiPropertyOptional({ description: 'Position X in grid', default: 0 }) @IsOptional() @@ -48,13 +32,4 @@ export class CreateDashboardWidgetDto { @Min(1) @Max(12) height?: number; - - @ApiPropertyOptional({ description: 'Visualization options as JSON' }) - @IsOptional() - widget_options?: Record; - - @ApiPropertyOptional({ description: 'Associated saved query ID' }) - @IsOptional() - @IsUUID() - query_id?: string; } diff --git a/backend/src/entities/visualizations/dashboard-widget/dto/found-dashboard-widget.dto.ts b/backend/src/entities/visualizations/dashboard-widget/dto/found-dashboard-widget.dto.ts index d0d5c0e59..ebfd78817 100644 --- a/backend/src/entities/visualizations/dashboard-widget/dto/found-dashboard-widget.dto.ts +++ b/backend/src/entities/visualizations/dashboard-widget/dto/found-dashboard-widget.dto.ts @@ -1,22 +1,9 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { DashboardWidgetTypeEnum } from '../../../../enums/dashboard-widget-type.enum.js'; export class FoundDashboardWidgetDto { @ApiProperty({ description: 'Widget ID' }) id: string; - @ApiProperty({ description: 'Widget type', enum: DashboardWidgetTypeEnum }) - widget_type: DashboardWidgetTypeEnum; - - @ApiPropertyOptional({ description: 'Chart type for chart widgets' }) - chart_type: string | null; - - @ApiPropertyOptional({ description: 'Widget name' }) - name: string | null; - - @ApiPropertyOptional({ description: 'Widget description' }) - description: string | null; - @ApiProperty({ description: 'Position X in grid' }) position_x: number; @@ -29,9 +16,6 @@ export class FoundDashboardWidgetDto { @ApiProperty({ description: 'Widget height in grid units' }) height: number; - @ApiPropertyOptional({ description: 'Visualization options' }) - widget_options: Record | null; - @ApiProperty({ description: 'Dashboard ID' }) dashboard_id: string; diff --git a/backend/src/entities/visualizations/dashboard-widget/dto/update-dashboard-widget.dto.ts b/backend/src/entities/visualizations/dashboard-widget/dto/update-dashboard-widget.dto.ts index 37b44a22f..4414a9262 100644 --- a/backend/src/entities/visualizations/dashboard-widget/dto/update-dashboard-widget.dto.ts +++ b/backend/src/entities/visualizations/dashboard-widget/dto/update-dashboard-widget.dto.ts @@ -1,27 +1,11 @@ import { ApiPropertyOptional } from '@nestjs/swagger'; -import { IsEnum, IsInt, IsOptional, IsString, IsUUID, Max, Min } from 'class-validator'; -import { DashboardWidgetTypeEnum } from '../../../../enums/dashboard-widget-type.enum.js'; +import { IsInt, IsOptional, IsUUID, Max, Min } from 'class-validator'; export class UpdateDashboardWidgetDto { - @ApiPropertyOptional({ description: 'Widget type', enum: DashboardWidgetTypeEnum }) - @IsOptional() - @IsEnum(DashboardWidgetTypeEnum) - widget_type?: DashboardWidgetTypeEnum; - - @ApiPropertyOptional({ description: 'Chart type for chart widgets' }) - @IsOptional() - @IsString() - chart_type?: string; - - @ApiPropertyOptional({ description: 'Widget name' }) - @IsOptional() - @IsString() - name?: string; - - @ApiPropertyOptional({ description: 'Widget description' }) + @ApiPropertyOptional({ description: 'Associated saved query ID' }) @IsOptional() - @IsString() - description?: string; + @IsUUID() + query_id?: string; @ApiPropertyOptional({ description: 'Position X in grid' }) @IsOptional() @@ -48,13 +32,4 @@ export class UpdateDashboardWidgetDto { @Min(1) @Max(12) height?: number; - - @ApiPropertyOptional({ description: 'Visualization options as JSON' }) - @IsOptional() - widget_options?: Record; - - @ApiPropertyOptional({ description: 'Associated saved query ID' }) - @IsOptional() - @IsUUID() - query_id?: string; } diff --git a/backend/src/entities/visualizations/dashboard-widget/use-cases/create-dashboard-widget.use.case.ts b/backend/src/entities/visualizations/dashboard-widget/use-cases/create-dashboard-widget.use.case.ts index 055e8153e..405f4876e 100644 --- a/backend/src/entities/visualizations/dashboard-widget/use-cases/create-dashboard-widget.use.case.ts +++ b/backend/src/entities/visualizations/dashboard-widget/use-cases/create-dashboard-widget.use.case.ts @@ -22,21 +22,7 @@ export class CreateDashboardWidgetUseCase } public async implementation(inputData: CreateDashboardWidgetDs): Promise { - const { - dashboardId, - connectionId, - masterPassword, - widget_type, - chart_type, - name, - description, - position_x, - position_y, - width, - height, - widget_options, - query_id, - } = inputData; + const { dashboardId, connectionId, masterPassword, query_id, position_x, position_y, width, height } = inputData; const foundConnection = await this._dbContext.connectionRepository.findAndDecryptConnection( connectionId, @@ -68,15 +54,10 @@ export class CreateDashboardWidgetUseCase } const newWidget = new DashboardWidgetEntity(); - newWidget.widget_type = widget_type; - newWidget.chart_type = chart_type || null; - newWidget.name = name || null; - newWidget.description = description || null; newWidget.position_x = position_x ?? 0; newWidget.position_y = position_y ?? 0; newWidget.width = width ?? 4; newWidget.height = height ?? 3; - newWidget.widget_options = widget_options ? JSON.stringify(widget_options) : null; newWidget.dashboard_id = dashboardId; newWidget.query_id = query_id || null; diff --git a/backend/src/entities/visualizations/dashboard-widget/use-cases/update-dashboard-widget.use.case.ts b/backend/src/entities/visualizations/dashboard-widget/use-cases/update-dashboard-widget.use.case.ts index a861e394f..710495a85 100644 --- a/backend/src/entities/visualizations/dashboard-widget/use-cases/update-dashboard-widget.use.case.ts +++ b/backend/src/entities/visualizations/dashboard-widget/use-cases/update-dashboard-widget.use.case.ts @@ -21,22 +21,8 @@ export class UpdateDashboardWidgetUseCase } public async implementation(inputData: UpdateDashboardWidgetDs): Promise { - const { - widgetId, - dashboardId, - connectionId, - masterPassword, - widget_type, - chart_type, - name, - description, - position_x, - position_y, - width, - height, - widget_options, - query_id, - } = inputData; + const { widgetId, dashboardId, connectionId, masterPassword, query_id, position_x, position_y, width, height } = + inputData; const foundConnection = await this._dbContext.connectionRepository.findAndDecryptConnection( connectionId, @@ -74,20 +60,9 @@ export class UpdateDashboardWidgetUseCase if (!foundQuery) { throw new BadRequestException(Messages.SAVED_QUERY_NOT_FOUND); } + foundWidget.query_id = query_id; } - if (widget_type !== undefined) { - foundWidget.widget_type = widget_type; - } - if (chart_type !== undefined) { - foundWidget.chart_type = chart_type; - } - if (name !== undefined) { - foundWidget.name = name; - } - if (description !== undefined) { - foundWidget.description = description; - } if (position_x !== undefined) { foundWidget.position_x = position_x; } @@ -100,12 +75,6 @@ export class UpdateDashboardWidgetUseCase if (height !== undefined) { foundWidget.height = height; } - if (widget_options !== undefined) { - foundWidget.widget_options = widget_options ? JSON.stringify(widget_options) : null; - } - if (query_id !== undefined) { - foundWidget.query_id = query_id; - } const savedWidget = await this._dbContext.dashboardWidgetRepository.saveWidget(foundWidget); return buildFoundDashboardWidgetDto(savedWidget); 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 index dce12ce97..ae6497adb 100644 --- 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 @@ -4,15 +4,10 @@ import { FoundDashboardWidgetDto } from '../dto/found-dashboard-widget.dto.js'; export function buildFoundDashboardWidgetDto(widget: DashboardWidgetEntity): FoundDashboardWidgetDto { return { id: widget.id, - widget_type: widget.widget_type, - chart_type: widget.chart_type, - name: widget.name, - description: widget.description, position_x: widget.position_x, position_y: widget.position_y, width: widget.width, height: widget.height, - widget_options: widget.widget_options as unknown as Record | null, dashboard_id: widget.dashboard_id, query_id: widget.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/saved-db-query/data-structures/create-saved-db-query.ds.ts index 9de6c7c62..f9a3acdd5 100644 --- a/backend/src/entities/visualizations/saved-db-query/data-structures/create-saved-db-query.ds.ts +++ b/backend/src/entities/visualizations/saved-db-query/data-structures/create-saved-db-query.ds.ts @@ -1,7 +1,12 @@ +import { DashboardWidgetTypeEnum } from '../../../../enums/dashboard-widget-type.enum.js'; + export class CreateSavedDbQueryDs { connectionId: string; masterPassword: string; name: string; description?: string; + widget_type: DashboardWidgetTypeEnum; + chart_type?: string; + widget_options?: Record; query_text: string; } diff --git a/backend/src/entities/visualizations/saved-db-query/data-structures/update-saved-db-query.ds.ts b/backend/src/entities/visualizations/saved-db-query/data-structures/update-saved-db-query.ds.ts index dee72a4cf..7dedb89ae 100644 --- a/backend/src/entities/visualizations/saved-db-query/data-structures/update-saved-db-query.ds.ts +++ b/backend/src/entities/visualizations/saved-db-query/data-structures/update-saved-db-query.ds.ts @@ -1,8 +1,13 @@ +import { DashboardWidgetTypeEnum } from '../../../../enums/dashboard-widget-type.enum.js'; + export class UpdateSavedDbQueryDs { queryId: string; connectionId: string; masterPassword: string; name?: string; description?: string; + widget_type?: DashboardWidgetTypeEnum; + chart_type?: string; + widget_options?: Record; query_text?: string; } diff --git a/backend/src/entities/visualizations/saved-db-query/dto/create-saved-db-query.dto.ts b/backend/src/entities/visualizations/saved-db-query/dto/create-saved-db-query.dto.ts index 2eed96c7c..bba0d427a 100644 --- a/backend/src/entities/visualizations/saved-db-query/dto/create-saved-db-query.dto.ts +++ b/backend/src/entities/visualizations/saved-db-query/dto/create-saved-db-query.dto.ts @@ -1,5 +1,6 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsOptional, IsString, MaxLength } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsEnum, IsNotEmpty, IsOptional, IsString, MaxLength } from 'class-validator'; +import { DashboardWidgetTypeEnum } from '../../../../enums/dashboard-widget-type.enum.js'; export class CreateSavedDbQueryDto { @ApiProperty({ type: String, description: 'The name of the saved query' }) @@ -8,11 +9,25 @@ export class CreateSavedDbQueryDto { @MaxLength(255) name: string; - @ApiProperty({ type: String, description: 'The description of the saved query', required: false }) + @ApiPropertyOptional({ type: String, description: 'The description of the saved query' }) @IsOptional() @IsString() description?: string; + @ApiProperty({ description: 'Widget type', enum: DashboardWidgetTypeEnum }) + @IsNotEmpty() + @IsEnum(DashboardWidgetTypeEnum) + widget_type: DashboardWidgetTypeEnum; + + @ApiPropertyOptional({ description: 'Chart type for chart widgets' }) + @IsOptional() + @IsString() + chart_type?: string; + + @ApiPropertyOptional({ description: 'Visualization options as JSON' }) + @IsOptional() + widget_options?: Record; + @ApiProperty({ type: String, description: 'The SQL query text' }) @IsNotEmpty() @IsString() diff --git a/backend/src/entities/visualizations/saved-db-query/dto/found-saved-db-query.dto.ts b/backend/src/entities/visualizations/saved-db-query/dto/found-saved-db-query.dto.ts index 8f12f1f73..9b39bbe18 100644 --- a/backend/src/entities/visualizations/saved-db-query/dto/found-saved-db-query.dto.ts +++ b/backend/src/entities/visualizations/saved-db-query/dto/found-saved-db-query.dto.ts @@ -1,4 +1,5 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { DashboardWidgetTypeEnum } from '../../../../enums/dashboard-widget-type.enum.js'; export class FoundSavedDbQueryDto { @ApiProperty({ type: String, description: 'The entity id' }) @@ -7,9 +8,18 @@ export class FoundSavedDbQueryDto { @ApiProperty({ type: String, description: 'The name of the saved query' }) name: string; - @ApiProperty({ type: String, description: 'The description of the saved query', nullable: true }) + @ApiPropertyOptional({ type: String, description: 'The description of the saved query' }) 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({ type: String, description: 'The SQL query text' }) query_text: string; diff --git a/backend/src/entities/visualizations/saved-db-query/dto/update-saved-db-query.dto.ts b/backend/src/entities/visualizations/saved-db-query/dto/update-saved-db-query.dto.ts index 1f6f01c47..438b03c3c 100644 --- a/backend/src/entities/visualizations/saved-db-query/dto/update-saved-db-query.dto.ts +++ b/backend/src/entities/visualizations/saved-db-query/dto/update-saved-db-query.dto.ts @@ -1,19 +1,34 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsOptional, IsString, MaxLength } from 'class-validator'; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsEnum, IsOptional, IsString, MaxLength } from 'class-validator'; +import { DashboardWidgetTypeEnum } from '../../../../enums/dashboard-widget-type.enum.js'; export class UpdateSavedDbQueryDto { - @ApiProperty({ type: String, description: 'The name of the saved query', required: false }) + @ApiPropertyOptional({ type: String, description: 'The name of the saved query' }) @IsOptional() @IsString() @MaxLength(255) name?: string; - @ApiProperty({ type: String, description: 'The description of the saved query', required: false }) + @ApiPropertyOptional({ type: String, description: 'The description of the saved query' }) @IsOptional() @IsString() description?: string; - @ApiProperty({ type: String, description: 'The SQL query text', required: false }) + @ApiPropertyOptional({ description: 'Widget type', enum: DashboardWidgetTypeEnum }) + @IsOptional() + @IsEnum(DashboardWidgetTypeEnum) + widget_type?: DashboardWidgetTypeEnum; + + @ApiPropertyOptional({ description: 'Chart type for chart widgets' }) + @IsOptional() + @IsString() + chart_type?: string; + + @ApiPropertyOptional({ description: 'Visualization options as JSON' }) + @IsOptional() + widget_options?: Record; + + @ApiPropertyOptional({ type: String, description: 'The SQL query text' }) @IsOptional() @IsString() query_text?: string; diff --git a/backend/src/entities/visualizations/saved-db-query/saved-db-query.controller.ts b/backend/src/entities/visualizations/saved-db-query/saved-db-query.controller.ts index 5f564fe1c..f926e0547 100644 --- a/backend/src/entities/visualizations/saved-db-query/saved-db-query.controller.ts +++ b/backend/src/entities/visualizations/saved-db-query/saved-db-query.controller.ts @@ -159,6 +159,9 @@ export class SavedDbQueryController { masterPassword: masterPwd, name: createDto.name, description: createDto.description, + widget_type: createDto.widget_type, + chart_type: createDto.chart_type, + widget_options: createDto.widget_options, query_text: createDto.query_text, }; return await this.createSavedDbQueryUseCase.execute(inputData, InTransactionEnum.OFF); @@ -195,6 +198,9 @@ export class SavedDbQueryController { masterPassword: masterPwd, name: updateDto.name, description: updateDto.description, + widget_type: updateDto.widget_type, + chart_type: updateDto.chart_type, + widget_options: updateDto.widget_options, query_text: updateDto.query_text, }; return await this.updateSavedDbQueryUseCase.execute(inputData, InTransactionEnum.OFF); diff --git a/backend/src/entities/visualizations/saved-db-query/saved-db-query.entity.ts b/backend/src/entities/visualizations/saved-db-query/saved-db-query.entity.ts index b21557ad0..c041376ce 100644 --- a/backend/src/entities/visualizations/saved-db-query/saved-db-query.entity.ts +++ b/backend/src/entities/visualizations/saved-db-query/saved-db-query.entity.ts @@ -1,3 +1,4 @@ +import sjson from 'secure-json-parse'; import { AfterLoad, BeforeInsert, @@ -13,6 +14,7 @@ import { import { Encryptor } from '../../../helpers/encryption/encryptor.js'; import { ConnectionEntity } from '../../connection/connection.entity.js'; import { DashboardWidgetEntity } from '../dashboard-widget/dashboard-widget.entity.js'; +import { DashboardWidgetTypeEnum } from '../../../enums/dashboard-widget-type.enum.js'; @Entity('saved_db_query') export class SavedDbQueryEntity { @@ -25,6 +27,15 @@ export class SavedDbQueryEntity { @Column({ type: 'text', default: null, nullable: true }) description: string | null; + @Column({ type: 'varchar' }) + widget_type: DashboardWidgetTypeEnum; + + @Column({ type: 'varchar', default: null, nullable: true }) + chart_type: string | null; + + @Column('json', { default: null, nullable: true }) + widget_options: string | null; + @Column({ type: 'text' }) query_text: string; @@ -35,25 +46,49 @@ export class SavedDbQueryEntity { updated_at: Date; @BeforeInsert() - encryptQueryTextOnInsert(): void { + encryptQueryTextStringifyOptionsOnInsert(): void { if (this.query_text) { 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); + } + } catch (e) { + console.error('-> Error widget options stringify ' + e.message); + } } @BeforeUpdate() - encryptQueryTextOnUpdate(): void { + encryptQueryTextStringifyOptionsOnUpdate(): void { this.updated_at = new Date(); if (this.query_text) { 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); + } + } catch (e) { + console.error('-> Error widget options stringify ' + e.message); + } } @AfterLoad() - decryptQueryText(): void { + decryptQueryTextParseOptions(): void { if (this.query_text) { 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, { + protoAction: 'remove', + constructorAction: 'remove', + }); + } + } catch (e) { + console.error('-> Error widget options parse ' + e.message); + } } @ManyToOne( diff --git a/backend/src/entities/visualizations/saved-db-query/use-cases/create-saved-db-query.use.case.ts b/backend/src/entities/visualizations/saved-db-query/use-cases/create-saved-db-query.use.case.ts index 19c08e41a..54ae95ec5 100644 --- a/backend/src/entities/visualizations/saved-db-query/use-cases/create-saved-db-query.use.case.ts +++ b/backend/src/entities/visualizations/saved-db-query/use-cases/create-saved-db-query.use.case.ts @@ -24,7 +24,7 @@ export class CreateSavedDbQueryUseCase } public async implementation(inputData: CreateSavedDbQueryDs): Promise { - const { connectionId, masterPassword, name, description, query_text } = inputData; + const { connectionId, masterPassword, name, description, widget_type, chart_type, widget_options, query_text } = inputData; const foundConnection = await this._dbContext.connectionRepository.findAndDecryptConnection( connectionId, @@ -40,6 +40,9 @@ export class CreateSavedDbQueryUseCase const newQuery = new SavedDbQueryEntity(); newQuery.name = name; newQuery.description = description || null; + newQuery.widget_type = widget_type; + newQuery.chart_type = chart_type || null; + newQuery.widget_options = widget_options ? (widget_options as unknown as string) : null; newQuery.query_text = query_text; newQuery.connection_id = foundConnection.id; diff --git a/backend/src/entities/visualizations/saved-db-query/use-cases/update-saved-db-query.use.case.ts b/backend/src/entities/visualizations/saved-db-query/use-cases/update-saved-db-query.use.case.ts index 489c24ec0..cafe12d86 100644 --- a/backend/src/entities/visualizations/saved-db-query/use-cases/update-saved-db-query.use.case.ts +++ b/backend/src/entities/visualizations/saved-db-query/use-cases/update-saved-db-query.use.case.ts @@ -24,7 +24,7 @@ export class UpdateSavedDbQueryUseCase } public async implementation(inputData: UpdateSavedDbQueryDs): Promise { - const { queryId, connectionId, masterPassword, name, description, query_text } = inputData; + const { queryId, connectionId, masterPassword, name, description, widget_type, chart_type, widget_options, query_text } = inputData; const foundConnection = await this._dbContext.connectionRepository.findAndDecryptConnection( connectionId, @@ -47,6 +47,15 @@ export class UpdateSavedDbQueryUseCase if (description !== undefined) { foundQuery.description = description; } + if (widget_type !== undefined) { + foundQuery.widget_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; + } if (query_text !== undefined) { validateQuerySafety(query_text, foundConnection.type as ConnectionTypesEnum); foundQuery.query_text = query_text; 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 index 9f569e787..a3fc8c49e 100644 --- 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 @@ -6,6 +6,9 @@ export function buildFoundSavedDbQueryDto(entity: SavedDbQueryEntity): FoundSave 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, diff --git a/backend/src/migrations/1770116525022-MovedDashboardWidgetPropertiesToSavedQuery.ts b/backend/src/migrations/1770116525022-MovedDashboardWidgetPropertiesToSavedQuery.ts new file mode 100644 index 000000000..894fdc1d3 --- /dev/null +++ b/backend/src/migrations/1770116525022-MovedDashboardWidgetPropertiesToSavedQuery.ts @@ -0,0 +1,27 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class MovedDashboardWidgetPropertiesToSavedQuery1770116525022 implements MigrationInterface { + name = 'MovedDashboardWidgetPropertiesToSavedQuery1770116525022'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "dashboard_widget" DROP COLUMN "widget_options"`); + await queryRunner.query(`ALTER TABLE "dashboard_widget" DROP COLUMN "widget_type"`); + await queryRunner.query(`ALTER TABLE "dashboard_widget" DROP COLUMN "name"`); + await queryRunner.query(`ALTER TABLE "dashboard_widget" DROP COLUMN "description"`); + await queryRunner.query(`ALTER TABLE "dashboard_widget" DROP COLUMN "chart_type"`); + await queryRunner.query(`ALTER TABLE "saved_db_query" ADD "widget_type" character varying NOT NULL`); + await queryRunner.query(`ALTER TABLE "saved_db_query" ADD "chart_type" character varying`); + await queryRunner.query(`ALTER TABLE "saved_db_query" ADD "widget_options" json`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "saved_db_query" DROP COLUMN "widget_options"`); + await queryRunner.query(`ALTER TABLE "saved_db_query" DROP COLUMN "chart_type"`); + await queryRunner.query(`ALTER TABLE "saved_db_query" DROP COLUMN "widget_type"`); + await queryRunner.query(`ALTER TABLE "dashboard_widget" ADD "chart_type" character varying`); + await queryRunner.query(`ALTER TABLE "dashboard_widget" ADD "description" text`); + await queryRunner.query(`ALTER TABLE "dashboard_widget" ADD "name" character varying`); + await queryRunner.query(`ALTER TABLE "dashboard_widget" ADD "widget_type" character varying NOT NULL`); + await queryRunner.query(`ALTER TABLE "dashboard_widget" ADD "widget_options" json`); + } +} diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-dashboard-e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-dashboard-e2e.test.ts index df3f98488..f34350df8 100644 --- a/backend/test/ava-tests/non-saas-tests/non-saas-dashboard-e2e.test.ts +++ b/backend/test/ava-tests/non-saas-tests/non-saas-dashboard-e2e.test.ts @@ -18,6 +18,7 @@ import { getTestData } from '../../utils/get-test-data.js'; import { registerUserAndReturnUserInfo } from '../../utils/register-user-and-return-user-info.js'; import { setSaasEnvVariable } from '../../utils/set-saas-env-variable.js'; import { TestUtils } from '../../utils/test.utils.js'; +import { DashboardWidgetTypeEnum } from '../../../src/enums/dashboard-widget-type.enum.js'; const mockFactory = new MockFactory(); let app: INestApplication; @@ -311,7 +312,7 @@ test.serial(`${currentTest} should delete dashboard`, async (t) => { currentTest = 'POST /dashboards/:connectionId/:dashboardId/widget'; -test.serial(`${currentTest} should create a new widget in dashboard`, async (t) => { +test.serial(`${currentTest} should create a new widget in dashboard without query`, async (t) => { const { token } = await registerUserAndReturnUserInfo(app); const newConnection = getTestData(mockFactory).newEncryptedConnection; const createdConnection = await request(app.getHttpServer()) @@ -335,9 +336,6 @@ test.serial(`${currentTest} should create a new widget in dashboard`, async (t) const dashboardId = JSON.parse(createDashboard.text).id; const newWidget = { - widget_type: 'table', - name: 'Test Widget', - description: 'Test widget description', position_x: 0, position_y: 0, width: 4, @@ -354,9 +352,7 @@ test.serial(`${currentTest} should create a new widget in dashboard`, async (t) const createWidgetRO = JSON.parse(createWidget.text); t.is(createWidget.status, 201); - t.is(createWidgetRO.widget_type, newWidget.widget_type); - t.is(createWidgetRO.name, newWidget.name); - t.is(createWidgetRO.description, newWidget.description); + t.is(createWidgetRO.query_id, null); t.is(createWidgetRO.position_x, newWidget.position_x); t.is(createWidgetRO.position_y, newWidget.position_y); t.is(createWidgetRO.width, newWidget.width); @@ -364,7 +360,7 @@ test.serial(`${currentTest} should create a new widget in dashboard`, async (t) t.is(createWidgetRO.dashboard_id, dashboardId); }); -test.serial(`${currentTest} should create widget with all types`, async (t) => { +test.serial(`${currentTest} should create widget and attach query later`, async (t) => { const { token } = await registerUserAndReturnUserInfo(app); const newConnection = getTestData(mockFactory).newEncryptedConnection; const createdConnection = await request(app.getHttpServer()) @@ -390,17 +386,45 @@ test.serial(`${currentTest} should create widget with all types`, async (t) => { const widgetTypes = ['table', 'chart', 'counter', 'text']; for (const widgetType of widgetTypes) { + // Create widget first without query const createWidget = await request(app.getHttpServer()) .post(`/dashboard/${dashboardId}/widget/${connectionId}`) - .send({ widget_type: widgetType, name: `${widgetType} Widget` }) + .send({}) .set('Cookie', token) .set('masterpwd', 'ahalaimahalai') .set('Content-Type', 'application/json') .set('Accept', 'application/json'); t.is(createWidget.status, 201); - const createWidgetRO = JSON.parse(createWidget.text); - t.is(createWidgetRO.widget_type, widgetType); + const widgetId = JSON.parse(createWidget.text).id; + + // Create a saved query with the specific widget_type + const createQuery = await request(app.getHttpServer()) + .post(`/connection/${connectionId}/saved-query`) + .send({ + name: `${widgetType} Query`, + query_text: 'SELECT * FROM connection LIMIT 10', + widget_type: widgetType, + }) + .set('Cookie', token) + .set('masterpwd', 'ahalaimahalai') + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const queryId = JSON.parse(createQuery.text).id; + + // Attach query to widget + const updateWidget = await request(app.getHttpServer()) + .put(`/dashboard/${dashboardId}/widget/${widgetId}/${connectionId}`) + .send({ query_id: queryId }) + .set('Cookie', token) + .set('masterpwd', 'ahalaimahalai') + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(updateWidget.status, 200); + const updateWidgetRO = JSON.parse(updateWidget.text); + t.is(updateWidgetRO.query_id, queryId); } }); @@ -431,7 +455,7 @@ test.serial(`${currentTest} should update widget`, async (t) => { const createWidget = await request(app.getHttpServer()) .post(`/dashboard/${dashboardId}/widget/${connectionId}`) - .send({ widget_type: 'table', name: 'Original Widget Name' }) + .send({}) .set('Cookie', token) .set('masterpwd', 'ahalaimahalai') .set('Content-Type', 'application/json') @@ -440,7 +464,6 @@ test.serial(`${currentTest} should update widget`, async (t) => { const widgetId = JSON.parse(createWidget.text).id; const updateData = { - name: 'Updated Widget Name', position_x: 2, position_y: 1, width: 6, @@ -457,7 +480,6 @@ test.serial(`${currentTest} should update widget`, async (t) => { const updateWidgetRO = JSON.parse(updateWidget.text); t.is(updateWidget.status, 200); - t.is(updateWidgetRO.name, updateData.name); t.is(updateWidgetRO.position_x, updateData.position_x); t.is(updateWidgetRO.position_y, updateData.position_y); t.is(updateWidgetRO.width, updateData.width); @@ -491,7 +513,7 @@ test.serial(`${currentTest} should delete widget from dashboard`, async (t) => { const createWidget = await request(app.getHttpServer()) .post(`/dashboard/${dashboardId}/widget/${connectionId}`) - .send({ widget_type: 'counter', name: 'Widget to Delete' }) + .send({}) .set('Cookie', token) .set('masterpwd', 'ahalaimahalai') .set('Content-Type', 'application/json') @@ -538,12 +560,13 @@ test.serial(`${currentTest} should create widget linked to a saved query`, async const connectionId = JSON.parse(createdConnection.text).id; - // Create a saved query first + // Create a saved query first (with widget_type) const createQuery = await request(app.getHttpServer()) .post(`/connection/${connectionId}/saved-query`) .send({ name: 'Test Query for Widget', query_text: 'SELECT * FROM connection LIMIT 10', + widget_type: DashboardWidgetTypeEnum.Table, }) .set('Cookie', token) .set('masterpwd', 'ahalaimahalai') @@ -567,8 +590,6 @@ test.serial(`${currentTest} should create widget linked to a saved query`, async const createWidget = await request(app.getHttpServer()) .post(`/dashboard/${dashboardId}/widget/${connectionId}`) .send({ - widget_type: 'table', - name: 'Query Result Widget', query_id: queryId, }) .set('Cookie', token) @@ -609,8 +630,6 @@ test.serial(`${currentTest} should fail to create widget with non-existent query const createWidget = await request(app.getHttpServer()) .post(`/dashboard/${dashboardId}/widget/${connectionId}`) .send({ - widget_type: 'table', - name: 'Widget with Fake Query', query_id: fakeQueryId, }) .set('Cookie', token) diff --git a/backend/test/ava-tests/saas-tests/dashboard-e2e.test.ts b/backend/test/ava-tests/saas-tests/dashboard-e2e.test.ts index e735bc3fb..e5c1b18ab 100644 --- a/backend/test/ava-tests/saas-tests/dashboard-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/dashboard-e2e.test.ts @@ -17,6 +17,7 @@ import { MockFactory } from '../../mock.factory.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'; +import { DashboardWidgetTypeEnum } from '../../../src/enums/dashboard-widget-type.enum.js'; const mockFactory = new MockFactory(); let app: INestApplication; @@ -310,60 +311,59 @@ test.serial(`${currentTest} should delete dashboard`, async (t) => { currentTest = 'POST /dashboards/:connectionId/:dashboardId/widget'; -test.serial(`${currentTest} should create a new widget in dashboard`, async (t) => { - const { token } = await registerUserAndReturnUserInfo(app); - const newConnection = getTestData(mockFactory).newEncryptedConnection; - const createdConnection = await request(app.getHttpServer()) - .post('/connection') - .send(newConnection) - .set('Cookie', token) - .set('masterpwd', 'ahalaimahalai') - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); +test.serial( + `${currentTest} should create a new widget in dashboard without query (query attached later)`, + async (t) => { + const { token } = await registerUserAndReturnUserInfo(app); + const newConnection = getTestData(mockFactory).newEncryptedConnection; + const createdConnection = 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(createdConnection.text).id; + const connectionId = JSON.parse(createdConnection.text).id; - const createDashboard = await request(app.getHttpServer()) - .post(`/dashboards/${connectionId}`) - .send({ name: 'Dashboard with Widgets' }) - .set('Cookie', token) - .set('masterpwd', 'ahalaimahalai') - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - const dashboardId = JSON.parse(createDashboard.text).id; + const createDashboard = await request(app.getHttpServer()) + .post(`/dashboards/${connectionId}`) + .send({ name: 'Dashboard with Widgets' }) + .set('Cookie', token) + .set('masterpwd', 'ahalaimahalai') + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); - const newWidget = { - widget_type: 'table', - name: 'Test Widget', - description: 'Test widget description', - position_x: 0, - position_y: 0, - width: 4, - height: 3, - }; + const dashboardId = JSON.parse(createDashboard.text).id; - const createWidget = await request(app.getHttpServer()) - .post(`/dashboard/${dashboardId}/widget/${connectionId}`) - .send(newWidget) - .set('Cookie', token) - .set('masterpwd', 'ahalaimahalai') - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + // Create widget without query first (query is optional) + const newWidget = { + position_x: 0, + position_y: 0, + width: 4, + height: 3, + }; - const createWidgetRO = JSON.parse(createWidget.text); - t.is(createWidget.status, 201); - t.is(createWidgetRO.widget_type, newWidget.widget_type); - t.is(createWidgetRO.name, newWidget.name); - t.is(createWidgetRO.description, newWidget.description); - t.is(createWidgetRO.position_x, newWidget.position_x); - t.is(createWidgetRO.position_y, newWidget.position_y); - t.is(createWidgetRO.width, newWidget.width); - t.is(createWidgetRO.height, newWidget.height); - t.is(createWidgetRO.dashboard_id, dashboardId); -}); + const createWidget = await request(app.getHttpServer()) + .post(`/dashboard/${dashboardId}/widget/${connectionId}`) + .send(newWidget) + .set('Cookie', token) + .set('masterpwd', 'ahalaimahalai') + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); -test.serial(`${currentTest} should create widget with all types`, async (t) => { + const createWidgetRO = JSON.parse(createWidget.text); + t.is(createWidget.status, 201); + t.is(createWidgetRO.query_id, null); + t.is(createWidgetRO.position_x, newWidget.position_x); + t.is(createWidgetRO.position_y, newWidget.position_y); + t.is(createWidgetRO.width, newWidget.width); + t.is(createWidgetRO.height, newWidget.height); + t.is(createWidgetRO.dashboard_id, dashboardId); + }, +); + +test.serial(`${currentTest} should create widget and then attach query (proper workflow)`, async (t) => { const { token } = await registerUserAndReturnUserInfo(app); const newConnection = getTestData(mockFactory).newEncryptedConnection; const createdConnection = await request(app.getHttpServer()) @@ -389,17 +389,45 @@ test.serial(`${currentTest} should create widget with all types`, async (t) => { const widgetTypes = ['table', 'chart', 'counter', 'text']; for (const widgetType of widgetTypes) { + // First create widget without query const createWidget = await request(app.getHttpServer()) .post(`/dashboard/${dashboardId}/widget/${connectionId}`) - .send({ widget_type: widgetType, name: `${widgetType} Widget` }) + .send({}) .set('Cookie', token) .set('masterpwd', 'ahalaimahalai') .set('Content-Type', 'application/json') .set('Accept', 'application/json'); t.is(createWidget.status, 201); - const createWidgetRO = JSON.parse(createWidget.text); - t.is(createWidgetRO.widget_type, widgetType); + const widgetId = JSON.parse(createWidget.text).id; + + // Then create a saved query with the specific widget_type + const createQuery = await request(app.getHttpServer()) + .post(`/connection/${connectionId}/saved-query`) + .send({ + name: `${widgetType} Query`, + query_text: 'SELECT * FROM connection LIMIT 10', + widget_type: widgetType, + }) + .set('Cookie', token) + .set('masterpwd', 'ahalaimahalai') + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const queryId = JSON.parse(createQuery.text).id; + + // Then attach query to widget via update + const updateWidget = await request(app.getHttpServer()) + .put(`/dashboard/${dashboardId}/widget/${widgetId}/${connectionId}`) + .send({ query_id: queryId }) + .set('Cookie', token) + .set('masterpwd', 'ahalaimahalai') + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(updateWidget.status, 200); + const updateWidgetRO = JSON.parse(updateWidget.text); + t.is(updateWidgetRO.query_id, queryId); } }); @@ -428,9 +456,10 @@ test.serial(`${currentTest} should update widget`, async (t) => { const dashboardId = JSON.parse(createDashboard.text).id; + // Create widget without query first const createWidget = await request(app.getHttpServer()) .post(`/dashboard/${dashboardId}/widget/${connectionId}`) - .send({ widget_type: 'table', name: 'Original Widget Name' }) + .send({}) .set('Cookie', token) .set('masterpwd', 'ahalaimahalai') .set('Content-Type', 'application/json') @@ -439,7 +468,6 @@ test.serial(`${currentTest} should update widget`, async (t) => { const widgetId = JSON.parse(createWidget.text).id; const updateData = { - name: 'Updated Widget Name', position_x: 2, position_y: 1, width: 6, @@ -456,7 +484,6 @@ test.serial(`${currentTest} should update widget`, async (t) => { const updateWidgetRO = JSON.parse(updateWidget.text); t.is(updateWidget.status, 200); - t.is(updateWidgetRO.name, updateData.name); t.is(updateWidgetRO.position_x, updateData.position_x); t.is(updateWidgetRO.position_y, updateData.position_y); t.is(updateWidgetRO.width, updateData.width); @@ -488,9 +515,10 @@ test.serial(`${currentTest} should delete widget from dashboard`, async (t) => { const dashboardId = JSON.parse(createDashboard.text).id; + // Create widget without query const createWidget = await request(app.getHttpServer()) .post(`/dashboard/${dashboardId}/widget/${connectionId}`) - .send({ widget_type: 'counter', name: 'Widget to Delete' }) + .send({}) .set('Cookie', token) .set('masterpwd', 'ahalaimahalai') .set('Content-Type', 'application/json') @@ -537,12 +565,13 @@ test.serial(`${currentTest} should create widget linked to a saved query`, async const connectionId = JSON.parse(createdConnection.text).id; - // Create a saved query first + // Create a saved query first (with widget_type) const createQuery = await request(app.getHttpServer()) .post(`/connection/${connectionId}/saved-query`) .send({ name: 'Test Query for Widget', query_text: 'SELECT * FROM connection LIMIT 10', + widget_type: DashboardWidgetTypeEnum.Table, }) .set('Cookie', token) .set('masterpwd', 'ahalaimahalai') @@ -566,8 +595,6 @@ test.serial(`${currentTest} should create widget linked to a saved query`, async const createWidget = await request(app.getHttpServer()) .post(`/dashboard/${dashboardId}/widget/${connectionId}`) .send({ - widget_type: 'table', - name: 'Query Result Widget', query_id: queryId, }) .set('Cookie', token) @@ -608,8 +635,6 @@ test.serial(`${currentTest} should fail to create widget with non-existent query const createWidget = await request(app.getHttpServer()) .post(`/dashboard/${dashboardId}/widget/${connectionId}`) .send({ - widget_type: 'table', - name: 'Widget with Fake Query', query_id: fakeQueryId, }) .set('Cookie', token) 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 1fec85773..992df0496 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 @@ -25,6 +25,7 @@ import { UpdateSavedDbQueryDto } from '../../../src/entities/visualizations/save import { ExecuteSavedDbQueryResultDto } from '../../../src/entities/visualizations/saved-db-query/dto/execute-saved-db-query-result.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 { DashboardWidgetTypeEnum } from '../../../src/enums/dashboard-widget-type.enum.js'; const mockFactory = new MockFactory(); let app: INestApplication; @@ -81,6 +82,7 @@ test.serial(`${currentTest} should create a new saved query`, async (t) => { name: 'Test Query', description: 'A test query for e2e testing', query_text: `SELECT * FROM ${testTableName}`, + widget_type: DashboardWidgetTypeEnum.Table, }; const createSavedQueryResponse = await request(app.getHttpServer()) @@ -179,6 +181,7 @@ test.serial(`${currentTest} should return all saved queries for a connection`, a name: 'Test Query 1', description: 'First test query', query_text: `SELECT * FROM ${testTableName}`, + widget_type: DashboardWidgetTypeEnum.Table, }; await request(app.getHttpServer()) @@ -193,6 +196,7 @@ test.serial(`${currentTest} should return all saved queries for a connection`, a name: 'Test Query 2', description: 'Second test query', query_text: `SELECT id FROM ${testTableName}`, + widget_type: DashboardWidgetTypeEnum.Table, }; await request(app.getHttpServer()) @@ -262,6 +266,7 @@ test.serial(`${currentTest} should return a saved query by id`, async (t) => { name: 'Test Query', description: 'A test query for e2e testing', query_text: `SELECT * FROM ${testTableName}`, + widget_type: DashboardWidgetTypeEnum.Table, }; const createSavedQueryResponse = await request(app.getHttpServer()) @@ -332,6 +337,7 @@ test.serial(`${currentTest} should update a saved query`, async (t) => { name: 'Original Query Name', description: 'Original description', query_text: `SELECT * FROM ${testTableName}`, + widget_type: DashboardWidgetTypeEnum.Table, }; const createSavedQueryResponse = await request(app.getHttpServer()) @@ -384,6 +390,7 @@ test.serial(`${currentTest} should update only provided fields`, async (t) => { name: 'Original Query Name', description: 'Original description', query_text: `SELECT * FROM ${testTableName}`, + widget_type: DashboardWidgetTypeEnum.Table, }; const createSavedQueryResponse = await request(app.getHttpServer()) @@ -462,6 +469,7 @@ test.serial(`${currentTest} should delete a saved query`, async (t) => { name: 'Query to Delete', description: 'This query will be deleted', query_text: `SELECT * FROM ${testTableName}`, + widget_type: DashboardWidgetTypeEnum.Table, }; const createSavedQueryResponse = await request(app.getHttpServer()) @@ -539,6 +547,7 @@ test.serial(`${currentTest} should execute a saved query and return results`, as name: 'Query to Execute', description: 'This query will be executed', query_text: `SELECT * FROM "${testTableName}"`, + widget_type: DashboardWidgetTypeEnum.Table, }; const createSavedQueryResponse = await request(app.getHttpServer()) @@ -621,6 +630,7 @@ test.serial(`${currentTest} should not return queries from other connections`, a name: 'Query in Connection 1', description: 'This query belongs to connection 1', query_text: `SELECT * FROM ${testTableName}`, + widget_type: DashboardWidgetTypeEnum.Table, }; await request(app.getHttpServer()) @@ -907,6 +917,7 @@ test.serial(`${currentTest} should reject unsafe query when creating saved query name: 'Unsafe Query', description: 'This should be rejected', query_text: `DELETE FROM "${testTableName}"`, + widget_type: DashboardWidgetTypeEnum.Table, }; const createSavedQueryResponse = await request(app.getHttpServer()) @@ -938,6 +949,7 @@ test.serial(`${currentTest} should reject unsafe query when updating saved query name: 'Safe Query', description: 'This is safe', query_text: `SELECT * FROM "${testTableName}"`, + widget_type: DashboardWidgetTypeEnum.Table, }; const createSavedQueryResponse = await request(app.getHttpServer())