Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ public void UpdateRowAnalyzer_SupportedOperationIds_ShouldContainExpectedValues(

// Assert
Assert.Contains("UpdateRecord", operationIds);
Assert.Contains("UpdateOnlyRecord", operationIds);
Assert.Contains("PatchItem", operationIds);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,42 @@ public async Task AnalyzeComponentAsync_WithCreateRowAction_ShouldNotThrow()
Assert.NotNull(attributeUsages);
}

[Fact]
public async Task AnalyzeComponentAsync_WithUpdateOnlyRecord_ShouldExtractCloudFlowUpdateUsage()
{
var action = JObject.Parse(@"{
'type': 'OpenApiConnection',
'inputs': {
'host': {
'apiId': '/providers/Microsoft.PowerApps/apis/shared_commondataserviceforapps',
'operationId': 'UpdateOnlyRecord'
},
'parameters': {
'entityName': 'dmvp_planets',
'recordId': '00000000-0000-0000-0000-000000000001',
'item/dmvp_weight': 42
}
}
}");
var flowJson = new PowerAutomateFlowBuilder()
.AddAction("Update_planet", action)
.BuildAsJson();
var flow = new PowerAutomateFlow("synthetic-flow", "Playground weight update", flowJson);
var attributeUsages = new Dictionary<string, Dictionary<string, List<AttributeUsage>>>();
var warnings = new List<SolutionWarning>();

await FlowAnalyzer.AnalyzeComponentAsync(flow, attributeUsages, warnings);

Assert.True(attributeUsages.ContainsKey("dmvp_planets"));
Assert.True(attributeUsages["dmvp_planets"].ContainsKey("dmvp_weight"));
var usage = Assert.Single(attributeUsages["dmvp_planets"]["dmvp_weight"]);
Assert.Equal("Playground weight update", usage.Name);
Assert.Equal(OperationType.Update, usage.OperationType);
Assert.Equal(ComponentType.PowerAutomateFlow, usage.ComponentType);
Assert.False(usage.IsFromDependencyAnalysis);
Assert.Contains("Update parameter", usage.Usage);
Assert.Empty(warnings);
}
[Fact]
public async Task AnalyzeComponentAsync_WithUpdateRowAction_ShouldNotThrow()
{
Expand Down
28 changes: 28 additions & 0 deletions Generator.Tests/WorkflowDependencyUsageTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Generator.DTO;
using Generator.Services;

namespace Generator.Tests;

public class WorkflowDependencyUsageTests
{
[Theory]
[InlineData(0, "Workflow", ComponentType.ClassicWorkflow)]
[InlineData(1, "Dialog", ComponentType.ClassicWorkflow)]
[InlineData(2, "Business Rule", ComponentType.BusinessRule)]
[InlineData(3, "Action", ComponentType.ClassicWorkflow)]
[InlineData(4, "Business Process Flow", ComponentType.ClassicWorkflow)]
[InlineData(5, "Power Automate Flow", ComponentType.PowerAutomateFlow)]
[InlineData(99, "Workflow", ComponentType.ClassicWorkflow)]
public void DependencyUsagePreservesWorkflowCategory(int category, string label, ComponentType componentType)
{
var workflow = new WorkflowInfo(Guid.NewGuid(), "Playground process", category, 1);

var usage = workflow.ToAttributeUsage();

Assert.Equal("Playground process", usage.Name);
Assert.Equal(label, usage.Usage);
Assert.Equal(componentType, usage.ComponentType);
Assert.Equal(OperationType.Other, usage.OperationType);
Assert.True(usage.IsFromDependencyAnalysis);
}
}
12 changes: 2 additions & 10 deletions Generator/DataverseService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -218,16 +218,8 @@ public DataverseService(
logger.LogInformation($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Getting workflow dependencies for attributes");

// Get workflow dependencies for attributes (returns attribute ObjectId -> list of workflow ObjectIds)
var explicitComponentsList = solutionComponents.ToList();
var workflowDependencyMap = await solutionComponentService.GetWorkflowDependenciesForAttributesAsync(
explicitComponentsList.Where(c => c.ComponentType == 2).Select(c => new SolutionComponentInfo(
c.ObjectId,
c.SolutionComponentId ?? Guid.Empty,
c.ComponentType,
c.RootComponentBehaviour,
new EntityReference("solution", c.SolutionId)
))
);
// Scan the same attributes we export, including columns included with a whole table.
var workflowDependencyMap = await solutionComponentService.GetWorkflowDependenciesForAttributesAsync(attributesInSolution);

// Get workflow details for all unique workflow IDs
var allWorkflowIds = workflowDependencyMap.Values.SelectMany(ids => ids).Distinct().ToList();
Expand Down
24 changes: 1 addition & 23 deletions Generator/Services/AttributeMappingService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,7 @@ public Attribute MapAttribute(
// Get workflow dependency usages
var workflowUsages = workflowDependencies
.GetValueOrDefault(metadata.MetadataId!.Value, [])
.Select(w => new AttributeUsage(
Name: w.Name,
Usage: DetermineWorkflowUsageContext(w),
OperationType: OperationType.Other,
ComponentType: w.Category == 2 ? ComponentType.BusinessRule : ComponentType.ClassicWorkflow,
IsFromDependencyAnalysis: true
))
.Select(w => w.ToAttributeUsage())
.ToList();

// Combine both sources
Expand All @@ -85,21 +79,5 @@ public Attribute MapAttribute(

return attr;
}

/// <summary>
/// Determines the usage context string for a workflow
/// </summary>
private static string DetermineWorkflowUsageContext(WorkflowInfo workflow)
{
return workflow.Category switch
{
2 => "Business Rule",
0 => "Workflow",
3 => "Action",
4 => "Business Process Flow",
5 => "Dialog",
_ => "Workflow"
};
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public class UpdateRowAnalyzer : DataverseActionAnalyzerBase
{
public override IEnumerable<string> SupportedOperationIds => new[]
{
"UpdateRow", "UpdateRecord", "UpdateItem", "PatchItem"
"UpdateRow", "UpdateRecord", "UpdateOnlyRecord", "UpdateItem", "PatchItem"
};

public override ActionAnalysisResult Analyze(JToken action, string actionName)
Expand Down
20 changes: 10 additions & 10 deletions Generator/Services/SolutionComponentService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -217,10 +217,10 @@ private async Task<List<ComponentNodeInfo>> GetComponentNodesAsync(List<Guid> no
return results;
}

private HashSet<DependencyInfo> GetDependentComponents(IEnumerable<SolutionComponentInfo> components)
private HashSet<DependencyInfo> GetAttributeDependentComponents(IEnumerable<Guid> attributeIds)
{
var results = new HashSet<DependencyInfo>();
var componentsList = components.ToList();
var componentsList = attributeIds.Distinct().ToList();
int totalCount = componentsList.Count;
int processedCount = 0;
int errorCount = 0;
Expand Down Expand Up @@ -250,8 +250,8 @@ private HashSet<DependencyInfo> GetDependentComponents(IEnumerable<SolutionCompo
{
executeMultiple.Requests.Add(new RetrieveDependentComponentsRequest
{
ComponentType = component.ComponentType,
ObjectId = component.ObjectId
ComponentType = 2,
ObjectId = component
});
}

Expand All @@ -267,7 +267,7 @@ private HashSet<DependencyInfo> GetDependentComponents(IEnumerable<SolutionCompo
{
errorCount++;
var component = batch[j];
_logger.LogWarning($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Failed to retrieve dependents for component type {component.ComponentType}, ObjectId {component.ObjectId}: {item.Fault.Message}");
_logger.LogWarning($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Failed to retrieve dependents for attribute {component}: {item.Fault.Message}");
continue;
}

Expand Down Expand Up @@ -372,14 +372,14 @@ private HashSet<DependencyInfo> GetRequiredComponents(IEnumerable<SolutionCompon
/// <summary>
/// Gets workflow dependencies for attributes by finding workflows (type 29) that depend on specified attributes
/// </summary>
/// <param name="attributeComponents">List of attribute components to check for dependencies</param>
/// <param name="attributeIds">Metadata IDs of exported attributes, including whole-table subcomponents</param>
/// <returns>Dictionary mapping attribute ObjectId to list of workflow ObjectIds that depend on it</returns>
public async Task<Dictionary<Guid, List<Guid>>> GetWorkflowDependenciesForAttributesAsync(IEnumerable<SolutionComponentInfo> attributeComponents)
public async Task<Dictionary<Guid, List<Guid>>> GetWorkflowDependenciesForAttributesAsync(IEnumerable<Guid> attributeIds)
{
var workflowDependencies = new Dictionary<Guid, List<Guid>>();

// Filter to only attributes (component type 2)
var attributes = attributeComponents.Where(c => c.ComponentType == 2).ToList();
// Whole-table additions may have no individual solutioncomponent rows.
var attributes = attributeIds.Distinct().ToList();

if (!attributes.Any())
{
Expand All @@ -390,7 +390,7 @@ public async Task<Dictionary<Guid, List<Guid>>> GetWorkflowDependenciesForAttrib
_logger.LogInformation($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Checking {attributes.Count} attributes for workflow dependencies");

// Get all dependent components for attributes
var dependencies = GetDependentComponents(attributes);
var dependencies = GetAttributeDependentComponents(attributes);
_logger.LogInformation($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Found {dependencies.Count} total dependencies for attributes");

if (!dependencies.Any())
Expand Down
24 changes: 22 additions & 2 deletions Generator/Services/WorkflowService.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
using Generator.DTO;
using Microsoft.PowerPlatform.Dataverse.Client;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;

namespace Generator.Services
{
/// <summary>
/// Service responsible for querying workflow details (business rules and classic workflows)
/// Service responsible for querying workflow details
/// </summary>
internal class WorkflowService
{
Expand Down Expand Up @@ -64,5 +65,24 @@ public record WorkflowInfo(
string Name,
int Category,
int Type
);
)
{
public AttributeUsage ToAttributeUsage()
{
// Dataverse category 1 is Dialog; category 5 is Modern Flow.
// https://learn.microsoft.com/en-us/power-automate/manage-flows-with-code
var (usage, componentType) = Category switch
{
1 => ("Dialog", ComponentType.ClassicWorkflow),
2 => ("Business Rule", ComponentType.BusinessRule),
3 => ("Action", ComponentType.ClassicWorkflow),
4 => ("Business Process Flow", ComponentType.ClassicWorkflow),
5 => ("Power Automate Flow", ComponentType.PowerAutomateFlow),
_ => ("Workflow", ComponentType.ClassicWorkflow)
};

return new AttributeUsage(Name, usage, OperationType.Other, componentType,
IsFromDependencyAnalysis: true);
}
}
}
Loading