import * as jsforce from 'jsforce';
import { SalesforceConnection } from './connection';
import logger from '../utils/logger';
import { AppError } from '../utils/errorHandler';
import { RateLimiter, RateLimitConfig } from '../utils/rateLimiter';
export interface ApexClass {
Id: string;
Name: string;
Body: string;
Status: string;
CreatedDate: string;
LastModifiedDate: string;
}
export interface ApexTrigger {
Id: string;
Name: string;
Body: string;
TableEnumOrId: string;
CreatedDate: string;
LastModifiedDate: string;
}
export interface ValidationRule {
Id: string;
ValidationName: string;
EntityDefinition: { QualifiedApiName: string };
Active: boolean;
ErrorMessage: string;
ErrorDisplayField: string;
Description: string;
Formula: string;
}
export class SalesforceToolingClient {
private connection: SalesforceConnection;
private conn: jsforce.Connection | null = null;
private rateLimiter: RateLimiter;
constructor(connection: SalesforceConnection, rateLimitConfig?: RateLimitConfig) {
this.connection = connection;
const defaultConfig: RateLimitConfig = {
maxRequestsPerSecond: 5,
maxRequestsPerHour: 5000,
retryAttempts: 3,
baseDelay: 1000
};
this.rateLimiter = new RateLimiter(rateLimitConfig || defaultConfig);
}
async authenticate(): Promise<void> {
try {
const username = process.env.SF_USERNAME;
const password = process.env.SF_PASSWORD;
const securityToken = process.env.SF_SECURITY_TOKEN;
if (!username || !password || !securityToken) {
throw new AppError('Missing Salesforce credentials', 400);
}
this.conn = await this.connection.connectWithOAuth(username, password, securityToken);
logger.info('Successfully authenticated with Salesforce for Tooling API');
} catch (error) {
logger.error('Failed to authenticate with Salesforce', { error });
throw new AppError('Salesforce authentication failed', 401);
}
}
async executeQuery(soql: string): Promise<any> {
if (!this.conn) {
throw new AppError('Not authenticated', 401);
}
try {
logger.debug('Executing Tooling API query', { soql });
const result = await this.rateLimiter.executeWithRateLimit(async () => {
return await this.conn!.tooling.query(soql);
});
logger.debug('Tooling query completed', { recordCount: result.records?.length || 0 });
return result;
} catch (error) {
logger.error('Failed to execute Tooling API query', { error, soql });
throw new AppError('Tooling API query failed', 500);
}
}
async getApexClasses(): Promise<ApexClass[]> {
const query = 'SELECT Id, Name, Body, Status, CreatedDate, LastModifiedDate FROM ApexClass';
const result = await this.executeQuery(query);
return result.records || [];
}
async getApexTriggers(): Promise<ApexTrigger[]> {
const query = 'SELECT Id, Name, Body, TableEnumOrId, CreatedDate, LastModifiedDate FROM ApexTrigger';
const result = await this.executeQuery(query);
return result.records || [];
}
async getValidationRules(): Promise<ValidationRule[]> {
const query = `SELECT Id, ValidationName, EntityDefinition.QualifiedApiName,
Active, ErrorMessage, ErrorDisplayField, Description, Formula
FROM ValidationRule`;
const result = await this.executeQuery(query);
return result.records || [];
}
async getApexClassByName(className: string): Promise<ApexClass | null> {
const query = `SELECT Id, Name, Body, Status, CreatedDate, LastModifiedDate
FROM ApexClass WHERE Name = '${className}'`;
const result = await this.executeQuery(query);
return result.records?.[0] || null;
}
getRateLimitStats() {
return this.rateLimiter.getStats();
}
}