@salesforce/source-deploy-retrieve
Version:
JavaScript library to run Salesforce metadata deploys and retrieves
280 lines • 18.2 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.calculatePollingFrequency = exports.normalizePollingInputs = exports.MetadataTransfer = void 0;
/*
* Copyright (c) 2021, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
const node_events_1 = require("node:events");
const node_path_1 = require("node:path");
const core_1 = require("@salesforce/core");
const kit_1 = require("@salesforce/kit");
const ts_types_1 = require("@salesforce/ts-types");
const graceful_fs_1 = __importDefault(require("graceful-fs"));
const metadataConverter_1 = require("../convert/metadataConverter");
const componentSet_1 = require("../collections/componentSet");
const types_1 = require("./types");
;
const messages = new core_1.Messages('@salesforce/source-deploy-retrieve', 'sdr', new Map([["md_request_fail", "Metadata API request failed: %s"], ["error_convert_invalid_format", "Invalid conversion format '%s'"], ["error_could_not_infer_type", "%s: Could not infer a metadata type"], ["error_unexpected_child_type", "Unexpected child metadata [%s] found for parent type [%s]"], ["noParent", "Could not find parent type for %s (%s)"], ["error_expected_source_files", "%s: Expected source files for type '%s'"], ["error_failed_convert", "Component conversion failed: %s"], ["error_merge_metadata_target_unsupported", "Merge convert for metadata target format currently unsupported"], ["error_missing_adapter", "Missing adapter '%s' for metadata type '%s'"], ["error_missing_transformer", "Missing transformer '%s' for metadata type '%s'"], ["error_missing_type_definition", "Missing metadata type definition in registry for id '%s'."], ["error_missing_child_type_definition", "Type %s does not have a child type definition %s."], ["noChildTypes", "No child types found in registry for %s (reading %s at %s)"], ["error_no_metadata_xml_ignore", "Metadata xml file %s is forceignored but is required for %s."], ["noSourceIgnore", "%s metadata types require source files, but %s is forceignored."], ["noSourceIgnore.actions", "- Metadata types with content are composed of two files: a content file (ie MyApexClass.cls) and a -meta.xml file (i.e MyApexClass.cls-meta.xml). You must include both files in your .forceignore file. Or try appending \u201C\\*\u201D to your existing .forceignore entry.\n\nSee <https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_exclude_source.htm> for examples"], ["error_path_not_found", "%s: File or folder not found"], ["noContentFound", "SourceComponent %s (metadata type = %s) is missing its content file."], ["noContentFound.actions", ["Ensure the content file exists in the expected location.", "If the content file is in your .forceignore file, ensure the meta-xml file is also ignored to completely exclude it."]], ["error_parsing_xml", "SourceComponent %s (metadata type = %s) does not have an associated metadata xml to parse"], ["error_expected_file_path", "%s: path is to a directory, expected a file"], ["error_expected_directory_path", "%s: path is to a file, expected a directory"], ["error_directory_not_found_or_not_directory", "%s: path is not a directory"], ["error_no_directory_stream", "%s doesn't support readable streams on directories."], ["error_no_source_to_deploy", "No source-backed components present in the package."], ["error_no_components_to_retrieve", "No components in the package to retrieve."], ["error_static_resource_expected_archive_type", "A StaticResource directory must have a content type of application/zip or application/jar - found %s for %s."], ["error_static_resource_missing_resource_file", "A StaticResource must have an associated .resource file, missing %s.resource-meta.xml"], ["error_no_job_id", "The %s operation is missing a job ID. Initialize an operation with an ID, or start a new job."], ["missingApiVersion", "Could not determine an API version to use for the generated manifest. Tried looking for sourceApiVersion in sfdx-project.json, apiVersion from config vars, and the highest apiVersion from the APEX REST endpoint. Using API version 58.0 as a last resort."], ["invalid_xml_parsing", "error parsing %s due to:\\n message: %s\\n line: %s\\n code: %s"], ["zipBufferError", "Zip buffer was not created during conversion"], ["undefinedComponentSet", "Unable to construct a componentSet. Check the logs for more information."], ["replacementsFileNotRead", "The file \"%s\" specified in the \"replacements\" property of sfdx-project.json could not be read."], ["unsupportedBundleType", "Unsupported Bundle Type: %s"], ["filePathGeneratorNoTypeSupport", "Type not supported for filepath generation: %s"], ["missingFolderType", "The registry has %s as is inFolder but it does not have a folderType"], ["tooManyFiles", "Multiple files found for path: %s."], ["cantGetName", "Unable to calculate fullName from path: %s (%s)"], ["missingMetaFileSuffix", "The metadata registry is configured incorrectly for %s. Expected a metaFileSuffix."], ["uniqueIdElementNotInRegistry", "No uniqueIdElement found in registry for %s (reading %s at %s)."], ["uniqueIdElementNotInChild", "The uniqueIdElement %s was not found the child (reading %s at %s)."], ["suggest_type_header", "A metadata type lookup for \"%s\" found the following close matches:"], ["suggest_type_did_you_mean", "-- Did you mean \".%s%s\" instead for the \"%s\" metadata type?"], ["suggest_type_more_suggestions", "Additional suggestions:\nConfirm the file name, extension, and directory names are correct. Validate against the registry at:\n<https://github.com/forcedotcom/source-deploy-retrieve/blob/main/src/registry/metadataRegistry.json>\n\nIf the type is not listed in the registry, check that it has Metadata API support via the Metadata Coverage Report:\n<https://developer.salesforce.com/docs/metadata-coverage>\n\nIf the type is available via Metadata API but not in the registry\n\n- Open an issue <https://github.com/forcedotcom/cli/issues>\n- Add the type via PR. Instructions: <https://github.com/forcedotcom/source-deploy-retrieve/blob/main/contributing/metadata.md>"], ["type_name_suggestions", "Confirm the metadata type name is correct. Validate against the registry at:\n<https://github.com/forcedotcom/source-deploy-retrieve/blob/main/src/registry/metadataRegistry.json>\n\nIf the type is not listed in the registry, check that it has Metadata API support via the Metadata Coverage Report:\n<https://developer.salesforce.com/docs/metadata-coverage>\n\nIf the type is available via Metadata API but not in the registry\n\n- Open an issue <https://github.com/forcedotcom/cli/issues>\n- Add the type via PR. Instructions: <https://github.com/forcedotcom/source-deploy-retrieve/blob/main/contributing/metadata.md>"]]));
class MetadataTransfer {
components;
logger;
canceled = false;
mdapiTempDir;
transferId;
event = new node_events_1.EventEmitter();
usernameOrConnection;
apiVersion;
constructor({ usernameOrConnection, components, apiVersion, id }) {
this.usernameOrConnection = usernameOrConnection;
this.components = components;
this.apiVersion = apiVersion;
this.transferId = id;
this.logger = core_1.Logger.childFromRoot(this.constructor.name);
this.mdapiTempDir = process.env.SF_MDAPI_TEMP_DIR;
}
// if you passed in an id, you don't have to worry about whether there'll be one if you ask for it
get id() {
return this.transferId;
}
/**
* Send the metadata transfer request to the org.
*
* @returns AsyncResult from the deploy or retrieve response.
*/
async start() {
this.canceled = false;
const asyncResult = await this.pre();
this.transferId = asyncResult.id;
this.logger.debug(`Started metadata transfer. ID = ${this.id ?? '<no id>'}`);
return asyncResult;
}
async pollStatus(frequencyOrOptions, timeout) {
const normalizedOptions = (0, exports.normalizePollingInputs)(frequencyOrOptions, timeout, sizeOfComponentSet(this.components));
const pollingClient = await core_1.PollingClient.create({
...normalizedOptions,
poll: this.poll.bind(this),
});
try {
this.logger.debug(`Polling for metadata transfer status. ID = ${this.id ?? '<no id>'}`);
this.logger.debug(`Polling frequency (ms): ${normalizedOptions.frequency.milliseconds}`);
this.logger.debug(`Polling timeout (min): ${normalizedOptions.timeout.minutes}`);
const completedMdapiStatus = (await pollingClient.subscribe());
const result = await this.post(completedMdapiStatus);
if (completedMdapiStatus.status === types_1.RequestStatus.Canceled) {
this.event.emit('cancel', completedMdapiStatus);
}
else {
this.event.emit('finish', result);
}
return result;
}
catch (e) {
const err = e;
const error = new core_1.SfError(messages.getMessage('md_request_fail', [err.message]), 'MetadataTransferError');
if (error.stack && err.stack) {
// append the original stack to this new error
error.stack += `\nDUE TO:\n${err.stack}`;
if (err instanceof core_1.SfError && err.data) {
// this keeps SfError data for failures in post deploy/retrieve.
error.setData({
id: this.id,
causeErrorData: error.data,
});
error.actions = err.actions;
}
else {
error.setData({
id: this.id,
});
}
}
if (this.event.listenerCount('error') === 0) {
throw error;
}
this.event.emit('error', error);
}
}
onUpdate(subscriber) {
this.event.on('update', subscriber);
}
onFinish(subscriber) {
this.event.on('finish', subscriber);
}
onCancel(subscriber) {
this.event.on('cancel', subscriber);
}
onError(subscriber) {
this.event.on('error', subscriber);
}
async maybeSaveTempDirectory(target, cs) {
if (this.mdapiTempDir) {
await core_1.Lifecycle.getInstance().emitWarning('The SF_MDAPI_TEMP_DIR environment variable is set, which may degrade performance');
this.logger.debug(`Converting metadata to: ${this.mdapiTempDir} because the SF_MDAPI_TEMP_DIR environment variable is set`);
try {
const source = cs ?? this.components ?? new componentSet_1.ComponentSet();
const outputDirectory = (0, node_path_1.join)(this.mdapiTempDir, target);
await new metadataConverter_1.MetadataConverter().convert(source, target, {
type: 'directory',
outputDirectory,
genUniqueDir: false,
});
if (target === 'source') {
// for source convert the package.xml isn't included so write it separately
await graceful_fs_1.default.promises.writeFile((0, node_path_1.join)(outputDirectory, 'package.xml'), await source.getPackageXml());
}
}
catch (e) {
this.logger.debug(e);
}
}
}
async getConnection() {
if (typeof this.usernameOrConnection === 'string') {
this.usernameOrConnection = await core_1.Connection.create({
authInfo: await core_1.AuthInfo.create({ username: this.usernameOrConnection }),
});
if (this.apiVersion && this.apiVersion !== this.usernameOrConnection.version) {
this.usernameOrConnection.setApiVersion(this.apiVersion);
this.logger.debug(`Overriding apiVersion to: ${this.apiVersion}`);
}
}
return getConnectionNoHigherThanOrgAllows(this.usernameOrConnection, this.apiVersion);
}
// eslint-disable-next-line class-methods-use-this
isRetryableError(error) {
if (!(error instanceof Error))
return false;
const retryableErrors = [
'ENOMEM',
'ETIMEDOUT',
'ENOTFOUND',
'ECONNRESET',
'socket hang up',
'connection timeout',
'INVALID_QUERY_LOCATOR',
'ERROR_HTTP_502',
'ERROR_HTTP_503',
'ERROR_HTTP_420',
'<h1>Bad Message 400</h1><pre>reason: Bad Request</pre>',
'Unable to complete the creation of the query cursor at this time',
'Failed while fetching query cursor data for this QueryLocator',
'Client network socket disconnected before secure TLS connection was established',
'Unexpected internal servlet state',
];
const isRetryable = (retryableNetworkError) => error.message.includes(retryableNetworkError) ||
('errorCode' in error && typeof error.errorCode === 'string' && error.errorCode.includes(retryableNetworkError));
return retryableErrors.some(isRetryable);
}
async poll() {
let completed = false;
let mdapiStatus;
if (this.canceled) {
// This only happens for a canceled retrieve. Canceled deploys are
// handled via checkStatus response.
if (!mdapiStatus) {
mdapiStatus = { id: this.id, success: false, done: true };
}
mdapiStatus.status = types_1.RequestStatus.Canceled;
completed = true;
this.canceled = false;
}
else {
try {
mdapiStatus = await this.checkStatus();
completed = mdapiStatus?.done;
if (!completed) {
this.event.emit('update', mdapiStatus);
}
}
catch (e) {
this.logger.error(e);
// tolerate a known mdapi problem 500/INVALID_CROSS_REFERENCE_KEY: invalid cross reference id
// that happens when request moves out of Pending
if (e instanceof Error && e.name === 'JsonParseError') {
this.logger.debug('Metadata API response not parseable', e);
await core_1.Lifecycle.getInstance().emitWarning('Metadata API response not parseable');
return { completed: false };
}
// tolerate intermittent network errors upto retry limit
if (this.isRetryableError(e)) {
this.logger.debug('Network error on the request', e);
await core_1.Lifecycle.getInstance().emitWarning('Network error occurred. Continuing to poll.');
return { completed: false };
}
throw e;
}
}
this.logger.debug(`MDAPI status update: ${mdapiStatus.status}`);
return { completed, payload: mdapiStatus };
}
}
exports.MetadataTransfer = MetadataTransfer;
let emitted = false;
/* prevent requests on apiVersions higher than the org supports */
const getConnectionNoHigherThanOrgAllows = async (conn, requestedVersion) => {
// uses a TTL cache, so mostly won't hit the server
const maxApiVersion = await conn.retrieveMaxApiVersion();
if (requestedVersion && parseInt(requestedVersion, 10) > parseInt(maxApiVersion, 10)) {
// the once function from kit wasn't working with this async method, manually create a "once" method for the warning
if (!emitted) {
await core_1.Lifecycle.getInstance().emitWarning(`The requested API version (${requestedVersion}) is higher than the org supports. Using ${maxApiVersion}.`);
emitted = true;
}
conn.setApiVersion(maxApiVersion);
}
return conn;
};
/** there's an options object OR 2 raw number param, there's defaults including freq based on the CS size */
const normalizePollingInputs = (frequencyOrOptions, timeout, componentSetSize = 0) => {
let pollingOptions = {
frequency: kit_1.Duration.milliseconds((0, exports.calculatePollingFrequency)(componentSetSize)),
timeout: kit_1.Duration.minutes(60),
};
if ((0, ts_types_1.isNumber)(frequencyOrOptions)) {
pollingOptions.frequency = kit_1.Duration.milliseconds(frequencyOrOptions);
}
else if (frequencyOrOptions !== undefined) {
pollingOptions = { ...pollingOptions, ...frequencyOrOptions };
}
if ((0, ts_types_1.isNumber)(timeout)) {
pollingOptions.timeout = kit_1.Duration.seconds(timeout);
}
// from the overloaded methods, there's a possibility frequency/timeout isn't set
// guarantee frequency and timeout are set
pollingOptions.frequency ??= kit_1.Duration.milliseconds((0, exports.calculatePollingFrequency)(componentSetSize));
pollingOptions.timeout ??= kit_1.Duration.minutes(60);
return pollingOptions;
};
exports.normalizePollingInputs = normalizePollingInputs;
/** yeah, there's a size property on CS. But we want the actual number of source components */
const sizeOfComponentSet = (cs) => cs?.getSourceComponents().toArray().length ?? 0;
/** based on the size of the components, pick a reasonable polling frequency */
const calculatePollingFrequency = (size) => {
if (size === 0) {
// no component set size is possible for retrieve
return 1000;
}
else if (size <= 10) {
return 100;
}
else if (size <= 50) {
return 250;
}
else if (size <= 100) {
return 500;
}
else if (size <= 1000) {
return 1000;
}
else {
return size;
}
};
exports.calculatePollingFrequency = calculatePollingFrequency;
//# sourceMappingURL=metadataTransfer.js.map