@salesforce/source-deploy-retrieve
Version:
JavaScript library to run Salesforce metadata deploys and retrieves
268 lines • 19.6 kB
JavaScript
/*
* 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
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConnectionResolver = void 0;
const node_util_1 = require("node:util");
const core_1 = require("@salesforce/core");
const ts_types_1 = require("@salesforce/ts-types");
const kit_1 = require("@salesforce/kit");
const registryAccess_1 = require("../registry/registryAccess");
const standardvalueset_1 = require("../registry/standardvalueset");
const path_1 = require("../utils/path");
;
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 requestCount = 0;
let shouldQueryStandardValueSets = false;
let logger;
const getLogger = () => {
if (!logger) {
logger = core_1.Logger.childFromRoot('ConnectionResolver');
}
return logger;
};
// *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
//
// NOTE: The `listMetadata` API supports passing 3 metadata types per call but we
// can't do this because if 1 of the 3 types is not supported by the org (or
// errors in some way) we don't get any data back about the other types. This
// means we are forced to make listMetadata calls for individual metadata types.
//
// *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
/**
* Resolve MetadataComponents from an org connection by making listMetadata API calls
* for the specified metadata types (`mdTypes` arg) or all supported metadata types
* in the registry.
*/
class ConnectionResolver {
connection;
registry;
// Array of metadata type names to use for listMembers. By default it includes
// all types defined in the registry.
mdTypeNames;
requestBatchSize;
constructor(connection, registry = new registryAccess_1.RegistryAccess(), mdTypes) {
this.connection = connection;
this.registry = registry;
this.mdTypeNames = mdTypes?.length
? // ensure the types passed in are valid per the registry
mdTypes.filter((t) => this.registry.getTypeByName(t))
: Object.values(this.registry.getRegistry().types).map((t) => t.name);
// Always reset this. listMembers() function detects and sets it.
shouldQueryStandardValueSets = false;
// To limit the number of concurrent requests, batch them per an env var.
// Default is 500. From testing we saw jsforce gets stuck on ~1K reqs.
this.requestBatchSize = kit_1.env.getNumber('SF_LIST_METADATA_BATCH_SIZE', 500);
}
async resolve(componentFilter = (component) => (0, ts_types_1.isPlainObject)(component)) {
// Aggregate array of metadata records in the org
let aggregator = [];
// Folder component type names. Each array value has the form [type::folder]
const folderComponentTypes = [];
// Child component type names
const childComponentTypes = new Set();
const lifecycle = core_1.Lifecycle.getInstance();
// Make batched listMetadata requests for top level metadata
const listMetadataResponses = await this.sendBatchedRequests(this.mdTypeNames);
for (const component of listMetadataResponses) {
let componentType;
if (isNonEmptyString(component.type)) {
componentType = this.registry.getTypeByName(component.type);
}
else if (isNonEmptyString(component.fileName)) {
// fix { type: { "$": { "xsi:nil": "true" } } }
componentType = (0, ts_types_1.ensurePlainObject)(this.registry.getTypeBySuffix((0, path_1.extName)(component.fileName)), `No type found for ${component.fileName} when matching by suffix. Check the file extension.`);
component.type = componentType.name;
}
else if (!isNonEmptyString(component.type) && !isNonEmptyString(component.fileName)) {
// has no type and has no filename! Warn and skip that component.
// eslint-disable-next-line no-await-in-loop
await Promise.all([
lifecycle.emitWarning(messages.getMessage('error_could_not_infer_type', [component.fullName])),
lifecycle.emitTelemetry({ TypeInferenceError: component, from: 'ConnectionResolver' }),
]);
continue;
}
else {
// it DOES have all the important info but we couldn't resolve it.
// has no type and has no filename!
throw new core_1.SfError(messages.getMessage('error_could_not_infer_type', [component.fullName]), 'TypeInferenceError', [messages.getMessage('suggest_type_more_suggestions')]);
}
aggregator.push(component);
if (componentType.folderContentType) {
const type = this.registry.getTypeByName(componentType.folderContentType).name;
const folder = component.fullName;
folderComponentTypes.push(`${type}::${folder}`);
}
const childTypes = componentType.children?.types;
if (childTypes) {
Object.values(childTypes).map((childType) => childComponentTypes.add(childType.name));
}
}
if (folderComponentTypes.length) {
const folderFileProps = await this.sendBatchedRequests(folderComponentTypes);
aggregator = aggregator.concat(folderFileProps);
}
if (childComponentTypes.size > 0) {
const childComponentFileProps = await this.sendBatchedRequests(Array.from(childComponentTypes));
aggregator = aggregator.concat(childComponentFileProps);
}
// If we need to query the list of StandardValueSets (i.e., it's included in this.mdTypeNames)
// make those requests now.
if (shouldQueryStandardValueSets) {
const svsFileProps = await this.sendBatchedQueries();
aggregator = aggregator.concat(svsFileProps);
}
getLogger().debug(`https request count = ${requestCount}`);
return {
components: aggregator.filter(componentFilter).map((component) => ({
fullName: (0, ts_types_1.ensureString)(component.fullName, `Component fullName was not set for ${component.fileName ?? '<missing filename>'}`),
type: this.registry.getTypeByName((0, ts_types_1.ensureString)(component.type, `Component type was not set for ${component.fullName ?? '<missing fullname>'} (${component.fileName ?? '<missing filename>'})`)),
})),
apiVersion: this.connection.getApiVersion(),
};
}
// Send batched listMetadata requests based on the SF_LIST_METADATA_BATCH_SIZE env var.
async sendBatchedRequests(listMdQueries) {
let listMetadataResponses = [];
let listMetadataRequests = [];
const sendIt = async () => {
const requestBatch = (await Promise.all(listMetadataRequests)).flat();
listMetadataResponses = listMetadataResponses.concat(requestBatch);
};
// Make batched listMetadata requests
for (let i = 0; i < listMdQueries.length;) {
const q = listMdQueries[i].split('::');
const listMdQuery = { type: q[0] };
if (q[1]) {
listMdQuery.folder = q[1];
}
listMetadataRequests.push(listMembers(this.registry, this.connection, listMdQuery));
i++;
if (this.requestBatchSize > 0 && i % this.requestBatchSize === 0) {
getLogger().debug(`Awaiting listMetadata requests ${i - this.requestBatchSize + 1} - ${i}`);
// We are deliberately awaiting the results of batches to throttle requests.
// eslint-disable-next-line no-await-in-loop
await sendIt();
// Reset the requests for the next batch
listMetadataRequests = [];
}
// Always flush the last batch; or send non-batched requests
if (i === listMdQueries.length) {
getLogger().debug('Awaiting listMetadata requests');
// We are deliberately awaiting the results of batches to throttle requests.
// eslint-disable-next-line no-await-in-loop
await sendIt();
}
}
return listMetadataResponses;
}
// Send batched queries for a known subset of StandardValueSets based on the
// SF_LIST_METADATA_BATCH_SIZE env var.
async sendBatchedQueries() {
const mdType = this.registry.getTypeByName('StandardValueSet');
let queryResponses = [];
let queryRequests = [];
const sendIt = async () => {
const requestBatch = (await Promise.all(queryRequests)).flat();
queryResponses = queryResponses.concat(requestBatch.filter((rb) => !!rb));
};
// Make batched query requests
const svsNames = standardvalueset_1.standardValueSet.fullnames;
for (let i = 0; i < svsNames.length;) {
const svsFullName = svsNames[i];
queryRequests.push(querySvs(this.connection)(svsFullName, mdType));
i++;
if (this.requestBatchSize > 0 && i % this.requestBatchSize === 0) {
getLogger().debug(`Awaiting StandardValueSet queries ${i - this.requestBatchSize + 1} - ${i}`);
// We are deliberately awaiting the results of batches to throttle requests.
// eslint-disable-next-line no-await-in-loop
await sendIt();
// Reset the requests for the next batch
queryRequests = [];
}
// Always flush the last batch; or send non-batched requests
if (i === svsNames.length) {
getLogger().debug('Awaiting StandardValueSet queries');
// We are deliberately awaiting the results of batches to throttle requests.
// eslint-disable-next-line no-await-in-loop
await sendIt();
}
}
return queryResponses;
}
}
exports.ConnectionResolver = ConnectionResolver;
const querySvs = (connection) => async (svsFullName, svsType) => {
try {
requestCount++;
getLogger().debug(`StandardValueSet query for ${svsFullName}`);
const standardValueSetRecord = await connection.singleRecordQuery(`SELECT Id, MasterLabel, Metadata FROM StandardValueSet WHERE MasterLabel = '${svsFullName}'`, { tooling: true });
if (standardValueSetRecord.Metadata.standardValue.length) {
return {
fullName: standardValueSetRecord.MasterLabel,
fileName: `${svsType.directoryName}/${standardValueSetRecord.MasterLabel}.${svsType.suffix ?? ''}`,
type: svsType.name,
};
}
}
catch (error) {
const err = core_1.SfError.wrap(error);
getLogger().debug(`[${svsFullName}] ${err.message}`);
}
};
async function listMembers(registry, connection, query) {
const mdType = registry.getTypeByName(query.type);
// Workaround because metadata.list({ type: 'StandardValueSet' }) returns [].
// Query for a subset of known StandardValueSets after all listMetadata calls.
if (mdType.name === registry.getRegistry().types.standardvalueset.name) {
shouldQueryStandardValueSets = true;
return [];
}
// Workaround because metadata.list({ type: 'BotVersion' }) returns [].
if (mdType.name === 'BotVersion') {
try {
const botDefQuery = 'SELECT Id, DeveloperName FROM BotDefinition';
const botVersionQuery = 'SELECT BotDefinitionId, DeveloperName FROM BotVersion';
const botDefs = (await connection.query(botDefQuery)).records;
const botVersionDefs = (await connection.query(botVersionQuery)).records;
return botVersionDefs
.map((bvd) => {
const botName = botDefs.find((bd) => bd.Id === bvd.BotDefinitionId)?.DeveloperName;
if (botName) {
return {
fullName: `${botName}.${bvd.DeveloperName}`,
fileName: `bots/${bvd.DeveloperName}.botVersion`,
type: 'BotVersion',
};
}
})
.filter((b) => !!b);
}
catch (error) {
const err = core_1.SfError.wrap(error);
getLogger().debug(`[${mdType.name}] ${err.message}`);
return [];
}
}
try {
requestCount++;
getLogger().debug(`listMetadata for ${(0, node_util_1.inspect)(query)}`);
return (await connection.metadata.list(query)).map(inferFilenamesFromType(mdType));
}
catch (error) {
const err = core_1.SfError.wrap(error);
getLogger().debug(`[${mdType.name}] ${err.message}`);
return [];
}
}
/* if the Metadata Type doesn't return a correct fileName then help it out */
const inferFilenamesFromType = (metadataType) => (member) => typeof member.fileName === 'object' && metadataType.suffix
? { ...member, fileName: `${metadataType.directoryName}/${member.fullName}.${metadataType.suffix}` }
: member;
const isNonEmptyString = (value) => typeof value === 'string' && value.length > 0;
//# sourceMappingURL=connectionResolver.js.map
;