UNPKG

powerplatform-review-tool

Version:

Evaluate Power Platform solution zip files based on best practice patterns

1,051 lines (1,050 loc) 53.9 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.generateTablesJSON = void 0; exports.getAppDescription = getAppDescription; exports.extractSolutionDetail = extractSolutionDetail; /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable sonarjs/no-duplicate-string */ const jszip_1 = __importDefault(require("jszip")); const ManifestConstant_1 = require("../ManifestConstant"); const logger_1 = __importStar(require("../utilities/logger")); const typeMap_1 = require("../schema/typeMap"); const reviewPowerAppsComponents_1 = require("./reviewPowerAppsComponents"); const appHelper_1 = require("../utilities/appHelper"); const xmldom_1 = require("@xmldom/xmldom"); const xpath_1 = __importDefault(require("xpath")); const xmlHelper_1 = require("../utilities/xmlHelper"); const bothelper_1 = require("../utilities/bothelper"); // Define a constant for the MIME type const XML_MIME_TYPE = "application/xml"; // Define the Category mappings using an object const WorkflowCategory = { 0: "Workflow", 1: "Dialog", 2: "Business Rule", 3: "Action", 4: "Business Process Flow", 5: "Modern Flow", 6: "Desktop Flow", 7: "AI Flow", }; // eslint-disable-next-line sonarjs/cognitive-complexity async function extractCanvasAppFlowsAndTables(zipContent) { const result = []; const mermaidExpressions = []; try { // eslint-disable-next-line sonarjs/no-duplicate-string const customizationsFile = zipContent.file("customizations.xml"); if (!customizationsFile) { logger_1.default.error("No customizations.xml file found in the solution."); return { mappings: result, mermaidExpressions }; } const xmlContent = await customizationsFile.async("string"); const parser = new xmldom_1.DOMParser(); const xmlDoc = (0, xmlHelper_1.safeParseXml)(xmlContent); if (!xmlDoc) { throw new Error("Failed to parse XML"); } const canvasApps = xpath_1.default.select("//CanvasApps/CanvasApp", xmlDoc).filter((node) => node.nodeType === 1); canvasApps.forEach((canvasApp, index) => { const nameNode = xpath_1.default.select1("Name", canvasApp); const displayNameNode = xpath_1.default.select1("DisplayName", canvasApp); const appName = nameNode?.textContent || `App_${index}`; const displayName = displayNameNode?.textContent || appName; const flows = []; const tables = []; // Extract Flows from ConnectionReferences using XPath const connectionRefsNode = xpath_1.default.select1("ConnectionReferences", canvasApp); const connectionRefsText = connectionRefsNode?.textContent || ""; try { const connectionData = JSON.parse(connectionRefsText); Object.values(connectionData).forEach((conn) => { if (conn.id?.includes("shared_logicflows") && conn.parameterHints?.workflowDisplayName?.value) { flows.push(conn.parameterHints.workflowDisplayName.value); } }); } catch (e) { console.warn("Failed to parse ConnectionReferences JSON:", e); } const dbRefsNode = xpath_1.default.select1("DatabaseReferences", canvasApp); const dbRefsText = dbRefsNode?.textContent || ""; try { const dbData = JSON.parse(dbRefsText); Object.values(dbData).forEach((db) => { if (db.dataSources) { Object.values(db.dataSources).forEach((source) => { if (source.entitySetName) { tables.push(source.entitySetName); } }); } }); } catch (e) { console.warn("Failed to parse DatabaseReferences JSON:", e); } result.push({ appName, displayName, flows, tables }); mermaidExpressions.push(generateMermaidDiagramForApp(appName, displayName, flows, tables, index)); }); return { mappings: result, mermaidExpressions }; } catch (error) { logger_1.default.error("Error processing customizations.xml:", error); return { mappings: result, mermaidExpressions }; } } function generateMermaidDiagramForApp(appName, displayName, flows, tables, appIndex) { let mermaidGraph = `---\nconfig:\n look: handDrawn\n---\n`; mermaidGraph += `%%{init: {'theme': 'neutral'}}%%\n\n`; mermaidGraph += `graph TB;\n`; mermaidGraph += ` classDef CanvasApps fill:#e7bfcf, stroke-width:0px,font-size: 24px;\n\n`; mermaidGraph += ` subgraph Solution[\"${displayName}\"]\n`; mermaidGraph += ` style Solution stroke-width:0px\n`; // PowerApps Cluster mermaidGraph += ` subgraph AppCluster[\"PowerApps\"]\n`; mermaidGraph += ` style AppCluster fill:#f5f5f5,stroke-width:0px;\n`; mermaidGraph += ` App${appIndex}[\"${displayName}\"]:::CanvasApps\n`; mermaidGraph += ` end\n\n`; // Power Automate Cluster (if any flows exist) if (flows.length > 0) { mermaidGraph += ` subgraph FlowCluster[\"Power Automate\"]\n`; mermaidGraph += ` style FlowCluster fill:#edf5fa,stroke-width:0px;\n`; flows.forEach((flow, flowIndex) => { mermaidGraph += ` Flow${appIndex}_${flowIndex}[\"${flow}\"]\n`; }); mermaidGraph += ` end\n\n`; // Link App to Flows flows.forEach((_, flowIndex) => { mermaidGraph += ` App${appIndex} -->|depends on| Flow${appIndex}_${flowIndex}\n`; }); } // Dataverse Cluster (if any tables exist) if (tables.length > 0) { mermaidGraph += ` subgraph TableCluster[\"Dataverse Tables\"]\n`; mermaidGraph += ` style TableCluster fill:#edfaf1,stroke-width:0px;\n`; tables.forEach((table, tableIndex) => { mermaidGraph += ` Table${appIndex}_${tableIndex}[\"${table}\"]\n`; }); mermaidGraph += ` end\n\n`; // Link App to Tables tables.forEach((_, tableIndex) => { mermaidGraph += ` App${appIndex} -->|depends on| Table${appIndex}_${tableIndex}\n`; }); } mermaidGraph += ` end\n`; return mermaidGraph; } // New function to extract solution overview async function extractSolutionOverview(zipContent) { const solutionFile = zipContent.file("solution.xml"); if (solutionFile) { const solutionXmlContent = await solutionFile.async("string"); // Parse the XML content const parser = new xmldom_1.DOMParser(); const xmlDoc = parser.parseFromString(solutionXmlContent, XML_MIME_TYPE); const uniqueName = xmlDoc.getElementsByTagName("UniqueName")[0]?.textContent || ""; const localizedNameNode = xpath_1.default.select1("//LocalizedNames/LocalizedName", xmlDoc); const localizedName = localizedNameNode?.getAttribute("description") || ""; const descriptionNode = xpath_1.default.select1("//Descriptions/Description", xmlDoc); const description = descriptionNode?.getAttribute("description") || ""; const version = xmlDoc.getElementsByTagName("Version")[0]?.textContent || ""; const solutionType = xmlDoc.getElementsByTagName("Managed")[0]?.textContent === "0" ? "Unmanaged" : "Managed"; return { displayName: localizedName, uniqueName: uniqueName, description: description, version: version, solutionType: solutionType, }; } else { console.error("solution.xml file not found."); return null; } } function generateERDiagram(solution) { const { tables } = solution; let erDiagram = `erDiagram\n`; tables.forEach((table) => { erDiagram += ` ${table.SchemaName} {\n`; // Add Fields table.Fields.forEach((field) => { erDiagram += ` ${field.DataType} ${field.Name}\n`; }); erDiagram += ` }\n`; }); // Add Relationships tables.forEach((table) => { table.Relationships.OneToMany.forEach((rel) => { erDiagram += ` ${rel.ReferencingEntity} ||--o| ${rel.ReferencedEntity} : "${rel.SchemaName}"\n`; }); table.Relationships.ManyToMany.forEach((rel) => { erDiagram += ` ${rel.Entity1LogicalName} }|--|{ ${rel.Entity2LogicalName} : "${rel.SchemaName}"\n`; }); table.Relationships.ManyToOne.forEach((rel) => { erDiagram += ` ${rel.ReferencedEntity} o|--|| ${rel.ReferencedEntity} : "${rel.SchemaName}"\n`; }); }); return erDiagram; } // eslint-disable-next-line sonarjs/cognitive-complexity function generateMermaidDiagram(solution) { const MAX_COMPONENTS = 3; // Limit the number of components per cluster const { solutionOverview, canvasApps, workflows, tables } = solution; let mermaidGraph = `%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#ffffff', 'primaryBorderColor': '#333333', 'clusterBkg': '#EBAED5', /* PowerApps */ 'clusterBorder': '#D38EBB', 'secondaryClusterBkg': '#72BDFD', /* Power Automate */ 'secondaryClusterBorder': '#5595DA', 'tertiaryClusterBkg': '#107C10', /* Dataverse */ 'tertiaryClusterBorder': '#0E6A0E', 'fontFamily': 'Arial' }}}%%\n\n`; mermaidGraph += `graph TB;\n Solution["\ud83c\udf81 <b>Solution: ${solutionOverview.displayName}</b>"]\n\n`; // Helper function to process components with limit const processComponents = (components, prefix, clusterName, color, borderColor) => { let componentString = ` subgraph ${prefix}Cluster["${clusterName}"]\n`; componentString += ` style ${prefix}Cluster fill:${color},stroke:${borderColor},stroke-width:2px;\n`; const limitedComponents = components.slice(0, MAX_COMPONENTS); limitedComponents.forEach((component, index) => { const name = component.appName || component.displayName || component.name || component.SchemaName || "Unnamed"; componentString += ` ${prefix}${index}["${name}"]\n`; }); const remaining = components.length - MAX_COMPONENTS; if (remaining > 0) { componentString += ` ${prefix}More["\ud83d\udcc4 ... +${remaining} more"]\n`; } componentString += " end\n"; componentString += ` Solution -->|Contains| ${prefix}Cluster\n\n`; return componentString; }; // PowerApps Cluster if (canvasApps.length) { mermaidGraph += processComponents(canvasApps, "App", "PowerApps", "#b45792", "#D38EBB"); } // Power Automate Cluster if (workflows.length) { mermaidGraph += processComponents(workflows, "Flow", "Power Automate", "#72BDFD", "#5595DA"); } // Dataverse Tables Cluster if (tables.length) { mermaidGraph += processComponents(tables, "Table", "Dataverse Tables", "#107C10", "#0E6A0E"); } return mermaidGraph; } async function extractAppCheckerSarifFile(appZip) { const sarifFile = appZip.file("AppCheckerResult.sarif"); if (sarifFile) { try { const sarifContent = await sarifFile.async("string"); return JSON.parse(sarifContent); } catch (error) { (0, logger_1.logError)(error, "Error parsing AppCheckerResult.sarif"); } } return undefined; } // New function to extract YAML files async function extractYamlFiles(appZip) { const srcFolder = appZip.folder("Src"); const screens = {}; if (srcFolder) { const srcFiles = Object.keys(srcFolder.files).filter((key) => key.endsWith(".pa.yaml") && !srcFolder.files[key].dir); for (const filePath of srcFiles) { const file = appZip.file(filePath); if (file) { const yamlContent = await file.async("string"); const screenName = filePath.split(/[/\\]/).pop()?.replace(".pa.yaml", ""); console.log(screenName); if (screenName) { screens[screenName] = { screenName: screenName, screenYaml: yamlContent }; console.log(`Processed File: ${filePath}, Screen: ${screenName}`); } } else { console.error(`File not found: ${filePath}`); } } } else { console.error("Src folder not found inside the .msapp file."); } return screens; } async function processMsAppFile(file, canvasAppsList) { try { const appZipContent = await file.async("arraybuffer"); const appZip = await jszip_1.default.loadAsync(appZipContent); const msapp = (0, appHelper_1.createMsApp)(); await Promise.all([(0, reviewPowerAppsComponents_1.readPropertiesFile)(appZip, msapp)]); const appName = msapp.appSettings.Name.toLowerCase().endsWith(".msapp") ? decodeURI(msapp.appSettings.Name.substring(0, msapp.appSettings.Name.length - 6)) .replace(/%2B/g, " ") .replace(/%2b/g, "+") || msapp.appSettings.Name || "Unknown App Name" : msapp.appSettings.Name; const appDescription = msapp.appSettings.AppDescription || ""; // Extract YAML files (assuming it returns an object with screen names as keys) const screensObject = await extractYamlFiles(appZip); // ExtractSaifFile const sarifFiles = await extractAppCheckerSarifFile(appZip); // Convert the screens object to an array of ScreenDetails const screensArray = Object.values(screensObject); // Create a new CanvasApp object const canvasApp = { appName: appName, appDescription: appDescription, screens: screensArray, sarifLog: sarifFiles, }; // Add the new app to the list canvasAppsList.push(canvasApp); } catch (error) { console.error(`Error processing app ${file.name}:`, error); } } // Function to process CanvasApps folder in the solution async function extractCanvasAppsDetails(zipContent) { const canvasAppsFolder = zipContent.folder("CanvasApps"); const result = { canvasApps: [] }; if (!canvasAppsFolder) { console.error("No CanvasApps folder found in the solution."); return result; } const processingPromises = []; canvasAppsFolder.forEach((relativePath, file) => { if (relativePath.endsWith(".msapp")) { processingPromises.push(processMsAppFile(file, result.canvasApps)); } }); await Promise.all(processingPromises); return result; } async function getAppDescription(appZip) { const propertiesFile = appZip.file("Properties.json"); if (propertiesFile) { try { const propertiesContent = await propertiesFile.async("string" /* StringConstants.STRING */); const propertiesJson = JSON.parse(propertiesContent); return propertiesJson.AppDescription; } catch (error) { (0, logger_1.logError)(error, "Error parsing Properties.json"); return ""; } } else { logger_1.default.error("Properties.json file not found in the .msapp file."); return ""; } } // eslint-disable-next-line sonarjs/cognitive-complexity /** @public */ async function extractSolutionDetail(solutionZipFile, solutionName, webapi) { try { let zipContent; // Uncomment before publishing // Retrieve solution zip via ExportSolution if (solutionName) { const exportSolutionRequest = { SolutionName: solutionName, Managed: false, getMetadata: () => ({ boundParameter: null, parameterTypes: { SolutionName: { typeName: "Edm.String", structuralProperty: 1 }, Managed: { typeName: "Edm.Boolean", structuralProperty: 1 }, }, operationType: 0, // Action operationName: "ExportSolution", }), }; try { // eslint-disable-next-line @typescript-eslint/no-explicit-any const response = await webapi.execute(exportSolutionRequest); const responseBody = await response.json(); if (responseBody?.ExportSolutionFile) { const exportedSolution = responseBody.ExportSolutionFile; // Decode the Base64-encoded ExportSolutionFile into a Uint8Array (binary format) for JSZip processing. const base64ToUint8Array = (base64) => { const binaryString = typeof atob === "function" ? atob(base64) : Buffer.from(base64, "base64").toString("binary"); return Uint8Array.from(binaryString, (char) => char.charCodeAt(0)); }; const exportedSolutionBuffer = base64ToUint8Array(exportedSolution); const zip = new jszip_1.default(); zipContent = await zip.loadAsync(new File([new Blob([exportedSolutionBuffer.buffer], { type: "application/zip" })], solutionName)); } else { console.error("No solution found or failed to export the solution."); return; } } catch (error) { if (error instanceof Error) { console.error("Error exporting the solution:", error.message, error.stack); } else { console.error("Unknown error occurred during solution export:", error); } return; } } else if (solutionZipFile) { const zip = new jszip_1.default(); zipContent = await zip.loadAsync(solutionZipFile); } if (zipContent) { // Extract Canvas Apps Details const canvasAppsDetails = await extractCanvasAppsDetails(zipContent); // Extract Solution Overview const solutionOverview = await extractSolutionOverview(zipContent); // Extract Workflows Details const workflowsDetails = await extractWorkflowsDetails(zipContent); // Extract Code Component Details const codeComponentDetails = await extractCodeComponentDetails(zipContent); // Extract Custom API Details const customApiDetails = await extractCustomApiDetails(zipContent); // Extract Environment Variable Details const environmentVariableDetails = await extractEnvironmentVariableDetails(zipContent); // Extract Security Roles const securityRolesDetails = { roles: [] }; await extractSecurityRolesFile(zipContent, securityRolesDetails); // Extract Plugin Details const result = { plugins: [] }; await extractPluginDetailsFromXML(zipContent, result); // Extract Connection Reference Details const connectionReferenceResult = { connectionReferences: [] }; await extractConnectionReferenceDetails(zipContent, connectionReferenceResult); // Extract AI Model Details const aiModelResult = { aiModels: [] }; await extractAIModelDetailsFromXML(zipContent, aiModelResult); // Extract Missing Dependencies Details const missingDependenciesResult = { missingDependencies: [] }; await extractMissingDependenciesFromXML(zipContent, missingDependenciesResult); // Extract Table Metadata let tablesDetails; try { tablesDetails = await (0, exports.generateTablesJSON)(zipContent); // Commented fetching Tables metadata using Custom Action // tablesDetails = await generateTablesJSONFromAPI(webapi, zipContent); } catch (error) { tablesDetails = { tables: [] }; } // Extract Canvas Apps Details with associated flows and tables const { mappings: canvasAppMappings, mermaidExpressions: generatedMermaidExpressions } = await extractCanvasAppFlowsAndTables(zipContent); const { agents } = await (0, bothelper_1.extractAgentsDetails)(zipContent); // Build the final result const finalResult = { solutionOverview: solutionOverview, canvasApps: canvasAppsDetails.canvasApps, tables: tablesDetails.tables, workflows: workflowsDetails.workflows, codeComponents: codeComponentDetails.codeComponents, customApis: customApiDetails.customApis, environmentVariables: environmentVariableDetails.environmentVariables, securityRoles: securityRolesDetails.roles, plugins: result.plugins, connectionReferences: connectionReferenceResult.connectionReferences, aiModels: aiModelResult.aiModels, missingDependencies: missingDependenciesResult.missingDependencies, agents, }; const mermaidSAExpression = generateMermaidDiagram(finalResult); const mermaidERDiagram = generateERDiagram(finalResult); console.log("Extracted Solution Details:", { mermaidSolutionOverviewExpression: mermaidSAExpression, mermaidERDiagram: mermaidERDiagram, mermaidSolutionArchitectureExpressions: generatedMermaidExpressions, ...finalResult, }); return { mermaidSolutionOverviewExpression: mermaidSAExpression, mermaidERDiagramExpression: mermaidERDiagram, mermaidSolutionArchitectureExpressions: generatedMermaidExpressions, ...finalResult, }; } else { throw new Error("Invalid solution ZIP file."); } } catch (err) { if (err instanceof Error) { console.error("An error occurred during review:", err.message, err.stack); } else { console.error("Unknown error during review:", err); } } } async function extractMissingDependenciesFromXML(zipContent, result) { try { const customizationsFile = zipContent.file("solution.xml"); if (!customizationsFile) { console.error("No solution.xml found in the solution."); return; } const xmlContent = await customizationsFile.async("string"); const parser = new xmldom_1.DOMParser(); const xmlDoc = (0, xmlHelper_1.safeParseXml)(xmlContent); if (!xmlDoc) return; const missingDependencies = xpath_1.default.select("//SolutionManifest/MissingDependencies/MissingDependency", xmlDoc).filter((node) => node.nodeType === 1); for (const dependency of missingDependencies) { const requiredNode = xpath_1.default.select1("Required", dependency); const dependentNode = xpath_1.default.select1("Dependent", dependency); const requiredType = parseInt(requiredNode?.getAttribute("type") || "0", 10); const dependentType = parseInt(dependentNode?.getAttribute("type") || "0", 10); result.missingDependencies.push({ Required: { Type: typeMap_1.typeMap[requiredType] || "", SchemaName: requiredNode?.getAttribute("schemaName") || "", DisplayName: requiredNode?.getAttribute("displayName") || "", Solution: requiredNode?.getAttribute("solution") || "", }, Dependent: { Type: typeMap_1.typeMap[dependentType] || "", SchemaName: dependentNode?.getAttribute("schemaName") || "", DisplayName: dependentNode?.getAttribute("displayName") || "", }, }); } } catch (error) { console.error("Error processing missing dependencies:", error); } } async function extractAIModelDetailsFromXML(zipContent, result) { try { const customizationFileName = "customizations.xml"; const customizationsFile = zipContent.file(customizationFileName); if (!customizationsFile) return; const xmlContent = await customizationsFile.async("string"); const parser = new xmldom_1.DOMParser(); const xmlDoc = (0, xmlHelper_1.safeParseXml)(xmlContent); if (!xmlDoc) return; const aiModelNodes = xpath_1.default.select("//AIModels/AIModel", xmlDoc); aiModelNodes .filter((node) => node.nodeType === 1) .forEach((aiModel) => { const aiModelName = xpath_1.default.select1("string(msdyn_name)", aiModel); // Try to extract raw JSON string from custom configuration const customConfigRaw = xpath_1.default.select1("string(AIConfigurations/AIConfiguration/msdyn_customconfiguration)", aiModel); let modelType = ""; if (customConfigRaw) { try { const parsedConfig = JSON.parse(customConfigRaw); modelType = parsedConfig?.modelParameters?.modelType || ""; } catch (jsonError) { console.warn("Failed to parse msdyn_customconfiguration JSON:", jsonError); } } result.aiModels.push({ Name: aiModelName, Model: modelType, }); }); } catch (error) { console.error("Error processing AI models from customizations.xml:", error); } } async function extractConnectionReferenceDetails(zipContent, result) { try { // 1. Read the 'customizations.xml' file from the root folder const customizationsFile = zipContent.file(ManifestConstant_1.customizationFileName); if (!customizationsFile) { return; } // 2. Read the content of the customizations.xml file const xmlContent = await customizationsFile.async("string"); // Parse the XML content const xmlDoc = (0, xmlHelper_1.safeParseXml)(xmlContent); if (!xmlDoc) return; // 3. Extract all <connectionreference> nodes within the <connectionreferences> node const connectionReferences = xpath_1.default.select("//connectionreferences/connectionreference", xmlDoc); // Filter to Elements only and process each connectionReferences .filter((node) => node.nodeType === 1) .forEach((connectionReference) => { const logicalName = connectionReference.getAttribute("connectionreferencelogicalname") || ""; const displayNameNode = xpath_1.default.select1("connectionreferencedisplayname", connectionReference); const connectorIDNode = xpath_1.default.select1("connectorid", connectionReference); const displayName = displayNameNode?.textContent || ""; const connectorID = connectorIDNode?.textContent || ""; console.log({ logicalName, displayName, connectorID }); }); } catch (error) { console.error("Error processing connection references:", error); } } async function extractSecurityRolesFile(zipContent, result) { try { // 1. Read the 'customizations.xml' file from the root folder const customizationsFile = zipContent.file(ManifestConstant_1.customizationFileName); if (!customizationsFile) { return; } // 2. Read the content of the customizations.xml file const xmlContent = await customizationsFile.async("string"); // Parse the XML content const parser = new xmldom_1.DOMParser(); const xmlDoc = (0, xmlHelper_1.safeParseXml)(xmlContent); if (!xmlDoc) return; // Select all <Role> nodes inside <Roles> const roleNodes = xpath_1.default.select("//Roles/Role", xmlDoc); // Filter only Element nodes and process each roleNodes .filter((node) => node.nodeType === 1) .forEach((role) => { const id = role.getAttribute("id") || ""; const name = role.getAttribute("name") || ""; // Add the extracted details to the result list result.roles.push({ name: name, }); }); } catch (error) { console.error(`Error processing security roles:`, error); } } async function extractPluginDetailsFromXML(zipContent, result) { try { // 1. Read the 'customizations.xml' file from the root folder const customizationsFile = zipContent.file(ManifestConstant_1.customizationFileName); if (!customizationsFile) { return; } // 2. Read the content of the customizations.xml file const xmlContent = await customizationsFile.async("string"); // 3. Parse the XML content const xmlDoc = (0, xmlHelper_1.safeParseXml)(xmlContent); if (!xmlDoc) return; // Step 1: Select all <PluginAssembly> nodes const pluginAssemblyNodes = xpath_1.default.select("//SolutionPluginAssemblies/PluginAssembly", xmlDoc).filter((node) => node.nodeType === 1); // Step 2: For each PluginAssembly, select nested <PluginType> nodes pluginAssemblyNodes.forEach((pluginAssembly) => { const pluginTypeNodes = xpath_1.default.select("PluginTypes/PluginType", pluginAssembly).filter((node) => node.nodeType === 1); pluginTypeNodes.forEach((pluginType) => { const pluginName = pluginType.getAttribute("Name") || ""; result.plugins.push({ Name: pluginName }); }); }); } catch (error) { console.error("Error processing plugins from customizations.xml:", error); } } // eslint-disable-next-line sonarjs/cognitive-complexity async function extractWorkflowsDetails(zipContent) { const result = { workflows: [] }; const customizationsFile = zipContent.file("customizations.xml"); if (!customizationsFile) { console.error("No customizations.xml file found in the solution."); return result; } else { console.log("customizations.xml file found. Parsing XML..."); } const customizationsXmlContent = await customizationsFile.async("string"); const parser = new xmldom_1.DOMParser(); let xmlDoc; try { xmlDoc = parser.parseFromString(customizationsXmlContent, XML_MIME_TYPE); } catch (error) { console.error("Failed to parse customizations.xml content:", error); return result; } const workflowsNode = xpath_1.default.select1("//ImportExportXml/Workflows", xmlDoc); if (!workflowsNode) { console.error("No Workflows node found in the customizations.xml file."); return result; } const workflowNodes = xpath_1.default.select("Workflow", workflowsNode); const workflowArray = Array.from(workflowNodes); for (const workflowNode of workflowArray) { try { const name = workflowNode.getAttribute("Name") ?? "None"; const primaryEntity = xpath_1.default.select1("PrimaryEntity", workflowNode)?.textContent ?? "None"; const categoryText = xpath_1.default.select1("Category", workflowNode)?.textContent ?? "0"; const categoryValue = parseInt(categoryText, 10); const categoryLabel = WorkflowCategory[categoryValue]; const workflowDetail = { name, primaryEntity, category: categoryLabel, json: undefined, }; if (categoryValue === 5) { let JsonFileName = xpath_1.default.select1("JsonFileName", workflowNode)?.textContent ?? ""; if (JsonFileName) { JsonFileName = JsonFileName.startsWith("/") ? JsonFileName.slice(1) : JsonFileName; const jsonFile = zipContent.file(JsonFileName); if (jsonFile) { try { const fileContent = await jsonFile.async("string"); workflowDetail.json = fileContent; } catch (error) { console.error(`Failed to read or parse JSON from ${JsonFileName}:`, error); } } else { console.warn(`JSON file ${JsonFileName} referenced but not found in the zip.`); } } else { console.warn("No JsonFileName specified for workflow with category 5."); } } result.workflows.push(workflowDetail); } catch (error) { console.error(`Error processing workflow node:`, error); } } if (result.workflows.length === 0) { console.warn("No workflows processed. The solution may not contain valid workflows."); } else { console.log(`Successfully processed ${result.workflows.length} workflows.`); } return result; } async function extractCodeComponentDetails(zipContent) { const result = { codeComponents: [] }; const controlsFolder = zipContent.folder("Controls"); if (!controlsFolder) return result; const processingPromises = []; controlsFolder.forEach((relativePath, file) => { if (relativePath.endsWith("ControlManifest.xml")) { processingPromises.push((async () => { try { const manifestContent = await file.async("string"); const parser = new xmldom_1.DOMParser(); const xmlDoc = parser.parseFromString(manifestContent, XML_MIME_TYPE); const controlNode = xpath_1.default.select1("//control", xmlDoc); const displayName = controlNode?.getAttribute("display-name-key") ?? ""; const description = controlNode?.getAttribute("description-key") ?? ""; const type = controlNode?.getAttribute("control-type") ?? ""; const version = controlNode?.getAttribute("version") ?? ""; result.codeComponents.push({ displayName, description, type, version, }); } catch (error) { console.error(`Error processing ControlManifest.xml in ${relativePath}:`, error); } })()); } }); await Promise.all(processingPromises); return result; } async function extractCustomApiDetails(zipContent) { const result = { customApis: [] }; // Check for 'customapis' folder const customApisFolder = zipContent.folder("customapis"); if (!customApisFolder) { console.error("No customapis folder found in the solution."); return result; } const processingPromises = []; // Iterate through all files and subfolders in 'customapis' customApisFolder.forEach((relativePath, file) => { // If it's a file and ends with 'customapi.xml', process it if (!file.dir && relativePath.endsWith("customapi.xml")) { processingPromises.push(processCustomApiFile(file, relativePath, result)); } // If it's a subfolder, process the subfolder else if (file.dir) { processingPromises.push(processSubfolder(relativePath, file, zipContent, result)); } }); // Wait for all promises to resolve await Promise.all(processingPromises); console.log(`Successfully processed ${result.customApis.length} custom APIs.`); return result; } // Helper function to process a subfolder async function processSubfolder(relativePath, file, zipContent, result) { const subfolder = zipContent.folder(relativePath); if (subfolder) { const customApiFile = subfolder.file("customapi.xml"); if (customApiFile) { await processCustomApiFile(customApiFile, relativePath, result); } else { console.error(`customapi.xml not found in ${relativePath}. Skipping this folder.`); } } } // Helper function to process the 'customapi.xml' file async function processCustomApiFile(customApiFile, relativePath, result) { try { const apiContent = await customApiFile.async("string"); const parser = new xmldom_1.DOMParser(); const xmlDoc = parser.parseFromString(apiContent, XML_MIME_TYPE); const displayNameNode = xpath_1.default.select1("//customapi/displayname", xmlDoc); const descriptionNode = xpath_1.default.select1("//customapi/description", xmlDoc); const isPrivateNode = xpath_1.default.select1("//customapi/isprivate", xmlDoc); const isFunctionNode = xpath_1.default.select1("//customapi/isfunction", xmlDoc); const displayName = displayNameNode?.getAttribute("default") ?? ""; const description = descriptionNode?.getAttribute("default") ?? ""; const isPrivate = isPrivateNode?.textContent === "1"; const isFunction = isFunctionNode?.textContent === "1"; result.customApis.push({ displayName, description, isPrivate, isFunction, }); } catch (error) { console.error(`Error processing customapi.xml in ${relativePath}:`, error); } } async function extractEnvironmentVariableDetails(zipContent) { const result = { environmentVariables: [] }; // Check for 'environmentvariabledefinitions' folder const environmentVariablesFolder = zipContent.folder("environmentvariabledefinitions"); if (!environmentVariablesFolder) { console.error("No environmentvariabledefinitions folder found in the solution."); return result; } const processingPromises = []; // Iterate through all files and subfolders in 'environmentvariabledefinitions' environmentVariablesFolder.forEach((relativePath, file) => { // If it's a subfolder (directory structure), process the subfolder if (file.dir) { processingPromises.push(processEVSubfolder(relativePath, file, zipContent, result)); } // If it's a file, check if it's 'environmentvariabledefinition.xml' else if (relativePath.endsWith("environmentvariabledefinition.xml")) { processingPromises.push(processEnvironmentVariableFile(file, relativePath, result)); } }); // Wait for all promises to resolve await Promise.all(processingPromises); return result; } // Helper function to process EV subfolder async function processEVSubfolder(relativePath, file, zipContent, result) { const subfolder = zipContent.folder(relativePath); if (subfolder) { const environmentVariableFile = subfolder.file("environmentvariabledefinition.xml"); if (environmentVariableFile) { await processEnvironmentVariableFile(environmentVariableFile, relativePath, result); } else { console.error(`environmentvariabledefinition.xml not found in ${relativePath}. Skipping this folder.`); } } } // Helper function to process the 'environmentvariabledefinition.xml' file async function processEnvironmentVariableFile(environmentVariableFile, relativePath, result) { try { const apiContent = await environmentVariableFile.async("string"); const parser = new xmldom_1.DOMParser(); const xmlDoc = parser.parseFromString(apiContent, XML_MIME_TYPE); const displayNameNode = xpath_1.default.select1("//environmentvariabledefinition/displayname", xmlDoc); const descriptionNode = xpath_1.default.select1("//environmentvariabledefinition/displayname/label", xmlDoc); const typeNode = xpath_1.default.select1("//environmentvariabledefinition/type", xmlDoc); const displayName = displayNameNode?.getAttribute("default") ?? ""; const description = descriptionNode?.getAttribute("description") ?? ""; const typeValue = typeNode?.textContent ?? ""; const typeMap = { "100000000": "String", "100000001": "Number", "100000002": "Boolean", "100000003": "JSON", "100000004": "Data Source", "100000005": "Secret", }; const typeString = typeMap[typeValue] ?? "Unknown"; result.environmentVariables.push({ displayName, description, type: typeString, }); } catch (error) { console.error(`Error processing environmentvariabledefinition.xml in ${relativePath}:`, error); } } async function extractRelationshipsFromXML(xmlDoc, schemaName) { const relationships = { OneToMany: [], ManyToOne: [], ManyToMany: [], }; try { const entityRelationships = xpath_1.default.select("//EntityRelationships/EntityRelationship", xmlDoc); for (const node of entityRelationships) { if (node.nodeType !== 1) continue; // Skip non-element nodes const element = node; //const relationshipType = xpath.select1("string(EntityRelationshipType)", element); const referencingEntityNameRaw = xpath_1.default.select1("string(ReferencingEntityName)", element); const referencedEntityNameRaw = xpath_1.default.select1("string(ReferencedEntityName)", element); const firstEntityNameRaw = xpath_1.default.select1("string(FirstEntityName)", element); const secondEntityNameRaw = xpath_1.default.select1("string(SecondEntityName)", element); const referencingEntityName = typeof referencingEntityNameRaw === "string" ? referencingEntityNameRaw : undefined; const referencedEntityName = typeof referencedEntityNameRaw === "string" ? referencedEntityNameRaw : undefined; const firstEntityName = typeof firstEntityNameRaw === "string" ? firstEntityNameRaw : undefined; const secondEntityName = typeof secondEntityNameRaw === "string" ? secondEntityNameRaw : undefined; processOneToMany(element, referencingEntityName, referencedEntityName, schemaName, relationships); processManyToOne(element, referencingEntityName, referencedEntityName, schemaName, relationships); processManyToMany(element, firstEntityName, secondEntityName, schemaName, relationships); } if (relationships.OneToMany.length === 0 && relationships.ManyToOne.length === 0 && relationships.ManyToMany.length === 0) { console.warn(`No relationships found for schema: ${schemaName}`); } } catch (error) { console.error(`Error parsing relationships for schema: ${schemaName}`, error); } return relationships; } // Helper function to process One-to-Many relationships function processOneToMany(relationship, referencingEntityName, referencedEntityName, schemaName, relationships) { const relationshipType = xpath_1.default.select1("string(EntityRelationshipType)", relationship); if (relationshipType === "OneToMany" && referencingEntityName === schemaName && (referencedEntityName?.includes("_") ?? false)) { const schemaNameAttr = relationship.getAttribute("Name") || ""; const referencingAttribute = xpath_1.default.select1("string(ReferencingAttributeName)", relationship) || ""; relationships.OneToMany.push({ SchemaName: schemaNameAttr, ReferencingEntity: referencingEntityName || "", ReferencingAttribute: referencingAttribute, ReferencedEntity: referencedEntityName || "", }); } } // Helper function to process Many-to-One relationships, considering custom relationships function processManyToOne(relationship, referencingEntityName, referencedEntityName, schemaName, relationships) { const relationshipType = xpath_1.default.select1("string(EntityRelationshipType)", relationship); if (relationshipType === "ManyToOne" && referencedEntityName === schemaName && (referencingEntityName?.includes("_") || referencedEntityName?.includes("_"))) { const schemaNameAttr = relationship.getAttribute("Name") || ""; const referencingAttribute = xpath_1.default.select1("string(ReferencingAttributeName)", relationship) || ""; relationships.ManyToOne.push({ SchemaName: schemaNameAttr, ReferencedEntity: referencedEntityName || "", ReferencingAttribute: referencingAttribute, }); } } // Helper function to process Many-to-Many relationships, considering custom relationships function processManyToMany(relationship, firstEntityName, secondEntityName, schemaName, relationships) { const relationshipType = xpath_1.default.select1("string(EntityRelationshipType)", relationship); if (relationshipType === "ManyToMany" && (firstEntityName === schemaName || secondEntityName === schemaName) && (firstEntityName?.includes("_") || secondEntityName?.includes("_"))) { const schemaNameAttr = relationship.getAttribute("Name") || ""; relationships.ManyToMany.push({ SchemaName: schemaNameAttr, Entity1LogicalName: firstEntityName || "", Entity2LogicalName: secondEntityName || "", }); } } // Helper function to extract display name and description from XML function extractTableDetailsFromXML(xmlDoc, entity) { const schemaName = xpath_1.default.select1("string(../EntityInfo/entity/@Name)", entity) || "Unknown Schema Name"; const displayName = xpath_1.default.select1("string(../EntityInfo/entity/LocalizedNames/LocalizedName[@languagecode='1033']/@description)", entity) || "Unnamed Table"; const description = xpath_1.default.select1("string(../EntityInfo/entity/Descriptions/Description[@languagecode='1033']/@description)", entity) || ""; return { displayName, description, schemaName }; } const generateTablesJSON = async (zipContent) => { console.log("Generating Tables JSON from customizations.xml"); const result = { tables: [] }; try { // 1. Read the 'customizations.xml' file from the root folder const customizationsFile = zipContent.file(ManifestConstant_1.customizationFileName); if (!customizationsFile) { return result; } // 2. Read the content of the customizations.xml file const xmlContent = await customizationsFile.async("string"); // 3. Parse the XML content const parser = new xmldom_1.DOMParser(); const xmlDoc = (0, xmlHelper_1.safeParseXml)(xmlContent); if (!xmlDoc) return result; // 4. Extract all <Entity><Name> elements using XPath const entityNameNodes = xpath_1.default.select("//Entities/Entity/Name", xmlDoc); if (entityNameNodes.length === 0) { return result; } // 5. Process each entity and fetch fields and relationships for (const entity of entityNameNodes) { const { displayName, description, schemaName } = extractTableDetailsFromXML(xmlDoc, entity); // Initialize table structure const tableDetails = { DisplayName: displayName, SchemaName: schemaName, Description: description, Fields: [], // Start with an empty fields array Relationships: { OneToMany: [],