diff --git a/Squidex.sln.DotSettings b/Squidex.sln.DotSettings
index 3698200b1..ecdd59c86 100644
--- a/Squidex.sln.DotSettings
+++ b/Squidex.sln.DotSettings
@@ -44,4 +44,7 @@
All rights reserved. Licensed under the MIT license.
==========================================================================
+ True
+ True
+ True
\ No newline at end of file
diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AlgoliaActionHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AlgoliaActionHandler.cs
index 0e5727cbc..398f01c91 100644
--- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AlgoliaActionHandler.cs
+++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AlgoliaActionHandler.cs
@@ -24,13 +24,13 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{
private const string SchemaNamePlaceholder = "$SCHEMA_NAME";
private readonly ConcurrentDictionary<(string AppId, string ApiKey, string IndexName), Index> clients = new ConcurrentDictionary<(string AppId, string ApiKey, string IndexName), Index>();
- private readonly JsonSerializer serializer;
+ private readonly RuleEventFormatter formatter;
- public AlgoliaActionHandler(JsonSerializer serializer)
+ public AlgoliaActionHandler(RuleEventFormatter formatter)
{
- Guard.NotNull(serializer, nameof(serializer));
+ Guard.NotNull(formatter, nameof(formatter));
- this.serializer = serializer;
+ this.formatter = formatter;
}
protected override (string Description, RuleJobData Data) CreateJob(Envelope @event, string eventName, AlgoliaAction action)
@@ -46,7 +46,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{
ruleData["ContentId"] = contentEvent.ContentId.ToString();
ruleData["Operation"] = "Upsert";
- ruleData["IndexName"] = action.IndexName.Replace(SchemaNamePlaceholder, contentEvent.SchemaId.Name);
+ ruleData["IndexName"] = formatter.FormatString(action.IndexName, @event);
var timestamp = @event.Headers.Timestamp().ToString();
@@ -66,7 +66,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
new JProperty("createdBy", created.Actor.ToString()),
new JProperty("lastModified", timestamp),
new JProperty("lastModifiedBy", created.Actor.ToString()),
- new JProperty("data", JObject.FromObject(created.Data, serializer)));
+ new JProperty("data", formatter.ToRouteData(created.Data)));
break;
}
@@ -76,7 +76,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
ruleData["Content"] = new JObject(
new JProperty("lastModified", timestamp),
new JProperty("lastModifiedBy", updated.Actor.ToString()),
- new JProperty("data", JObject.FromObject(updated.Data, serializer)));
+ new JProperty("data", formatter.ToRouteData(updated.Data)));
break;
}
diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs
index 0400ef435..7b85b1b0e 100644
--- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs
+++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs
@@ -24,14 +24,13 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
public sealed class WebhookActionHandler : RuleActionHandler
{
private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(2);
+ private readonly RuleEventFormatter formatter;
- private readonly JsonSerializer serializer;
-
- public WebhookActionHandler(JsonSerializer serializer)
+ public WebhookActionHandler(RuleEventFormatter formatter)
{
- Guard.NotNull(serializer, nameof(serializer));
+ Guard.NotNull(formatter, nameof(formatter));
- this.serializer = serializer;
+ this.formatter = formatter;
}
protected override (string Description, RuleJobData Data) CreateJob(Envelope @event, string eventName, WebhookAction action)
@@ -55,7 +54,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{
return new JObject(
new JProperty("type", eventName),
- new JProperty("payload", JObject.FromObject(@event.Payload, serializer)),
+ new JProperty("payload", formatter.ToRouteData(@event.Payload)),
new JProperty("timestamp", @event.Headers.Timestamp().ToString()));
}
diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs
new file mode 100644
index 000000000..cf7f653a5
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs
@@ -0,0 +1,137 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// =========================================-=================================
+
+using System.Globalization;
+using System.Text;
+using System.Text.RegularExpressions;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using Squidex.Domain.Apps.Core.Contents;
+using Squidex.Domain.Apps.Events;
+using Squidex.Domain.Apps.Events.Contents;
+using Squidex.Infrastructure;
+using Squidex.Infrastructure.EventSourcing;
+
+namespace Squidex.Domain.Apps.Core.HandleRules
+{
+ public class RuleEventFormatter
+ {
+ private const string Undefined = "UNDEFINED";
+ private const string AppIdPlaceholder = "$APP_ID";
+ private const string AppNamePlaceholder = "$APP_NAME";
+ private const string SchemaIdPlaceholder = "$SCHEMA_ID";
+ private const string SchemaNamePlaceholder = "$SCHEMA_NAME";
+ private const string TimestampDatePlaceholder = "$TIMESTAMP_DATE";
+ private const string TimestampDateTimePlaceholder = "$TIMESTAMP_DATETIME";
+ private static readonly Regex ContentDataPlaceholder = new Regex(@"\$CONTENT_DATA(\.([0-9A-Za-z\-_]*)){2,}", RegexOptions.Compiled);
+ private readonly JsonSerializer serializer;
+
+ public RuleEventFormatter(JsonSerializer serializer)
+ {
+ Guard.NotNull(serializer, nameof(serializer));
+
+ this.serializer = serializer;
+ }
+
+ public virtual JToken ToRouteData(object value)
+ {
+ return JToken.FromObject(value, serializer);
+ }
+
+ public virtual string FormatString(string text, Envelope @event)
+ {
+ var sb = new StringBuilder(text);
+
+ if (@event.Headers.Contains(CommonHeaders.Timestamp))
+ {
+ var timestamp = @event.Headers.Timestamp().ToDateTimeUtc();
+
+ sb.Replace(TimestampDateTimePlaceholder, timestamp.ToString("yyy-MM-dd-hh-mm-ss", CultureInfo.InvariantCulture));
+ sb.Replace(TimestampDatePlaceholder, timestamp.ToString("yyy-MM-dd", CultureInfo.InvariantCulture));
+ }
+
+ if (@event.Payload.AppId != null)
+ {
+ sb.Replace(AppIdPlaceholder, @event.Payload.AppId.Id.ToString());
+ sb.Replace(AppNamePlaceholder, @event.Payload.AppId.Name);
+ }
+
+ if (@event.Payload is SchemaEvent schemaEvent && schemaEvent.SchemaId != null)
+ {
+ sb.Replace(SchemaIdPlaceholder, schemaEvent.SchemaId.Id.ToString());
+ sb.Replace(SchemaNamePlaceholder, schemaEvent.SchemaId.Name);
+ }
+
+ var result = sb.ToString();
+
+ if (@event.Payload is ContentCreated contentCreated && contentCreated.Data != null)
+ {
+ result = ReplaceData(contentCreated.Data, result);
+ }
+
+ if (@event.Payload is ContentUpdated contentUpdated && contentUpdated.Data != null)
+ {
+ result = ReplaceData(contentUpdated.Data, result);
+ }
+
+ return result;
+ }
+
+ private static string ReplaceData(NamedContentData data, string text)
+ {
+ return ContentDataPlaceholder.Replace(text, match =>
+ {
+ var captures = match.Groups[2].Captures;
+
+ var path = new string[captures.Count];
+
+ for (var i = 0; i < path.Length; i++)
+ {
+ path[i] = captures[i].Value;
+ }
+
+ if (!data.TryGetValue(path[0], out var field))
+ {
+ return Undefined;
+ }
+
+ if (!field.TryGetValue(path[1], out var value))
+ {
+ return Undefined;
+ }
+
+ for (var j = 2; j < path.Length; j++)
+ {
+ if (value is JObject obj && obj.TryGetValue(path[j], out value))
+ {
+ continue;
+ }
+ if (value is JArray arr && int.TryParse(path[j], out var idx) && idx >= 0 && idx < arr.Count)
+ {
+ value = arr[idx];
+ }
+ else
+ {
+ return Undefined;
+ }
+ }
+
+ if (value == null || value.Type == JTokenType.Null || value.Type == JTokenType.Undefined)
+ {
+ return Undefined;
+ }
+
+ if (value is JValue jValue && jValue != null)
+ {
+ return jValue.Value.ToString();
+ }
+
+ return value?.ToString(Formatting.Indented) ?? Undefined;
+ });
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Events/SquidexEvent.cs b/src/Squidex.Domain.Apps.Events/SquidexEvent.cs
index a9a71bdc1..8d42986bf 100644
--- a/src/Squidex.Domain.Apps.Events/SquidexEvent.cs
+++ b/src/Squidex.Domain.Apps.Events/SquidexEvent.cs
@@ -12,8 +12,6 @@ namespace Squidex.Domain.Apps.Events
{
public abstract class SquidexEvent : IEvent
{
- public string Username { get; set; }
-
public RefToken Actor { get; set; }
}
}
diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs
new file mode 100644
index 000000000..9b58ca316
--- /dev/null
+++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs
@@ -0,0 +1,246 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using NodaTime;
+using Squidex.Domain.Apps.Core.Contents;
+using Squidex.Domain.Apps.Core.HandleRules;
+using Squidex.Domain.Apps.Events;
+using Squidex.Domain.Apps.Events.Contents;
+using Squidex.Infrastructure;
+using Squidex.Infrastructure.EventSourcing;
+using Xunit;
+
+namespace Squidex.Domain.Apps.Core.Operations.HandleRules
+{
+ public class RuleEventFormatterTests
+ {
+ private readonly JsonSerializer serializer = JsonSerializer.CreateDefault();
+ private readonly RuleEventFormatter sut;
+
+ public RuleEventFormatterTests()
+ {
+ sut = new RuleEventFormatter(serializer);
+ }
+
+ [Fact]
+ public void Should_serialize_object_to_json()
+ {
+ var result = sut.ToRouteData(new { Value = 1 });
+
+ Assert.True(result is JObject);
+ }
+
+ [Fact]
+ public void Should_replace_app_information_from_event()
+ {
+ var appId = Guid.NewGuid();
+
+ var @event = new ContentCreated
+ {
+ AppId = new NamedId(appId, "my-app")
+ };
+
+ var result = sut.FormatString("Name $APP_NAME has id $APP_ID", Envelope.Create(@event).To());
+
+ Assert.Equal($"Name my-app has id {appId}", result);
+ }
+
+ [Fact]
+ public void Should_replace_schema_information_from_event()
+ {
+ var schemaId = Guid.NewGuid();
+
+ var @event = new ContentCreated
+ {
+ SchemaId = new NamedId(schemaId, "my-schema")
+ };
+
+ var result = sut.FormatString("Name $SCHEMA_NAME has id $SCHEMA_ID", Envelope.Create(@event).To());
+
+ Assert.Equal($"Name my-schema has id {schemaId}", result);
+ }
+
+ [Fact]
+ public void Should_replace_timestamp_information_from_event()
+ {
+ var now = DateTime.UtcNow;
+
+ var envelope = Envelope.Create(new ContentCreated()).To().SetTimestamp(Instant.FromDateTimeUtc(now));
+
+ var result = sut.FormatString("Date: $TIMESTAMP_DATE, Full: $TIMESTAMP_DATETIME", envelope);
+
+ Assert.Equal($"Date: {now:yyyy-MM-dd}, Full: {now:yyyy-MM-dd-hh-mm-ss}", result);
+ }
+
+ [Fact]
+ public void Should_return_undefined_when_field_not_found()
+ {
+ var @event = new ContentCreated
+ {
+ Data =
+ new NamedContentData()
+ .AddField("city", new ContentFieldData()
+ .AddValue("iv", "Berlin"))
+ };
+
+ var result = sut.FormatString("$CONTENT_DATA.country.iv", Envelope.Create(@event).To());
+
+ Assert.Equal("UNDEFINED", result);
+ }
+
+ [Fact]
+ public void Should_return_undefined_when_partition_not_found()
+ {
+ var @event = new ContentCreated
+ {
+ Data =
+ new NamedContentData()
+ .AddField("city", new ContentFieldData()
+ .AddValue("iv", "Berlin"))
+ };
+
+ var result = sut.FormatString("$CONTENT_DATA.city.de", Envelope.Create(@event).To());
+
+ Assert.Equal("UNDEFINED", result);
+ }
+
+ [Fact]
+ public void Should_return_undefined_when_array_item_not_found()
+ {
+ var @event = new ContentCreated
+ {
+ Data =
+ new NamedContentData()
+ .AddField("city", new ContentFieldData()
+ .AddValue("iv", new JArray()))
+ };
+
+ var result = sut.FormatString("$CONTENT_DATA.city.de.10", Envelope.Create(@event).To());
+
+ Assert.Equal("UNDEFINED", result);
+ }
+
+ [Fact]
+ public void Should_return_undefined_when_property_not_found()
+ {
+ var @event = new ContentCreated
+ {
+ Data =
+ new NamedContentData()
+ .AddField("city", new ContentFieldData()
+ .AddValue("iv", new JObject(
+ new JProperty("name", "Berlin"))))
+ };
+
+ var result = sut.FormatString("$CONTENT_DATA.city.de.Name", Envelope.Create(@event).To());
+
+ Assert.Equal("UNDEFINED", result);
+ }
+
+ [Fact]
+ public void Should_return_plain_value_when_found()
+ {
+ var @event = new ContentCreated
+ {
+ Data =
+ new NamedContentData()
+ .AddField("city", new ContentFieldData()
+ .AddValue("iv", "Berlin"))
+ };
+
+ var result = sut.FormatString("$CONTENT_DATA.city.iv", Envelope.Create(@event).To());
+
+ Assert.Equal("Berlin", result);
+ }
+
+ [Fact]
+ public void Should_return_undefined_when_null()
+ {
+ var @event = new ContentCreated
+ {
+ Data =
+ new NamedContentData()
+ .AddField("city", new ContentFieldData()
+ .AddValue("iv", JValue.CreateNull()))
+ };
+
+ var result = sut.FormatString("$CONTENT_DATA.city.iv", Envelope.Create(@event).To());
+
+ Assert.Equal("UNDEFINED", result);
+ }
+
+ [Fact]
+ public void Should_return_undefined_when_undefined()
+ {
+ var @event = new ContentCreated
+ {
+ Data =
+ new NamedContentData()
+ .AddField("city", new ContentFieldData()
+ .AddValue("iv", JValue.CreateUndefined()))
+ };
+
+ var result = sut.FormatString("$CONTENT_DATA.city.iv", Envelope.Create(@event).To());
+
+ Assert.Equal("UNDEFINED", result);
+ }
+
+ [Fact]
+ public void Should_return_string_when_object()
+ {
+ var @event = new ContentCreated
+ {
+ Data =
+ new NamedContentData()
+ .AddField("city", new ContentFieldData()
+ .AddValue("iv", new JObject(
+ new JProperty("name", "Berlin"))))
+ };
+
+ var result = sut.FormatString("$CONTENT_DATA.city.iv", Envelope.Create(@event).To());
+
+ Assert.Equal(JObject.FromObject(new { name = "Berlin" }).ToString(Formatting.Indented), result);
+ }
+
+ [Fact]
+ public void Should_return_plain_value_from_array_when_found()
+ {
+ var @event = new ContentCreated
+ {
+ Data =
+ new NamedContentData()
+ .AddField("city", new ContentFieldData()
+ .AddValue("iv", new JArray(
+ "Berlin")))
+ };
+
+ var result = sut.FormatString("$CONTENT_DATA.city.iv.0", Envelope.Create(@event).To());
+
+ Assert.Equal("Berlin", result);
+ }
+
+ [Fact]
+ public void Should_return_plain_value_from_object_when_found()
+ {
+ var @event = new ContentCreated
+ {
+ Data =
+ new NamedContentData()
+ .AddField("city", new ContentFieldData()
+ .AddValue("iv", new JObject(
+ new JProperty("name", "Berlin"))))
+ };
+
+ var result = sut.FormatString("$CONTENT_DATA.city.iv.name", Envelope.Create(@event).To());
+
+ Assert.Equal("Berlin", result);
+ }
+ }
+}