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