UNPKG

ci-sf-plugin

Version:

Set of commands making CI and dev's life easier.

290 lines 14.8 kB
/* * 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