From 71d56e16652454b33c66458294cfbfd350bba825 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 13 Aug 2024 19:58:18 +0200 Subject: [PATCH] Add ExpandoObjectConverter for JavaScript object conversion Introduced `ConverterHelper` and `ExpandoObjectConverter` to handle conversions from .NET dynamic objects to JavaScript objects. Updated integration tests to validate the conversion and sorting functionalities. Fixes #5895 --- Elsa.sln | 1 + .../Helpers/ConverterHelper.cs | 59 ++++++++++++++ .../ExpandoObjectConverter.cs | 22 ++++++ .../Services/JintJavaScriptEvaluator.cs | 2 +- .../JavaScriptListsAndArrays/Tests.cs | 76 +++++++++++++++---- 5 files changed, 146 insertions(+), 14 deletions(-) create mode 100644 src/modules/Elsa.JavaScript/Helpers/ConverterHelper.cs create mode 100644 src/modules/Elsa.JavaScript/ObjectConverters/ExpandoObjectConverter.cs diff --git a/Elsa.sln b/Elsa.sln index 421df26cf..3d9bca0c0 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -799,6 +799,7 @@ Global {F50336DA-42D1-4DD1-A107-67AFEB8A33EE}.Release|Any CPU.Build.0 = Release|Any CPU {99F2B1DA-2F69-4D70-A2A3-AC985AD91EC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {99F2B1DA-2F69-4D70-A2A3-AC985AD91EC4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {99F2B1DA-2F69-4D70-A2A3-AC985AD91EC4}.Debug|Any CPU.Build.0 = Debug|Any CPU {BBCE36D1-6767-4ED1-B3E8-84D2567A962A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BBCE36D1-6767-4ED1-B3E8-84D2567A962A}.Debug|Any CPU.Build.0 = Debug|Any CPU {BBCE36D1-6767-4ED1-B3E8-84D2567A962A}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/src/modules/Elsa.JavaScript/Helpers/ConverterHelper.cs b/src/modules/Elsa.JavaScript/Helpers/ConverterHelper.cs new file mode 100644 index 000000000..44e7645b7 --- /dev/null +++ b/src/modules/Elsa.JavaScript/Helpers/ConverterHelper.cs @@ -0,0 +1,59 @@ +using System.Collections; +using Elsa.Extensions; +using Jint; +using Jint.Native; +using Jint.Native.Object; +using Jint.Runtime.Descriptors; + +namespace Elsa.JavaScript.Helpers; + +internal static class ConverterHelper +{ + public static ObjectInstance ConvertToJsObject(Engine engine, IDictionary expando) + { + var jsObject = engine.Intrinsics.Object.Construct([]); + + foreach (var kvp in expando) + { + var value = kvp.Value; + var jsValue = ConvertToJsValue(engine, value); + var propertyDescriptor = new PropertyDescriptor(jsValue, true, true, true); + jsObject.DefineOwnProperty(kvp.Key, propertyDescriptor); + } + + return jsObject; + } + + private static JsValue ConvertToJsValue(Engine engine, object? value) + { + if (value == null) + return JsValue.Null; + + if (value is IDictionary dict) + return ConvertToJsObject(engine, dict); + + var valueType = value.GetType(); + if (valueType.IsCollectionType()) + { + var list = (ICollection)value; + var jsArray = engine.Intrinsics.Array.Construct(list.Count); + var index = 0; + + foreach (var item in list) + jsArray.Set(index++, ConvertToJsValue(engine, item), true); + + return jsArray; + } + + if (value is string str) + return JsValue.FromObject(engine, str); + + if (value is int or double or float or decimal) + return JsValue.FromObject(engine, Convert.ToDouble(value)); + + if (value is bool b) + return JsValue.FromObject(engine, b); + + return JsValue.FromObject(engine, value); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.JavaScript/ObjectConverters/ExpandoObjectConverter.cs b/src/modules/Elsa.JavaScript/ObjectConverters/ExpandoObjectConverter.cs new file mode 100644 index 000000000..23976a57c --- /dev/null +++ b/src/modules/Elsa.JavaScript/ObjectConverters/ExpandoObjectConverter.cs @@ -0,0 +1,22 @@ +using System.Dynamic; +using Elsa.JavaScript.Helpers; +using Jint; +using Jint.Native; +using Jint.Runtime.Interop; + +namespace Elsa.JavaScript.ObjectConverters; + +internal class ExpandoObjectConverter : IObjectConverter +{ + public bool TryConvert(Engine engine, object value, out JsValue result) + { + if (value is ExpandoObject expandoObject) + { + result = ConverterHelper.ConvertToJsObject(engine, expandoObject); + return true; + } + + result = JsValue.Null; + return false; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs b/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs index e718f7c4b..c185bb8e4 100644 --- a/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs +++ b/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs @@ -70,7 +70,7 @@ public class JintJavaScriptEvaluator(IConfiguration configuration, INotification return instance; }); - opts.Interop.ObjectConverters.Add(new ByteArrayConverter()); + opts.Interop.ObjectConverters.AddRange([new ByteArrayConverter(), new ExpandoObjectConverter()]); }); configureEngine?.Invoke(engine); diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JavaScriptListsAndArrays/Tests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JavaScriptListsAndArrays/Tests.cs index 483323224..161d9e51c 100644 --- a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JavaScriptListsAndArrays/Tests.cs +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JavaScriptListsAndArrays/Tests.cs @@ -11,40 +11,50 @@ namespace Elsa.Workflows.IntegrationTests.Scenarios.JavaScriptListsAndArrays; public class Tests { - private readonly IServiceProvider _services; private readonly IJavaScriptEvaluator _evaluator; private readonly ExpressionExecutionContext _expressionContext; public Tests(ITestOutputHelper testOutputHelper) { var testOutputHelper1 = testOutputHelper ?? throw new ArgumentNullException(nameof(testOutputHelper)); - _services = new TestApplicationBuilder(testOutputHelper1).Build(); - _evaluator = _services.GetRequiredService(); - _expressionContext = new ExpressionExecutionContext(_services, new MemoryRegister()); + var services = new TestApplicationBuilder(testOutputHelper1).Build(); + _evaluator = services.GetRequiredService(); + _expressionContext = new ExpressionExecutionContext(services, new MemoryRegister()); } [Fact(DisplayName = "Workflow inputs containing .NET lists on dynamic objects are converted to arrays for use in JavaScript.")] public async Task Test1() { dynamic dynamicObject = new ExpandoObject(); - - dynamicObject.List = new List { "a", "b", "c" }; + + dynamicObject.List = new List + { + "a", + "b", + "c" + }; var script = "getObj().List.filter(x => x === 'b').length === 1"; _expressionContext.SetVariable("obj", (object)dynamicObject); var result = await _evaluator.EvaluateAsync(script, typeof(bool), _expressionContext); - + Assert.True((bool)result!); } - + [Fact(DisplayName = "Can access list properties as arrays")] public async Task Test2() { var person = new ExpandoObject(); - var magicNumbers = new[]{42, 43, 44}; + var magicNumbers = new[] + { + 42, 43, 44 + }; var order1 = new ExpandoObject(); - + order1.TryAdd("magicNumbers", magicNumbers); - person.TryAdd("orders", new []{ order1 }); + person.TryAdd("orders", new[] + { + order1 + }); person.TryAdd("name", "John"); person.TryAdd("age", 12); @@ -57,8 +67,11 @@ public class Tests person.TryAdd("languages", languages); var obj = new ExpandoObject(); - obj.TryAdd("persons", new List { person }); - + obj.TryAdd("persons", new List + { + person + }); + var name = await _evaluator.EvaluateAsync("o.persons.filter(x => x.age == 12)[0].name", typeof(string), _expressionContext, configureEngine: engine => engine.SetValue("o", obj)); var language = await _evaluator.EvaluateAsync("o.persons[0].languages.filter(x => x == 'English')[0]", typeof(string), _expressionContext, configureEngine: engine => engine.SetValue("o", obj)); var magicNumber = await _evaluator.EvaluateAsync("o.persons[0].orders[0].magicNumbers[1]", typeof(int), _expressionContext, configureEngine: engine => engine.SetValue("o", obj)); @@ -66,4 +79,41 @@ public class Tests Assert.Equal("English", language); Assert.Equal(43, magicNumber); } + + [Theory(DisplayName = "Can sort array and list properties as mutable arrays")] + [MemberData(nameof(ArraySortEnumerableData))] + // See also: https://github.com/sebastienros/jint/issues/1942 + public async Task Test5(IEnumerable collection) + { + dynamic dynamicObject = new ExpandoObject(); + dynamicObject.Items = collection; + var script = """ + const model = getModel(); + model.Items.sort((a, b) => a - b); + return model; + """; + _expressionContext.SetVariable("Model", (object)dynamicObject); + dynamicObject = (ExpandoObject)(await _evaluator.EvaluateAsync(script, typeof(ExpandoObject), _expressionContext))!; + var items = ((object[])dynamicObject.Items).Cast().ToArray(); + + Assert.Equal([2, 4, 8], items); + } + + public static IEnumerable ArraySortEnumerableData = new object[][] + { + [ + new double[] + { + 8, 4, 2 + } + ], + [ + new List + { + 8, + 4, + 2 + } + ] + }; } \ No newline at end of file