UNPKG

@salesforce/source-deploy-retrieve

Version:

JavaScript library to run Salesforce metadata deploys and retrieves

353 lines 24.7 kB
"use strict"; /* * Copyright (c) 2020, 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 */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.entryToTypeAndName = exports.ComponentSetBuilder = void 0; const path = __importStar(require("node:path")); const core_1 = require("@salesforce/core"); const graceful_fs_1 = __importDefault(require("graceful-fs")); const minimatch_1 = require("minimatch"); const sourceComponent_1 = require("../resolve/sourceComponent"); const componentSet_1 = require("../collections/componentSet"); const registryAccess_1 = require("../registry/registryAccess"); const resolve_1 = require("../resolve"); const agentResolver_1 = require("../resolve/pseudoTypes/agentResolver"); 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>"]])); let logger; const getLogger = () => { if (!logger) { logger = core_1.Logger.childFromRoot('ComponentSetBuilder'); } return logger; }; const PSEUDO_TYPES = { AGENT: 'Agent' }; class ComponentSetBuilder { /** * Builds a ComponentSet that can be used for source conversion, * deployment, or retrieval, using all specified options. * * @see https://github.com/forcedotcom/source-deploy-retrieve/blob/develop/src/collections/componentSet.ts * * @param options: options for creating a ComponentSet */ static async build(options) { let componentSet; const { sourcepath, manifest, metadata, packagenames, org } = options; const registry = new registryAccess_1.RegistryAccess(undefined, options.projectDir); if (sourcepath?.length) { getLogger().debug(`Building ComponentSet from sourcepath: ${sourcepath.join(', ')}`); const fsPaths = sourcepath.map(validateAndResolvePath); componentSet = componentSet_1.ComponentSet.fromSource({ fsPaths, registry, }); if (metadata?.excludedEntries?.length) { const toRemove = metadata.excludedEntries .map((0, exports.entryToTypeAndName)(registry)) .flatMap(typeAndNameToMetadataComponents({ directoryPaths: fsPaths, registry })); componentSet = componentSet.filter((md) => !toRemove.some((n) => n.type.name === md.type.name && (n.fullName === md.fullName || n.fullName === '*'))); } if (metadata?.metadataEntries?.length) { const toKeep = metadata.metadataEntries .map((0, exports.entryToTypeAndName)(registry)) .flatMap(typeAndNameToMetadataComponents({ directoryPaths: fsPaths, registry })); componentSet = componentSet.filter((md) => toKeep.some((n) => n.type.name === md.type.name && (n.fullName === md.fullName || n.fullName === '*'))); } } // Return empty ComponentSet and use packageNames in the connection via `.retrieve` options if (packagenames) { getLogger().debug(`Building ComponentSet for packagenames: ${packagenames.toString()}`); componentSet ??= new componentSet_1.ComponentSet(undefined, registry); } // Resolve manifest with source in package directories. if (manifest) { getLogger().debug(`Building ComponentSet from manifest: ${manifest.manifestPath}`); assertFileExists(manifest.manifestPath); getLogger().debug(`Searching in packageDir: ${manifest.directoryPaths.join(', ')} for matching metadata`); componentSet = await componentSet_1.ComponentSet.fromManifest({ manifestPath: manifest.manifestPath, resolveSourcePaths: manifest.directoryPaths, forceAddWildcards: true, destructivePre: manifest.destructiveChangesPre, destructivePost: manifest.destructiveChangesPost, registry, }); } // Resolve metadata entries with source in package directories, unless we are building a ComponentSet // from metadata in an org. if (metadata && !org && !sourcepath?.length) { getLogger().debug(`Building ComponentSet from metadata: ${metadata.metadataEntries.toString()}`); const directoryPaths = metadata.directoryPaths; componentSet ??= new componentSet_1.ComponentSet(undefined, registry); const componentSetFilter = new componentSet_1.ComponentSet(undefined, registry); // If pseudo types were passed without an org option replace the pseudo types with // "client side spidering" metadata.metadataEntries = await replacePseudoTypes({ mdOption: metadata, registry }); // Build a Set of metadata entries metadata.metadataEntries .map((0, exports.entryToTypeAndName)(registry)) .flatMap(typeAndNameToMetadataComponents({ directoryPaths, registry })) .map(addToComponentSet(componentSet)) .map(addToComponentSet(componentSetFilter)); getLogger().debug(`Searching for matching metadata in directories: ${directoryPaths.join(', ')}`); // add destructive changes if defined. Because these are deletes, all entries // are resolved to SourceComponents if (metadata.destructiveEntriesPre) { metadata.destructiveEntriesPre .map((0, exports.entryToTypeAndName)(registry)) .map(assertNoWildcardInDestructiveEntries) .flatMap(typeAndNameToMetadataComponents({ directoryPaths, registry })) .map((mdComponent) => new sourceComponent_1.SourceComponent({ type: mdComponent.type, name: mdComponent.fullName })) .map(addToComponentSet(componentSet, types_1.DestructiveChangesType.PRE)); } if (metadata.destructiveEntriesPost) { metadata.destructiveEntriesPost .map((0, exports.entryToTypeAndName)(registry)) .map(assertNoWildcardInDestructiveEntries) .flatMap(typeAndNameToMetadataComponents({ directoryPaths, registry })) .map((mdComponent) => new sourceComponent_1.SourceComponent({ type: mdComponent.type, name: mdComponent.fullName })) .map(addToComponentSet(componentSet, types_1.DestructiveChangesType.POST)); } const resolvedComponents = componentSet_1.ComponentSet.fromSource({ fsPaths: directoryPaths, include: componentSetFilter, registry, }); if (resolvedComponents.forceIgnoredPaths) { // if useFsForceIgnore = true, then we won't be able to resolve a forceignored path, // which we need to do to get the ignored source component const resolver = new resolve_1.MetadataResolver(registry, undefined, false); for (const ignoredPath of resolvedComponents.forceIgnoredPaths ?? []) { resolver.getComponentsFromPath(ignoredPath).map((ignored) => { componentSet = componentSet?.filter((resolved) => !(resolved.fullName === ignored.name && resolved.type === ignored.type)); }); } componentSet.forceIgnoredPaths = resolvedComponents.forceIgnoredPaths; } resolvedComponents.toArray().map(addToComponentSet(componentSet)); } // Resolve metadata entries with an org connection if (org) { componentSet ??= new componentSet_1.ComponentSet(undefined, registry); const orgComponentSet = await this.resolveOrgComponents(registry, options); orgComponentSet.toArray().map(addToComponentSet(componentSet)); } // there should have been a componentSet created by this point. componentSet = assertComponentSetIsNotUndefined(componentSet); componentSet.apiVersion ??= options.apiversion; componentSet.sourceApiVersion ??= options.sourceapiversion; componentSet.projectDirectory = options.projectDir; logComponents(componentSet); return componentSet; } static async resolveOrgComponents(registry, options) { // Get a connection from the OrgOption const { apiversion, org, metadata } = options; if (!org) { throw core_1.SfError.create({ message: 'ComponentSetBuilder.resolveOrgComponents() requires an OrgOption' }); } const username = (await core_1.StateAggregator.getInstance()).aliases.getUsername(org.username) ?? org.username; const connection = await core_1.Connection.create({ authInfo: await core_1.AuthInfo.create({ username }) }); if (apiversion) { connection.setApiVersion(apiversion); } let mdMap = new Map(); let debugMsg = `Building ComponentSet from metadata in an org using targetUsername: ${username}`; if (metadata) { if (metadata.metadataEntries?.length) { debugMsg += ` filtering on metadata: ${metadata.metadataEntries.toString()}`; // Replace pseudo-types from the metadataEntries metadata.metadataEntries = await replacePseudoTypes({ mdOption: metadata, connection, registry }); } if (metadata.excludedEntries?.length) { debugMsg += ` excluding metadata: ${metadata.excludedEntries.toString()}`; } mdMap = buildMapFromMetadata(metadata, registry); } getLogger().debug(debugMsg); return componentSet_1.ComponentSet.fromConnection({ usernameOrConnection: connection, componentFilter: getOrgComponentFilter(org, mdMap, metadata), metadataTypes: mdMap.size ? Array.from(mdMap.keys()) : undefined, registry, }); } } exports.ComponentSetBuilder = ComponentSetBuilder; const addToComponentSet = (cs, deletionType) => (cmp) => { cs.add(cmp, deletionType); return cmp; }; const validateAndResolvePath = (filepath) => path.resolve(assertFileExists(filepath)); const assertFileExists = (filepath) => { if (!graceful_fs_1.default.existsSync(filepath)) { throw new core_1.SfError(messages.getMessage('error_path_not_found', [filepath])); } return filepath; }; const assertComponentSetIsNotUndefined = (componentSet) => { if (componentSet === undefined) { throw new core_1.SfError('undefinedComponentSet'); } return componentSet; }; const assertNoWildcardInDestructiveEntries = (mdEntry) => { if (mdEntry.metadataName.includes('*')) { throw core_1.SfError.create({ message: 'Wildcards are not supported when providing destructive metadata entries' }); } return mdEntry; }; /** This is only for debug output of matched files based on the command flags. * It will log up to 20 file matches. */ const logComponents = (componentSet) => { getLogger().debug(`Matching metadata files (${componentSet.size}):`); const components = componentSet.getSourceComponents().toArray(); components .slice(0, 20) .map((cmp) => cmp.content ?? cmp.xml ?? cmp.fullName) .map((m) => getLogger().debug(m)); if (components.length > 20) getLogger().debug(`(showing 20 of ${componentSet.size} matches)`); getLogger().debug(`ComponentSet apiVersion = ${componentSet.apiVersion ?? '<not set>'}`); getLogger().debug(`ComponentSet sourceApiVersion = ${componentSet.sourceApiVersion ?? '<not set>'}`); }; const getOrgComponentFilter = (org, mdMap, metadata) => metadata?.metadataEntries?.length ? (component) => { if (component.type && component.fullName) { const mdMapEntry = mdMap.get(component.type); // using minimatch versus RegExp provides better (more expected) matching results return (!!mdMapEntry && mdMapEntry.some((mdName) => typeof component.fullName === 'string' && (0, minimatch_1.minimatch)(component.fullName, mdName))); } return false; } : // *** Default Filter *** // exclude components based on the results of componentFilter function // components with namespacePrefix where org.exclude includes manageableState (to exclude managed packages) // components with namespacePrefix where manageableState equals undefined (to exclude components e.g. InstalledPackage) // components where org.exclude includes manageableState (to exclude packages without namespacePrefix e.g. unlocked packages) (component) => !component?.manageableState || !org.exclude?.includes(component.manageableState); // The registry will throw if it doesn't know what this type is. const entryToTypeAndName = (reg) => (rawEntry) => { // split on the first colon, and then join the rest back together to support names that include colons const [typeName, ...name] = rawEntry.split(':'); const type = reg.getTypeByName(typeName.trim()); if (type.name === 'CustomLabels' && type.strategies?.transformer === 'decomposedLabels') { throw new Error('Use CustomLabel instead of CustomLabels for decomposed labels'); } return { type, metadataName: name.length ? name.join(':').trim() : '*' }; }; exports.entryToTypeAndName = entryToTypeAndName; const typeAndNameToMetadataComponents = (context) => ({ type, metadataName }) => // this '.*' is a surprisingly valid way to specify a metadata, especially a DEB :sigh: // https://github.com/salesforcecli/plugin-deploy-retrieve/blob/main/test/nuts/digitalExperienceBundle/constants.ts#L140 // because we're filtering from what we have locally, this won't allow you to retrieve new metadata (on the server only) using the partial wildcard // to do that, you'd need check the size of the CS created below, see if it's 0, and then query the org for the metadata that matches the regex // but building a CS from a metadata argument doesn't require an org, so we can't do that here metadataName?.includes('*') && metadataName.length > 1 && !metadataName.includes('.*') ? // get all components of the type, and then filter by the regex of the fullName componentSet_1.ComponentSet.fromSource({ fsPaths: context.directoryPaths, include: new componentSet_1.ComponentSet([{ type, fullName: componentSet_1.ComponentSet.WILDCARD }], context.registry), registry: context.registry, }) .getSourceComponents() .toArray() // using minimatch versus RegExp provides better (more expected) matching results .filter((cs) => (0, minimatch_1.minimatch)(cs.fullName, metadataName)) : [{ type, fullName: metadataName }]; const buildMapFromMetadata = (mdOption, registry) => { const mdMap = new Map(); // Add metadata type entries we were told to include if (mdOption.metadataEntries?.length) { mdOption.metadataEntries.map((0, exports.entryToTypeAndName)(registry)).map((cmp) => { mdMap.set(cmp.type.name, [...(mdMap.get(cmp.type.name) ?? []), cmp.metadataName]); }); } // Build an array of excluded types from the options if (mdOption.excludedEntries?.length) { const excludedTypes = []; mdOption.excludedEntries.map((0, exports.entryToTypeAndName)(registry)).map((cmp) => { if (cmp.metadataName === '*') { excludedTypes.push(cmp.type.name); } if (cmp.type.folderType) { excludedTypes.push(registry.getTypeByName(cmp.type.folderType).name); } }); if (mdMap.size === 0) { // we are excluding specific metadata types from all supported types Object.values(registry.getRegistry().types).map((t) => { if (!excludedTypes.includes(t.name)) { mdMap.set(t.name, []); } }); } } return mdMap; }; // Replace pseudo types with actual types. const replacePseudoTypes = async (pseudoTypeInfo) => { const { mdOption, connection, registry } = pseudoTypeInfo; const pseudoEntries = []; let replacedEntries = []; mdOption.metadataEntries.map((rawEntry) => { const [typeName, ...name] = rawEntry.split(':'); if (Object.values(PSEUDO_TYPES).includes(typeName)) { pseudoEntries.push([typeName, name.join(':').trim()]); } else { replacedEntries.push(rawEntry); } }); if (pseudoEntries.length) { await Promise.all(pseudoEntries.map(async (pseudoEntry) => { const pseudoType = pseudoEntry[0]; const pseudoName = pseudoEntry[1] || '*'; getLogger().debug(`Converting pseudo-type ${pseudoType}:${pseudoName}`); if (pseudoType === PSEUDO_TYPES.AGENT) { const agentMdEntries = await (0, agentResolver_1.resolveAgentMdEntries)({ botName: pseudoName, connection, directoryPaths: mdOption.directoryPaths, registry, }); replacedEntries = [...replacedEntries, ...agentMdEntries]; } })); } return replacedEntries; }; //# sourceMappingURL=componentSetBuilder.js.map