Add support for list/array conversion in JavaScript evaluator
A StringObjectDictionaryConverter has been added, which converts all IList fields of an object to array fields. This enhances the interaction of JS expressions with list types, allowing common Array methods to be applied directly. Furthermore, defaults have been added to the ExpressionEvaluatorOptions parameters to improve usability. Lastly, adjustments were made to ensure variables are correctly set while evaluating expressions.
This commit is contained in:
parent
e7edb1d3ea
commit
1e1abfa100
|
|
@ -26,8 +26,8 @@ const bool useDapper = false;
|
|||
const bool useProtoActor = false;
|
||||
const bool useHangfire = false;
|
||||
const bool useQuartz = true;
|
||||
const bool useMassTransit = true;
|
||||
const bool useMassTransitAzureServiceBus = false;
|
||||
const bool useMassTransit = false;
|
||||
const bool useMassTransitAzureServiceBus = true;
|
||||
const bool useMassTransitRabbitMq = false;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
|
@ -133,7 +133,10 @@ services
|
|||
});
|
||||
}
|
||||
|
||||
runtime.UseMassTransitDispatcher();
|
||||
if(useMassTransit)
|
||||
{
|
||||
runtime.UseMassTransitDispatcher();
|
||||
}
|
||||
runtime.WorkflowInboxCleanupOptions = options => configuration.GetSection("Runtime:WorkflowInboxCleanup").Bind(options);
|
||||
})
|
||||
.UseEnvironments(environments => environments.EnvironmentsOptions = options => configuration.GetSection("Environments").Bind(options))
|
||||
|
|
@ -186,7 +189,11 @@ services
|
|||
ef.UseSqlite(sqliteConnectionString);
|
||||
});
|
||||
|
||||
alterations.UseMassTransitDispatcher();
|
||||
if (useMassTransit)
|
||||
{
|
||||
alterations.UseMassTransitDispatcher();
|
||||
}
|
||||
|
||||
})
|
||||
.UseWorkflowContexts();
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ public interface IJavaScriptEvaluator
|
|||
string expression,
|
||||
Type returnType,
|
||||
ExpressionExecutionContext context,
|
||||
ExpressionEvaluatorOptions options,
|
||||
ExpressionEvaluatorOptions? options = default,
|
||||
Action<Engine>? configureEngine = default,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -37,7 +38,7 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator
|
|||
public async Task<object?> EvaluateAsync(string expression,
|
||||
Type returnType,
|
||||
ExpressionExecutionContext context,
|
||||
ExpressionEvaluatorOptions options,
|
||||
ExpressionEvaluatorOptions? options = default,
|
||||
Action<Engine>? configureEngine = default,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
|
|
@ -47,8 +48,10 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator
|
|||
return result.ConvertTo(returnType);
|
||||
}
|
||||
|
||||
private async Task<Engine> GetConfiguredEngine(Action<Engine>? configureEngine, ExpressionExecutionContext context, ExpressionEvaluatorOptions options, CancellationToken cancellationToken)
|
||||
private async Task<Engine> GetConfiguredEngine(Action<Engine>? configureEngine, ExpressionExecutionContext context, ExpressionEvaluatorOptions? options, CancellationToken cancellationToken)
|
||||
{
|
||||
options ??= new ExpressionEvaluatorOptions();
|
||||
|
||||
var engine = new Engine(opts =>
|
||||
{
|
||||
if (_jintOptions.AllowClrAccess)
|
||||
|
|
@ -76,11 +79,11 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator
|
|||
|
||||
// Create output getters for each activity.
|
||||
CreateActivityOutputAccessors(engine, context);
|
||||
|
||||
|
||||
// Create argument getters for each argument.
|
||||
foreach (var argument in options.Arguments)
|
||||
engine.SetValue($"get{argument.Key}", (Func<object?>)(() => argument.Value));
|
||||
|
||||
|
||||
// Add common functions.
|
||||
engine.SetValue("isNullOrWhiteSpace", (Func<string, bool>)(value => string.IsNullOrWhiteSpace(value)));
|
||||
engine.SetValue("isNullOrEmpty", (Func<string, bool>)(value => string.IsNullOrEmpty(value)));
|
||||
|
|
@ -119,7 +122,7 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator
|
|||
var inputs = context.GetWorkflowInputs();
|
||||
|
||||
foreach (var input in inputs)
|
||||
engine.SetValue($"get{input.Name}", (Func<object?>)(() => input.Value));
|
||||
engine.SetValue($"get{input.Name}", (Func<object?>)(() => StringObjectDictionaryConverter.ConvertListsToArray(input.Value)));
|
||||
}
|
||||
|
||||
private static void CreateVariableAccessors(Engine engine, ExpressionExecutionContext context)
|
||||
|
|
|
|||
|
|
@ -292,10 +292,10 @@ public static class ExpressionExecutionContextExtensions
|
|||
select v;
|
||||
|
||||
var variable = q.FirstOrDefault();
|
||||
|
||||
if(variable != null)
|
||||
|
||||
if (variable != null)
|
||||
variable.Set(context, value);
|
||||
|
||||
|
||||
if (variable == null)
|
||||
CreateVariable(context, variableName, value);
|
||||
}
|
||||
|
|
@ -362,7 +362,7 @@ public static class ExpressionExecutionContextExtensions
|
|||
var input = workflowExecutionContext.Input;
|
||||
return input.TryGetValue(name, out var value) ? value : default;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns the value of the specified input.
|
||||
/// </summary>
|
||||
|
|
@ -393,7 +393,9 @@ public static class ExpressionExecutionContextExtensions
|
|||
/// </summary>
|
||||
public static IEnumerable<ActivityOutputs> GetActivityOutputs(this ExpressionExecutionContext context)
|
||||
{
|
||||
var activityExecutionContext = context.GetActivityExecutionContext();
|
||||
if (!context.TryGetActivityExecutionContext(out var activityExecutionContext))
|
||||
yield break;
|
||||
|
||||
var useActivityName = activityExecutionContext.WorkflowExecutionContext.Workflow.CreatedWithModernTooling();
|
||||
var activitiesWithOutputs = activityExecutionContext.GetActivitiesWithOutputs();
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Dynamic;
|
||||
using System.Threading.Tasks;
|
||||
using Elsa.Expressions.Models;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.JavaScript.Contracts;
|
||||
using Elsa.Testing.Shared;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Elsa.IntegrationTests.Scenarios.JavaScriptListsAndArrays;
|
||||
|
||||
public class Tests
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly IJavaScriptEvaluator _evaluator;
|
||||
|
||||
public Tests(ITestOutputHelper testOutputHelper)
|
||||
{
|
||||
var testOutputHelper1 = testOutputHelper ?? throw new ArgumentNullException(nameof(testOutputHelper));
|
||||
_services = new TestApplicationBuilder(testOutputHelper1).Build();
|
||||
_evaluator = _services.GetRequiredService<IJavaScriptEvaluator>();
|
||||
}
|
||||
|
||||
[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<string> { "a", "b", "c" };
|
||||
var script = "getObj().List.filter(x => x === 'b').length === 1";
|
||||
var context = new ExpressionExecutionContext(_services, new MemoryRegister());
|
||||
context.SetVariable("obj", (object)dynamicObject);
|
||||
var result = await _evaluator.EvaluateAsync(script, typeof(bool), context);
|
||||
|
||||
Assert.True((bool)result!);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue