From 106bb0f760bc6d20b7c76017ced91dd78de5592a Mon Sep 17 00:00:00 2001 From: Artem Niehrieiev Date: Wed, 25 Feb 2026 16:45:51 +0000 Subject: [PATCH] reworked ai dashboards generation --- backend/src/ai-core/tools/database-tools.ts | 32 ++ .../generate-table-dashboard-with-ai.ds.ts | 1 - .../panel-position.controller.ts | 7 +- ...nerate-table-dashboard-with-ai.use.case.ts | 452 +++++++++--------- ...rd-ai-generate-table-dashboard-e2e.test.ts | 189 ++++---- ...rd-ai-generate-table-dashboard-e2e.test.ts | 194 ++++---- 6 files changed, 448 insertions(+), 427 deletions(-) diff --git a/backend/src/ai-core/tools/database-tools.ts b/backend/src/ai-core/tools/database-tools.ts index 9b718f2ca..6c683e699 100644 --- a/backend/src/ai-core/tools/database-tools.ts +++ b/backend/src/ai-core/tools/database-tools.ts @@ -64,6 +64,38 @@ export function createDatabaseTools(isMongoDB: boolean): AIToolDefinition[] { return tools; } +export function createDashboardGenerationTools(): AIToolDefinition[] { + return [ + { + name: 'getTablesList', + description: + 'Returns the list of all tables and views available in the database. Use this to discover what tables exist before requesting their structure.', + parameters: { + type: 'object', + properties: {}, + required: [], + additionalProperties: false, + }, + }, + { + name: 'getTableStructure', + description: + 'Returns the structure of the specified table including column names, data types, and nullability. Use this to inspect tables before generating dashboard panels.', + parameters: { + type: 'object', + properties: { + tableName: { + type: 'string', + description: 'The name of the table to get the structure for.', + }, + }, + required: ['tableName'], + additionalProperties: false, + }, + }, + ]; +} + export function createTableAnalysisTools(): AIToolDefinition[] { return [ { diff --git a/backend/src/entities/visualizations/panel-position/data-structures/generate-table-dashboard-with-ai.ds.ts b/backend/src/entities/visualizations/panel-position/data-structures/generate-table-dashboard-with-ai.ds.ts index f825f2d88..fd0e079fe 100644 --- a/backend/src/entities/visualizations/panel-position/data-structures/generate-table-dashboard-with-ai.ds.ts +++ b/backend/src/entities/visualizations/panel-position/data-structures/generate-table-dashboard-with-ai.ds.ts @@ -2,7 +2,6 @@ export class GenerateTableDashboardWithAiDs { connectionId: string; masterPassword: string; userId: string; - table_name: string; max_panels?: number; dashboard_name?: string; } 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 6b111c29f..03c95fd90 100644 --- a/backend/src/entities/visualizations/panel-position/panel-position.controller.ts +++ b/backend/src/entities/visualizations/panel-position/panel-position.controller.ts @@ -193,7 +193,7 @@ export class DashboardWidgetController { @ApiOperation({ summary: 'Generate a full table dashboard using AI', description: - 'Analyzes a table structure and auto-generates multiple panel configurations for a dashboard using AI', + 'AI autonomously discovers database tables, analyzes their structure, and auto-generates multiple panel configurations for a dashboard', }) @ApiResponse({ status: 201, @@ -201,13 +201,11 @@ export class DashboardWidgetController { }) @ApiBody({ type: GenerateTableDashboardWithAiDto }) @ApiParam({ name: 'connectionId', required: true }) - @ApiQuery({ name: 'tableName', required: true, description: 'The table name to generate the dashboard for' }) @UseGuards(ConnectionEditGuard) @Timeout(TimeoutDefaults.AI) @Post('/dashboard/generate-table-dashboard/:connectionId') async generateTableDashboardWithAi( @SlugUuid('connectionId') connectionId: string, - @Query('tableName') tableName: string, @MasterPassword() masterPwd: string, @UserId() userId: string, @Body() generateDto: GenerateTableDashboardWithAiDto, @@ -216,10 +214,9 @@ export class DashboardWidgetController { connectionId, masterPassword: masterPwd, userId, - table_name: tableName, max_panels: generateDto.max_panels, dashboard_name: generateDto.dashboard_name, }; - return await this.generateTableDashboardWithAiUseCase.execute(inputData, InTransactionEnum.ON); + return await this.generateTableDashboardWithAiUseCase.execute(inputData, InTransactionEnum.OFF); } } diff --git a/backend/src/entities/visualizations/panel-position/use-cases/generate-table-dashboard-with-ai.use.case.ts b/backend/src/entities/visualizations/panel-position/use-cases/generate-table-dashboard-with-ai.use.case.ts index 332fc91e4..b25ff7e02 100644 --- a/backend/src/entities/visualizations/panel-position/use-cases/generate-table-dashboard-with-ai.use.case.ts +++ b/backend/src/entities/visualizations/panel-position/use-cases/generate-table-dashboard-with-ai.use.case.ts @@ -8,7 +8,17 @@ import { IGlobalDatabaseContext } from '../../../../common/application/global-da 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 { + AICoreService, + AIProviderType, + AIToolCall, + AIToolDefinition, + cleanAIJsonResponse, + createDashboardGenerationTools, + encodeToToon, + encodeError, + MessageBuilder, +} from '../../../../ai-core/index.js'; import { GenerateTableDashboardWithAiDs } from '../data-structures/generate-table-dashboard-with-ai.ds.js'; import { GeneratedPanelWithPositionDto } from '../dto/generated-panel-with-position.dto.js'; import { IGenerateTableDashboardWithAi } from './panel-position-use-cases.interface.js'; @@ -16,6 +26,7 @@ import { validateQuerySafety } from '../../panel/utils/check-query-is-safe.util. import { DashboardEntity } from '../../dashboard/dashboard.entity.js'; import { PanelEntity } from '../../panel/panel.entity.js'; import { PanelPositionEntity } from '../panel-position.entity.js'; +import { isConnectionTypeAgent } from '../../../../helpers/is-connection-entity-agent.js'; interface AIGeneratedPanelResponse { name: string; @@ -41,19 +52,13 @@ interface AIGeneratedPanelResponse { }; } -interface AISuggestedChart { - chart_description: string; - suggested_panel_type: string; - suggested_chart_type?: string; -} - -interface AIDashboardSuggestion { +interface AIDashboardResponse { dashboard_name: string; dashboard_description: string; - charts: AISuggestedChart[]; + panels: AIGeneratedPanelResponse[]; } -const MAX_FEEDBACK_ITERATIONS = 3; +const MAX_TOOL_ITERATIONS = 10; const DEFAULT_MAX_PANELS = 6; const PANEL_WIDTH = 6; const PANEL_HEIGHT = 4; @@ -68,10 +73,7 @@ const EXPLAIN_SUPPORTED_TYPES: ReadonlySet = new Set([ ConnectionTypesEnum.agent_clickhouse, ]); -interface TableInfo { - table_name: string; - columns: Array<{ name: string; type: string; nullable: boolean }>; -} +const MAX_FEEDBACK_ITERATIONS = 3; @Injectable({ scope: Scope.REQUEST }) export class GenerateTableDashboardWithAiUseCase @@ -89,7 +91,7 @@ export class GenerateTableDashboardWithAiUseCase } public async implementation(inputData: GenerateTableDashboardWithAiDs): Promise<{ success: boolean }> { - const { connectionId, masterPassword, table_name, max_panels, dashboard_name } = inputData; + const { connectionId, masterPassword, userId, max_panels, dashboard_name } = inputData; const maxPanels = max_panels ?? DEFAULT_MAX_PANELS; @@ -104,64 +106,81 @@ export class GenerateTableDashboardWithAiUseCase const dao = getDataAccessObject(foundConnection); - let tableInfo: TableInfo; - - 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.`); + let userEmail: string; + if (isConnectionTypeAgent(foundConnection.type)) { + userEmail = await this._dbContext.userRepository.getUserEmailOrReturnNull(userId); } - const suggestionPrompt = this.buildDashboardSuggestionPrompt(tableInfo, foundConnection.type, maxPanels); + const tools = createDashboardGenerationTools(); - const suggestionResponse = await this.aiCoreService.completeWithProvider(AIProviderType.BEDROCK, suggestionPrompt, { - temperature: 0.4, - }); + const systemPrompt = this.buildDashboardSystemPrompt(foundConnection.type as ConnectionTypesEnum, maxPanels); - const dashboardSuggestion = this.parseDashboardSuggestion(suggestionResponse); + const messages = new MessageBuilder() + .system(systemPrompt) + .human( + `Analyze the database and generate a dashboard with up to ${maxPanels} panels. ` + + `Start by listing the available tables, then inspect the ones that look most interesting for analytics. ` + + `Generate diverse and meaningful visualizations.`, + ) + .build(); - const effectiveDashboardName = - dashboard_name || dashboardSuggestion.dashboard_name || `${table_name} Dashboard`; + const dashboardResponse = await this.runToolLoop(messages, tools, dao, userEmail); - const panelPromises = dashboardSuggestion.charts.slice(0, maxPanels).map((chart, index) => - this.generateSinglePanel(chart, tableInfo, dao, foundConnection.type as ConnectionTypesEnum, connectionId, index), - ); + const parsedDashboard = this.parseDashboardResponse(dashboardResponse); - const results = await Promise.allSettled(panelPromises); + const effectiveDashboardName = dashboard_name || parsedDashboard.dashboard_name || 'AI Generated Dashboard'; - const panels: GeneratedPanelWithPositionDto[] = []; + const validPanels: GeneratedPanelWithPositionDto[] = []; - for (const result of results) { - if (result.status === 'fulfilled') { - panels.push(result.value); - } else { - this.logger.warn(`Panel generation failed: ${result.reason?.message || result.reason}`); + for (let i = 0; i < parsedDashboard.panels.length && validPanels.length < maxPanels; i++) { + const panel = parsedDashboard.panels[i]; + try { + validateQuerySafety(panel.query_text, foundConnection.type as ConnectionTypesEnum); + + const refinedPanel = await this.validateAndRefineQueryWithExplain( + dao, + panel, + foundConnection.type as ConnectionTypesEnum, + ); + + const index = validPanels.length; + const row = Math.floor(index / PANELS_PER_ROW); + const col = index % PANELS_PER_ROW; + + validPanels.push({ + name: refinedPanel.name, + description: refinedPanel.description || null, + panel_type: this.mapPanelType(refinedPanel.panel_type), + chart_type: refinedPanel.chart_type || null, + panel_options: refinedPanel.panel_options + ? (refinedPanel.panel_options as unknown as Record) + : null, + query_text: refinedPanel.query_text, + connection_id: connectionId, + panel_position: { + position_x: col * PANEL_WIDTH, + position_y: row * PANEL_HEIGHT, + width: PANEL_WIDTH, + height: PANEL_HEIGHT, + dashboard_id: null, + }, + }); + } catch (error) { + this.logger.warn(`Panel "${panel.name}" skipped: ${error.message}`); } } - if (panels.length === 0) { - throw new BadRequestException('Failed to generate any panels for the table. Please try again.'); + if (validPanels.length === 0) { + throw new BadRequestException('Failed to generate any valid panels. Please try again.'); } const dashboardEntity = new DashboardEntity(); dashboardEntity.name = effectiveDashboardName; - dashboardEntity.description = dashboardSuggestion.dashboard_description || null; + dashboardEntity.description = parsedDashboard.dashboard_description || null; dashboardEntity.connection_id = connectionId; const savedDashboard = await this._dbContext.dashboardRepository.saveDashboard(dashboardEntity); - for (const panel of panels) { + for (const panel of validPanels) { const panelEntity = new PanelEntity(); panelEntity.name = panel.name; panelEntity.description = panel.description || null; @@ -185,202 +204,210 @@ export class GenerateTableDashboardWithAiUseCase return { success: true }; } - private buildDashboardSuggestionPrompt(tableInfo: TableInfo, databaseType: string, maxPanels: number): 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. Analyze the following table schema and suggest ${maxPanels} useful chart/panel visualizations that would provide meaningful insights. + private buildDashboardSystemPrompt(databaseType: ConnectionTypesEnum, maxPanels: number): string { + return `You are a database analytics assistant that generates dashboards. You have access to two tools: getTablesList and getTableStructure. You do NOT have the ability to execute queries. DATABASE TYPE: ${databaseType} -DATABASE SCHEMA: -${schemaDescription} +YOUR WORKFLOW: +1. Call getTablesList to see all available tables +2. Call getTableStructure for tables that look most interesting for analytics +3. Based on the table schemas, generate a complete dashboard with up to ${maxPanels} panels -Suggest diverse visualizations based on the column types: -- Numeric columns: counters (totals, averages), bar/line charts for distributions -- Date/timestamp columns: line charts for trends over time -- String/categorical columns: pie/doughnut charts for category distributions, bar charts for top N -- Boolean columns: pie charts for true/false distributions -- Combinations: group numeric data by categories or time periods - -Generate a JSON response with the following structure: +CRITICAL: Your final response MUST be a raw JSON object only — no explanations, no markdown, no code fences, no text before or after. Just the JSON object in this format: { "dashboard_name": "Descriptive name for the dashboard (max 100 chars)", "dashboard_description": "Brief description of what this dashboard shows", - "charts": [ + "panels": [ { - "chart_description": "Detailed natural language description of what the chart should show, including specific columns, aggregations, and groupings to use", - "suggested_panel_type": "chart" | "counter" | "table", - "suggested_chart_type": "bar" | "line" | "pie" | "doughnut" | "polarArea" + "name": "Short descriptive name for the panel (max 50 chars)", + "description": "Brief description of what the panel shows", + "query_text": "SELECT query that returns data for the visualization", + "panel_type": "chart" | "table" | "counter" | "text", + "chart_type": "bar" | "line" | "pie" | "doughnut" | "polarArea", + "panel_options": { + "label_column": "column name for labels/categories (x-axis)", + "value_column": "column name for values (y-axis) - use 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. Suggest exactly ${maxPanels} charts -2. Each chart_description should be specific enough to generate a SQL query -3. Include a mix of panel types (counters, charts, tables) for variety -4. Reference actual column names from the schema -5. Consider which visualizations are most meaningful for the data types present -6. Avoid duplicate or overly similar visualizations - -Respond ONLY with the JSON object, no additional text or explanation.`; +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. Include a mix of panel types (counters, charts, tables) for variety +7. Generate exactly ${maxPanels} panels +8. You may use multiple tables in queries (JOINs) if they are related +9. Your final response must start with { and end with } — no text, no markdown, no explanation`; } - private parseDashboardSuggestion(aiResponse: string): AIDashboardSuggestion { - try { - const cleanedResponse = cleanAIJsonResponse(aiResponse); - const parsed = JSON.parse(cleanedResponse) as AIDashboardSuggestion; + private async runToolLoop( + messages: ReturnType, + tools: AIToolDefinition[], + dao: IDataAccessObject | IDataAccessObjectAgent, + userEmail: string, + ): Promise { + let currentMessages = [...messages]; + let depth = 0; + + while (depth < MAX_TOOL_ITERATIONS) { + const stream = await this.aiCoreService.streamChatWithToolsAndProvider( + AIProviderType.BEDROCK, + currentMessages, + tools, + { temperature: 0.4 }, + ); - if (!parsed.charts || !Array.isArray(parsed.charts) || parsed.charts.length === 0) { - throw new Error('Missing or empty charts array in AI response'); + let accumulatedContent = ''; + const pendingToolCalls: AIToolCall[] = []; + + for await (const chunk of stream) { + if (chunk.type === 'text' && chunk.content) { + accumulatedContent += chunk.content; + } + if (chunk.type === 'tool_call' && chunk.toolCall) { + pendingToolCalls.push(chunk.toolCall); + } } - return parsed; - } catch (error) { - this.logger.error('Error parsing dashboard suggestion AI response:', error.message); - throw new BadRequestException( - 'Failed to generate dashboard suggestions from AI. Please try again.', + this.logger.log( + `Tool loop iteration ${depth + 1}: toolCalls=${pendingToolCalls.map((tc) => tc.name).join(', ') || 'none'}, ` + + `contentLength=${accumulatedContent.length}`, ); - } - } - - private async generateSinglePanel( - chart: AISuggestedChart, - tableInfo: TableInfo, - dao: IDataAccessObject | IDataAccessObjectAgent, - connectionType: ConnectionTypesEnum, - connectionId: string, - index: number, - ): Promise { - const prompt = this.buildPanelPrompt(chart.chart_description, tableInfo, connectionType); - const aiResponse = await this.aiCoreService.completeWithProvider(AIProviderType.BEDROCK, prompt, { - temperature: 0.3, - }); + if (pendingToolCalls.length === 0) { + return accumulatedContent; + } - const generatedPanel = this.parseAIResponse(aiResponse); + const toolResults = await this.executeToolCalls(pendingToolCalls, dao, userEmail); - validateQuerySafety(generatedPanel.query_text, connectionType); + const continuationBuilder = MessageBuilder.fromMessages(currentMessages); + continuationBuilder.ai(accumulatedContent, pendingToolCalls); + for (const tr of toolResults) { + continuationBuilder.toolResult(tr.toolCallId, tr.result); + } + currentMessages = continuationBuilder.build(); - const refinedPanel = await this.validateAndRefineQueryWithExplain( - dao, - generatedPanel, - tableInfo, - connectionType, - chart.chart_description, - ); + depth++; + } - const row = Math.floor(index / PANELS_PER_ROW); - const col = index % PANELS_PER_ROW; - - return { - name: refinedPanel.name, - description: refinedPanel.description || null, - panel_type: this.mapPanelType(refinedPanel.panel_type), - chart_type: refinedPanel.chart_type || null, - panel_options: refinedPanel.panel_options - ? (refinedPanel.panel_options as unknown as Record) - : null, - query_text: refinedPanel.query_text, - connection_id: connectionId, - panel_position: { - position_x: col * PANEL_WIDTH, - position_y: row * PANEL_HEIGHT, - width: PANEL_WIDTH, - height: PANEL_HEIGHT, - dashboard_id: null, - }, - }; + throw new BadRequestException('AI tool loop exceeded maximum iterations. Please try again.'); } - private buildPanelPrompt( - chartDescription: string, - tableInfo: TableInfo, - databaseType: string | ConnectionTypesEnum, - ): string { - const schemaDescription = `Table: ${tableInfo.table_name}\n Columns:\n${tableInfo.columns.map((col) => ` - ${col.name}: ${col.type}${col.nullable ? ' (nullable)' : ''}`).join('\n')}`; + private async executeToolCalls( + toolCalls: AIToolCall[], + dao: IDataAccessObject | IDataAccessObjectAgent, + userEmail: string, + ): Promise> { + const results: Array<{ toolCallId: string; result: string }> = []; - 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. + for (const toolCall of toolCalls) { + let result: string; -DATABASE TYPE: ${databaseType} + try { + switch (toolCall.name) { + case 'getTablesList': { + const tables = await dao.getTablesFromDB(userEmail); + result = encodeToToon(tables); + break; + } + + case 'getTableStructure': { + const tableName = toolCall.arguments.tableName as string; + if (!tableName) { + throw new Error('Missing required argument "tableName"'); + } + const structure = await dao.getTableStructure(tableName, userEmail); + result = encodeToToon({ + tableName, + columns: structure.map((col) => ({ + name: col.column_name, + type: col.data_type, + nullable: col.allow_null, + })), + }); + break; + } + + default: + result = encodeError({ error: `Unknown tool: ${toolCall.name}` }); + } + } catch (error) { + result = encodeError({ error: error.message }); + } -DATABASE SCHEMA: -${schemaDescription} + results.push({ toolCallId: toolCall.id, result }); + } -USER'S CHART DESCRIPTION: -"${chartDescription}" + return results; + } -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}'.", - "panel_type": "chart" | "table" | "counter" | "text", - "chart_type": "bar" | "line" | "pie" | "doughnut" | "polarArea", - "panel_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" - } - } -} + private extractJsonFromResponse(response: string): string { + const jsonBlockMatch = response.match(/```(?:json)?\s*\n?([\s\S]*?)```/); + if (jsonBlockMatch) { + return jsonBlockMatch[1].trim(); + } -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 panel_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 + const firstBrace = response.indexOf('{'); + if (firstBrace !== -1) { + return response.slice(firstBrace); + } -Respond ONLY with the JSON object, no additional text or explanation.`; + return response; } - private parseAIResponse(aiResponse: string): AIGeneratedPanelResponse { + private parseDashboardResponse(aiResponse: string): AIDashboardResponse { try { - const cleanedResponse = cleanAIJsonResponse(aiResponse); - const parsed = JSON.parse(cleanedResponse) as AIGeneratedPanelResponse; + const extracted = this.extractJsonFromResponse(aiResponse); + const cleanedResponse = cleanAIJsonResponse(extracted); + const parsed = JSON.parse(cleanedResponse) as AIDashboardResponse; - if (!parsed.name || !parsed.query_text || !parsed.panel_type) { - throw new Error('Missing required fields in AI response'); + if (!parsed.panels || !Array.isArray(parsed.panels) || parsed.panels.length === 0) { + throw new Error('Missing or empty panels array in AI response'); + } + + for (const panel of parsed.panels) { + if (!panel.name || !panel.query_text || !panel.panel_type) { + throw new Error('Panel missing required fields (name, query_text, panel_type)'); + } } return parsed; } catch (error) { - this.logger.error('Error parsing AI response:', error.message); - throw new BadRequestException( - 'Failed to generate panel configuration from AI. Please try again with a different description.', - ); + this.logger.error('Error parsing dashboard AI response:', error.message); + throw new BadRequestException('Failed to generate dashboard from AI. Please try again.'); } } private async validateAndRefineQueryWithExplain( dao: IDataAccessObject | IDataAccessObjectAgent, generatedPanel: AIGeneratedPanelResponse, - tableInfo: TableInfo, connectionType: ConnectionTypesEnum, - chartDescription: string, ): Promise { if (!EXPLAIN_SUPPORTED_TYPES.has(connectionType)) { return generatedPanel; @@ -389,15 +416,14 @@ Respond ONLY with the JSON object, no additional text or explanation.`; let currentQuery = generatedPanel.query_text; for (let iteration = 0; iteration < MAX_FEEDBACK_ITERATIONS; iteration++) { - const explainResult = await this.runExplainQuery(dao, currentQuery, tableInfo.table_name); + const explainResult = await this.runExplainQuery(dao, currentQuery); const correctionPrompt = this.buildQueryCorrectionPrompt( currentQuery, explainResult.success ? explainResult.result : explainResult.error, !explainResult.success, - tableInfo, connectionType, - chartDescription, + generatedPanel.name, ); const aiResponse = await this.aiCoreService.completeWithProvider(AIProviderType.BEDROCK, correctionPrompt, { @@ -427,11 +453,10 @@ Respond ONLY with the JSON object, no additional text or explanation.`; private async runExplainQuery( dao: IDataAccessObject | IDataAccessObjectAgent, query: string, - tableName: string, ): Promise<{ success: boolean; result?: string; error?: string }> { try { const explainQuery = `EXPLAIN ${query.replace(/;\s*$/, '')}`; - const result = await (dao as IDataAccessObject).executeRawQuery(explainQuery, tableName); + const result = await (dao as IDataAccessObject).executeRawQuery(explainQuery, ''); return { success: true, result: JSON.stringify(result, null, 2) }; } catch (error) { return { success: false, error: error.message }; @@ -442,14 +467,9 @@ Respond ONLY with the JSON object, no additional text or explanation.`; currentQuery: string, explainResultOrError: string, isError: boolean, - tableInfo: TableInfo, connectionType: ConnectionTypesEnum, - chartDescription: string, + panelName: string, ): string { - const schemaDescription = `Table: ${tableInfo.table_name}\n Columns:\n${tableInfo.columns - .map((col) => ` - ${col.name}: ${col.type}${col.nullable ? ' (nullable)' : ''}`) - .join('\n')}`; - const feedbackSection = isError ? `The query FAILED with the following error:\n${explainResultOrError}\n\nPlease fix the query to resolve this error.` : `The EXPLAIN output for the query is:\n${explainResultOrError}\n\nReview the execution plan. If the query has performance issues (full table scans on large datasets, inefficient joins, etc.), optimize it. If the query is already acceptable, return it unchanged.`; @@ -458,11 +478,7 @@ Respond ONLY with the JSON object, no additional text or explanation.`; DATABASE TYPE: ${connectionType} -DATABASE SCHEMA: -${schemaDescription} - -ORIGINAL USER REQUEST: -"${chartDescription}" +PANEL NAME: "${panelName}" CURRENT QUERY: ${currentQuery} diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-dashboard-ai-generate-table-dashboard-e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-dashboard-ai-generate-table-dashboard-e2e.test.ts index 87eb12883..a7ce2086d 100644 --- a/backend/test/ava-tests/non-saas-tests/non-saas-dashboard-ai-generate-table-dashboard-e2e.test.ts +++ b/backend/test/ava-tests/non-saas-tests/non-saas-dashboard-ai-generate-table-dashboard-e2e.test.ts @@ -29,69 +29,106 @@ let app: INestApplication; let testUtils: TestUtils; let currentTest: string; -const MOCK_DASHBOARD_SUGGESTION = JSON.stringify({ +const MOCK_DASHBOARD_RESPONSE = JSON.stringify({ dashboard_name: 'Test Analytics Dashboard', dashboard_description: 'Automated dashboard for test table analysis', - charts: [ + panels: [ { - chart_description: 'Show total count of records', - suggested_panel_type: 'counter', + name: 'Record Count', + description: 'Total number of records', + query_text: 'SELECT COUNT(*) as total FROM test_table', + panel_type: 'counter', + panel_options: { + value_column: 'total', + }, }, { - chart_description: 'Show distribution of records by name', - suggested_panel_type: 'chart', - suggested_chart_type: 'bar', + name: 'Records by Name', + description: 'Distribution of records by name', + query_text: "SELECT name as label, COUNT(*) as count FROM test_table GROUP BY name", + panel_type: 'chart', + chart_type: 'bar', + panel_options: { + label_column: 'label', + value_column: 'count', + }, }, ], }); -const MOCK_PANEL_RESPONSE = JSON.stringify({ - name: 'Record Count', - description: 'Total number of records', - query_text: 'SELECT COUNT(*) as total FROM test_table', - panel_type: 'counter', - panel_options: { - value_column: 'total', - }, -}); - -const MOCK_PANEL_RESPONSE_UNSAFE = JSON.stringify({ - name: 'Drop Table', - description: 'Unsafe query', - query_text: 'DROP TABLE test_table', - panel_type: 'table', +const MOCK_DASHBOARD_RESPONSE_UNSAFE = JSON.stringify({ + dashboard_name: 'Unsafe Dashboard', + dashboard_description: 'Dashboard with unsafe queries', + panels: [ + { + name: 'Drop Table', + description: 'Unsafe query', + query_text: 'DROP TABLE test_table', + panel_type: 'table', + }, + { + name: 'Delete All', + description: 'Another unsafe query', + query_text: 'DELETE FROM test_table', + panel_type: 'counter', + }, + ], }); -let mockDashboardSuggestion = MOCK_DASHBOARD_SUGGESTION; -let mockPanelResponse = MOCK_PANEL_RESPONSE; +let mockDashboardResponse = MOCK_DASHBOARD_RESPONSE; +let toolCallCounter = 0; const mockAICoreService = { - completeWithProvider: async (_provider: string, prompt: string) => { - if (prompt.includes('chart/panel visualizations')) { - return mockDashboardSuggestion; + streamChatWithToolsAndProvider: async () => { + toolCallCounter++; + // First call: AI requests getTablesList + if (toolCallCounter === 1) { + return { + *[Symbol.asyncIterator]() { + yield { type: 'tool_call', toolCall: { id: 'tc_1', name: 'getTablesList', arguments: {} } }; + yield { type: 'done' }; + }, + }; } + // Second call: AI requests getTableStructure + if (toolCallCounter === 2) { + return { + *[Symbol.asyncIterator]() { + yield { + type: 'tool_call', + toolCall: { id: 'tc_2', name: 'getTableStructure', arguments: { tableName: 'test_table' } }, + }; + yield { type: 'done' }; + }, + }; + } + // Third call: AI returns final dashboard JSON + return { + *[Symbol.asyncIterator]() { + yield { type: 'text', content: mockDashboardResponse }; + yield { type: 'done' }; + }, + }; + }, + completeWithProvider: async (_provider: string, prompt: string) => { if (prompt.includes('query optimization assistant')) { const match = prompt.match(/CURRENT QUERY:\n([\s\S]*?)\n\n/); return match ? match[1].trim() : 'SELECT 1'; } - return mockPanelResponse; + return 'SELECT 1'; }, - complete: async () => mockPanelResponse, - chat: async () => ({ content: mockPanelResponse, responseId: faker.string.uuid() }), + complete: async () => 'SELECT 1', + chat: async () => ({ content: '{}', responseId: faker.string.uuid() }), + chatWithToolsAndProvider: async () => ({ content: '{}', toolCalls: [] }), streamChat: async () => ({ *[Symbol.asyncIterator]() { - yield { type: 'text', content: mockPanelResponse, responseId: faker.string.uuid() }; + yield { type: 'text', content: '{}', responseId: faker.string.uuid() }; }, }), - chatWithTools: async () => ({ content: mockPanelResponse, responseId: faker.string.uuid() }), + chatWithTools: async () => ({ content: '{}', responseId: faker.string.uuid() }), streamChatWithTools: async () => ({ *[Symbol.asyncIterator]() { - yield { type: 'text', content: mockPanelResponse, responseId: faker.string.uuid() }; - }, - }), - streamChatWithToolsAndProvider: async () => ({ - *[Symbol.asyncIterator]() { - yield { type: 'text', content: mockPanelResponse, responseId: faker.string.uuid() }; + yield { type: 'text', content: '{}', responseId: faker.string.uuid() }; }, }), getDefaultProvider: () => 'bedrock', @@ -138,8 +175,8 @@ test.after(async () => { currentTest = 'POST /dashboard/generate-table-dashboard/:connectionId'; test.serial(`${currentTest} should generate and persist a table dashboard with AI`, async (t) => { - mockDashboardSuggestion = MOCK_DASHBOARD_SUGGESTION; - mockPanelResponse = MOCK_PANEL_RESPONSE; + mockDashboardResponse = MOCK_DASHBOARD_RESPONSE; + toolCallCounter = 0; const connectionToTestDB = getTestData(mockFactory).connectionToPostgres; const { token } = await registerUserAndReturnUserInfo(app); @@ -155,7 +192,7 @@ test.serial(`${currentTest} should generate and persist a table dashboard with A t.is(createConnectionResponse.status, 201); const generateDashboard = await request(app.getHttpServer()) - .post(`/dashboard/generate-table-dashboard/${connectionId}?tableName=${testTableName}`) + .post(`/dashboard/generate-table-dashboard/${connectionId}`) .send({}) .set('Cookie', token) .set('masterpwd', 'ahalaimahalai') @@ -163,6 +200,7 @@ test.serial(`${currentTest} should generate and persist a table dashboard with A .set('Accept', 'application/json'); const generateDashboardRO = JSON.parse(generateDashboard.text); + console.log('🚀 ~ generateDashboardRO:', generateDashboardRO) t.is(generateDashboard.status, 201); t.deepEqual(generateDashboardRO, { success: true }); @@ -208,7 +246,7 @@ test.serial(`${currentTest} should generate and persist a table dashboard with A t.true(queries.length >= 2); const generatedPanels = queries.filter((q: any) => q.name === 'Record Count'); - t.is(generatedPanels.length, 2); + t.is(generatedPanels.length, 1); for (const panel of generatedPanels) { t.truthy(panel.id); t.is(panel.connection_id, connectionId); @@ -217,8 +255,8 @@ test.serial(`${currentTest} should generate and persist a table dashboard with A }); test.serial(`${currentTest} should use custom dashboard name when provided`, async (t) => { - mockDashboardSuggestion = MOCK_DASHBOARD_SUGGESTION; - mockPanelResponse = MOCK_PANEL_RESPONSE; + mockDashboardResponse = MOCK_DASHBOARD_RESPONSE; + toolCallCounter = 0; const connectionToTestDB = getTestData(mockFactory).connectionToPostgres; const { token } = await registerUserAndReturnUserInfo(app); @@ -236,12 +274,13 @@ test.serial(`${currentTest} should use custom dashboard name when provided`, asy const customDashboardName = 'My Custom AI Dashboard'; const generateDashboard = await request(app.getHttpServer()) - .post(`/dashboard/generate-table-dashboard/${connectionId}?tableName=${testTableName}`) + .post(`/dashboard/generate-table-dashboard/${connectionId}`) .send({ dashboard_name: customDashboardName }) .set('Cookie', token) .set('masterpwd', 'ahalaimahalai') .set('Content-Type', 'application/json') .set('Accept', 'application/json'); + console.log('🚀 ~ generateDashboard:', generateDashboard.text) t.is(generateDashboard.status, 201); t.deepEqual(JSON.parse(generateDashboard.text), { success: true }); @@ -260,8 +299,8 @@ test.serial(`${currentTest} should use custom dashboard name when provided`, asy }); test.serial(`${currentTest} should reject when all AI panels have unsafe queries`, async (t) => { - mockDashboardSuggestion = MOCK_DASHBOARD_SUGGESTION; - mockPanelResponse = MOCK_PANEL_RESPONSE_UNSAFE; + mockDashboardResponse = MOCK_DASHBOARD_RESPONSE_UNSAFE; + toolCallCounter = 0; const connectionToTestDB = getTestData(mockFactory).connectionToPostgres; const { token } = await registerUserAndReturnUserInfo(app); @@ -277,7 +316,7 @@ test.serial(`${currentTest} should reject when all AI panels have unsafe queries t.is(createConnectionResponse.status, 201); const generateDashboard = await request(app.getHttpServer()) - .post(`/dashboard/generate-table-dashboard/${connectionId}?tableName=${testTableName}`) + .post(`/dashboard/generate-table-dashboard/${connectionId}`) .send({}) .set('Cookie', token) .set('masterpwd', 'ahalaimahalai') @@ -288,57 +327,3 @@ test.serial(`${currentTest} should reject when all AI panels have unsafe queries const errorResponse = JSON.parse(generateDashboard.text); t.truthy(errorResponse.message); }); - -test.serial(`${currentTest} should fail for non-existent table`, async (t) => { - mockDashboardSuggestion = MOCK_DASHBOARD_SUGGESTION; - mockPanelResponse = MOCK_PANEL_RESPONSE; - - 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; - t.is(createConnectionResponse.status, 201); - - const generateDashboard = await request(app.getHttpServer()) - .post(`/dashboard/generate-table-dashboard/${connectionId}?tableName=non_existent_table_xyz`) - .send({}) - .set('Cookie', token) - .set('masterpwd', 'ahalaimahalai') - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - t.is(generateDashboard.status, 400); -}); - -test.serial(`${currentTest} should fail without tableName query parameter`, async (t) => { - mockDashboardSuggestion = MOCK_DASHBOARD_SUGGESTION; - mockPanelResponse = MOCK_PANEL_RESPONSE; - - 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; - t.is(createConnectionResponse.status, 201); - - const generateDashboard = await request(app.getHttpServer()) - .post(`/dashboard/generate-table-dashboard/${connectionId}`) - .send({}) - .set('Cookie', token) - .set('masterpwd', 'ahalaimahalai') - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - t.is(generateDashboard.status, 400); -}); diff --git a/backend/test/ava-tests/saas-tests/dashboard-ai-generate-table-dashboard-e2e.test.ts b/backend/test/ava-tests/saas-tests/dashboard-ai-generate-table-dashboard-e2e.test.ts index 8ed3c04a9..da0b15bc1 100644 --- a/backend/test/ava-tests/saas-tests/dashboard-ai-generate-table-dashboard-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/dashboard-ai-generate-table-dashboard-e2e.test.ts @@ -25,69 +25,115 @@ let app: INestApplication; let testUtils: TestUtils; let currentTest: string; -const MOCK_DASHBOARD_SUGGESTION = JSON.stringify({ +const MOCK_DASHBOARD_RESPONSE = JSON.stringify({ dashboard_name: 'Test Analytics Dashboard', dashboard_description: 'Automated dashboard for test table analysis', - charts: [ + panels: [ { - chart_description: 'Show total count of records', - suggested_panel_type: 'counter', + name: 'Record Count', + description: 'Total number of records', + query_text: 'SELECT COUNT(*) as total FROM test_table', + panel_type: 'counter', + panel_options: { + value_column: 'total', + }, }, { - chart_description: 'Show distribution of records by name', - suggested_panel_type: 'chart', - suggested_chart_type: 'bar', + name: 'Records by Name', + description: 'Distribution of records by name', + query_text: "SELECT name as label, COUNT(*) as count FROM test_table GROUP BY name", + panel_type: 'chart', + chart_type: 'bar', + panel_options: { + label_column: 'label', + value_column: 'count', + }, }, ], }); -const MOCK_PANEL_RESPONSE = JSON.stringify({ - name: 'Record Count', - description: 'Total number of records', - query_text: 'SELECT COUNT(*) as total FROM test_table', - panel_type: 'counter', - panel_options: { - value_column: 'total', - }, +const MOCK_DASHBOARD_RESPONSE_UNSAFE = JSON.stringify({ + dashboard_name: 'Unsafe Dashboard', + dashboard_description: 'Dashboard with unsafe queries', + panels: [ + { + name: 'Drop Table', + description: 'Unsafe query', + query_text: 'DROP TABLE test_table', + panel_type: 'table', + }, + { + name: 'Delete All', + description: 'Another unsafe query', + query_text: 'DELETE FROM test_table', + panel_type: 'counter', + }, + ], }); -const MOCK_PANEL_RESPONSE_UNSAFE = JSON.stringify({ - name: 'Drop Table', - description: 'Unsafe query', - query_text: 'DROP TABLE test_table', - panel_type: 'table', -}); +const MOCK_TABLES_LIST = [ + { tableName: 'test_table', isView: false }, +]; -let mockDashboardSuggestion = MOCK_DASHBOARD_SUGGESTION; -let mockPanelResponse = MOCK_PANEL_RESPONSE; +const MOCK_TABLE_STRUCTURE = [ + { column_name: 'id', data_type: 'integer', allow_null: false }, + { column_name: 'name', data_type: 'varchar', allow_null: true }, +]; + +let mockDashboardResponse = MOCK_DASHBOARD_RESPONSE; +let toolCallCounter = 0; const mockAICoreService = { - completeWithProvider: async (_provider: string, prompt: string) => { - if (prompt.includes('chart/panel visualizations')) { - return mockDashboardSuggestion; + streamChatWithToolsAndProvider: async () => { + toolCallCounter++; + // First call: AI requests getTablesList + if (toolCallCounter === 1) { + return { + *[Symbol.asyncIterator]() { + yield { type: 'tool_call', toolCall: { id: 'tc_1', name: 'getTablesList', arguments: {} } }; + yield { type: 'done' }; + }, + }; } + // Second call: AI requests getTableStructure + if (toolCallCounter === 2) { + return { + *[Symbol.asyncIterator]() { + yield { + type: 'tool_call', + toolCall: { id: 'tc_2', name: 'getTableStructure', arguments: { tableName: 'test_table' } }, + }; + yield { type: 'done' }; + }, + }; + } + // Third call: AI returns final dashboard JSON + return { + *[Symbol.asyncIterator]() { + yield { type: 'text', content: mockDashboardResponse }; + yield { type: 'done' }; + }, + }; + }, + completeWithProvider: async (_provider: string, prompt: string) => { if (prompt.includes('query optimization assistant')) { const match = prompt.match(/CURRENT QUERY:\n([\s\S]*?)\n\n/); return match ? match[1].trim() : 'SELECT 1'; } - return mockPanelResponse; + return 'SELECT 1'; }, - complete: async () => mockPanelResponse, - chat: async () => ({ content: mockPanelResponse, responseId: faker.string.uuid() }), + complete: async () => 'SELECT 1', + chat: async () => ({ content: '{}', responseId: faker.string.uuid() }), + chatWithToolsAndProvider: async () => ({ content: '{}', toolCalls: [] }), streamChat: async () => ({ *[Symbol.asyncIterator]() { - yield { type: 'text', content: mockPanelResponse, responseId: faker.string.uuid() }; + yield { type: 'text', content: '{}', responseId: faker.string.uuid() }; }, }), - chatWithTools: async () => ({ content: mockPanelResponse, responseId: faker.string.uuid() }), + chatWithTools: async () => ({ content: '{}', responseId: faker.string.uuid() }), streamChatWithTools: async () => ({ *[Symbol.asyncIterator]() { - yield { type: 'text', content: mockPanelResponse, responseId: faker.string.uuid() }; - }, - }), - streamChatWithToolsAndProvider: async () => ({ - *[Symbol.asyncIterator]() { - yield { type: 'text', content: mockPanelResponse, responseId: faker.string.uuid() }; + yield { type: 'text', content: '{}', responseId: faker.string.uuid() }; }, }), getDefaultProvider: () => 'bedrock', @@ -132,8 +178,8 @@ test.after(async () => { currentTest = 'POST /dashboard/generate-table-dashboard/:connectionId'; test.serial(`${currentTest} should generate and persist a table dashboard with AI`, async (t) => { - mockDashboardSuggestion = MOCK_DASHBOARD_SUGGESTION; - mockPanelResponse = MOCK_PANEL_RESPONSE; + mockDashboardResponse = MOCK_DASHBOARD_RESPONSE; + toolCallCounter = 0; const connectionToTestDB = getTestData(mockFactory).connectionToPostgres; const { token } = await registerUserAndReturnUserInfo(app); @@ -149,7 +195,7 @@ test.serial(`${currentTest} should generate and persist a table dashboard with A t.is(createConnectionResponse.status, 201); const generateDashboard = await request(app.getHttpServer()) - .post(`/dashboard/generate-table-dashboard/${connectionId}?tableName=${testTableName}`) + .post(`/dashboard/generate-table-dashboard/${connectionId}`) .send({}) .set('Cookie', token) .set('masterpwd', 'ahalaimahalai') @@ -202,7 +248,7 @@ test.serial(`${currentTest} should generate and persist a table dashboard with A t.true(queries.length >= 2); const generatedPanels = queries.filter((q: any) => q.name === 'Record Count'); - t.is(generatedPanels.length, 2); + t.is(generatedPanels.length, 1); for (const panel of generatedPanels) { t.truthy(panel.id); t.is(panel.connection_id, connectionId); @@ -211,8 +257,8 @@ test.serial(`${currentTest} should generate and persist a table dashboard with A }); test.serial(`${currentTest} should use custom dashboard name when provided`, async (t) => { - mockDashboardSuggestion = MOCK_DASHBOARD_SUGGESTION; - mockPanelResponse = MOCK_PANEL_RESPONSE; + mockDashboardResponse = MOCK_DASHBOARD_RESPONSE; + toolCallCounter = 0; const connectionToTestDB = getTestData(mockFactory).connectionToPostgres; const { token } = await registerUserAndReturnUserInfo(app); @@ -230,7 +276,7 @@ test.serial(`${currentTest} should use custom dashboard name when provided`, asy const customDashboardName = 'My Custom AI Dashboard'; const generateDashboard = await request(app.getHttpServer()) - .post(`/dashboard/generate-table-dashboard/${connectionId}?tableName=${testTableName}`) + .post(`/dashboard/generate-table-dashboard/${connectionId}`) .send({ dashboard_name: customDashboardName }) .set('Cookie', token) .set('masterpwd', 'ahalaimahalai') @@ -254,8 +300,8 @@ test.serial(`${currentTest} should use custom dashboard name when provided`, asy }); test.serial(`${currentTest} should reject when all AI panels have unsafe queries`, async (t) => { - mockDashboardSuggestion = MOCK_DASHBOARD_SUGGESTION; - mockPanelResponse = MOCK_PANEL_RESPONSE_UNSAFE; + mockDashboardResponse = MOCK_DASHBOARD_RESPONSE_UNSAFE; + toolCallCounter = 0; const connectionToTestDB = getTestData(mockFactory).connectionToPostgres; const { token } = await registerUserAndReturnUserInfo(app); @@ -271,7 +317,7 @@ test.serial(`${currentTest} should reject when all AI panels have unsafe queries t.is(createConnectionResponse.status, 201); const generateDashboard = await request(app.getHttpServer()) - .post(`/dashboard/generate-table-dashboard/${connectionId}?tableName=${testTableName}`) + .post(`/dashboard/generate-table-dashboard/${connectionId}`) .send({}) .set('Cookie', token) .set('masterpwd', 'ahalaimahalai') @@ -282,57 +328,3 @@ test.serial(`${currentTest} should reject when all AI panels have unsafe queries const errorResponse = JSON.parse(generateDashboard.text); t.truthy(errorResponse.message); }); - -test.serial(`${currentTest} should fail for non-existent table`, async (t) => { - mockDashboardSuggestion = MOCK_DASHBOARD_SUGGESTION; - mockPanelResponse = MOCK_PANEL_RESPONSE; - - 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; - t.is(createConnectionResponse.status, 201); - - const generateDashboard = await request(app.getHttpServer()) - .post(`/dashboard/generate-table-dashboard/${connectionId}?tableName=non_existent_table_xyz`) - .send({}) - .set('Cookie', token) - .set('masterpwd', 'ahalaimahalai') - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - t.is(generateDashboard.status, 400); -}); - -test.serial(`${currentTest} should fail without tableName query parameter`, async (t) => { - mockDashboardSuggestion = MOCK_DASHBOARD_SUGGESTION; - mockPanelResponse = MOCK_PANEL_RESPONSE; - - 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; - t.is(createConnectionResponse.status, 201); - - const generateDashboard = await request(app.getHttpServer()) - .post(`/dashboard/generate-table-dashboard/${connectionId}`) - .send({}) - .set('Cookie', token) - .set('masterpwd', 'ahalaimahalai') - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - t.is(generateDashboard.status, 400); -});