powerplatform-review-tool
Version:
Evaluate Power Platform solution zip files based on best practice patterns
747 lines (746 loc) • 34.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.patternCheckUnusedVariables = patternCheckUnusedVariables;
exports.patternOptimizeActionCounts = patternOptimizeActionCounts;
exports.patternCheckConsistentNaming = patternCheckConsistentNaming;
exports.patternCheckNestedLoops = patternCheckNestedLoops;
exports.patternCheckConcurrencySettings = patternCheckConcurrencySettings;
exports.patternCheckTriggerCondition = patternCheckTriggerCondition;
exports.patternCheckMultipleInitializeVariable = patternCheckMultipleInitializeVariable;
exports.patternCheckStaticVariables = patternCheckStaticVariables;
exports.patternCheckPowerAppResponse = patternCheckPowerAppResponse;
exports.patternCheckScopeUsage = patternCheckScopeUsage;
exports.patternCheckDataRetrievalParameters = patternCheckDataRetrievalParameters;
exports.patternCheckForActionNote = patternCheckForActionNote;
exports.patternCheckCreateUpdateInsideLoop = patternCheckCreateUpdateInsideLoop;
const ManifestConstant_1 = require("../ManifestConstant");
const flowHelper_1 = require("../utilities/flowHelper");
// 2. Unused Variables Check
// eslint-disable-next-line sonarjs/cognitive-complexity
function patternCheckUnusedVariables(jsonContent) {
const { patternID, patternName, description, docLink, recommendation, severity, category } = ManifestConstant_1.FlowPatternDetails.UnusedVariables;
const declaredVariables = (0, flowHelper_1.getDeclaredVariables)(jsonContent);
const usedVariables = (0, flowHelper_1.getVariablesUsedInVariableActions)(jsonContent);
const unusedVariables = [];
// Recursive helper function to search for variable references throughout the entire JSON
function searchEntireJSON(obj, variableName) {
if (typeof obj === "string" && obj.includes(`variables('${variableName}')`)) {
// Return true if we find the variable reference at any point in the JSON
return true;
}
if (obj && typeof obj === "object") {
// Recursively search each value in objects and arrays
return Object.values(obj).some((value) => searchEntireJSON(value, variableName));
}
return false;
}
// Iterate over each declared variable and search for its usage in the entire JSON
declaredVariables.forEach((variable) => {
if (searchEntireJSON(jsonContent, variable.name)) {
usedVariables.add(variable.name);
}
});
// Identify declared but unused variables
declaredVariables.forEach((variable) => {
if (!usedVariables.has(variable.name)) {
unusedVariables.push(variable.name);
}
});
// console.log("Unused Variables:", unusedVariables);
return {
patternID,
patternName,
description,
status: unusedVariables.length === 0 ? "Pass" /* ManifestPropertyStatusNames.Pass */ : "Fail" /* ManifestPropertyStatusNames.Fail */,
instanceValue: unusedVariables,
failureReason: unusedVariables.length > 0 ? "Unused variables detected." : "",
recommendation,
docLinks: docLink,
severity,
category,
};
}
// 3. Action Count Optimization
// eslint-disable-next-line sonarjs/cognitive-complexity
function patternOptimizeActionCounts(jsonContent) {
const { patternID, patternName, description, docLink, recommendation, severity, category } = ManifestConstant_1.FlowPatternDetails.ActionCount;
// Recursive function to count all actions within every "actions" key in the JSON
function countAllActions(obj) {
let count = 0;
if (obj && typeof obj === "object") {
// Ensure it's an object
// Check if "actions" is present and is an object
if (obj.hasOwnProperty("actions") && typeof obj.actions === "object") {
count += Object.keys(obj.actions).length;
}
// Recursively search through all nested objects and arrays
Object.values(obj).forEach((nestedValue) => {
if (typeof nestedValue === "object" || Array.isArray(nestedValue)) {
count += countAllActions(nestedValue);
}
});
}
else if (Array.isArray(obj)) {
// Handle array of objects
obj.forEach((item) => {
count += countAllActions(item);
});
}
return count;
}
// Start counting actions from the root JSON content
const actionCount = countAllActions(jsonContent);
// console.log("Total action count:", actionCount);
const exceedsLimit = actionCount > 50;
return {
patternID,
patternName,
description,
status: !exceedsLimit ? "Pass" /* ManifestPropertyStatusNames.Pass */ : "Fail" /* ManifestPropertyStatusNames.Fail */,
instanceValue: exceedsLimit ? [`${actionCount} actions detected, consider reducing to 50 or less actions.`] : [],
failureReason: exceedsLimit ? "Action count exceeds recommended limit of 50 actions per flow." : "",
recommendation,
docLinks: docLink,
severity,
category,
};
}
// 4. Consistent Naming for Flow Components
function patternCheckConsistentNaming(jsonContent) {
const { patternID, patternName, description, docLink, recommendation, severity, category } = ManifestConstant_1.FlowPatternDetails.NamingConventions;
const actions = jsonContent?.properties?.definition?.actions || {};
const triggers = jsonContent?.properties?.definition?.triggers || {};
const inconsistentNames = [];
function checkNaming(obj, prefix) {
if (!obj.manual) {
Object.keys(obj).forEach((key) => {
if (!key.startsWith(prefix))
inconsistentNames.push(key);
});
}
}
checkNaming(triggers, "Trg_");
checkNaming(actions, "Act_");
return {
patternID,
patternName,
description,
status: inconsistentNames.length === 0 ? "Pass" /* ManifestPropertyStatusNames.Pass */ : "Warning" /* ManifestPropertyStatusNames.Warning */,
instanceValue: inconsistentNames,
failureReason: inconsistentNames.length > 0 ? "Inconsistent naming detected." : "",
recommendation,
docLinks: docLink,
severity,
category,
};
}
// 5. Check nested loops
// eslint-disable-next-line sonarjs/cognitive-complexity
function patternCheckNestedLoops(jsonContent) {
const { patternID, patternName, description, docLink, recommendation, severity, category } = ManifestConstant_1.FlowPatternDetails.NestedLoops;
const actions = jsonContent?.properties?.definition?.actions || {};
const nestedLoops = [];
const visitedActions = new Set(); // Track visited actions
function checkLoops(action, actionName, parentLoopStack) {
if (action && typeof action === "object") {
// Check if we've already visited this action
if (visitedActions.has(actionName)) {
return; // Skip if already visited
}
visitedActions.add(actionName);
const actionType = action.type;
// If the action is a loop
if (actionType === "Foreach" || actionType === "Until") {
// If there's already a loop in the stack, this is a nested loop
if (parentLoopStack.length > 0) {
const parentLoopName = parentLoopStack[parentLoopStack.length - 1];
nestedLoops.push(`${parentLoopName} -> ${actionName}`);
}
// Add this loop to the stack
parentLoopStack.push(actionName);
}
// Traverse actions within this action
if (action.actions) {
Object.keys(action.actions).forEach((nestedActionName) => {
const nestedAction = action.actions[nestedActionName];
checkLoops(nestedAction, nestedActionName, [...parentLoopStack]);
});
}
// Handle conditional actions
if (actionType === "If") {
// 'Then' branch
if (action.actions) {
Object.keys(action.actions).forEach((nestedActionName) => {
const nestedAction = action.actions[nestedActionName];
checkLoops(nestedAction, nestedActionName, [...parentLoopStack]);
});
}
// 'Else' branch
if (action.else && action.else.actions) {
Object.keys(action.else.actions).forEach((nestedActionName) => {
const nestedAction = action.else.actions[nestedActionName];
checkLoops(nestedAction, nestedActionName, [...parentLoopStack]);
});
}
}
// Handle switch cases
if (actionType === "Switch" && action.cases) {
Object.entries(action.cases).forEach(([, caseObj]) => {
if (caseObj.actions) {
Object.keys(caseObj.actions).forEach((nestedActionName) => {
const nestedAction = caseObj.actions[nestedActionName];
checkLoops(nestedAction, nestedActionName, [...parentLoopStack]);
});
}
});
}
// Handle scopes
if (actionType === "Scope" && action.actions) {
Object.keys(action.actions).forEach((nestedActionName) => {
const nestedAction = action.actions[nestedActionName];
checkLoops(nestedAction, nestedActionName, [...parentLoopStack]);
});
}
// If we added this loop to the stack, remove it after processing
if (actionType === "Foreach" || actionType === "Until") {
parentLoopStack.pop();
}
}
}
Object.keys(actions).forEach((actionName) => {
const action = actions[actionName];
checkLoops(action, actionName, []);
});
return {
patternID,
patternName,
description,
status: nestedLoops.length === 0 ? "Pass" /* ManifestPropertyStatusNames.Pass */ : "Fail" /* ManifestPropertyStatusNames.Fail */,
instanceValue: nestedLoops,
failureReason: nestedLoops.length > 0 ? "Nested loops detected." : "",
recommendation,
docLinks: docLink,
severity,
category,
};
}
// 7. Parallel Execution and Concurrency Check
// eslint-disable-next-line sonarjs/cognitive-complexity
function patternCheckConcurrencySettings(jsonContent) {
const { patternID, patternName, description, docLink, recommendation, severity, category } = ManifestConstant_1.FlowPatternDetails.Concurrency;
const missingConcurrency = [];
function traverseActions(actions) {
if (actions && typeof actions === "object") {
Object.keys(actions).forEach((actionName) => {
const action = actions[actionName];
// Check if the action is of type 'Foreach' (loop)
if (action.type === "Foreach" && !action.runtimeConfiguration?.concurrency?.repetitions) {
missingConcurrency.push(actionName);
}
// Handle different action types
if (action.type === "If") {
// 'If' action: traverse 'then' and 'else' branches
if (action.actions) {
traverseActions(action.actions);
}
if (action.else && action.else.actions) {
traverseActions(action.else.actions);
}
}
else if (action.type === "Switch" && action.cases) {
// 'Switch' action: traverse each case's actions
Object.values(action.cases).forEach((caseObj) => {
if (caseObj.actions) {
traverseActions(caseObj.actions);
}
});
}
else if (action.type === "Until" && action.actions) {
// 'Until' loop: traverse its actions
traverseActions(action.actions);
}
else if (action.type === "Scope" && action.actions) {
// 'Scope' action: traverse its actions
traverseActions(action.actions);
}
else if (action.actions) {
// Any other action with 'actions'
traverseActions(action.actions);
}
});
}
}
const actions = jsonContent?.properties?.definition?.actions || {};
traverseActions(actions);
return {
patternID,
patternName,
description,
status: missingConcurrency.length === 0 ? "Pass" /* ManifestPropertyStatusNames.Pass */ : "Fail" /* ManifestPropertyStatusNames.Fail */,
instanceValue: missingConcurrency,
failureReason: missingConcurrency.length > 0 ? "Concurrency settings missing." : "",
recommendation,
docLinks: docLink,
severity,
category,
};
}
// 8. Avoid Infinite Runs
function patternCheckTriggerCondition(jsonContent) {
const { patternID, patternName, description, docLink, recommendation, severity, category } = ManifestConstant_1.FlowPatternDetails.TriggerCondition;
const triggers = jsonContent?.properties?.definition?.triggers || {};
const infiniteRunTriggers = [];
Object.keys(triggers).forEach((triggerName) => {
const trigger = triggers[triggerName];
if (!(trigger.type === "Recurrence" || trigger.type === "Request") &&
(!trigger.conditions || trigger.conditions.length === 0)) {
infiniteRunTriggers.push(triggerName);
}
});
return {
patternID,
patternName,
description,
status: infiniteRunTriggers.length === 0 ? "Pass" /* ManifestPropertyStatusNames.Pass */ : "Fail" /* ManifestPropertyStatusNames.Fail */,
instanceValue: infiniteRunTriggers,
failureReason: infiniteRunTriggers.length > 0 ? "Missing trigger conditions to avoid unnecessary flow executions." : "",
recommendation,
docLinks: docLink,
severity,
category,
};
}
// 9. Multiple InitializeVariable Actions Check
// eslint-disable-next-line sonarjs/cognitive-complexity
function patternCheckMultipleInitializeVariable(jsonContent) {
const { patternID, patternName, description, docLink, recommendation, severity, category } = ManifestConstant_1.FlowPatternDetails.MultipleInitializeVariableActions;
let initializeVariableCount = 0;
function countInitializeVariableActions(obj) {
if (obj && typeof obj === "object") {
if (obj.type === "InitializeVariable") {
initializeVariableCount += 1;
}
if (obj.actions) {
for (const actionName in obj.actions) {
countInitializeVariableActions(obj.actions[actionName]);
}
}
}
}
countInitializeVariableActions(jsonContent.properties.definition);
const hasMultipleInitializeVariable = initializeVariableCount > 1;
return {
patternID,
patternName,
description,
status: !hasMultipleInitializeVariable ? "Pass" /* ManifestPropertyStatusNames.Pass */ : "Fail" /* ManifestPropertyStatusNames.Fail */,
instanceValue: hasMultipleInitializeVariable
? [`InitializeVariable actions count: ${initializeVariableCount}`]
: [],
failureReason: hasMultipleInitializeVariable ? "More than one 'InitializeVariable' action detected." : "",
recommendation,
docLinks: docLink,
severity,
category,
};
}
// 10. Check Static Variables
// eslint-disable-next-line sonarjs/cognitive-complexity
function patternCheckStaticVariables(jsonContent) {
const { patternID, patternName, description, docLink, recommendation, severity, category } = ManifestConstant_1.FlowPatternDetails.StaticVariables;
const initializedVariables = new Set();
const updatedVariables = new Set();
const staticVariables = [];
function traverseActions(obj) {
if (obj && typeof obj === "object") {
// Check if the current object is an action
if (obj.type) {
if (obj.type === "InitializeVariable") {
const variableName = obj.inputs?.variables?.[0]?.name;
if (variableName) {
initializedVariables.add(variableName);
}
}
else if (obj.type === "SetVariable" ||
obj.type === "IncrementVariable" ||
obj.type === "DecrementVariable" ||
obj.type === "AppendToStringVariable" ||
obj.type === "AppendToArrayVariable") {
const variableName = obj.inputs?.name;
if (variableName) {
updatedVariables.add(variableName);
}
}
}
// Recursively traverse nested actions
if (obj.actions) {
for (const actionName in obj.actions) {
traverseActions(obj.actions[actionName]);
}
}
// Traverse other properties
for (const key in obj) {
if (key !== "actions" && obj.hasOwnProperty(key)) {
traverseActions(obj[key]);
}
}
}
}
traverseActions(jsonContent.properties.definition);
// Identify variables that are initialized but never updated
initializedVariables.forEach((variableName) => {
if (!updatedVariables.has(variableName)) {
staticVariables.push(variableName);
}
});
const hasStaticVariables = staticVariables.length > 0;
return {
patternID,
patternName,
description,
status: !hasStaticVariables ? "Pass" /* ManifestPropertyStatusNames.Pass */ : "Fail" /* ManifestPropertyStatusNames.Fail */,
instanceValue: staticVariables,
failureReason: hasStaticVariables ? "Variables initialized but never updated detected. Use " : "",
recommendation,
docLinks: docLink,
severity,
category,
};
}
// 11. PowerApp Response Check
// eslint-disable-next-line sonarjs/cognitive-complexity
function patternCheckPowerAppResponse(jsonContent) {
const { patternID, patternName, description, docLink, recommendation, severity, category } = ManifestConstant_1.FlowPatternDetails.PowerAppResponse;
let powerAppResponseFound = false;
function traverseActions(obj) {
if (obj && typeof obj === "object") {
// Check if the current object is a 'Response' action with kind 'PowerApp' or 'Http' and operationOptions property does not exist
if (obj.type === "Response" && (obj.kind === "PowerApp" || obj.kind === "Http") && !obj.operationOptions) {
powerAppResponseFound = true;
}
// Recursively traverse nested actions
if (obj.actions) {
for (const actionName in obj.actions) {
traverseActions(obj.actions[actionName]);
}
}
// Traverse other properties
for (const key in obj) {
if (key !== "actions" && obj.hasOwnProperty(key)) {
traverseActions(obj[key]);
}
}
}
}
traverseActions(jsonContent.properties.definition);
return {
patternID,
patternName,
description,
status: !powerAppResponseFound ? "Pass" /* ManifestPropertyStatusNames.Pass */ : "Fail" /* ManifestPropertyStatusNames.Fail */,
instanceValue: powerAppResponseFound ? ["PowerApp response action found"] : [],
failureReason: powerAppResponseFound
? "Warning: Logic may fail if the response is not sent within 120 seconds."
: "",
recommendation,
docLinks: docLink,
severity,
category,
};
}
// 12. Scope Usage with Action Count Check
// eslint-disable-next-line sonarjs/cognitive-complexity
function patternCheckScopeUsage(jsonContent) {
const { patternID, patternName, description, docLink, recommendation, severity, category } = ManifestConstant_1.FlowPatternDetails.ScopeUsage;
// let actionCount = 0;
let hasScopeAction = false;
// Recursive function to count all actions within every "actions" key in the JSON
function countAllActions(obj) {
let count = 0;
if (obj && typeof obj === "object") {
// Ensure it's an object
// Check if "actions" is present and is an object
if (obj.hasOwnProperty("actions") && typeof obj.actions === "object") {
count += Object.keys(obj.actions).length;
}
// Check if the current action is of type "Scope"
if (obj.type === "Scope") {
hasScopeAction = true;
}
// Recursively search through all nested objects and arrays
Object.values(obj).forEach((nestedValue) => {
if (typeof nestedValue === "object" || Array.isArray(nestedValue)) {
count += countAllActions(nestedValue);
}
});
}
else if (Array.isArray(obj)) {
// Handle array of objects
obj.forEach((item) => {
count += countAllActions(item);
});
}
return count;
}
// Start counting actions from the root JSON content
const actionCount = countAllActions(jsonContent);
// console.log("Total action count:", actionCount);
const hasManyActions = actionCount > 10;
const lacksScope = !hasScopeAction;
const patternFails = hasManyActions && lacksScope;
return {
patternID,
patternName,
description,
status: !patternFails ? "Pass" /* ManifestPropertyStatusNames.Pass */ : "Fail" /* ManifestPropertyStatusNames.Fail */,
instanceValue: patternFails ? [`Action count: ${actionCount}`] : [],
failureReason: patternFails
? "Use Scopes to group actions and implement error handling (with Run After conditions) to handle failures gracefully."
: "",
recommendation,
docLinks: docLink,
severity,
category,
};
}
// 13. Data Retrieval Parameters Check
// eslint-disable-next-line sonarjs/cognitive-complexity
function patternCheckDataRetrievalParameters(jsonContent) {
const { patternID, patternName, description, docLink, recommendation, severity, category } = ManifestConstant_1.FlowPatternDetails.DataRetrievalParameters;
const missingParameters = [];
// Helper function to format missing parameters
function formatMissingParams(params) {
if (params.length === 1) {
return params[0];
}
else if (params.length === 2) {
return `${params[0]} and ${params[1]}`;
}
else {
return `${params.slice(0, -1).join(", ")}, and ${params[params.length - 1]}`;
}
}
const processedActions = new Set(); // Set to track processed actions
function traverseActions(actions) {
if (actions && typeof actions === "object") {
Object.keys(actions).forEach((actionName) => {
const action = actions[actionName];
// Check if this action has already been processed
if (processedActions.has(actionName)) {
return; // Skip processing this action again
}
processedActions.add(actionName);
// Check if action is of type 'OpenApiConnection' or 'OpenApiConnectionWebhook'
if (action.type === "OpenApiConnection") {
const inputs = action.inputs;
if (inputs && inputs.host && inputs.parameters) {
const operationId = inputs.host.operationId;
const apiId = inputs.host.apiId || "";
if (operationId === "GetItems" && apiId.includes("sharepoint")) {
// SharePoint GetItems action
const parameters = inputs.parameters;
const hasFilter = parameters.hasOwnProperty("$filter");
const hasTop = parameters.hasOwnProperty("$top");
if (!hasFilter || !hasTop) {
missingParameters.push(`Action '${actionName}' (SharePoint GetItems) is missing '${!hasFilter ? "$filter" : ""}${!hasFilter && !hasTop ? " and " : ""}${!hasTop ? "$top" : ""}'.`);
}
}
else if (operationId === "GetItem" && apiId.includes("commondataservice")) {
// CDS GetItem action
const parameters = inputs.parameters;
const hasSelect = parameters.hasOwnProperty("$select");
if (!hasSelect) {
missingParameters.push(`Action '${actionName}' (CDS GetItem) is missing '$select'.`);
}
}
else if (operationId === "ListRecords" && apiId.includes("commondataservice")) {
// CDS ListRecords action
const parameters = inputs.parameters;
const missingParams = [];
if (!parameters.hasOwnProperty("$select")) {
missingParams.push("$select");
}
if (!parameters.hasOwnProperty("$filter")) {
missingParams.push("$filter");
}
if (!parameters.hasOwnProperty("$top")) {
missingParams.push("$top");
}
if (missingParams.length > 0) {
const missingParamsStr = formatMissingParams(missingParams);
missingParameters.push(`Action '${actionName}' (CDS ListRecords) is missing '${missingParamsStr}'.`);
}
}
}
}
// Recursively check nested actions
if (action.actions) {
traverseActions(action.actions);
}
// Check for actions within conditions (e.g., 'If' actions)
if (action.type === "If") {
if (action.actions) {
traverseActions(action.actions);
}
if (action.else && action.else.actions) {
traverseActions(action.else.actions);
}
}
// Check for actions within switch cases
if (action.type === "Switch" && action.cases) {
Object.values(action.cases).forEach((caseObj) => {
if (caseObj.actions) {
traverseActions(caseObj.actions);
}
});
}
// Check for actions within loops
if ((action.type === "Foreach" || action.type === "Until") && action.actions) {
traverseActions(action.actions);
}
});
}
}
const actions = jsonContent?.properties?.definition?.actions || {};
traverseActions(actions);
const hasIssue = missingParameters.length > 0;
return {
patternID,
patternName,
description,
status: !hasIssue ? "Pass" /* ManifestPropertyStatusNames.Pass */ : "Fail" /* ManifestPropertyStatusNames.Fail */,
instanceValue: missingParameters,
failureReason: hasIssue ? "Missing recommended parameters in data retrieval actions." : "",
recommendation,
docLinks: docLink,
severity,
category,
};
}
// eslint-disable-next-line sonarjs/cognitive-complexity
function patternCheckForActionNote(jsonContent) {
const { patternID, patternName, description, docLink, recommendation, severity, category } = ManifestConstant_1.FlowPatternDetails.ActionNote;
const actionsWithoutDescription = [];
function traverseActions(actions) {
if (actions && typeof actions === "object") {
Object.keys(actions).forEach((actionName) => {
const action = actions[actionName];
// Check if the action has a description
if (!action.description || action.description.trim() === "") {
actionsWithoutDescription.push(actionName);
}
// Recursively check nested actions
if (action.actions) {
traverseActions(action.actions);
}
// Handle conditional actions
if (action.type === "If") {
if (action.actions)
traverseActions(action.actions);
if (action.else?.actions)
traverseActions(action.else.actions);
}
// Handle switch cases
if (action.type === "Switch" && action.cases) {
Object.values(action.cases).forEach((caseObj) => {
if (caseObj.actions)
traverseActions(caseObj.actions);
});
}
// Handle loops
if ((action.type === "Foreach" || action.type === "Until") && action.actions) {
traverseActions(action.actions);
}
});
}
}
const actions = jsonContent?.properties?.definition?.actions || {};
traverseActions(actions);
const hasIssue = actionsWithoutDescription.length > 0;
return {
patternID,
patternName,
description,
status: !hasIssue ? "Pass" /* ManifestPropertyStatusNames.Pass */ : "Warning" /* ManifestPropertyStatusNames.Warning */,
instanceValue: actionsWithoutDescription,
failureReason: hasIssue ? "Actions missing notes." : "",
recommendation,
docLinks: docLink,
severity,
category,
};
}
// eslint-disable-next-line sonarjs/cognitive-complexity
function patternCheckCreateUpdateInsideLoop(jsonContent) {
const { patternID, patternName, description, docLink, recommendation, severity, category } = ManifestConstant_1.FlowPatternDetails.CreateUpdateInsideLoop;
const createUpdateActions = new Set();
// Dataverse operation IDs
const dataverseOperationIds = [
"CreateRecord",
"CreateRecordWithOrganization",
"UpdateOnlyRecord",
"UpdateOnlyRecordWithOrganization",
"UpdateRecord",
"UpdateRecordWithOrganization",
"DeleteRecord",
"DeleteRecordWithOrganization",
];
// SharePoint operation IDs (sample IDs included)
const sharePointOperationIds = ["CreateFile", "PostItem", "DeleteFile", "DeleteItem", "UpdateFile", "PatchItem"];
function traverseActions(actions, isInsideLoop) {
if (actions && typeof actions === "object") {
Object.keys(actions).forEach((actionName) => {
const action = actions[actionName];
// Check for Create/Update in Dataverse or SharePoint within a loop
const operationId = action.inputs?.host?.operationId;
const apiId = action.inputs?.host?.apiId;
if (isInsideLoop &&
action.type === "OpenApiConnection" &&
((apiId?.includes("commondataservice") && dataverseOperationIds.includes(operationId)) ||
(apiId?.includes("sharepoint") && sharePointOperationIds.includes(operationId)))) {
createUpdateActions.add(actionName);
}
// If action is a loop, traverse its nested actions
if (action.type === "Foreach" || action.type === "Until") {
traverseActions(action.actions, true);
}
// Recursively check nested actions
if (action.actions) {
traverseActions(action.actions, isInsideLoop);
}
// Handle conditional actions
if (action.type === "If") {
if (action.actions) {
traverseActions(action.actions, isInsideLoop);
}
if (action.else?.actions) {
traverseActions(action.else.actions, isInsideLoop);
}
}
// Handle switch cases
if (action.type === "Switch" && action.cases) {
Object.values(action.cases).forEach((caseObj) => {
if (caseObj.actions) {
traverseActions(caseObj.actions, isInsideLoop);
}
});
}
});
}
}
const actions = jsonContent?.properties?.definition?.actions || {};
traverseActions(actions, false);
const hasIssue = createUpdateActions.size > 0;
return {
patternID,
patternName,
description,
status: !hasIssue ? "Pass" /* ManifestPropertyStatusNames.Pass */ : "Fail" /* ManifestPropertyStatusNames.Fail */,
instanceValue: Array.from(createUpdateActions),
failureReason: hasIssue ? "Create/Update actions found inside loops." : "",
recommendation,
docLinks: docLink,
severity,
category,
};
}