From 6b6ef14d0c4262e26350f330ad566da60d300263 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 28 Apr 2018 14:28:32 +0200 Subject: [PATCH] Rule formatter improved. --- .../HandleRules/IRuleUrlGenerator.cs | 17 +++ .../HandleRules/RuleEventFormatter.cs | 76 ++++++++++- ...Squidex.Domain.Apps.Core.Operations.csproj | 1 + src/Squidex.Shared/Squidex.Shared.csproj | 3 + .../Users}/UserExtensions.cs | 3 +- src/Squidex/Config/Domain/EntitiesServices.cs | 6 +- ...GraphQLUrlGenerator.cs => UrlGenerator.cs} | 12 +- src/Squidex/app/features/rules/module.ts | 2 +- .../HandleRules/RuleEventFormatterTests.cs | 123 +++++++++++++----- 9 files changed, 206 insertions(+), 37 deletions(-) create mode 100644 src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleUrlGenerator.cs rename src/{Squidex.Domain.Users => Squidex.Shared/Users}/UserExtensions.cs (98%) rename src/Squidex/Pipeline/{GraphQLUrlGenerator.cs => UrlGenerator.cs} (79%) diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleUrlGenerator.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleUrlGenerator.cs new file mode 100644 index 000000000..51698c4b9 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleUrlGenerator.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public interface IRuleUrlGenerator + { + string GenerateContentUIUrl(NamedId appId, NamedId schemaId, Guid contentId); + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs index 0e64960f9..3a4587954 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs @@ -5,9 +5,11 @@ // All rights reserved. Licensed under the MIT license. // =========================================-================================= +using System; using System.Globalization; using System.Text; using System.Text.RegularExpressions; +using Microsoft.Extensions.Caching.Memory; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Squidex.Domain.Apps.Core.Contents; @@ -15,6 +17,7 @@ using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Contents; using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; +using Squidex.Shared.Users; namespace Squidex.Domain.Apps.Core.HandleRules { @@ -28,14 +31,27 @@ namespace Squidex.Domain.Apps.Core.HandleRules private const string TimestampDatePlaceholder = "$TIMESTAMP_DATE"; private const string TimestampDateTimePlaceholder = "$TIMESTAMP_DATETIME"; private const string ContentActionPlaceholder = "$CONTENT_ACTION"; + private const string ContentUrlPlaceholder = "$CONTENT_URL"; + private const string UserNamePlaceholder = "$USER_NAME"; + private const string UserEmailPlaceholder = "$USER_EMAIL"; private static readonly Regex ContentDataPlaceholder = new Regex(@"\$CONTENT_DATA(\.([0-9A-Za-z\-_]*)){2,}", RegexOptions.Compiled); + private static readonly TimeSpan UserCacheDuration = TimeSpan.FromMinutes(10); private readonly JsonSerializer serializer; + private readonly IRuleUrlGenerator urlGenerator; + private readonly IMemoryCache memoryCache; + private readonly IUserResolver userResolver; - public RuleEventFormatter(JsonSerializer serializer) + public RuleEventFormatter(JsonSerializer serializer, IRuleUrlGenerator urlGenerator, IMemoryCache memoryCache, IUserResolver userResolver) { + Guard.NotNull(memoryCache, nameof(memoryCache)); Guard.NotNull(serializer, nameof(serializer)); + Guard.NotNull(urlGenerator, nameof(urlGenerator)); + Guard.NotNull(userResolver, nameof(userResolver)); + this.memoryCache = memoryCache; this.serializer = serializer; + this.userResolver = userResolver; + this.urlGenerator = urlGenerator; } public virtual JToken ToRouteData(object value) @@ -75,6 +91,12 @@ namespace Squidex.Domain.Apps.Core.HandleRules sb.Replace(SchemaNamePlaceholder, schemaEvent.SchemaId.Name); } + if (@event.Payload is ContentEvent contentEvent) + { + sb.Replace(ContentUrlPlaceholder, urlGenerator.GenerateContentUIUrl(@event.Payload.AppId, contentEvent.SchemaId, contentEvent.ContentId)); + } + + FormatUserInfo(@event, sb); FormatContentAction(@event, sb); var result = sb.ToString(); @@ -92,6 +114,39 @@ namespace Squidex.Domain.Apps.Core.HandleRules return result; } + private void FormatUserInfo(Envelope @event, StringBuilder sb) + { + var text = sb.ToString(); + + if (text.Contains(UserEmailPlaceholder) || text.Contains(UserNamePlaceholder)) + { + var actor = @event.Payload.Actor; + + if (actor.Type.Equals("client", StringComparison.OrdinalIgnoreCase)) + { + var displayText = actor.ToString(); + + sb.Replace(UserEmailPlaceholder, displayText); + sb.Replace(UserNamePlaceholder, displayText); + } + else + { + var user = FindUser(actor); + + if (user != null) + { + sb.Replace(UserEmailPlaceholder, user.Email); + sb.Replace(UserNamePlaceholder, user.DisplayName()); + } + else + { + sb.Replace(UserEmailPlaceholder, Undefined); + sb.Replace(UserNamePlaceholder, Undefined); + } + } + } + } + private static void FormatContentAction(Envelope @event, StringBuilder sb) { switch (@event.Payload) @@ -166,5 +221,24 @@ namespace Squidex.Domain.Apps.Core.HandleRules return value?.ToString(Formatting.Indented) ?? Undefined; }); } + + private IUser FindUser(RefToken actor) + { + var key = $"RuleEventFormatter_Users_${actor.Identifier}"; + + return memoryCache.GetOrCreate(key, x => + { + x.AbsoluteExpirationRelativeToNow = UserCacheDuration; + + try + { + return userResolver.FindByIdOrEmailAsync(actor.Identifier).Result; + } + catch + { + return null; + } + }); + } } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj b/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj index ab5251d93..2178c9023 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj +++ b/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Squidex.Shared/Squidex.Shared.csproj b/src/Squidex.Shared/Squidex.Shared.csproj index 244e5b12e..de495fab0 100644 --- a/src/Squidex.Shared/Squidex.Shared.csproj +++ b/src/Squidex.Shared/Squidex.Shared.csproj @@ -17,4 +17,7 @@ + + + \ No newline at end of file diff --git a/src/Squidex.Domain.Users/UserExtensions.cs b/src/Squidex.Shared/Users/UserExtensions.cs similarity index 98% rename from src/Squidex.Domain.Users/UserExtensions.cs rename to src/Squidex.Shared/Users/UserExtensions.cs index 1884648ce..42a5b8611 100644 --- a/src/Squidex.Domain.Users/UserExtensions.cs +++ b/src/Squidex.Shared/Users/UserExtensions.cs @@ -9,9 +9,8 @@ using System; using System.Linq; using Squidex.Infrastructure; using Squidex.Shared.Identity; -using Squidex.Shared.Users; -namespace Squidex.Domain.Users +namespace Squidex.Shared.Users { public static class UserExtensions { diff --git a/src/Squidex/Config/Domain/EntitiesServices.cs b/src/Squidex/Config/Domain/EntitiesServices.cs index 6c6b98f40..689991790 100644 --- a/src/Squidex/Config/Domain/EntitiesServices.cs +++ b/src/Squidex/Config/Domain/EntitiesServices.cs @@ -13,6 +13,7 @@ using Migrate_01; using Migrate_01.Migrations; using Orleans; using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Apps; @@ -42,11 +43,12 @@ namespace Squidex.Config.Domain { var exposeSourceUrl = config.GetOptionalValue("assetStore:exposeSourceUrl", true); - services.AddSingletonAs(c => new GraphQLUrlGenerator( + services.AddSingletonAs(c => new UrlGenerator( c.GetRequiredService>(), c.GetRequiredService(), exposeSourceUrl)) - .As(); + .As() + .As(); services.AddSingletonAs() .As(); diff --git a/src/Squidex/Pipeline/GraphQLUrlGenerator.cs b/src/Squidex/Pipeline/UrlGenerator.cs similarity index 79% rename from src/Squidex/Pipeline/GraphQLUrlGenerator.cs rename to src/Squidex/Pipeline/UrlGenerator.cs index 2908dc215..48249b15b 100644 --- a/src/Squidex/Pipeline/GraphQLUrlGenerator.cs +++ b/src/Squidex/Pipeline/UrlGenerator.cs @@ -5,25 +5,28 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using Microsoft.Extensions.Options; using Squidex.Config; +using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents.GraphQL; using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; using Squidex.Infrastructure.Assets; namespace Squidex.Pipeline { - public sealed class GraphQLUrlGenerator : IGraphQLUrlGenerator + public sealed class UrlGenerator : IGraphQLUrlGenerator, IRuleUrlGenerator { private readonly IAssetStore assetStore; private readonly MyUrlsOptions urlsOptions; public bool CanGenerateAssetSourceUrl { get; } - public GraphQLUrlGenerator(IOptions urlsOptions, IAssetStore assetStore, bool allowAssetSourceUrl) + public UrlGenerator(IOptions urlsOptions, IAssetStore assetStore, bool allowAssetSourceUrl) { this.assetStore = assetStore; this.urlsOptions = urlsOptions.Value; @@ -51,6 +54,11 @@ namespace Squidex.Pipeline return urlsOptions.BuildUrl($"api/content/{app.Name}/{schema.Name}/{content.Id}"); } + public string GenerateContentUIUrl(NamedId appId, NamedId schemaId, Guid contentId) + { + return urlsOptions.BuildUrl($"app/{appId.Name}/content/{schemaId.Name}/{contentId}/history"); + } + public string GenerateAssetSourceUrl(IAppEntity app, IAssetEntity asset) { return assetStore.GenerateSourceUrl(asset.Id.ToString(), asset.FileVersion, null); diff --git a/src/Squidex/app/features/rules/module.ts b/src/Squidex/app/features/rules/module.ts index 43d47233d..d7ab32982 100644 --- a/src/Squidex/app/features/rules/module.ts +++ b/src/Squidex/app/features/rules/module.ts @@ -42,7 +42,7 @@ const routes: Routes = [ path: 'help', component: HelpComponent, data: { - helpPage: '06-integrated/rules' + helpPage: '05-integrated/rules' } } ] diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs index c8addbb71..8f314e5aa 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs @@ -6,6 +6,12 @@ // ========================================================================== using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using FakeItEasy; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using NodaTime; @@ -15,6 +21,8 @@ using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Contents; using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; +using Squidex.Shared.Identity; +using Squidex.Shared.Users; using Xunit; namespace Squidex.Domain.Apps.Core.Operations.HandleRules @@ -22,11 +30,24 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules public class RuleEventFormatterTests { private readonly JsonSerializer serializer = JsonSerializer.CreateDefault(); + private readonly MemoryCache memoryCache = new MemoryCache(Options.Create(new MemoryCacheOptions())); + private readonly IUserResolver userResolver = A.Fake(); + private readonly IUser user = A.Fake(); + private readonly IRuleUrlGenerator urlGenerator = A.Fake(); + private readonly NamedId appId = new NamedId(Guid.NewGuid(), "my-app"); + private readonly NamedId schemaId = new NamedId(Guid.NewGuid(), "my-schema"); + private readonly Guid contentId = Guid.NewGuid(); private readonly RuleEventFormatter sut; public RuleEventFormatterTests() { - sut = new RuleEventFormatter(serializer); + A.CallTo(() => user.Email) + .Returns("me@email.com"); + + A.CallTo(() => user.Claims) + .Returns(new List { new Claim(SquidexClaimTypes.SquidexDisplayName, "me") }); + + sut = new RuleEventFormatter(serializer, urlGenerator, memoryCache, userResolver); } [Fact] @@ -40,12 +61,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Fact] public void Should_create_route_data() { - var appId = Guid.NewGuid(); - - var @event = new ContentCreated - { - AppId = new NamedId(appId, "my-app") - }; + var @event = new ContentCreated { AppId = appId }; var result = sut.ToRouteData(AsEnvelope(@event)); @@ -55,12 +71,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Fact] public void Should_create_route_data_from_event() { - var appId = Guid.NewGuid(); - - var @event = new ContentCreated - { - AppId = new NamedId(appId, "my-app") - }; + var @event = new ContentCreated { AppId = appId }; var result = sut.ToRouteData(AsEnvelope(@event), "MyEventName"); @@ -70,31 +81,21 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Fact] public void Should_replace_app_information_from_event() { - var appId = Guid.NewGuid(); - - var @event = new ContentCreated - { - AppId = new NamedId(appId, "my-app") - }; + var @event = new ContentCreated { AppId = appId }; var result = sut.FormatString("Name $APP_NAME has id $APP_ID", AsEnvelope(@event)); - Assert.Equal($"Name my-app has id {appId}", result); + Assert.Equal($"Name my-app has id {appId.Id}", result); } [Fact] public void Should_replace_schema_information_from_event() { - var schemaId = Guid.NewGuid(); - - var @event = new ContentCreated - { - SchemaId = new NamedId(schemaId, "my-schema") - }; + var @event = new ContentCreated { SchemaId = schemaId }; var result = sut.FormatString("Name $SCHEMA_NAME has id $SCHEMA_ID", AsEnvelope(@event)); - Assert.Equal($"Name my-schema has id {schemaId}", result); + Assert.Equal($"Name my-schema has id {schemaId.Id}", result); } [Fact] @@ -102,13 +103,77 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var now = DateTime.UtcNow; - var envelope = Envelope.Create(new ContentCreated()).To().SetTimestamp(Instant.FromDateTimeUtc(now)); + var envelope = AsEnvelope(new ContentCreated()).SetTimestamp(Instant.FromDateTimeUtc(now)); var result = sut.FormatString("Date: $TIMESTAMP_DATE, Full: $TIMESTAMP_DATETIME", envelope); Assert.Equal($"Date: {now:yyyy-MM-dd}, Full: {now:yyyy-MM-dd-hh-mm-ss}", result); } + [Fact] + public void Should_format_email_and_display_name_from_user() + { + A.CallTo(() => userResolver.FindByIdOrEmailAsync("123")) + .Returns(user); + + var @event = new ContentCreated { Actor = new RefToken("subject", "123") }; + + var result = sut.FormatString("From $USER_NAME ($USER_EMAIL)", AsEnvelope(@event)); + + Assert.Equal($"From me (me@email.com)", result); + } + + [Fact] + public void Should_return_undefined_if_user_is_not_found() + { + A.CallTo(() => userResolver.FindByIdOrEmailAsync("123")) + .Returns(Task.FromResult(null)); + + var @event = new ContentCreated { Actor = new RefToken("subject", "123") }; + + var result = sut.FormatString("From $USER_NAME ($USER_EMAIL)", AsEnvelope(@event)); + + Assert.Equal($"From UNDEFINED (UNDEFINED)", result); + } + + [Fact] + public void Should_return_undefined_if_user_failed_to_resolve() + { + A.CallTo(() => userResolver.FindByIdOrEmailAsync("123")) + .Throws(new InvalidOperationException()); + + var @event = new ContentCreated { Actor = new RefToken("subject", "123") }; + + var result = sut.FormatString("From $USER_NAME ($USER_EMAIL)", AsEnvelope(@event)); + + Assert.Equal($"From UNDEFINED (UNDEFINED)", result); + } + + [Fact] + public void Should_format_email_and_display_name_from_client() + { + var @event = new ContentCreated { Actor = new RefToken("client", "android") }; + + var result = sut.FormatString("From $USER_NAME ($USER_EMAIL)", AsEnvelope(@event)); + + Assert.Equal($"From client:android (client:android)", result); + } + + [Fact] + public void Should_replacecontent_url_from_event() + { + var url = "http://content"; + + A.CallTo(() => urlGenerator.GenerateContentUIUrl(appId, schemaId, contentId)) + .Returns(url); + + var @event = new ContentCreated { AppId = appId, ContentId = contentId, SchemaId = schemaId }; + + var result = sut.FormatString("Go to $CONTENT_URL", AsEnvelope(@event)); + + Assert.Equal($"Go to {url}", result); + } + [Fact] public void Should_return_undefined_when_field_not_found() { @@ -301,7 +366,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules } [Fact] - public void Should_format_content_action_for_created_when_found() + public void Should_format_content_actions_when_found() { Assert.Equal("created", sut.FormatString("$CONTENT_ACTION", AsEnvelope(new ContentCreated()))); Assert.Equal("updated", sut.FormatString("$CONTENT_ACTION", AsEnvelope(new ContentUpdated())));