@salesforce/source-deploy-retrieve
Version:
JavaScript library to run Salesforce metadata deploys and retrieves
263 lines • 18.5 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.MetadataApiRetrieve = exports.RetrieveResult = 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_path_1 = require("node:path");
const graceful_fs_1 = __importDefault(require("graceful-fs"));
const jszip_1 = __importDefault(require("jszip"));
const ts_types_1 = require("@salesforce/ts-types");
const core_1 = require("@salesforce/core");
const kit_1 = require("@salesforce/kit");
const componentSet_1 = require("../collections/componentSet");
const metadataTransfer_1 = require("./metadataTransfer");
const types_1 = require("./types");
const retrieveExtract_1 = require("./retrieveExtract");
const retrieveExtract_2 = require("./retrieveExtract");
;
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 RetrieveResult {
response;
components;
partialDeleteFileResponses;
// This ComponentSet is most likely just the components on the local file
// system and is used to set the state of a SourceComponent to "Created"
// rather than "Changed".
localComponents;
fileResponses;
/**
* @param response The metadata retrieve response from the server
* @param components The ComponentSet of retrieved source components
* @param localComponents The ComponentSet used to create the retrieve request
*/
constructor(response, components, localComponents, partialDeleteFileResponses = []) {
this.response = response;
this.components = components;
this.partialDeleteFileResponses = partialDeleteFileResponses;
this.localComponents = new componentSet_1.ComponentSet(localComponents?.getSourceComponents());
}
getFileResponses() {
if (this.response && this.fileResponses) {
return this.fileResponses;
}
this.fileResponses = [];
// construct failures
if (this.response.messages) {
const retrieveMessages = (0, kit_1.ensureArray)(this.response.messages);
for (const message of retrieveMessages) {
// match type name and fullname of problem component
const matches = new RegExp(/.+'(.+)'.+'(.+)'/).exec(message.problem);
if (matches) {
const [typeName, fullName] = matches.slice(1);
this.fileResponses.push({
fullName,
type: typeName,
state: types_1.ComponentStatus.Failed,
error: message.problem,
problemType: 'Error',
});
}
else {
this.fileResponses.push({
fullName: '',
type: '',
problemType: 'Error',
state: types_1.ComponentStatus.Failed,
error: message.problem,
});
}
}
}
// construct successes
for (const retrievedComponent of this.components.getSourceComponents()) {
const { fullName, type, xml } = retrievedComponent;
const baseResponse = {
fullName,
type: type.name,
state: this.localComponents.has(retrievedComponent) ? types_1.ComponentStatus.Changed : types_1.ComponentStatus.Created,
};
if (!type.children || Object.values(type.children.types).some((t) => t.unaddressableWithoutParent)) {
for (const filePath of retrievedComponent.walkContent()) {
this.fileResponses.push({ ...baseResponse, filePath });
}
}
if (xml) {
this.fileResponses.push({ ...baseResponse, filePath: xml });
}
}
// Add file responses for components that support partial delete (e.g., DigitalExperience)
// where pieces of the component were deleted in the org, then retrieved.
this.fileResponses.push(...(this.partialDeleteFileResponses ?? []));
return this.fileResponses;
}
}
exports.RetrieveResult = RetrieveResult;
class MetadataApiRetrieve extends metadataTransfer_1.MetadataTransfer {
static DEFAULT_OPTIONS = { merge: false };
options;
orgId;
constructor(options) {
super(options);
this.options = Object.assign({}, MetadataApiRetrieve.DEFAULT_OPTIONS, options);
if (this.mdapiTempDir) {
this.mdapiTempDir = (0, node_path_1.join)(this.mdapiTempDir, `${new Date().toISOString()}_retrieve`);
}
}
/**
* Check the status of the retrieve operation.
*
* @returns Status of the retrieve
*/
async checkStatus() {
if (!this.id) {
throw new core_1.SfError(messages.getMessage('error_no_job_id', ['retrieve']), 'MissingJobIdError');
}
const connection = await this.getConnection();
// Cast RetrieveResult returned by jsForce to MetadataApiRetrieveStatus
const status = (await connection.metadata.checkRetrieveStatus(this.id));
return {
...status,
// TODO: UT insist that this should NOT be an array
// fileProperties: ensureArray(status.fileProperties),
success: coerceBoolean(status.success),
done: coerceBoolean(status.done),
};
}
/**
* Cancel the retrieve operation.
*
* Canceling a retrieve occurs immediately and requires no additional status
* checks to the org, unlike {@link MetadataApiDeploy.cancel}.
*/
// eslint-disable-next-line @typescript-eslint/require-await
async cancel() {
this.canceled = true;
}
async post(result) {
let componentSet;
let partialDeleteFileResponses = [];
const isMdapiRetrieve = this.options.format === 'metadata';
if (result.status === types_1.RequestStatus.Succeeded) {
const zipFileContents = Buffer.from(result.zipFile, 'base64');
if (isMdapiRetrieve) {
await handleMdapiResponse(this.options, zipFileContents);
}
else {
// If mdapiTempDir is set, write the raw retrieve result to the temp dir
if (this.mdapiTempDir && zipFileContents) {
const outputDir = (0, node_path_1.join)(this.mdapiTempDir, 'metadata');
graceful_fs_1.default.mkdirSync(outputDir, { recursive: true });
const mdapiTempOptions = {
usernameOrConnection: this.options.usernameOrConnection,
output: outputDir,
unzip: true,
};
await handleMdapiResponse(mdapiTempOptions, zipFileContents);
}
({ componentSet, partialDeleteFileResponses } = await (0, retrieveExtract_1.extract)({
zip: zipFileContents,
options: this.options,
logger: this.logger,
mainComponents: this.components,
}));
}
}
componentSet ??= new componentSet_1.ComponentSet(undefined, this.options.registry);
const retrieveResult = new RetrieveResult(result, componentSet, this.components, partialDeleteFileResponses);
if (!isMdapiRetrieve && !this.options.suppressEvents) {
// This should only be done when retrieving source format since retrieving
// mdapi format has no conversion or events/hooks
await this.maybeSaveTempDirectory('source', componentSet);
await core_1.Lifecycle.getInstance().emit('scopedPostRetrieve', {
retrieveResult,
orgId: this.orgId,
});
}
return retrieveResult;
}
async pre() {
const packageNames = getPackageNames(this.options.packageOptions);
if (this.components?.size === 0 && !packageNames?.length) {
throw new core_1.SfError(messages.getMessage('error_no_components_to_retrieve'), 'MetadataApiRetrieveError');
}
const connection = await this.getConnection();
const apiVersion = connection.getApiVersion();
this.orgId = connection.getAuthInfoFields().orgId;
if (this.components) {
this.components.apiVersion ??= apiVersion;
this.components.sourceApiVersion ??= apiVersion;
}
// only do event hooks if source, (NOT a metadata format) retrieve
if (this.options.components && !this.options.suppressEvents) {
await core_1.Lifecycle.getInstance().emit('scopedPreRetrieve', {
componentSet: this.options.components,
orgId: this.orgId,
});
}
const manifestData = (await this.components?.getObject())?.Package;
const requestBody = {
// This apiVersion is only used when the version in the package.xml (manifestData) is not defined.
// see docs here: https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/meta_retrieve_request.htm
apiVersion: this.components?.sourceApiVersion ?? (await connection.retrieveMaxApiVersion()),
...(manifestData ? { unpackaged: manifestData } : {}),
...(this.options.singlePackage ? { singlePackage: this.options.singlePackage } : {}),
// if we're retrieving with packageNames add it
// otherwise don't - it causes errors if undefined or an empty array
...(packageNames.length ? { packageNames } : {}),
};
if (packageNames?.length && this.options.format === 'metadata' && this.components?.size === 0) {
// delete unpackaged when no components and metadata format to prevent
// sending an empty unpackaged manifest.
delete requestBody.unpackaged;
}
// Debug output for API version used for retrieve
const manifestVersion = manifestData?.version;
if (manifestVersion) {
this.logger.debug(`Retrieving source in v${manifestVersion} shape using SOAP v${apiVersion}`);
await core_1.Lifecycle.getInstance().emit('apiVersionRetrieve', { manifestVersion, apiVersion });
}
// TODO: are the jsforce types wrong? ApiVersion string vs. number
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore required callback
return connection.metadata.retrieve(requestBody);
}
}
exports.MetadataApiRetrieve = MetadataApiRetrieve;
const handleMdapiResponse = async (options, zipFileContents) => {
const name = options.zipFileName ?? 'unpackaged.zip';
const zipFilePath = (0, node_path_1.join)(options.output, name);
graceful_fs_1.default.writeFileSync(zipFilePath, zipFileContents);
if (options.unzip) {
const zip = await jszip_1.default.loadAsync(zipFileContents, { base64: true, createFolders: true });
const extractPath = (0, node_path_1.join)(options.output, (0, node_path_1.parse)(name).name);
graceful_fs_1.default.mkdirSync(extractPath, { recursive: true });
for (const filePath of Object.keys(zip.files)) {
const zipObj = zip.file(filePath);
if (!zipObj || zipObj?.dir) {
graceful_fs_1.default.mkdirSync((0, node_path_1.join)(extractPath, filePath), { recursive: true });
}
else {
// eslint-disable-next-line no-await-in-loop
const content = await zipObj?.async('nodebuffer');
if (content) {
graceful_fs_1.default.writeFileSync((0, node_path_1.join)(extractPath, filePath), content);
}
}
}
}
};
const coerceBoolean = (field) => {
if ((0, ts_types_1.isString)(field)) {
return field.toLowerCase() === 'true';
}
return (0, ts_types_1.asBoolean)(field, false);
};
const getPackageNames = (packageOptions) => (0, retrieveExtract_2.getPackageOptions)(packageOptions)?.map((pkg) => pkg.name);
//# sourceMappingURL=metadataApiRetrieve.js.map