"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SalesforceMetadataClient = void 0;
const logger_1 = __importDefault(require("../utils/logger"));
const errorHandler_1 = require("../utils/errorHandler");
const rateLimiter_1 = require("../utils/rateLimiter");
const incrementalTracker_1 = require("../utils/incrementalTracker");
class SalesforceMetadataClient {
constructor(connection, rateLimitConfig) {
this.conn = null;
this.incrementalTracker = null;
this.connection = connection;
// Default rate limiting configuration for Salesforce
const defaultConfig = {
maxRequestsPerSecond: 5,
maxRequestsPerHour: 5000,
retryAttempts: 3,
baseDelay: 1000
};
this.rateLimiter = new rateLimiter_1.RateLimiter(rateLimitConfig || defaultConfig);
}
async authenticate() {
try {
// Use OAuth authentication by default
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 errorHandler_1.AppError('Missing Salesforce credentials', 400);
}
this.conn = await this.connection.connectWithOAuth(username, password, securityToken);
logger_1.default.info('Successfully authenticated with Salesforce');
}
catch (error) {
logger_1.default.error('Failed to authenticate with Salesforce', { error });
throw new errorHandler_1.AppError('Salesforce authentication failed', 401);
}
}
async listMetadata(types) {
if (!this.conn) {
throw new errorHandler_1.AppError('Not authenticated', 401);
}
try {
logger_1.default.debug('Listing metadata types', { types });
// Implement rate limiting by batching requests
const batchSize = 3; // Salesforce allows up to 3 types per call
const results = [];
for (let i = 0; i < types.length; i += batchSize) {
const batch = types.slice(i, i + batchSize);
const batchResults = await this.rateLimiter.executeWithRateLimit(async () => {
return await this.conn.metadata.list(batch);
});
// Handle single result or array
const resultArray = Array.isArray(batchResults) ? batchResults : [batchResults];
results.push(...resultArray);
}
logger_1.default.info(`Retrieved ${results.length} metadata items`);
return results;
}
catch (error) {
logger_1.default.error('Failed to list metadata', { error, types });
throw new errorHandler_1.AppError('Failed to retrieve metadata list', 500);
}
}
async readMetadata(type, fullNames) {
if (!this.conn) {
throw new errorHandler_1.AppError('Not authenticated', 401);
}
try {
logger_1.default.debug('Reading metadata', { type, count: fullNames.length });
// Batch requests to stay within limits
const batchSize = 10;
const results = [];
for (let i = 0; i < fullNames.length; i += batchSize) {
const batch = fullNames.slice(i, i + batchSize);
const batchResults = await this.rateLimiter.executeWithRateLimit(async () => {
return await this.conn.metadata.read({ type }, batch);
});
// Handle single result or array
const resultArray = Array.isArray(batchResults) ? batchResults : [batchResults];
results.push(...resultArray);
}
logger_1.default.info(`Read ${results.length} ${type} metadata items`);
return results;
}
catch (error) {
logger_1.default.error('Failed to read metadata', { error, type, fullNames });
throw new errorHandler_1.AppError(`Failed to read ${type} metadata`, 500);
}
}
async retrievePackage(packageXml) {
if (!this.conn) {
throw new errorHandler_1.AppError('Not authenticated', 401);
}
try {
logger_1.default.debug('Starting package retrieval');
const retrieveRequest = await this.rateLimiter.executeWithRateLimit(async () => {
return await this.conn.metadata.retrieve({
unpackaged: JSON.parse(packageXml)
});
});
if (!retrieveRequest.id) {
throw new errorHandler_1.AppError('Failed to initiate retrieve request', 500);
}
const result = await this.pollRetrieveStatus(retrieveRequest.id);
logger_1.default.info('Package retrieval completed', { success: result.success });
return result;
}
catch (error) {
logger_1.default.error('Failed to retrieve package', { error });
throw new errorHandler_1.AppError('Package retrieval failed', 500);
}
}
async pollRetrieveStatus(retrieveId) {
const maxAttempts = 30;
let attempts = 0;
let delay = 1000; // Start with 1 second
while (attempts < maxAttempts) {
try {
const result = await this.conn.metadata.checkRetrieveStatus(retrieveId);
if (result.done) {
return result;
}
attempts++;
logger_1.default.debug(`Retrieve status check ${attempts}/${maxAttempts}`, {
retrieveId,
done: result.done
});
// Exponential backoff with jitter
await this.delay(delay + Math.random() * 1000);
delay = Math.min(delay * 1.5, 10000); // Cap at 10 seconds
}
catch (error) {
logger_1.default.error('Error checking retrieve status', { error, retrieveId, attempts });
throw new errorHandler_1.AppError('Failed to check retrieve status', 500);
}
}
throw new errorHandler_1.AppError('Retrieve operation timed out', 408);
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
getConnection() {
if (!this.conn) {
throw new errorHandler_1.AppError('Not authenticated', 401);
}
return this.conn;
}
async initializeIncrementalTracking(orgId) {
this.incrementalTracker = new incrementalTracker_1.IncrementalTracker(orgId);
await this.incrementalTracker.loadState();
logger_1.default.info('Initialized incremental tracking', { orgId });
}
async listMetadataIncremental(types) {
if (!this.incrementalTracker) {
throw new errorHandler_1.AppError('Incremental tracking not initialized', 500);
}
// Get all current metadata
const allMetadata = await this.listMetadata(types);
// Detect changes
const changes = this.incrementalTracker.detectChanges(allMetadata);
// Update snapshots with current state
this.incrementalTracker.updateSnapshots(allMetadata);
await this.incrementalTracker.saveState();
return {
all: allMetadata,
changes
};
}
async getChangedMetadataSince(sinceDate) {
if (!this.incrementalTracker) {
throw new errorHandler_1.AppError('Incremental tracking not initialized', 500);
}
const changedSnapshots = this.incrementalTracker.getChangedSince(sinceDate);
// Convert snapshots back to MetadataInfo format
return changedSnapshots.map(snapshot => ({
id: '',
fullName: snapshot.fullName,
type: snapshot.type,
createdDate: '',
lastModifiedDate: snapshot.lastModifiedDate
}));
}
async performIncrementalSync(types) {
if (!this.incrementalTracker) {
throw new errorHandler_1.AppError('Incremental tracking not initialized', 500);
}
logger_1.default.info('Starting incremental metadata sync');
const result = await this.listMetadataIncremental(types);
const stats = {
totalItems: result.all.length,
newItems: result.changes.added.length,
modifiedItems: result.changes.modified.length,
deletedItems: result.changes.deleted.length
};
logger_1.default.info('Incremental sync completed', stats);
return stats;
}
getRateLimitStats() {
return this.rateLimiter.getStats();
}
}
exports.SalesforceMetadataClient = SalesforceMetadataClient;