Merge pull request #4856 from elsa-workflows/issue(4849)

Remove StringObjectDictionaryConverter and update JintJavaScriptEvaluator
This commit is contained in:
Sipke Schoorstra 2024-01-31 21:11:58 +01:00 committed by GitHub
commit a53c792c4b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 110 additions and 42 deletions

View file

@ -19,7 +19,7 @@
<ItemGroup>
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
<PackageReference Include="Jint" Version="3.0.0-beta-2057" />
<PackageReference Include="Jint" Version="3.0.0" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,39 @@
using System.Collections;
namespace Elsa.JavaScript.Helpers;
/// <summary>
/// Contains helper methods for working with object arrays.
/// </summary>
public static class ObjectArrayHelper
{
/// <summary>
/// Determines if the specified object is an array-like CLR collection.
/// </summary>
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;
}
}

View file

@ -1,40 +0,0 @@
using System.Collections;
namespace Elsa.JavaScript.Helpers;
/// <summary>
/// Contains methods for converting dictionaries with string keys and object values by replacing IList fields with Array fields.
/// </summary>
public static class StringObjectDictionaryConverter
{
/// <summary>
/// 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.
/// </summary>
public static object? ConvertListsToArray(object? value)
{
if (value is not IDictionary<string, object> dictionary)
return value;
// Copy the dictionary to avoid modifying the original.
dictionary = new Dictionary<string, object>(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;
}
}

View file

@ -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<object?>)(() => StringObjectDictionaryConverter.ConvertListsToArray(input.Value)));
engine.SetValue($"get{input.Name}", (Func<object?>)(() => input.Value));
}
private static void CreateVariableAccessors(Engine engine, ExpressionExecutionContext context)

View file

@ -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;
/// <summary>
/// Contains test cases for the functionality of the Engine class to enable list and array-like objects to be used in JavaScript as arrays.
/// </summary>
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<object>
{
"English",
"French"
};
person.TryAdd("languages", languages);
var obj = new ExpandoObject();
obj.TryAdd("persons", new List<object> { 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);
}
}