From 69744bd63cc4084f912ea4ae1c22335bf82cea12 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 6 Sep 2023 13:50:08 +0200 Subject: [PATCH] Fix naming collision between workflow input and activity input --- .../ServiceProviderExtensions.cs | 7 +- .../Services/JintJavaScriptEvaluator.cs | 25 +++- .../Elsa.IntegrationTests.csproj | 10 ++ .../Scenarios/ParentChildInputs/Tests.cs | 71 +++++++++ .../ParentChildInputs/Workflows/child.json | 139 ++++++++++++++++++ .../ParentChildInputs/Workflows/parent1.json | 138 +++++++++++++++++ .../ParentChildInputs/Workflows/parent2.json | 138 +++++++++++++++++ 7 files changed, 522 insertions(+), 6 deletions(-) create mode 100644 test/integration/Elsa.IntegrationTests/Scenarios/ParentChildInputs/Tests.cs create mode 100644 test/integration/Elsa.IntegrationTests/Scenarios/ParentChildInputs/Workflows/child.json create mode 100644 test/integration/Elsa.IntegrationTests/Scenarios/ParentChildInputs/Workflows/parent1.json create mode 100644 test/integration/Elsa.IntegrationTests/Scenarios/ParentChildInputs/Workflows/parent2.json diff --git a/src/common/Elsa.Testing.Shared/ServiceProviderExtensions.cs b/src/common/Elsa.Testing.Shared/ServiceProviderExtensions.cs index 67d1bfa48..7a7ab30b9 100644 --- a/src/common/Elsa.Testing.Shared/ServiceProviderExtensions.cs +++ b/src/common/Elsa.Testing.Shared/ServiceProviderExtensions.cs @@ -53,10 +53,11 @@ public static class ServiceProviderExtensions /// /// The services. /// The ID of the workflow definition. + /// An optional dictionary of input values. /// The workflow state. - public static async Task RunWorkflowUntilEndAsync(this IServiceProvider services, string workflowDefinitionId) + public static async Task RunWorkflowUntilEndAsync(this IServiceProvider services, string workflowDefinitionId, IDictionary? input = default) { - var startWorkflowOptions = new StartWorkflowRuntimeOptions(null, new Dictionary(), VersionOptions.Published); + var startWorkflowOptions = new StartWorkflowRuntimeOptions(null, input, VersionOptions.Published); var workflowRuntime = services.GetRequiredService(); var result = await workflowRuntime.StartWorkflowAsync(workflowDefinitionId, startWorkflowOptions); var bookmarks = new Stack(result.Bookmarks); @@ -67,7 +68,7 @@ public static class ServiceProviderExtensions var resumeOptions = new ResumeWorkflowRuntimeOptions(BookmarkId: bookmark.Id); var resumeResult = await workflowRuntime.ResumeWorkflowAsync(result.WorkflowInstanceId, resumeOptions); - foreach (var newBookmark in resumeResult.Bookmarks) + foreach (var newBookmark in resumeResult!.Bookmarks) bookmarks.Push(newBookmark); } diff --git a/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs b/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs index a18e31c7d..877d84b0b 100644 --- a/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs +++ b/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs @@ -9,6 +9,7 @@ using Elsa.JavaScript.Extensions; using Elsa.JavaScript.Notifications; using Elsa.JavaScript.Options; using Elsa.Mediator.Contracts; +using Elsa.Workflows.Core.Activities; using Elsa.Workflows.Core.Memory; using Humanizer; using Jint; @@ -67,12 +68,14 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator engine.SetValue("getInput", (Func)(name => context.GetWorkflowExecutionContext().Input.GetValue(name))); engine.SetValue("getOutputFrom", (Func)((activityIdOrNodeId, outputName) => GetOutput(context, activityIdOrNodeId, outputName))); engine.SetValue("getLastResult", (Func)(() => GetLastResult(context))); - - // Create workflow input accessors. - CreateWorkflowInputAccessors(engine, context); // Create variable getters and setters for each variable. CreateVariableAccessors(engine, context); + + // Create workflow input accessors - only if the current activity is not part of a composite activity definition. + // Otherwise, the workflow input accessors will hide the composite activity input accessors which rely on variable accessors created above. + if(!IsInsideCompositeActivity(context)) + CreateWorkflowInputAccessors(engine, context); // Create output getters for each activity. CreateOutputAccessors(engine, context); @@ -103,6 +106,17 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator return engine; } + private static bool IsInsideCompositeActivity(ExpressionExecutionContext context) + { + if(!context.TryGetActivityExecutionContext(out var activityExecutionContext)) + return false; + + // If the first workflow definition in the ancestor hierarchy and that workflow definition has a parent, then we are inside a composite activity. + var firstWorkflowContext = activityExecutionContext.GetAncestors().FirstOrDefault(x => x.Activity is Workflow); + + return firstWorkflowContext?.ParentActivityExecutionContext != null; + } + private static object? GetLastResult(ExpressionExecutionContext context) { var workflowExecutionContext = context.GetWorkflowExecutionContext(); @@ -123,6 +137,11 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator private void CreateWorkflowInputAccessors(Engine engine, ExpressionExecutionContext context) { + if(!context.TryGetActivityExecutionContext(out var activityExecutionContext)) + return; + + // Only create workflow input accessors if the current activity is not part of a composite activity definition. + if(context.TryGetWorkflowExecutionContext(out var workflowExecutionContext)) { var input = workflowExecutionContext.Input; diff --git a/test/integration/Elsa.IntegrationTests/Elsa.IntegrationTests.csproj b/test/integration/Elsa.IntegrationTests/Elsa.IntegrationTests.csproj index e3d941252..e8ccb4ac4 100644 --- a/test/integration/Elsa.IntegrationTests/Elsa.IntegrationTests.csproj +++ b/test/integration/Elsa.IntegrationTests/Elsa.IntegrationTests.csproj @@ -87,8 +87,18 @@ Always + + Always + + + Always + + + Always + + diff --git a/test/integration/Elsa.IntegrationTests/Scenarios/ParentChildInputs/Tests.cs b/test/integration/Elsa.IntegrationTests/Scenarios/ParentChildInputs/Tests.cs new file mode 100644 index 000000000..85adf1c23 --- /dev/null +++ b/test/integration/Elsa.IntegrationTests/Scenarios/ParentChildInputs/Tests.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Elsa.Testing.Shared; +using Elsa.Workflows.Core; +using Xunit; +using Xunit.Abstractions; + +namespace Elsa.IntegrationTests.Scenarios.ParentChildInputs; + +/// +/// Tests for the flowchart completion feature using various workflow setups. +/// +public class Tests +{ + private readonly CapturingTextWriter _capturingTextWriter = new(); + private readonly IServiceProvider _services; + + public Tests(ITestOutputHelper testOutputHelper) + { + _services = new TestApplicationBuilder(testOutputHelper).WithCapturingTextWriter(_capturingTextWriter).Build(); + } + + [Fact(DisplayName = "Child activity receives workflow input from parent event if same name.")] + public async Task Test1() + { + // Populate registries. + await _services.PopulateRegistriesAsync(); + + // Import child workflow. + var childFileName = $"Scenarios/ParentChildInputs/Workflows/child.json"; + await _services.ImportWorkflowDefinitionAsync(childFileName); + + // Import parent workflow. + var parentFileName = $"Scenarios/ParentChildInputs/Workflows/parent1.json"; + var workflowDefinition = await _services.ImportWorkflowDefinitionAsync(parentFileName); + + // Execute. + var input = new Dictionary { ["Input1"] = "Foo" }; + await _services.RunWorkflowUntilEndAsync(workflowDefinition.DefinitionId, input); + + // Assert expected output. + var lines = _capturingTextWriter.Lines.ToList(); + Assert.Equal(new[] { "Parent: Foo", "Child: Bar" }, lines); + } + + [Fact(DisplayName = "Child activity receives input from parent event if same name and does not use global workflow input.")] + public async Task Test2() + { + // Populate registries. + await _services.PopulateRegistriesAsync(); + + // Import child workflow. + var childFileName = $"Scenarios/ParentChildInputs/Workflows/child.json"; + await _services.ImportWorkflowDefinitionAsync(childFileName); + + // Import parent workflow. + var parentFileName = $"Scenarios/ParentChildInputs/Workflows/parent2.json"; + var workflowDefinition = await _services.ImportWorkflowDefinitionAsync(parentFileName); + + // Execute. + var input = new Dictionary { ["Input1"] = "Foo" }; + await _services.RunWorkflowUntilEndAsync(workflowDefinition.DefinitionId, input); + + // Assert expected output. + var lines = _capturingTextWriter.Lines.ToList(); + Assert.Equal(new[] { "Parent: Foo", "Child: Foo" }, lines); + } +} \ No newline at end of file diff --git a/test/integration/Elsa.IntegrationTests/Scenarios/ParentChildInputs/Workflows/child.json b/test/integration/Elsa.IntegrationTests/Scenarios/ParentChildInputs/Workflows/child.json new file mode 100644 index 000000000..2f89aac94 --- /dev/null +++ b/test/integration/Elsa.IntegrationTests/Scenarios/ParentChildInputs/Workflows/child.json @@ -0,0 +1,139 @@ +{ + "id": "228c0150ab7b4745b038b950be574935", + "definitionId": "25b097966de245818e9a5c0d04f3323e", + "name": "Child", + "createdAt": "2023-09-06T09:57:32.944622+00:00", + "version": 5, + "variables": [], + "inputs": [ + { + "uiHint": "single-line", + "storageDriverType": "Elsa.Workflows.Core.Services.WorkflowStorageDriver, Elsa.Workflows.Core", + "type": "Object", + "name": "Input1", + "displayName": "Input1", + "description": "", + "category": "", + "isArray": false + } + ], + "outputs": [ + { + "type": "Object", + "name": "Output1", + "displayName": "Output1", + "description": "", + "isArray": false + } + ], + "outcomes": [], + "customProperties": {}, + "isReadonly": false, + "isLatest": true, + "isPublished": true, + "options": { + "usableAsActivity": true, + "autoUpdateConsumingWorkflows": true + }, + "root": { + "type": "Elsa.Flowchart", + "version": 1, + "id": "Flowchart1", + "metadata": {}, + "customProperties": { + "source": "FlowchartJsonConverter.cs:45", + "NotFoundConnectionsKey": [], + "canStartWorkflow": false, + "runAsynchronously": false + }, + "start": "WriteLine1", + "activities": [ + { + "outputName": { + "typeName": "String", + "expression": { + "type": "Literal", + "value": "Output1" + }, + "memoryReference": { + "id": "ca8473b5-83ec-4f75-a2f1-dfba71f349d1" + } + }, + "outputValue": { + "typeName": "Object", + "expression": { + "type": "JavaScript", + "value": "return getInput1();" + }, + "memoryReference": { + "id": "06c6243d-8a57-45e3-ae54-f209b95d2bb2" + } + }, + "id": "SetOutput1", + "name": null, + "type": "Elsa.SetOutput", + "version": 1, + "customProperties": { + "canStartWorkflow": false, + "runAsynchronously": false + }, + "metadata": { + "designer": { + "position": { + "x": 636, + "y": 250.01388549804688 + }, + "size": { + "width": 107.734375, + "height": 50 + } + } + } + }, + { + "text": { + "typeName": "String", + "expression": { + "type": "JavaScript", + "value": "\u0022Child: \u0022 \u002B getInput1();" + }, + "memoryReference": { + "id": "3f726554-0fa1-4e54-b5e7-74b18807437b" + } + }, + "id": "WriteLine1", + "name": null, + "type": "Elsa.WriteLine", + "version": 1, + "customProperties": { + "canStartWorkflow": false, + "runAsynchronously": false + }, + "metadata": { + "designer": { + "position": { + "x": 332.01385498046875, + "y": 222.01388549804688 + }, + "size": { + "width": 139.296875, + "height": 50 + } + } + } + } + ], + "connections": [ + { + "source": { + "activity": "WriteLine1", + "port": "Done" + }, + "target": { + "activity": "SetOutput1", + "port": "In" + } + } + ] + } +} \ No newline at end of file diff --git a/test/integration/Elsa.IntegrationTests/Scenarios/ParentChildInputs/Workflows/parent1.json b/test/integration/Elsa.IntegrationTests/Scenarios/ParentChildInputs/Workflows/parent1.json new file mode 100644 index 000000000..0fe003b43 --- /dev/null +++ b/test/integration/Elsa.IntegrationTests/Scenarios/ParentChildInputs/Workflows/parent1.json @@ -0,0 +1,138 @@ +{ + "id": "e49f81715e29460187671ee298bbd0b2", + "definitionId": "6a4f5aeecbf94fc09dbba65363c5cb57", + "name": "Parent1", + "createdAt": "2023-09-06T11:38:10.701838+00:00", + "version": 8, + "variables": [], + "inputs": [ + { + "uiHint": "single-line", + "storageDriverType": "Elsa.Workflows.Core.Services.WorkflowStorageDriver, Elsa.Workflows.Core", + "type": "Object", + "name": "Input1", + "displayName": "Input1", + "description": "", + "category": "", + "isArray": false + } + ], + "outputs": [ + { + "type": "Object", + "name": "Output1", + "displayName": "Output1", + "description": "", + "isArray": false + } + ], + "outcomes": [], + "customProperties": {}, + "isReadonly": false, + "isLatest": true, + "isPublished": false, + "options": { + "autoUpdateConsumingWorkflows": false + }, + "root": { + "type": "Elsa.Flowchart", + "version": 1, + "id": "Flowchart1", + "metadata": {}, + "customProperties": { + "source": "FlowchartJsonConverter.cs:45", + "NotFoundConnectionsKey": [], + "canStartWorkflow": false, + "runAsynchronously": false + }, + "start": "WriteLine1", + "activities": [ + { + "text": { + "typeName": "String", + "expression": { + "type": "JavaScript", + "value": "\u0022Parent: \u0022 \u002B getInput1();" + }, + "memoryReference": { + "id": "3070bee4-8309-44e3-a805-a9ec7fdd7ac2" + } + }, + "id": "WriteLine1", + "name": null, + "type": "Elsa.WriteLine", + "version": 1, + "customProperties": { + "canStartWorkflow": false, + "runAsynchronously": false + }, + "metadata": { + "designer": { + "position": { + "x": 160, + "y": 280 + }, + "size": { + "width": 139.296875, + "height": 50 + } + } + } + }, + { + "workflowDefinitionId": "25b097966de245818e9a5c0d04f3323e", + "workflowDefinitionVersionId": "228c0150ab7b4745b038b950be574935", + "latestAvailablePublishedVersion": 5, + "latestAvailablePublishedVersionId": "228c0150ab7b4745b038b950be574935", + "id": "22jW7xiYM0K30H99EDWztQ", + "name": "Child1", + "type": "Child", + "version": 5, + "customProperties": { + "canStartWorkflow": false, + "runAsynchronously": false + }, + "metadata": { + "designer": { + "position": { + "x": 405.3984375, + "y": 204.0078125 + }, + "size": { + "width": 94.765625, + "height": 116.015625 + } + } + }, + "input1": { + "typeName": "Object", + "expression": { + "type": "JavaScript", + "value": "\u0060Bar\u0060" + }, + "memoryReference": { + "id": "22jW7xiYM0K30H99EDWztQ:input-0" + } + }, + "output1": { + "typeName": "Object", + "memoryReference": { + "id": "Output1" + } + } + } + ], + "connections": [ + { + "source": { + "activity": "WriteLine1", + "port": "Done" + }, + "target": { + "activity": "22jW7xiYM0K30H99EDWztQ", + "port": "In" + } + } + ] + } +} \ No newline at end of file diff --git a/test/integration/Elsa.IntegrationTests/Scenarios/ParentChildInputs/Workflows/parent2.json b/test/integration/Elsa.IntegrationTests/Scenarios/ParentChildInputs/Workflows/parent2.json new file mode 100644 index 000000000..948b8eeed --- /dev/null +++ b/test/integration/Elsa.IntegrationTests/Scenarios/ParentChildInputs/Workflows/parent2.json @@ -0,0 +1,138 @@ +{ + "id": "c5aa339e124f4486b14dbfdc133a45d1", + "definitionId": "8ad20b5ff9ee4ff59b8dfcfb6d4e29a0", + "name": "Parent2", + "createdAt": "2023-09-06T11:38:30.217265+00:00", + "version": 1, + "variables": [], + "inputs": [ + { + "uiHint": "single-line", + "storageDriverType": "Elsa.Workflows.Core.Services.WorkflowStorageDriver, Elsa.Workflows.Core", + "type": "Object", + "name": "Input1", + "displayName": "Input1", + "description": "", + "category": "", + "isArray": false + } + ], + "outputs": [ + { + "type": "Object", + "name": "Output1", + "displayName": "Output1", + "description": "", + "isArray": false + } + ], + "outcomes": [], + "customProperties": {}, + "isReadonly": false, + "isLatest": true, + "isPublished": true, + "options": { + "autoUpdateConsumingWorkflows": false + }, + "root": { + "type": "Elsa.Flowchart", + "version": 1, + "id": "Flowchart1", + "metadata": {}, + "customProperties": { + "source": "FlowchartJsonConverter.cs:45", + "NotFoundConnectionsKey": [], + "canStartWorkflow": false, + "runAsynchronously": false + }, + "start": "WriteLine1", + "activities": [ + { + "text": { + "typeName": "String", + "expression": { + "type": "JavaScript", + "value": "\u0022Parent: \u0022 \u002B getInput1();" + }, + "memoryReference": { + "id": "3070bee4-8309-44e3-a805-a9ec7fdd7ac2" + } + }, + "id": "WriteLine1", + "name": null, + "type": "Elsa.WriteLine", + "version": 1, + "customProperties": { + "canStartWorkflow": false, + "runAsynchronously": false + }, + "metadata": { + "designer": { + "position": { + "x": 160, + "y": 280 + }, + "size": { + "width": 139.296875, + "height": 50 + } + } + } + }, + { + "workflowDefinitionId": "25b097966de245818e9a5c0d04f3323e", + "workflowDefinitionVersionId": "228c0150ab7b4745b038b950be574935", + "latestAvailablePublishedVersion": 5, + "latestAvailablePublishedVersionId": "228c0150ab7b4745b038b950be574935", + "id": "22jW7xiYM0K30H99EDWztQ", + "name": "Child1", + "type": "Child", + "version": 5, + "customProperties": { + "canStartWorkflow": false, + "runAsynchronously": false + }, + "metadata": { + "designer": { + "position": { + "x": 405.3984375, + "y": 204.0078125 + }, + "size": { + "width": 135.390625, + "height": 120 + } + } + }, + "input1": { + "typeName": "Object", + "expression": { + "type": "JavaScript", + "value": "getInput1()" + }, + "memoryReference": { + "id": "22jW7xiYM0K30H99EDWztQ:input-0" + } + }, + "output1": { + "typeName": "Object", + "memoryReference": { + "id": "Output1" + } + } + } + ], + "connections": [ + { + "source": { + "activity": "WriteLine1", + "port": "Done" + }, + "target": { + "activity": "22jW7xiYM0K30H99EDWztQ", + "port": "In" + } + } + ] + } +} \ No newline at end of file