diff --git a/backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs index 257ab688f..aeaf40fbb 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs @@ -33,7 +33,7 @@ namespace Squidex.Extensions.Actions.Algolia }); } - protected override (string Description, AlgoliaJob Data) CreateJob(EnrichedEvent @event, AlgoliaAction action) + protected override async Task<(string Description, AlgoliaJob Data)> CreateJobAsync(EnrichedEvent @event, AlgoliaAction action) { if (@event is EnrichedContentEvent contentEvent) { @@ -45,7 +45,7 @@ namespace Squidex.Extensions.Actions.Algolia AppId = action.AppId, ApiKey = action.ApiKey, ContentId = contentId, - IndexName = Format(action.IndexName, @event) + IndexName = await FormatAsync(action.IndexName, @event) }; if (contentEvent.Type == EnrichedContentEventType.Deleted || @@ -64,7 +64,8 @@ namespace Squidex.Extensions.Actions.Algolia if (!string.IsNullOrEmpty(action.Document)) { - jsonString = Format(action.Document, @event)?.Trim(); + jsonString = await FormatAsync(action.Document, @event); + jsonString = jsonString?.Trim(); } else { diff --git a/backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueActionHandler.cs index 79e953045..8339a74de 100644 --- a/backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueActionHandler.cs @@ -32,9 +32,9 @@ namespace Squidex.Extensions.Actions.AzureQueue }); } - protected override (string Description, AzureQueueJob Data) CreateJob(EnrichedEvent @event, AzureQueueAction action) + protected override async Task<(string Description, AzureQueueJob Data)> CreateJobAsync(EnrichedEvent @event, AzureQueueAction action) { - var queueName = Format(action.Queue, @event); + var queueName = await FormatAsync(action.Queue, @event); var ruleDescription = $"Send AzureQueueJob to azure queue '{queueName}'"; var ruleJob = new AzureQueueJob diff --git a/backend/extensions/Squidex.Extensions/Actions/Comment/CommentActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Comment/CommentActionHandler.cs index 25f053d9f..c2b89fb54 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Comment/CommentActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Comment/CommentActionHandler.cs @@ -30,11 +30,11 @@ namespace Squidex.Extensions.Actions.Comment this.commandBus = commandBus; } - protected override (string Description, CommentJob Data) CreateJob(EnrichedEvent @event, CommentAction action) + protected override async Task<(string Description, CommentJob Data)> CreateJobAsync(EnrichedEvent @event, CommentAction action) { if (@event is EnrichedContentEvent contentEvent) { - var text = Format(action.Text, @event); + var text = await FormatAsync(action.Text, @event); var actor = contentEvent.Actor; diff --git a/backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs index 6bec528fe..5d1576ab7 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs @@ -28,13 +28,13 @@ namespace Squidex.Extensions.Actions.Discourse this.httpClientFactory = httpClientFactory; } - protected override (string Description, DiscourseJob Data) CreateJob(EnrichedEvent @event, DiscourseAction action) + protected override async Task<(string Description, DiscourseJob Data)> CreateJobAsync(EnrichedEvent @event, DiscourseAction action) { var url = $"{action.Url.ToString().TrimEnd('/')}/posts.json?api_key={action.ApiKey}&api_username={action.ApiUsername}"; var json = new Dictionary { - ["title"] = Format(action.Title, @event) + ["title"] = await FormatAsync(action.Title, @event) }; if (action.Topic.HasValue) @@ -47,7 +47,7 @@ namespace Squidex.Extensions.Actions.Discourse json.Add("category", action.Category.Value); } - json["raw"] = Format(action.Text, @event); + json["raw"] = await FormatAsync(action.Text, @event); var requestBody = ToJson(json); diff --git a/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs index cf0ec87fc..19e367783 100644 --- a/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs @@ -36,7 +36,7 @@ namespace Squidex.Extensions.Actions.ElasticSearch }); } - protected override (string Description, ElasticSearchJob Data) CreateJob(EnrichedEvent @event, ElasticSearchAction action) + protected override async Task<(string Description, ElasticSearchJob Data)> CreateJobAsync(EnrichedEvent @event, ElasticSearchAction action) { if (@event is EnrichedContentEvent contentEvent) { @@ -46,7 +46,7 @@ namespace Squidex.Extensions.Actions.ElasticSearch var ruleJob = new ElasticSearchJob { - IndexName = Format(action.IndexName, @event), + IndexName = await FormatAsync(action.IndexName, @event), ServerHost = action.Host.ToString(), ServerUser = action.Username, ServerPassword = action.Password, diff --git a/backend/extensions/Squidex.Extensions/Actions/Email/EmailActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Email/EmailActionHandler.cs index ea23d1eae..499f2f75d 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Email/EmailActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Email/EmailActionHandler.cs @@ -23,7 +23,7 @@ namespace Squidex.Extensions.Actions.Email { } - protected override (string Description, EmailJob Data) CreateJob(EnrichedEvent @event, EmailAction action) + protected override async Task<(string Description, EmailJob Data)> CreateJobAsync(EnrichedEvent @event, EmailAction action) { var ruleJob = new EmailJob { @@ -31,11 +31,11 @@ namespace Squidex.Extensions.Actions.Email ServerUseSsl = action.ServerUseSsl, ServerPassword = action.ServerPassword, ServerPort = action.ServerPort, - ServerUsername = Format(action.ServerUsername, @event), - MessageFrom = Format(action.MessageFrom, @event), - MessageTo = Format(action.MessageTo, @event), - MessageSubject = Format(action.MessageSubject, @event), - MessageBody = Format(action.MessageBody, @event) + ServerUsername = await FormatAsync(action.ServerUsername, @event), + MessageFrom = await FormatAsync(action.MessageFrom, @event), + MessageTo = await FormatAsync(action.MessageTo, @event), + MessageSubject = await FormatAsync(action.MessageSubject, @event), + MessageBody = await FormatAsync(action.MessageBody, @event) }; var description = $"Send an email to {action.MessageTo}"; diff --git a/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaActionHandler.cs index dd5964e83..aa7b060fc 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaActionHandler.cs @@ -27,13 +27,13 @@ namespace Squidex.Extensions.Actions.Kafka this.kafkaProducer = kafkaProducer; } - protected override (string Description, KafkaJob Data) CreateJob(EnrichedEvent @event, KafkaAction action) + protected override async Task<(string Description, KafkaJob Data)> CreateJobAsync(EnrichedEvent @event, KafkaAction action) { string value, key; if (!string.IsNullOrEmpty(action.Payload)) { - value = Format(action.Payload, @event); + value = await FormatAsync(action.Payload, @event); } else { @@ -42,7 +42,7 @@ namespace Squidex.Extensions.Actions.Kafka if (!string.IsNullOrEmpty(action.Key)) { - key = Format(action.Key, @event); + key = await FormatAsync(action.Key, @event); } else { @@ -54,14 +54,14 @@ namespace Squidex.Extensions.Actions.Kafka TopicName = action.TopicName, MessageKey = key, MessageValue = value, - Headers = ParseHeaders(action.Headers, @event), + Headers = await ParseHeadersAsync(action.Headers, @event), Schema = action.Schema }; return (Description, ruleJob); } - private Dictionary ParseHeaders(string headers, EnrichedEvent @event) + private async Task> ParseHeadersAsync(string headers, EnrichedEvent @event) { if (string.IsNullOrWhiteSpace(headers)) { @@ -81,7 +81,7 @@ namespace Squidex.Extensions.Actions.Kafka var key = line.Substring(0, indexEqual); var val = line.Substring(indexEqual + 1); - val = Format(val, @event); + val = await FormatAsync(val, @event); headersDictionary[key] = val; } diff --git a/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducer.cs b/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducer.cs index a8f2ecfaa..ec7d8c5b7 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducer.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducer.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using System.Collections.Generic; using System.Threading.Tasks; using Avro; @@ -13,7 +12,6 @@ using Avro.Generic; using Confluent.Kafka; using Confluent.SchemaRegistry; using Confluent.SchemaRegistry.Serdes; -using GraphQL.Types; using Microsoft.Extensions.Options; using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json.Objects; diff --git a/backend/extensions/Squidex.Extensions/Actions/Medium/MediumActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Medium/MediumActionHandler.cs index 9af62802b..4ed8564ce 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Medium/MediumActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Medium/MediumActionHandler.cs @@ -42,17 +42,17 @@ namespace Squidex.Extensions.Actions.Medium this.serializer = serializer; } - protected override (string Description, MediumJob Data) CreateJob(EnrichedEvent @event, MediumAction action) + protected override async Task<(string Description, MediumJob Data)> CreateJobAsync(EnrichedEvent @event, MediumAction action) { var ruleJob = new MediumJob { AccessToken = action.AccessToken, PublicationId = action.PublicationId }; var requestBody = new { - title = Format(action.Title, @event), + title = await FormatAsync(action.Title, @event), contentFormat = action.IsHtml ? "html" : "markdown", - content = Format(action.Content, @event), - canonicalUrl = Format(action.CanonicalUrl, @event), - tags = ParseTags(@event, action) + content = await FormatAsync(action.Content, @event), + canonicalUrl = await FormatAsync(action.CanonicalUrl, @event), + tags = await ParseTagsAsync(@event, action) }; ruleJob.RequestBody = ToJson(requestBody); @@ -60,7 +60,7 @@ namespace Squidex.Extensions.Actions.Medium return (Description, ruleJob); } - private string[] ParseTags(EnrichedEvent @event, MediumAction action) + private async Task ParseTagsAsync(EnrichedEvent @event, MediumAction action) { if (string.IsNullOrWhiteSpace(action.Tags)) { @@ -69,7 +69,7 @@ namespace Squidex.Extensions.Actions.Medium try { - var jsonTags = Format(action.Tags, @event); + var jsonTags = await FormatAsync(action.Tags, @event); return serializer.Deserialize(jsonTags); } diff --git a/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationActionHandler.cs index 42f8580de..0093b2335 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationActionHandler.cs @@ -40,7 +40,7 @@ namespace Squidex.Extensions.Actions.Notification { if (@event is EnrichedUserEventBase userEvent) { - var text = Format(action.Text, @event); + var text = await FormatAsync(action.Text, @event); var actor = userEvent.Actor; @@ -60,7 +60,7 @@ namespace Squidex.Extensions.Actions.Notification if (!string.IsNullOrWhiteSpace(action.Url)) { - var url = Format(action.Url, @event); + var url = await FormatAsync(action.Url, @event); if (Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out var uri)) { diff --git a/backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderActionHandler.cs index 2b4e83be8..3e35e6a9c 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderActionHandler.cs @@ -24,9 +24,9 @@ namespace Squidex.Extensions.Actions.Prerender this.httpClientFactory = httpClientFactory; } - protected override (string Description, PrerenderJob Data) CreateJob(EnrichedEvent @event, PrerenderAction action) + protected override async Task<(string Description, PrerenderJob Data)> CreateJobAsync(EnrichedEvent @event, PrerenderAction action) { - var url = Format(action.Url, @event); + var url = await FormatAsync(action.Url, @event); var request = new { prerenderToken = action.Token, url }; var requestBody = ToJson(request); diff --git a/backend/extensions/Squidex.Extensions/Actions/Slack/SlackActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Slack/SlackActionHandler.cs index 39779a5b9..0143e52e6 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Slack/SlackActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Slack/SlackActionHandler.cs @@ -30,9 +30,9 @@ namespace Squidex.Extensions.Actions.Slack this.httpClientFactory = httpClientFactory; } - protected override (string Description, SlackJob Data) CreateJob(EnrichedEvent @event, SlackAction action) + protected override async Task<(string Description, SlackJob Data)> CreateJobAsync(EnrichedEvent @event, SlackAction action) { - var body = new { text = Format(action.Text, @event) }; + var body = new { text = await FormatAsync(action.Text, @event) }; var ruleJob = new SlackJob { diff --git a/backend/extensions/Squidex.Extensions/Actions/Twitter/TweetActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Twitter/TweetActionHandler.cs index 83c95e790..a7cfebe09 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Twitter/TweetActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Twitter/TweetActionHandler.cs @@ -30,11 +30,11 @@ namespace Squidex.Extensions.Actions.Twitter this.twitterOptions = twitterOptions.Value; } - protected override (string Description, TweetJob Data) CreateJob(EnrichedEvent @event, TweetAction action) + protected override async Task<(string Description, TweetJob Data)> CreateJobAsync(EnrichedEvent @event, TweetAction action) { var ruleJob = new TweetJob { - Text = Format(action.Text, @event), + Text = await FormatAsync(action.Text, @event), AccessToken = action.AccessToken, AccessSecret = action.AccessSecret }; diff --git a/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs index 16cb53a36..6cd86eb5d 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs @@ -27,25 +27,25 @@ namespace Squidex.Extensions.Actions.Webhook this.httpClientFactory = httpClientFactory; } - protected override (string Description, WebhookJob Data) CreateJob(EnrichedEvent @event, WebhookAction action) + protected override async Task<(string Description, WebhookJob Data)> CreateJobAsync(EnrichedEvent @event, WebhookAction action) { string requestBody; if (!string.IsNullOrEmpty(action.Payload)) { - requestBody = Format(action.Payload, @event); + requestBody = await FormatAsync(action.Payload, @event); } else { requestBody = ToEnvelopeJson(@event); } - var requestUrl = Format(action.Url, @event); + var requestUrl = await FormatAsync(action.Url, @event); var ruleDescription = $"Send event to webhook '{requestUrl}'"; var ruleJob = new WebhookJob { - RequestUrl = Format(action.Url.ToString(), @event), + RequestUrl = await FormatAsync(action.Url.ToString(), @event), RequestSignature = $"{requestBody}{action.SharedSecret}".Sha256Base64(), RequestBody = requestBody }; diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleEventFormatter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleEventFormatter.cs new file mode 100644 index 000000000..8fc4762de --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleEventFormatter.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public interface IRuleEventFormatter + { + (bool Match, string?, int ReplacedLength) Format(EnrichedEvent @event, string text) + { + return default; + } + + (bool Match, ValueTask) Format(EnrichedEvent @event, object value, string[] path) + { + return default; + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/PredefinedPatternsFormatter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/PredefinedPatternsFormatter.cs new file mode 100644 index 000000000..f2877da5f --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/PredefinedPatternsFormatter.cs @@ -0,0 +1,197 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Globalization; +using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; +using Squidex.Infrastructure; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public sealed class PredefinedPatternsFormatter : IRuleEventFormatter + { + private readonly List<(string Pattern, Func Replacer)> patterns = new List<(string Pattern, Func Replacer)>(); + private readonly IUrlGenerator urlGenerator; + + public PredefinedPatternsFormatter(IUrlGenerator urlGenerator) + { + Guard.NotNull(urlGenerator, nameof(urlGenerator)); + + this.urlGenerator = urlGenerator; + + AddPattern("APP_ID", AppId); + AddPattern("APP_NAME", AppName); + AddPattern("ASSET_CONTENT_URL", AssetContentUrl); + AddPattern("CONTENT_ACTION", ContentAction); + AddPattern("CONTENT_URL", ContentUrl); + AddPattern("MENTIONED_ID", MentionedId); + AddPattern("MENTIONED_NAME", MentionedName); + AddPattern("MENTIONED_EMAIL", MentionedEmail); + AddPattern("SCHEMA_ID", SchemaId); + AddPattern("SCHEMA_NAME", SchemaName); + AddPattern("TIMESTAMP_DATETIME", TimestampTime); + AddPattern("TIMESTAMP_DATE", TimestampDate); + AddPattern("USER_ID", UserId); + AddPattern("USER_NAME", UserName); + AddPattern("USER_EMAIL", UserEmail); + } + + private void AddPattern(string placeholder, Func generator) + { + patterns.Add((placeholder, generator)); + } + + public (bool Match, string?, int ReplacedLength) Format(EnrichedEvent @event, string text) + { + for (var j = 0; j < patterns.Count; j++) + { + var (pattern, replacer) = patterns[j]; + + if (text.StartsWith(pattern, StringComparison.OrdinalIgnoreCase)) + { + var result = replacer(@event); + + return (true, result, pattern.Length); + } + } + + return default; + } + + private static string TimestampDate(EnrichedEvent @event) + { + return @event.Timestamp.ToDateTimeUtc().ToString("yyy-MM-dd", CultureInfo.InvariantCulture); + } + + private static string TimestampTime(EnrichedEvent @event) + { + return @event.Timestamp.ToDateTimeUtc().ToString("yyy-MM-dd-hh-mm-ss", CultureInfo.InvariantCulture); + } + + private static string AppId(EnrichedEvent @event) + { + return @event.AppId.Id.ToString(); + } + + private static string AppName(EnrichedEvent @event) + { + return @event.AppId.Name; + } + + private static string? SchemaId(EnrichedEvent @event) + { + if (@event is EnrichedSchemaEventBase schemaEvent) + { + return schemaEvent.SchemaId.Id.ToString(); + } + + return null; + } + + private static string? SchemaName(EnrichedEvent @event) + { + if (@event is EnrichedSchemaEventBase schemaEvent) + { + return schemaEvent.SchemaId.Name; + } + + return null; + } + + private static string? ContentAction(EnrichedEvent @event) + { + if (@event is EnrichedContentEvent contentEvent) + { + return contentEvent.Type.ToString(); + } + + return null; + } + + private string? AssetContentUrl(EnrichedEvent @event) + { + if (@event is EnrichedAssetEvent assetEvent) + { + return urlGenerator.AssetContent(assetEvent.Id); + } + + return null; + } + + private string? ContentUrl(EnrichedEvent @event) + { + if (@event is EnrichedContentEvent contentEvent) + { + return urlGenerator.ContentUI(contentEvent.AppId, contentEvent.SchemaId, contentEvent.Id); + } + + return null; + } + + private static string? UserName(EnrichedEvent @event) + { + if (@event is EnrichedUserEventBase userEvent) + { + return userEvent.User?.DisplayName(); + } + + return null; + } + + private static string? UserId(EnrichedEvent @event) + { + if (@event is EnrichedUserEventBase userEvent) + { + return userEvent.User?.Id; + } + + return null; + } + + private static string? UserEmail(EnrichedEvent @event) + { + if (@event is EnrichedUserEventBase userEvent) + { + return userEvent.User?.Email; + } + + return null; + } + + private static string? MentionedName(EnrichedEvent @event) + { + if (@event is EnrichedCommentEvent commentEvent) + { + return commentEvent.MentionedUser.DisplayName(); + } + + return null; + } + + private static string? MentionedId(EnrichedEvent @event) + { + if (@event is EnrichedCommentEvent commentEvent) + { + return commentEvent.MentionedUser.Id; + } + + return null; + } + + private static string? MentionedEmail(EnrichedEvent @event) + { + if (@event is EnrichedCommentEvent commentEvent) + { + return commentEvent.MentionedUser.Email; + } + + return null; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs index 92f0b3256..e9b3fb1f1 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs @@ -47,14 +47,14 @@ namespace Squidex.Domain.Apps.Core.HandleRules return formatter.ToEnvelope(@event); } - protected string? Format(Uri uri, EnrichedEvent @event) + protected ValueTask FormatAsync(Uri uri, EnrichedEvent @event) { - return formatter.Format(uri.ToString(), @event); + return formatter.FormatAsync(uri.ToString(), @event); } - protected string? Format(string text, EnrichedEvent @event) + protected ValueTask FormatAsync(string text, EnrichedEvent @event) { - return formatter.Format(text, @event); + return formatter.FormatAsync(text, @event); } async Task<(string Description, object Data)> IRuleActionHandler.CreateJobAsync(EnrichedEvent @event, RuleAction action) 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 4a84a20fa..c1b807b75 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs @@ -7,64 +7,72 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; -using System.Reflection; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using Newtonsoft.Json; -using Squidex.Domain.Apps.Core.Contents; +using NodaTime.Text; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Scripting; using Squidex.Infrastructure; using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Json.Objects; -using Squidex.Shared.Identity; -using Squidex.Shared.Users; namespace Squidex.Domain.Apps.Core.HandleRules { public class RuleEventFormatter { - private const string Fallback = "null"; + private const string GlobalFallback = "null"; private static readonly Regex RegexPatternOld = new Regex(@"^(?(?[^_]*)_(?[^\s]*))", RegexOptions.Compiled); private static readonly Regex RegexPatternNew = new Regex(@"^\{(?(?[\w]+)_(?[\w\.\-]+))[\s]*(\|[\s]*(?[^\?}]+))?(\?[\s]*(?[^\}\s]+))?[\s]*\}", RegexOptions.Compiled); - private readonly List<(string Pattern, Func Replacer)> patterns = new List<(string Pattern, Func Replacer)>(); private readonly IJsonSerializer jsonSerializer; - private readonly IUrlGenerator urlGenerator; + private readonly IEnumerable formatters; private readonly IScriptEngine scriptEngine; - public RuleEventFormatter(IJsonSerializer jsonSerializer, IUrlGenerator urlGenerator, IScriptEngine scriptEngine) + private struct TextPart + { + public bool IsText; + + public int Length; + + public int Offset; + + public string Fallback; + + public string Transform; + + public ValueTask Replacement; + + public static TextPart Text(int offset, int length) + { + var result = default(TextPart); + result.Offset = offset; + result.Length = length; + result.IsText = true; + + return result; + } + + public static TextPart Variable(ValueTask replacement, string fallback, string transform) + { + var result = default(TextPart); + result.Replacement = replacement; + result.Fallback = fallback; + result.Transform = transform; + + return result; + } + } + + public RuleEventFormatter(IJsonSerializer jsonSerializer, IEnumerable formatters, IScriptEngine scriptEngine) { Guard.NotNull(jsonSerializer, nameof(jsonSerializer)); Guard.NotNull(scriptEngine, nameof(scriptEngine)); - Guard.NotNull(urlGenerator, nameof(urlGenerator)); + Guard.NotNull(formatters, nameof(formatters)); this.jsonSerializer = jsonSerializer; + this.formatters = formatters; this.scriptEngine = scriptEngine; - this.urlGenerator = urlGenerator; - - AddPattern("APP_ID", AppId); - AddPattern("APP_NAME", AppName); - AddPattern("ASSET_CONTENT_URL", AssetContentUrl); - AddPattern("CONTENT_ACTION", ContentAction); - AddPattern("CONTENT_URL", ContentUrl); - AddPattern("MENTIONED_ID", MentionedId); - AddPattern("MENTIONED_NAME", MentionedName); - AddPattern("MENTIONED_EMAIL", MentionedEmail); - AddPattern("SCHEMA_ID", SchemaId); - AddPattern("SCHEMA_NAME", SchemaName); - AddPattern("TIMESTAMP_DATETIME", TimestampTime); - AddPattern("TIMESTAMP_DATE", TimestampDate); - AddPattern("USER_ID", UserId); - AddPattern("USER_NAME", UserName); - AddPattern("USER_EMAIL", UserEmail); - } - - private void AddPattern(string placeholder, Func generator) - { - patterns.Add((placeholder, generator)); } public virtual string ToPayload(T @event) @@ -77,7 +85,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules return jsonSerializer.Serialize(new { type = @event.Name, payload = @event, timestamp = @event.Timestamp }); } - public string? Format(string text, EnrichedEvent @event) + public async ValueTask FormatAsync(string text, EnrichedEvent @event) { if (string.IsNullOrWhiteSpace(text)) { @@ -94,11 +102,55 @@ namespace Squidex.Domain.Apps.Core.HandleRules return scriptEngine.Interpolate(context, script); } + var parts = BuildParts(text, @event); + + await Task.WhenAll(parts.Select(x => x.Replacement.AsTask())); + + return CombineParts(text, parts); + } + + private string CombineParts(string text, List parts) + { var span = text.AsSpan(); - var currentOffset = 0; + var sb = new StringBuilder(); + + foreach (var part in parts) + { + if (!part.IsText) + { + var result = part.Replacement.Result; + + result = TransformText(result, part.Transform); + + if (result == null) + { + result = part.Fallback; + } + + if (string.IsNullOrEmpty(result)) + { + result = GlobalFallback; + } + + sb.Append(result); + } + else + { + sb.Append(span.Slice(part.Offset, part.Length)); + } + } + + return sb.ToString(); + } + + private List BuildParts(string text, EnrichedEvent @event) + { + var parts = new List(); - var parts = new List<(int Offset, int Length, ValueTask Task)>(); + var span = text.AsSpan(); + + var currentOffset = 0; for (var i = 0; i < text.Length; i++) { @@ -106,13 +158,13 @@ namespace Squidex.Domain.Apps.Core.HandleRules if (c == '$') { - parts.Add((currentOffset, i - currentOffset, default)); + parts.Add(TextPart.Text(currentOffset, i - currentOffset)); - var (replacement, length) = GetReplacement(span.Slice(i + 1).ToString(), @event); + var (length, part) = GetReplacement(span.Slice(i + 1).ToString(), @event); if (length > 0) { - parts.Add((0, 0, new ValueTask(replacement))); + parts.Add(part); i += length + 1; } @@ -121,54 +173,28 @@ namespace Squidex.Domain.Apps.Core.HandleRules } } - parts.Add((currentOffset, text.Length - currentOffset, default)); - - var sb = new StringBuilder(); - - foreach (var (offset, length, task) in parts) - { - if (task.Result != null) - { - sb.Append(task.Result); - } - else - { - sb.Append(span.Slice(offset, length)); - } - } + parts.Add(TextPart.Text(currentOffset, text.Length - currentOffset)); - return sb.ToString(); + return parts; } - private (string Result, int Length) GetReplacement(string test, EnrichedEvent @event) + private (int Length, TextPart Part) GetReplacement(string test, EnrichedEvent @event) { var (isNewRegex, match) = Match(test); if (match.Success) { - var (length, text) = ResolveOldPatterns(match, isNewRegex, @event); + var (length, replacement) = ResolveOldPatterns(match, isNewRegex, @event); if (length == 0) { - (length, text) = ResolveFromPath(match, @event); - } - - var result = TransformText(text, match.Groups["Transform"]?.Value); - - if (result == null) - { - result = match.Groups["Fallback"]?.Value; + (length, replacement) = ResolveFromPath(match, @event); } - if (string.IsNullOrEmpty(result)) - { - result = Fallback; - } - - return (result, length); + return (length, TextPart.Variable(replacement, match.Groups["Fallback"].Value, match.Groups["Transform"].Value)); } - return (Fallback, 0); + return default; } private (bool IsNew, Match) Match(string test) @@ -183,162 +209,28 @@ namespace Squidex.Domain.Apps.Core.HandleRules return (false, RegexPatternOld.Match(test)); } - private (int Length, string? Result) ResolveOldPatterns(Match match, bool isNewRegex, EnrichedEvent @event) + private (int Length, ValueTask Result) ResolveOldPatterns(Match match, bool isNewRegex, EnrichedEvent @event) { var fullPath = match.Groups["FullPath"].Value; - for (var j = 0; j < patterns.Count; j++) + foreach (var formatter in formatters) { - var (pattern, replacer) = patterns[j]; + var (replaced, result, replacedLength) = formatter.Format(@event, fullPath); - if (fullPath.StartsWith(pattern, StringComparison.OrdinalIgnoreCase)) + if (replaced) { - var result = replacer(@event); - if (isNewRegex) { - return (match.Length, result); - } - else - { - return (pattern.Length, result); + replacedLength = match.Length; } + + return (replacedLength, new ValueTask(result)); } } return default; } - private static string TimestampDate(EnrichedEvent @event) - { - return @event.Timestamp.ToDateTimeUtc().ToString("yyy-MM-dd", CultureInfo.InvariantCulture); - } - - private static string TimestampTime(EnrichedEvent @event) - { - return @event.Timestamp.ToDateTimeUtc().ToString("yyy-MM-dd-hh-mm-ss", CultureInfo.InvariantCulture); - } - - private static string AppId(EnrichedEvent @event) - { - return @event.AppId.Id.ToString(); - } - - private static string AppName(EnrichedEvent @event) - { - return @event.AppId.Name; - } - - private static string? SchemaId(EnrichedEvent @event) - { - if (@event is EnrichedSchemaEventBase schemaEvent) - { - return schemaEvent.SchemaId.Id.ToString(); - } - - return null; - } - - private static string? SchemaName(EnrichedEvent @event) - { - if (@event is EnrichedSchemaEventBase schemaEvent) - { - return schemaEvent.SchemaId.Name; - } - - return null; - } - - private static string? ContentAction(EnrichedEvent @event) - { - if (@event is EnrichedContentEvent contentEvent) - { - return contentEvent.Type.ToString(); - } - - return null; - } - - private string? AssetContentUrl(EnrichedEvent @event) - { - if (@event is EnrichedAssetEvent assetEvent) - { - return urlGenerator.AssetContent(assetEvent.Id); - } - - return null; - } - - private string? ContentUrl(EnrichedEvent @event) - { - if (@event is EnrichedContentEvent contentEvent) - { - return urlGenerator.ContentUI(contentEvent.AppId, contentEvent.SchemaId, contentEvent.Id); - } - - return null; - } - - private static string? UserName(EnrichedEvent @event) - { - if (@event is EnrichedUserEventBase userEvent) - { - return userEvent.User?.DisplayName(); - } - - return null; - } - - private static string? UserId(EnrichedEvent @event) - { - if (@event is EnrichedUserEventBase userEvent) - { - return userEvent.User?.Id; - } - - return null; - } - - private static string? UserEmail(EnrichedEvent @event) - { - if (@event is EnrichedUserEventBase userEvent) - { - return userEvent.User?.Email; - } - - return null; - } - - private static string? MentionedName(EnrichedEvent @event) - { - if (@event is EnrichedCommentEvent commentEvent) - { - return commentEvent.MentionedUser.DisplayName(); - } - - return null; - } - - private static string? MentionedId(EnrichedEvent @event) - { - if (@event is EnrichedCommentEvent commentEvent) - { - return commentEvent.MentionedUser.Id; - } - - return null; - } - - private static string? MentionedEmail(EnrichedEvent @event) - { - if (@event is EnrichedCommentEvent commentEvent) - { - return commentEvent.MentionedUser.Email; - } - - return null; - } - private static string? TransformText(string? text, string? transform) { if (text != null && !string.IsNullOrWhiteSpace(transform)) @@ -365,6 +257,29 @@ namespace Squidex.Domain.Apps.Core.HandleRules case "trim": text = text.Trim(); break; + case "timestamp_ms": + { + var instant = InstantPattern.General.Parse(text); + + if (instant.Success) + { + text = instant.Value.ToUnixTimeMilliseconds().ToString(); + } + + break; + } + + case "timestamp_seconds": + { + var instant = InstantPattern.General.Parse(text); + + if (instant.Success) + { + text = instant.Value.ToUnixTimeSeconds().ToString(); + } + + break; + } } } } @@ -372,90 +287,30 @@ namespace Squidex.Domain.Apps.Core.HandleRules return text; } - private (int Length, string? Result) ResolveFromPath(Match match, EnrichedEvent @event) + private (int Length, ValueTask Result) ResolveFromPath(Match match, EnrichedEvent @event) { var path = match.Groups["Path"].Value.Split('.', StringSplitOptions.RemoveEmptyEntries); - var result = CalculateData(@event, path); - - return (match.Length, result); - } - - private static string? CalculateData(object @event, string[] path) - { - object? current = @event; + var (result, remaining) = RuleVariable.GetValue(@event, path); - foreach (var segment in path) + if (remaining.Length > 0 && result != null) { - if (current is NamedContentData data) + foreach (var formatter in formatters) { - if (!data.TryGetValue(segment, out var temp) || temp == null) - { - return null; - } + var (replaced, result2) = formatter.Format(@event, result, remaining); - current = temp; - } - else if (current is ContentFieldData field) - { - if (!field.TryGetValue(segment, out var temp) || temp == null) + if (replaced) { - return null; + return (match.Length, result2); } - - current = temp; - } - else if (current is IJsonValue json) - { - if (!json.TryGet(segment, out var temp) || temp == null || temp.Type == JsonValueType.Null) - { - return null; - } - - current = temp; - } - else if (current != null) - { - if (current is IUser user) - { - var type = segment; - - if (string.Equals(type, "Name", StringComparison.OrdinalIgnoreCase)) - { - type = SquidexClaimTypes.DisplayName; - } - - var claim = user.Claims.FirstOrDefault(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase)); - - if (claim != null) - { - current = claim.Value; - continue; - } - } - - const BindingFlags bindingFlags = - BindingFlags.FlattenHierarchy | - BindingFlags.Public | - BindingFlags.Instance; - - var properties = current.GetType().GetProperties(bindingFlags); - var property = properties.FirstOrDefault(x => x.CanRead && string.Equals(x.Name, segment, StringComparison.OrdinalIgnoreCase)); - - if (property == null) - { - return null; - } - - current = property.GetValue(current); - } - else - { - return null; } } + else if (remaining.Length == 0) + { + return (match.Length, new ValueTask(result?.ToString())); + } - return current?.ToString(); + return (match.Length, default); } private static bool TryGetScript(string text, out string script) diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleVariable.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleVariable.cs new file mode 100644 index 000000000..f4a1f7665 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleVariable.cs @@ -0,0 +1,101 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using System.Reflection; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Shared.Identity; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public static class RuleVariable + { + public static (object? Result, string[] Remaining) GetValue(object @event, string[] path) + { + object? current = @event; + + var i = 0; + + for (; i < path.Length; i++) + { + var segment = path[i]; + + if (current is NamedContentData data) + { + if (!data.TryGetValue(segment, out var temp) || temp == null) + { + break; + } + + current = temp; + } + else if (current is ContentFieldData field) + { + if (!field.TryGetValue(segment, out var temp) || temp == null) + { + break; + } + + current = temp; + } + else if (current is IJsonValue json) + { + if (!json.TryGet(segment, out var temp) || temp == null || temp.Type == JsonValueType.Null) + { + break; + } + + current = temp; + } + else if (current != null) + { + if (current is IUser user) + { + var type = segment; + + if (string.Equals(type, "Name", StringComparison.OrdinalIgnoreCase)) + { + type = SquidexClaimTypes.DisplayName; + } + + var claim = user.Claims.FirstOrDefault(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase)); + + if (claim != null) + { + current = claim.Value; + continue; + } + } + + const BindingFlags bindingFlags = + BindingFlags.FlattenHierarchy | + BindingFlags.Public | + BindingFlags.Instance; + + var properties = current.GetType().GetProperties(bindingFlags); + var property = properties.FirstOrDefault(x => x.CanRead && string.Equals(x.Name, segment, StringComparison.OrdinalIgnoreCase)); + + if (property == null) + { + break; + } + + current = property.GetValue(current); + } + else + { + break; + } + } + + return (current, path.Skip(i).ToArray()); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs index f1bc57915..05401a941 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs @@ -14,24 +14,62 @@ using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Events.Contents; using Squidex.Infrastructure; +using Squidex.Infrastructure.Caching; using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.Contents { - public sealed class ContentChangedTriggerHandler : RuleTriggerHandler + public sealed class ContentChangedTriggerHandler : RuleTriggerHandler, IRuleEventFormatter { private readonly IScriptEngine scriptEngine; private readonly IContentLoader contentLoader; + private readonly ILocalCache localCache; - public ContentChangedTriggerHandler(IScriptEngine scriptEngine, IContentLoader contentLoader) + public ContentChangedTriggerHandler(IScriptEngine scriptEngine, IContentLoader contentLoader, ILocalCache localCache) { Guard.NotNull(scriptEngine, nameof(scriptEngine)); Guard.NotNull(contentLoader, nameof(contentLoader)); + Guard.NotNull(localCache, nameof(localCache)); this.scriptEngine = scriptEngine; - this.contentLoader = contentLoader; + this.localCache = localCache; + } + + public (bool Match, ValueTask) Format(EnrichedEvent @event, object value, string[] path) + { + if (value is JsonArray array && + array.Count > 0 && + array[0] is JsonString s && + Guid.TryParse(s.Value, out var referenceId)) + { + return (true, GetReferenceValueAsync(referenceId, path)); + } + + return default; + } + + private async ValueTask GetReferenceValueAsync(Guid referenceId, string[] path) + { + var reference = await GetContentFromCacheAsync(referenceId); + + var (result, remaining) = RuleVariable.GetValue(reference, path); + + if (remaining.Length == 0) + { + return result?.ToString(); + } + + return default; + } + + private Task GetContentFromCacheAsync(Guid referenceId) + { + var cacheKey = $"FORMAT_REFERENCE_{referenceId}"; + + return localCache.GetOrCreate(cacheKey, () => contentLoader.GetAsync(referenceId)); } protected override async Task CreateEnrichedEventAsync(Envelope @event) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs index f934c86c8..40fa07190 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs @@ -14,6 +14,7 @@ using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Entities.Rules.Repositories; using Squidex.Domain.Apps.Events; using Squidex.Infrastructure; +using Squidex.Infrastructure.Caching; using Squidex.Infrastructure.EventSourcing; namespace Squidex.Domain.Apps.Entities.Rules @@ -24,6 +25,7 @@ namespace Squidex.Domain.Apps.Entities.Rules private readonly IRuleEventRepository ruleEventRepository; private readonly IAppProvider appProvider; private readonly IMemoryCache cache; + private readonly ILocalCache localCache; private readonly RuleService ruleService; public string Name @@ -36,18 +38,19 @@ namespace Squidex.Domain.Apps.Entities.Rules get { return ".*"; } } - public RuleEnqueuer(IAppProvider appProvider, IMemoryCache cache, IRuleEventRepository ruleEventRepository, + public RuleEnqueuer(IAppProvider appProvider, IMemoryCache cache, ILocalCache localCache, IRuleEventRepository ruleEventRepository, RuleService ruleService) { Guard.NotNull(appProvider, nameof(appProvider)); Guard.NotNull(cache, nameof(cache)); + Guard.NotNull(localCache, nameof(localCache)); Guard.NotNull(ruleEventRepository, nameof(ruleEventRepository)); Guard.NotNull(ruleService, nameof(ruleService)); this.appProvider = appProvider; this.cache = cache; - + this.localCache = localCache; this.ruleEventRepository = ruleEventRepository; this.ruleService = ruleService; } @@ -67,11 +70,14 @@ namespace Squidex.Domain.Apps.Entities.Rules Guard.NotNull(rule, nameof(rule)); Guard.NotNull(@event, nameof(@event)); - var jobs = await ruleService.CreateJobsAsync(rule, ruleId, @event); - - foreach (var job in jobs) + using (localCache.StartContext()) { - await ruleEventRepository.EnqueueAsync(job, job.Created); + var jobs = await ruleService.CreateJobsAsync(rule, ruleId, @event); + + foreach (var job in jobs) + { + await ruleEventRepository.EnqueueAsync(job, job.Created); + } } } diff --git a/backend/src/Squidex/Config/Domain/RuleServices.cs b/backend/src/Squidex/Config/Domain/RuleServices.cs index 7b4bab01a..250bc5600 100644 --- a/backend/src/Squidex/Config/Domain/RuleServices.cs +++ b/backend/src/Squidex/Config/Domain/RuleServices.cs @@ -44,7 +44,7 @@ namespace Squidex.Config.Domain .As(); services.AddSingletonAs() - .As(); + .As().As(); services.AddSingletonAs() .As(); @@ -73,6 +73,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + 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 e941a6c27..cb469f17f 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 @@ -7,7 +7,9 @@ using System; using System.Collections.Generic; +using System.Runtime.InteropServices; using System.Security.Claims; +using System.Threading.Tasks; using FakeItEasy; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; @@ -38,6 +40,26 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules private readonly Guid assetId = Guid.NewGuid(); private readonly RuleEventFormatter sut; + private class FakeContentResolver : IRuleEventFormatter + { + public (bool Match, ValueTask) Format(EnrichedEvent @event, object value, string[] path) + { + if (path[0] == "data" && value is JsonArray _) + { + return (true, GetValueAsync()); + } + + return default; + } + + private async ValueTask GetValueAsync() + { + await Task.Delay(5); + + return "Reference"; + } + } + public RuleEventFormatterTests() { A.CallTo(() => urlGenerator.ContentUI(appId, schemaId, contentId)) @@ -64,7 +86,13 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules var cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); - sut = new RuleEventFormatter(TestUtils.DefaultSerializer, urlGenerator, new JintScriptEngine(cache, extensions)); + var formatters = new IRuleEventFormatter[] + { + new PredefinedPatternsFormatter(urlGenerator), + new FakeContentResolver() + }; + + sut = new RuleEventFormatter(TestUtils.DefaultSerializer, formatters, new JintScriptEngine(cache, extensions)); } [Fact] @@ -99,11 +127,11 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [InlineData("Name $APP_NAME has id $APP_ID")] [InlineData("Name ${EVENT_APPID.NAME} has id ${EVENT_APPID.ID}")] [InlineData("Script(`Name ${event.appId.name} has id ${event.appId.id}`)")] - public void Should_format_app_information_from_event(string script) + public async Task Should_format_app_information_from_event(string script) { var @event = new EnrichedContentEvent { AppId = appId }; - var result = sut.Format(script, @event); + var result = await sut.FormatAsync(script, @event); Assert.Equal($"Name my-app has id {appId.Id}", result); } @@ -111,11 +139,11 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Theory] [InlineData("Name $SCHEMA_NAME has id $SCHEMA_ID")] [InlineData("Script(`Name ${event.schemaId.name} has id ${event.schemaId.id}`)")] - public void Should_format_schema_information_from_event(string script) + public async Task Should_format_schema_information_from_event(string script) { var @event = new EnrichedContentEvent { SchemaId = schemaId }; - var result = sut.Format(script, @event); + var result = await sut.FormatAsync(script, @event); Assert.Equal($"Name my-schema has id {schemaId.Id}", result); } @@ -123,11 +151,11 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Theory] [InlineData("Full: $TIMESTAMP_DATETIME")] [InlineData("Script(`Full: ${formatDate(event.timestamp, 'yyyy-MM-dd-hh-mm-ss')}`)")] - public void Should_format_timestamp_information_from_event(string script) + public async Task Should_format_timestamp_information_from_event(string script) { var @event = new EnrichedContentEvent { Timestamp = now }; - var result = sut.Format(script, @event); + var result = await sut.FormatAsync(script, @event); Assert.Equal($"Full: {now:yyyy-MM-dd-hh-mm-ss}", result); } @@ -135,11 +163,11 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Theory] [InlineData("Date: $TIMESTAMP_DATE")] [InlineData("Script(`Date: ${formatDate(event.timestamp, 'yyyy-MM-dd')}`)")] - public void Should_format_timestamp_date_information_from_event(string script) + public async Task Should_format_timestamp_date_information_from_event(string script) { var @event = new EnrichedContentEvent { Timestamp = now }; - var result = sut.Format(script, @event); + var result = await sut.FormatAsync(script, @event); Assert.Equal($"Date: {now:yyyy-MM-dd}", result); } @@ -148,11 +176,11 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [InlineData("From $MENTIONED_NAME ($MENTIONED_EMAIL, $MENTIONED_ID)")] [InlineData("From ${COMMENT_MENTIONEDUSER.NAME} (${COMMENT_MENTIONEDUSER.EMAIL}, ${COMMENT_MENTIONEDUSER.ID})")] [InlineData("Script(`From ${event.mentionedUser.name} (${event.mentionedUser.email}, ${event.mentionedUser.id})`)")] - public void Should_format_email_and_display_name_from_mentioned_user(string script) + public async Task Should_format_email_and_display_name_from_mentioned_user(string script) { var @event = new EnrichedCommentEvent { MentionedUser = user }; - var result = sut.Format(script, @event); + var result = await sut.FormatAsync(script, @event); Assert.Equal("From me (me@email.com, user123)", result); } @@ -160,11 +188,11 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Theory] [InlineData("From $USER_NAME ($USER_EMAIL, $USER_ID)")] [InlineData("Script(`From ${event.user.name} (${event.user.email}, ${event.user.id})`)")] - public void Should_format_email_and_display_name_from_user(string script) + public async Task Should_format_email_and_display_name_from_user(string script) { var @event = new EnrichedContentEvent { User = user }; - var result = sut.Format(script, @event); + var result = await sut.FormatAsync(script, @event); Assert.Equal("From me (me@email.com, user123)", result); } @@ -172,11 +200,11 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Theory] [InlineData("From $USER_NAME ($USER_EMAIL, $USER_ID)")] [InlineData("Script(`From ${event.user.name} (${event.user.email}, ${event.user.id})`)")] - public void Should_return_null_if_user_is_not_found(string script) + public async Task Should_return_null_if_user_is_not_found(string script) { var @event = new EnrichedContentEvent(); - var result = sut.Format(script, @event); + var result = await sut.FormatAsync(script, @event); Assert.Equal("From null (null, null)", result); } @@ -184,11 +212,11 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Theory] [InlineData("From $USER_NAME ($USER_EMAIL, $USER_ID)")] [InlineData("Script(`From ${event.user.name} (${event.user.email}, ${event.user.id})`)")] - public void Should_format_email_and_display_name_from_client(string script) + public async Task Should_format_email_and_display_name_from_client(string script) { var @event = new EnrichedContentEvent { User = new ClientUser(new RefToken(RefTokenType.Client, "android")) }; - var result = sut.Format(script, @event); + var result = await sut.FormatAsync(script, @event); Assert.Equal("From client:android (client:android, android)", result); } @@ -196,11 +224,11 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Theory] [InlineData("Version: $ASSET_VERSION")] [InlineData("Script(`Version: ${event.version}`)")] - public void Should_format_base_property(string script) + public async Task Should_format_base_property(string script) { var @event = new EnrichedAssetEvent { Version = 13 }; - var result = sut.Format(script, @event); + var result = await sut.FormatAsync(script, @event); Assert.Equal("Version: 13", result); } @@ -208,11 +236,11 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Theory] [InlineData("File: $ASSET_FILENAME")] [InlineData("Script(`File: ${event.fileName}`)")] - public void Should_format_asset_file_name_from_event(string script) + public async Task Should_format_asset_file_name_from_event(string script) { var @event = new EnrichedAssetEvent { FileName = "my-file.png" }; - var result = sut.Format(script, @event); + var result = await sut.FormatAsync(script, @event); Assert.Equal("File: my-file.png", result); } @@ -220,11 +248,11 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Theory] [InlineData("Type: $ASSET_ASSETTYPE")] [InlineData("Script(`Type: ${event.assetType}`)")] - public void Should_format_asset_asset_type_from_event(string script) + public async Task Should_format_asset_asset_type_from_event(string script) { var @event = new EnrichedAssetEvent { AssetType = AssetType.Audio }; - var result = sut.Format(script, @event); + var result = await sut.FormatAsync(script, @event); Assert.Equal("Type: Audio", result); } @@ -232,11 +260,11 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Theory] [InlineData("Download at $ASSET_CONTENT_URL")] [InlineData("Script(`Download at ${assetContentUrl()}`)")] - public void Should_format_asset_content_url_from_event(string script) + public async Task Should_format_asset_content_url_from_event(string script) { var @event = new EnrichedAssetEvent { Id = assetId }; - var result = sut.Format(script, @event); + var result = await sut.FormatAsync(script, @event); Assert.Equal("Download at asset-content-url", result); } @@ -244,11 +272,11 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Theory] [InlineData("Download at $ASSET_CONTENT_URL")] [InlineData("Script(`Download at ${assetContentUrl()}`)")] - public void Should_return_null_when_asset_content_url_not_found(string script) + public async Task Should_return_null_when_asset_content_url_not_found(string script) { var @event = new EnrichedContentEvent(); - var result = sut.Format(script, @event); + var result = await sut.FormatAsync(script, @event); Assert.Equal("Download at null", result); } @@ -256,11 +284,11 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Theory] [InlineData("Go to $CONTENT_URL")] [InlineData("Script(`Go to ${contentUrl()}`)")] - public void Should_format_content_url_from_event(string script) + public async Task Should_format_content_url_from_event(string script) { var @event = new EnrichedContentEvent { AppId = appId, Id = contentId, SchemaId = schemaId }; - var result = sut.Format(script, @event); + var result = await sut.FormatAsync(script, @event); Assert.Equal("Go to content-url", result); } @@ -268,11 +296,11 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Theory] [InlineData("Go to $CONTENT_URL")] [InlineData("Script(`Go to ${contentUrl()}`)")] - public void Should_return_null_when_content_url_when_not_found(string script) + public async Task Should_return_null_when_content_url_when_not_found(string script) { var @event = new EnrichedAssetEvent(); - var result = sut.Format(script, @event); + var result = await sut.FormatAsync(script, @event); Assert.Equal("Go to null", result); } @@ -281,11 +309,11 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [InlineData("$CONTENT_STATUS")] [InlineData("Script(contentAction())")] [InlineData("Script(`${event.status}`)")] - public void Should_format_content_status_when_found(string script) + public async Task Should_format_content_status_when_found(string script) { var @event = new EnrichedContentEvent { Status = Status.Published }; - var result = sut.Format(script, @event); + var result = await sut.FormatAsync(script, @event); Assert.Equal("Published", result); } @@ -293,11 +321,11 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Theory] [InlineData("$CONTENT_ACTION")] [InlineData("Script(contentAction())")] - public void Should_return_null_when_content_status_not_found(string script) + public async Task Should_return_null_when_content_status_not_found(string script) { var @event = new EnrichedAssetEvent(); - var result = sut.Format(script, @event); + var result = await sut.FormatAsync(script, @event); Assert.Equal("null", result); } @@ -305,11 +333,11 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Theory] [InlineData("$CONTENT_ACTION")] [InlineData("Script(`${event.type}`)")] - public void Should_format_content_actions_when_found(string script) + public async Task Should_format_content_actions_when_found(string script) { var @event = new EnrichedContentEvent { Type = EnrichedContentEventType.Created }; - var result = sut.Format(script, @event); + var result = await sut.FormatAsync(script, @event); Assert.Equal("Created", result); } @@ -317,11 +345,11 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Theory] [InlineData("$CONTENT_ACTION")] [InlineData("Script(contentAction())")] - public void Should_return_null_when_content_action_not_found(string script) + public async Task Should_return_null_when_content_action_not_found(string script) { var @event = new EnrichedAssetEvent(); - var result = sut.Format(script, @event); + var result = await sut.FormatAsync(script, @event); Assert.Equal("null", result); } @@ -329,7 +357,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Theory] [InlineData("$CONTENT_DATA.country.iv")] [InlineData("Script(`${event.data.country.iv}`)")] - public void Should_return_null_when_field_not_found(string script) + public async Task Should_return_null_when_field_not_found(string script) { var @event = new EnrichedContentEvent { @@ -340,7 +368,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules .AddValue("iv", "Berlin")) }; - var result = sut.Format(script, @event); + var result = await sut.FormatAsync(script, @event); Assert.Equal("null", result); } @@ -348,7 +376,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Theory] [InlineData("$CONTENT_DATA.city.de")] [InlineData("Script(`${event.data.country.iv}`)")] - public void Should_return_null_when_partition_not_found(string script) + public async Task Should_return_null_when_partition_not_found(string script) { var @event = new EnrichedContentEvent { @@ -359,7 +387,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules .AddValue("iv", "Berlin")) }; - var result = sut.Format(script, @event); + var result = await sut.FormatAsync(script, @event); Assert.Equal("null", result); } @@ -367,7 +395,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [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) + public async Task Should_return_null_when_array_item_not_found(string script) { var @event = new EnrichedContentEvent { @@ -378,7 +406,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules .AddJsonValue(JsonValue.Array())) }; - var result = sut.Format(script, @event); + var result = await sut.FormatAsync(script, @event); Assert.Equal("null", result); } @@ -386,7 +414,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [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) + public async Task Should_return_null_when_property_not_found(string script) { var @event = new EnrichedContentEvent { @@ -397,7 +425,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules .AddJsonValue(JsonValue.Object().Add("name", "Berlin"))) }; - var result = sut.Format(script, @event); + var result = await sut.FormatAsync(script, @event); Assert.Equal("null", result); } @@ -405,7 +433,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Theory] [InlineData("$CONTENT_DATA.city.iv")] [InlineData("Script(`${event.data.city.iv}`)")] - public void Should_return_plain_value_when_found(string script) + public async Task Should_return_plain_value_when_found(string script) { var @event = new EnrichedContentEvent { @@ -416,7 +444,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules .AddValue("iv", "Berlin")) }; - var result = sut.Format(script, @event); + var result = await sut.FormatAsync(script, @event); Assert.Equal("Berlin", result); } @@ -424,7 +452,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [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) + public async Task Should_return_plain_value_from_array_when_found(string script) { var @event = new EnrichedContentEvent { @@ -435,7 +463,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules .AddJsonValue(JsonValue.Array("Berlin"))) }; - var result = sut.Format(script, @event); + var result = await sut.FormatAsync(script, @event); Assert.Equal("Berlin", result); } @@ -443,7 +471,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [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) + public async Task Should_return_plain_value_from_object_when_found(string script) { var @event = new EnrichedContentEvent { @@ -454,7 +482,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules .AddJsonValue(JsonValue.Object().Add("name", "Berlin"))) }; - var result = sut.Format(script, @event); + var result = await sut.FormatAsync(script, @event); Assert.Equal("Berlin", result); } @@ -462,7 +490,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Theory] [InlineData("$CONTENT_DATA.city.iv")] [InlineData("Script(`${JSON.stringify(event.data.city.iv)}`)")] - public void Should_return_json_string_when_object(string script) + public async Task Should_return_json_string_when_object(string script) { var @event = new EnrichedContentEvent { @@ -473,40 +501,57 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules .AddJsonValue(JsonValue.Object().Add("name", "Berlin"))) }; - var result = sut.Format(script, @event); + var result = await sut.FormatAsync(script, @event); Assert.Equal("{\"name\":\"Berlin\"}", result); } + [Fact] + public async Task Should_resolve_reference() + { + var @event = new EnrichedContentEvent + { + Data = + new NamedContentData() + .AddField("city", + new ContentFieldData() + .AddJsonValue(JsonValue.Array())) + }; + + var result = await sut.FormatAsync("${CONTENT_DATA.city.iv.data.name}", @event); + + Assert.Equal("Reference", result); + } + [Theory] [InlineData("Script(`From ${event.actor}`)")] - public void Should_format_actor(string script) + public async Task Should_format_actor(string script) { var @event = new EnrichedContentEvent { Actor = new RefToken(RefTokenType.Client, "android") }; - var result = sut.Format(script, @event); + var result = await sut.FormatAsync(script, @event); Assert.Equal("From client:android", result); } [Theory] [InlineData("${EVENT_INVALID ? file}", "file")] - public void Should_provide_fallback_if_path_is_invalid(string script, string expect) + public async Task Should_provide_fallback_if_path_is_invalid(string script, string expect) { var @event = new EnrichedAssetEvent { FileName = null! }; - var result = sut.Format(script, @event); + var result = await sut.FormatAsync(script, @event); Assert.Equal(expect, result); } [Theory] [InlineData("${ASSET_FILENAME ? file}", "file")] - public void Should_provide_fallback_if_value_is_null(string script, string expect) + public async Task Should_provide_fallback_if_value_is_null(string script, string expect) { var @event = new EnrichedAssetEvent { FileName = null! }; - var result = sut.Format(script, @event); + var result = await sut.FormatAsync(script, @event); Assert.Equal(expect, result); } @@ -515,11 +560,11 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [InlineData("Found in ${ASSET_FILENAME | Upper}.docx", "Found in DONALD DUCK.docx")] [InlineData("Found in ${ASSET_FILENAME| Upper }.docx", "Found in DONALD DUCK.docx")] [InlineData("Found in ${ASSET_FILENAME|Upper }.docx", "Found in DONALD DUCK.docx")] - public void Should_transform_replacements_and_igore_whitepsaces(string script, string expect) + public async Task Should_transform_replacements_and_igore_whitepsaces(string script, string expect) { var @event = new EnrichedAssetEvent { FileName = "Donald Duck" }; - var result = sut.Format(script, @event); + var result = await sut.FormatAsync(script, @event); Assert.Equal(expect, result); } @@ -531,11 +576,11 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [InlineData("Found in ${ASSET_FILENAME | Lower}.docx", "Found in donald duck.docx", "Donald Duck")] [InlineData("Found in ${ASSET_FILENAME | Slugify}.docx", "Found in donald-duck.docx", "Donald Duck")] [InlineData("Found in ${ASSET_FILENAME | Trim}.docx", "Found in Donald Duck.docx", "Donald Duck ")] - public void Should_transform_replacements(string script, string expect, string name) + public async Task Should_transform_replacements(string script, string expect, string name) { var @event = new EnrichedAssetEvent { FileName = name }; - var result = sut.Format(script, @event); + var result = await sut.FormatAsync(script, @event); Assert.Equal(expect, result); } @@ -547,14 +592,14 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [InlineData("From ${USER_NAME | Lower}", "From donald duck", "Donald Duck")] [InlineData("From ${USER_NAME | Slugify}", "From donald-duck", "Donald Duck")] [InlineData("From ${USER_NAME | Trim}", "From Donald Duck", "Donald Duck ")] - public void Should_transform_replacements_with_simple_pattern(string script, string expect, string name) + public async Task Should_transform_replacements_with_simple_pattern(string script, string expect, string name) { var @event = new EnrichedContentEvent { User = user }; A.CallTo(() => user.Claims) .Returns(new List { new Claim(SquidexClaimTypes.DisplayName, name) }); - var result = sut.Format(script, @event); + var result = await sut.FormatAsync(script, @event); Assert.Equal(expect, result); } @@ -562,51 +607,63 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Theory] [InlineData("{'Key':'${ASSET_FILENAME | Upper}'}", "{'Key':'DONALD DUCK'}")] [InlineData("{'Key':'${ASSET_FILENAME}'}", "{'Key':'Donald Duck'}")] - public void Should_transform_json_examples(string script, string expect) + public async Task Should_transform_json_examples(string script, string expect) { var @event = new EnrichedAssetEvent { FileName = "Donald Duck" }; - var result = sut.Format(script, @event); + var result = await sut.FormatAsync(script, @event); + + Assert.Equal(expect, result); + } + + [Theory] + [InlineData("${ASSET_LASTMODIFIED | timestamp_seconds}", "1590769584")] + [InlineData("${ASSET_LASTMODIFIED | timestamp_ms}", "1590769584000")] + public async Task Should_transform_timestamp(string script, string expect) + { + var @event = new EnrichedAssetEvent { LastModified = Instant.FromUnixTimeSeconds(1590769584) }; + + var result = await sut.FormatAsync(script, @event); Assert.Equal(expect, result); } [Fact] - public void Should_format_json() + public async Task Should_format_json() { var @event = new EnrichedContentEvent { Actor = new RefToken(RefTokenType.Client, "android") }; - var result = sut.Format("Script(JSON.stringify({ actor: event.actor.toString() }))", @event); + var result = await sut.FormatAsync("Script(JSON.stringify({ actor: event.actor.toString() }))", @event); Assert.Equal("{\"actor\":\"client:android\"}", result); } [Fact] - public void Should_format_json_with_special_characters() + public async Task Should_format_json_with_special_characters() { var @event = new EnrichedContentEvent { Actor = new RefToken(RefTokenType.Client, "mobile\"android") }; - var result = sut.Format("Script(JSON.stringify({ actor: event.actor.toString() }))", @event); + var result = await sut.FormatAsync("Script(JSON.stringify({ actor: event.actor.toString() }))", @event); Assert.Equal("{\"actor\":\"client:mobile\\\"android\"}", result); } [Fact] - public void Should_evaluate_script_if_starting_with_whitespace() + public async Task Should_evaluate_script_if_starting_with_whitespace() { var @event = new EnrichedContentEvent { Type = EnrichedContentEventType.Created }; - var result = sut.Format(" Script(`${event.type}`)", @event); + var result = await sut.FormatAsync(" Script(`${event.type}`)", @event); Assert.Equal("Created", result); } [Fact] - public void Should_evaluate_script_if_ends_with_whitespace() + public async Task Should_evaluate_script_if_ends_with_whitespace() { var @event = new EnrichedContentEvent { Type = EnrichedContentEventType.Created }; - var result = sut.Format("Script(`${event.type}`) ", @event); + var result = await sut.FormatAsync("Script(`${event.type}`) ", @event); Assert.Equal("Created", result); } 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 29a41aaa0..376aa73f7 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs @@ -20,7 +20,9 @@ using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Assets; using Squidex.Domain.Apps.Events.Contents; using Squidex.Infrastructure; +using Squidex.Infrastructure.Caching; using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Json.Objects; using Xunit; #pragma warning disable SA1401 // Fields must be private @@ -31,8 +33,10 @@ namespace Squidex.Domain.Apps.Entities.Contents public class ContentChangedTriggerHandlerTests { private readonly IScriptEngine scriptEngine = A.Fake(); + private readonly ILocalCache localCache = new AsyncLocalCache(); private readonly IContentLoader contentLoader = A.Fake(); - private readonly IRuleTriggerHandler sut; + private readonly ContentChangedTriggerHandler sut; + private readonly IRuleTriggerHandler handler; private readonly Guid ruleId = Guid.NewGuid(); private static readonly NamedId SchemaMatch = NamedId.Of(Guid.NewGuid(), "my-schema1"); private static readonly NamedId SchemaNonMatch = NamedId.Of(Guid.NewGuid(), "my-schema2"); @@ -45,7 +49,9 @@ namespace Squidex.Domain.Apps.Entities.Contents A.CallTo(() => scriptEngine.Evaluate(A._, "false")) .Returns(false); - sut = new ContentChangedTriggerHandler(scriptEngine, contentLoader); + sut = new ContentChangedTriggerHandler(scriptEngine, contentLoader, localCache); + + handler = sut; } public static IEnumerable TestEvents() @@ -58,6 +64,58 @@ namespace Squidex.Domain.Apps.Entities.Contents yield return new object[] { new ContentStatusChanged { Change = StatusChange.Unpublished }, EnrichedContentEventType.Unpublished }; } + [Fact] + public async Task Should_resolve_reference_if_value_from_content_loader() + { + var referenceId = Guid.NewGuid(); + var referenceValue = JsonValue.Array(referenceId); + + SetupReference(referenceId); + + var (handled, result) = sut.Format(null!, referenceValue, new[] { "data", "field1", "iv" }); + + Assert.True(handled); + Assert.Equal("Hello", await result); + } + + [Fact] + public async Task Should_resolve_reference_only_once() + { + using (localCache.StartContext()) + { + var referenceId = Guid.NewGuid(); + var referenceValue = JsonValue.Array(referenceId); + + SetupReference(referenceId); + + var (handled1, result1) = sut.Format(null!, referenceValue, new[] { "data", "field1", "iv" }); + var (handled2, result2) = sut.Format(null!, referenceValue, new[] { "data", "field2", "iv" }); + + Assert.True(handled1); + Assert.Equal("Hello", await result1); + + Assert.True(handled2); + Assert.Equal("World", await result2); + + A.CallTo(() => contentLoader.GetAsync(A._, A._)) + .MustHaveHappenedOnceExactly(); + } + } + + [Fact] + public async Task Should_not_return_value_if_path_not_found_in_reference() + { + var referenceId = Guid.NewGuid(); + var referenceValue = JsonValue.Array(referenceId); + + SetupReference(referenceId); + + var (handled, result) = sut.Format(null!, referenceValue, new[] { "data", "invalid", "iv" }); + + Assert.True(handled); + Assert.Null(await result); + } + [Theory] [MemberData(nameof(TestEvents))] public async Task Should_create_enriched_events(ContentEvent @event, EnrichedContentEventType type) @@ -67,7 +125,7 @@ namespace Squidex.Domain.Apps.Entities.Contents A.CallTo(() => contentLoader.GetAsync(@event.ContentId, 12)) .Returns(new ContentEntity { SchemaId = SchemaMatch }); - var result = await sut.CreateEnrichedEventsAsync(envelope); + var result = await handler.CreateEnrichedEventsAsync(envelope); var enrichedEvent = result.Single() as EnrichedContentEvent; @@ -90,7 +148,7 @@ namespace Squidex.Domain.Apps.Entities.Contents A.CallTo(() => contentLoader.GetAsync(@event.ContentId, 11)) .Returns(new ContentEntity { SchemaId = SchemaMatch, Version = 11, Data = dataOld }); - var result = await sut.CreateEnrichedEventsAsync(envelope); + var result = await handler.CreateEnrichedEventsAsync(envelope); var enrichedEvent = result.Single() as EnrichedContentEvent; @@ -103,7 +161,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { TestForTrigger(handleAll: true, schemaId: null, condition: null, action: trigger => { - var result = sut.Trigger(new AssetCreated(), trigger, ruleId); + var result = handler.Trigger(new AssetCreated(), trigger, ruleId); Assert.False(result); }); @@ -114,7 +172,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { TestForTrigger(handleAll: false, schemaId: null, condition: null, action: trigger => { - var result = sut.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger, ruleId); + var result = handler.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger, ruleId); Assert.False(result); }); @@ -125,7 +183,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { TestForTrigger(handleAll: true, schemaId: SchemaMatch, condition: null, action: trigger => { - var result = sut.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger, ruleId); + var result = handler.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger, ruleId); Assert.True(result); }); @@ -136,7 +194,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: string.Empty, action: trigger => { - var result = sut.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger, ruleId); + var result = handler.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger, ruleId); Assert.True(result); }); @@ -147,7 +205,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { TestForTrigger(handleAll: false, schemaId: SchemaNonMatch, condition: null, action: trigger => { - var result = sut.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger, ruleId); + var result = handler.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger, ruleId); Assert.False(result); }); @@ -158,7 +216,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { TestForTrigger(handleAll: true, schemaId: null, condition: null, action: trigger => { - var result = sut.Trigger(new EnrichedAssetEvent(), trigger); + var result = handler.Trigger(new EnrichedAssetEvent(), trigger); Assert.False(result); }); @@ -169,7 +227,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { TestForTrigger(handleAll: false, schemaId: null, condition: null, action: trigger => { - var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); + var result = handler.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); Assert.False(result); }); @@ -180,7 +238,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { TestForTrigger(handleAll: true, schemaId: SchemaMatch, condition: null, action: trigger => { - var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); + var result = handler.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); Assert.True(result); }); @@ -191,7 +249,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: string.Empty, action: trigger => { - var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); + var result = handler.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); Assert.True(result); }); @@ -202,7 +260,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: "true", action: trigger => { - var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); + var result = handler.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); Assert.True(result); }); @@ -213,7 +271,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { TestForTrigger(handleAll: false, schemaId: SchemaNonMatch, condition: null, action: trigger => { - var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); + var result = handler.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); Assert.False(result); }); @@ -224,7 +282,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: "false", action: trigger => { - var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); + var result = handler.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); Assert.False(result); }); @@ -258,5 +316,21 @@ namespace Squidex.Domain.Apps.Entities.Contents .MustHaveHappened(); } } + + private void SetupReference(Guid referenceId) + { + A.CallTo(() => contentLoader.GetAsync(referenceId, EtagVersion.Any)) + .Returns(new ContentEntity + { + Data = + new NamedContentData() + .AddField("field1", + new ContentFieldData() + .AddJsonValue(JsonValue.Create("Hello"))) + .AddField("field2", + new ContentFieldData() + .AddJsonValue(JsonValue.Create("World"))) + }); + } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs index c008c9650..36f1195c3 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs @@ -18,6 +18,7 @@ using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Domain.Apps.Entities.Rules.Repositories; using Squidex.Domain.Apps.Events.Contents; using Squidex.Infrastructure; +using Squidex.Infrastructure.Caching; using Squidex.Infrastructure.EventSourcing; using Xunit; @@ -27,6 +28,7 @@ namespace Squidex.Domain.Apps.Entities.Rules { private readonly IAppProvider appProvider = A.Fake(); private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); + private readonly ILocalCache localCache = A.Fake(); private readonly IRuleEventRepository ruleEventRepository = A.Fake(); private readonly Instant now = SystemClock.Instance.GetCurrentInstant(); private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); @@ -43,6 +45,7 @@ namespace Squidex.Domain.Apps.Entities.Rules sut = new RuleEnqueuer( appProvider, cache, + localCache, ruleEventRepository, ruleService); } @@ -81,6 +84,9 @@ namespace Squidex.Domain.Apps.Entities.Rules A.CallTo(() => ruleEventRepository.EnqueueAsync(job, now, default)) .MustHaveHappened(); + + A.CallTo(() => localCache.StartContext()) + .MustHaveHappened(); } [Fact]