UNPKG

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
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 VsCode, 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" + "%% - At last resort, you can copy-paste this MermaidJS code in 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