@channel.io/be-user-chat-mcp
Version:
Backend UserChat Model Context Protocol server for Channel.io
158 lines • 7.56 kB
JavaScript
import axios from "axios";
/**
* Executes a specific Redash query and returns the result rows.
*
* @template T The expected type of a single row in the result set.
* @param queryId The ID of the Redash query to execute.
* @param parameters Optional parameters for the Redash query (strings, numbers, booleans, null).
* @param context RequestContext containing Redash credentials.
* @returns An array of result rows, typed as T[].
* @throws If the API call fails or the context is not provided.
*/
export async function executeRedashQuery(queryId, parameters, context) {
const apiUrl = `${context.redashUrl}/api/queries/${queryId}/results`;
const headers = {
Authorization: `Key ${context.redashApiKey}`,
"Content-Type": "application/json",
};
try {
const response = await axios.post(apiUrl, { parameters: parameters, max_age: 0 }, { headers });
if (response.data.job) {
console.warn(`Redash returned a job ID (${response.data.job.id}). Polling for results...`);
const resultData = await pollRedashJob(response.data.job.id, context);
// Assuming the rows match the expected type T.
// Add runtime validation (e.g., with Zod) if necessary for robustness.
return resultData.rows;
}
else if (response.data.query_result) {
return response.data.query_result.data.rows;
}
else {
throw new Error("Unexpected response format from Redash API (no job or query_result)");
}
}
catch (error) {
handleRedashError(error, `query ${queryId}`);
}
}
/**
* Executes a raw SQL query directly via the Redash API on a specific data source.
* WARNING: SQL INJECTION RISK! Parameters must be safely handled *before* calling this function.
*
* @template T The expected type of a single row in the result set.
* @param dataSourceId The ID of the Redash Data Source to run the query against.
* @param sqlQuery The raw SQL query string to execute.
* @param context Optional context string for error logging.
* @param requestContext RequestContext containing Redash credentials.
* @returns An array of result rows, typed as T[].
* @throws If the API call fails or the context is not provided.
*/
export async function executeRawRedashQuery(dataSourceId, sqlQuery, context = "raw query", requestContext) {
if (typeof dataSourceId !== "number" || isNaN(dataSourceId)) {
throw new Error(`Invalid dataSourceId provided: ${dataSourceId}. Must be a number.`);
}
// Basic check for obviously unsafe query patterns (example only, not exhaustive)
if (sqlQuery.match(/;|--|\/\*|\*\//)) {
console.error(`Potential unsafe characters detected in raw SQL query (context: ${context}): ${sqlQuery}`);
// Depending on policy, might throw an error here
// throw new Error("Unsafe characters detected in raw SQL query.");
}
const apiUrl = `${requestContext.redashUrl}/api/query_results`;
const headers = {
Authorization: `Key ${requestContext.redashApiKey}`,
"Content-Type": "application/json",
};
try {
const response = await axios.post(apiUrl, {
data_source_id: dataSourceId,
query: sqlQuery,
max_age: 0,
}, { headers });
if (response.data.job) {
console.warn(`Redash returned a job ID (${response.data.job.id}) for raw query. Polling for results...`);
const resultData = await pollRedashJob(response.data.job.id, requestContext);
return resultData.rows;
}
else if (response.data.query_result) {
console.warn(`Redash returned direct results for raw query (unexpected).`);
return response.data.query_result.data.rows;
}
else {
throw new Error("Unexpected response format from Redash API (no job or query_result)");
}
}
catch (error) {
handleRedashError(error, context);
}
}
// Helper function for polling Redash job status
async function pollRedashJob(jobId, requestContext) {
const jobUrl = `${requestContext.redashUrl}/api/jobs/${jobId}`;
const headers = { Authorization: `Key ${requestContext.redashApiKey}` };
const maxAttempts = 10;
const initialDelayMs = 1000;
for (let attempts = 0; attempts < maxAttempts; attempts++) {
// Use slightly increasing delay
await new Promise((resolve) => setTimeout(resolve, initialDelayMs * (attempts + 1)));
try {
const jobResponse = await axios.get(jobUrl, { headers });
const job = jobResponse.data.job;
if (job.status === 3 && job.query_result_id) {
return await getQueryResult(job.query_result_id, requestContext);
}
else if (job.status === 4) {
throw new Error(`Redash job ${jobId} failed: ${job.error || "Unknown error"}`);
}
else if (job.status > 4) {
throw new Error(`Redash job ${jobId} terminated unexpectedly with status ${job.status}`);
} // Else: status 1 or 2, continue polling
}
catch (error) {
console.error(`Error polling Redash job ${jobId} (attempt ${attempts + 1}/${maxAttempts}):`, error);
// Re-throw immediately if it's a job failure/termination error
if (error instanceof Error &&
(error.message.includes("failed:") ||
error.message.includes("terminated"))) {
handleRedashError(error, `job poll ${jobId}`);
}
// If it's the last attempt and still failing, let handleRedashError throw
if (attempts === maxAttempts - 1) {
handleRedashError(error, `job poll ${jobId} - max attempts reached`);
}
// Otherwise, log and continue polling loop
}
}
// Should not be reached if handleRedashError throws correctly on last attempt
throw new Error(`Redash job ${jobId} polling timed out after ${maxAttempts} attempts.`);
}
// Helper function to get query results using query_result_id
async function getQueryResult(queryResultId, requestContext) {
const resultUrl = `${requestContext.redashUrl}/api/query_results/${queryResultId}`;
const headers = { Authorization: `Key ${requestContext.redashApiKey}` };
try {
const response = await axios.get(resultUrl, { headers });
if (!response.data?.query_result?.data) {
// Use optional chaining
throw new Error("Invalid query result format from Redash API");
}
return response.data.query_result.data;
}
catch (error) {
handleRedashError(error, `query result ${queryResultId}`);
}
}
// Centralized error handling for Redash API calls
function handleRedashError(error, context) {
console.error(`Error during Redash operation (context: ${context}):`, error);
let message = `Redash operation failed (context: ${context}).`;
if (axios.isAxiosError(error)) {
const status = error.response?.status || "N/A";
const responseData = error.response?.data;
message = `Redash API request failed (context: ${context}, status: ${status}): ${responseData ? JSON.stringify(responseData) : "No response data"}`;
}
else if (error instanceof Error) {
message = `Redash operation failed (context: ${context}): ${error.message}`;
}
throw new Error(message); // Always throw
}
//# sourceMappingURL=redash.service.js.map