diff --git a/NuGet.Config b/NuGet.Config
index 3f0e00340..b49f363c2 100644
--- a/NuGet.Config
+++ b/NuGet.Config
@@ -1,6 +1,7 @@
+
\ No newline at end of file
diff --git a/extensions/Squidex.Extensions/Squidex.Extensions.csproj b/extensions/Squidex.Extensions/Squidex.Extensions.csproj
index 50361886d..8ec17c724 100644
--- a/extensions/Squidex.Extensions/Squidex.Extensions.csproj
+++ b/extensions/Squidex.Extensions/Squidex.Extensions.csproj
@@ -10,13 +10,13 @@
-
+
-
+
-
+
-
+
diff --git a/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj b/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj
index 366672e5b..5c0960c6c 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj
+++ b/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj
@@ -8,7 +8,7 @@
True
-
+
all
runtime; build; native; contentfiles; analyzers
diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs
index 8f3bb8919..c01feff2e 100644
--- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs
+++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs
@@ -12,6 +12,7 @@ using System.Text;
using System.Text.RegularExpressions;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
+using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Json.Objects;
@@ -21,7 +22,9 @@ namespace Squidex.Domain.Apps.Core.HandleRules
{
public class RuleEventFormatter
{
- private const string Undefined = "UNDEFINED";
+ private const string Undefined = "null";
+ private const string ScriptSuffix = ")";
+ private const string ScriptPrefix = "Script(";
private static readonly char[] ContentPlaceholderStartOld = "CONTENT_DATA".ToCharArray();
private static readonly char[] ContentPlaceholderStartNew = "{CONTENT_DATA".ToCharArray();
private static readonly Regex ContentDataPlaceholderOld = new Regex(@"^CONTENT_DATA(\.([0-9A-Za-z\-_]*)){2,}", RegexOptions.Compiled);
@@ -29,13 +32,16 @@ namespace Squidex.Domain.Apps.Core.HandleRules
private readonly List<(char[] Pattern, Func Replacer)> patterns = new List<(char[] Pattern, Func Replacer)>();
private readonly IJsonSerializer jsonSerializer;
private readonly IRuleUrlGenerator urlGenerator;
+ private readonly IScriptEngine scriptEngine;
- public RuleEventFormatter(IJsonSerializer jsonSerializer, IRuleUrlGenerator urlGenerator)
+ public RuleEventFormatter(IJsonSerializer jsonSerializer, IRuleUrlGenerator urlGenerator, IScriptEngine scriptEngine)
{
Guard.NotNull(jsonSerializer, nameof(jsonSerializer));
+ Guard.NotNull(scriptEngine, nameof(scriptEngine));
Guard.NotNull(urlGenerator, nameof(urlGenerator));
this.jsonSerializer = jsonSerializer;
+ this.scriptEngine = scriptEngine;
this.urlGenerator = urlGenerator;
AddPattern("APP_ID", AppId);
@@ -72,6 +78,18 @@ namespace Squidex.Domain.Apps.Core.HandleRules
return text;
}
+ if (text.StartsWith(ScriptPrefix, StringComparison.OrdinalIgnoreCase) && text.EndsWith(ScriptSuffix, StringComparison.OrdinalIgnoreCase))
+ {
+ var script = text.Substring(ScriptPrefix.Length, text.Length - ScriptPrefix.Length - ScriptSuffix.Length);
+
+ var customFunctions = new Dictionary>
+ {
+ ["contentUrl"] = () => ContentUrl(@event)
+ };
+
+ return scriptEngine.Interpolate("event", @event, script, customFunctions);
+ }
+
var current = text.AsSpan();
var sb = new StringBuilder();
@@ -186,7 +204,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
{
if (@event is EnrichedContentEvent contentEvent)
{
- return contentEvent.Type.ToString().ToLowerInvariant();
+ return contentEvent.Type.ToString();
}
return Undefined;
diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs
index 6a72f9508..e75e1f0b8 100644
--- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs
+++ b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs
@@ -21,7 +21,7 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
{
private readonly NamedContentData contentData;
private HashSet fieldsToDelete;
- private Dictionary fieldProperties;
+ private Dictionary fieldProperties;
private bool isChanged;
public ContentDataObject(Engine engine, NamedContentData contentData)
@@ -55,7 +55,9 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
{
foreach (var kvp in fieldProperties)
{
- if (kvp.Value.ContentField.TryUpdate(out var fieldData))
+ var value = (ContentDataProperty)kvp.Value;
+
+ if (value.ContentField.TryUpdate(out var fieldData))
{
contentData[kvp.Key] = fieldData;
}
@@ -109,17 +111,14 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
{
EnsurePropertiesInitialized();
- foreach (var property in fieldProperties)
- {
- yield return new KeyValuePair(property.Key, property.Value);
- }
+ return fieldProperties;
}
private void EnsurePropertiesInitialized()
{
if (fieldProperties == null)
{
- fieldProperties = new Dictionary(contentData.Count);
+ fieldProperties = new Dictionary(contentData.Count);
foreach (var kvp in contentData)
{
diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs
index 4ea31b9ea..b9a51cfbf 100644
--- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs
+++ b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs
@@ -7,18 +7,17 @@
using Jint.Native;
using Jint.Runtime;
-using Jint.Runtime.Descriptors;
using Squidex.Domain.Apps.Core.Contents;
namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
{
- public sealed class ContentDataProperty : PropertyDescriptor
+ public sealed class ContentDataProperty : CustomProperty
{
private readonly ContentDataObject contentData;
private ContentFieldObject contentField;
private JsValue value;
- public override JsValue Value
+ protected override JsValue CustomValue
{
get
{
@@ -42,7 +41,7 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
contentField.Put(kvp.Key, kvp.Value.Value, true);
}
- this.value = new JsValue(contentField);
+ this.value = contentField;
}
}
}
@@ -53,14 +52,13 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
}
public ContentDataProperty(ContentDataObject contentData, ContentFieldObject contentField = null)
- : base(null, true, true, true)
{
this.contentData = contentData;
this.contentField = contentField;
if (contentField != null)
{
- value = new JsValue(contentField);
+ value = contentField;
}
}
}
diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs
index f92963096..d6ef06266 100644
--- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs
+++ b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs
@@ -20,7 +20,7 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
private readonly ContentDataObject contentData;
private readonly ContentFieldData fieldData;
private HashSet valuesToDelete;
- private Dictionary valueProperties;
+ private Dictionary valueProperties;
private bool isChanged;
public ContentFieldData FieldData
@@ -67,9 +67,11 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
{
foreach (var kvp in valueProperties)
{
- if (kvp.Value.IsChanged)
+ var value = (ContentFieldProperty)kvp.Value;
+
+ if (value.IsChanged)
{
- fieldData[kvp.Key] = kvp.Value.ContentValue;
+ fieldData[kvp.Key] = value.ContentValue;
}
}
}
@@ -114,17 +116,14 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
{
EnsurePropertiesInitialized();
- foreach (var property in valueProperties)
- {
- yield return new KeyValuePair(property.Key, property.Value);
- }
+ return valueProperties;
}
private void EnsurePropertiesInitialized()
{
if (valueProperties == null)
{
- valueProperties = new Dictionary(FieldData.Count);
+ valueProperties = new Dictionary(FieldData.Count);
foreach (var kvp in FieldData)
{
diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs
index ee279b40a..ed5aa34d2 100644
--- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs
+++ b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs
@@ -6,19 +6,18 @@
// ==========================================================================
using Jint.Native;
-using Jint.Runtime.Descriptors;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
{
- public sealed class ContentFieldProperty : PropertyDescriptor
+ public sealed class ContentFieldProperty : CustomProperty
{
private readonly ContentFieldObject contentField;
private IJsonValue contentValue;
private JsValue value;
private bool isChanged;
- public override JsValue Value
+ protected override JsValue CustomValue
{
get
{
@@ -49,7 +48,6 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
}
public ContentFieldProperty(ContentFieldObject contentField, IJsonValue contentValue = null)
- : base(null, true, true, true)
{
this.contentField = contentField;
this.contentValue = contentValue;
diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/CustomProperty.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/CustomProperty.cs
new file mode 100644
index 000000000..bd1230537
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/CustomProperty.cs
@@ -0,0 +1,24 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using Jint.Runtime.Descriptors;
+
+namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
+{
+ public abstract class CustomProperty : PropertyDescriptor
+ {
+ public CustomProperty()
+ : base(PropertyFlag.CustomJsValue)
+ {
+ Enumerable = true;
+
+ Writable = true;
+
+ Configurable = true;
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs
index ec9d52054..33e5fd891 100644
--- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs
+++ b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs
@@ -27,11 +27,11 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
case JsonNull n:
return JsValue.Null;
case JsonScalar s:
- return new JsValue(s.Value);
+ return new JsString(s.Value);
case JsonScalar b:
- return new JsValue(b.Value);
+ return new JsBoolean(b.Value);
case JsonScalar b:
- return new JsValue(b.Value);
+ return new JsNumber(b.Value);
case JsonObject obj:
return FromObject(obj, engine);
case JsonArray arr:
diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs
index 265ac898e..55742c99d 100644
--- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs
+++ b/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs
@@ -5,6 +5,8 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
+using System;
+using System.Collections.Generic;
using Squidex.Domain.Apps.Core.Contents;
namespace Squidex.Domain.Apps.Core.Scripting
@@ -19,6 +21,6 @@ namespace Squidex.Domain.Apps.Core.Scripting
bool Evaluate(string name, object context, string script);
- string Interpolate(string name, object context, string script);
+ string Interpolate(string name, object context, string script, Dictionary> customFormatters = null);
}
}
diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs
index a1efe015f..35cc085ac 100644
--- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs
+++ b/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs
@@ -7,17 +7,19 @@
using System;
using System.Collections.Generic;
-using System.Reflection;
+using System.Globalization;
using System.Security.Claims;
using Jint;
using Jint.Native;
+using Jint.Native.Date;
using Jint.Native.Object;
-using Jint.Parser;
using Jint.Runtime;
using Jint.Runtime.Interop;
+using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Scripting.ContentWrapper;
using Squidex.Infrastructure;
+using Squidex.Shared.Users;
namespace Squidex.Domain.Apps.Core.Scripting
{
@@ -25,12 +27,38 @@ namespace Squidex.Domain.Apps.Core.Scripting
{
public TimeSpan Timeout { get; set; } = TimeSpan.FromMilliseconds(200);
- static JintScriptEngine()
+ public sealed class Converter : IObjectConverter
{
- var typeMappers = (Dictionary>)typeof(Engine).GetField("TypeMappers", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null);
+ public Engine Engine { get; set; }
- typeMappers.Add(typeof(NamedContentData), (engine, data) => new ContentDataObject(engine, (NamedContentData)data));
- typeMappers.Add(typeof(ClaimsPrincipal), (engine, data) => JintUser.Create(engine, (ClaimsPrincipal)data));
+ public bool TryConvert(object value, out JsValue result)
+ {
+ result = null;
+
+ if (value is Enum)
+ {
+ result = value.ToString();
+ return true;
+ }
+
+ switch (value)
+ {
+ case IUser user:
+ result = JintUser.Create(Engine, user);
+ return true;
+ case ClaimsPrincipal principal:
+ result = JintUser.Create(Engine, principal);
+ return true;
+ case Instant instant:
+ result = JsValue.FromObject(Engine, instant.ToDateTimeUtc());
+ return true;
+ case NamedContentData content:
+ result = new ContentDataObject(Engine, content);
+ return true;
+ }
+
+ return false;
+ }
}
public void Execute(ScriptContext context, string script)
@@ -126,7 +154,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
{
engine.Execute(script);
}
- catch (ParserException ex)
+ catch (ArgumentException ex)
{
throw new ValidationException($"Failed to execute script with javascript syntax error: {ex.Message}", new ValidationError(ex.Message));
}
@@ -138,7 +166,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
private Engine CreateScriptEngine(ScriptContext context)
{
- var engine = new Engine(options => options.TimeoutInterval(Timeout).Strict());
+ var engine = CreateScriptEngine();
var contextInstance = new ObjectInstance(engine);
@@ -164,11 +192,56 @@ namespace Squidex.Domain.Apps.Core.Scripting
engine.SetValue("ctx", contextInstance);
engine.SetValue("context", contextInstance);
- engine.SetValue("slugify", new ClrFunctionInstance(engine, Slugify));
return engine;
}
+ private Engine CreateScriptEngine(IReferenceResolver resolver = null, Dictionary> customFormatters = null)
+ {
+ var converter = new Converter();
+
+ var engine = new Engine(options =>
+ {
+ if (resolver != null)
+ {
+ options.SetReferencesResolver(resolver);
+ }
+
+ options.TimeoutInterval(Timeout).Strict().AddObjectConverter(converter);
+ });
+
+ if (customFormatters != null)
+ {
+ foreach (var kvp in customFormatters)
+ {
+ engine.SetValue(kvp.Key, Safe(kvp.Value));
+ }
+ }
+
+ converter.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;
+ }
+
+ private static Func Safe(Func func)
+ {
+ return new Func(() =>
+ {
+ try
+ {
+ return func();
+ }
+ catch
+ {
+ return "null";
+ }
+ });
+ }
+
private static JsValue Slugify(JsValue thisObject, JsValue[] arguments)
{
try
@@ -189,6 +262,21 @@ namespace Squidex.Domain.Apps.Core.Scripting
}
}
+ 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;
+ }
+ }
+
private static void EnableDisallow(Engine engine)
{
engine.SetValue("disallow", new Action(message =>
@@ -214,7 +302,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
try
{
var result =
- new Engine(options => options.TimeoutInterval(Timeout).Strict())
+ CreateScriptEngine(NullPropagation.Instance)
.SetValue(name, context)
.Execute(script)
.GetCompletionValue()
@@ -228,22 +316,24 @@ namespace Squidex.Domain.Apps.Core.Scripting
}
}
- public string Interpolate(string name, object context, string script)
+ public string Interpolate(string name, object context, string script, Dictionary> customFormatters = null)
{
try
{
var result =
- new Engine(options => options.TimeoutInterval(Timeout).Strict())
+ CreateScriptEngine(NullPropagation.Instance, customFormatters)
.SetValue(name, context)
.Execute(script)
.GetCompletionValue()
.ToObject();
- return (string)result;
+ var converted = result.ToString();
+
+ return converted == "undefined" ? "null" : converted;
}
- catch
+ catch (Exception ex)
{
- return string.Empty;
+ return ex.Message;
}
}
}
diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs
index f790c2739..083e028c1 100644
--- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs
+++ b/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs
@@ -5,11 +5,14 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
+using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using Jint;
using Jint.Runtime.Interop;
using Squidex.Infrastructure.Security;
+using Squidex.Shared.Identity;
+using Squidex.Shared.Users;
namespace Squidex.Domain.Apps.Core.Scripting
{
@@ -17,6 +20,11 @@ namespace Squidex.Domain.Apps.Core.Scripting
{
private static readonly char[] ClaimSeparators = { '/', '.', ':' };
+ public static ObjectWrapper Create(Engine engine, IUser user)
+ {
+ return CreateUser(engine, user.Id, false, user.Email, user.DisplayName(), user.Claims);
+ }
+
public static ObjectWrapper Create(Engine engine, ClaimsPrincipal principal)
{
var id = principal.OpenIdSubject();
@@ -28,19 +36,20 @@ namespace Squidex.Domain.Apps.Core.Scripting
id = principal.OpenIdClientId();
}
+ var name = principal.FindFirst(SquidexClaimTypes.DisplayName)?.Value;
+
+ return CreateUser(engine, id, isClient, principal.OpenIdEmail(), name, principal.Claims);
+ }
+
+ private static ObjectWrapper CreateUser(Engine engine, string id, bool isClient, string email, string name, IEnumerable allClaims)
+ {
var claims =
- principal.Claims.GroupBy(x => x.Type)
+ allClaims.GroupBy(x => x.Type)
.ToDictionary(
x => x.Key.Split(ClaimSeparators).Last(),
x => x.Select(y => y.Value).ToArray());
- return new ObjectWrapper(engine, new
- {
- Id = id,
- IsClient = isClient,
- Email = principal.OpenIdEmail(),
- Claims = claims,
- });
+ return new ObjectWrapper(engine, new { id, isClient, email, name, claims });
}
}
}
diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/NullPropagation.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/NullPropagation.cs
new file mode 100644
index 000000000..adc3d577b
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core.Operations/Scripting/NullPropagation.cs
@@ -0,0 +1,43 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using Jint;
+using Jint.Native;
+using Jint.Runtime.Interop;
+using Jint.Runtime.References;
+
+namespace Squidex.Domain.Apps.Core.Scripting
+{
+ public sealed class NullPropagation : IReferenceResolver
+ {
+ public static readonly NullPropagation Instance = new NullPropagation();
+
+ public bool TryUnresolvableReference(Engine engine, Reference reference, out JsValue value)
+ {
+ value = reference.GetBase();
+
+ return true;
+ }
+
+ public bool TryPropertyReference(Engine engine, Reference reference, ref JsValue value)
+ {
+ return value.IsNull() || value.IsUndefined();
+ }
+
+ public bool TryGetCallable(Engine engine, object reference, out JsValue value)
+ {
+ value = new ClrFunctionInstance(engine, "anonymous", (thisObj, values) => thisObj);
+
+ return true;
+ }
+
+ public bool CheckCoercible(JsValue value)
+ {
+ return true;
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj b/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
index 73e3a75a6..95b5cdc62 100644
--- a/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
+++ b/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
@@ -14,9 +14,9 @@
-
-
-
+
+
+
diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj b/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj
index 1b1b62890..23096e86d 100644
--- a/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj
+++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj
@@ -15,7 +15,7 @@
-
+
diff --git a/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj b/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj
index 227f17c93..58839d32f 100644
--- a/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj
+++ b/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj
@@ -15,12 +15,12 @@
-
+
all
runtime; build; native; contentfiles; analyzers
-
-
+
+
diff --git a/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj b/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj
index e679181fb..0e58689d7 100644
--- a/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj
+++ b/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj
@@ -12,7 +12,7 @@
-
+
diff --git a/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj b/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
index 42e0bc9be..2be4be747 100644
--- a/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
+++ b/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
@@ -11,15 +11,15 @@
-
-
+
+
all
runtime; build; native; contentfiles; analyzers
-
-
+
+
-
+
diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/AssetChangedRuleTriggerDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/AssetChangedRuleTriggerDto.cs
index 31e81f872..c0c7c8899 100644
--- a/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/AssetChangedRuleTriggerDto.cs
+++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/AssetChangedRuleTriggerDto.cs
@@ -14,24 +14,9 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers
public sealed class AssetChangedRuleTriggerDto : RuleTriggerDto
{
///
- /// Determines whether to handle the event when an asset is created.
+ /// Javascript condition when to trigger.
///
- public bool SendCreate { get; set; }
-
- ///
- /// Determines whether to handle the event when an asset is updated.
- ///
- public bool SendUpdate { get; set; }
-
- ///
- /// Determines whether to handle the event when an asset is renamed.
- ///
- public bool SendRename { get; set; }
-
- ///
- /// Determines whether to handle the event when an asset is deleted.
- ///
- public bool SendDelete { get; set; }
+ public string Condition { get; set; }
public override RuleTrigger ToTrigger()
{
diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerSchemaDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerSchemaDto.cs
index 9570a5182..93c05c61d 100644
--- a/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerSchemaDto.cs
+++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedRuleTriggerSchemaDto.cs
@@ -17,38 +17,8 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers
public Guid SchemaId { get; set; }
///
- /// Determines whether to handle the event when a content is created.
+ /// Javascript condition when to trigger.
///
- public bool SendCreate { get; set; }
-
- ///
- /// Determines whether to handle the event when a content is updated.
- ///
- public bool SendUpdate { get; set; }
-
- ///
- /// Determines whether to handle the event when a content is deleted.
- ///
- public bool SendDelete { get; set; }
-
- ///
- /// Determines whether to handle the event when a content is published.
- ///
- public bool SendPublish { get; set; }
-
- ///
- /// Determines whether to handle the event when a content is unpublished.
- ///
- public bool SendUnpublish { get; set; }
-
- ///
- /// Determines whether to handle the event when a content is archived.
- ///
- public bool SendArchived { get; set; }
-
- ///
- /// Determines whether to handle the event when a content is restored.
- ///
- public bool SendRestore { get; set; }
+ public string Condition { get; set; }
}
}
diff --git a/src/Squidex/Squidex.csproj b/src/Squidex/Squidex.csproj
index 750f8bdde..40fab103a 100644
--- a/src/Squidex/Squidex.csproj
+++ b/src/Squidex/Squidex.csproj
@@ -54,7 +54,7 @@
-
+
@@ -74,14 +74,14 @@
-
-
-
-
-
+
+
+
+
+
-
-
+
+
diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs
index 06226aa4d..64ca3d93f 100644
--- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs
+++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs
@@ -13,6 +13,7 @@ using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
+using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Shared.Identity;
@@ -38,7 +39,10 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
A.CallTo(() => user.Claims)
.Returns(new List { new Claim(SquidexClaimTypes.DisplayName, "me") });
- sut = new RuleEventFormatter(TestUtils.DefaultSerializer, urlGenerator);
+ A.CallTo(() => urlGenerator.GenerateContentUIUrl(appId, schemaId, contentId))
+ .Returns("content-url");
+
+ sut = new RuleEventFormatter(TestUtils.DefaultSerializer, urlGenerator, new JintScriptEngine());
}
[Fact]
@@ -69,91 +73,112 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
Assert.Contains("MyEventName", result);
}
- [Fact]
- public void Should_replace_app_information_from_event()
+ [Theory]
+ [InlineData("Name $APP_NAME has id $APP_ID")]
+ [InlineData("Script(`Name ${event.appId.name} has id ${event.appId.id}`)")]
+ public void Should_replace_app_information_from_event(string script)
{
var @event = new EnrichedContentEvent { AppId = appId };
- var result = sut.Format("Name $APP_NAME has id $APP_ID", @event);
+ var result = sut.Format(script, @event);
Assert.Equal($"Name my-app has id {appId.Id}", result);
}
- [Fact]
- public void Should_replace_schema_information_from_event()
+ [Theory]
+ [InlineData("Name $SCHEMA_NAME has id $SCHEMA_ID")]
+ [InlineData("Script(`Name ${event.schemaId.name} has id ${event.schemaId.id}`)")]
+ public void Should_replace_schema_information_from_event(string script)
{
var @event = new EnrichedContentEvent { SchemaId = schemaId };
- var result = sut.Format("Name $SCHEMA_NAME has id $SCHEMA_ID", @event);
+ var result = sut.Format(script, @event);
Assert.Equal($"Name my-schema has id {schemaId.Id}", result);
}
- [Fact]
- public void Should_replace_timestamp_information_from_event()
+ [Theory]
+ [InlineData("Date: $TIMESTAMP_DATE, Full: $TIMESTAMP_DATETIME")]
+ [InlineData("Script(`Date: ${formatDate(event.timestamp, 'yyyy-MM-dd')}, Full: ${formatDate(event.timestamp, 'yyyy-MM-dd-hh-mm-ss')}`)")]
+ public void Should_replace_timestamp_information_from_event(string script)
{
var now = DateTime.UtcNow;
var envelope = new EnrichedContentEvent { Timestamp = Instant.FromDateTimeUtc(now) };
- var result = sut.Format("Date: $TIMESTAMP_DATE, Full: $TIMESTAMP_DATETIME", envelope);
+ var result = sut.Format(script, envelope);
Assert.Equal($"Date: {now:yyyy-MM-dd}, Full: {now:yyyy-MM-dd-hh-mm-ss}", result);
}
- [Fact]
- public void Should_format_email_and_display_name_from_user()
+ [Theory]
+ [InlineData("From $USER_NAME ($USER_EMAIL)")]
+ [InlineData("Script(`From ${event.user.name} (${event.user.email})`)")]
+ public void Should_format_email_and_display_name_from_user(string script)
{
var @event = new EnrichedContentEvent { User = user, Actor = new RefToken(RefTokenType.Subject, "123") };
- var result = sut.Format("From $USER_NAME ($USER_EMAIL)", @event);
+ var result = sut.Format(script, @event);
Assert.Equal("From me (me@email.com)", result);
}
- [Fact]
- public void Should_return_undefined_if_user_is_not_found()
+ [Theory]
+ [InlineData("From $USER_NAME ($USER_EMAIL)")]
+ [InlineData("Script(`From ${event.user.name} (${event.user.email})`)")]
+ public void Should_return_null_if_user_is_not_found(string script)
{
var @event = new EnrichedContentEvent { Actor = new RefToken(RefTokenType.Subject, "123") };
- var result = sut.Format("From $USER_NAME ($USER_EMAIL)", @event);
+ var result = sut.Format(script, @event);
- Assert.Equal("From UNDEFINED (UNDEFINED)", result);
+ Assert.Equal("From null (null)", result);
}
- [Fact]
- public void Should_format_email_and_display_name_from_client()
+ [Theory]
+ [InlineData("From $USER_NAME ($USER_EMAIL)")]
+ [InlineData("Script(`From ${event.user.name} (${event.user.email})`)", Skip = "Not Supported")]
+ public void Should_return_null_if_user_is_not_found_with_scripting(string script)
{
- var @event = new EnrichedContentEvent { Actor = new RefToken(RefTokenType.Client, "android") };
+ var @event = new EnrichedContentEvent { Actor = new RefToken(RefTokenType.Subject, "123") };
- var result = sut.Format("From $USER_NAME ($USER_EMAIL)", @event);
+ var result = sut.Format(script, @event);
- Assert.Equal("From client:android (client:android)", result);
+ Assert.Equal("From null (null)", result);
}
- [Fact]
- public void Should_replace_content_url_from_event()
+ [Theory]
+ [InlineData("Go to $CONTENT_URL")]
+ [InlineData("Script(`Go to ${contentUrl()}`)")]
+ public void Should_replace_content_url_from_event(string script)
{
- var url = "http://content";
-
- A.CallTo(() => urlGenerator.GenerateContentUIUrl(appId, schemaId, contentId))
- .Returns(url);
-
var @event = new EnrichedContentEvent { AppId = appId, Id = contentId, SchemaId = schemaId };
- var result = sut.Format("Go to $CONTENT_URL", @event);
+ var result = sut.Format(script, @event);
- Assert.Equal($"Go to {url}", result);
+ Assert.Equal("Go to content-url", result);
}
- [Fact]
- public void Should_format_content_url_when_not_found()
+ [Theory]
+ [InlineData("Go to $CONTENT_URL")]
+ [InlineData("Script(`Go to ${contentUrl()}`)")]
+ public void Should_format_content_url_when_not_found(string script)
{
- Assert.Equal("UNDEFINED", sut.Format("$CONTENT_URL", new EnrichedAssetEvent()));
+ Assert.Equal("Go to null", sut.Format(script, new EnrichedAssetEvent()));
}
- [Fact]
- public void Should_return_undefined_when_field_not_found()
+ [Theory]
+ [InlineData("$CONTENT_ACTION")]
+ [InlineData("Script(`${event.type}`)")]
+ public void Should_format_content_actions_when_found(string script)
+ {
+ Assert.Equal("Created", sut.Format(script, new EnrichedContentEvent { Type = EnrichedContentEventType.Created }));
+ }
+
+ [Theory]
+ [InlineData("$CONTENT_DATA.country.iv")]
+ [InlineData("Script(`${event.data.country.iv}`)")]
+ public void Should_return_null_when_field_not_found(string script)
{
var @event = new EnrichedContentEvent
{
@@ -164,13 +189,15 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
.AddValue("iv", "Berlin"))
};
- var result = sut.Format("$CONTENT_DATA.country.iv", @event);
+ var result = sut.Format(script, @event);
- Assert.Equal("UNDEFINED", result);
+ Assert.Equal("null", result);
}
- [Fact]
- public void Should_return_undefined_when_partition_not_found()
+ [Theory]
+ [InlineData("$CONTENT_DATA.city.de")]
+ [InlineData("Script(`${event.data.country.iv}`)")]
+ public void Should_return_null_when_partition_not_found(string script)
{
var @event = new EnrichedContentEvent
{
@@ -181,13 +208,15 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
.AddValue("iv", "Berlin"))
};
- var result = sut.Format("$CONTENT_DATA.city.de", @event);
+ var result = sut.Format(script, @event);
- Assert.Equal("UNDEFINED", result);
+ Assert.Equal("null", result);
}
- [Fact]
- public void Should_return_undefined_when_array_item_not_found()
+ [Theory]
+ [InlineData("$CONTENT_DATA.city.iv.10")]
+ [InlineData("Script(`${event.data.country.de[10]}`)")]
+ public void Should_return_null_when_array_item_not_found(string script)
{
var @event = new EnrichedContentEvent
{
@@ -198,13 +227,15 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
.AddValue("iv", JsonValue.Array()))
};
- var result = sut.Format("$CONTENT_DATA.city.de.10", @event);
+ var result = sut.Format(script, @event);
- Assert.Equal("UNDEFINED", result);
+ Assert.Equal("null", result);
}
- [Fact]
- public void Should_return_undefined_when_property_not_found()
+ [Theory]
+ [InlineData("$CONTENT_DATA.city.de.Name")]
+ [InlineData("Script(`${event.data.city.de.Location}`)")]
+ public void Should_return_null_when_property_not_found(string script)
{
var @event = new EnrichedContentEvent
{
@@ -215,13 +246,15 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
.AddValue("iv", JsonValue.Object().Add("name", "Berlin")))
};
- var result = sut.Format("$CONTENT_DATA.city.de.Name", @event);
+ var result = sut.Format(script, @event);
- Assert.Equal("UNDEFINED", result);
+ Assert.Equal("null", result);
}
- [Fact]
- public void Should_return_plain_value_when_found()
+ [Theory]
+ [InlineData("$CONTENT_DATA.city.iv")]
+ [InlineData("Script(`${event.data.city.iv}`)")]
+ public void Should_return_plain_value_when_found(string script)
{
var @event = new EnrichedContentEvent
{
@@ -232,13 +265,15 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
.AddValue("iv", "Berlin"))
};
- var result = sut.Format("$CONTENT_DATA.city.iv", @event);
+ var result = sut.Format(script, @event);
Assert.Equal("Berlin", result);
}
- [Fact]
- public void Should_return_plain_value_when_found_from_update_event()
+ [Theory]
+ [InlineData("$CONTENT_DATA.city.iv.0")]
+ [InlineData("Script(`${event.data.city.iv[0]}`)")]
+ public void Should_return_plain_value_from_array_when_found(string script)
{
var @event = new EnrichedContentEvent
{
@@ -246,16 +281,19 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
new NamedContentData()
.AddField("city",
new ContentFieldData()
- .AddValue("iv", "Berlin"))
+ .AddValue("iv", JsonValue.Array(
+ "Berlin")))
};
- var result = sut.Format("$CONTENT_DATA.city.iv", @event);
+ var result = sut.Format(script, @event);
Assert.Equal("Berlin", result);
}
- [Fact]
- public void Should_return_undefined_when_null()
+ [Theory]
+ [InlineData("$CONTENT_DATA.city.iv.name")]
+ [InlineData("Script(`${event.data.city.iv.name}`)")]
+ public void Should_return_plain_value_from_object_when_found(string script)
{
var @event = new EnrichedContentEvent
{
@@ -263,16 +301,18 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
new NamedContentData()
.AddField("city",
new ContentFieldData()
- .AddValue("iv", JsonValue.Null))
+ .AddValue("iv", JsonValue.Object().Add("name", "Berlin")))
};
- var result = sut.Format("$CONTENT_DATA.city.iv", @event);
+ var result = sut.Format(script, @event);
- Assert.Equal("UNDEFINED", result);
+ Assert.Equal("Berlin", result);
}
- [Fact]
- public void Should_return_string_when_object()
+ [Theory]
+ [InlineData("$CONTENT_DATA.city.iv")]
+ [InlineData("Script(`${event.data.city.iv}`)", Skip = "Not Supported")]
+ public void Should_return_json_string_when_object(string script)
{
var @event = new EnrichedContentEvent
{
@@ -283,56 +323,27 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
.AddValue("iv", JsonValue.Object().Add("name", "Berlin")))
};
- var result = sut.Format("$CONTENT_DATA.city.iv", @event);
+ var result = sut.Format(script, @event);
Assert.Equal("{\"name\":\"Berlin\"}", result);
}
- [Fact]
- public void Should_return_plain_value_from_array_when_found()
+ [Theory]
+ [InlineData("$CONTENT_ACTION")]
+ public void Should_null_when_content_action_not_found(string script)
{
- var @event = new EnrichedContentEvent
- {
- Data =
- new NamedContentData()
- .AddField("city",
- new ContentFieldData()
- .AddValue("iv", JsonValue.Array(
- "Berlin")))
- };
-
- var result = sut.Format("$CONTENT_DATA.city.iv.0", @event);
-
- Assert.Equal("Berlin", result);
+ Assert.Equal("null", sut.Format(script, new EnrichedAssetEvent()));
}
- [Fact]
- public void Should_return_plain_value_from_object_when_found()
+ [Theory]
+ [InlineData("From $USER_NAME ($USER_EMAIL)")]
+ public void Should_format_email_and_display_name_from_client(string script)
{
- var @event = new EnrichedContentEvent
- {
- Data =
- new NamedContentData()
- .AddField("city",
- new ContentFieldData()
- .AddValue("iv", JsonValue.Object().Add("name", "Berlin")))
- };
-
- var result = sut.Format("$CONTENT_DATA.city.iv.name", @event);
-
- Assert.Equal("Berlin", result);
- }
+ var @event = new EnrichedContentEvent { Actor = new RefToken(RefTokenType.Client, "android") };
- [Fact]
- public void Should_format_content_actions_when_found()
- {
- Assert.Equal("created", sut.Format("$CONTENT_ACTION", new EnrichedContentEvent { Type = EnrichedContentEventType.Created }));
- }
+ var result = sut.Format(script, @event);
- [Fact]
- public void Should_format_content_actions_when_not_found()
- {
- Assert.Equal("UNDEFINED", sut.Format("$CONTENT_ACTION", new EnrichedAssetEvent()));
+ Assert.Equal("From client:android (client:android)", result);
}
}
}
diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj b/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj
index c68aff69b..ed4d7b441 100644
--- a/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj
+++ b/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj
@@ -11,7 +11,7 @@
-
+
diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj b/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj
index c3cd39700..20340d722 100644
--- a/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj
+++ b/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj
@@ -16,7 +16,7 @@
-
+
diff --git a/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj b/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj
index 6a0f7b16e..ca2daac0e 100644
--- a/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj
+++ b/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj
@@ -12,7 +12,7 @@
-
+
diff --git a/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj b/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj
index 576a53a5c..02579d476 100644
--- a/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj
+++ b/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj
@@ -12,7 +12,7 @@
-
+
diff --git a/tests/Squidex.Tests/Squidex.Tests.csproj b/tests/Squidex.Tests/Squidex.Tests.csproj
index d1c85aa9d..43a04e1fe 100644
--- a/tests/Squidex.Tests/Squidex.Tests.csproj
+++ b/tests/Squidex.Tests/Squidex.Tests.csproj
@@ -11,11 +11,11 @@
-
+
-
+