Browse Source

Rule formatter improved.

pull/283/head
Sebastian 8 years ago
parent
commit
6b6ef14d0c
  1. 17
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleUrlGenerator.cs
  2. 76
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs
  3. 1
      src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
  4. 3
      src/Squidex.Shared/Squidex.Shared.csproj
  5. 3
      src/Squidex.Shared/Users/UserExtensions.cs
  6. 6
      src/Squidex/Config/Domain/EntitiesServices.cs
  7. 12
      src/Squidex/Pipeline/UrlGenerator.cs
  8. 2
      src/Squidex/app/features/rules/module.ts
  9. 123
      tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs

17
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<Guid> appId, NamedId<Guid> schemaId, Guid contentId);
}
}

76
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<AppEvent> @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<AppEvent> @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;
}
});
}
}
}

1
src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj

@ -11,6 +11,7 @@
<ProjectReference Include="..\Squidex.Domain.Apps.Core.Model\Squidex.Domain.Apps.Core.Model.csproj" />
<ProjectReference Include="..\Squidex.Domain.Apps.Events\Squidex.Domain.Apps.Events.csproj" />
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
<ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Algolia.Search" Version="4.2.2" />

3
src/Squidex.Shared/Squidex.Shared.csproj

@ -17,4 +17,7 @@
<ItemGroup>
<AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup>
</Project>

3
src/Squidex.Domain.Users/UserExtensions.cs → 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
{

6
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<IOptions<MyUrlsOptions>>(),
c.GetRequiredService<IAssetStore>(),
exposeSourceUrl))
.As<IGraphQLUrlGenerator>();
.As<IGraphQLUrlGenerator>()
.As<IRuleUrlGenerator>();
services.AddSingletonAs<CachingGraphQLService>()
.As<IGraphQLService>();

12
src/Squidex/Pipeline/GraphQLUrlGenerator.cs → 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<MyUrlsOptions> urlsOptions, IAssetStore assetStore, bool allowAssetSourceUrl)
public UrlGenerator(IOptions<MyUrlsOptions> 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<Guid> appId, NamedId<Guid> 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);

2
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'
}
}
]

123
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<IUserResolver>();
private readonly IUser user = A.Fake<IUser>();
private readonly IRuleUrlGenerator urlGenerator = A.Fake<IRuleUrlGenerator>();
private readonly NamedId<Guid> appId = new NamedId<Guid>(Guid.NewGuid(), "my-app");
private readonly NamedId<Guid> schemaId = new NamedId<Guid>(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<Claim> { 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<Guid>(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<Guid>(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<Guid>(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<Guid>(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<AppEvent>().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<IUser>(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())));

Loading…
Cancel
Save