UNPKG

@salesforce/source-deploy-retrieve

Version:

JavaScript library to run Salesforce metadata deploys and retrieves

263 lines 18.5 kB
"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