refactor: move widget properties from dashboard to saved query#1554
Conversation
Artuomka
commented
Feb 3, 2026
- Removed widget_type, chart_type, name, description, and widget_options from the dashboard widget entity and DTOs.
- Added widget_type, chart_type, and widget_options to the saved_db_query entity and DTOs.
- Updated use cases for creating and updating dashboard widgets to reflect the new structure.
- Adjusted tests to accommodate changes in widget creation and updating workflows.
- Created a migration to alter the database schema accordingly.
- Removed widget_type, chart_type, name, description, and widget_options from the dashboard widget entity and DTOs. - Added widget_type, chart_type, and widget_options to the saved_db_query entity and DTOs. - Updated use cases for creating and updating dashboard widgets to reflect the new structure. - Adjusted tests to accommodate changes in widget creation and updating workflows. - Created a migration to alter the database schema accordingly.
There was a problem hiding this comment.
Pull request overview
This PR refactors the dashboard widget system by moving visualization properties (widget_type, chart_type, name, description, and widget_options) from the dashboard_widget table to the saved_db_query table. This architectural change means widgets now obtain their display properties through their associated saved query rather than storing them directly.
Changes:
- Moved widget visualization properties from dashboard widgets to saved queries
- Created database migration to alter schema accordingly
- Updated DTOs, entities, use cases, and tests to reflect new architecture
Reviewed changes
Copilot reviewed 24 out of 24 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| backend/src/migrations/1770116525022-MovedDashboardWidgetPropertiesToSavedQuery.ts | Database migration dropping columns from dashboard_widget and adding them to saved_db_query |
| backend/src/entities/visualizations/dashboard-widget/dashboard-widget.entity.ts | Removed widget_type, chart_type, name, description, and widget_options columns and lifecycle hooks |
| backend/src/entities/visualizations/saved-db-query/saved-db-query.entity.ts | Added widget_type, chart_type, and widget_options columns with JSON serialization hooks |
| backend/src/entities/visualizations/dashboard-widget/dto/*.dto.ts | Removed visualization properties from widget DTOs |
| backend/src/entities/visualizations/saved-db-query/dto/*.dto.ts | Added visualization properties to saved query DTOs |
| backend/src/entities/visualizations/dashboard-widget/use-cases/*.use.case.ts | Removed visualization property handling from widget use cases |
| backend/src/entities/visualizations/saved-db-query/use-cases/*.use.case.ts | Added visualization property handling to saved query use cases |
| backend/test/ava-tests/saas-tests/dashboard-e2e.test.ts | Updated tests to create widgets without properties and attach queries |
| backend/test/ava-tests/non-saas-tests/non-saas-dashboard-e2e.test.ts | Updated tests to match new widget creation workflow |
| backend/test/ava-tests/saas-tests/saved-database-queries-e2e.test.ts | Added widget_type to all saved query test cases |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 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"`); |
There was a problem hiding this comment.
This migration will cause data loss for existing dashboard widgets. The migration drops columns from dashboard_widget (lines 7-11) without first migrating the existing data to the saved_db_query table. If there are existing widgets with these properties, their data will be permanently lost.
A proper migration should:
- First, copy existing widget data to their associated saved queries
- Handle widgets that don't have an associated saved query
- Then drop the columns from dashboard_widget
Consider adding data migration logic or documenting that this is a breaking change requiring a fresh database.
| 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`); |
There was a problem hiding this comment.
The widget_type column is added as NOT NULL without a default value on line 12. If there are existing records in the saved_db_query table, this ALTER TABLE statement will fail because existing rows won't have a value for this required column. Either provide a default value or ensure existing records are updated before making this column NOT NULL.
| await queryRunner.query(`ALTER TABLE "saved_db_query" ADD "widget_type" character varying NOT NULL`); | |
| await queryRunner.query(`ALTER TABLE "saved_db_query" ADD "widget_type" character varying`); |
| @Column('json', { default: null, nullable: true }) | ||
| widget_options: string | null; |
There was a problem hiding this comment.
The widget_options property is declared as string | null but the lifecycle hooks treat it as an object. After @AfterLoad() on line 78, the property will contain a parsed object, not a string. This type inconsistency can lead to type safety issues throughout the codebase. Consider either:
- Declaring the type as
Record<string, unknown> | nulland handling the serialization differently - Using a transformer to handle the JSON conversion automatically
- Creating separate internal/external properties for type safety
| 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; |
There was a problem hiding this comment.
The type casting widget_options as unknown as string is problematic. The incoming widget_options is typed as Record<string, unknown> in the data structure, but it's being cast to string here. The entity's @BeforeInsert() hook will handle the JSON stringification, so casting to string is incorrect and could cause issues. This line should simply assign the value without type casting: newQuery.widget_options = widget_options || null;
| newQuery.widget_options = widget_options ? (widget_options as unknown as string) : null; | |
| newQuery.widget_options = widget_options || null; |
| foundQuery.chart_type = chart_type; | ||
| } | ||
| if (widget_options !== undefined) { | ||
| foundQuery.widget_options = widget_options ? (widget_options as unknown as string) : null; |
There was a problem hiding this comment.
The same type casting issue exists here as in the create use case. The type casting widget_options as unknown as string is problematic since the @BeforeUpdate() hook in the entity will handle JSON stringification. This line should simply assign the value: foundQuery.widget_options = widget_options || null;
| foundQuery.widget_options = widget_options ? (widget_options as unknown as string) : null; | |
| foundQuery.widget_options = widget_options || null; |
| 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) |
There was a problem hiding this comment.
The test creates a widget with a query_id but doesn't verify that the associated query's properties (widget_type, chart_type, widget_options) are accessible. Consider adding a test that fetches the dashboard and verifies the widget can retrieve its visualization properties through the query relation, or fetch the widget with its query data included to ensure the relationship is properly configured.