Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for list/array conversion in JavaScript evaluator #4850

Merged
merged 1 commit into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions src/bundles/Elsa.Server.Web/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@
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;
sfmskywalker marked this conversation as resolved.
Show resolved Hide resolved
const bool useMassTransitRabbitMq = false;

var builder = WebApplication.CreateBuilder(args);
Expand Down Expand Up @@ -133,7 +133,10 @@
});
}

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))
Expand Down Expand Up @@ -186,7 +189,11 @@
ef.UseSqlite(sqliteConnectionString);
});

alterations.UseMassTransitDispatcher();
if (useMassTransit)
{
alterations.UseMassTransitDispatcher();
}

})
.UseWorkflowContexts();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
13 changes: 8 additions & 5 deletions src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
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;
Expand Down Expand Up @@ -37,7 +38,7 @@ public JintJavaScriptEvaluator(INotificationSender mediator, IOptions<JintOption
public async Task<object?> EvaluateAsync(string expression,
Type returnType,
ExpressionExecutionContext context,
ExpressionEvaluatorOptions options,
ExpressionEvaluatorOptions? options = default,
Action<Engine>? configureEngine = default,
CancellationToken cancellationToken = default)
{
Expand All @@ -47,8 +48,10 @@ public JintJavaScriptEvaluator(INotificationSender mediator, IOptions<JintOption
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)
Expand Down Expand Up @@ -76,11 +79,11 @@ private async Task<Engine> GetConfiguredEngine(Action<Engine>? configureEngine,

// 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)));
Expand Down Expand Up @@ -119,7 +122,7 @@ private void CreateWorkflowInputAccessors(Engine engine, ExpressionExecutionCont
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,10 +292,10 @@ where v.TryGet(context, out _)
select v;

var variable = q.FirstOrDefault();
if(variable != null)

if (variable != null)
variable.Set(context, value);

if (variable == null)
CreateVariable(context, variableName, value);
}
Expand Down Expand Up @@ -362,7 +362,7 @@ public static IEnumerable<Variable> EnumerateVariablesInScope(this ExpressionExe
var input = workflowExecutionContext.Input;
return input.TryGetValue(name, out var value) ? value : default;
}

/// <summary>
/// Returns the value of the specified input.
/// </summary>
Expand Down Expand Up @@ -393,7 +393,9 @@ public static IEnumerable<Variable> EnumerateVariablesInScope(this ExpressionExe
/// </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();

Expand Down
Original file line number Diff line number Diff line change
@@ -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!);
}
}