"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.authenticateWithPassword = exports.generateSalesforceRequest = void 0;
const actions_core_1 = require("@segment/actions-core");
const sf_object_to_shape_1 = require("./sf-object-to-shape");
const sf_utils_1 = require("./sf-utils");
const actions_core_2 = require("@segment/actions-core");
const versioning_info_1 = require("./versioning-info");
const throwBulkMismatchError = () => {
    const errorMsg = 'Bulk operation triggered where enable_batching is false.';
    throw new actions_core_1.IntegrationError(errorMsg, errorMsg, 400);
};
const validateSOQLOperator = (operator) => {
    if (operator !== undefined && operator !== 'OR' && operator !== 'AND') {
        throw new actions_core_1.IntegrationError(`Invalid SOQL operator - ${operator}`, 'Invalid SOQL operator', 400);
    }
    if (operator === undefined) {
        return 'OR';
    }
    return operator;
};
const generateSalesforceRequest = async (settings, request) => {
    if (!settings.auth_password || !settings.username) {
        return request;
    }
    const { accessToken } = await (0, exports.authenticateWithPassword)(settings.username, settings.auth_password, settings.security_token, settings.isSandbox);
    const passwordRequestClient = (0, actions_core_2.createRequestClient)({
        headers: {
            Authorization: `Bearer ${accessToken}`
        }
    });
    return passwordRequestClient;
};
exports.generateSalesforceRequest = generateSalesforceRequest;
const constructPassword = (password, securityToken) => {
    let combined = '';
    if (password) {
        combined = password;
    }
    if (securityToken) {
        combined = password + securityToken;
    }
    return combined;
};
const authenticateWithPassword = async (username, auth_password, security_token, isSandbox) => {
    const clientId = process.env.SALESFORCE_CLIENT_ID;
    const clientSecret = process.env.SALESFORCE_CLIENT_SECRET;
    if (!clientId || !clientSecret) {
        throw new actions_core_1.IntegrationError('Missing Salesforce client ID or client secret', 'Missing Credentials', 400);
    }
    const newRequest = (0, actions_core_2.createRequestClient)();
    const loginUrl = isSandbox ? 'https://test.salesforce.com' : 'https://login.salesforce.com';
    const password = constructPassword(auth_password, security_token);
    const res = await newRequest(`${loginUrl}/services/oauth2/token`, {
        method: 'post',
        body: new URLSearchParams({
            grant_type: 'password',
            client_id: clientId,
            client_secret: clientSecret,
            username: username,
            password
        })
    });
    return { accessToken: res.data.access_token };
};
exports.authenticateWithPassword = authenticateWithPassword;
class Salesforce {
    constructor(instanceUrl, request) {
        this.createRecord = async (payload, sobject) => {
            const json = this.buildJSONData(payload, sobject);
            return this.request(`${this.instanceUrl}services/data/${versioning_info_1.SALESFORCE_API_VERSION}/sobjects/${sobject}`, {
                method: 'post',
                json: json
            });
        };
        this.updateRecord = async (payload, sobject) => {
            if (!payload.traits || Object.keys(payload.traits).length === 0) {
                throw new actions_core_1.IntegrationError('Undefined Traits when using update operation', 'Undefined Traits', 400);
            }
            if (Object.keys(payload.traits).includes('Id') && payload.traits['Id']) {
                return await this.baseUpdate(payload.traits['Id'], sobject, payload);
            }
            const soqlOperator = validateSOQLOperator(payload.recordMatcherOperator);
            const [recordId, err] = await this.lookupTraits(payload.traits, sobject, soqlOperator);
            if (err) {
                throw err;
            }
            return await this.baseUpdate(recordId, sobject, payload);
        };
        this.upsertRecord = async (payload, sobject) => {
            if (!payload.traits || Object.keys(payload.traits).length === 0) {
                throw new actions_core_1.IntegrationError('Undefined Traits when using upsert operation', 'Undefined Traits', 400);
            }
            const soqlOperator = validateSOQLOperator(payload.recordMatcherOperator);
            const [recordId, err] = await this.lookupTraits(payload.traits, sobject, soqlOperator);
            if (err) {
                if (err.status === 404) {
                    return await this.createRecord(payload, sobject);
                }
                throw err;
            }
            return await this.baseUpdate(recordId, sobject, payload);
        };
        this.deleteRecord = async (payload, sobject) => {
            if (!payload.traits || Object.keys(payload.traits).length === 0) {
                throw new actions_core_1.IntegrationError('Undefined Traits when using delete operation', 'Undefined Traits', 400);
            }
            if (Object.keys(payload.traits).includes('Id') && payload.traits['Id']) {
                return await this.baseDelete(payload.traits['Id'], sobject);
            }
            const soqlOperator = validateSOQLOperator(payload.recordMatcherOperator);
            const [recordId, err] = await this.lookupTraits(payload.traits, sobject, soqlOperator);
            if (err) {
                throw err;
            }
            return await this.baseDelete(recordId, sobject);
        };
        this.bulkHandler = async (payloads, sobject, logging) => {
            if (!payloads[0].enable_batching) {
                throwBulkMismatchError();
            }
            if (payloads[0].operation === 'upsert') {
                return await this.bulkUpsert(payloads, sobject, logging);
            }
            else if (payloads[0].operation === 'update') {
                return await this.bulkUpdate(payloads, sobject, logging);
            }
            else if (payloads[0].operation === 'create') {
                return await this.bulkInsert(payloads, sobject, logging);
            }
            if (payloads[0].operation === 'delete') {
                throw new actions_core_1.IntegrationError(`Unsupported operation: Bulk API does not support the delete operation`, 'Unsupported operation', 400);
            }
        };
        this.bulkHandlerWithSyncMode = async (payloads, sobject, syncMode, logging) => {
            if (!payloads[0].enable_batching) {
                throwBulkMismatchError();
            }
            if (syncMode === undefined) {
                throw new actions_core_1.IntegrationError('syncMode is required', 'Undefined syncMode', 400);
            }
            if (syncMode === 'delete') {
                throw new actions_core_1.IntegrationError(`Unsupported operation: Bulk API does not support the delete operation`, 'Unsupported operation', 400);
            }
            if (syncMode === 'upsert') {
                return await this.bulkUpsert(payloads, sobject, logging);
            }
            else if (syncMode === 'update') {
                return await this.bulkUpdate(payloads, sobject, logging);
            }
            else if (syncMode === 'add') {
                return await this.bulkInsert(payloads, sobject, logging);
            }
        };
        this.customObjectName = async () => {
            try {
                const result = await this.request(`${this.instanceUrl}services/data/${versioning_info_1.SALESFORCE_API_VERSION}/sobjects`, {
                    method: 'get',
                    skipResponseCloning: true
                });
                const fields = result.data.sobjects.filter((field) => {
                    return field.createable === true;
                });
                const choices = fields.map((field) => {
                    return { value: field.name, label: field.label };
                });
                return {
                    choices: choices,
                    nextPage: '2'
                };
            }
            catch (err) {
                return {
                    choices: [],
                    nextPage: '',
                    error: {
                        message: err.response?.data[0]?.message ?? 'Unknown error',
                        code: err.response?.data[0]?.errorCode ?? 'Unknown error'
                    }
                };
            }
        };
        this.bulkInsert = async (payloads, sobject, logging) => {
            return this.handleBulkJob(payloads, sobject, '', 'insert', logging);
        };
        this.bulkUpsert = async (payloads, sobject, logging) => {
            if (!payloads[0].bulkUpsertExternalId ||
                !payloads[0].bulkUpsertExternalId.externalIdName ||
                !payloads[0].bulkUpsertExternalId.externalIdValue) {
                throw new actions_core_1.IntegrationError('Undefined bulkUpsertExternalId.externalIdName or externalIdValue when using bulkUpsert operation', 'Undefined bulkUpsertExternalId.externalIdName externalIdValue', 400);
            }
            const externalIdFieldName = payloads[0].bulkUpsertExternalId.externalIdName;
            return this.handleBulkJob(payloads, sobject, externalIdFieldName, 'upsert', logging);
        };
        this.bulkUpdate = async (payloads, sobject, logging) => {
            if (!payloads[0].bulkUpdateRecordId) {
                throw new actions_core_1.IntegrationError('Undefined bulkUpdateRecordId when using bulkUpdate operation', 'Undefined bulkUpdateRecordId', 400);
            }
            return this.handleBulkJob(payloads, sobject, 'Id', 'update', logging);
        };
        this.createBulkJob = async (sobject, externalIdFieldName, operation, logging) => {
            const jsonData = {
                object: sobject,
                contentType: 'CSV',
                operation: operation
            };
            if (operation === 'update' || operation === 'upsert') {
                jsonData.externalIdFieldName = externalIdFieldName;
            }
            const res = await this.request(`${this.instanceUrl}services/data/${versioning_info_1.SALESFORCE_API_VERSION}/jobs/ingest`, {
                method: 'post',
                json: jsonData
            });
            if (!res || !res.data || !res.data.id) {
                throw new actions_core_1.IntegrationError('Failed to create bulk job', 'Failed to create bulk job', 500);
            }
            if (logging?.shouldLog) {
                logging.logger?.error(`Created bulk job: ${res.data.id} with data: ${JSON.stringify(jsonData)}`);
                const statsClient = logging.stats?.statsClient;
                const tags = logging.stats?.tags;
                tags?.push('jobId:' + res.data.id);
                statsClient?.incr('bulkJob.createBulkJob', 1, tags);
            }
            return res.data.id;
        };
        this.uploadBulkCSV = async (jobId, csv, logging) => {
            if (logging?.shouldLog) {
                logging.logger?.error(`Uploading CSV to job: ${jobId}\nCSV: ${csv}`);
                const statsClient = logging.stats?.statsClient;
                const tags = logging.stats?.tags;
                tags?.push('jobId:' + jobId);
                statsClient?.incr('bulkJob.uploadCSV', 1, tags);
            }
            return this.request(`${this.instanceUrl}services/data/${versioning_info_1.SALESFORCE_API_VERSION}/jobs/ingest/${jobId}/batches`, {
                method: 'put',
                headers: {
                    'Content-Type': 'text/csv',
                    Accept: 'application/json'
                },
                body: csv
            });
        };
        this.closeBulkJob = async (jobId, logging) => {
            if (logging?.shouldLog) {
                logging.logger?.error(`Closing job: ${jobId}`);
                const statsClient = logging.stats?.statsClient;
                const tags = logging.stats?.tags;
                tags?.push('jobId:' + jobId);
                statsClient?.incr('bulkJob.closeBulkJob', 1, tags);
            }
            return this.request(`${this.instanceUrl}services/data/${versioning_info_1.SALESFORCE_API_VERSION}/jobs/ingest/${jobId}`, {
                method: 'PATCH',
                json: {
                    state: 'UploadComplete'
                }
            });
        };
        this.baseUpdate = async (recordId, sobject, payload) => {
            const json = this.buildJSONData(payload, sobject);
            return this.request(`${this.instanceUrl}services/data/${versioning_info_1.SALESFORCE_API_VERSION}/sobjects/${sobject}/${recordId}`, {
                method: 'patch',
                json: json
            });
        };
        this.baseDelete = async (recordId, sobject) => {
            return this.request(`${this.instanceUrl}services/data/${versioning_info_1.SALESFORCE_API_VERSION}/sobjects/${sobject}/${recordId}`, {
                method: 'delete'
            });
        };
        this.buildJSONData = (payload, sobject) => {
            let baseShape = {};
            if (!payload.customObjectName) {
                baseShape = (0, sf_object_to_shape_1.mapObjectToShape)(payload, sobject);
            }
            if (payload.customFields) {
                baseShape = { ...baseShape, ...payload.customFields };
            }
            return baseShape;
        };
        this.escapeQuotes = (value) => value.replace(/'/g, "\\'");
        this.removeInvalidChars = (value) => value.replace(/[^a-zA-Z0-9_]/g, '');
        this.typecast = (value) => {
            switch (typeof value) {
                case 'boolean':
                    return value;
                case 'number':
                    return value;
                case 'string':
                    return `'${this.escapeQuotes(value)}'`;
                default:
                    throw new actions_core_1.IntegrationError('Unsupported datatype for record matcher traits - ' + typeof value, 'Unsupported Type', 400);
            }
        };
        this.buildQuery = (traits, sobject, soqlOperator) => {
            let soql = `SELECT Id FROM ${sobject} WHERE `;
            const entries = Object.entries(traits);
            let i = 0;
            for (const [key, value] of entries) {
                let token = `${this.removeInvalidChars(key)} = ${this.typecast(value)}`;
                if (i < entries.length - 1) {
                    token += ' ' + soqlOperator + ' ';
                }
                soql += token;
                i += 1;
            }
            return soql;
        };
        this.lookupTraits = async (traits, sobject, soqlOperator) => {
            const SOQLQuery = encodeURIComponent(this.buildQuery(traits, sobject, soqlOperator));
            const res = await this.request(`${this.instanceUrl}services/data/${versioning_info_1.SALESFORCE_API_VERSION}/query/?q=${SOQLQuery}`, { method: 'GET' });
            if (!res || !res.data || res.data.totalSize === undefined) {
                return ['', new actions_core_1.IntegrationError('Response missing expected fields', 'Bad Response', 400)];
            }
            if (res.data.totalSize === 0) {
                return ['', new actions_core_1.IntegrationError('No record found with given traits', 'Record Not Found', 404)];
            }
            if (res.data.totalSize > 1) {
                return ['', new actions_core_1.IntegrationError('Multiple records returned with given traits', 'Multiple Records Found', 300)];
            }
            if (!res.data.records || !res.data.records[0] || !res.data.records[0].Id) {
                return ['', new actions_core_1.IntegrationError('Response missing expected fields', 'Bad Response', 400)];
            }
            return [res.data.records[0].Id, undefined];
        };
        this.instanceUrl = (0, sf_utils_1.validateInstanceURL)(instanceUrl);
        this.instanceUrl = this.instanceUrl.concat(instanceUrl.slice(-1) === '/' ? '' : '/');
        this.request = request;
    }
    async handleBulkJob(payloads, sobject, idField, operation, logging) {
        const csvStats = {
            shouldLog: logging?.shouldLog === true,
            numberOfColumns: 0,
            numberOfValuesInCSV: 0,
            numberOfNullsInCSV: 0
        };
        const csv = (0, sf_utils_1.buildCSVData)(payloads, idField, operation, csvStats);
        const jobId = await this.createBulkJob(sobject, idField, operation, logging);
        if (logging?.shouldLog === true) {
            const statsClient = logging?.stats?.statsClient;
            const tags = logging?.stats?.tags;
            tags?.push('jobId:' + jobId);
            statsClient?.incr('bulkCSV.payloadSize', payloads.length, tags);
            statsClient?.incr('bulkCSV.numberOfColumns', csvStats.numberOfColumns, tags);
            statsClient?.incr('bulkCSV.numberOfValuesInCSV', csvStats.numberOfValuesInCSV, tags);
            statsClient?.incr('bulkCSV.numberOfNullsInCSV', csvStats.numberOfNullsInCSV ?? 0, tags);
        }
        try {
            await this.uploadBulkCSV(jobId, csv, logging);
        }
        catch (err) {
            this.closeBulkJob(jobId, logging).catch((_) => {
                const message = err.response?.data[0]?.message || 'Failed to parse message';
                const code = err.response?.data[0]?.errorCode || 'Failed to parse code';
                const statsClient = logging?.stats?.statsClient;
                const tags = logging?.stats?.tags;
                tags?.push('jobId:' + jobId);
                statsClient?.incr('bulkJobError.caughUploadError', 1, tags);
                logging?.logger?.error(`Failed to close bulk job: ${jobId}. Message: ${message}. Code: ${code}`);
            });
            throw err;
        }
        try {
            return await this.closeBulkJob(jobId, logging);
        }
        catch (err) {
            const message = err.response?.data[0]?.message || 'Failed to parse message';
            const code = err.response?.data[0]?.errorCode || 'Failed to parse code';
            const statsClient = logging?.stats?.statsClient;
            const tags = logging?.stats?.tags;
            tags?.push('jobId:' + jobId);
            statsClient?.incr('bulkJobError', 1, tags);
            logging?.logger?.error(`Failed to close bulk job: ${jobId}. Message: ${message}. Code: ${code}`);
            throw err;
        }
    }
}
exports.default = Salesforce;
//# sourceMappingURL=sf-operations.js.map