UNPKG

@salesforce/source-deploy-retrieve

Version:

JavaScript library to run Salesforce metadata deploys and retrieves

195 lines 11.3 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 */ Object.defineProperty(exports, "__esModule", { value: true }); exports.DecomposedPermissionSetTransformer = void 0; const node_path_1 = require("node:path"); const ts_types_1 = require("@salesforce/ts-types"); const kit_1 = require("@salesforce/kit"); const path_1 = require("../../utils/path"); const decomposed_1 = require("../../utils/decomposed"); const resolve_1 = require("../../resolve"); const streams_1 = require("../streams"); const common_1 = require("../../common"); const baseMetadataTransformer_1 = require("./baseMetadataTransformer"); const decomposedMetadataTransformer_1 = require("./decomposedMetadataTransformer"); class DecomposedPermissionSetTransformer extends baseMetadataTransformer_1.BaseMetadataTransformer { /** * Combines a decomposed Permission Set into a singular .permissonset metadata-formatted file * * @param {SourceComponent} component - either the parent or child of a decomposed permission set to be combined with * @returns {Promise<WriteInfo[]>} will be an array with one WriteInfo in it, because they're ending in one file */ // eslint-disable-next-line @typescript-eslint/require-await async toMetadataFormat(component) { // only need to do this once this.context.decomposedPermissionSet.permissionSetType ??= this.registry.getTypeByName('PermissionSet'); const children = component.getChildren(); // component is the first (alphabetically) file in the PS dir, if it happens to be the parent (.permissionset) use it, const parent = // otherwise, build our own component.xml?.endsWith('.permissionset-meta.xml') ? component : new resolve_1.SourceComponent({ // because the children have the same name as the parent name: children[0]?.name, xml: children[0]?.xml.replace(/(\w+\.\w+-meta\.xml)/gm, `${children[0].name}.permissionset-meta.xml`), type: this.context.decomposedPermissionSet.permissionSetType, }); [...children, parent].map((c) => { // eslint-disable-next-line no-unused-expressions this.context.decomposedPermissionSet.transactionState.parentToChild.has(parent.fullName) ? this.context.decomposedPermissionSet.transactionState.parentToChild .get(parent.fullName) .push((0, decomposed_1.unwrapAndOmitNS)('PermissionSet')(c.parseXmlSync())) : this.context.decomposedPermissionSet.transactionState.parentToChild.set(parent.fullName, [ (0, decomposed_1.unwrapAndOmitNS)('PermissionSet')(c.parseXmlSync()), ]); }); // noop since the finalizer will push the writes to the component writer return []; } /** * will decompose a .permissionset into a directory containing files, and an 'objectSettings' folder for object-specific settings * * @param {SourceComponent} component A SourceComponent representing a metadata-formatted permission set * @param {SourceComponent | undefined} mergeWith any existing source-formatted permission sets to be merged with, think existing source merging with new information from a retrieve * @returns {Promise<WriteInfo[]>} Will contain file content information, and file paths */ 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, decomposedMetadataTransformer_1.getOutputFile)(component, mergeWith))) { return []; } const composedMetadata = await getComposedMetadataEntries(component); const parentXmlObject = { [component.type.name]: { [common_1.XML_NS_KEY]: common_1.XML_NS_URL, ...Object.fromEntries(composedMetadata.filter((v) => v.childTypeId === undefined).map(({ tagKey, tagValue }) => [tagKey, tagValue])), }, }; const preparedMetadata = composedMetadata .filter(decomposedMetadataTransformer_1.hasChildTypeId) .map(decomposedMetadataTransformer_1.addChildType) .map((child) => toInfoContainer(mergeWith)(component)(child.childType)(child.tagValue)) .flat() .filter((0, decomposedMetadataTransformer_1.forceIgnoreAllowsComponent)(forceIgnore)); const writeInfosForChildren = combineChildWriteInfos([ // children whose type don't have a directory assigned will be written to the top level, separate them into individual WriteInfo[] with only one entry // a [WriteInfo] with one entry, will result in one file ...preparedMetadata.filter((c) => !c.childComponent.type.directoryName).map((c) => [c]), // children whose type have a directory name will be grouped accordingly, bundle these together as a WriteInfo[][] with length > 1 // a [WriteInfo, WriteInfo, ...] will be combined into a [WriteInfo] with combined contents preparedMetadata.filter((c) => c.childComponent.type.directoryName), ]); const writeInfoForParent = mergeWith ? [ { output: (0, node_path_1.join)((0, ts_types_1.ensureString)(mergeWith.content), `${component.name}.${(0, ts_types_1.ensureString)(component.type.suffix)}${common_1.META_XML_SUFFIX}`), source: new streams_1.JsToXml(parentXmlObject), }, ] : (0, decomposedMetadataTransformer_1.getWriteInfosWithoutMerge)(this.defaultDirectory)(parentXmlObject)(component); return [...writeInfosForChildren, ...writeInfoForParent]; } } exports.DecomposedPermissionSetTransformer = DecomposedPermissionSetTransformer; /** for a component, parse the xml and create a 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, decomposedMetadataTransformer_1.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 ? type.directoryName : '', `${childName ?? baseName}.${(0, ts_types_1.ensureString)(component.type.suffix)}${common_1.META_XML_SUFFIX}`); return (0, node_path_1.join)((0, path_1.calculateRelativePath)('source')({ self: parent?.type ?? type })(fullName)(baseName), output); }; const buildSource = (parentType, info, childDirectories) => new streams_1.JsToXml({ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment [parentType]: Object.assign({}, ...info.map((i) => ({ [childDirectories // the child tag values correspond to the parents directories, not the names , classAccess => <classAccesses> // find the value that matches, and use the key e[0]=value .find((e) => e[1] === i.childComponent.type.name.toLowerCase()) ?.at(0) ?? '']: i.value, }))), }); const combineChildWriteInfos = (containers) => { // aggregator write info, will be returned at the end const writeInfos = []; containers.forEach((infoContainers) => { // we have multiple InfoContainers, build a map of output file => file content // this is how we'll combine multiple children into one file const nameWriteInfoMap = new Map(); infoContainers.map((info) => nameWriteInfoMap.has(info.entryName) ? nameWriteInfoMap.get(info.entryName).push(info) : nameWriteInfoMap.set(info.entryName, [info])); nameWriteInfoMap.forEach((info) => { // all children share the same parent type, grab the first entry for top-level parent name const childDirectories = Object.entries(info[0].parentComponent.type.children?.directories); // if there's nothing to merge with, push write operation now to default location const childInfo = info[0].childComponent; writeInfos.push({ source: buildSource(info[0].parentComponent.type.name, info, childDirectories), output: !info[0].mergeWith ? getDefaultOutput(childInfo) : (0, node_path_1.join)((0, ts_types_1.ensureString)(info[0].mergeWith.content), childInfo.type.directoryName, `${info[0].entryName}.${(0, ts_types_1.ensureString)(childInfo.type.suffix)}${common_1.META_XML_SUFFIX}`), }); return; }); }); return writeInfos; }; /** returns a data structure with lots of context information in it - this is also where the name of the file/component is calculated */ const toInfoContainer = (mergeWith) => (parent) => (childType) => (tagValue) => { const tagEntry = Array.isArray(tagValue) ? tagValue : [tagValue]; const buildInfoContainer = (entryName, value) => ({ parentComponent: parent, entryName, childComponent: { fullName: `${parent.fullName}.${entryName}`, type: childType, parent, }, value, mergeWith, }); // ObjectSettings, ObjectPermission (object), FieldPermission (field), // RecordTypeVisibility (recordType), TabSetting (tab) if (childType.directoryName) { const infoContainers = new Map(); tagEntry.map((entry) => { let entryName = entry[childType.uniqueIdElement].split('.')[0]; // If the object name starts with `standard-`, we need to remove the prefix if (entryName.startsWith('standard-')) { entryName = entryName.replace('standard-', ''); } const infoContainer = infoContainers.get(entryName); if (infoContainer) { // Add to the value of the existing InfoContainer // @ts-expect-error this is either JsonMap or JsonArray infoContainer.value = (0, kit_1.ensureArray)(infoContainer.value).concat(entry); } else { infoContainers.set(entryName, buildInfoContainer(entryName, entry)); } }); return Array.from(infoContainers.values()); } return [buildInfoContainer(parent.name, tagValue)]; }; //# sourceMappingURL=decomposedPermissionSetTransformer.js.map