@salesforce/source-deploy-retrieve
Version:
JavaScript library to run Salesforce metadata deploys and retrieves
353 lines • 24.7 kB
JavaScript
/*
* 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
;