Browse Source

Feature/async formatting2 (#528)

* Make everything async ready.

* Testing the extension interface and extracting predefined rules.

* Resolve reference.

* Do not check for data.

* Timestamp transform.
pull/531/head
Sebastian Stehle 6 years ago
committed by GitHub
parent
commit
80bb0642a8
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs
  2. 4
      backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueActionHandler.cs
  3. 4
      backend/extensions/Squidex.Extensions/Actions/Comment/CommentActionHandler.cs
  4. 6
      backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs
  5. 4
      backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs
  6. 12
      backend/extensions/Squidex.Extensions/Actions/Email/EmailActionHandler.cs
  7. 12
      backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaActionHandler.cs
  8. 2
      backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducer.cs
  9. 14
      backend/extensions/Squidex.Extensions/Actions/Medium/MediumActionHandler.cs
  10. 4
      backend/extensions/Squidex.Extensions/Actions/Notification/NotificationActionHandler.cs
  11. 4
      backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderActionHandler.cs
  12. 4
      backend/extensions/Squidex.Extensions/Actions/Slack/SlackActionHandler.cs
  13. 4
      backend/extensions/Squidex.Extensions/Actions/Twitter/TweetActionHandler.cs
  14. 8
      backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs
  15. 25
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleEventFormatter.cs
  16. 197
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/PredefinedPatternsFormatter.cs
  17. 8
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs
  18. 425
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs
  19. 101
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleVariable.cs
  20. 44
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs
  21. 18
      backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs
  22. 5
      backend/src/Squidex/Config/Domain/RuleServices.cs
  23. 211
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs
  24. 106
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs
  25. 6
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs

7
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
{

4
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

4
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;

6
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<string, object>
{
["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);

4
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,

12
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}";

12
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<string, string> ParseHeaders(string headers, EnrichedEvent @event)
private async Task<Dictionary<string, string>> 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;
}

2
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;

14
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<string[]> 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<string[]>(jsonTags);
}

4
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))
{

4
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);

4
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
{

4
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
};

8
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
};

25
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<string?>) Format(EnrichedEvent @event, object value, string[] path)
{
return default;
}
}
}

197
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<EnrichedEvent, string?> Replacer)> patterns = new List<(string Pattern, Func<EnrichedEvent, string?> 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<EnrichedEvent, string?> 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;
}
}
}

8
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<string?> 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<string?> 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)

425
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(@"^(?<FullPath>(?<Type>[^_]*)_(?<Path>[^\s]*))", RegexOptions.Compiled);
private static readonly Regex RegexPatternNew = new Regex(@"^\{(?<FullPath>(?<Type>[\w]+)_(?<Path>[\w\.\-]+))[\s]*(\|[\s]*(?<Transform>[^\?}]+))?(\?[\s]*(?<Fallback>[^\}\s]+))?[\s]*\}", RegexOptions.Compiled);
private readonly List<(string Pattern, Func<EnrichedEvent, string?> Replacer)> patterns = new List<(string Pattern, Func<EnrichedEvent, string?> Replacer)>();
private readonly IJsonSerializer jsonSerializer;
private readonly IUrlGenerator urlGenerator;
private readonly IEnumerable<IRuleEventFormatter> 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<string?> 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<string?> 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<IRuleEventFormatter> 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<EnrichedEvent, string?> generator)
{
patterns.Add((placeholder, generator));
}
public virtual string ToPayload<T>(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<string?> 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<TextPart> 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<TextPart> BuildParts(string text, EnrichedEvent @event)
{
var parts = new List<TextPart>();
var parts = new List<(int Offset, int Length, ValueTask<string?> 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<string?>(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<string?> 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<string?>(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<string?> 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<string?>(result?.ToString()));
}
return current?.ToString();
return (match.Length, default);
}
private static bool TryGetScript(string text, out string script)

101
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());
}
}
}

44
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<ContentChangedTriggerV2, ContentEvent, EnrichedContentEvent>
public sealed class ContentChangedTriggerHandler : RuleTriggerHandler<ContentChangedTriggerV2, ContentEvent, EnrichedContentEvent>, 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<string?>) 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<string?> 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<IContentEntity> GetContentFromCacheAsync(Guid referenceId)
{
var cacheKey = $"FORMAT_REFERENCE_{referenceId}";
return localCache.GetOrCreate(cacheKey, () => contentLoader.GetAsync(referenceId));
}
protected override async Task<EnrichedContentEvent?> CreateEnrichedEventAsync(Envelope<ContentEvent> @event)

18
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);
}
}
}

5
backend/src/Squidex/Config/Domain/RuleServices.cs

@ -44,7 +44,7 @@ namespace Squidex.Config.Domain
.As<IRuleTriggerHandler>();
services.AddSingletonAs<ContentChangedTriggerHandler>()
.As<IRuleTriggerHandler>();
.As<IRuleTriggerHandler>().As<IRuleEventFormatter>();
services.AddSingletonAs<ManualTriggerHandler>()
.As<IRuleTriggerHandler>();
@ -73,6 +73,9 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<EventScriptExtension>()
.As<IScriptExtension>();
services.AddSingletonAs<PredefinedPatternsFormatter>()
.As<IRuleEventFormatter>();
services.AddSingletonAs<RuleEventFormatter>()
.AsSelf();

211
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<string?>) Format(EnrichedEvent @event, object value, string[] path)
{
if (path[0] == "data" && value is JsonArray _)
{
return (true, GetValueAsync());
}
return default;
}
private async ValueTask<string?> 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<Claim> { 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);
}

106
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<IScriptEngine>();
private readonly ILocalCache localCache = new AsyncLocalCache();
private readonly IContentLoader contentLoader = A.Fake<IContentLoader>();
private readonly IRuleTriggerHandler sut;
private readonly ContentChangedTriggerHandler sut;
private readonly IRuleTriggerHandler handler;
private readonly Guid ruleId = Guid.NewGuid();
private static readonly NamedId<Guid> SchemaMatch = NamedId.Of(Guid.NewGuid(), "my-schema1");
private static readonly NamedId<Guid> SchemaNonMatch = NamedId.Of(Guid.NewGuid(), "my-schema2");
@ -45,7 +49,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, "false"))
.Returns(false);
sut = new ContentChangedTriggerHandler(scriptEngine, contentLoader);
sut = new ContentChangedTriggerHandler(scriptEngine, contentLoader, localCache);
handler = sut;
}
public static IEnumerable<object[]> 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<Guid>._, A<long>._))
.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")))
});
}
}
}

6
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<IAppProvider>();
private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
private readonly ILocalCache localCache = A.Fake<ILocalCache>();
private readonly IRuleEventRepository ruleEventRepository = A.Fake<IRuleEventRepository>();
private readonly Instant now = SystemClock.Instance.GetCurrentInstant();
private readonly NamedId<Guid> 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]

Loading…
Cancel
Save