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/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..c7d733d65 100644 --- a/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs +++ b/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs @@ -11,6 +11,7 @@ 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 +57,17 @@ 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 (ObjectArrayHelper.DetermineIfObjectIsArrayLikeClrCollection(target.GetType())) + instance.Prototype = engine.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) 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..d1ef1c657 --- /dev/null +++ b/test/integration/Elsa.IntegrationTests/Scenarios/JavaScriptListAndArray/Tests.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using System.Dynamic; +using Elsa.JavaScript.Helpers; +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 (ObjectArrayHelper.DetermineIfObjectIsArrayLikeClrCollection(target.GetType())) + instance.Prototype = engine.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