@salesforce/source-deploy-retrieve
Version:
JavaScript library to run Salesforce metadata deploys and retrieves
265 lines • 20.6 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 __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getOutputFile = exports.forceIgnoreAllowsComponent = exports.addChildType = exports.hasChildTypeId = exports.tagToChildTypeId = exports.setDecomposedState = exports.getWriteInfosWithoutMerge = exports.getWriteInfosFromMerge = exports.DecomposedMetadataTransformer = void 0;
const node_path_1 = require("node:path");
const node_fs_1 = __importDefault(require("node:fs"));
const ts_types_1 = require("@salesforce/ts-types");
const kit_1 = require("@salesforce/kit");
const core_1 = require("@salesforce/core");
const path_1 = require("../../utils/path");
const decomposed_1 = require("../../utils/decomposed");
const streams_1 = require("../streams");
const constants_1 = require("../../common/constants");
const componentSet_1 = require("../../collections/componentSet");
const baseMetadataTransformer_1 = require("./baseMetadataTransformer");
;
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 DecomposedMetadataTransformer extends baseMetadataTransformer_1.BaseMetadataTransformer {
// eslint-disable-next-line @typescript-eslint/require-await
async toMetadataFormat(component) {
if (component.parent) {
const key = (0, componentSet_1.simpleKey)(component.parent);
const stateForParent = this.context.recomposition.transactionState.get(key) ?? {
component: component.parent,
children: new componentSet_1.ComponentSet([], this.registry),
};
stateForParent.children?.add(component);
this.context.recomposition.transactionState.set(key, stateForParent);
}
else {
const key = (0, componentSet_1.simpleKey)(component);
const existing = this.context.recomposition.transactionState.get(key) ?? {
component,
children: new componentSet_1.ComponentSet([], this.registry),
};
if (component.xml && existing.component && !existing.component.xml) {
// we've already found and created the parent of this component on L~38
// but now we have more information about the parent (xml) that we didn't have before, so add it
existing.component = component;
}
(component.getChildren() ?? []).map((child) => {
existing.children?.add(child);
});
this.context.recomposition.transactionState.set(key, existing);
}
// noop since the finalizer will push the writes to the component writer
return [];
}
async toSourceFormat({ component, mergeWith }) {
const forceIgnore = component.getForceIgnore();
// if the whole parent is ignored, we won't worry about decomposing things
// this can happen if the manifest had a *; all the members will be retrieved.
if (forceIgnore.denies((0, exports.getOutputFile)(component, mergeWith))) {
return [];
}
const childrenOfMergeComponent = new componentSet_1.ComponentSet(mergeWith?.getChildren(), this.registry);
const composedMetadata = await getComposedMetadataEntries(component);
const parentXmlObject = buildParentXml(component.type)(composedMetadata);
const stateSetter = (0, exports.setDecomposedState)(this.context.decomposition.transactionState);
const writeInfosForChildren = composedMetadata
.filter(exports.hasChildTypeId)
.map(exports.addChildType)
.flatMap(({ tagValue, childType }) =>
// iterate each array member if it's Object-like (ex: customField of a CustomObject)
(0, kit_1.ensureArray)(tagValue)
.filter(ts_types_1.isJsonMap)
.map(toInfoContainer(mergeWith)(component)(childType))
.filter((0, exports.forceIgnoreAllowsComponent)(forceIgnore)) // only process child types that aren't forceignored
.map(handleUnaddressableChildAlone(composedMetadata.length)(parentXmlObject)(stateSetter))
.flatMap(getChildWriteInfos(stateSetter)(childrenOfMergeComponent)));
const writeInfoForParent = mergeWith
? (0, exports.getWriteInfosFromMerge)(mergeWith)(stateSetter)(parentXmlObject)(component)
: (0, exports.getWriteInfosWithoutMerge)(this.defaultDirectory)(parentXmlObject)(component);
const childDestinations = new Set(writeInfosForChildren.map((w) => w.output));
// files that exist in FS (therefore, in mergeWith) but aren't in the component should be deleted by returning a writeInfo
// only do this if all the children have isAddressable marked false
const writeInfosForMissingChildrenToDelete = mergeWith && allChildrenAreUnaddressable(component.type)
? childrenOfMergeComponent
.getSourceComponents()
.toArray()
.filter(hasXml)
.filter((c) => !childDestinations.has(c.xml))
.map((c) => ({ shouldDelete: true, output: c.xml, fullName: c.fullName, type: c.type.name }))
: [];
return [...writeInfosForChildren, ...writeInfoForParent, ...writeInfosForMissingChildrenToDelete];
}
}
exports.DecomposedMetadataTransformer = DecomposedMetadataTransformer;
const hasXml = (c) => typeof c.xml === 'string';
const allChildrenAreUnaddressable = (type) => Object.values(type.children?.types ?? {}).every(
// exclude the COFT (unaddressableWithoutParent) from being deleted because its absence *might* not mean it was deleted from the org
(c) => c.isAddressable === false && c.unaddressableWithoutParent !== true);
/**
* composedMetadata is a representation of the parent's xml
*
* if there is no CustomObjectTranslation in the org, the composedMetadata will be 2 entries
* the xml declaration, and a fields attribute, which points to the child CustomObjectFieldTranslation
*
* because CustomObjectFieldTranslation is the only metadata type with 'requiresParent' = true we can
* calculate if a CustomObjectTranslation was retrieved from the org (composedMetadata.length > 2), or,
* if we'll have to write an empty CustomObjectTranslation file (composedMetadata.length <=2).
*
* CustomObjectFieldTranslations are only addressable through their parent, and require a
* CustomObjectTranslation file to be present
*/
const handleUnaddressableChildAlone = (composedMetadataLength) => (parentXmlObject) => (stateSetter) => (v) => {
if (v.childComponent.type.unaddressableWithoutParent && composedMetadataLength <= 2) {
stateSetter(v.childComponent, {
writeInfo: {
source: new streams_1.JsToXml(parentXmlObject),
output: getDefaultOutput(v.parentComponent),
},
});
}
return v;
};
const getChildWriteInfos = (stateSetter) => (childrenOfMergeComponent) => ({ mergeWith, childComponent, value, entryName }) => {
const source = objectToSource(childComponent.type.name)(value);
// if there's nothing to merge with, push write operation now to default location
if (!mergeWith) {
return [{ source, output: getDefaultOutput(childComponent) }];
}
// if the merge parent has a child that can be merged with, push write
// operation now and mark it as merged in the state
if (childrenOfMergeComponent.has(childComponent)) {
const mergeChild = childrenOfMergeComponent.getSourceComponents(childComponent).first();
if (!mergeChild?.xml) {
throw messages.createError('error_parsing_xml', [childComponent.fullName, childComponent.type.name]);
}
stateSetter(childComponent, { foundMerge: true });
return [{ source, output: mergeChild.xml }];
}
// If we have a parent and the child is unaddressable without the parent, keep them
// together on the file system, meaning a new child will not be written to the default dir.
if (childComponent.type.unaddressableWithoutParent && typeof mergeWith?.xml === 'string') {
// get output path from parent
return [
{
source,
output: (0, node_path_1.join)((0, node_path_1.dirname)(mergeWith.xml), `${entryName}.${(0, ts_types_1.ensureString)(childComponent.type.suffix)}${constants_1.META_XML_SUFFIX}`),
},
];
}
// we didn't find a merge, so we add it to the state for later processing
stateSetter(childComponent, {
writeInfo: { source, output: getDefaultOutput(childComponent) },
});
return [];
};
const getWriteInfosFromMerge = (mergeWith) => (stateSetter) => (parentXmlObject) => (parentComponent) => {
const writeInfo = { source: new streams_1.JsToXml(parentXmlObject), output: (0, exports.getOutputFile)(parentComponent, mergeWith) };
const parentHasRealValues = (0, decomposed_1.objectHasSomeRealValues)(parentComponent.type)(parentXmlObject);
if (mergeWith?.xml) {
// mark the component as found
stateSetter(parentComponent, { foundMerge: true });
return (0, decomposed_1.objectHasSomeRealValues)(parentComponent.type)(mergeWith.parseXmlSync()) && !parentHasRealValues
? [] // the target file has values but this process doesn't, so we don't want to overwrite it
: [writeInfo];
}
if ((0, decomposed_1.objectHasSomeRealValues)(parentComponent.type)(parentXmlObject)) {
// set the state but don't return any writeInfo to avoid writing "empty" (ns-only) parent files
stateSetter(parentComponent, { writeInfo });
}
return [];
};
exports.getWriteInfosFromMerge = getWriteInfosFromMerge;
const getWriteInfosWithoutMerge = (defaultDirectory) => (parentXmlObject) => (component) => {
const output = (0, exports.getOutputFile)(component);
// if the parent would be empty
// and it exists
// and every child is addressable
// don't overwrite the existing parent
if (!(0, decomposed_1.objectHasSomeRealValues)(component.type)(parentXmlObject) &&
node_fs_1.default.existsSync((0, node_path_1.join)(defaultDirectory ?? '', output)) &&
Object.values(component.type.children ?? {}).every((child) => !child.isAddressable)) {
return [];
}
else {
return [{ source: new streams_1.JsToXml(parentXmlObject), output }];
}
};
exports.getWriteInfosWithoutMerge = getWriteInfosWithoutMerge;
/**
* Helper for setting the decomposed transaction state
*
* @param state
*/
const setDecomposedState = (state) => (forComponent, props) => {
const key = (0, componentSet_1.simpleKey)(forComponent);
state.set(key, {
// origin gets set the first time
...(state.get(key) ?? { origin: forComponent.parent ?? forComponent }),
...(props ?? {}),
});
};
exports.setDecomposedState = setDecomposedState;
/** for a component, parse the xml and create an json object with contents, child typeId, etc */
const getComposedMetadataEntries = async (component) =>
// composedMetadata might be undefined if you call toSourceFormat() from a non-source-backed Component
Object.entries((await component.parseXml())[component.type.name] ?? {}).map(([tagKey, tagValue]) => ({
tagKey,
tagValue,
parentType: component.type,
childTypeId: (0, exports.tagToChildTypeId)({ tagKey, type: component.type }),
}));
/** where the file goes if there's nothing to merge with */
const getDefaultOutput = (component) => {
const { parent, fullName, type } = component;
const [baseName, ...tail] = fullName.split('.');
// there could be a '.' inside the child name (ex: PermissionSet.FieldPermissions.field uses Obj__c.Field__c)
const childName = tail.length ? tail.join('.') : undefined;
const output = (0, node_path_1.join)(parent?.type.strategies?.decomposition === 'folderPerType' ? type.directoryName : '', `${childName ?? baseName}.${(0, ts_types_1.ensureString)(component.type.suffix)}${constants_1.META_XML_SUFFIX}`);
return (0, node_path_1.join)((0, path_1.calculateRelativePath)('source')({ self: parent?.type ?? type })(fullName)(baseName), output);
};
/** use the given xmlElementName name if it exists, otherwise use see if one matches the directories */
const tagToChildTypeId = ({ tagKey, type }) => Object.values(type.children?.types ?? {}).find((c) => c.xmlElementName === tagKey)?.id ??
type.children?.directories?.[tagKey];
exports.tagToChildTypeId = tagToChildTypeId;
const hasChildTypeId = (cm) => !!cm.childTypeId;
exports.hasChildTypeId = hasChildTypeId;
const addChildType = (cm) => {
const childType = cm.parentType.children?.types[cm.childTypeId];
if (childType) {
return { ...cm, childType };
}
throw messages.createError('error_missing_child_type_definition', [cm.parentType.name, cm.childTypeId]);
};
exports.addChildType = addChildType;
/** returns an data structure with lots of context information in it */
const toInfoContainer = (mergeWith) => (parent) => (childType) => (tagValue) => {
const entryName = (0, ts_types_1.ensureString)((0, decomposed_1.extractUniqueElementValue)(tagValue, childType.uniqueIdElement));
return {
parentComponent: parent,
entryName,
childComponent: {
fullName: `${parent.fullName}.${entryName}`,
type: childType,
parent,
},
value: tagValue,
mergeWith,
};
};
const forceIgnoreAllowsComponent = (forceIgnore) => (ic) => forceIgnore.accepts(getDefaultOutput(ic.childComponent));
exports.forceIgnoreAllowsComponent = forceIgnoreAllowsComponent;
/** wrap some xml in the Metadata type and add the NS stuff */
const objectToSource = (childTypeName) => (obj) => new streams_1.JsToXml({ [childTypeName]: { [constants_1.XML_NS_KEY]: constants_1.XML_NS_URL, ...obj } });
/** filter out the children and create the remaining parentXml */
const buildParentXml = (parentType) => (c) => ({
[parentType.name]: {
[constants_1.XML_NS_KEY]: constants_1.XML_NS_URL,
...Object.fromEntries(c.filter((v) => v.childTypeId === undefined).map(({ tagKey, tagValue }) => [tagKey, tagValue])),
},
});
const getOutputFile = (component, mergeWith) => mergeWith?.xml ?? getDefaultOutput(component);
exports.getOutputFile = getOutputFile;
//# sourceMappingURL=decomposedMetadataTransformer.js.map
;