/**
* Twenty CRM REST Client
*
* Infrastructure layer client for Twenty CRM REST API.
* Handles REST operations for all Twenty CRM entities.
*/
import { ILogger } from '../logging/logger.js';
/**
* REST client interface for Twenty CRM
*/
export interface ITwentyRESTClient {
get<T>(endpoint: string, params?: Record<string, any>): Promise<T | null>;
post<T>(endpoint: string, body?: Record<string, any>): Promise<T | null>;
patch<T>(endpoint: string, body?: Record<string, any>): Promise<T | null>;
delete<T>(endpoint: string): Promise<T | null>;
}
/**
* Twenty CRM REST Client implementation
*/
export class TwentyRESTClient implements ITwentyRESTClient {
private readonly baseUrl: string;
private readonly apiKey: string;
private readonly logger: ILogger;
constructor(apiUrl: string, apiKey: string, logger: ILogger) {
this.baseUrl = `${apiUrl}/rest`;
this.apiKey = apiKey;
this.logger = logger;
this.logger.info('Twenty REST client initialized', { baseUrl: this.baseUrl });
}
/**
* Execute a GET request
*
* @param endpoint - API endpoint (e.g., '/people', '/companies/123')
* @param params - Query parameters
* @returns Response data or null on error
*/
async get<T>(endpoint: string, params?: Record<string, any>): Promise<T | null> {
try {
const url = this.buildUrl(endpoint, params);
this.logger.debug('Executing REST GET', { url, params });
const response = await fetch(url, {
method: 'GET',
headers: this.getHeaders(),
});
return await this.handleResponse<T>(response);
} catch (error) {
this.logger.error(
'REST GET failed',
error instanceof Error ? error : new Error(String(error)),
{ endpoint, params }
);
return null;
}
}
/**
* Execute a POST request
*
* @param endpoint - API endpoint
* @param body - Request body
* @returns Response data or null on error
*/
async post<T>(endpoint: string, body?: Record<string, any>): Promise<T | null> {
try {
const url = this.buildUrl(endpoint);
this.logger.debug('Executing REST POST', { url, body });
const response = await fetch(url, {
method: 'POST',
headers: this.getHeaders(),
body: body ? JSON.stringify(body) : undefined,
});
return await this.handleResponse<T>(response);
} catch (error) {
this.logger.error(
'REST POST failed',
error instanceof Error ? error : new Error(String(error)),
{ endpoint, body }
);
return null;
}
}
/**
* Execute a PATCH request
*
* @param endpoint - API endpoint
* @param body - Request body
* @returns Response data or null on error
*/
async patch<T>(endpoint: string, body?: Record<string, any>): Promise<T | null> {
try {
const url = this.buildUrl(endpoint);
this.logger.debug('Executing REST PATCH', { url, body });
const response = await fetch(url, {
method: 'PATCH',
headers: this.getHeaders(),
body: body ? JSON.stringify(body) : undefined,
});
return await this.handleResponse<T>(response);
} catch (error) {
this.logger.error(
'REST PATCH failed',
error instanceof Error ? error : new Error(String(error)),
{ endpoint, body }
);
return null;
}
}
/**
* Execute a DELETE request
*
* @param endpoint - API endpoint
* @returns Response data or null on error
*/
async delete<T>(endpoint: string): Promise<T | null> {
try {
const url = this.buildUrl(endpoint);
this.logger.debug('Executing REST DELETE', { url });
const response = await fetch(url, {
method: 'DELETE',
headers: this.getHeaders(),
});
return await this.handleResponse<T>(response);
} catch (error) {
this.logger.error(
'REST DELETE failed',
error instanceof Error ? error : new Error(String(error)),
{ endpoint }
);
return null;
}
}
/**
* Build full URL with query parameters
*/
private buildUrl(endpoint: string, params?: Record<string, any>): string {
const url = new URL(endpoint, this.baseUrl);
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
url.searchParams.append(key, String(value));
}
});
}
return url.toString();
}
/**
* Get standard headers for requests
*/
private getHeaders(): Record<string, string> {
return {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
};
}
/**
* Handle API response
*/
private async handleResponse<T>(response: Response): Promise<T | null> {
if (!response.ok) {
const errorText = await response.text();
this.logger.error(
'REST API error response',
new Error(`HTTP ${response.status}: ${response.statusText}`),
{ status: response.status, errorText }
);
return null;
}
// Handle 204 No Content
if (response.status === 204) {
return {} as T;
}
const data = await response.json();
return data as T;
}
}
/**
* Factory function to create Twenty REST client
*
* @param logger - Logger instance
* @returns Twenty REST client instance
*/
export function createTwentyRESTClient(logger: ILogger): ITwentyRESTClient {
const apiUrl = process.env.TWENTY_API_URL || 'https://api.twenty.com';
const apiKey = process.env.TWENTY_API_KEY || '';
if (!apiKey) {
logger.error('TWENTY_API_KEY environment variable is not set', new Error('Missing API key'));
throw new Error('TWENTY_API_KEY environment variable is required');
}
return new TwentyRESTClient(apiUrl, apiKey, logger);
}