UNPKG

salesforce-alm

Version:

This package contains tools, and APIs, for an improved salesforce.com developer experience.

230 lines (228 loc) 10.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 */ // Node const path = require("path"); const fs = require("fs"); const glob = require("glob"); // Thirdparty const xmldom_sfdx_encoding_1 = require("xmldom-sfdx-encoding"); const core_1 = require("@salesforce/core"); // Local core_1.Messages.importMessagesDirectory(__dirname); const profileApiMessages = core_1.Messages.loadMessages('salesforce-alm', 'packaging'); /* * This class provides functions used to re-write .profiles in the workspace when creating a package2 version. * All profiles found in the workspaces are extracted out and then re-written to only include metadata in the profile * that is relevant to the source in the package directory being packaged. */ class ProfileApi { constructor(org, includeUserLicenses, generateProfileInformation = false) { this.org = org; this.includeUserLicenses = includeUserLicenses; this.config = this.org.config; this.apiVersion = this.config.getApiVersion(); this.profiles = []; this.generateProfileInformation = generateProfileInformation; this.includeUserLicenses = includeUserLicenses; // nodeEntities is used to determine which elements in the profile are relevant to the source being packaged. // name refers to the entity type name in source that the element pertains to. As an example, a profile may // have an entry like the example below, which should only be added to the packaged profile if the related // CustomObject is in the source being packaged: // <objectPermissions> // <allowCreate>true</allowCreate> // ... // <object>MyCustomObject__c</object> // ... // </objectPermissions> // // For this example: nodeEntities.parentElement = objectPermissions and nodeEntities.childElement = object this.nodeEntities = { name: ['CustomObject', 'CustomField', 'Layout', 'CustomTab', 'CustomApplication', 'ApexClass'], parentElement: [ 'objectPermissions', 'fieldPermissions', 'layoutAssignments', 'tabVisibilities', 'applicationVisibilities', 'classAccesses', ], childElement: ['object', 'field', 'layout', 'tab', 'application', 'apexClass'], }; } _copyNodes(originalDom, parentElement, childElement, members, appendToNode, profileName) { let nodesAdded = false; const nodes = originalDom.getElementsByTagName(parentElement); if (!nodes) { return nodesAdded; } for (let i = 0; i < nodes.length; i++) { const name = nodes[i].getElementsByTagName(childElement)[0].childNodes[0].nodeValue; if (members.indexOf(name) >= 0) { // appendChild will take the passed in node (newNode) and find the parent if it exists and then remove // the newNode from the parent. This causes issues with the way this is copying the nodes, so pass in a clone instead. const currentNode = nodes[i].cloneNode(true); appendToNode.appendChild(currentNode); nodesAdded = true; } else { // Tell the user which profile setting has been removed from the package if (this.generateProfileInformation) { let profile = this.profiles.find(({ ProfileName }) => ProfileName === profileName); if (profile) { profile.appendRemovedSetting(name); } } } } return nodesAdded; } _findAllProfiles(excludedDirectories = []) { for (let i = 0; i < excludedDirectories.length; i++) { excludedDirectories[i] = '**/' + excludedDirectories[i] + '/**'; } return glob.sync(path.join(this.config.getProjectPath(), '**', '*.profile-meta.xml'), { ignore: excludedDirectories, }); } /** * For any profile present in the workspace, this function generates a subset of data that only contains references * to items in the manifest. * * @param destPath location of new profiles * @param manifest * @param excludedDirectories Directories to not include profiles from */ generateProfiles(destPath, manifest, excludedDirectories = []) { const excludedProfiles = []; const profilePaths = this._findAllProfiles(excludedDirectories); if (!profilePaths) { return excludedProfiles; } profilePaths.forEach((profilePath) => { // profile metadata can present in any directory in the package structure const profileName = profilePath.match(/([^\/]+)\.profile-meta.xml/)[1]; const profileDom = new xmldom_sfdx_encoding_1.DOMParser().parseFromString(fs.readFileSync(profilePath, 'utf-8')); const newDom = new xmldom_sfdx_encoding_1.DOMParser().parseFromString('<?xml version="1.0" encoding="UTF-8"?><Profile xmlns="http://soap.sforce.com/2006/04/metadata"></Profile>'); const profileNode = newDom.getElementsByTagName('Profile')[0]; let hasNodes = false; manifest.Package.types.forEach((element) => { const name = element['name']; const members = element['members']; const idx = this.nodeEntities.name.indexOf(name[0]); if (idx > -1) { hasNodes = this._copyNodes(profileDom, this.nodeEntities.parentElement[idx], this.nodeEntities.childElement[idx], members, profileNode, profileName) || hasNodes; } }); // add userLicenses to the profile if (this.includeUserLicenses === true) { const userLicenses = profileDom.getElementsByTagName('userLicense'); if (userLicenses) { hasNodes = true; for (let i = 0; i < userLicenses.length; i++) { const node = userLicenses[i].cloneNode(true); profileNode.appendChild(node); } } } const xmlSrcFile = path.basename(profilePath); const xmlFile = xmlSrcFile.replace(/(.*)(-meta.xml)/, '$1'); const destFilePath = path.join(destPath, xmlFile); if (hasNodes) { const serializer = new xmldom_sfdx_encoding_1.XMLSerializer(); fs.writeFileSync(destFilePath, serializer.serializeToString(newDom), 'utf-8'); } else { // remove from manifest // eslint-disable-next-line @typescript-eslint/no-shadow const profileName = xmlFile.replace(/(.*)(\.profile)/, '$1'); excludedProfiles.push(profileName); if (this.generateProfileInformation) { let profile = this.profiles.find(({ ProfileName }) => ProfileName === profileName); if (profile) { profile.setIsPackaged(false); } } try { fs.unlinkSync(destFilePath); } catch (err) { // It is normal for the file to not exist if the profile is in the worskpace but not in the directory being packaged. if (err.code !== 'ENOENT') { throw err; } } } }); return excludedProfiles; } /** * Filter out all profiles in the manifest and if any profiles exists in the workspace, add them to the manifest. * * @param typesArr array of objects { name[], members[] } that represent package types JSON. * @param excludedDirectories Direcotires not to generate profiles for */ filterAndGenerateProfilesForManifest(typesArr, excludedDirectories = []) { const profilePaths = this._findAllProfiles(excludedDirectories); // Filter all profiles typesArr = typesArr.filter((kvp) => kvp.name[0] !== 'Profile'); if (profilePaths) { const members = []; profilePaths.forEach((profilePath) => { // profile metadata can present in any directory in the package structure const profileName = profilePath.match(/([^\/]+)\.profile-meta.xml/)[1]; members.push(profileName); if (this.generateProfileInformation) { this.profiles.push(new ProfileInformation(profileName, profilePath, true, [])); } }); if (members.length > 0) { typesArr.push({ name: ['Profile'], members }); } } return typesArr; } getProfileInformation() { return this.profiles; } } class ProfileInformation { constructor(ProfileName, ProfilePath, IsPackaged, settingsRemoved) { this.ProfileName = ProfileName; this.ProfilePath = ProfilePath; this.IsPackaged = IsPackaged; this.settingsRemoved = settingsRemoved; } setIsPackaged(IsPackaged) { this.IsPackaged = IsPackaged; } appendRemovedSetting(setting) { this.settingsRemoved.push(setting); } logDebug() { let info = profileApiMessages.getMessage('profile_api.addProfileToPackage', [this.ProfileName, this.ProfilePath]); this.settingsRemoved.forEach((setting) => { info += '\n\t' + profileApiMessages.getMessage('profile_api.removeProfileSetting', [setting, this.ProfileName]); }); if (!this.IsPackaged) { info += '\n\t' + profileApiMessages.getMessage('profile_api.removeProfile', [this.ProfileName]); } info += '\n'; return info; } logInfo() { if (this.IsPackaged) { return profileApiMessages.getMessage('profile_api.addProfileToPackage', [this.ProfileName, this.ProfilePath]); } else { return profileApiMessages.getMessage('profile_api.profileNotIncluded', [this.ProfileName]); } } } module.exports = ProfileApi; //# sourceMappingURL=profileApi.js.map