diff --git a/src/modules/Elsa.JavaScript/Extensions/EngineExtensions.cs b/src/modules/Elsa.JavaScript/Extensions/EngineExtensions.cs index 9589b2fce..b12b9f449 100644 --- a/src/modules/Elsa.JavaScript/Extensions/EngineExtensions.cs +++ b/src/modules/Elsa.JavaScript/Extensions/EngineExtensions.cs @@ -1,5 +1,8 @@ +using Elsa.JavaScript.Helpers; +using Elsa.JavaScript.Options; using Jint; using Jint.Runtime.Interop; +using Microsoft.Extensions.Options; // ReSharper disable once CheckNamespace namespace Elsa.Extensions; @@ -18,4 +21,15 @@ public static class EngineExtensions /// Register the specified type T with the engine. /// public static void RegisterType(this Engine engine, Type type) => engine.SetValue(type.Name, TypeReference.CreateTypeReference(engine, type)); + + internal static void SyncVariablesContainer(this Engine engine, IOptions options, string name, object? value) + { + if (!options.Value.DisableWrappers) + { + // To ensure both variable accessor syntaxes work, we need to update the variables container in the engine as well as the context to keep them in sync. + var variablesContainer = (IDictionary)engine.GetValue("variables").ToObject()!; + variablesContainer[name] = ObjectConverterHelper.ProcessVariableValue(engine, value); + engine.SetValue("variables", variablesContainer); + } + } } \ No newline at end of file diff --git a/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithCommonFunctions.cs b/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithCommonFunctions.cs index 1eee959c6..3c9295288 100644 --- a/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithCommonFunctions.cs +++ b/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithCommonFunctions.cs @@ -5,9 +5,12 @@ using System.Text.Json.Serialization; using System.Text.RegularExpressions; using System.Text.Unicode; using Elsa.Extensions; +using Elsa.JavaScript.Helpers; using Elsa.JavaScript.Notifications; +using Elsa.JavaScript.Options; using Elsa.Mediator.Contracts; using JetBrains.Annotations; +using Microsoft.Extensions.Options; namespace Elsa.JavaScript.Handlers; @@ -15,7 +18,7 @@ namespace Elsa.JavaScript.Handlers; /// A handler that configures the Jint engine with common functions. /// [UsedImplicitly] -public class ConfigureEngineWithCommonFunctions : INotificationHandler +public class ConfigureEngineWithCommonFunctions(IOptions options) : INotificationHandler { private readonly JsonSerializerOptions _jsonSerializerOptions = CreateJsonSerializerOptions(); @@ -32,7 +35,11 @@ public class ConfigureEngineWithCommonFunctions : INotificationHandler)(() => context.GetActivityExecutionContext().WorkflowExecutionContext.Id)); engine.SetValue("setCorrelationId", (Action)(value => context.GetActivityExecutionContext().WorkflowExecutionContext.CorrelationId = value)); engine.SetValue("getCorrelationId", (Func)(() => context.GetActivityExecutionContext().WorkflowExecutionContext.CorrelationId)); - engine.SetValue("setVariable", (Action)((name, value) => context.SetVariableInScope(name, value))); + engine.SetValue("setVariable", (Action)((name, value) => + { + engine.SyncVariablesContainer(options, name, value); + context.SetVariableInScope(name, value); + })); engine.SetValue("getVariable", (Func)(name => context.GetVariableInScope(name))); engine.SetValue("getInput", (Func)(name => context.GetInput(name))); engine.SetValue("getOutputFrom", (Func)((activityIdName, outputName) => context.GetOutput(activityIdName, outputName))); diff --git a/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithVariables.cs b/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithVariables.cs index 57174a43a..76d4926c0 100644 --- a/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithVariables.cs +++ b/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithVariables.cs @@ -23,18 +23,18 @@ public class ConfigureEngineWithVariables(IOptions options) : INoti /// public Task HandleAsync(EvaluatingJavaScript notification, CancellationToken cancellationToken) { - if(options.Value.DisableWrappers) + if (options.Value.DisableWrappers) return Task.CompletedTask; - + CopyVariablesIntoEngine(notification); return Task.CompletedTask; } public Task HandleAsync(EvaluatedJavaScript notification, CancellationToken cancellationToken) { - if(options.Value.DisableWrappers) + if (options.Value.DisableWrappers) return Task.CompletedTask; - + CopyVariablesIntoWorkflowExecutionContext(notification); return Task.CompletedTask; } @@ -56,6 +56,23 @@ public class ConfigureEngineWithVariables(IOptions options) : INoti } } + private void CopyVariablesIntoEngine(EvaluatingJavaScript notification) + { + var engine = notification.Engine; + var context = notification.Context; + var variableNames = context.GetVariableNamesInScope().FilterInvalidVariableNames().ToList(); + var variablesContainer = (IDictionary)new ExpandoObject(); + + foreach (var variableName in variableNames) + { + var variableValue = context.GetVariableInScope(variableName); + variableValue = ObjectConverterHelper.ProcessVariableValue(engine, variableValue); + variablesContainer[variableName] = variableValue; + } + + engine.SetValue("variables", variablesContainer); + } + private IEnumerable GetInputNames(ExpressionExecutionContext context) { var activityExecutionContext = context.TryGetActivityExecutionContext(out var aec) ? aec : null; @@ -73,32 +90,4 @@ public class ConfigureEngineWithVariables(IOptions options) : INoti activityExecutionContext = activityExecutionContext.ParentActivityExecutionContext; } } - - private void CopyVariablesIntoEngine(EvaluatingJavaScript notification) - { - var engine = notification.Engine; - var context = notification.Context; - var variableNames = context.GetVariableNamesInScope().FilterInvalidVariableNames().ToList(); - var variablesContainer = (IDictionary)new ExpandoObject(); - - foreach (var variableName in variableNames) - { - var variableValue = context.GetVariableInScope(variableName); - variableValue = ProcessVariableValue(engine, variableValue); - variablesContainer[variableName] = variableValue; - } - - engine.SetValue("variables", variablesContainer); - } - - private object? ProcessVariableValue(Engine engine, object? variableValue) - { - if (variableValue == null) - return null; - - if (variableValue is not ExpandoObject expandoObject) - return variableValue; - - return ObjectConverterHelper.ConvertToJsObject(engine, expandoObject); - } } \ No newline at end of file diff --git a/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithVariablesAndInputOutputAccessors.cs b/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithVariablesAndInputOutputAccessors.cs index f0a810625..c64611074 100644 --- a/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithVariablesAndInputOutputAccessors.cs +++ b/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithVariablesAndInputOutputAccessors.cs @@ -21,12 +21,12 @@ public class ConfigureEngineWithVariablesAndInputOutputAccessors(IOptions public async Task HandleAsync(EvaluatingJavaScript notification, CancellationToken cancellationToken) { - if(options.Value.DisableWrappers) + if (options.Value.DisableWrappers) return; - + var engine = notification.Engine; var context = notification.Context; - + // The order of the next 3 lines is important. CreateVariableAccessors(engine, context); CreateWorkflowInputAccessors(engine, context); @@ -41,7 +41,11 @@ public class ConfigureEngineWithVariablesAndInputOutputAccessors(IOptions)(() => context.GetVariableInScope(variableName))); - engine.SetValue($"set{pascalName}", (Action)(value => context.SetVariableInScope(variableName, value))); + engine.SetValue($"set{pascalName}", (Action)(value => + { + engine.SyncVariablesContainer(options, variableName, value); + context.SetVariableInScope(variableName, value); + })); } } @@ -65,7 +69,7 @@ public class ConfigureEngineWithVariablesAndInputOutputAccessors(IOptions)(() => input?.Value)); } } - + private static async Task CreateActivityOutputAccessorsAsync(Engine engine, ExpressionExecutionContext context) { var activityOutputs = context.GetActivityOutputs(); diff --git a/src/modules/Elsa.JavaScript/Helpers/ObjectConverterHelper.cs b/src/modules/Elsa.JavaScript/Helpers/ObjectConverterHelper.cs index b41d3d4b8..5627d5b98 100644 --- a/src/modules/Elsa.JavaScript/Helpers/ObjectConverterHelper.cs +++ b/src/modules/Elsa.JavaScript/Helpers/ObjectConverterHelper.cs @@ -1,14 +1,30 @@ using System.Collections; +using System.Dynamic; using Elsa.Extensions; +using Elsa.JavaScript.Options; using Jint; using Jint.Native; using Jint.Native.Object; using Jint.Runtime.Descriptors; +using Microsoft.Extensions.Options; namespace Elsa.JavaScript.Helpers; internal static class ObjectConverterHelper { + + + public static object? ProcessVariableValue(Engine engine, object? variableValue) + { + if (variableValue == null) + return null; + + if (variableValue is not ExpandoObject expandoObject) + return variableValue; + + return ConvertToJsObject(engine, expandoObject); + } + public static ObjectInstance ConvertToJsObject(Engine engine, IDictionary expando) { var jsObject = engine.Intrinsics.Object.Construct([]); diff --git a/src/modules/Elsa.Tenants/Features/TenantsFeature.cs b/src/modules/Elsa.Tenants/Features/TenantsFeature.cs index a1a850eb4..ce4a18a9a 100644 --- a/src/modules/Elsa.Tenants/Features/TenantsFeature.cs +++ b/src/modules/Elsa.Tenants/Features/TenantsFeature.cs @@ -1,6 +1,7 @@ using Elsa.Common.Features; using Elsa.Common.Multitenancy; using Elsa.Features.Abstractions; +using Elsa.Features.Attributes; using Elsa.Features.Services; using Elsa.Tenants.Options; using Elsa.Tenants.Providers; @@ -11,6 +12,7 @@ namespace Elsa.Tenants.Features; /// /// Configures multi-tenancy features. /// +[DependencyOf(typeof(MultitenancyFeature))] public class TenantsFeature(IModule serviceConfiguration) : FeatureBase(serviceConfiguration) { /// diff --git a/test/component/Elsa.Workflows.ComponentTests/Scenarios/JavaScriptVariables/JavaScriptVariablesWorkflow1.cs b/test/component/Elsa.Workflows.ComponentTests/Scenarios/JavaScriptVariables/JavaScriptVariablesWorkflow1.cs new file mode 100644 index 000000000..ea7ebdc89 --- /dev/null +++ b/test/component/Elsa.Workflows.ComponentTests/Scenarios/JavaScriptVariables/JavaScriptVariablesWorkflow1.cs @@ -0,0 +1,16 @@ +using Elsa.Extensions; +using Elsa.JavaScript.Activities; + +namespace Elsa.Workflows.ComponentTests.Scenarios.JavaScriptVariables; + +public class JavaScriptVariablesWorkflow1 : WorkflowBase +{ + public static readonly string DefinitionId = Guid.NewGuid().ToString(); + + protected override void Build(IWorkflowBuilder builder) + { + builder.WithDefinitionId(DefinitionId); + builder.WithVariable("MagicNumber", 3).WithWorkflowStorage(); + builder.Root = new RunJavaScript("setMagicNumber(42)", default, default); + } +} \ No newline at end of file diff --git a/test/component/Elsa.Workflows.ComponentTests/Scenarios/JavaScriptVariables/JavaScriptVariablesWorkflow2.cs b/test/component/Elsa.Workflows.ComponentTests/Scenarios/JavaScriptVariables/JavaScriptVariablesWorkflow2.cs new file mode 100644 index 000000000..d6684b401 --- /dev/null +++ b/test/component/Elsa.Workflows.ComponentTests/Scenarios/JavaScriptVariables/JavaScriptVariablesWorkflow2.cs @@ -0,0 +1,16 @@ +using Elsa.Extensions; +using Elsa.JavaScript.Activities; + +namespace Elsa.Workflows.ComponentTests.Scenarios.JavaScriptVariables; + +public class JavaScriptVariablesWorkflow2 : WorkflowBase +{ + public static readonly string DefinitionId = Guid.NewGuid().ToString(); + + protected override void Build(IWorkflowBuilder builder) + { + builder.WithDefinitionId(DefinitionId); + builder.WithVariable("MagicNumber", 3).WithWorkflowStorage(); + builder.Root = new RunJavaScript("variables.MagicNumber = 42", default, default); + } +} \ No newline at end of file diff --git a/test/component/Elsa.Workflows.ComponentTests/Scenarios/JavaScriptVariables/JavaScriptVariablesWorkflow3.cs b/test/component/Elsa.Workflows.ComponentTests/Scenarios/JavaScriptVariables/JavaScriptVariablesWorkflow3.cs new file mode 100644 index 000000000..34486085e --- /dev/null +++ b/test/component/Elsa.Workflows.ComponentTests/Scenarios/JavaScriptVariables/JavaScriptVariablesWorkflow3.cs @@ -0,0 +1,16 @@ +using Elsa.Extensions; +using Elsa.JavaScript.Activities; + +namespace Elsa.Workflows.ComponentTests.Scenarios.JavaScriptVariables; + +public class JavaScriptVariablesWorkflow3 : WorkflowBase +{ + public static readonly string DefinitionId = Guid.NewGuid().ToString(); + + protected override void Build(IWorkflowBuilder builder) + { + builder.WithDefinitionId(DefinitionId); + builder.WithVariable("MagicNumber", 3).WithWorkflowStorage(); + builder.Root = new RunJavaScript("setVariable('MagicNumber', 42)", default, default); + } +} \ No newline at end of file diff --git a/test/component/Elsa.Workflows.ComponentTests/Scenarios/JavaScriptVariables/JavaScriptVariablesWorkflowTests.cs b/test/component/Elsa.Workflows.ComponentTests/Scenarios/JavaScriptVariables/JavaScriptVariablesWorkflowTests.cs new file mode 100644 index 000000000..c2876f599 --- /dev/null +++ b/test/component/Elsa.Workflows.ComponentTests/Scenarios/JavaScriptVariables/JavaScriptVariablesWorkflowTests.cs @@ -0,0 +1,51 @@ +using Elsa.Expressions.Helpers; +using Elsa.Extensions; +using Elsa.Workflows.ComponentTests.Abstractions; +using Elsa.Workflows.ComponentTests.Fixtures; +using Elsa.Workflows.Management; +using Elsa.Workflows.Models; +using Elsa.Workflows.Runtime; +using Elsa.Workflows.Runtime.Messages; +using Elsa.Workflows.State; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Workflows.ComponentTests.Scenarios.JavaScriptVariables; + +public class JavaScriptVariablesWorkflowTests(App app) : AppComponentTest(app) +{ + [Theory(DisplayName = "SetVariable JS function sets a variable and does not get overridden by variables API")] + [MemberData(nameof(GetWorkflowDefinitions))] + public async Task SetVariableRetainsValue(string workflowDefinitionId) + { + var workflowRuntime = Scope.ServiceProvider.GetRequiredService(); + var workflowInstanceStore = Scope.ServiceProvider.GetRequiredService(); + var workflowClient = await workflowRuntime.CreateClientAsync(); + var runAndCreateRequest = new CreateAndRunWorkflowInstanceRequest + { + WorkflowDefinitionHandle = WorkflowDefinitionHandle.ByDefinitionId(workflowDefinitionId) + }; + var runResponse = await workflowClient.CreateAndRunInstanceAsync(runAndCreateRequest); + var workflowInstanceId = runResponse.WorkflowInstanceId; + var workflowInstance = await workflowInstanceStore.FindAsync(workflowInstanceId); + var workflowState = workflowInstance!.WorkflowState; + var rootWorkflowActivityExecutionContext = workflowState.ActivityExecutionContexts.Single(x => x.ParentContextId == null); + var variables = GetVariablesDictionary(rootWorkflowActivityExecutionContext); + var magicNumber = variables["Workflow1:variable-1"].ConvertTo(); + Assert.Equal(42, magicNumber); + } + + public static IEnumerable GetWorkflowDefinitions() + { + return + [ + [JavaScriptVariablesWorkflow1.DefinitionId], + [JavaScriptVariablesWorkflow2.DefinitionId], + [JavaScriptVariablesWorkflow3.DefinitionId] + ]; + } + + private VariablesDictionary GetVariablesDictionary(ActivityExecutionContextState context) + { + return context.Properties.GetOrAdd(WorkflowInstanceStorageDriver.VariablesDictionaryStateKey, () => new VariablesDictionary()); + } +} \ No newline at end of file