diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs index 168e102e4..3d4817e39 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs @@ -90,13 +90,12 @@ namespace Squidex.Domain.Apps.Core.HandleRules { var script = trimmed.Substring(ScriptPrefix.Length, trimmed.Length - ScriptPrefix.Length - ScriptSuffix.Length); - var customFunctions = new Dictionary> + var context = new ScriptContext { - ["contentUrl"] = () => ContentUrl(@event), - ["contentAction"] = () => ContentAction(@event) + ["event"] = @event }; - return scriptEngine.Interpolate("event", @event, script, customFunctions); + return scriptEngine.Interpolate(context, script); } var current = text.AsSpan(); diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Scripting/EventScriptExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Scripting/EventScriptExtension.cs new file mode 100644 index 000000000..09679802a --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Scripting/EventScriptExtension.cs @@ -0,0 +1,50 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Jint.Native; +using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.HandleRules.Scripting +{ + public sealed class EventScriptExtension : IScriptExtension + { + private delegate JsValue EventDelegate(); + private readonly IUrlGenerator urlGenerator; + + public EventScriptExtension(IUrlGenerator urlGenerator) + { + Guard.NotNull(urlGenerator); + + this.urlGenerator = urlGenerator; + } + + public void Extend(ExecutionContext context, bool async) + { + context.Engine.SetValue("contentAction", new EventDelegate(() => + { + if (context.TryGetValue("event", out var temp) && temp is EnrichedContentEvent contentEvent) + { + return contentEvent.Status.ToString(); + } + + return JsValue.Null; + })); + + context.Engine.SetValue("contentUrl", new EventDelegate(() => + { + if (context.TryGetValue("event", out var temp) && temp is EnrichedContentEvent contentEvent) + { + return urlGenerator.ContentUI(contentEvent.AppId, contentEvent.SchemaId, contentEvent.Id); + } + + return JsValue.Null; + })); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/DefaultConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/DefaultConverter.cs index b65a10901..17de5000d 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/DefaultConverter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/DefaultConverter.cs @@ -44,6 +44,9 @@ namespace Squidex.Domain.Apps.Core.Scripting case ClaimsPrincipal principal: result = JintUser.Create(engine, principal); return true; + case Guid guid: + result = guid.ToString(); + return true; case Instant instant: result = JsValue.FromObject(engine, instant.ToDateTimeUtc()); return true; diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ExecutionContext.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ExecutionContext.cs new file mode 100644 index 000000000..63e63bc4f --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ExecutionContext.cs @@ -0,0 +1,47 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading; +using Jint; + +namespace Squidex.Domain.Apps.Core.Scripting +{ + public delegate bool ExceptionHandler(Exception exception); + + public sealed class ExecutionContext : Dictionary + { + private readonly ExceptionHandler? exceptionHandler; + + public Engine Engine { get; } + + public CancellationToken CancellationToken { get; } + + internal ExecutionContext(Engine engine, CancellationToken cancellationToken, ExceptionHandler? exceptionHandler = null) + : base(StringComparer.OrdinalIgnoreCase) + { + Engine = engine; + + CancellationToken = cancellationToken; + + this.exceptionHandler = exceptionHandler; + } + + public void Fail(Exception exception) + { + exceptionHandler?.Invoke(exception); + } + + public ExecutionContext SetValue(string key, object value) + { + this[key] = value; + + return this; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/DateTimeScriptExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/DateTimeScriptExtension.cs new file mode 100644 index 000000000..38bac0bb7 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/DateTimeScriptExtension.cs @@ -0,0 +1,37 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Globalization; +using Jint; +using Jint.Native; + +namespace Squidex.Domain.Apps.Core.Scripting.Extensions +{ + public sealed class DateTimeScriptExtension : IScriptExtension + { + private delegate JsValue FormatDateDelegate(DateTime date, string format); + + public void Extend(Engine engine) + { + engine.SetValue("formatTime", new FormatDateDelegate(FormatDate)); + engine.SetValue("formatDate", new FormatDateDelegate(FormatDate)); + } + + private static JsValue FormatDate(DateTime date, string format) + { + try + { + return date.ToString(format, CultureInfo.InvariantCulture); + } + catch + { + return JsValue.Undefined; + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpScriptExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpScriptExtension.cs new file mode 100644 index 000000000..054ca3935 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpScriptExtension.cs @@ -0,0 +1,111 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Jint.Native; +using Jint.Native.Json; +using Jint.Runtime; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Core.Scripting.Extensions +{ + public sealed class HttpScriptExtension : IScriptExtension + { + private delegate void GetJsonDelegate(string url, Action callback, JsValue? headers = null); + private readonly IHttpClientFactory httpClientFactory; + + public HttpScriptExtension(IHttpClientFactory httpClientFactory) + { + Guard.NotNull(httpClientFactory); + + this.httpClientFactory = httpClientFactory; + } + + public void Extend(ExecutionContext context, bool async) + { + if (async) + { + var engine = context.Engine; + + engine.SetValue("getJSON", new GetJsonDelegate((url, callback, headers) => GetJson(context, url, callback, headers))); + } + } + + private void GetJson(ExecutionContext context, string url, Action callback, JsValue? headers) + { + GetJsonAsync(context, url, callback, headers).Forget(); + } + + private async Task GetJsonAsync(ExecutionContext context, string url, Action callback, JsValue? headers) + { + try + { + using (var httpClient = httpClientFactory.CreateClient()) + { + var request = CreateRequest(url, headers); + var response = await httpClient.SendAsync(request, context.CancellationToken); + + response.EnsureSuccessStatusCode(); + + var responseObject = await ParseResponse(context, response); + + context.Engine.ResetTimeoutTicks(); + + callback(responseObject); + } + } + catch (Exception ex) + { + context.Fail(ex); + } + } + + private static HttpRequestMessage CreateRequest(string url, JsValue? headers) + { + if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) + { + throw new ArgumentException("Url must be an absolute URL"); + } + + var request = new HttpRequestMessage(HttpMethod.Get, uri); + + if (headers != null && headers.Type == Types.Object) + { + var obj = headers.AsObject(); + + foreach (var (key, property) in obj.GetOwnProperties()) + { + var value = TypeConverter.ToString(property.Value); + + if (!string.IsNullOrWhiteSpace(key)) + { + request.Headers.TryAddWithoutValidation(key, value ?? string.Empty); + } + } + } + + return request; + } + + private async Task ParseResponse(ExecutionContext context, HttpResponseMessage response) + { + var responseString = await response.Content.ReadAsStringAsync(); + + context.CancellationToken.ThrowIfCancellationRequested(); + + var jsonParser = new JsonParser(context.Engine); + var jsonValue = jsonParser.Parse(responseString); + + context.CancellationToken.ThrowIfCancellationRequested(); + + return jsonValue; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringScriptExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringScriptExtension.cs new file mode 100644 index 000000000..1ef1bae0a --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringScriptExtension.cs @@ -0,0 +1,63 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Jint; +using Jint.Native; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Scripting.Extensions +{ + public sealed class StringScriptExtension : IScriptExtension + { + private delegate JsValue StringSlugifyDelegate(string text, bool single = false); + private delegate JsValue StringFormatDelegate(string text); + + public void Extend(Engine engine) + { + engine.SetValue("slugify", new StringSlugifyDelegate(Slugify)); + + engine.SetValue("toCamelCase", new StringFormatDelegate(ToCamelCase)); + engine.SetValue("toPascalCase", new StringFormatDelegate(ToPascalCase)); + } + + private static JsValue Slugify(string text, bool single = false) + { + try + { + return text.Slugify(null, single); + } + catch + { + return JsValue.Undefined; + } + } + + private static JsValue ToCamelCase(string text) + { + try + { + return text.ToCamelCase(); + } + catch + { + return JsValue.Undefined; + } + } + + private static JsValue ToPascalCase(string text) + { + try + { + return text.ToPascalCase(); + } + catch + { + return JsValue.Undefined; + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs index 5e7b99fd2..5db71761f 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs @@ -5,8 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; -using System.Collections.Generic; using System.Threading.Tasks; using Squidex.Domain.Apps.Core.Contents; using Squidex.Infrastructure.Json.Objects; @@ -15,16 +13,16 @@ namespace Squidex.Domain.Apps.Core.Scripting { public interface IScriptEngine { - void Execute(ScriptContext context, string script); + Task ExecuteAsync(ScriptContext context, string script); - NamedContentData ExecuteAndTransform(ScriptContext context, string script); + Task ExecuteAndTransformAsync(ScriptContext context, string script); - NamedContentData Transform(ScriptContext context, string script); + Task TransformAsync(ScriptContext context, string script); Task GetAsync(ScriptContext context, string script); - bool Evaluate(string name, object context, string script); + bool Evaluate(ScriptContext context, string script); - string? Interpolate(string name, object context, string script, Dictionary>? customFormatters = null); + string? Interpolate(ScriptContext context, string script); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptExtension.cs new file mode 100644 index 000000000..aea19fb4b --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptExtension.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Jint; + +namespace Squidex.Domain.Apps.Core.Scripting +{ + public interface IScriptExtension + { + void Extend(Engine engine) + { + } + + void Extend(ExecutionContext context, bool async) + { + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/Parser.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/Parser.cs new file mode 100644 index 000000000..9023427ea --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/Parser.cs @@ -0,0 +1,54 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Esprima; +using Esprima.Ast; +using Microsoft.Extensions.Caching.Memory; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Scripting.Internal +{ + internal sealed class Parser + { + private static readonly TimeSpan Expiration = TimeSpan.FromMinutes(10); + private static readonly ParserOptions DefaultParserOptions = new ParserOptions + { + AdaptRegexp = true, Tolerant = true, Loc = true + }; + + private readonly IMemoryCache memoryCache; + + public Parser(IMemoryCache memoryCache) + { + Guard.NotNull(memoryCache); + + this.memoryCache = memoryCache; + } + + public Program Parse(string script) + { + var key = Key(script); + + if (!memoryCache.TryGetValue(key, out var program)) + { + var parser = new JavaScriptParser(script, DefaultParserOptions); + + program = parser.ParseProgram(); + + memoryCache.Set(key, program, Expiration); + } + + return program; + } + + private static string Key(string script) + { + return $"SCRIPT_{script}"; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintHelpers.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintHelpers.cs deleted file mode 100644 index 9007d0d19..000000000 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintHelpers.cs +++ /dev/null @@ -1,96 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Globalization; -using Jint; -using Jint.Native; -using Jint.Native.Date; -using Jint.Runtime; -using Jint.Runtime.Interop; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.Scripting -{ - internal static class JintHelpers - { - public static Engine AddHelpers(this Engine engine) - { - engine.SetValue("slugify", new ClrFunctionInstance(engine, "slugify", Slugify)); - engine.SetValue("formatTime", new ClrFunctionInstance(engine, "formatTime", FormatDate)); - engine.SetValue("formatDate", new ClrFunctionInstance(engine, "formatDate", FormatDate)); - - return engine; - } - - public static Engine AddFormatters(this Engine engine, Dictionary>? customFormatters = null) - { - if (customFormatters != null) - { - foreach (var (key, value) in customFormatters) - { - engine.SetValue(key, Safe(value)); - } - } - - engine.AddHelpers(); - - return engine; - } - - private static Func Safe(Func func) - { - return () => - { - try - { - return func(); - } - catch - { - return "null"; - } - }; - } - - private static JsValue Slugify(JsValue thisObject, JsValue[] arguments) - { - try - { - var stringInput = TypeConverter.ToString(arguments.At(0)); - var single = false; - - if (arguments.Length > 1) - { - single = TypeConverter.ToBoolean(arguments.At(1)); - } - - return stringInput.Slugify(null, single); - } - catch - { - return JsValue.Undefined; - } - } - - private static JsValue FormatDate(JsValue thisObject, JsValue[] arguments) - { - try - { - var dateValue = ((DateInstance)arguments.At(0)).ToDateTime(); - var dateFormat = TypeConverter.ToString(arguments.At(1)); - - return dateValue.ToString(dateFormat, CultureInfo.InvariantCulture); - } - catch - { - return JsValue.Undefined; - } - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintHttp.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintHttp.cs deleted file mode 100644 index 61ae14626..000000000 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintHttp.cs +++ /dev/null @@ -1,98 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Jint; -using Jint.Native; -using Jint.Native.Json; -using Jint.Runtime; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Core.Scripting -{ - internal sealed class JintHttp - { - private delegate void GetJsonDelegate(string url, Action callback, JsValue? headers = null); - private readonly IHttpClientFactory httpClientFactory; - private readonly Action exceptionHandler; - private readonly CancellationToken cancellationToken; - private JsonParser parser; - - public JintHttp(IHttpClientFactory httpClientFactory, CancellationToken cancellationToken, Action exceptionHandler) - { - this.httpClientFactory = httpClientFactory; - this.exceptionHandler = exceptionHandler; - this.cancellationToken = cancellationToken; - } - - public Engine Add(Engine engine) - { - parser = new JsonParser(engine); - - engine.SetValue("getJSON", new GetJsonDelegate(GetJson)); - - return engine; - } - - private void GetJson(string url, Action callback, JsValue? headers) - { - GetJSONAsync(url, callback, headers).Forget(); - } - - private async Task GetJSONAsync(string url, Action callback, JsValue? headers) - { - try - { - using (var httpClient = httpClientFactory.CreateClient()) - { - if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) - { - throw new ArgumentException("Url must be an absolute URL"); - } - - var request = new HttpRequestMessage(HttpMethod.Get, uri); - - if (headers != null && headers.Type == Types.Object) - { - var obj = headers.AsObject(); - - foreach (var (key, property) in obj.GetOwnProperties()) - { - var value = TypeConverter.ToString(property.Value); - - if (!string.IsNullOrWhiteSpace(key)) - { - request.Headers.TryAddWithoutValidation(key, value ?? string.Empty); - } - } - } - - var response = await httpClient.SendAsync(request, cancellationToken); - - response.EnsureSuccessStatusCode(); - - cancellationToken.ThrowIfCancellationRequested(); - - var responseString = await response.Content.ReadAsStringAsync(); - - cancellationToken.ThrowIfCancellationRequested(); - - var responseJson = parser.Parse(responseString); - - callback(responseJson); - } - } - catch (Exception ex) - { - exceptionHandler(ex); - } - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs index 34539345c..43e5bc308 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs @@ -7,16 +7,17 @@ using System; using System.Collections.Generic; -using System.Net.Http; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Esprima; using Jint; using Jint.Native; using Jint.Runtime; -using Jint.Runtime.Interop; +using Microsoft.Extensions.Caching.Memory; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Scripting.ContentWrapper; +using Squidex.Domain.Apps.Core.Scripting.Internal; using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Validation; @@ -25,74 +26,73 @@ namespace Squidex.Domain.Apps.Core.Scripting { public sealed class JintScriptEngine : IScriptEngine { - private readonly IHttpClientFactory? httpClientFactory; + private readonly IScriptExtension[] extensions; + private readonly Parser parser; public TimeSpan Timeout { get; set; } = TimeSpan.FromMilliseconds(200); - public JintScriptEngine(IHttpClientFactory? httpClientFactory = null) - { - this.httpClientFactory = httpClientFactory; - } + public TimeSpan ExecutionTimeout { get; set; } = TimeSpan.FromMilliseconds(4000); - public void Execute(ScriptContext context, string script) + public JintScriptEngine(IMemoryCache memoryCache, IEnumerable? extensions = null) { - Guard.NotNull(context); - - if (!string.IsNullOrWhiteSpace(script)) - { - var engine = - CreateScriptEngine() - .AddContext(context) - .AddDisallow() - .AddReject(); + parser = new Parser(memoryCache); - Execute(engine, script); - } + this.extensions = extensions?.ToArray() ?? Array.Empty(); } - public NamedContentData ExecuteAndTransform(ScriptContext context, string script) + public async Task ExecuteAsync(ScriptContext context, string script) { Guard.NotNull(context); + Guard.NotNullOrEmpty(script); - var result = context.Data!; - - if (!string.IsNullOrWhiteSpace(script)) + using (var cts = new CancellationTokenSource(ExecutionTimeout)) { - var engine = - CreateScriptEngine() - .AddContext(context) - .AddDisallow() - .AddReject(); + var tcs = new TaskCompletionSource(); - engine.SetValue("replace", new Action(() => + using (cts.Token.Register(() => tcs.TrySetCanceled())) { - var dataInstance = engine.GetValue("ctx").AsObject().Get("data"); + var engine = + CreateEngine(context, true, cts.Token, tcs.TrySetException, true) + .AddDisallow() + .AddReject(); + + engine.SetValue("complete", new Action(value => + { + tcs.TrySetResult(true); + })); + + Execute(engine, script); - if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data) + if (engine.GetValue("async") != true) { - data.TryUpdate(out result); + tcs.TrySetResult(true); } - })); - Execute(engine, script); + await tcs.Task; + } } - - return result; } - public NamedContentData Transform(ScriptContext context, string script) + public async Task ExecuteAndTransformAsync(ScriptContext context, string script) { Guard.NotNull(context); + Guard.NotNullOrEmpty(script); - var result = context.Data!; - - if (!string.IsNullOrWhiteSpace(script)) + using (var cts = new CancellationTokenSource(ExecutionTimeout)) { - try + var tcs = new TaskCompletionSource(); + + using (cts.Token.Register(() => tcs.TrySetCanceled())) { var engine = - CreateScriptEngine() - .AddContext(context); + CreateEngine(context, true, cts.Token, tcs.TrySetException, true) + .AddDisallow() + .AddReject(); + + engine.SetValue("complete", new Action(value => + { + tcs.TrySetResult(context.Data!); + })); engine.SetValue("replace", new Action(() => { @@ -100,70 +100,96 @@ namespace Squidex.Domain.Apps.Core.Scripting if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data) { - data.TryUpdate(out result); + if (!tcs.Task.IsCompleted) + { + if (data.TryUpdate(out var modified)) + { + tcs.TrySetResult(modified); + } + else + { + tcs.TrySetResult(context.Data!); + } + } } })); - engine.Execute(script); - } - catch (Exception) - { - result = context.Data!; - } - } + Execute(engine, script); - return result; - } + if (engine.GetValue("async") != true) + { + tcs.TrySetResult(context.Data!); + } - private static void Execute(Engine engine, string script) - { - try - { - engine.Execute(script); - } - catch (ArgumentException ex) - { - throw new ValidationException($"Failed to execute script with javascript syntax error: {ex.Message}", new ValidationError(ex.Message)); - } - catch (JavaScriptException ex) - { - throw new ValidationException($"Failed to execute script with javascript error: {ex.Message}", new ValidationError(ex.Message)); - } - catch (ParserException ex) - { - throw new ValidationException($"Failed to execute script with javascript error: {ex.Message}", new ValidationError(ex.Message)); + return await tcs.Task; + } } } - private Engine CreateScriptEngine(IReferenceResolver? resolver = null) + public async Task TransformAsync(ScriptContext context, string script) { - var engine = new Engine(options => + Guard.NotNull(context); + Guard.NotNullOrEmpty(script); + + using (var cts = new CancellationTokenSource(ExecutionTimeout)) { - if (resolver != null) + var tcs = new TaskCompletionSource(); + + using (cts.Token.Register(() => tcs.TrySetCanceled())) { - options.SetReferencesResolver(resolver); - } + var engine = CreateEngine(context, true, cts.Token, tcs.TrySetException, true); - options.TimeoutInterval(Timeout).Strict().AddObjectConverter(DefaultConverter.Instance); - }); + engine.SetValue("complete", new Action(value => + { + tcs.TrySetResult(context.Data!); + })); + + engine.SetValue("replace", new Action(() => + { + var dataInstance = engine.GetValue("ctx").AsObject().Get("data"); + + if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data) + { + if (!tcs.Task.IsCompleted) + { + if (data.TryUpdate(out var modified)) + { + tcs.TrySetResult(modified); + } + else + { + tcs.TrySetResult(context.Data!); + } + } + } + })); - engine.AddHelpers(); + Execute(engine, script); + + if (engine.GetValue("async") != true) + { + tcs.TrySetResult(context.Data!); + } - return engine; + return await tcs.Task; + } + } } - public bool Evaluate(string name, object context, string script) + public bool Evaluate(ScriptContext context, string script) { + Guard.NotNull(context); + Guard.NotNullOrEmpty(script); + try { - var result = - CreateScriptEngine(NullPropagation.Instance) - .SetValue(name, context) - .Execute(script) - .GetCompletionValue() - .ToObject(); + var engine = CreateEngine(context, false); + + Execute(engine, script); + + var converted = Equals(engine.GetCompletionValue().ToObject(), true); - return (bool)result; + return converted; } catch { @@ -171,19 +197,18 @@ namespace Squidex.Domain.Apps.Core.Scripting } } - public string? Interpolate(string name, object context, string script, Dictionary>? customFormatters = null) + public string? Interpolate(ScriptContext context, string script) { + Guard.NotNull(context); + Guard.NotNullOrEmpty(script); + try { - var result = - CreateScriptEngine(NullPropagation.Instance) - .AddFormatters(customFormatters) - .SetValue(name, context) - .Execute(script) - .GetCompletionValue() - .ToObject(); + var engine = CreateEngine(context, false); + + Execute(engine, script); - var converted = result.ToString(); + var converted = engine.GetCompletionValue().ToObject()?.ToString() ?? "null"; return converted == "undefined" ? "null" : converted; } @@ -195,36 +220,89 @@ namespace Squidex.Domain.Apps.Core.Scripting public Task GetAsync(ScriptContext context, string script) { - using (var cts = new CancellationTokenSource(Timeout)) + Guard.NotNull(context); + Guard.NotNullOrEmpty(script); + + using (var cts = new CancellationTokenSource(ExecutionTimeout)) { var tcs = new TaskCompletionSource(); using (cts.Token.Register(() => { - tcs.SetCanceled(); + tcs.TrySetCanceled(); })) { - var engine = - CreateScriptEngine() - .AddContext(context); - - if (httpClientFactory != null) - { - var http = new JintHttp(httpClientFactory, cts.Token, tcs.SetException); - - http.Add(engine); - } + var engine = CreateEngine(context, true, cts.Token, ex => tcs.TrySetException(ex), true); engine.SetValue("complete", new Action(value => { - tcs.SetResult(JsonMapper.Map(value)); + tcs.TrySetResult(JsonMapper.Map(value)); })); engine.Execute(script); + + if (engine.GetValue("async") != true) + { + tcs.TrySetResult(JsonMapper.Map(engine.GetCompletionValue())); + } } return tcs.Task; } } + + private Engine CreateEngine(ScriptContext context, bool nested, CancellationToken cancellationToken = default, ExceptionHandler? exceptionHandler = null, bool async = false) + { + var engine = new Engine(options => + { + options.AddObjectConverter(DefaultConverter.Instance); + options.SetReferencesResolver(NullPropagation.Instance); + options.Strict(); + options.TimeoutInterval(Timeout); + }); + + if (async) + { + engine.SetValue("async", false); + } + + foreach (var extension in extensions) + { + extension.Extend(engine); + } + + var executionContext = new ExecutionContext(engine, cancellationToken, exceptionHandler); + + foreach (var extension in extensions) + { + extension.Extend(executionContext, async); + } + + context.Add(executionContext, nested); + + return executionContext.Engine; + } + + private void Execute(Engine engine, string script) + { + try + { + var program = parser.Parse(script); + + engine.Execute(program); + } + catch (ArgumentException ex) + { + throw new ValidationException($"Failed to execute script with javascript syntax error: {ex.Message}", new ValidationError(ex.Message)); + } + catch (JavaScriptException ex) + { + throw new ValidationException($"Failed to execute script with javascript error: {ex.Message}", new ValidationError(ex.Message)); + } + catch (ParserException ex) + { + throw new ValidationException($"Failed to execute script with javascript error: {ex.Message}", new ValidationError(ex.Message)); + } + } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs index 7d1d89193..174d9bafd 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs @@ -6,25 +6,118 @@ // ========================================================================== using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; using System.Security.Claims; +using Jint.Native; +using Jint.Native.Object; using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Core.Scripting { - public sealed class ScriptContext + public sealed class ScriptContext : Dictionary { - public ClaimsPrincipal User { get; set; } + public ScriptContext() + : base(StringComparer.OrdinalIgnoreCase) + { + } - public Guid ContentId { get; set; } + public ClaimsPrincipal? User + { + get => GetValue(); + set => SetValue(value); + } - public NamedContentData? Data { get; set; } + public Guid ContentId + { + get => GetValue(); + set => SetValue(value); + } - public NamedContentData DataOld { get; set; } + public NamedContentData? Data + { + get => GetValue(); + set => SetValue(value); + } - public Status Status { get; set; } + public NamedContentData? DataOld + { + get => GetValue("oldData"); + set => SetValue(value, "oldData"); + } - public Status StatusOld { get; set; } + public Status Status + { + get => GetValue(); + set => SetValue(value); + } - public string Operation { get; set; } + public Status StatusOld + { + get => GetValue(); + set => SetValue(value); + } + + public string? Operation + { + get => GetValue(); + set => SetValue(value); + } + + public void SetValue(object? value, [CallerMemberNameAttribute] string? key = null) + { + if (key != null) + { + this[key] = value; + } + } + + public T GetValue([CallerMemberNameAttribute] string? key = null) + { + if (key != null && TryGetValue(key, out var temp) && temp is T result) + { + return result; + } + + return default!; + } + + internal void Add(ExecutionContext context, bool nested) + { + var engine = context.Engine; + + if (nested) + { + var contextInstance = new ObjectInstance(engine); + + foreach (var (key, value) in this) + { + var property = key.ToCamelCase(); + + if (value != null) + { + contextInstance.FastAddProperty(property, JsValue.FromObject(engine, value), true, true, true); + context[property] = value; + } + } + + engine.SetValue("ctx", contextInstance); + engine.SetValue("context", contextInstance); + } + else + { + foreach (var (key, value) in this) + { + var property = key.ToCamelCase(); + + if (value != null) + { + engine.SetValue(property, value); + context[property] = value; + } + } + } + } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContextExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContextExtensions.cs deleted file mode 100644 index c853f0bc3..000000000 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContextExtensions.cs +++ /dev/null @@ -1,53 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Jint; -using Jint.Native.Object; -using Squidex.Domain.Apps.Core.Scripting.ContentWrapper; - -namespace Squidex.Domain.Apps.Core.Scripting -{ - internal static class ScriptContextExtensions - { - public static Engine AddContext(this Engine engine, ScriptContext context) - { - var contextInstance = new ObjectInstance(engine); - - if (context.Data != null) - { - contextInstance.FastAddProperty("data", new ContentDataObject(engine, context.Data), true, true, true); - } - - if (context.DataOld != null) - { - contextInstance.FastAddProperty("oldData", new ContentDataObject(engine, context.DataOld), true, true, true); - } - - if (context.User != null) - { - contextInstance.FastAddProperty("user", JintUser.Create(engine, context.User), false, true, false); - } - - if (!string.IsNullOrWhiteSpace(context.Operation)) - { - contextInstance.FastAddProperty("operation", context.Operation, false, false, false); - } - - contextInstance.FastAddProperty("status", context.Status.ToString(), false, false, false); - - if (context.StatusOld != default) - { - contextInstance.FastAddProperty("oldStatus", context.StatusOld.ToString(), false, false, false); - } - - engine.SetValue("ctx", contextInstance); - engine.SetValue("context", contextInstance); - - return engine; - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs index feae623d1..78598674a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs @@ -68,7 +68,17 @@ namespace Squidex.Domain.Apps.Entities.Assets protected override bool Trigger(EnrichedAssetEvent @event, AssetChangedTriggerV2 trigger) { - return string.IsNullOrWhiteSpace(trigger.Condition) || scriptEngine.Evaluate("event", @event, trigger.Condition); + if (string.IsNullOrWhiteSpace(trigger.Condition)) + { + return true; + } + + var context = new ScriptContext + { + ["event"] = @event + }; + + return scriptEngine.Evaluate(context, trigger.Condition); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentTriggerHandler.cs index 6d6ff68d8..7ffcb2c8c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentTriggerHandler.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentTriggerHandler.cs @@ -71,7 +71,17 @@ namespace Squidex.Domain.Apps.Entities.Comments protected override bool Trigger(EnrichedCommentEvent @event, CommentTrigger trigger) { - return string.IsNullOrWhiteSpace(trigger.Condition) || scriptEngine.Evaluate("event", @event, trigger.Condition); + if (string.IsNullOrWhiteSpace(trigger.Condition)) + { + return true; + } + + var context = new ScriptContext + { + ["event"] = @event + }; + + return scriptEngine.Evaluate(context, trigger.Condition); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs index 408422bbf..7667fbb93 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs @@ -140,7 +140,17 @@ namespace Squidex.Domain.Apps.Entities.Contents private bool MatchsCondition(ContentChangedTriggerSchemaV2 schema, EnrichedSchemaEventBase @event) { - return string.IsNullOrWhiteSpace(schema.Condition) || scriptEngine.Evaluate("event", @event, schema.Condition); + if (string.IsNullOrWhiteSpace(schema.Condition)) + { + return true; + } + + var context = new ScriptContext + { + ["event"] = @event + }; + + return scriptEngine.Evaluate(context, schema.Condition); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs index f79d792e8..bf87d3845 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs @@ -96,22 +96,32 @@ namespace Squidex.Domain.Apps.Entities.Contents return data.ValidatePartialAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), message); } - public Task ExecuteScriptAndTransformAsync(Func script, ScriptContext context) + public async Task ExecuteScriptAndTransformAsync(Func script, ScriptContext context) { Enrich(context); - var result = scriptEngine.ExecuteAndTransform(context, GetScript(script)); + var actualScript = GetScript(script); - return Task.FromResult(result); + if (string.IsNullOrWhiteSpace(actualScript)) + { + return context.Data!; + } + + return await scriptEngine.ExecuteAndTransformAsync(context, actualScript); } - public Task ExecuteScriptAsync(Func script, ScriptContext context) + public async Task ExecuteScriptAsync(Func script, ScriptContext context) { Enrich(context); - scriptEngine.Execute(context, GetScript(script)); + var actualScript = GetScript(script); - return Task.CompletedTask; + if (string.IsNullOrWhiteSpace(actualScript)) + { + return; + } + + await scriptEngine.ExecuteAsync(context, GetScript(script)); } private void Enrich(ScriptContext context) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs index eb7f1cb9d..73e282435 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs @@ -115,7 +115,12 @@ namespace Squidex.Domain.Apps.Entities.Contents if (!string.IsNullOrWhiteSpace(condition?.Expression)) { - return scriptEngine.Evaluate("data", data, condition.Expression); + var context = new ScriptContext + { + ["data"] = data + }; + + return scriptEngine.Evaluate(context, condition.Expression); } return true; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ScriptContent.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ScriptContent.cs index 17a910259..2a04a6475 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ScriptContent.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ScriptContent.cs @@ -38,20 +38,22 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps { var results = new List(); - var scriptContext = new ScriptContext { User = context.User }; - - foreach (var content in group) - { - scriptContext.Data = content.Data; - scriptContext.ContentId = content.Id; - - content.Data = scriptEngine.Transform(scriptContext, script); - } + await Task.WhenAll(group.Select(x => TransformAsync(context, script, x))); } } } } + private async Task TransformAsync(Context context, string script, ContentEntity content) + { + var scriptContext = new ScriptContext { User = context.User }; + + scriptContext.Data = content.Data; + scriptContext.ContentId = content.Id; + + content.Data = await scriptEngine.TransformAsync(scriptContext, script); + } + private static bool ShouldEnrich(Context context) { return !context.IsFrontendClient; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs index ebbff50ce..f8fd69840 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs @@ -71,7 +71,17 @@ namespace Squidex.Domain.Apps.Entities.Schemas protected override bool Trigger(EnrichedSchemaEvent @event, SchemaChangedTrigger trigger) { - return string.IsNullOrWhiteSpace(trigger.Condition) || scriptEngine.Evaluate("event", @event, trigger.Condition); + if (string.IsNullOrWhiteSpace(trigger.Condition)) + { + return true; + } + + var context = new ScriptContext + { + ["event"] = @event + }; + + return scriptEngine.Evaluate(context, trigger.Condition); } } } diff --git a/backend/src/Squidex/Config/Domain/InfrastructureServices.cs b/backend/src/Squidex/Config/Domain/InfrastructureServices.cs index 38c942abc..c72717e2c 100644 --- a/backend/src/Squidex/Config/Domain/InfrastructureServices.cs +++ b/backend/src/Squidex/Config/Domain/InfrastructureServices.cs @@ -16,6 +16,7 @@ using Squidex.Areas.Api.Controllers.News; using Squidex.Areas.Api.Controllers.News.Service; using Squidex.Areas.Api.Controllers.UI; using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Core.Scripting.Extensions; using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Entities.Rules.UsageTracking; using Squidex.Domain.Apps.Entities.Tags; @@ -58,6 +59,15 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .AsOptional(); + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + services.AddSingleton>(DomainObjectGrainFormatter.Format); } diff --git a/backend/src/Squidex/Config/Domain/RuleServices.cs b/backend/src/Squidex/Config/Domain/RuleServices.cs index 2ea4f6806..80938ce15 100644 --- a/backend/src/Squidex/Config/Domain/RuleServices.cs +++ b/backend/src/Squidex/Config/Domain/RuleServices.cs @@ -8,6 +8,8 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.HandleRules.Scripting; +using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Comments; using Squidex.Domain.Apps.Entities.Contents; @@ -64,6 +66,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As().AsSelf(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .AsSelf(); diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs index 05fb6986b..1eb040d97 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs @@ -9,11 +9,15 @@ using System; using System.Collections.Generic; using System.Security.Claims; using FakeItEasy; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; using NodaTime; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.HandleRules.Scripting; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Core.Scripting.Extensions; using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; using Squidex.Shared.Identity; @@ -46,7 +50,16 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules A.CallTo(() => urlGenerator.ContentUI(appId, schemaId, contentId)) .Returns("content-url"); - sut = new RuleEventFormatter(TestUtils.DefaultSerializer, urlGenerator, new JintScriptEngine(null)); + var extensions = new IScriptExtension[] + { + new DateTimeScriptExtension(), + new EventScriptExtension(urlGenerator), + new StringScriptExtension() + }; + + var cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); + + sut = new RuleEventFormatter(TestUtils.DefaultSerializer, urlGenerator, new JintScriptEngine(cache, extensions)); } [Fact] @@ -187,6 +200,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Theory] [InlineData("$CONTENT_STATUS")] + [InlineData("Script(contentAction())")] [InlineData("Script(`${event.status}`)")] public void Should_format_content_status_when_found(string script) { @@ -200,7 +214,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Theory] [InlineData("$CONTENT_ACTION")] [InlineData("Script(contentAction())")] - public void Should_null_when_content_status_not_found(string script) + public void Should_return_null_when_content_status_not_found(string script) { var @event = new EnrichedAssetEvent(); @@ -224,7 +238,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Theory] [InlineData("$CONTENT_ACTION")] [InlineData("Script(contentAction())")] - public void Should_null_when_content_action_not_found(string script) + public void Should_return_null_when_content_action_not_found(string script) { var @event = new EnrichedAssetEvent(); diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs new file mode 100644 index 000000000..783a3b508 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs @@ -0,0 +1,231 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using FakeItEasy; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Core.Scripting.Extensions; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Validation; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Operations.Scripting +{ + public class JintScriptEngineHelperTests + { + private readonly IHttpClientFactory httpClientFactory = A.Fake(); + private readonly JintScriptEngine sut; + + public JintScriptEngineHelperTests() + { + var extensions = new IScriptExtension[] + { + new DateTimeScriptExtension(), + new HttpScriptExtension(httpClientFactory), + new StringScriptExtension() + }; + + var cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); + + sut = new JintScriptEngine(cache, extensions) + { + Timeout = TimeSpan.FromSeconds(1) + }; + } + + [Fact] + public void Should_camel_case_value() + { + const string script = @" + return toCamelCase(value); + "; + + var context = new ScriptContext + { + ["value"] = "Hello World" + }; + + var result = sut.Interpolate(context, script); + + Assert.Equal("helloWorld", result); + } + + [Fact] + public void Should_pascal_case_value() + { + const string script = @" + return toPascalCase(value); + "; + + var context = new ScriptContext + { + ["value"] = "Hello World" + }; + + var result = sut.Interpolate(context, script); + + Assert.Equal("HelloWorld", result); + } + + [Fact] + public void Should_slugify_value() + { + const string script = @" + return slugify(value); + "; + + var context = new ScriptContext + { + ["value"] = "4 Häuser" + }; + + var result = sut.Interpolate(context, script); + + Assert.Equal("4-haeuser", result); + } + + [Fact] + public void Should_slugify_value_with_single_char() + { + const string script = @" + return slugify(value, true); + "; + + var context = new ScriptContext + { + ["value"] = "4 Häuser" + }; + + var result = sut.Interpolate(context, script); + + Assert.Equal("4-hauser", result); + } + + [Fact] + public async Task Should_throw_validation_exception_when_calling_reject() + { + const string script = @" + reject() + "; + + var ex = await Assert.ThrowsAsync(() => sut.ExecuteAsync(new ScriptContext(), script)); + + Assert.Empty(ex.Errors); + } + + [Fact] + public async Task Should_throw_validation_exception_when_calling_reject_with_message() + { + const string script = @" + reject('Not valid') + "; + + var ex = await Assert.ThrowsAsync(() => sut.ExecuteAsync(new ScriptContext(), script)); + + Assert.Equal("Not valid", ex.Errors.Single().Message); + } + + [Fact] + public async Task Should_throw_security_exception_when_calling_reject() + { + const string script = @" + disallow() + "; + + var ex = await Assert.ThrowsAsync(() => sut.ExecuteAsync(new ScriptContext(), script)); + + Assert.Equal("Not allowed", ex.Message); + } + + [Fact] + public async Task Should_throw_security_exception_when_calling_reject_with_message() + { + const string script = @" + disallow('Operation not allowed') + "; + + var ex = await Assert.ThrowsAsync(() => sut.ExecuteAsync(new ScriptContext(), script)); + + Assert.Equal("Operation not allowed", ex.Message); + } + + [Fact] + public async Task Should_make_json_request() + { + var httpHandler = SetupRequest(); + + const string script = @" + async = true; + + getJSON('http://squidex.io', function(result) { + complete(result); + }); + "; + + var result = await sut.GetAsync(new ScriptContext(), script); + + httpHandler.ShouldBeMethod(HttpMethod.Get); + httpHandler.ShouldBeUrl("http://squidex.io/"); + + var expectedResult = JsonValue.Object().Add("key", 42); + + Assert.Equal(expectedResult, result); + } + + [Fact] + public async Task Should_make_json_request_with_headers() + { + var httpHandler = SetupRequest(); + + const string script = @" + async = true; + + var headers = { + 'X-Header1': 1, + 'X-Header2': '2' + }; + + getJSON('http://squidex.io', function(result) { + complete(result); + }, headers); + "; + + var result = await sut.GetAsync(new ScriptContext(), script); + + httpHandler.ShouldBeMethod(HttpMethod.Get); + httpHandler.ShouldBeUrl("http://squidex.io/"); + httpHandler.ShouldBeHeader("X-Header1", "1"); + httpHandler.ShouldBeHeader("X-Header2", "2"); + + var expectedResult = JsonValue.Object().Add("key", 42); + + Assert.Equal(expectedResult, result); + } + + private MockupHttpHandler SetupRequest() + { + var httpResponse = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("{ \"key\": 42 }") + }; + + var httpHandler = new MockupHttpHandler(httpResponse); + + A.CallTo(() => httpClientFactory.CreateClient(A._)) + .Returns(new HttpClient(httpHandler)); + + return httpHandler; + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs index cc2734e61..79427dcbf 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs @@ -6,17 +6,16 @@ // ========================================================================== using System; -using System.Linq; using System.Net; using System.Net.Http; using System.Security.Claims; -using System.Threading; using System.Threading.Tasks; using FakeItEasy; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; +using Squidex.Domain.Apps.Core.Scripting.Extensions; using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Validation; using Xunit; @@ -30,135 +29,165 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting public JintScriptEngineTests() { - sut = new JintScriptEngine(httpClientFactory) + var extensions = new IScriptExtension[] { - Timeout = TimeSpan.FromSeconds(1) + new DateTimeScriptExtension(), + new HttpScriptExtension(httpClientFactory), + new StringScriptExtension() }; - } - [Fact] - public void Should_throw_validation_exception_when_calling_reject() - { - const string script = @" - reject() - "; + var httpResponse = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("{ \"key\": 42 }") + }; + + var httpHandler = new MockupHttpHandler(httpResponse); + + A.CallTo(() => httpClientFactory.CreateClient(A._)) + .Returns(new HttpClient(httpHandler)); - var ex = Assert.Throws(() => sut.Execute(new ScriptContext(), script)); + var cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); - Assert.Empty(ex.Errors); + sut = new JintScriptEngine(cache, extensions) + { + Timeout = TimeSpan.FromSeconds(1) + }; } [Fact] - public void Should_throw_validation_exception_when_calling_reject_with_message() + public async Task ExecuteAsync_should_catch_script_syntax_errors() { const string script = @" - reject('Not valid') + invalid() "; - var ex = Assert.Throws(() => sut.Execute(new ScriptContext(), script)); - - Assert.Equal("Not valid", ex.Errors.Single().Message); + await Assert.ThrowsAsync(() => sut.ExecuteAsync(new ScriptContext(), script)); } [Fact] - public void Should_throw_security_exception_when_calling_reject() + public async Task ExecuteAsync_should_catch_script_runtime_errors() { const string script = @" - disallow() + throw 'Error'; "; - var ex = Assert.Throws(() => sut.Execute(new ScriptContext(), script)); - - Assert.Equal("Not allowed", ex.Message); + await Assert.ThrowsAsync(() => sut.ExecuteAsync(new ScriptContext(), script)); } [Fact] - public void Should_throw_security_exception_when_calling_reject_with_message() + public async Task TransformAsync_should_return_original_content_when_script_failed() { + var content = new NamedContentData(); + var context = new ScriptContext { Data = content }; + const string script = @" - disallow('Operation not allowed') + x => x "; - var ex = Assert.Throws(() => sut.Execute(new ScriptContext(), script)); + var result = await sut.TransformAsync(context, script); - Assert.Equal("Operation not allowed", ex.Message); + Assert.Empty(result); } [Fact] - public void Should_catch_script_syntax_errors() + public async Task TransformAsync_should_transform_content() { - const string script = @" - invalid() - "; + var content = + new NamedContentData() + .AddField("number0", + new ContentFieldData() + .AddValue("iv", 1.0)) + .AddField("number1", + new ContentFieldData() + .AddValue("iv", 1.0)); + var expected = + new NamedContentData() + .AddField("number1", + new ContentFieldData() + .AddValue("iv", 2.0)) + .AddField("number2", + new ContentFieldData() + .AddValue("iv", 10.0)); - Assert.Throws(() => sut.Execute(new ScriptContext(), script)); - } + var context = new ScriptContext { Data = content }; - [Fact] - public void Should_catch_script_runtime_errors() - { const string script = @" - throw 'Error'; + var data = ctx.data; + + delete data.number0; + + data.number1.iv = data.number1.iv + 1; + data.number2 = { 'iv': 10 }; + + replace(data); "; - Assert.Throws(() => sut.Execute(new ScriptContext(), script)); + var result = await sut.TransformAsync(context, script); + + Assert.Equal(expected, result); } [Fact] - public void Should_catch_script_runtime_errors_on_execute_and_transform() + public async Task ExecuteAndTransformAsync_should_catch_javascript_error() { const string script = @" throw 'Error'; "; - Assert.Throws(() => sut.ExecuteAndTransform(new ScriptContext(), script)); + await Assert.ThrowsAsync(() => sut.ExecuteAndTransformAsync(new ScriptContext(), script)); } [Fact] - public void Should_return_original_content_when_transform_script_failed() + public async Task ExecuteAndTransformAsync_should_throw_when_script_failed() { var content = new NamedContentData(); var context = new ScriptContext { Data = content }; const string script = @" - x => x + invalid(); "; - var result = sut.Transform(context, script); - - Assert.Same(content, result); + await Assert.ThrowsAsync(() => sut.ExecuteAndTransformAsync(context, script)); } [Fact] - public void Should_throw_when_execute_and_transform_script_failed() + public async Task ExecuteAndTransformAsync_should_return_original_content_when_not_replaced() { var content = new NamedContentData(); var context = new ScriptContext { Data = content }; const string script = @" - invalid(); + var x = 0; "; - Assert.Throws(() => sut.ExecuteAndTransform(context, script)); + var result = await sut.ExecuteAndTransformAsync(context, script); + + Assert.Empty(result); } [Fact] - public void Should_return_original_content_when_content_is_not_replaced() + public async Task ExecuteAndTransformAsync_should_return_original_content_when_not_replaced_async() { var content = new NamedContentData(); var context = new ScriptContext { Data = content }; const string script = @" + async = true; + var x = 0; + + getJSON('http://squidex.io', function(result) { + complete(); + }); "; - var result = sut.ExecuteAndTransform(context, script); + var result = await sut.ExecuteAndTransformAsync(context, script); - Assert.Same(content, result); + Assert.Empty(result); } [Fact] - public void Should_fetch_operation_name() + public async Task ExecuteAndTransformAsync_should_transform_object() { var content = new NamedContentData(); @@ -178,117 +207,86 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting replace(data); "; - var result = sut.ExecuteAndTransform(context, script); + var result = await sut.ExecuteAndTransformAsync(context, script); Assert.Equal(expected, result); } [Fact] - public void Should_transform_content_and_return_with_transform() + public async Task ExecuteAndTransformAsync_should_transform_object_async() { - var content = - new NamedContentData() - .AddField("number0", - new ContentFieldData() - .AddValue("iv", 1.0)) - .AddField("number1", - new ContentFieldData() - .AddValue("iv", 1.0)); + var content = new NamedContentData(); + var expected = new NamedContentData() - .AddField("number1", - new ContentFieldData() - .AddValue("iv", 2.0)) - .AddField("number2", + .AddField("operation", new ContentFieldData() - .AddValue("iv", 10.0)); + .AddValue("iv", 42)); - var context = new ScriptContext { Data = content }; + var context = new ScriptContext { Data = content, Operation = "MyOperation" }; const string script = @" + async = true; + var data = ctx.data; - delete data.number0; + getJSON('http://squidex.io', function(result) { + data.operation = { iv: result.key }; - data.number1.iv = data.number1.iv + 1; - data.number2 = { 'iv': 10 }; + replace(data); + }); - replace(data); "; - var result = sut.Transform(context, script); + var result = await sut.ExecuteAndTransformAsync(context, script); Assert.Equal(expected, result); } [Fact] - public void Should_slugify_value() + public async Task ExecuteAndTransformAsync_should_ignore_transformation_when_async_not_set() { - var content = - new NamedContentData() - .AddField("title", - new ContentFieldData() - .AddValue("iv", "4 Häuser")); - - var expected = - new NamedContentData() - .AddField("title", - new ContentFieldData() - .AddValue("iv", "4 Häuser")) - .AddField("slug", - new ContentFieldData() - .AddValue("iv", "4-haeuser")); - - var context = new ScriptContext { Data = content }; + var content = new NamedContentData(); + var context = new ScriptContext { Data = content, Operation = "MyOperation" }; const string script = @" var data = ctx.data; - data.slug = { iv: slugify(data.title.iv) }; + getJSON('http://squidex.io', function(result) { + data.operation = { iv: result.key }; + + replace(data); + }); - replace(data); "; - var result = sut.Transform(context, script); + var result = await sut.ExecuteAndTransformAsync(context, script); - Assert.Equal(expected, result); + Assert.Empty( result); } [Fact] - public void Should_slugify_value_with_single_char() + public async Task ExecuteAndTransformAsync_should_timeout_when_replace_never_called() { - var content = - new NamedContentData() - .AddField("title", - new ContentFieldData() - .AddValue("iv", "4 Häuser")); - - var expected = - new NamedContentData() - .AddField("title", - new ContentFieldData() - .AddValue("iv", "4 Häuser")) - .AddField("slug", - new ContentFieldData() - .AddValue("iv", "4-hauser")); - - var context = new ScriptContext { Data = content }; + var content = new NamedContentData(); + var context = new ScriptContext { Data = content, Operation = "MyOperation" }; const string script = @" + async = true; + var data = ctx.data; - data.slug = { iv: slugify(data.title.iv, true) }; + getJSON('http://squidex.io', function(result) { + data.operation = { iv: result.key }; + }); - replace(data); "; - var result = sut.Transform(context, script); - - Assert.Equal(expected, result); + await Assert.ThrowsAnyAsync(() => sut.ExecuteAndTransformAsync(context, script)); } [Fact] - public void Should_transform_content_and_return_with_execute_transform() + public async Task ExecuteAndTransformAsync_should_transform_content_and_return_with_execute_transform() { var content = new NamedContentData() @@ -320,13 +318,13 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting replace(data); "; - var result = sut.ExecuteAndTransform(context, script); + var result = await sut.ExecuteAndTransformAsync(context, script); Assert.Equal(expected, result); } [Fact] - public void Should_transform_content_with_old_content() + public async Task ExecuteAndTransformAsync_should_transform_content_with_old_content() { var content = new NamedContentData() @@ -359,155 +357,77 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting replace(ctx.data); "; - var result = sut.ExecuteAndTransform(context, script); + var result = await sut.ExecuteAndTransformAsync(context, script); Assert.Equal(expected, result); } [Fact] - public void Should_evaluate_to_true_when_expression_match() + public void Evaluate_should_return_true_when_expression_match() { const string script = @" value.i == 2 "; - var result = sut.Evaluate("value", new { i = 2 }, script); + var context = new ScriptContext + { + ["value"] = new { i = 2 } + }; + + var result = sut.Evaluate(context, script); Assert.True(result); } [Fact] - public void Should_evaluate_to_true_when_status_match() + public void Evaluate_should_return_true_when_status_match() { const string script = @" value.status == 'Published' "; - var result = sut.Evaluate("value", new { status = Status.Published }, script); + var context = new ScriptContext + { + ["value"] = new { status = Status.Published } + }; + + var result = sut.Evaluate(context, script); Assert.True(result); } [Fact] - public void Should_evaluate_to_false_when_expression_match() + public void Evaluate_should_return_false_when_expression_match() { const string script = @" value.i == 3 "; - var result = sut.Evaluate("value", new { i = 2 }, script); - - Assert.False(result); - } - - [Fact] - public void Should_evaluate_to_false_when_script_is_invalid() - { - const string script = @" - function(); - "; - - var result = sut.Evaluate("value", new { i = 2 }, script); - - Assert.False(result); - } - - [Fact] - public async Task Should_make_json_request() - { - var httpResponse = new HttpResponseMessage(HttpStatusCode.OK) + var context = new ScriptContext { - Content = new StringContent("{ \"key\": 42 }") + ["value"] = new { i = 2 } }; - var httpHandler = new MockupHander(httpResponse); - - A.CallTo(() => httpClientFactory.CreateClient(A._)) - .Returns(new HttpClient(httpHandler)); - - const string script = @" - getJSON('http://squidex.io', function(result) { - complete(result); - }); - "; - - var result = await sut.GetAsync(new ScriptContext(), script); - - httpHandler.ShouldBeMethod(HttpMethod.Get); - httpHandler.ShouldBeUrl("http://squidex.io/"); - - var expectedResult = JsonValue.Object().Add("key", 42); + var result = sut.Evaluate(context, script); - Assert.Equal(expectedResult, result); + Assert.False(result); } [Fact] - public async Task Should_make_json_request_with_headers() + public void Evaluate_should_return_false_when_script_is_invalid() { - var httpResponse = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent("{ \"key\": 42 }") - }; - - var httpHandler = new MockupHander(httpResponse); - - A.CallTo(() => httpClientFactory.CreateClient(A._)) - .Returns(new HttpClient(httpHandler)); - const string script = @" - var headers = { - 'X-Header1': 1, - 'X-Header2': '2' - }; - - getJSON('http://squidex.io', function(result) { - complete(result); - }, headers); + function(); "; - var result = await sut.GetAsync(new ScriptContext(), script); - - httpHandler.ShouldBeMethod(HttpMethod.Get); - httpHandler.ShouldBeUrl("http://squidex.io/"); - httpHandler.ShouldBeHeader("X-Header1", "1"); - httpHandler.ShouldBeHeader("X-Header2", "2"); - - var expectedResult = JsonValue.Object().Add("key", 42); - - Assert.Equal(expectedResult, result); - } - - private sealed class MockupHander : HttpMessageHandler - { - private readonly HttpResponseMessage response; - private HttpRequestMessage madeRequest; - - public void ShouldBeMethod(HttpMethod method) - { - Assert.Equal(method, madeRequest.Method); - } - - public void ShouldBeUrl(string url) - { - Assert.Equal(url, madeRequest.RequestUri.ToString()); - } - - public void ShouldBeHeader(string key, string value) + var context = new ScriptContext { - Assert.Equal(value, madeRequest.Headers.GetValues(key).FirstOrDefault()); - } + ["value"] = new { i = 2 } + }; - public MockupHander(HttpResponseMessage response) - { - this.response = response; - } + var result = sut.Evaluate(context, script); - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - madeRequest = request; - - return Task.FromResult(response); - } + Assert.False(result); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/MockupHttpHandler.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/MockupHttpHandler.cs new file mode 100644 index 000000000..a282c9ef8 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/MockupHttpHandler.cs @@ -0,0 +1,50 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Operations.Scripting +{ + internal sealed class MockupHttpHandler : HttpMessageHandler + { + private readonly HttpResponseMessage response; + private HttpRequestMessage madeRequest; + + public void ShouldBeMethod(HttpMethod method) + { + Assert.Equal(method, madeRequest.Method); + } + + public void ShouldBeUrl(string url) + { + Assert.Equal(url, madeRequest.RequestUri.ToString()); + } + + public void ShouldBeHeader(string key, string value) + { + Assert.Equal(value, madeRequest.Headers.GetValues(key).FirstOrDefault()); + } + + public MockupHttpHandler(HttpResponseMessage response) + { + this.response = response; + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + await Task.Delay(1000); + + madeRequest = request; + + return response; + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj b/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj index 710ad4f66..628e30aab 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj @@ -14,6 +14,7 @@ + diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs index c981168f9..38b9e0347 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs @@ -32,10 +32,10 @@ namespace Squidex.Domain.Apps.Entities.Assets public AssetChangedTriggerHandlerTests() { - A.CallTo(() => scriptEngine.Evaluate("event", A._, "true")) + A.CallTo(() => scriptEngine.Evaluate(A._, "true")) .Returns(true); - A.CallTo(() => scriptEngine.Evaluate("event", A._, "false")) + A.CallTo(() => scriptEngine.Evaluate(A._, "false")) .Returns(false); sut = new AssetChangedTriggerHandler(scriptEngine, assetLoader); @@ -149,12 +149,12 @@ namespace Squidex.Domain.Apps.Entities.Assets if (string.IsNullOrWhiteSpace(condition)) { - A.CallTo(() => scriptEngine.Evaluate("event", A._, condition)) + A.CallTo(() => scriptEngine.Evaluate(A._, condition)) .MustNotHaveHappened(); } else { - A.CallTo(() => scriptEngine.Evaluate("event", A._, condition)) + A.CallTo(() => scriptEngine.Evaluate(A._, condition)) .MustHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentTriggerHandlerTests.cs index 1b539a7eb..f7947ce44 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentTriggerHandlerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentTriggerHandlerTests.cs @@ -10,6 +10,8 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using FakeItEasy; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules.Triggers; @@ -31,10 +33,10 @@ namespace Squidex.Domain.Apps.Entities.Comments public CommentTriggerHandlerTests() { - A.CallTo(() => scriptEngine.Evaluate("event", A._, "true")) + A.CallTo(() => scriptEngine.Evaluate(A._, "true")) .Returns(true); - A.CallTo(() => scriptEngine.Evaluate("event", A._, "false")) + A.CallTo(() => scriptEngine.Evaluate(A._, "false")) .Returns(false); sut = new CommentTriggerHandler(scriptEngine, userResolver); @@ -265,27 +267,35 @@ namespace Squidex.Domain.Apps.Entities.Comments private void TestForRealCondition(string condition, Action action) { - var trigger = new CommentTrigger { Condition = condition }; + var trigger = new CommentTrigger + { + Condition = condition + }; + + var memoryCache = new MemoryCache(Options.Create(new MemoryCacheOptions())); - var handler = new CommentTriggerHandler(new JintScriptEngine(null), userResolver); + var handler = new CommentTriggerHandler(new JintScriptEngine(memoryCache), userResolver); action(handler, trigger); } private void TestForCondition(string condition, Action action) { - var trigger = new CommentTrigger { Condition = condition }; + var trigger = new CommentTrigger + { + Condition = condition + }; action(trigger); if (string.IsNullOrWhiteSpace(condition)) { - A.CallTo(() => scriptEngine.Evaluate("event", A._, condition)) + A.CallTo(() => scriptEngine.Evaluate(A._, condition)) .MustNotHaveHappened(); } else { - A.CallTo(() => scriptEngine.Evaluate("event", A._, condition)) + A.CallTo(() => scriptEngine.Evaluate(A._, condition)) .MustHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs index ace58b350..29a41aaa0 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs @@ -39,10 +39,10 @@ namespace Squidex.Domain.Apps.Entities.Contents public ContentChangedTriggerHandlerTests() { - A.CallTo(() => scriptEngine.Evaluate("event", A._, "true")) + A.CallTo(() => scriptEngine.Evaluate(A._, "true")) .Returns(true); - A.CallTo(() => scriptEngine.Evaluate("event", A._, "false")) + A.CallTo(() => scriptEngine.Evaluate(A._, "false")) .Returns(false); sut = new ContentChangedTriggerHandler(scriptEngine, contentLoader); @@ -249,12 +249,12 @@ namespace Squidex.Domain.Apps.Entities.Contents if (string.IsNullOrWhiteSpace(condition)) { - A.CallTo(() => scriptEngine.Evaluate("event", A._, A._)) + A.CallTo(() => scriptEngine.Evaluate(A._, A._)) .MustNotHaveHappened(); } else { - A.CallTo(() => scriptEngine.Evaluate("event", A._, condition)) + A.CallTo(() => scriptEngine.Evaluate(A._, condition)) .MustHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs index e2960b9a6..0af098ed6 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs @@ -101,8 +101,8 @@ namespace Squidex.Domain.Apps.Entities.Contents A.CallTo(() => appProvider.GetAppWithSchemaAsync(AppId, SchemaId)) .Returns((app, schema)); - A.CallTo(() => scriptEngine.ExecuteAndTransform(A._, A._)) - .ReturnsLazily(x => x.GetArgument(0)!.Data!); + A.CallTo(() => scriptEngine.ExecuteAndTransformAsync(A._, A._)) + .ReturnsLazily(x => Task.FromResult(x.GetArgument(0)!.Data!)); patched = patch.MergeInto(data); @@ -136,9 +136,9 @@ namespace Squidex.Domain.Apps.Entities.Contents CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft }) ); - A.CallTo(() => scriptEngine.ExecuteAndTransform(ScriptContext(data, null, Status.Draft), "")) + A.CallTo(() => scriptEngine.ExecuteAndTransformAsync(ScriptContext(data, null, Status.Draft), "")) .MustHaveHappened(); - A.CallTo(() => scriptEngine.Execute(A._, "")) + A.CallTo(() => scriptEngine.ExecuteAsync(A._, "")) .MustNotHaveHappened(); } @@ -159,9 +159,9 @@ namespace Squidex.Domain.Apps.Entities.Contents CreateContentEvent(new ContentStatusChanged { Status = Status.Published, Change = StatusChange.Published }) ); - A.CallTo(() => scriptEngine.ExecuteAndTransform(ScriptContext(data, null, Status.Draft), "")) + A.CallTo(() => scriptEngine.ExecuteAndTransformAsync(ScriptContext(data, null, Status.Draft), "")) .MustHaveHappened(); - A.CallTo(() => scriptEngine.Execute(ScriptContext(data, null, Status.Published), "")) + A.CallTo(() => scriptEngine.ExecuteAsync(ScriptContext(data, null, Status.Published), "")) .MustHaveHappened(); } @@ -191,7 +191,7 @@ namespace Squidex.Domain.Apps.Entities.Contents CreateContentEvent(new ContentUpdated { Data = otherData }) ); - A.CallTo(() => scriptEngine.ExecuteAndTransform(ScriptContext(otherData, data, Status.Draft), "")) + A.CallTo(() => scriptEngine.ExecuteAndTransformAsync(ScriptContext(otherData, data, Status.Draft), "")) .MustHaveHappened(); } @@ -215,7 +215,7 @@ namespace Squidex.Domain.Apps.Entities.Contents CreateContentEvent(new ContentUpdated { Data = otherData }) ); - A.CallTo(() => scriptEngine.ExecuteAndTransform(ScriptContext(otherData, data, Status.Draft), "")) + A.CallTo(() => scriptEngine.ExecuteAndTransformAsync(ScriptContext(otherData, data, Status.Draft), "")) .MustHaveHappened(); } @@ -232,7 +232,7 @@ namespace Squidex.Domain.Apps.Entities.Contents Assert.Single(LastEvents); - A.CallTo(() => scriptEngine.ExecuteAndTransform(A._, "")) + A.CallTo(() => scriptEngine.ExecuteAndTransformAsync(A._, "")) .MustNotHaveHappened(); } @@ -264,7 +264,7 @@ namespace Squidex.Domain.Apps.Entities.Contents CreateContentEvent(new ContentUpdated { Data = patched }) ); - A.CallTo(() => scriptEngine.ExecuteAndTransform(ScriptContext(patched, data, Status.Draft), "")) + A.CallTo(() => scriptEngine.ExecuteAndTransformAsync(ScriptContext(patched, data, Status.Draft), "")) .MustHaveHappened(); } @@ -288,7 +288,7 @@ namespace Squidex.Domain.Apps.Entities.Contents CreateContentEvent(new ContentUpdated { Data = patched }) ); - A.CallTo(() => scriptEngine.ExecuteAndTransform(ScriptContext(patched, data, Status.Draft), "")) + A.CallTo(() => scriptEngine.ExecuteAndTransformAsync(ScriptContext(patched, data, Status.Draft), "")) .MustHaveHappened(); } @@ -305,7 +305,7 @@ namespace Squidex.Domain.Apps.Entities.Contents Assert.Single(LastEvents); - A.CallTo(() => scriptEngine.ExecuteAndTransform(A._, "")) + A.CallTo(() => scriptEngine.ExecuteAndTransformAsync(A._, "")) .MustNotHaveHappened(); } @@ -327,7 +327,7 @@ namespace Squidex.Domain.Apps.Entities.Contents CreateContentEvent(new ContentStatusChanged { Status = Status.Published, Change = StatusChange.Published }) ); - A.CallTo(() => scriptEngine.Execute(ScriptContext(data, null, Status.Published, Status.Draft), "")) + A.CallTo(() => scriptEngine.ExecuteAsync(ScriptContext(data, null, Status.Published, Status.Draft), "")) .MustHaveHappened(); } @@ -349,7 +349,7 @@ namespace Squidex.Domain.Apps.Entities.Contents CreateContentEvent(new ContentStatusChanged { Status = Status.Archived }) ); - A.CallTo(() => scriptEngine.Execute(ScriptContext(data, null, Status.Archived, Status.Draft), "")) + A.CallTo(() => scriptEngine.ExecuteAsync(ScriptContext(data, null, Status.Archived, Status.Draft), "")) .MustHaveHappened(); } @@ -372,7 +372,7 @@ namespace Squidex.Domain.Apps.Entities.Contents CreateContentEvent(new ContentStatusChanged { Status = Status.Draft, Change = StatusChange.Unpublished }) ); - A.CallTo(() => scriptEngine.Execute(ScriptContext(data, null, Status.Draft, Status.Published), "")) + A.CallTo(() => scriptEngine.ExecuteAsync(ScriptContext(data, null, Status.Draft, Status.Published), "")) .MustHaveHappened(); } @@ -396,7 +396,7 @@ namespace Squidex.Domain.Apps.Entities.Contents CreateContentEvent(new ContentStatusChanged { Change = StatusChange.Change, Status = Status.Archived }) ); - A.CallTo(() => scriptEngine.Execute(ScriptContext(data, null, Status.Archived, Status.Draft), "")) + A.CallTo(() => scriptEngine.ExecuteAsync(ScriptContext(data, null, Status.Archived, Status.Draft), "")) .MustHaveHappened(); } @@ -423,7 +423,7 @@ namespace Squidex.Domain.Apps.Entities.Contents CreateContentEvent(new ContentStatusScheduled { Status = Status.Published, DueTime = dueTime }) ); - A.CallTo(() => scriptEngine.Execute(A._, "")) + A.CallTo(() => scriptEngine.ExecuteAsync(A._, "")) .MustNotHaveHappened(); } @@ -451,7 +451,7 @@ namespace Squidex.Domain.Apps.Entities.Contents CreateContentEvent(new ContentStatusChanged { Status = Status.Archived }) ); - A.CallTo(() => scriptEngine.Execute(A._, "")) + A.CallTo(() => scriptEngine.ExecuteAsync(A._, "")) .MustHaveHappened(); } @@ -479,7 +479,7 @@ namespace Squidex.Domain.Apps.Entities.Contents CreateContentEvent(new ContentSchedulingCancelled()) ); - A.CallTo(() => scriptEngine.Execute(A._, "")) + A.CallTo(() => scriptEngine.ExecuteAsync(A._, "")) .MustNotHaveHappened(); } @@ -501,7 +501,7 @@ namespace Squidex.Domain.Apps.Entities.Contents CreateContentEvent(new ContentDeleted()) ); - A.CallTo(() => scriptEngine.Execute(ScriptContext(data, null, Status.Draft), "")) + A.CallTo(() => scriptEngine.ExecuteAsync(ScriptContext(data, null, Status.Draft), "")) .MustHaveHappened(); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs index f97a20bc3..0f89f5878 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs @@ -10,6 +10,8 @@ using System.Collections.Generic; using System.Threading.Tasks; using FakeItEasy; using FluentAssertions; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Entities.Apps; @@ -90,7 +92,9 @@ namespace Squidex.Domain.Apps.Entities.Contents A.CallTo(() => app.Workflows) .Returns(workflows); - sut = new DynamicContentWorkflow(new JintScriptEngine(null), appProvider); + var memoryCache = new MemoryCache(Options.Create(new MemoryCacheOptions())); + + sut = new DynamicContentWorkflow(new JintScriptEngine(memoryCache), appProvider); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ScriptContentTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ScriptContentTests.cs index 879e916c5..4884dab98 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ScriptContentTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ScriptContentTests.cs @@ -67,7 +67,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries await sut.EnrichAsync(ctx, new[] { content }, schemaProvider); - A.CallTo(() => scriptEngine.ExecuteAndTransform(A._, A._)) + A.CallTo(() => scriptEngine.ExecuteAndTransformAsync(A._, A._)) .MustNotHaveHappened(); } @@ -80,7 +80,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries await sut.EnrichAsync(ctx, new[] { content }, schemaProvider); - A.CallTo(() => scriptEngine.ExecuteAndTransform(A._, A._)) + A.CallTo(() => scriptEngine.ExecuteAndTransformAsync(A._, A._)) .MustNotHaveHappened(); } @@ -93,14 +93,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries var content = new ContentEntity { SchemaId = schemaWithScriptId, Data = oldData }; - A.CallTo(() => scriptEngine.Transform(A._, "my-query")) + A.CallTo(() => scriptEngine.TransformAsync(A._, "my-query")) .Returns(new NamedContentData()); await sut.EnrichAsync(ctx, new[] { content }, schemaProvider); Assert.NotSame(oldData, content.Data); - A.CallTo(() => scriptEngine.Transform( + A.CallTo(() => scriptEngine.TransformAsync( A.That.Matches(x => ReferenceEquals(x.User, ctx.User) && ReferenceEquals(x.Data, oldData) && diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaChangedTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaChangedTriggerHandlerTests.cs index 7feeba107..86019758d 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaChangedTriggerHandlerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaChangedTriggerHandlerTests.cs @@ -31,10 +31,10 @@ namespace Squidex.Domain.Apps.Entities.Schemas public SchemaChangedTriggerHandlerTests() { - A.CallTo(() => scriptEngine.Evaluate("event", A._, "true")) + A.CallTo(() => scriptEngine.Evaluate(A._, "true")) .Returns(true); - A.CallTo(() => scriptEngine.Evaluate("event", A._, "false")) + A.CallTo(() => scriptEngine.Evaluate(A._, "false")) .Returns(false); sut = new SchemaChangedTriggerHandler(scriptEngine); @@ -136,12 +136,12 @@ namespace Squidex.Domain.Apps.Entities.Schemas if (string.IsNullOrWhiteSpace(condition)) { - A.CallTo(() => scriptEngine.Evaluate("event", A._, condition)) + A.CallTo(() => scriptEngine.Evaluate(A._, condition)) .MustNotHaveHappened(); } else { - A.CallTo(() => scriptEngine.Evaluate("event", A._, condition)) + A.CallTo(() => scriptEngine.Evaluate(A._, condition)) .MustHaveHappened(); } }