Browse Source

Script counter. (#507)

pull/508/head 4.2.0
Sebastian Stehle 6 years ago
committed by GitHub
parent
commit
9f20a3ab7b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 11
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/DateTimeScriptExtension.cs
  2. 19
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringScriptExtension.cs
  3. 4
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs
  4. 12
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs
  5. 6
      backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettingsGrain.cs
  6. 3
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs
  7. 49
      backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterGrain.cs
  8. 59
      backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterScriptExtension.cs
  9. 19
      backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/ICounterGrain.cs
  10. 12
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ScriptContent.cs
  11. 6
      backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs
  12. 6
      backend/src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs
  13. 4
      backend/src/Squidex/Config/Domain/InfrastructureServices.cs
  14. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsGrainTests.cs
  15. 52
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterGrainTests.cs
  16. 89
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterScriptExtensionTests.cs
  17. 4
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagGrainTests.cs

11
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/DateTimeScriptExtension.cs

@ -14,12 +14,17 @@ namespace Squidex.Domain.Apps.Core.Scripting.Extensions
{ {
public sealed class DateTimeScriptExtension : IScriptExtension public sealed class DateTimeScriptExtension : IScriptExtension
{ {
private delegate JsValue FormatDateDelegate(DateTime date, string format); private readonly Func<DateTime, string, JsValue> formatDate;
public DateTimeScriptExtension()
{
formatDate = new Func<DateTime, string, JsValue>(FormatDate);
}
public void Extend(Engine engine) public void Extend(Engine engine)
{ {
engine.SetValue("formatTime", new FormatDateDelegate(FormatDate)); engine.SetValue("formatTime", formatDate);
engine.SetValue("formatDate", new FormatDateDelegate(FormatDate)); engine.SetValue("formatDate", formatDate);
} }
private static JsValue FormatDate(DateTime date, string format) private static JsValue FormatDate(DateTime date, string format)

19
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringScriptExtension.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using Jint; using Jint;
using Jint.Native; using Jint.Native;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -14,14 +15,24 @@ namespace Squidex.Domain.Apps.Core.Scripting.Extensions
public sealed class StringScriptExtension : IScriptExtension public sealed class StringScriptExtension : IScriptExtension
{ {
private delegate JsValue StringSlugifyDelegate(string text, bool single = false); private delegate JsValue StringSlugifyDelegate(string text, bool single = false);
private delegate JsValue StringFormatDelegate(string text); private readonly StringSlugifyDelegate slugify;
private readonly Func<string, JsValue> toCamelCase;
private readonly Func<string, JsValue> toPascalCase;
public StringScriptExtension()
{
slugify = new StringSlugifyDelegate(Slugify);
toCamelCase = new Func<string, JsValue>(ToCamelCase);
toPascalCase = new Func<string, JsValue>(ToPascalCase);
}
public void Extend(Engine engine) public void Extend(Engine engine)
{ {
engine.SetValue("slugify", new StringSlugifyDelegate(Slugify)); engine.SetValue("slugify", slugify);
engine.SetValue("toCamelCase", new StringFormatDelegate(ToCamelCase)); engine.SetValue("toCamelCase", toCamelCase);
engine.SetValue("toPascalCase", new StringFormatDelegate(ToPascalCase)); engine.SetValue("toPascalCase", toPascalCase);
} }
private static JsValue Slugify(string text, bool single = false) private static JsValue Slugify(string text, bool single = false)

4
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs

@ -273,13 +273,13 @@ namespace Squidex.Domain.Apps.Core.Scripting
var executionContext = new ExecutionContext(engine, cancellationToken, exceptionHandler); var executionContext = new ExecutionContext(engine, cancellationToken, exceptionHandler);
context.Add(executionContext, nested);
foreach (var extension in extensions) foreach (var extension in extensions)
{ {
extension.Extend(executionContext, async); extension.Extend(executionContext, async);
} }
context.Add(executionContext, nested);
return executionContext.Engine; return executionContext.Engine;
} }

12
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs

@ -29,6 +29,12 @@ namespace Squidex.Domain.Apps.Core.Scripting
set => SetValue(value); set => SetValue(value);
} }
public Guid AppId
{
get => GetValue<Guid>();
set => SetValue(value);
}
public Guid ContentId public Guid ContentId
{ {
get => GetValue<Guid>(); get => GetValue<Guid>();
@ -59,6 +65,12 @@ namespace Squidex.Domain.Apps.Core.Scripting
set => SetValue(value); set => SetValue(value);
} }
public string? AppName
{
get => GetValue<string?>();
set => SetValue(value);
}
public string? Operation public string? Operation
{ {
get => GetValue<string?>(); get => GetValue<string?>();

6
backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettingsGrain.cs

@ -17,15 +17,15 @@ namespace Squidex.Domain.Apps.Entities.Apps
{ {
public sealed class AppUISettingsGrain : GrainOfString, IAppUISettingsGrain public sealed class AppUISettingsGrain : GrainOfString, IAppUISettingsGrain
{ {
private readonly IGrainState<GrainState> state; private readonly IGrainState<State> state;
[CollectionName("UISettings")] [CollectionName("UISettings")]
public sealed class GrainState public sealed class State
{ {
public JsonObject Settings { get; set; } = JsonValue.Object(); public JsonObject Settings { get; set; } = JsonValue.Object();
} }
public AppUISettingsGrain(IGrainState<GrainState> state) public AppUISettingsGrain(IGrainState<State> state)
{ {
Guard.NotNull(state); Guard.NotNull(state);

3
backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs

@ -127,7 +127,8 @@ namespace Squidex.Domain.Apps.Entities.Contents
private void Enrich(ScriptContext context) private void Enrich(ScriptContext context)
{ {
context.ContentId = command.ContentId; context.ContentId = command.ContentId;
context.AppId = appEntity.Id;
context.AppName = appEntity.Name;
context.User = command.User; context.User = command.User;
} }

49
backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterGrain.cs

@ -0,0 +1,49 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Contents.Counter
{
public sealed class CounterGrain : GrainOfGuid, ICounterGrain
{
private readonly IGrainState<State> state;
[CollectionName("Counters")]
public sealed class State
{
public Dictionary<string, long> Counters { get; set; } = new Dictionary<string, long>();
}
public CounterGrain(IGrainState<State> state)
{
Guard.NotNull(state);
this.state = state;
}
public Task<long> IncrementAsync(string name)
{
state.Value.Counters.TryGetValue(name, out var value);
return ResetAsync(name, value + 1);
}
public async Task<long> ResetAsync(string name, long value)
{
state.Value.Counters[name] = value;
await state.WriteAsync();
return value;
}
}
}

59
backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterScriptExtension.cs

@ -0,0 +1,59 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Orleans;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.Counter
{
public sealed class CounterScriptExtension : IScriptExtension
{
private readonly IGrainFactory grainFactory;
public CounterScriptExtension(IGrainFactory grainFactory)
{
Guard.NotNull(grainFactory);
this.grainFactory = grainFactory;
}
public void Extend(ExecutionContext context, bool async)
{
if (context.TryGetValue("appId", out var temp) && temp is Guid appId)
{
var engine = context.Engine;
engine.SetValue("incrementCounter", new Func<string, long>(name =>
{
return Increment(appId, name);
}));
engine.SetValue("resetCounter", new Func<string, long, long>((name, value) =>
{
return Reset(appId, name, value);
}));
}
}
private long Increment(Guid appId, string name)
{
var grain = grainFactory.GetGrain<ICounterGrain>(appId);
return Task.Run(() => grain.IncrementAsync(name)).Result;
}
private long Reset(Guid appId, string name, long value)
{
var grain = grainFactory.GetGrain<ICounterGrain>(appId);
return Task.Run(() => grain.ResetAsync(name, value)).Result;
}
}
}

19
backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/ICounterGrain.cs

@ -0,0 +1,19 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using Orleans;
namespace Squidex.Domain.Apps.Entities.Contents.Counter
{
public interface ICounterGrain : IGrainWithGuidKey
{
Task<long> IncrementAsync(string name);
Task<long> ResetAsync(string name, long value);
}
}

12
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ScriptContent.cs

@ -46,10 +46,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
private async Task TransformAsync(Context context, string script, ContentEntity content) private async Task TransformAsync(Context context, string script, ContentEntity content)
{ {
var scriptContext = new ScriptContext { User = context.User }; var scriptContext = new ScriptContext
{
scriptContext.Data = content.Data; ContentId = content.Id,
scriptContext.ContentId = content.Id; Data = content.Data,
AppId = context.App.Id,
AppName = context.App.Name,
User = context.User
};
content.Data = await scriptEngine.TransformAsync(scriptContext, script); content.Data = await scriptEngine.TransformAsync(scriptContext, script);
} }

6
backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs

@ -23,7 +23,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking
[Reentrant] [Reentrant]
public sealed class UsageTrackerGrain : GrainOfString, IRemindable, IUsageTrackerGrain public sealed class UsageTrackerGrain : GrainOfString, IRemindable, IUsageTrackerGrain
{ {
private readonly IGrainState<GrainState> state; private readonly IGrainState<State> state;
private readonly IApiUsageTracker usageTracker; private readonly IApiUsageTracker usageTracker;
public sealed class Target public sealed class Target
@ -38,12 +38,12 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking
} }
[CollectionName("UsageTracker")] [CollectionName("UsageTracker")]
public sealed class GrainState public sealed class State
{ {
public Dictionary<Guid, Target> Targets { get; set; } = new Dictionary<Guid, Target>(); public Dictionary<Guid, Target> Targets { get; set; } = new Dictionary<Guid, Target>();
} }
public UsageTrackerGrain(IGrainState<GrainState> state, IApiUsageTracker usageTracker) public UsageTrackerGrain(IGrainState<State> state, IApiUsageTracker usageTracker)
{ {
Guard.NotNull(state); Guard.NotNull(state);
Guard.NotNull(usageTracker); Guard.NotNull(usageTracker);

6
backend/src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs

@ -18,15 +18,15 @@ namespace Squidex.Domain.Apps.Entities.Tags
{ {
public sealed class TagGrain : GrainOfString, ITagGrain public sealed class TagGrain : GrainOfString, ITagGrain
{ {
private readonly IGrainState<GrainState> state; private readonly IGrainState<State> state;
[CollectionName("Index_Tags")] [CollectionName("Index_Tags")]
public sealed class GrainState public sealed class State
{ {
public TagsExport Tags { get; set; } = new TagsExport(); public TagsExport Tags { get; set; } = new TagsExport();
} }
public TagGrain(IGrainState<GrainState> state) public TagGrain(IGrainState<State> state)
{ {
Guard.NotNull(state); Guard.NotNull(state);

4
backend/src/Squidex/Config/Domain/InfrastructureServices.cs

@ -18,6 +18,7 @@ using Squidex.Areas.Api.Controllers.UI;
using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.Scripting.Extensions; using Squidex.Domain.Apps.Core.Scripting.Extensions;
using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities.Contents.Counter;
using Squidex.Domain.Apps.Entities.Rules.UsageTracking; using Squidex.Domain.Apps.Entities.Rules.UsageTracking;
using Squidex.Domain.Apps.Entities.Tags; using Squidex.Domain.Apps.Entities.Tags;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -59,6 +60,9 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<JintScriptEngine>() services.AddSingletonAs<JintScriptEngine>()
.AsOptional<IScriptEngine>(); .AsOptional<IScriptEngine>();
services.AddSingletonAs<CounterScriptExtension>()
.As<IScriptExtension>();
services.AddSingletonAs<DateTimeScriptExtension>() services.AddSingletonAs<DateTimeScriptExtension>()
.As<IScriptExtension>(); .As<IScriptExtension>();

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsGrainTests.cs

@ -16,7 +16,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
{ {
public sealed class AppUISettingsGrainTests public sealed class AppUISettingsGrainTests
{ {
private readonly IGrainState<AppUISettingsGrain.GrainState> grainState = A.Fake<IGrainState<AppUISettingsGrain.GrainState>>(); private readonly IGrainState<AppUISettingsGrain.State> grainState = A.Fake<IGrainState<AppUISettingsGrain.State>>();
private readonly AppUISettingsGrain sut; private readonly AppUISettingsGrain sut;
public AppUISettingsGrainTests() public AppUISettingsGrainTests()

52
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterGrainTests.cs

@ -0,0 +1,52 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Infrastructure.Orleans;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Contents.Counter
{
public class CounterGrainTests
{
private readonly IGrainState<CounterGrain.State> grainState = A.Fake<IGrainState<CounterGrain.State>>();
private readonly CounterGrain sut;
public CounterGrainTests()
{
sut = new CounterGrain(grainState);
}
[Fact]
public async Task Should_increment_counters()
{
Assert.Equal(1, await sut.IncrementAsync("Counter1"));
Assert.Equal(2, await sut.IncrementAsync("Counter1"));
Assert.Equal(1, await sut.IncrementAsync("Counter2"));
Assert.Equal(2, await sut.IncrementAsync("Counter2"));
A.CallTo(() => grainState.WriteAsync())
.MustHaveHappened(4, Times.Exactly);
}
[Fact]
public async Task Should_reset_counter()
{
Assert.Equal(1, await sut.IncrementAsync("Counter1"));
Assert.Equal(2, await sut.IncrementAsync("Counter1"));
Assert.Equal(1, await sut.ResetAsync("Counter1", 1));
Assert.Equal(2, await sut.IncrementAsync("Counter1"));
A.CallTo(() => grainState.WriteAsync())
.MustHaveHappened(4, Times.Exactly);
}
}
}

89
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterScriptExtensionTests.cs

@ -0,0 +1,89 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using FakeItEasy;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Orleans;
using Squidex.Domain.Apps.Core.Scripting;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Contents.Counter
{
public class CounterScriptExtensionTests
{
private readonly IGrainFactory grainFactory = A.Fake<IGrainFactory>();
private readonly ICounterGrain counter = A.Fake<ICounterGrain>();
private readonly JintScriptEngine sut;
public CounterScriptExtensionTests()
{
var extensions = new IScriptExtension[]
{
new CounterScriptExtension(grainFactory)
};
var cache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
sut = new JintScriptEngine(cache, extensions)
{
Timeout = TimeSpan.FromSeconds(1)
};
}
[Fact]
public void Should_reset_counter()
{
var appId = Guid.NewGuid();
A.CallTo(() => grainFactory.GetGrain<ICounterGrain>(appId, null))
.Returns(counter);
A.CallTo(() => counter.ResetAsync("my", 4))
.Returns(3);
const string script = @"
return resetCounter('my', 4);
";
var context = new ScriptContext
{
["appId"] = appId
};
var result = sut.Interpolate(context, script);
Assert.Equal("3", result);
}
[Fact]
public void Should_increment_counter()
{
var appId = Guid.NewGuid();
A.CallTo(() => grainFactory.GetGrain<ICounterGrain>(appId, null))
.Returns(counter);
A.CallTo(() => counter.IncrementAsync("my"))
.Returns(3);
const string script = @"
return incrementCounter('my');
";
var context = new ScriptContext
{
["appId"] = appId
};
var result = sut.Interpolate(context, script);
Assert.Equal("3", result);
}
}
}

4
backend/tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagGrainTests.cs

@ -20,14 +20,14 @@ namespace Squidex.Domain.Apps.Entities.Tags
{ {
public class TagGrainTests public class TagGrainTests
{ {
private readonly IGrainState<TagGrain.GrainState> grainState = A.Fake<IGrainState<TagGrain.GrainState>>(); private readonly IGrainState<TagGrain.State> grainState = A.Fake<IGrainState<TagGrain.State>>();
private readonly string id = Guid.NewGuid().ToString(); private readonly string id = Guid.NewGuid().ToString();
private readonly TagGrain sut; private readonly TagGrain sut;
public TagGrainTests() public TagGrainTests()
{ {
A.CallTo(() => grainState.ClearAsync()) A.CallTo(() => grainState.ClearAsync())
.Invokes(() => grainState.Value = new TagGrain.GrainState()); .Invokes(() => grainState.Value = new TagGrain.State());
sut = new TagGrain(grainState); sut = new TagGrain(grainState);
sut.ActivateAsync(id).Wait(); sut.ActivateAsync(id).Wait();

Loading…
Cancel
Save