sfdx-hardis
Version:
Swiss-army-knife Toolbox for Salesforce. Allows you to define a complete CD/CD Pipeline. Orchestrate base commands and assist users with interactive wizards
178 lines (175 loc) • 8.76 kB
JavaScript
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
import c from 'chalk';
import fs from 'fs-extra';
import * as path from 'path';
import * as util from 'util';
import * as xml2js from 'xml2js';
import { uxLog } from '../../../../common/utils/index.js';
import { writeXmlFile } from '../../../../common/utils/xmlUtils.js';
// The code of this method is awful... it's migrated from sfdx-essentials, written when async / await were not existing ^^
export class FilterXmlContent extends SfCommand {
static description = `Filter content of metadatas (XML) in order to be able to deploy only part of them on an org (See [Example configuration](https://github.com/nvuillam/sfdx-essentials/blob/master/examples/filter-xml-content-config.json))
When you perform deployments from one org to another, the features activated in the target org may not fit the content of the sfdx/metadata files extracted from the source org.
You may need to filter some elements in the XML files, for example in the Profiles
This script requires a filter-config.json file`;
static examples = [
'sf hardis:project:clean:filter-xml-content -i "./mdapi_output"',
'sf hardis:project:clean:filter-xml-content -i "retrieveUnpackaged"',
];
static requiresProject = true;
static flags = {
configfile: Flags.string({
char: 'c',
description: 'Config JSON file path',
}),
inputfolder: Flags.string({
char: 'i',
description: 'Input folder (default: "." )',
}),
outputfolder: Flags.string({
char: 'f',
description: 'Output folder (default: parentFolder + _xml_content_filtered)',
}),
debug: Flags.boolean({
default: false,
description: 'debug',
}),
websocket: Flags.string({
description: 'websocket',
}),
};
// Input params properties
configFile;
inputFolder;
outputFolder;
// Internal properties
smmryUpdatedFiles = {};
smmryResult = { filterResults: {} };
async run() {
const { flags } = await this.parse(FilterXmlContent);
this.configFile = flags.configfile || './filter-config.json';
this.inputFolder = flags.inputfolder || '.';
this.outputFolder =
flags.outputfolder ||
'./' + path.dirname(this.inputFolder) + '/' + path.basename(this.inputFolder) + '_xml_content_filtered';
uxLog(this, c.cyan(`Initialize XML content filtering of ${this.inputFolder}, using ${c.bold(this.configFile)} , into ${this.outputFolder}`));
// Read json config file
const filterConfig = fs.readJsonSync(this.configFile);
if (flags.debug) {
uxLog(this, c.grey('Filtering config file content:\n' + JSON.stringify(filterConfig, null, 2)));
}
// Create output folder/empty it if existing
if (fs.existsSync(this.outputFolder) && this.outputFolder !== this.inputFolder) {
uxLog(this, c.grey('Empty output folder ' + this.outputFolder));
fs.emptyDirSync(this.outputFolder);
}
else if (!fs.existsSync(this.outputFolder)) {
uxLog(this, c.grey('Create output folder ' + this.outputFolder));
fs.mkdirSync(this.outputFolder);
}
// Copy input folder to output folder
if (this.outputFolder !== this.inputFolder) {
uxLog(this, 'Copy in output folder ' + this.outputFolder);
fs.copySync(this.inputFolder, this.outputFolder);
}
// Browse filters
filterConfig.filters.forEach((filter) => {
uxLog(this, c.grey(filter.name + ' (' + filter.description + ')...'));
// Browse filter folders
filter.folders.forEach((filterFolder) => {
// Browse folder files
if (!fs.existsSync(this.outputFolder + '/' + filterFolder)) {
return;
}
const folderFiles = fs.readdirSync(this.outputFolder + '/' + filterFolder);
folderFiles.forEach((file) => {
// Build file name
const fpath = file.replace(/\\/g, '/');
const browsedFileExtension = fpath.substring(fpath.lastIndexOf('.') + 1);
filter.file_extensions.forEach((filterFileExt) => {
if (browsedFileExtension === filterFileExt) {
// Found a matching file, process it
const fullFilePath = this.outputFolder + '/' + filterFolder + '/' + fpath;
uxLog(this, c.grey('- ' + fullFilePath));
this.filterXmlFromFile(filter, fullFilePath);
}
});
});
});
});
this.smmryResult.filterResults = this.smmryUpdatedFiles;
// Display results as JSON
uxLog(this, c.grey('Filtering results:' + JSON.stringify(this.smmryResult)));
return {};
}
// Filter XML content of the file
filterXmlFromFile(filter, file) {
const parser = new xml2js.Parser();
const data = fs.readFileSync(file);
parser.parseString(data, (err2, fileXmlContent) => {
uxLog(this, 'Parsed XML \n' + util.inspect(fileXmlContent, false, null));
Object.keys(fileXmlContent).forEach((eltKey) => {
fileXmlContent[eltKey] = this.filterElement(fileXmlContent[eltKey], filter, file);
});
if (this.smmryUpdatedFiles[file] != null && this.smmryUpdatedFiles[file].updated === true) {
writeXmlFile(file, fileXmlContent);
uxLog(this, 'Updated ' + file);
}
});
}
filterElement(elementValue, filter, file) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
// Object case
if (typeof elementValue === 'object') {
Object.keys(elementValue).forEach((eltKey) => {
let found = false;
// Browse filter exclude_list for elementValue
filter.exclude_list.forEach((excludeDef) => {
if (excludeDef.type_tag === eltKey) {
// Found matching type tag
found = true;
uxLog(this, '\nFound type: ' + eltKey);
uxLog(this, elementValue[eltKey]);
// Filter type values
const typeValues = elementValue[eltKey];
const newTypeValues = [];
typeValues.forEach((typeItem) => {
// If identifier tag not found, do not filter and avoid crash
if (typeItem[excludeDef.identifier_tag] &&
(excludeDef.values.includes(typeItem[excludeDef.identifier_tag]) ||
excludeDef.values.includes(typeItem[excludeDef.identifier_tag][0]))) {
uxLog(this, '----- filtered ' + typeItem[excludeDef.identifier_tag]);
if (self.smmryUpdatedFiles[file] == null) {
self.smmryUpdatedFiles[file] = { updated: true, excluded: {} };
}
if (self.smmryUpdatedFiles[file].excluded[excludeDef.type_tag] == null) {
self.smmryUpdatedFiles[file].excluded[excludeDef.type_tag] = [];
}
self.smmryUpdatedFiles[file].excluded[excludeDef.type_tag].push(typeItem[excludeDef.identifier_tag][0]);
}
else {
uxLog(this, '--- kept ' + typeItem[excludeDef.identifier_tag]);
newTypeValues.push(typeItem);
}
});
elementValue[eltKey] = newTypeValues;
}
});
if (!found) {
elementValue[eltKey] = self.filterElement(elementValue[eltKey], filter, file);
}
});
}
else if (Array.isArray(elementValue)) {
const newElementValue = [];
elementValue.forEach((element) => {
element = self.filterElement(element, filter, file);
newElementValue.push(element);
});
elementValue = newElementValue;
}
return elementValue;
}
}
//# sourceMappingURL=filter-xml-content.js.map