Skip to main content
Glama
attio-client.tsβ€’12.3 kB
/** * Attio API client and related utilities * Unified client factory with strategy pattern for different environments */ import axios, { AxiosInstance } from 'axios'; import { debug, error, OperationType } from '@/utils/logger.js'; import { getContextApiKey, validateApiKey } from './client-context.js'; import { AttioAttributeSchema, AttioSelectOption, AttioStatusOption, } from './types.js'; import { configureStandardInterceptors } from './client-interceptors.js'; import { ClientConfig, ClientMode, EnvironmentModeHandler, } from './client-config.js'; import { ClientStrategyFactory } from './client-strategies.js'; import { ClientCache } from './client-cache.js'; // Module identification for debugging (compatible with both ESM and CJS) const MODULE_FILE = 'attio-client'; // Debug loading info - use logger instead of console.log to avoid JSON parsing issues debug('attio-client', 'Module loaded', { file: `${MODULE_FILE}.js`, E2E_MODE: process.env.E2E_MODE, USE_MOCK_DATA: process.env.USE_MOCK_DATA, }); export const __MODULE_PATH__ = MODULE_FILE; export type AttioClient = AxiosInstance; /** * Validates and throws standardized error for API key issues * @param apiKey - The API key to validate * @param source - Source of the API key for error context * @throws {Error} With standardized error message for invalid API keys */ function validateAndThrowForApiKey( apiKey: string | undefined | null, source: string = 'provided' ): asserts apiKey is string { if (!apiKey || typeof apiKey !== 'string') { throw new Error( `Invalid API key: API key must be a non-empty string (source: ${source})` ); } if (!validateApiKey(apiKey)) { throw new Error( `Invalid API key format: API key contains invalid characters or whitespace (source: ${source})` ); } // Basic length validation - Attio API keys should be a reasonable length if (apiKey.length < 10) { throw new Error( `Invalid API key: API key appears to be too short (source: ${source})` ); } } // LEGACY: Global API client instance - replaced by ClientCache let apiInstance: AxiosInstance | null = null; /** * UNIFIED CLIENT FACTORY: Support new createAttioClient(config) signature * * @param config - Client configuration options * @returns Configured Axios instance */ export function createAttioClient(config?: ClientConfig): AxiosInstance; /** * BACKWARD COMPATIBILITY: Support old createAttioClient(apiKey) signature * * @param apiKey - The Attio API key (legacy signature) * @returns Configured Axios instance */ export function createAttioClient(apiKey: string): AxiosInstance; /** * Implementation of overloaded createAttioClient function */ export function createAttioClient( configOrApiKey: ClientConfig | string = {} ): AxiosInstance { // Handle legacy string API key parameter if (typeof configOrApiKey === 'string') { debug( 'attio-client', 'createAttioClient (LEGACY string signature) - redirecting to unified version' ); return createLegacyAttioClient(configOrApiKey); } // Handle new config object parameter - this is the main implementation we defined above const config = configOrApiKey as ClientConfig; // Get API key from config, environment, or context const apiKey = config.apiKey ?? process.env.ATTIO_API_KEY ?? getContextApiKey() ?? ''; // Determine the source for better error messages const apiKeySource = config.apiKey ? 'config parameter' : process.env.ATTIO_API_KEY ? 'environment variable' : 'context configuration'; // Validate API key using standardized validation validateAndThrowForApiKey(apiKey, apiKeySource); // Get environment-aware configuration const environmentConfig = EnvironmentModeHandler.getClientConfig({ rawE2E: config.mode === ClientMode.E2E_RAW, }); // Merge configurations: environment defaults < config parameter const finalConfig: ClientConfig = { ...environmentConfig, ...config, apiKey, // Always use the validated API key interceptors: { ...environmentConfig.interceptors, ...config.interceptors, }, }; debug('attio-client', 'Creating unified client', { mode: finalConfig.mode, hasApiKey: Boolean(apiKey), apiKeySource, bypassCache: finalConfig.bypassCache, baseURL: finalConfig.baseURL, timeout: finalConfig.timeout, }); // Use strategy factory to create the appropriate client return ClientStrategyFactory.createClient(finalConfig); } /** * LEGACY: Centralized authenticated Attio client builder * * @deprecated Use createAttioClient() instead for unified interface * Guarantees proper Authorization header and fails fast if API key is missing */ export function buildAttioClient(opts?: { apiKey?: string; baseURL?: string; timeoutMs?: number; }): AttioClient { // Convert legacy options to new config format const config: ClientConfig = { apiKey: opts?.apiKey, baseURL: opts?.baseURL, timeout: opts?.timeoutMs, mode: ClientMode.PRODUCTION, // buildAttioClient was used for production }; debug( 'attio-client', 'buildAttioClient (LEGACY) - redirecting to unified createAttioClient', { hasApiKey: Boolean(opts?.apiKey), baseURL: opts?.baseURL, timeout: opts?.timeoutMs, } ); // Use the new unified client factory return createAttioClient(config); } // Legacy getAttioClient exists below - it's already implemented /** * LEGACY: Creates and configures an Axios instance for the Attio API * * @deprecated Use createAttioClient(config) instead for unified interface * @param apiKey - The Attio API key * @returns Configured Axios instance */ export function createLegacyAttioClient(apiKey: string): AxiosInstance { // Use standardized validation validateAndThrowForApiKey(apiKey, 'parameter'); // Log client initialization for debugging (without exposing sensitive data) debug( MODULE_FILE, 'Initializing Attio client with provided API key', { hasApiKey: Boolean(apiKey) }, 'createAttioClient', OperationType.API_CALL ); const baseURL = ( process.env.ATTIO_BASE_URL || 'https://api.attio.com/v2' ).replace(/\/+$/, ''); // Log which client path is being used debug('attio-client', 'Client baseURL configured', { baseURL }); const client = axios.create({ baseURL, timeout: 20000, headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', Accept: 'application/json', }, // do NOT transform the response; we want raw server JSON transformResponse: [ (data) => { try { return JSON.parse(data); } catch { return data; } }, ], validateStatus: (s) => s >= 200 && s < 300, // don't swallow 4xx/5xx }); // Configure interceptors using the centralized module const isE2EMode = process.env.E2E_MODE === 'true'; const interceptorPrefix = isE2EMode ? 'E2E' : ''; debug('attio-client', 'Configuring client interceptors', { baseURL, isE2EMode, prefix: interceptorPrefix, }); configureStandardInterceptors(client, { prefix: interceptorPrefix, enableDiagnostics: true, enableErrorHandling: true, }); return client; } /** * Gets the schema for a specific attribute. * @param objectSlug - The slug of the object (e.g., 'companies'). * @param attributeSlug - The slug of the attribute (e.g., 'categories'). * @returns The attribute schema. */ export async function getAttributeSchema( objectSlug: string, attributeSlug: string ): Promise<AttioAttributeSchema> { const client = getAttioClient(); const path = `/objects/${objectSlug}/attributes/${attributeSlug}`; try { const response = await client.get(path); return response.data?.data; } catch (err) { error( 'attio-client', `Failed to get attribute schema for ${objectSlug}.${attributeSlug}`, err, { objectSlug, attributeSlug } ); throw err; } } /** * Lists the available options for a select attribute. * @param objectSlug - The slug of the object. * @param attributeSlug - The slug of the select attribute. * @returns A list of available select options. */ export async function getSelectOptions( objectSlug: string, attributeSlug: string ): Promise<AttioSelectOption[]> { const client = getAttioClient(); const path = `/objects/${objectSlug}/attributes/${attributeSlug}/options`; try { const response = await client.get(path); return response.data?.data || []; } catch (err) { error( 'attio-client', `Failed to get select options for ${objectSlug}.${attributeSlug}`, err, { objectSlug, attributeSlug } ); throw err; } } /** * Lists the available statuses for a status attribute. * @param objectSlug - The slug of the object. * @param attributeSlug - The slug of the status attribute. * @returns A list of available statuses. */ export async function getStatusOptions( objectSlug: string, attributeSlug: string ): Promise<AttioStatusOption[]> { const client = getAttioClient(); const path = `/objects/${objectSlug}/attributes/${attributeSlug}/statuses`; try { const response = await client.get(path); return response.data?.data || []; } catch (err) { error( 'attio-client', `Failed to get status options for ${objectSlug}.${attributeSlug}`, err, { objectSlug, attributeSlug } ); throw err; } } /** * Initializes the global API client with the provided API key * * @deprecated Use createAttioClient() with ClientCache instead * @param apiKey - The Attio API key */ export function initializeAttioClient(apiKey: string): AxiosInstance { apiInstance = createAttioClient(apiKey); // This will use the legacy signature ClientCache.setInstance(apiInstance); return apiInstance; } /** * Gets the global API client instance - SIMPLIFIED using unified architecture * * @deprecated Use createAttioClient() or getLazyAttioClient() instead * @returns The Axios instance for the Attio API * @throws If the API client hasn't been initialized and no API key is available */ export function getAttioClient(opts?: { rawE2E?: boolean }): AxiosInstance { debug('attio-client', 'getAttioClient (LEGACY) called', { rawE2E: opts?.rawE2E, hasCache: ClientCache.hasInstance(), E2E_MODE: process.env.E2E_MODE, USE_MOCK_DATA: process.env.USE_MOCK_DATA, }); // Check if we should bypass cache and create fresh client const shouldBypassCache = EnvironmentModeHandler.shouldUseRealClient() || opts?.rawE2E; if (shouldBypassCache) { debug('attio-client', 'Bypassing cache - creating fresh client'); // Clear cache to ensure fresh client ClientCache.clearInstance(); apiInstance = null; // Determine mode based on rawE2E option const mode = opts?.rawE2E ? ClientMode.E2E_RAW : EnvironmentModeHandler.determineMode(); const config: ClientConfig = { mode, bypassCache: true, }; const client = createAttioClient(config); debug('attio-client', 'Returning fresh E2E client'); return client; } // Check cache first const cachedClient = ClientCache.getInstance(); if (cachedClient) { debug('attio-client', 'Returning cached client from ClientCache'); return cachedClient; } // Check legacy cache if (apiInstance) { debug('attio-client', 'Returning cached client from legacy apiInstance'); return apiInstance; } // No cached client - create new one debug('attio-client', 'No cached client found - creating new client'); try { const config: ClientConfig = {}; // Use environment defaults const client = createAttioClient(config); // Cache the client in both new and legacy systems ClientCache.setInstance(client); apiInstance = client; debug('attio-client', 'Created and cached new client'); return client; } catch (validationError) { const errorMessage = validationError instanceof Error ? validationError.message : String(validationError); throw new Error( `API client not initialized and no valid API key available. ${errorMessage} Call initializeAttioClient first or set ATTIO_API_KEY environment variable.` ); } }

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/kesslerio/attio-mcp-server'

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