UNPKG

@salesforce/source-deploy-retrieve

Version:

JavaScript library to run Salesforce metadata deploys and retrieves

265 lines 20.6 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 __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