From cad624c85fb9973721abb0ac94301503ae28908d Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 31 Jan 2024 19:48:00 +0100 Subject: [PATCH 1/3] Remove StringObjectDictionaryConverter and update JintJavaScriptEvaluator The StringObjectDictionaryConverter class was removed as it is no longer necessary. Instead, the JintJavaScriptEvaluator now wraps objects in ObjectWrapper instances and sets their prototype to Array.prototype if they are array-like. This allows for more convenient and intuitive use of Lists and arrays in JavaScript code within Elsa. A new integration test was created to validate this functionality. --- .../StringObjectDictionaryConverter.cs | 40 ------------------- .../Services/JintJavaScriptEvaluator.cs | 16 +++++++- 2 files changed, 14 insertions(+), 42 deletions(-) delete mode 100644 src/modules/Elsa.JavaScript/Helpers/StringObjectDictionaryConverter.cs diff --git a/src/modules/Elsa.JavaScript/Helpers/StringObjectDictionaryConverter.cs b/src/modules/Elsa.JavaScript/Helpers/StringObjectDictionaryConverter.cs deleted file mode 100644 index 41b8f593a..000000000 --- a/src/modules/Elsa.JavaScript/Helpers/StringObjectDictionaryConverter.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Collections; - -namespace Elsa.JavaScript.Helpers; - -/// -/// Contains methods for converting dictionaries with string keys and object values by replacing IList fields with Array fields. -/// -public static class StringObjectDictionaryConverter -{ - /// - /// Recursively converts all IList fields of an ExpandoObject to Array fields. - /// This allows JS expressions to properly use Array methods on lists, such as .length, filter, etc. - /// - public static object? ConvertListsToArray(object? value) - { - if (value is not IDictionary dictionary) - return value; - - // Copy the dictionary to avoid modifying the original. - dictionary = new Dictionary(dictionary); - var keys = dictionary.Keys.ToList(); - foreach (var key in keys) - { - if (dictionary[key] is IList && dictionary[key].GetType().IsGenericType) - { - var list = (IList)dictionary[key]; - var elementType = dictionary[key].GetType().GetGenericArguments()[0]; - var array = Array.CreateInstance(elementType, list.Count); - list.CopyTo(array, 0); - dictionary[key] = array; - } - else - { - ConvertListsToArray(dictionary[key]); - } - } - - return dictionary; - } -} \ 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 127043d00..fbae349f6 100644 --- a/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs +++ b/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs @@ -5,12 +5,12 @@ using Elsa.Expressions.Helpers; using Elsa.Expressions.Models; using Elsa.Extensions; using Elsa.JavaScript.Contracts; -using Elsa.JavaScript.Helpers; using Elsa.JavaScript.Notifications; using Elsa.JavaScript.Options; using Elsa.Mediator.Contracts; using Humanizer; using Jint; +using Jint.Runtime.Interop; using Microsoft.Extensions.Options; // ReSharper disable ConvertClosureToMethodGroup @@ -56,6 +56,18 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator { if (_jintOptions.AllowClrAccess) opts.AllowClr(); + + // Wrap objects in ObjectWrapper instances and set their prototype to Array.prototype if they are array-like. + opts.SetWrapObjectHandler((engine, target, type) => + { + var instance = new ObjectWrapper(engine, target); + if (instance.IsArrayLike) + { + instance.SetPrototypeOf(engine.Realm.Intrinsics.Array.PrototypeObject); + } + + return instance; + }); }); configureEngine?.Invoke(engine); @@ -122,7 +134,7 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator var inputs = context.GetWorkflowInputs(); foreach (var input in inputs) - engine.SetValue($"get{input.Name}", (Func)(() => StringObjectDictionaryConverter.ConvertListsToArray(input.Value))); + engine.SetValue($"get{input.Name}", (Func)(() => input.Value)); } private static void CreateVariableAccessors(Engine engine, ExpressionExecutionContext context) From 7575b78d8bd8790ab74606f5940f0d8a8201971c Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 31 Jan 2024 19:48:07 +0100 Subject: [PATCH 2/3] Add JavaScript list and array tests A new unit test class is added in the IntegrationTests project. The class, located in the JavaScriptListAndArray directory, focuses on testing the engine's ability to handle JavaScript list and array-like objects. --- .../Scenarios/JavaScriptListAndArray/Tests.cs | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 test/integration/Elsa.IntegrationTests/Scenarios/JavaScriptListAndArray/Tests.cs diff --git a/test/integration/Elsa.IntegrationTests/Scenarios/JavaScriptListAndArray/Tests.cs b/test/integration/Elsa.IntegrationTests/Scenarios/JavaScriptListAndArray/Tests.cs new file mode 100644 index 000000000..12e9ba714 --- /dev/null +++ b/test/integration/Elsa.IntegrationTests/Scenarios/JavaScriptListAndArray/Tests.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using System.Dynamic; +using Jint; +using Jint.Runtime.Interop; +using Xunit; + +namespace Elsa.IntegrationTests.Scenarios.JavaScriptListAndArray; + +/// +/// Contains test cases for the functionality of the Engine class to enable list and array-like objects to be used in JavaScript as arrays. +/// +public class Tests +{ + private readonly Engine _engine; + + public Tests() + { + _engine = new Engine(cfg => cfg + .SetWrapObjectHandler((engine, target, type) => + { + var instance = new ObjectWrapper(engine, target); + if (instance.IsArrayLike) + { + instance.SetPrototypeOf(engine.Realm.Intrinsics.Array.PrototypeObject); + } + + return instance; + }) + ); + } + + [Fact(DisplayName = "Can access list properties as arrays")] + public void Test1() + { + var person = new ExpandoObject(); + + person.TryAdd("name", "John"); + person.TryAdd("age", 12); + + var languages = new List + { + "English", + "French" + }; + + person.TryAdd("languages", languages); + + var obj = new ExpandoObject(); + obj.TryAdd("persons", new List { person }); + _engine.SetValue("o", obj); + + var name = _engine.Evaluate("o.persons.filter(x => x.age == 12)[0].name").ToString(); + var language = _engine.Evaluate("o.persons[0].languages.filter(x => x == 'English')[0]").ToString(); + Assert.Equal("John", name); + Assert.Equal("English", language); + } +} \ No newline at end of file From a1c30facbae44590ebd8d5662e052109950d535c Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 31 Jan 2024 21:10:13 +0100 Subject: [PATCH 3/3] Add ObjectArrayHelper and update array detection method A new ObjectArrayHelper class has been added into the Elsa.JavaScript module to improve array detection. The previous IsArrayLike method used in JintJavaScriptEvaluator and integration tests has been replaced by the DetermineIfObjectIsArrayLikeClrCollection method from this helper. These changes also provoked an update of the Jint package version from 3.0.0-beta-2057 to 3.0.0. --- .../Elsa.JavaScript/Elsa.JavaScript.csproj | 2 +- .../Helpers/ObjectArrayHelper.cs | 39 +++++++++++++++++++ .../Services/JintJavaScriptEvaluator.cs | 8 ++-- .../Scenarios/JavaScriptListAndArray/Tests.cs | 8 ++-- 4 files changed, 48 insertions(+), 9 deletions(-) create mode 100644 src/modules/Elsa.JavaScript/Helpers/ObjectArrayHelper.cs diff --git a/src/modules/Elsa.JavaScript/Elsa.JavaScript.csproj b/src/modules/Elsa.JavaScript/Elsa.JavaScript.csproj index fcc2b11b8..26dc246fa 100644 --- a/src/modules/Elsa.JavaScript/Elsa.JavaScript.csproj +++ b/src/modules/Elsa.JavaScript/Elsa.JavaScript.csproj @@ -19,7 +19,7 @@ - + diff --git a/src/modules/Elsa.JavaScript/Helpers/ObjectArrayHelper.cs b/src/modules/Elsa.JavaScript/Helpers/ObjectArrayHelper.cs new file mode 100644 index 000000000..059a7b49e --- /dev/null +++ b/src/modules/Elsa.JavaScript/Helpers/ObjectArrayHelper.cs @@ -0,0 +1,39 @@ +using System.Collections; + +namespace Elsa.JavaScript.Helpers; + +/// +/// Contains helper methods for working with object arrays. +/// +public static class ObjectArrayHelper +{ + /// + /// Determines if the specified object is an array-like CLR collection. + /// + public static bool DetermineIfObjectIsArrayLikeClrCollection(Type type) + { + var isDictionary = typeof(IDictionary).IsAssignableFrom(type); + + if (isDictionary) + return false; + + if (typeof(ICollection).IsAssignableFrom(type)) + return true; + + foreach (var interfaceType in type.GetInterfaces()) + { + if (!interfaceType.IsGenericType) + { + continue; + } + + if (interfaceType.GetGenericTypeDefinition() == typeof(IReadOnlyCollection<>) + || interfaceType.GetGenericTypeDefinition() == typeof(ICollection<>)) + { + return true; + } + } + + 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 fbae349f6..c7d733d65 100644 --- a/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs +++ b/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs @@ -5,6 +5,7 @@ using Elsa.Expressions.Helpers; using Elsa.Expressions.Models; using Elsa.Extensions; using Elsa.JavaScript.Contracts; +using Elsa.JavaScript.Helpers; using Elsa.JavaScript.Notifications; using Elsa.JavaScript.Options; using Elsa.Mediator.Contracts; @@ -61,10 +62,9 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator opts.SetWrapObjectHandler((engine, target, type) => { var instance = new ObjectWrapper(engine, target); - if (instance.IsArrayLike) - { - instance.SetPrototypeOf(engine.Realm.Intrinsics.Array.PrototypeObject); - } + + if (ObjectArrayHelper.DetermineIfObjectIsArrayLikeClrCollection(target.GetType())) + instance.Prototype = engine.Intrinsics.Array.PrototypeObject; return instance; }); diff --git a/test/integration/Elsa.IntegrationTests/Scenarios/JavaScriptListAndArray/Tests.cs b/test/integration/Elsa.IntegrationTests/Scenarios/JavaScriptListAndArray/Tests.cs index 12e9ba714..d1ef1c657 100644 --- a/test/integration/Elsa.IntegrationTests/Scenarios/JavaScriptListAndArray/Tests.cs +++ b/test/integration/Elsa.IntegrationTests/Scenarios/JavaScriptListAndArray/Tests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Dynamic; +using Elsa.JavaScript.Helpers; using Jint; using Jint.Runtime.Interop; using Xunit; @@ -19,10 +20,9 @@ public class Tests .SetWrapObjectHandler((engine, target, type) => { var instance = new ObjectWrapper(engine, target); - if (instance.IsArrayLike) - { - instance.SetPrototypeOf(engine.Realm.Intrinsics.Array.PrototypeObject); - } + + if (ObjectArrayHelper.DetermineIfObjectIsArrayLikeClrCollection(target.GetType())) + instance.Prototype = engine.Intrinsics.Array.PrototypeObject; return instance; })