import {
type IonApiConfig,
getRestApiBaseUrl,
getGenAiApiBaseUrl,
getIcwApiBaseUrl,
} from "../config/ionapi.js";
import {
loadEnvironmentConfig,
getCurrentEnvironment,
type Environment,
} from "../config/environments.js";
import { getAccessToken } from "../auth/ionAuth.js";
import { logger } from "../utils/logger.js";
import { mapHttpErrorToMcp, extractErrorMessage } from "../utils/errors.js";
export type ApiType = "rest" | "genai" | "icw";
interface RequestOptions {
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
body?: unknown;
headers?: Record<string, string>;
queryParams?: Record<string, string | number | boolean | undefined>;
}
export interface BirstClient {
/**
* Make an authenticated request to the Birst REST API
*/
rest<T = unknown>(path: string, options?: RequestOptions): Promise<T>;
/**
* Make an authenticated request to the Birst GenAI API
*/
genai<T = unknown>(path: string, options?: RequestOptions): Promise<T>;
/**
* Make an authenticated request to the Birst ICW API
*/
icw<T = unknown>(path: string, options?: RequestOptions): Promise<T>;
/**
* Get the current environment
*/
getEnvironment(): Environment;
/**
* Switch to a different environment
*/
setEnvironment(env: Environment): void;
}
class BirstClientImpl implements BirstClient {
private config: IonApiConfig;
private environment: Environment;
constructor() {
this.environment = getCurrentEnvironment();
this.config = loadEnvironmentConfig(this.environment);
logger.info(`Birst client initialized for environment: ${this.environment}`);
}
getEnvironment(): Environment {
return this.environment;
}
setEnvironment(env: Environment): void {
this.environment = env;
this.config = loadEnvironmentConfig(env);
logger.info(`Switched to environment: ${env}`);
}
async rest<T = unknown>(path: string, options?: RequestOptions): Promise<T> {
const baseUrl = getRestApiBaseUrl(this.config);
return this.request<T>(baseUrl, path, options);
}
async genai<T = unknown>(path: string, options?: RequestOptions): Promise<T> {
const baseUrl = getGenAiApiBaseUrl(this.config);
return this.request<T>(baseUrl, path, options);
}
async icw<T = unknown>(path: string, options?: RequestOptions): Promise<T> {
const baseUrl = getIcwApiBaseUrl(this.config);
return this.request<T>(baseUrl, path, options);
}
private async request<T>(
baseUrl: string,
path: string,
options: RequestOptions = {}
): Promise<T> {
const { method = "GET", body, headers = {}, queryParams } = options;
// Build URL with query parameters
let url = `${baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
if (queryParams) {
const params = new URLSearchParams();
for (const [key, value] of Object.entries(queryParams)) {
if (value !== undefined) {
params.append(key, String(value));
}
}
const queryString = params.toString();
if (queryString) {
url += `?${queryString}`;
}
}
// Get access token
const accessToken = await getAccessToken(this.config);
// Build request headers
const requestHeaders: Record<string, string> = {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json",
...headers,
};
// Add content-type for requests with body
if (body !== undefined) {
requestHeaders["Content-Type"] = "application/json";
}
logger.debug(`${method} ${url}`);
// Make request
const response = await fetch(url, {
method,
headers: requestHeaders,
body: body !== undefined ? JSON.stringify(body) : undefined,
});
// Handle response
if (!response.ok) {
let errorBody: unknown;
try {
errorBody = await response.json();
} catch {
errorBody = await response.text();
}
const errorMessage = extractErrorMessage(errorBody);
logger.error(`API error: ${response.status} ${errorMessage}`, errorBody);
throw mapHttpErrorToMcp(response.status, errorMessage, errorBody);
}
// Parse successful response
const contentType = response.headers.get("content-type");
if (contentType?.includes("application/json")) {
return (await response.json()) as T;
}
// Return empty object for no-content responses
if (response.status === 204) {
return {} as T;
}
// Return text as-is for non-JSON responses
return (await response.text()) as unknown as T;
}
}
// Singleton instance
let clientInstance: BirstClient | null = null;
/**
* Get the Birst client singleton instance
*/
export function getBirstClient(): BirstClient {
if (!clientInstance) {
clientInstance = new BirstClientImpl();
}
return clientInstance;
}
/**
* Reset the client instance (useful for testing)
*/
export function resetBirstClient(): void {
clientInstance = null;
}