Skip to main content
Glama
redash-client.ts5.77 kB
/** * Redash API Client */ import type { RedashClientConfig, DataSource, Query, QueryResult, JobResponse, QueryExecutionRequest, QueryExecutionResponse, RedashApiError, } from './types'; export class RedashClient { private readonly config: Required<RedashClientConfig>; constructor(config: RedashClientConfig) { this.config = { apiKey: config.apiKey, baseUrl: config.baseUrl.replace(/\/$/, ''), // Remove trailing slash timeout: config.timeout ?? 30000, }; } /** * Create RedashClient from environment variables */ static fromEnv(): RedashClient { const apiKey = process.env.REDASH_API_KEY; const baseUrl = process.env.REDASH_BASE_URL; if (!apiKey || !baseUrl) { throw new Error('REDASH_API_KEY and REDASH_BASE_URL environment variables are required'); } const config: RedashClientConfig = { apiKey, baseUrl, }; if (process.env.REDASH_API_TIMEOUT) { config.timeout = parseInt(process.env.REDASH_API_TIMEOUT, 10); } return new RedashClient(config); } /** * Make API request */ private async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> { const url = `${this.config.baseUrl}${endpoint}`; const headers: Record<string, string> = { Authorization: `Key ${this.config.apiKey}`, ...(options.body ? { 'Content-Type': 'application/json' } : {}), ...(options.headers as Record<string, string>), }; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.config.timeout); try { const response = await fetch(url, { ...options, headers, signal: controller.signal, }); if (!response.ok) { let errorMessage = `API request failed: ${response.status} ${response.statusText}`; // Try to get response body for better error messages try { const contentType = response.headers.get('content-type'); if (contentType?.includes('application/json')) { const errorBody = await response.json(); errorMessage += ` - ${JSON.stringify(errorBody)}`; } else { const textBody = await response.text(); if (textBody) { errorMessage += ` - ${textBody}`; } } } catch { // If we can't read the body, just use the status message } const error: RedashApiError = { message: errorMessage, status: response.status, }; throw error; } return (await response.json()) as T; } catch (error) { if (error instanceof Error && error.name === 'AbortError') { throw new Error(`Request timeout after ${this.config.timeout}ms`); } throw error; } finally { clearTimeout(timeoutId); } } /** * List all data sources */ async listDataSources(): Promise<DataSource[]> { return this.request<DataSource[]>('/api/data_sources'); } /** * Get specific data source */ async getDataSource(id: number): Promise<DataSource> { return this.request<DataSource>(`/api/data_sources/${id}`); } /** * Execute a query */ async executeQuery(request: QueryExecutionRequest): Promise<QueryExecutionResponse> { return this.request<QueryExecutionResponse>('/api/query_results', { method: 'POST', body: JSON.stringify(request), }); } /** * Get job status */ async getJob(jobId: string): Promise<JobResponse> { return this.request<JobResponse>(`/api/jobs/${jobId}`); } /** * Get query result */ async getQueryResult(resultId: string): Promise<QueryResult> { const response = await this.request<{ query_result: QueryResult }>( `/api/query_results/${resultId}` ); return response.query_result; } /** * Execute query and wait for result */ async executeQueryAndWait( request: QueryExecutionRequest, pollInterval = 1000, maxAttempts = 60 ): Promise<QueryResult> { // Execute query const response = await this.executeQuery(request); // Check if we got a cached result directly if ('query_result' in response) { return (response as unknown as { query_result: QueryResult }).query_result; } // Otherwise, poll for job completion const { job } = response; if (!job) { throw new Error('Invalid response: neither job nor query_result found'); } // Poll for completion let attempts = 0; while (attempts < maxAttempts) { const { job: currentJob } = await this.getJob(job.id); // Status: 3 = success if (currentJob.status === 3) { if (!currentJob.query_result_id) { throw new Error('Query completed but no result ID found'); } return this.getQueryResult(currentJob.query_result_id); } // Status: 4 = failure if (currentJob.status === 4) { const error: RedashApiError = { message: currentJob.error ?? 'Query execution failed', job: currentJob, }; throw error; } // Wait before polling again await new Promise((resolve) => setTimeout(resolve, pollInterval)); attempts++; } throw new Error(`Query execution timeout after ${maxAttempts} attempts`); } /** * List queries */ async listQueries(page = 1, pageSize = 25): Promise<Query[]> { const response = await this.request<{ results: Query[] }>( `/api/queries?page=${page}&page_size=${pageSize}` ); return response.results; } /** * Get specific query */ async getQuery(id: number): Promise<Query> { return this.request<Query>(`/api/queries/${id}`); } }

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jasonsmithj/redash-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server