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
409 lines • 19.9 kB
JavaScript
import { NODE_CONFIG } from "./renderConfig.js";
import { XMLParser } from "fast-xml-parser";
import { CONSTANTS } from "../../../config/index.js";
import { getCurrentGitBranch } from "../index.js";
import farmhash from 'farmhash';
import { buildCustomMarkdownTable, buildGenericMarkdownTable, flowNodeToMarkdown, handleFilterItems, handleInputParameters, handleprocessMetadataValues, handleSchedule, handleScheduledPaths, mdEndSection, simplifyNode, stringifyValue } from "./nodeFormatUtils.js";
const FLOW_NODE_TYPES = [
'actionCalls',
'assignments',
'customErrors',
'collectionProcessors',
'decisions',
'loops',
'recordCreates',
'recordDeletes',
'recordLookups',
'recordUpdates',
'screens',
'subflows',
'transforms'
];
export async function parseFlow(xml, renderAs = "mermaid", options = {}) {
try {
const flowObj = new XMLParser().parse(xml).Flow;
const flowMap = await createFlowMap(flowObj);
if (Object.keys(flowMap).length === 0) {
throw new Error("no-renderable-content-found");
}
if (renderAs === "mermaid") {
return {
flowMap: flowMap,
uml: await generateMermaidContent(flowMap, flowObj, options)
};
}
throw new Error("unknown-renderAs-" + renderAs);
}
catch (error) {
console.error("salesforce-flow-visualiser", error);
throw (error);
}
}
/*===================================================================
* P R I V A T E
*=================================================================*/
async function createFlowMap(flowObj) {
const flowMap = {};
for (const property in flowObj) {
// Common first descriptive elements
if (['description', 'environments', 'formulas', 'interviewLabel', 'label', 'processType', 'status', 'textTemplates'].includes(property)) {
flowMap[property] = flowObj[property];
if (property === "formulas" && !Array.isArray(flowObj[property])) {
flowMap["formulas"] = [flowObj["formulas"]];
}
}
// Start element
else if (property === 'start') {
flowMap[property] = flowObj[property];
flowMap[property].type = property;
flowMap[property].nextNode = flowObj[property].connector?.targetReference;
flowMap[property].scheduledPaths = (!flowMap[property].scheduledPaths) ? [] : (flowMap[property].scheduledPaths.length) ? flowMap[property].scheduledPaths : [flowMap[property].scheduledPaths];
flowMap[property].flowNodeDescription = flowObj[property];
}
else {
// If only one entry (e.g one loop) then it will be an object, not an Array, so make it an Array of one
if (!flowObj[property].length) {
flowObj[property] = [flowObj[property]];
}
// Loop through array and create an mapped entry for each
for (const el of flowObj[property]) {
if (el.name) {
let nextNode;
let tmpRules;
switch (property) {
case 'decisions':
nextNode = (el.defaultConnector) ? el.defaultConnector.targetReference : "END";
tmpRules = (el.rules.length) ? el.rules : [el.rules];
el.rules2 = tmpRules.map((ruleEl) => {
return {
name: ruleEl.name,
label: ruleEl.label,
nextNode: ruleEl.connector,
nextNodeLabel: el.defaultConnectorLabel,
};
});
break;
case 'loops':
nextNode = (el.noMoreValuesConnector) ? el.noMoreValuesConnector.targetReference : "END";
break;
default:
if (el.connector) {
nextNode = el.connector.targetReference;
}
break;
}
if (NODE_CONFIG[property]) {
const mappedEl = {
name: el.name,
label: el.label,
type: property,
nextNode: nextNode,
faultPath: el.faultConnector?.targetReference,
nextNodeLabel: el.defaultConnectorLabel,
nextValueConnector: (el.nextValueConnector) ?
el.nextValueConnector.targetReference : null,
rules: el.rules2,
elementSubtype: el.elementSubtype,
actionType: el.actionType,
flowNodeDescription: el
};
flowMap[el.name] = mappedEl;
flowMap[el.name].flowNodeDescription.type = property;
}
else if (property === 'variables') {
flowMap.variables = flowObj[property];
}
else if (property === 'constants') {
flowMap.constants = flowObj[property];
}
}
}
}
}
return (flowMap);
}
function getFlowType(flowMap) {
if (flowMap.processType === 'Flow') {
return "Screen Flow";
}
// Avoid crash if flowMap.start is not set
else if (!flowMap.start) {
return flowMap.processType || "ERROR: no processType";
}
else {
switch (flowMap.start.triggerType) {
case "Scheduled":
return "Scheduled Flow";
case "RecordAfterSave":
return "Record Triggered Flow: After Save (" + flowMap.start.object + ")";
case "RecordBeforeSave":
return "Record Triggered Flow: Before Save (" + flowMap.start.object + ")";
case "PlatformEvent":
return "Platform Event Triggered flow (" + flowMap.start.object + ")";
default:
return flowMap.processType || "ERROR: no processType";
}
}
}
/*===================================================================
* M E R M A I D
*=================================================================*/
async function generateMermaidContent(flowMap, flowObj, options) {
// console.log("options", options)
const flowType = getFlowType(flowMap);
const title = `# ${flowMap['label']}\n\n`;
const { generalInfoMd, startFingerPrint, startNodeLabel, startElementReference } = getGeneralInfoMd(flowObj, flowMap);
const variables = getVariablesMd(flowMap.variables || []);
const constants = getConstantsMd(flowMap.constants || []);
const formulas = getFormulasMd(flowMap.formulas || []);
const textTemplates = getTemplatesMd(flowMap.textTemplates || []);
const mdStart = "## Flow Diagram\n\n```mermaid\n";
const { nodeDefStr, nodeDetailMd } = await getNodeDefStr(flowMap, flowType, startFingerPrint, startNodeLabel, startElementReference, options);
const mdClasses = getMermaidClasses() + "\n\n";
const mdBody = await getMermaidBody(flowMap) + "\n\n";
const mdEnd = "```\n\n<!-- Flow description -->\n\n";
const currentBranch = await getCurrentGitBranch();
const footer = `\n\n___\n\n_Documentation generated from branch ${currentBranch} by [sfdx-hardis](${CONSTANTS.DOC_URL_ROOT}), featuring [salesforce-flow-visualiser](https://github.com/toddhalfpenny/salesforce-flow-visualiser)_`;
const mdDiagram = "%% If you read this, your Markdown visualizer does not handle MermaidJS syntax.\n" +
"%% - If you are in VS Code, install extension `Markdown Preview Mermaid Support` at https://marketplace.visualstudio.com/items?itemName=bierner.markdown-mermaid\n" +
"%% - If you are using sfdx-hardis, try to define env variable `MERMAID_MODES=cli,docker` ,then run again the command to regenerate markdown with SVG images.\n" +
"%% - If you are within mkdocs-material, define mermaid plugin in `mkdocs.yml` as described in https://squidfunk.github.io/mkdocs-material/extensions/mermaid/\n" +
"%% - As a last resort, you can copy-paste this MermaidJS code into https://mermaid.live/ to see the flow diagram\n\n" +
"flowchart TB\n" +
nodeDefStr +
mdBody +
mdClasses;
if (options.wrapInMarkdown === false) {
return (mdDiagram);
}
else {
return (title + mdStart + mdDiagram + mdEnd + generalInfoMd + variables + formulas + constants + textTemplates + nodeDetailMd + footer);
}
}
async function getMermaidBody(flowMap) {
let bodyStr = "";
const endNodeIds = [];
for (const property in flowMap) {
const node = flowMap[property];
const type = node.type;
let nextNode = node.nextNode ? node.nextNode : "END";
if (nextNode === "END") {
nextNode = "END_" + node.name;
}
let faultNode = node.faultPath ? node.faultPath : "END";
if (faultNode === "END") {
faultNode = "END_" + node.name;
}
let loopNextNode;
switch (type) {
case 'actionCalls':
case 'assignments':
case 'collectionProcessors':
case 'customErrors':
case 'recordCreates':
case 'recordDeletes':
case 'recordLookups':
case 'recordUpdates':
case 'screens':
case 'transforms':
bodyStr += node.name + " --> " + nextNode + "\n";
manageAddEndNode(nextNode, endNodeIds);
if (node.faultPath) {
bodyStr += node.name + " -. Fault .->" + faultNode + "\n";
manageAddEndNode(faultNode, endNodeIds);
}
break;
case 'start':
if (!nextNode.startsWith("END")) {
// 'start' may not have a default path
const defaultPathLabel = (node.scheduledPaths.length > 0) ? '|"Run Immediately"|' : "";
bodyStr += "START --> " + defaultPathLabel + " " + nextNode + "\n";
}
// scheduled paths
for (const path of node.scheduledPaths) {
path.label = (path.label) ? path.label : 'Run Immediately';
if (path?.connector?.targetReference) {
bodyStr += 'START --> |"' + path.label + '"| ' + path.connector.targetReference + "\n";
}
else if (nextNode) {
bodyStr += 'START --> |"' + path.label + '"| ' + nextNode + "\n";
}
}
break;
case 'decisions':
// rules
for (const rule of node.rules) {
if (rule.nextNode?.targetReference) {
bodyStr += node.name + ' --> |"' + rule.label + '"| ' + rule.nextNode.targetReference + "\n";
}
}
// default
bodyStr += node.name + ' --> |"' + node.nextNodeLabel + '"| ' + nextNode + "\n";
manageAddEndNode(nextNode, endNodeIds);
break;
case 'loops':
loopNextNode = node.nextValueConnector;
bodyStr += node.name + ' --> |"For Each"|' + loopNextNode + "\n";
bodyStr += node.name + ' ---> |"After Last"|' + node.nextNode + "\n";
break;
case 'subflows':
bodyStr += node.name + " --> " + nextNode + "\n";
manageAddEndNode(nextNode, endNodeIds);
}
}
for (const endNodeId of [...new Set(endNodeIds)]) {
bodyStr += `${endNodeId}(( END )):::endClass\n`;
}
return (bodyStr);
}
function manageAddEndNode(nextOrFaultNode, endNodeIds) {
if (nextOrFaultNode.startsWith("END")) {
endNodeIds.push(nextOrFaultNode);
}
}
async function getNodeDefStr(flowMap, flowType, startFingerPrint, startNodeLabel, startElementReference, options) {
let nodeDetailMd = "## Flow Nodes Details\n\n";
if (options?.collapsedDetails) {
nodeDetailMd += "<details><summary>NODES CONTENT (expand to view)</summary>\n\n";
}
let nodeDefStr = "";
if (!["InvocableProcess", "Workflow"].includes(flowType) || (startNodeLabel !== 'START')) {
nodeDefStr += `START(["${startNodeLabel}"]):::startClass\n`;
nodeDefStr += `click START "#general-information" "${startFingerPrint}"\n\n`;
if (startElementReference) {
nodeDefStr += `START --> ${startElementReference}\n`;
}
}
const allproperties = Object.keys(flowMap);
for (const property of allproperties) {
const type = flowMap?.[property]?.type;
let label = (NODE_CONFIG[type]) ? NODE_CONFIG[type].label : "";
let icon = (NODE_CONFIG[type]) ? NODE_CONFIG[type].mermaidIcon : null;
let nodeSimplified;
let tooltipClassMermaid;
if (type === 'actionCalls') {
icon = (NODE_CONFIG[type].mermaidIcon[flowMap[property].actionType]) ?
NODE_CONFIG[type].mermaidIcon[flowMap[property].actionType] :
NODE_CONFIG[type].mermaidIcon.submit;
}
else if (type === 'collectionProcessors') {
icon = (NODE_CONFIG[type].mermaidIcon[flowMap[property].elementSubtype]) ?
NODE_CONFIG[type].mermaidIcon[flowMap[property].elementSubtype] :
NODE_CONFIG[type].mermaidIcon.submit;
label = (NODE_CONFIG[type].label[flowMap[property].elementSubtype]) ?
NODE_CONFIG[type].label[flowMap[property].elementSubtype] :
NODE_CONFIG[type].label;
}
// Create Mermaid Lines
if (FLOW_NODE_TYPES.includes(type)) {
// Mermaid node
nodeDefStr += property + NODE_CONFIG[type].mermaidOpen + '"' + icon + " <em>" + label + "</em><br/>" + flowMap[property].label + '"' + NODE_CONFIG[type].mermaidClose + ':::' + type + "\n";
// Remove not relevant properties from node display
nodeSimplified = simplifyNode(flowMap[property]?.flowNodeDescription || flowMap[property]);
// Mermaid compare node
tooltipClassMermaid = `click ${property} "#${property.toLowerCase()}" "${farmhash.fingerprint32(JSON.stringify(nodeSimplified))}"`;
nodeDefStr += tooltipClassMermaid + "\n\n";
// Markdown details
nodeDetailMd += `### ${property}\n\n` + flowNodeToMarkdown(nodeSimplified, allproperties);
}
}
if (options?.collapsedDetails) {
nodeDetailMd += "</details>\n\n";
}
return {
nodeDefStr: nodeDefStr,
nodeDetailMd: nodeDetailMd + "\n\n"
};
}
function getGeneralInfoMd(flowObj, flowMap) {
let flowObjCopy = Object.assign({}, flowObj);
// Remove sections that are somewhere else
for (const nodeKey of [...["constants", "formulas", "variables"], ...FLOW_NODE_TYPES]) {
delete flowObjCopy[nodeKey];
}
const metadataValue = handleprocessMetadataValues(flowObjCopy, Object.keys(flowMap));
// Remove nodes that will be processed after
for (const nodeKey of Object.keys(flowObjCopy)) {
if (typeof flowObjCopy?.[nodeKey] === "object" && flowObjCopy?.[nodeKey]?.name !== 'null') {
delete flowObjCopy[nodeKey];
}
}
const startFingerPrint = farmhash.fingerprint32(JSON.stringify(flowObjCopy));
handleInputParameters(flowObjCopy, Object.keys(flowMap));
let startNodeLabel = "START";
let detailTablesMd = "";
let startElementReference = "";
if (flowObj.start) {
const startObjCopy = simplifyNode(Object.assign({}, flowObj.start.flowNodeDescription || flowObj.start));
delete startObjCopy.flowNodeDescription;
flowObjCopy = Object.assign({}, startObjCopy, flowObjCopy);
delete flowObjCopy.start;
delete flowObjCopy.type;
detailTablesMd += handleSchedule(flowObjCopy, Object.keys(flowMap));
detailTablesMd += handleScheduledPaths(flowObjCopy, Object.keys(flowMap));
detailTablesMd += handleFilterItems(flowObjCopy, Object.keys(flowMap));
if (flowObjCopy.processType === "Flow") {
startNodeLabel = "START<br/>" + "<b>Screen Flow</b>";
}
else if (flowObjCopy.processType === "AutoLaunchedFlow") {
startNodeLabel = "START<br/>" + "<b>AutoLaunched Flow</b></br>" +
(flowObjCopy.triggerType ? "Type: <b>" + stringifyValue(flowObjCopy.triggerType, "triggerType", Object.keys(flowObjCopy)) + "</b><br/>" : '');
}
else {
startNodeLabel = "START<br/>" +
(flowObjCopy.object ? "<b>" + stringifyValue(flowObjCopy.object, "object", Object.keys(flowObjCopy)) + "</b><br/>" : '') +
(flowObjCopy.triggerType ? "Type: <b>" + stringifyValue(flowObjCopy.triggerType, "triggerType", Object.keys(flowObjCopy)) + "</b><br/>" : '') +
(flowObjCopy.recordTriggerType ? "On: <b>" + stringifyValue(flowObjCopy.recordTriggerType, "recordTriggerType", Object.keys(flowObjCopy)) + "</b><br/>" : '');
}
}
else if (metadataValue && metadataValue.TriggerType && metadataValue.ObjectType) {
startNodeLabel = "START<br/>" + "<b>" + metadataValue.ObjectType + "</b></br>" +
(metadataValue.TriggerType ? "Type: <b>" + stringifyValue(metadataValue.TriggerType, "triggerType", Object.keys(flowObjCopy)) + "</b><br/>" : '');
startElementReference = flowObj.startElementReference;
}
else if (flowObj.startElementReference) {
startElementReference = flowObj.startElementReference;
}
const generalInfoMd = mdEndSection(buildGenericMarkdownTable(flowObjCopy, ["allFields"], "## General Information", Object.keys(flowMap)) + detailTablesMd);
if (startNodeLabel.endsWith("<br/>")) {
startNodeLabel = startNodeLabel.slice(0, -5);
}
return {
generalInfoMd: generalInfoMd,
startNodeLabel: startNodeLabel,
startFingerPrint: startFingerPrint,
startElementReference: startElementReference
};
}
function getVariablesMd(vars) {
if (vars && vars.length > 0) {
return mdEndSection(buildCustomMarkdownTable(vars, ["name", "dataType", "isCollection", "isInput", "isOutput", "objectType", "description"], "## Variables", []));
}
return "";
}
function getConstantsMd(constants) {
if (constants && constants.length > 0) {
return mdEndSection(buildCustomMarkdownTable(constants, ["name", "dataType", "value", "description"], "## Constants", []));
}
return "";
}
function getFormulasMd(formulas) {
if (formulas && formulas.length > 0) {
return mdEndSection(buildCustomMarkdownTable(formulas, ["name", "dataType", "expression", "description"], "## Formulas", []));
}
return "";
}
function getTemplatesMd(textTemplates) {
if (textTemplates && textTemplates.length > 0) {
return mdEndSection(buildCustomMarkdownTable(textTemplates, ["name", "text", "description"], "## Text Templates", []));
}
return "";
}
function getMermaidClasses() {
let classStr = "";
for (const property in NODE_CONFIG) {
classStr += "classDef " + property + " fill:" + NODE_CONFIG[property].background + ",color:" + NODE_CONFIG[property].color + ",text-decoration:none,max-height:100px\n";
}
return classStr;
}
//# sourceMappingURL=flowParser.js.map