powerplatform-review-tool
Version:
Evaluate Power Platform solution zip files based on best practice patterns
1,051 lines (1,050 loc) • 53.9 kB
JavaScript
"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: [],