diff --git a/Generator.Tests/PowerAutomateAnalyzerTests/ActionAnalyzersTests.cs b/Generator.Tests/PowerAutomateAnalyzerTests/ActionAnalyzersTests.cs index bf2cbfe..8a7cb14 100644 --- a/Generator.Tests/PowerAutomateAnalyzerTests/ActionAnalyzersTests.cs +++ b/Generator.Tests/PowerAutomateAnalyzerTests/ActionAnalyzersTests.cs @@ -231,6 +231,7 @@ public void UpdateRowAnalyzer_SupportedOperationIds_ShouldContainExpectedValues( // Assert Assert.Contains("UpdateRecord", operationIds); + Assert.Contains("UpdateOnlyRecord", operationIds); Assert.Contains("PatchItem", operationIds); } diff --git a/Generator.Tests/PowerAutomateAnalyzerTests/PowerAutomateFlowAnalyzerTests.cs b/Generator.Tests/PowerAutomateAnalyzerTests/PowerAutomateFlowAnalyzerTests.cs index 820a2ce..501c802 100644 --- a/Generator.Tests/PowerAutomateAnalyzerTests/PowerAutomateFlowAnalyzerTests.cs +++ b/Generator.Tests/PowerAutomateAnalyzerTests/PowerAutomateFlowAnalyzerTests.cs @@ -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>>(); + var warnings = new List(); + + 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() { diff --git a/Generator.Tests/WorkflowDependencyUsageTests.cs b/Generator.Tests/WorkflowDependencyUsageTests.cs new file mode 100644 index 0000000..a3bc7e0 --- /dev/null +++ b/Generator.Tests/WorkflowDependencyUsageTests.cs @@ -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); + } +} diff --git a/Generator/DataverseService.cs b/Generator/DataverseService.cs index 555b294..e93ab5a 100644 --- a/Generator/DataverseService.cs +++ b/Generator/DataverseService.cs @@ -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(); diff --git a/Generator/Services/AttributeMappingService.cs b/Generator/Services/AttributeMappingService.cs index 2c6b943..9be8625 100644 --- a/Generator/Services/AttributeMappingService.cs +++ b/Generator/Services/AttributeMappingService.cs @@ -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 @@ -85,21 +79,5 @@ public Attribute MapAttribute( return attr; } - - /// - /// Determines the usage context string for a workflow - /// - private static string DetermineWorkflowUsageContext(WorkflowInfo workflow) - { - return workflow.Category switch - { - 2 => "Business Rule", - 0 => "Workflow", - 3 => "Action", - 4 => "Business Process Flow", - 5 => "Dialog", - _ => "Workflow" - }; - } } } diff --git a/Generator/Services/Power Automate/Analyzers/UpdateRowAnalyzer.cs b/Generator/Services/Power Automate/Analyzers/UpdateRowAnalyzer.cs index 8352980..a2621c8 100644 --- a/Generator/Services/Power Automate/Analyzers/UpdateRowAnalyzer.cs +++ b/Generator/Services/Power Automate/Analyzers/UpdateRowAnalyzer.cs @@ -11,7 +11,7 @@ public class UpdateRowAnalyzer : DataverseActionAnalyzerBase { public override IEnumerable SupportedOperationIds => new[] { - "UpdateRow", "UpdateRecord", "UpdateItem", "PatchItem" + "UpdateRow", "UpdateRecord", "UpdateOnlyRecord", "UpdateItem", "PatchItem" }; public override ActionAnalysisResult Analyze(JToken action, string actionName) diff --git a/Generator/Services/SolutionComponentService.cs b/Generator/Services/SolutionComponentService.cs index 895c52a..2a0bf78 100644 --- a/Generator/Services/SolutionComponentService.cs +++ b/Generator/Services/SolutionComponentService.cs @@ -217,10 +217,10 @@ private async Task> GetComponentNodesAsync(List no return results; } - private HashSet GetDependentComponents(IEnumerable components) + private HashSet GetAttributeDependentComponents(IEnumerable attributeIds) { var results = new HashSet(); - var componentsList = components.ToList(); + var componentsList = attributeIds.Distinct().ToList(); int totalCount = componentsList.Count; int processedCount = 0; int errorCount = 0; @@ -250,8 +250,8 @@ private HashSet GetDependentComponents(IEnumerable GetDependentComponents(IEnumerable GetRequiredComponents(IEnumerable /// Gets workflow dependencies for attributes by finding workflows (type 29) that depend on specified attributes /// - /// List of attribute components to check for dependencies + /// Metadata IDs of exported attributes, including whole-table subcomponents /// Dictionary mapping attribute ObjectId to list of workflow ObjectIds that depend on it - public async Task>> GetWorkflowDependenciesForAttributesAsync(IEnumerable attributeComponents) + public async Task>> GetWorkflowDependenciesForAttributesAsync(IEnumerable attributeIds) { var workflowDependencies = new Dictionary>(); - // 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()) { @@ -390,7 +390,7 @@ public async Task>> 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()) diff --git a/Generator/Services/WorkflowService.cs b/Generator/Services/WorkflowService.cs index da1ef5e..af4fcca 100644 --- a/Generator/Services/WorkflowService.cs +++ b/Generator/Services/WorkflowService.cs @@ -1,3 +1,4 @@ +using Generator.DTO; using Microsoft.PowerPlatform.Dataverse.Client; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Query; @@ -5,7 +6,7 @@ namespace Generator.Services { /// - /// Service responsible for querying workflow details (business rules and classic workflows) + /// Service responsible for querying workflow details /// internal class WorkflowService { @@ -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); + } + } }