ci-sf-plugin
Version:
Set of commands making CI and dev's life easier.
290 lines • 14.8 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
*/
import * as os from 'os';
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'fs';
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
import { Messages, SfError } from '@salesforce/core';
import { create } from 'xmlbuilder2';
import { ux } from '@oclif/core';
// Initialize Messages with the current plugin directory
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
// Load the specific messages for this file. Messages from @salesforce/command, @salesforce/core,
// or any library that is using the messages framework can also be loaded this way.
const messages = Messages.loadMessages('ci-sf-plugin', 'source.sort');
export default class Sort extends SfCommand {
static description = messages.getMessage('commandDescription');
static examples = messages.getMessage('examples').split(os.EOL);
static flags = {
// flag with a value (-n, --name=VALUE)
'directory': Flags.string({
char: 'd',
default: 'src/main/default',
description: messages.getMessage('directoryFlagDescription')
})
};
// Set this to true if your command requires a project workspace; 'requiresProject' is false by default
static requiresProject = false;
async run() {
try {
const { flags } = await this.parse(Sort);
const directoryFlag = flags['directory'];
const rootDir = directoryFlag.slice(-1) === '/' ? directoryFlag : directoryFlag + '/';
ux.action.start(messages.getMessage('infoSorting', [rootDir]), 'in progress', { stdout: true });
const output = { stdout: [], stderr: [] };
if (!existsSync(rootDir)) {
throw new Error(`Target directory '${rootDir}' does not exist.`);
}
// xmlbuilder2 xml format options
const xmlOptions = { format: 'xml', prettyPrint: true, indent: ' ' };
// sort permission set groups
let dirToSortPath = rootDir + 'permissionsetgroups';
if (existsSync(dirToSortPath)) {
readdirSync(dirToSortPath).filter(filename => /^[\w]+\.permissionsetgroup-meta\.xml$/.test(filename)).forEach(filename => {
process.stdout.write(`Sorting permission set group '${filename}'.\n`);
const filepath = dirToSortPath + '/' + filename;
// verbose option for having each data value as an array, better for processing
const dataObj = create(readFileSync(filepath, 'utf-8')).end({
format: 'object',
group: false,
verbose: true
});
// 'PermissionSetGroup' is parent element, inner elements and their child items must be sorted
// '#' key symbolizes duplicate elem groups array - we always need this array to keep ordered elements
if (!dataObj['PermissionSetGroup'][0].hasOwnProperty('#')) {
const newDataElem = { '#': [] };
Object.entries(dataObj['PermissionSetGroup'][0]).forEach(([key, value]) => {
if (Array.isArray(value)) { // sortable array
const item = {};
item[key] = value;
newDataElem['#'].push(item);
}
else {
newDataElem[key] = value; // xml namespaces etc.
}
});
// assign refactored element
dataObj['PermissionSetGroup'][0] = newDataElem;
}
const childElementsGrouped = mergeElementGroups(dataObj['PermissionSetGroup'][0]['#']);
dataObj['PermissionSetGroup'][0]['#'] = childElementsGrouped.sort(firstKeyObjCompare);
// overwrite the xml file
writeFileSync(filepath, create({ version: '1.0', encoding: 'UTF-8' }, dataObj).end(xmlOptions));
});
}
// sort permission sets
dirToSortPath = rootDir + 'permissionsets';
if (existsSync(dirToSortPath)) {
readdirSync(dirToSortPath).filter(filename => /^[\w]+\.permissionset-meta\.xml$/.test(filename)).forEach(filename => {
process.stdout.write(`Sorting permission set '${filename}'.\n`);
const filepath = dirToSortPath + '/' + filename;
// verbose option for having each data value as an array, better for processing
const dataObj = create(readFileSync(filepath, 'utf-8')).end({
format: 'object',
group: false,
verbose: true
});
// 'PermissionSet' is parent element, inner elements and their child items must be sorted
// '#' key symbolizes duplicate elem groups array - we always need this array to keep ordered elements
if (!dataObj['PermissionSet'][0].hasOwnProperty('#')) {
const newDataElem = { '#': [] };
Object.entries(dataObj['PermissionSet'][0]).forEach(([key, value]) => {
if (Array.isArray(value)) { // sortable array
const item = {};
item[key] = value;
newDataElem['#'].push(item);
}
else {
newDataElem[key] = value; // xml namespaces etc.
}
});
// assign refactored element
dataObj['PermissionSet'][0] = newDataElem;
}
const childElementsGrouped = mergeElementGroups(dataObj['PermissionSet'][0]['#']);
dataObj['PermissionSet'][0]['#'] = childElementsGrouped.sort(firstKeyObjCompare);
sortNestedElements(dataObj['PermissionSet'][0]['#']);
// overwrite the xml file
writeFileSync(filepath, create({ version: '1.0', encoding: 'UTF-8' }, dataObj).end(xmlOptions));
});
}
// sort muting permission set
dirToSortPath = rootDir + 'mutingpermissionsets';
if (existsSync(dirToSortPath)) {
readdirSync(dirToSortPath).filter(filename => /^[\w]+\.mutingpermissionset-meta\.xml$/.test(filename)).forEach(filename => {
process.stdout.write(`Sorting muting permission set '${filename}'.\n`);
const filepath = dirToSortPath + '/' + filename;
// verbose option for having each data value as an array, better for processing
const dataObj = create(readFileSync(filepath, 'utf-8')).end({
format: 'object',
group: false,
verbose: true
});
// 'MutingPermissionSet' is parent element, inner elements and their child items must be sorted
// '#' key symbolizes duplicate elem groups array - we always need this array to keep ordered elements
if (!dataObj['MutingPermissionSet'][0].hasOwnProperty('#')) {
const newDataElem = { '#': [] };
Object.entries(dataObj['MutingPermissionSet'][0]).forEach(([key, value]) => {
if (Array.isArray(value)) { // sortable array
const item = {};
item[key] = value;
newDataElem['#'].push(item);
}
else {
newDataElem[key] = value; // xml namespaces etc.
}
});
// assign refactored element
dataObj['MutingPermissionSet'][0] = newDataElem;
}
const childElementsGrouped = mergeElementGroups(dataObj['MutingPermissionSet'][0]['#']);
dataObj['MutingPermissionSet'][0]['#'] = childElementsGrouped.sort(firstKeyObjCompare);
sortNestedElements(dataObj['MutingPermissionSet'][0]['#']);
// overwrite the xml file
writeFileSync(filepath, create({ version: '1.0', encoding: 'UTF-8' }, dataObj).end(xmlOptions));
});
}
// sort profiles
dirToSortPath = rootDir + 'profiles';
if (existsSync(dirToSortPath)) {
readdirSync(dirToSortPath).filter(filename => /^[\w]+\.profile-meta\.xml$/.test(filename)).forEach(filename => {
process.stdout.write(`Sorting profile '${filename}'.\n`);
const filepath = dirToSortPath + '/' + filename;
// verbose option for having each data value as an array, better for processing
const dataObj = create(readFileSync(filepath, 'utf-8')).end({
format: 'object',
group: false,
verbose: true
});
// 'Profile' is parent element, inner elements and their child items must be sorted
// '#' key symbolizes duplicate elem groups array - we always need this array to keep ordered elements
if (!dataObj['Profile'][0].hasOwnProperty('#')) {
const newDataElem = { '#': [] };
Object.entries(dataObj['Profile'][0]).forEach(([key, value]) => {
if (Array.isArray(value)) { // sortable array
const item = {};
item[key] = value;
newDataElem['#'].push(item);
}
else {
newDataElem[key] = value; // xml namespaces etc.
}
});
// assign refactored element
dataObj['Profile'][0] = newDataElem;
}
const childElementsGrouped = mergeElementGroups(dataObj['Profile'][0]['#']);
dataObj['Profile'][0]['#'] = childElementsGrouped.sort(firstKeyObjCompare);
sortNestedElements(dataObj['Profile'][0]['#']);
// overwrite the xml file
writeFileSync(filepath, create({ version: '1.0', encoding: 'UTF-8' }, dataObj).end(xmlOptions));
});
}
ux.action.stop('done');
// Return an object
return { output };
}
catch (error) {
ux.action.stop('failed');
throw new SfError(messages.getMessage('errorSortingFailed', [JSON.stringify(error, null, 2)]));
}
}
}
/*
* Transform data - merge duplicate elements and sort.
*
* @param groups - array of objects to merge and sort
*
* @returns sorted array of objects, sorted child items
*/
function mergeElementGroups(groups) {
const keyToItems = new Map();
groups.forEach(group => {
const groupKey = Object.keys(group)[0];
if (keyToItems.has(groupKey)) {
keyToItems.get(groupKey).push(...group[groupKey]);
}
else {
keyToItems.set(groupKey, [...group[groupKey]]);
}
});
// transform to target structure, remove duplicates on a group level and sort values
const output = [];
keyToItems.forEach((value, key) => {
const item = {};
item[key] = Array.from(new Set(value)).sort();
output.push(item);
});
return output;
}
/*
* Compare elements by node keys.
*
* @param a - item to compare
* @param b - item to compare
*
* @returns std compareTo output {-1, 0, 1}
*/
function firstKeyObjCompare(a, b) {
const aKey = Object.keys(a)[0];
const bKey = Object.keys(b)[0];
if (aKey < bKey) {
return -1;
}
if (aKey > bKey) {
return 1;
}
return 0; // equal keys
}
// 1st level element to nested elements sorting rules
const nodeNameToNestedSortKey = new Map([
['applicationVisibilities', 'application'],
['classAccesses', 'apexClass'],
['customMetadataTypeAccesses', 'name'],
['customSettingAccesses', 'name'],
['fieldPermissions', 'field'],
['layoutAssignments', 'layout'],
['objectPermissions', 'object'],
['recordTypeVisibilities', 'recordType'],
['tabVisibilities', 'tab'],
['userPermissions', 'name']
]);
/*
* Sort by nested elements and their value, using 'nodeNameToNestedSortKey' for mapping.
* Duplicates will be removed.
*
* @param elems - array with inner elements to be sorted in place
*/
function sortNestedElements(elems) {
elems.forEach(elem => {
const key = Object.keys(elem)[0];
if (nodeNameToNestedSortKey.has(key) && Array.isArray(elem[key])) {
const nestedKey = nodeNameToNestedSortKey.get(key);
// sort and filter out duplicates
elem[key].sort((a, b) => {
const aValue = a[nestedKey][0];
const bValue = b[nestedKey][0];
if (aValue < bValue) {
return -1;
}
if (aValue > bValue) {
return 1;
}
// simpler objects first
if (Object.keys(a).length < Object.keys(b).length) {
return -1;
}
if (Object.keys(a).length > Object.keys(b).length) {
return 1;
}
return 0; // equal values and keySet length
});
// eliminate JS objects with the same value and amount of keys (eg. layoutAssignments element can, but does not have to have recordType specified, these are not duplicates then)
elem[key] = elem[key].filter((it, index, arrayOrig) => index + 1 === arrayOrig.length || it[nestedKey][0] !== arrayOrig[index + 1][nestedKey][0] || Object.keys(it).length !== Object.keys(arrayOrig[index + 1]).length);
}
});
}
//# sourceMappingURL=sort.js.map