From 40f3b3019bb5ee8dc6b5d7030d1a836426a9cc69 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Thu, 10 Jan 2019 21:00:06 +0100 Subject: [PATCH 1/7] Started with usage trigger. --- .../Actions/RuleElementRegistry.cs | 13 +- .../Rules/IRuleTriggerVisitor.cs | 2 + .../Rules/Triggers/AssetChangedTriggerV2.cs | 2 + .../Rules/Triggers/ContentChangedTriggerV2.cs | 2 + .../Rules/Triggers/UsageTrigger.cs | 24 +++ .../EnrichedEvents/EnrichedEntityEvent.cs | 6 +- .../EnrichedEvents/EnrichedEvent.cs | 11 +- .../EnrichedUsageExceededEvent.cs | 21 +++ .../EnrichedEvents/EnrichedUserEvent.cs | 21 +++ .../HandleRules/IRuleTriggerHandler.cs | 5 +- .../HandleRules/RuleEventFormatter.cs | 21 ++- .../HandleRules/RuleService.cs | 8 +- .../HandleRules/RuleTriggerHandler.cs | 16 +- .../Triggers/AssetChangedTriggerHandler.cs | 5 +- .../Triggers/ContentChangedTriggerHandler.cs | 18 ++- .../Rules/EventEnricher.cs | 51 ++++--- .../Rules/Guards/RuleTriggerValidator.cs | 5 + .../Rules/UsageTracking/IUsageTrackerGrain.cs | 25 +++ .../Rules/UsageTracking/UsageTrackerGrain.cs | 143 ++++++++++++++++++ .../UsageTracking/UsageTriggerHandler.cs | 69 +++++++++ .../AppUsageExceeded.cs | 19 +++ .../Commands/DomainObjectGrain.cs | 7 +- .../Converters/RuleTriggerDtoFactory.cs | 5 + .../Rules/Models/Triggers/UsageTriggerDto.cs | 26 ++++ .../HandleRules/RuleServiceTests.cs | 18 ++- .../Triggers/AssetChangedTriggerTests.cs | 41 ++--- .../Triggers/ContentChangedTriggerTests.cs | 77 +++++----- 27 files changed, 533 insertions(+), 128 deletions(-) create mode 100644 src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/UsageTrigger.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUsageExceededEvent.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUserEvent.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/IUsageTrackerGrain.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs create mode 100644 src/Squidex.Domain.Apps.Events/AppUsageExceeded.cs create mode 100644 src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageTriggerDto.cs diff --git a/extensions/Squidex.Extensions/Actions/RuleElementRegistry.cs b/extensions/Squidex.Extensions/Actions/RuleElementRegistry.cs index b8e3dae0a..72eaa0db7 100644 --- a/extensions/Squidex.Extensions/Actions/RuleElementRegistry.cs +++ b/extensions/Squidex.Extensions/Actions/RuleElementRegistry.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Infrastructure; namespace Squidex.Extensions.Actions @@ -21,20 +22,26 @@ namespace Squidex.Extensions.Actions private static readonly Dictionary ActionTypes = new Dictionary(); private static readonly Dictionary TriggerTypes = new Dictionary { - ["ContentChanged"] = new RuleElement + [ContentChangedTriggerV2.Name] = new RuleElement { IconImage = "", IconColor = "#3389ff", Display = "Content changed", Description = "For content changes like created, updated, published, unpublished..." }, - - ["AssetChanged"] = new RuleElement + [AssetChangedTriggerV2.Name] = new RuleElement { IconImage = "", IconColor = "#3389ff", Display = "Asset changed", Description = "For asset changes like uploaded, updated, renamed, deleted..." + }, + [UsageTrigger.Name] = new RuleElement + { + IconImage = "", + IconColor = "#3389ff", + Display = "Usage limitations", + Description = "When monthly API calls exceeed a specified limit..." } }; diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs index 3edd96d94..b3c97d8bd 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs @@ -14,5 +14,7 @@ namespace Squidex.Domain.Apps.Core.Rules T Visit(AssetChangedTriggerV2 trigger); T Visit(ContentChangedTriggerV2 trigger); + + T Visit(UsageTrigger trigger); } } diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/AssetChangedTriggerV2.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/AssetChangedTriggerV2.cs index 70c9bc3ad..3d414227b 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/AssetChangedTriggerV2.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/AssetChangedTriggerV2.cs @@ -12,6 +12,8 @@ namespace Squidex.Domain.Apps.Core.Rules.Triggers [TypeName(nameof(AssetChangedTriggerV2))] public sealed class AssetChangedTriggerV2 : RuleTrigger { + public const string Name = "AssetChanged"; + public string Condition { get; set; } public override T Accept(IRuleTriggerVisitor visitor) diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerV2.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerV2.cs index ed1d9b033..0228a781b 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerV2.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerV2.cs @@ -13,6 +13,8 @@ namespace Squidex.Domain.Apps.Core.Rules.Triggers [TypeName(nameof(ContentChangedTriggerV2))] public sealed class ContentChangedTriggerV2 : RuleTrigger { + public const string Name = "ContentChanged"; + public ReadOnlyCollection Schemas { get; set; } public bool HandleAll { get; set; } diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/UsageTrigger.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/UsageTrigger.cs new file mode 100644 index 000000000..6e4ac5104 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/UsageTrigger.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Rules.Triggers +{ + [TypeName(nameof(UsageTrigger))] + public sealed class UsageTrigger : RuleTrigger + { + public const string Name = "Usage"; + + public int Limit { get; set; } + + public override T Accept(IRuleTriggerVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedEntityEvent.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedEntityEvent.cs index 3293e293c..1e2d882dc 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedEntityEvent.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedEntityEvent.cs @@ -11,7 +11,7 @@ using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents { - public abstract class EnrichedEntityEvent : EnrichedEvent + public abstract class EnrichedEntityEvent : EnrichedUserEvent { public Guid Id { get; set; } @@ -23,9 +23,9 @@ namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents public RefToken LastModifiedBy { get; set; } - public override void CalculatePartition() + public override long Partition { - Partition = Id.GetHashCode(); + get { return Id.GetHashCode(); } } } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedEvent.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedEvent.cs index 02929c715..00379b772 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedEvent.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedEvent.cs @@ -6,10 +6,8 @@ // ========================================================================== using System; -using System.Runtime.Serialization; using NodaTime; using Squidex.Infrastructure; -using Squidex.Shared.Users; namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents { @@ -17,19 +15,12 @@ namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents { public NamedId AppId { get; set; } - public RefToken Actor { get; set; } - public Instant Timestamp { get; set; } public string Name { get; set; } public long Version { get; set; } - public long Partition { get; set; } - - [IgnoreDataMember] - public IUser User { get; set; } - - public abstract void CalculatePartition(); + public abstract long Partition { get; } } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUsageExceededEvent.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUsageExceededEvent.cs new file mode 100644 index 000000000..ec4d672da --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUsageExceededEvent.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents +{ + public sealed class EnrichedUsageExceededEvent : EnrichedEvent + { + public long Current { get; set; } + + public long Limit { get; set; } + + public override long Partition + { + get { return AppId.GetHashCode(); } + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUserEvent.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUserEvent.cs new file mode 100644 index 000000000..56a011b35 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUserEvent.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Runtime.Serialization; +using Squidex.Infrastructure; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents +{ + public abstract class EnrichedUserEvent : EnrichedEvent + { + public RefToken Actor { get; set; } + + [IgnoreDataMember] + public IUser User { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs index 5c8cff42e..31d786afb 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Threading.Tasks; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules; using Squidex.Infrastructure.EventSourcing; @@ -16,8 +17,8 @@ namespace Squidex.Domain.Apps.Core.HandleRules { Type TriggerType { get; } - bool Triggers(EnrichedEvent @event, RuleTrigger trigger); + Task TriggersAsync(EnrichedEvent @event, RuleTrigger trigger); - bool Triggers(IEvent @event, RuleTrigger trigger); + Task TriggersAsync(IEvent @event, RuleTrigger trigger); } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs index 54d9dbb9f..647412f70 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs @@ -224,17 +224,32 @@ namespace Squidex.Domain.Apps.Core.HandleRules private static string UserName(EnrichedEvent @event) { - return @event.User?.DisplayName() ?? Fallback; + if (@event is EnrichedUserEvent userEvent) + { + return userEvent.User?.DisplayName() ?? Fallback; + } + + return Fallback; } private static string UserId(EnrichedEvent @event) { - return @event.User?.Id ?? Fallback; + if (@event is EnrichedUserEvent userEvent) + { + return userEvent.User?.Id ?? Fallback; + } + + return Fallback; } private static string UserEmail(EnrichedEvent @event) { - return @event.User?.Email ?? Fallback; + if (@event is EnrichedUserEvent userEvent) + { + return userEvent.User?.Email ?? Fallback; + } + + return Fallback; } private static string CalculateData(NamedContentData data, Match match) diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs index 32b8eddde..71f613ff7 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs @@ -83,7 +83,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules return null; } - if (!triggerHandler.Triggers(@event.Payload, rule.Trigger)) + if (!await triggerHandler.TriggersAsync(@event.Payload, rule.Trigger)) { return null; } @@ -106,7 +106,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules var enrichedEvent = await eventEnricher.EnrichAsync(appEventEnvelope); - if (!triggerHandler.Triggers(enrichedEvent, rule.Trigger)) + if (!await triggerHandler.TriggersAsync(enrichedEvent, rule.Trigger)) { return null; } @@ -116,14 +116,12 @@ namespace Squidex.Domain.Apps.Core.HandleRules var json = jsonSerializer.Serialize(actionData.Data); - enrichedEvent.CalculatePartition(); - var job = new RuleJob { JobId = Guid.NewGuid(), ActionName = actionName, ActionData = json, - AppId = appEvent.AppId.Id, + AppId = enrichedEvent.AppId.Id, Created = now, EventName = enrichedEvent.Name, ExecutionPartition = enrichedEvent.Partition, diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs index b6ae11c04..48bcaa262 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs @@ -6,9 +6,11 @@ // ========================================================================== using System; +using System.Threading.Tasks; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules; using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Tasks; namespace Squidex.Domain.Apps.Core.HandleRules { @@ -22,21 +24,21 @@ namespace Squidex.Domain.Apps.Core.HandleRules get { return typeof(TTrigger); } } - bool IRuleTriggerHandler.Triggers(EnrichedEvent @event, RuleTrigger trigger) + Task IRuleTriggerHandler.TriggersAsync(EnrichedEvent @event, RuleTrigger trigger) { - return @event is TEnrichedEvent e && Triggers(e, (TTrigger)trigger); + return @event is TEnrichedEvent e ? TriggersAsync(e, (TTrigger)trigger) : TaskHelper.False; } - bool IRuleTriggerHandler.Triggers(IEvent @event, RuleTrigger trigger) + Task IRuleTriggerHandler.TriggersAsync(IEvent @event, RuleTrigger trigger) { - return @event is TEvent e && Triggers(e, (TTrigger)trigger); + return @event is TEvent e ? TriggersAsync(e, (TTrigger)trigger) : TaskHelper.False; } - protected abstract bool Triggers(TEnrichedEvent @event, TTrigger trigger); + protected abstract Task TriggersAsync(TEnrichedEvent @event, TTrigger trigger); - protected virtual bool Triggers(TEvent @event, TTrigger trigger) + protected virtual Task TriggersAsync(TEvent @event, TTrigger trigger) { - return true; + return TaskHelper.True; } } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/AssetChangedTriggerHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/AssetChangedTriggerHandler.cs index 3aa2b107b..7ec57b881 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/AssetChangedTriggerHandler.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/AssetChangedTriggerHandler.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Threading.Tasks; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Domain.Apps.Core.Scripting; @@ -24,9 +25,9 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Triggers this.scriptEngine = scriptEngine; } - protected override bool Triggers(EnrichedAssetEvent @event, AssetChangedTriggerV2 trigger) + protected override Task TriggersAsync(EnrichedAssetEvent @event, AssetChangedTriggerV2 trigger) { - return string.IsNullOrWhiteSpace(trigger.Condition) || scriptEngine.Evaluate("event", @event, trigger.Condition); + return Task.FromResult(string.IsNullOrWhiteSpace(trigger.Condition) || scriptEngine.Evaluate("event", @event, trigger.Condition)); } } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs index 25bd8ccfb..e8210cbe1 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs @@ -6,11 +6,13 @@ // ========================================================================== using System; +using System.Threading.Tasks; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; 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.Tasks; namespace Squidex.Domain.Apps.Core.HandleRules.Triggers { @@ -25,11 +27,11 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Triggers this.scriptEngine = scriptEngine; } - protected override bool Triggers(ContentEvent @event, ContentChangedTriggerV2 trigger) + protected override Task TriggersAsync(ContentEvent @event, ContentChangedTriggerV2 trigger) { if (trigger.HandleAll) { - return true; + return TaskHelper.True; } if (trigger.Schemas != null) @@ -38,19 +40,19 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Triggers { if (MatchsSchema(schema, @event.SchemaId)) { - return true; + return TaskHelper.True; } } } - return false; + return TaskHelper.False; } - protected override bool Triggers(EnrichedContentEvent @event, ContentChangedTriggerV2 trigger) + protected override Task TriggersAsync(EnrichedContentEvent @event, ContentChangedTriggerV2 trigger) { if (trigger.HandleAll) { - return true; + return TaskHelper.True; } if (trigger.Schemas != null) @@ -59,12 +61,12 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Triggers { if (MatchsSchema(schema, @event.SchemaId) && MatchsCondition(schema, @event)) { - return true; + return TaskHelper.True; } } } - return false; + return TaskHelper.False; } private static bool MatchsSchema(ContentChangedTriggerSchemaV2 schema, NamedId eventId) diff --git a/src/Squidex.Domain.Apps.Entities/Rules/EventEnricher.cs b/src/Squidex.Domain.Apps.Entities/Rules/EventEnricher.cs index a9532e97f..99e915073 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/EventEnricher.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/EventEnricher.cs @@ -46,26 +46,38 @@ namespace Squidex.Domain.Apps.Entities.Rules { Guard.NotNull(@event, nameof(@event)); - if (@event.Payload is ContentEvent contentEvent) + switch (@event.Payload) { - var result = new EnrichedContentEvent(); + case ContentEvent contentEvent: + { + var result = new EnrichedContentEvent(); - await Task.WhenAll( - EnrichContentAsync(result, contentEvent, @event), - EnrichDefaultAsync(result, @event)); + await Task.WhenAll( + EnrichContentAsync(result, contentEvent, @event), + EnrichDefaultAsync(result, @event)); - return result; - } + return result; + } - if (@event.Payload is AssetEvent assetEvent) - { - var result = new EnrichedAssetEvent(); + case AssetEvent assetEvent: + { + var result = new EnrichedAssetEvent(); + + await Task.WhenAll( + EnrichAssetAsync(result, assetEvent, @event), + EnrichDefaultAsync(result, @event)); - await Task.WhenAll( - EnrichAssetAsync(result, assetEvent, @event), - EnrichDefaultAsync(result, @event)); + return result; + } - return result; + case AppUsageExceeded usageExceeded: + { + var result = new EnrichedUsageExceededEvent { Current = usageExceeded.Current, Limit = usageExceeded.Limit }; + + await EnrichDefaultAsync(result, @event); + + return result; + } } return null; @@ -149,17 +161,20 @@ namespace Squidex.Domain.Apps.Entities.Rules { result.Timestamp = @event.Headers.Timestamp(); - if (@event.Payload is SquidexEvent squidexEvent) + if (result is EnrichedUserEvent userEvent) { - result.Actor = squidexEvent.Actor; + if (@event.Payload is SquidexEvent squidexEvent) + { + userEvent.Actor = squidexEvent.Actor; + } + + userEvent.User = await FindUserAsync(userEvent.Actor); } if (@event.Payload is AppEvent appEvent) { result.AppId = appEvent.AppId; } - - result.User = await FindUserAsync(result.Actor); } private Task FindUserAsync(RefToken actor) diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs b/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs index 1e50e9606..f709c24f1 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs @@ -40,6 +40,11 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards return Task.FromResult(Enumerable.Empty()); } + public Task> Visit(UsageTrigger trigger) + { + return Task.FromResult(Enumerable.Empty()); + } + public async Task> Visit(ContentChangedTriggerV2 trigger) { if (trigger.Schemas != null) diff --git a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/IUsageTrackerGrain.cs b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/IUsageTrackerGrain.cs new file mode 100644 index 000000000..19deb0be8 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/IUsageTrackerGrain.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// 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.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking +{ + public interface IUsageTrackerGrain : IGrainWithStringKey + { + Task AddTargetAsync(NamedId appId, int limits); + + Task ActivateTargetAsync(NamedId appId); + + Task DeactivateTargetAsync(NamedId appId); + + Task RemoveTargetAsync(NamedId appId); + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs new file mode 100644 index 000000000..886aea2e6 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs @@ -0,0 +1,143 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Orleans; +using Orleans.Runtime; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.States; +using Squidex.Infrastructure.UsageTracking; + +namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking +{ + public sealed class UsageTrackerGrain : GrainOfString, IRemindable, IUsageTrackerGrain + { + private readonly IStore store; + private readonly IUsageTracker usageTracker; + private IPersistence persistence; + private State state; + + public sealed class Target + { + public int Limit { get; set; } + + public bool Enabled { get; set; } + + public DateTime Triggered { get; set; } + } + + [CollectionName("UsageTracker")] + public sealed class State + { + public Dictionary, Target> Targets { get; set; } = new Dictionary, Target>(); + } + + public UsageTrackerGrain(IStore store, IUsageTracker usageTracker) + { + Guard.NotNull(store, nameof(store)); + Guard.NotNull(usageTracker, nameof(usageTracker)); + + this.store = store; + + this.usageTracker = usageTracker; + } + + public override Task OnActivateAsync(string key) + { + DelayDeactivation(TimeSpan.FromDays(1)); + + RegisterOrUpdateReminder("Default", TimeSpan.Zero, TimeSpan.FromMinutes(10)); + + persistence = store.WithSnapshotsAndEventSourcing(GetType(), key, ApplySnapshot, ApplyEvent); + + return persistence.ReadAsync(); + } + + private void ApplySnapshot(State s) + { + state = s; + } + + private void ApplyEvent(Envelope @event) + { + } + + public async Task ReceiveReminder(string reminderName, TickStatus status) + { + var today = DateTime.Today; + + foreach (var kvp in state.Targets) + { + var appId = kvp.Key; + + if (!IsSameMonth(today, kvp.Value.Triggered)) + { + var usage = await usageTracker.GetMonthlyCallsAsync(appId.Id.ToString(), today); + + var limit = kvp.Value.Limit; + + if (usage > limit) + { + kvp.Value.Triggered = today; + + var @event = new AppUsageExceeded { AppId = appId, Current = usage, Limit = limit }; + + await persistence.WriteEventsAsync(new[] + { + Envelope.Create(@event) + }); + } + } + } + + await persistence.WriteSnapshotAsync(state); + } + + private static bool IsSameMonth(DateTime lhs, DateTime rhs) + { + return lhs.Year == rhs.Year && lhs.Month == rhs.Month; + } + + public Task ActivateTargetAsync(NamedId appId) + { + UpdateTarget(appId, t => t.Enabled = true); + + return persistence.WriteSnapshotAsync(state); + } + + public Task DeactivateTargetAsync(NamedId appId) + { + UpdateTarget(appId, t => t.Enabled = false); + + return persistence.WriteSnapshotAsync(state); + } + + public Task AddTargetAsync(NamedId appId, int limits) + { + UpdateTarget(appId, t => t.Limit = limits); + + return persistence.WriteSnapshotAsync(state); + } + + public Task RemoveTargetAsync(NamedId appId) + { + state.Targets.Remove(appId); + + return persistence.WriteSnapshotAsync(state); + } + + private void UpdateTarget(NamedId appId, Action updater) + { + updater(state.Targets.GetOrAddNew(appId));; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs new file mode 100644 index 000000000..a690b06ce --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs @@ -0,0 +1,69 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Orleans; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Rules; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking +{ + public sealed class UsageTriggerHandler : RuleTriggerHandler + { + private readonly IUsageTrackerGrain usageTrackerGrain; + + public UsageTriggerHandler(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory, nameof(grainFactory)); + + usageTrackerGrain = grainFactory.GetGrain(SingleGrain.Id); + } + + protected override async Task TriggersAsync(AppEvent @event, UsageTrigger trigger) + { + switch (@event) + { + case RuleDeleted _: + await usageTrackerGrain.RemoveTargetAsync(@event.AppId); + break; + case RuleEnabled _: + await usageTrackerGrain.ActivateTargetAsync(@event.AppId); + break; + case RuleDisabled _: + await usageTrackerGrain.DeactivateTargetAsync(@event.AppId); + break; + case RuleCreated ruleCreated: + if (ruleCreated.Trigger is UsageTrigger createdTrigger) + { + await usageTrackerGrain.AddTargetAsync(ruleCreated.AppId, createdTrigger.Limit); + } + + break; + case RuleUpdated ruleUpdated: + if (ruleUpdated.Trigger is UsageTrigger updatedTrigger) + { + await usageTrackerGrain.AddTargetAsync(ruleUpdated.AppId, updatedTrigger.Limit); + } + + break; + } + + return @event is AppUsageExceeded; + } + + protected override Task TriggersAsync(EnrichedUsageExceededEvent @event, UsageTrigger trigger) + { + return TaskHelper.True; + } + } +} diff --git a/src/Squidex.Domain.Apps.Events/AppUsageExceeded.cs b/src/Squidex.Domain.Apps.Events/AppUsageExceeded.cs new file mode 100644 index 000000000..dc1313ee6 --- /dev/null +++ b/src/Squidex.Domain.Apps.Events/AppUsageExceeded.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Events +{ + [EventType(nameof(AppUsageExceeded))] + public sealed class AppUsageExceeded : AppEvent + { + public long Current { get; set; } + + public long Limit { get; set; } + } +} diff --git a/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs b/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs index 04b05efd3..a8a81df4b 100644 --- a/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs +++ b/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs @@ -47,11 +47,16 @@ namespace Squidex.Infrastructure.Commands protected sealed override Task ReadAsync(Type type, Guid id) { - persistence = store.WithSnapshotsAndEventSourcing(GetType(), id, x => snapshot = x, ApplyEvent); + persistence = store.WithSnapshotsAndEventSourcing(GetType(), id, ApplySnapshot, ApplyEvent); return persistence.ReadAsync(); } + private void ApplySnapshot(T state) + { + snapshot = state; + } + protected sealed override async Task WriteAsync(Envelope[] events, long previousVersion) { if (events.Length > 0) diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs index ab6339b9d..3b74047de 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs @@ -31,6 +31,11 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models.Converters return SimpleMapper.Map(trigger, new AssetChangedRuleTriggerDto()); } + public RuleTriggerDto Visit(UsageTrigger trigger) + { + return SimpleMapper.Map(trigger, new UsageTriggerDto()); + } + public RuleTriggerDto Visit(ContentChangedTriggerV2 trigger) { var schemas = trigger.Schemas.Select(x => SimpleMapper.Map(x, new ContentChangedRuleTriggerSchemaDto())).ToArray(); diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageTriggerDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageTriggerDto.cs new file mode 100644 index 000000000..f298b6ef6 --- /dev/null +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageTriggerDto.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers +{ + public sealed class UsageTriggerDto : RuleTriggerDto + { + /// + /// The number of monthly api calls. + /// + public int Limit { get; set; } + + public override RuleTrigger ToTrigger() + { + return SimpleMapper.Map(this, new UsageTrigger()); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs index 24397e3cb..1b9229222 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs @@ -33,6 +33,8 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules private readonly string actionDump = "MyDump"; private readonly string actionName = "ValidAction"; private readonly string actionDescription = "MyDescription"; + private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); + private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); private readonly TypeNameRegistry typeNameRegistry = new TypeNameRegistry(); private readonly RuleService sut; @@ -68,7 +70,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules typeNameRegistry.Map(typeof(ValidAction), actionName); A.CallTo(() => eventEnricher.EnrichAsync(A>.Ignored)) - .Returns(new EnrichedContentEvent()); + .Returns(new EnrichedContentEvent { AppId = appId }); A.CallTo(() => ruleActionHandler.ActionType) .Returns(typeof(ValidAction)); @@ -144,7 +146,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules var ruleConfig = ValidRule(); var ruleEnvelope = Envelope.Create(new ContentCreated()); - A.CallTo(() => ruleTriggerHandler.Triggers(A.Ignored, ruleConfig.Trigger)) + A.CallTo(() => ruleTriggerHandler.TriggersAsync(A.Ignored, ruleConfig.Trigger)) .Returns(false); var job = await sut.CreateJobAsync(ruleConfig, ruleEnvelope); @@ -161,10 +163,10 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules var ruleConfig = ValidRule(); var ruleEnvelope = Envelope.Create(new ContentCreated()); - A.CallTo(() => ruleTriggerHandler.Triggers(A.Ignored, ruleConfig.Trigger)) + A.CallTo(() => ruleTriggerHandler.TriggersAsync(A.Ignored, ruleConfig.Trigger)) .Returns(true); - A.CallTo(() => ruleTriggerHandler.Triggers(A.Ignored, ruleConfig.Trigger)) + A.CallTo(() => ruleTriggerHandler.TriggersAsync(A.Ignored, ruleConfig.Trigger)) .Returns(false); var job = await sut.CreateJobAsync(ruleConfig, ruleEnvelope); @@ -175,7 +177,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Fact] public async Task Should_not_create_job_if_too_old() { - var @event = new ContentCreated { SchemaId = NamedId.Of(Guid.NewGuid(), "my-schema"), AppId = NamedId.Of(Guid.NewGuid(), "my-event") }; + var @event = new ContentCreated { SchemaId = schemaId, AppId = appId }; var now = SystemClock.Instance.GetCurrentInstant(); @@ -201,7 +203,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Fact] public async Task Should_create_job_if_triggered() { - var @event = new ContentCreated { SchemaId = NamedId.Of(Guid.NewGuid(), "my-schema"), AppId = NamedId.Of(Guid.NewGuid(), "my-event") }; + var @event = new ContentCreated { SchemaId = schemaId, AppId = appId }; var now = Instant.FromUnixTimeSeconds(SystemClock.Instance.GetCurrentInstant().ToUnixTimeSeconds()); @@ -213,10 +215,10 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules A.CallTo(() => clock.GetCurrentInstant()) .Returns(now); - A.CallTo(() => ruleTriggerHandler.Triggers(A.Ignored, ruleConfig.Trigger)) + A.CallTo(() => ruleTriggerHandler.TriggersAsync(A.Ignored, ruleConfig.Trigger)) .Returns(true); - A.CallTo(() => ruleTriggerHandler.Triggers(A.Ignored, ruleConfig.Trigger)) + A.CallTo(() => ruleTriggerHandler.TriggersAsync(A.Ignored, ruleConfig.Trigger)) .Returns(true); A.CallTo(() => ruleActionHandler.CreateJobAsync(A.Ignored, ruleConfig.Action)) diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/AssetChangedTriggerTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/AssetChangedTriggerTests.cs index 976d24ac3..4a82ec138 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/AssetChangedTriggerTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/AssetChangedTriggerTests.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Threading.Tasks; using FakeItEasy; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; @@ -35,76 +36,76 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers } [Fact] - public void Should_not_trigger_precheck_when_event_type_not_correct() + public Task Should_not_trigger_precheck_when_event_type_not_correct() { - TestForCondition(string.Empty, trigger => + return TestForConditionAsync(string.Empty, async trigger => { - var result = sut.Triggers(new ContentCreated(), trigger); + var result = await sut.TriggersAsync(new ContentCreated(), trigger); Assert.False(result); }); } [Fact] - public void Should_trigger_precheck_when_event_type_correct() + public Task Should_trigger_precheck_when_event_type_correct() { - TestForCondition(string.Empty, trigger => + return TestForConditionAsync(string.Empty, async trigger => { - var result = sut.Triggers(new AssetCreated(), trigger); + var result = await sut.TriggersAsync(new AssetCreated(), trigger); Assert.True(result); }); } [Fact] - public void Should_not_trigger_check_when_event_type_not_correct() + public Task Should_not_trigger_check_when_event_type_not_correct() { - TestForCondition(string.Empty, trigger => + return TestForConditionAsync(string.Empty, async trigger => { - var result = sut.Triggers(new EnrichedContentEvent(), trigger); + var result = await sut.TriggersAsync(new EnrichedContentEvent(), trigger); Assert.False(result); }); } [Fact] - public void Should_trigger_check_when_condition_is_empty() + public Task Should_trigger_check_when_condition_is_empty() { - TestForCondition(string.Empty, trigger => + return TestForConditionAsync(string.Empty, async trigger => { - var result = sut.Triggers(new EnrichedAssetEvent(), trigger); + var result = await sut.TriggersAsync(new EnrichedAssetEvent(), trigger); Assert.True(result); }); } [Fact] - public void Should_trigger_check_when_condition_matchs() + public Task Should_trigger_check_when_condition_matchs() { - TestForCondition("true", trigger => + return TestForConditionAsync("true", async trigger => { - var result = sut.Triggers(new EnrichedAssetEvent(), trigger); + var result = await sut.TriggersAsync(new EnrichedAssetEvent(), trigger); Assert.True(result); }); } [Fact] - public void Should_not_trigger_check_when_condition_does_not_matchs() + public Task Should_not_trigger_check_when_condition_does_not_matchs() { - TestForCondition("false", trigger => + return TestForConditionAsync("false", async trigger => { - var result = sut.Triggers(new EnrichedAssetEvent(), trigger); + var result = await sut.TriggersAsync(new EnrichedAssetEvent(), trigger); Assert.False(result); }); } - private void TestForCondition(string condition, Action action) + private async Task TestForConditionAsync(string condition, Func action) { var trigger = new AssetChangedTriggerV2 { Condition = condition }; - action(trigger); + await action(trigger); if (string.IsNullOrWhiteSpace(condition)) { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/ContentChangedTriggerTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/ContentChangedTriggerTests.cs index 297495f6f..e363a418b 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/ContentChangedTriggerTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/ContentChangedTriggerTests.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Threading.Tasks; using FakeItEasy; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; @@ -43,138 +44,138 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers } [Fact] - public void Should_not_trigger_precheck_when_event_type_not_correct() + public Task Should_not_trigger_precheck_when_event_type_not_correct() { - TestForTrigger(handleAll: true, schemaId: null, condition: null, action: trigger => + return TestForTriggerAsync(handleAll: true, schemaId: null, condition: null, action: async trigger => { - var result = sut.Triggers(new AssetCreated(), trigger); + var result = await sut.TriggersAsync(new AssetCreated(), trigger); Assert.False(result); }); } [Fact] - public void Should_not_trigger_precheck_when_trigger_contains_no_schemas() + public Task Should_not_trigger_precheck_when_trigger_contains_no_schemas() { - TestForTrigger(handleAll: false, schemaId: null, condition: null, action: trigger => + return TestForTriggerAsync(handleAll: false, schemaId: null, condition: null, action: async trigger => { - var result = sut.Triggers(new ContentCreated { SchemaId = SchemaMatch }, trigger); + var result = await sut.TriggersAsync(new ContentCreated { SchemaId = SchemaMatch }, trigger); Assert.False(result); }); } [Fact] - public void Should_trigger_precheck_when_handling_all_events() + public Task Should_trigger_precheck_when_handling_all_events() { - TestForTrigger(handleAll: true, schemaId: SchemaMatch, condition: null, action: trigger => + return TestForTriggerAsync(handleAll: true, schemaId: SchemaMatch, condition: null, action: async trigger => { - var result = sut.Triggers(new ContentCreated { SchemaId = SchemaMatch }, trigger); + var result = await sut.TriggersAsync(new ContentCreated { SchemaId = SchemaMatch }, trigger); Assert.True(result); }); } [Fact] - public void Should_trigger_precheck_when_condition_is_empty() + public Task Should_trigger_precheck_when_condition_is_empty() { - TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: string.Empty, action: trigger => + return TestForTriggerAsync(handleAll: false, schemaId: SchemaMatch, condition: string.Empty, action: async trigger => { - var result = sut.Triggers(new ContentCreated { SchemaId = SchemaMatch }, trigger); + var result = await sut.TriggersAsync(new ContentCreated { SchemaId = SchemaMatch }, trigger); Assert.True(result); }); } [Fact] - public void Should_not_trigger_precheck_when_schema_id_does_not_match() + public Task Should_not_trigger_precheck_when_schema_id_does_not_match() { - TestForTrigger(handleAll: false, schemaId: SchemaNonMatch, condition: null, action: trigger => + return TestForTriggerAsync(handleAll: false, schemaId: SchemaNonMatch, condition: null, action: async trigger => { - var result = sut.Triggers(new ContentCreated { SchemaId = SchemaMatch }, trigger); + var result = await sut.TriggersAsync(new ContentCreated { SchemaId = SchemaMatch }, trigger); Assert.False(result); }); } [Fact] - public void Should_not_trigger_check_when_event_type_not_correct() + public Task Should_not_trigger_check_when_event_type_not_correct() { - TestForTrigger(handleAll: true, schemaId: null, condition: null, action: trigger => + return TestForTriggerAsync(handleAll: true, schemaId: null, condition: null, action: async trigger => { - var result = sut.Triggers(new EnrichedAssetEvent(), trigger); + var result = await sut.TriggersAsync(new EnrichedAssetEvent(), trigger); Assert.False(result); }); } [Fact] - public void Should_not_trigger_check_when_trigger_contains_no_schemas() + public Task Should_not_trigger_check_when_trigger_contains_no_schemas() { - TestForTrigger(handleAll: false, schemaId: null, condition: null, action: trigger => + return TestForTriggerAsync(handleAll: false, schemaId: null, condition: null, action: async trigger => { - var result = sut.Triggers(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); + var result = await sut.TriggersAsync(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); Assert.False(result); }); } [Fact] - public void Should_trigger_check_when_handling_all_events() + public Task Should_trigger_check_when_handling_all_events() { - TestForTrigger(handleAll: true, schemaId: SchemaMatch, condition: null, action: trigger => + return TestForTriggerAsync(handleAll: true, schemaId: SchemaMatch, condition: null, action: async trigger => { - var result = sut.Triggers(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); + var result = await sut.TriggersAsync(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); Assert.True(result); }); } [Fact] - public void Should_trigger_check_when_condition_is_empty() + public Task Should_trigger_check_when_condition_is_empty() { - TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: string.Empty, action: trigger => + return TestForTriggerAsync(handleAll: false, schemaId: SchemaMatch, condition: string.Empty, action: async trigger => { - var result = sut.Triggers(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); + var result = await sut.TriggersAsync(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); Assert.True(result); }); } [Fact] - public void Should_trigger_check_when_condition_matchs() + public Task Should_trigger_check_when_condition_matchs() { - TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: "true", action: trigger => + return TestForTriggerAsync(handleAll: false, schemaId: SchemaMatch, condition: "true", action: async trigger => { - var result = sut.Triggers(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); + var result = await sut.TriggersAsync(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); Assert.True(result); }); } [Fact] - public void Should_not_trigger_check_when_schema_id_does_not_match() + public Task Should_not_trigger_check_when_schema_id_does_not_match() { - TestForTrigger(handleAll: false, schemaId: SchemaNonMatch, condition: null, action: trigger => + return TestForTriggerAsync(handleAll: false, schemaId: SchemaNonMatch, condition: null, action: async trigger => { - var result = sut.Triggers(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); + var result = await sut.TriggersAsync(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); Assert.False(result); }); } [Fact] - public void Should_not_trigger_check_when_condition_does_not_matchs() + public Task Should_not_trigger_check_when_condition_does_not_matchs() { - TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: "false", action: trigger => + return TestForTriggerAsync(handleAll: false, schemaId: SchemaMatch, condition: "false", action: async trigger => { - var result = sut.Triggers(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); + var result = await sut.TriggersAsync(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); Assert.False(result); }); } - private void TestForTrigger(bool handleAll, NamedId schemaId, string condition, Action action) + private async Task TestForTriggerAsync(bool handleAll, NamedId schemaId, string condition, Func action) { var trigger = new ContentChangedTriggerV2 { HandleAll = handleAll }; @@ -189,7 +190,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers }); } - action(trigger); + await action(trigger); if (string.IsNullOrWhiteSpace(condition)) { From 46f081f60f1d4491294f4ded7593c4a650134735 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Fri, 11 Jan 2019 20:29:06 +0100 Subject: [PATCH 2/7] Usage trigger tests and improvements to persistence system. --- .../HandleRules/IRuleTriggerHandler.cs | 5 +- .../HandleRules/RuleService.cs | 4 +- .../HandleRules/RuleTriggerHandler.cs | 15 +-- .../Triggers/AssetChangedTriggerHandler.cs | 4 +- .../Triggers/ContentChangedTriggerHandler.cs | 18 ++- .../Triggers/UsageTriggerHandler.cs | 23 ++++ .../Backup/BackupHandlerWithStore.cs | 2 +- .../Comments/CommentsGrain.cs | 2 +- .../Rules/UsageTracking/UsageTrackerGrain.cs | 48 ++----- .../UsageTracking/UsageTriggerHandler.cs | 120 +++++++++--------- .../Commands/DomainObjectGrain.cs | 2 +- .../Commands/LogSnapshotDomainObjectGrain.cs | 2 +- .../Grains/EventConsumerGrain.cs | 43 +++---- src/Squidex.Infrastructure/None.cs | 20 +++ .../Orleans/GrainOfGuid{T}.cs | 2 +- .../Orleans/GrainOfString{T}.cs | 2 +- .../States/IPersistence.cs | 2 +- src/Squidex.Infrastructure/States/IStore.cs | 11 +- .../States/Persistence.cs | 7 +- .../States/Persistence{TSnapshot,TKey}.cs | 14 +- src/Squidex.Infrastructure/States/Store.cs | 15 ++- .../States/StoreExtensions.cs | 63 ++------- src/Squidex/Config/Domain/RuleServices.cs | 3 + .../HandleRules/RuleServiceTests.cs | 10 +- .../Triggers/AssetChangedTriggerTests.cs | 41 +++--- .../Triggers/ContentChangedTriggerTests.cs | 77 ++++++----- .../Triggers/UsageTriggerHandlerTests.cs | 54 ++++++++ .../Apps/AppUISettingsGrainTests.cs | 2 +- .../Apps/Indexes/AppsByNameIndexGrainTests.cs | 2 +- .../Apps/Indexes/AppsByUserIndexGrainTests.cs | 5 +- .../Indexes/RulesByAppIndexGrainTests.cs | 5 +- .../Indexes/SchemasByAppIndexGrainTests.cs | 5 +- .../Tags/TagGrainTests.cs | 5 +- .../TestHelpers/HandlerTestBase.cs | 5 +- .../Commands/DomainObjectGrainTests.cs | 2 +- .../LogSnapshotDomainObjectGrainTests.cs | 2 +- .../Grains/EventConsumerGrainTests.cs | 6 +- .../Orleans/GrainOfGuidTests.cs | 101 +++++++++++++++ .../Orleans/GrainOfStringTests.cs | 101 +++++++++++++++ .../States/PersistenceEventSourcingTests.cs | 45 ++++--- .../States/PersistenceSnapshotTests.cs | 17 +-- tools/Migrate_01/Rebuilder.cs | 2 +- 42 files changed, 576 insertions(+), 338 deletions(-) create mode 100644 src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/UsageTriggerHandler.cs create mode 100644 src/Squidex.Infrastructure/None.cs create mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/UsageTriggerHandlerTests.cs create mode 100644 tests/Squidex.Infrastructure.Tests/Orleans/GrainOfGuidTests.cs create mode 100644 tests/Squidex.Infrastructure.Tests/Orleans/GrainOfStringTests.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs index 31d786afb..023d0908d 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs @@ -6,7 +6,6 @@ // ========================================================================== using System; -using System.Threading.Tasks; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules; using Squidex.Infrastructure.EventSourcing; @@ -17,8 +16,8 @@ namespace Squidex.Domain.Apps.Core.HandleRules { Type TriggerType { get; } - Task TriggersAsync(EnrichedEvent @event, RuleTrigger trigger); + bool Trigger(EnrichedEvent @event, RuleTrigger trigger); - Task TriggersAsync(IEvent @event, RuleTrigger trigger); + bool Trigger(IEvent @event, RuleTrigger trigger); } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs index 71f613ff7..14db51bf2 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs @@ -83,7 +83,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules return null; } - if (!await triggerHandler.TriggersAsync(@event.Payload, rule.Trigger)) + if (!triggerHandler.Trigger(@event.Payload, rule.Trigger)) { return null; } @@ -106,7 +106,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules var enrichedEvent = await eventEnricher.EnrichAsync(appEventEnvelope); - if (!await triggerHandler.TriggersAsync(enrichedEvent, rule.Trigger)) + if (!triggerHandler.Trigger(enrichedEvent, rule.Trigger)) { return null; } diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs index 48bcaa262..334e759d1 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs @@ -10,7 +10,6 @@ using System.Threading.Tasks; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules; using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Tasks; namespace Squidex.Domain.Apps.Core.HandleRules { @@ -24,21 +23,21 @@ namespace Squidex.Domain.Apps.Core.HandleRules get { return typeof(TTrigger); } } - Task IRuleTriggerHandler.TriggersAsync(EnrichedEvent @event, RuleTrigger trigger) + bool IRuleTriggerHandler.Trigger(EnrichedEvent @event, RuleTrigger trigger) { - return @event is TEnrichedEvent e ? TriggersAsync(e, (TTrigger)trigger) : TaskHelper.False; + return @event is TEnrichedEvent e && Trigger(e, (TTrigger)trigger); } - Task IRuleTriggerHandler.TriggersAsync(IEvent @event, RuleTrigger trigger) + bool IRuleTriggerHandler.Trigger(IEvent @event, RuleTrigger trigger) { - return @event is TEvent e ? TriggersAsync(e, (TTrigger)trigger) : TaskHelper.False; + return @event is TEvent e && Trigger(e, (TTrigger)trigger); } - protected abstract Task TriggersAsync(TEnrichedEvent @event, TTrigger trigger); + protected abstract bool Trigger(TEnrichedEvent @event, TTrigger trigger); - protected virtual Task TriggersAsync(TEvent @event, TTrigger trigger) + protected virtual bool Trigger(TEvent @event, TTrigger trigger) { - return TaskHelper.True; + return true; } } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/AssetChangedTriggerHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/AssetChangedTriggerHandler.cs index 7ec57b881..8daa7db20 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/AssetChangedTriggerHandler.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/AssetChangedTriggerHandler.cs @@ -25,9 +25,9 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Triggers this.scriptEngine = scriptEngine; } - protected override Task TriggersAsync(EnrichedAssetEvent @event, AssetChangedTriggerV2 trigger) + protected override bool Trigger(EnrichedAssetEvent @event, AssetChangedTriggerV2 trigger) { - return Task.FromResult(string.IsNullOrWhiteSpace(trigger.Condition) || scriptEngine.Evaluate("event", @event, trigger.Condition)); + return string.IsNullOrWhiteSpace(trigger.Condition) || scriptEngine.Evaluate("event", @event, trigger.Condition); } } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs index e8210cbe1..21bc4124c 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs @@ -6,13 +6,11 @@ // ========================================================================== using System; -using System.Threading.Tasks; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; 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.Tasks; namespace Squidex.Domain.Apps.Core.HandleRules.Triggers { @@ -27,11 +25,11 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Triggers this.scriptEngine = scriptEngine; } - protected override Task TriggersAsync(ContentEvent @event, ContentChangedTriggerV2 trigger) + protected override bool Trigger(ContentEvent @event, ContentChangedTriggerV2 trigger) { if (trigger.HandleAll) { - return TaskHelper.True; + return true; } if (trigger.Schemas != null) @@ -40,19 +38,19 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Triggers { if (MatchsSchema(schema, @event.SchemaId)) { - return TaskHelper.True; + return true; } } } - return TaskHelper.False; + return false; } - protected override Task TriggersAsync(EnrichedContentEvent @event, ContentChangedTriggerV2 trigger) + protected override bool Trigger(EnrichedContentEvent @event, ContentChangedTriggerV2 trigger) { if (trigger.HandleAll) { - return TaskHelper.True; + return true; } if (trigger.Schemas != null) @@ -61,12 +59,12 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Triggers { if (MatchsSchema(schema, @event.SchemaId) && MatchsCondition(schema, @event)) { - return TaskHelper.True; + return true; } } } - return TaskHelper.False; + return false; } private static bool MatchsSchema(ContentChangedTriggerSchemaV2 schema, NamedId eventId) diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/UsageTriggerHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/UsageTriggerHandler.cs new file mode 100644 index 000000000..f3bb0965f --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/UsageTriggerHandler.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// 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.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Core.HandleRules.Triggers +{ + public sealed class UsageTriggerHandler : RuleTriggerHandler + { + protected override bool Trigger(EnrichedUsageExceededEvent @event, UsageTrigger trigger) + { + return true; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Backup/BackupHandlerWithStore.cs b/src/Squidex.Domain.Apps.Entities/Backup/BackupHandlerWithStore.cs index d6a2eba0d..3c2cb4354 100644 --- a/src/Squidex.Domain.Apps.Entities/Backup/BackupHandlerWithStore.cs +++ b/src/Squidex.Domain.Apps.Entities/Backup/BackupHandlerWithStore.cs @@ -46,7 +46,7 @@ namespace Squidex.Domain.Apps.Entities.Backup Version = EtagVersion.Empty }; - var persistence = store.WithSnapshotsAndEventSourcing(typeof(TGrain), key, s => state = s, e => + var persistence = store.WithSnapshotsAndEventSourcing(typeof(TGrain), key, (TState s) => state = s, e => { state = func(e, state); diff --git a/src/Squidex.Domain.Apps.Entities/Comments/CommentsGrain.cs b/src/Squidex.Domain.Apps.Entities/Comments/CommentsGrain.cs index 7e20027f8..e95b25248 100644 --- a/src/Squidex.Domain.Apps.Entities/Comments/CommentsGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Comments/CommentsGrain.cs @@ -55,7 +55,7 @@ namespace Squidex.Domain.Apps.Entities.Comments protected override Task ReadAsync(Type type, Guid id) { - persistence = store.WithEventSourcing(GetType(), id, ApplyEvent); + persistence = store.WithEventSourcing(GetType(), id, ApplyEvent); return persistence.ReadAsync(); } diff --git a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs index 886aea2e6..d1607d097 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs @@ -12,19 +12,15 @@ using Orleans; using Orleans.Runtime; using Squidex.Domain.Apps.Events; using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.States; using Squidex.Infrastructure.UsageTracking; namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking { - public sealed class UsageTrackerGrain : GrainOfString, IRemindable, IUsageTrackerGrain + public sealed class UsageTrackerGrain : GrainOfString, IRemindable, IUsageTrackerGrain { - private readonly IStore store; private readonly IUsageTracker usageTracker; - private IPersistence persistence; - private State state; public sealed class Target { @@ -36,46 +32,33 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking } [CollectionName("UsageTracker")] - public sealed class State + public sealed class GrainState { public Dictionary, Target> Targets { get; set; } = new Dictionary, Target>(); } public UsageTrackerGrain(IStore store, IUsageTracker usageTracker) + : base(store) { - Guard.NotNull(store, nameof(store)); Guard.NotNull(usageTracker, nameof(usageTracker)); - this.store = store; - this.usageTracker = usageTracker; } - public override Task OnActivateAsync(string key) + protected override Task OnActivateAsync(string key) { DelayDeactivation(TimeSpan.FromDays(1)); RegisterOrUpdateReminder("Default", TimeSpan.Zero, TimeSpan.FromMinutes(10)); - persistence = store.WithSnapshotsAndEventSourcing(GetType(), key, ApplySnapshot, ApplyEvent); - - return persistence.ReadAsync(); - } - - private void ApplySnapshot(State s) - { - state = s; - } - - private void ApplyEvent(Envelope @event) - { + return Task.CompletedTask; } public async Task ReceiveReminder(string reminderName, TickStatus status) { var today = DateTime.Today; - foreach (var kvp in state.Targets) + foreach (var kvp in State.Targets) { var appId = kvp.Key; @@ -91,15 +74,12 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking var @event = new AppUsageExceeded { AppId = appId, Current = usage, Limit = limit }; - await persistence.WriteEventsAsync(new[] - { - Envelope.Create(@event) - }); + await Persistence.WriteEventAsync(@event); } } } - await persistence.WriteSnapshotAsync(state); + await WriteStateAsync(); } private static bool IsSameMonth(DateTime lhs, DateTime rhs) @@ -111,33 +91,33 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking { UpdateTarget(appId, t => t.Enabled = true); - return persistence.WriteSnapshotAsync(state); + return WriteStateAsync(); } public Task DeactivateTargetAsync(NamedId appId) { UpdateTarget(appId, t => t.Enabled = false); - return persistence.WriteSnapshotAsync(state); + return WriteStateAsync(); } public Task AddTargetAsync(NamedId appId, int limits) { UpdateTarget(appId, t => t.Limit = limits); - return persistence.WriteSnapshotAsync(state); + return WriteStateAsync(); } public Task RemoveTargetAsync(NamedId appId) { - state.Targets.Remove(appId); + State.Targets.Remove(appId); - return persistence.WriteSnapshotAsync(state); + return WriteStateAsync(); } private void UpdateTarget(NamedId appId, Action updater) { - updater(state.Targets.GetOrAddNew(appId));; + updater(State.Targets.GetOrAddNew(appId));; } } } diff --git a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs index a690b06ce..c33e903a8 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs @@ -1,69 +1,69 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== +//// ========================================================================== +//// Squidex Headless CMS +//// ========================================================================== +//// Copyright (c) Squidex UG (haftungsbeschraenkt) +//// All rights reserved. Licensed under the MIT license. +//// ========================================================================== -using System.Threading.Tasks; -using Orleans; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Rules; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.Tasks; +//using System.Threading.Tasks; +//using Orleans; +//using Squidex.Domain.Apps.Core.HandleRules; +//using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +//using Squidex.Domain.Apps.Core.Rules.Triggers; +//using Squidex.Domain.Apps.Events; +//using Squidex.Domain.Apps.Events.Rules; +//using Squidex.Infrastructure; +//using Squidex.Infrastructure.Orleans; +//using Squidex.Infrastructure.Tasks; -namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking -{ - public sealed class UsageTriggerHandler : RuleTriggerHandler - { - private readonly IUsageTrackerGrain usageTrackerGrain; +//namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking +//{ +// public sealed class UsageTriggerHandler : RuleTriggerHandler +// { +// private readonly IUsageTrackerGrain usageTrackerGrain; - public UsageTriggerHandler(IGrainFactory grainFactory) - { - Guard.NotNull(grainFactory, nameof(grainFactory)); +// public UsageTriggerHandler(IGrainFactory grainFactory) +// { +// Guard.NotNull(grainFactory, nameof(grainFactory)); - usageTrackerGrain = grainFactory.GetGrain(SingleGrain.Id); - } +// usageTrackerGrain = grainFactory.GetGrain(SingleGrain.Id); +// } - protected override async Task TriggersAsync(AppEvent @event, UsageTrigger trigger) - { - switch (@event) - { - case RuleDeleted _: - await usageTrackerGrain.RemoveTargetAsync(@event.AppId); - break; - case RuleEnabled _: - await usageTrackerGrain.ActivateTargetAsync(@event.AppId); - break; - case RuleDisabled _: - await usageTrackerGrain.DeactivateTargetAsync(@event.AppId); - break; - case RuleCreated ruleCreated: - if (ruleCreated.Trigger is UsageTrigger createdTrigger) - { - await usageTrackerGrain.AddTargetAsync(ruleCreated.AppId, createdTrigger.Limit); - } +// protected override async Task Trigger(AppEvent @event, UsageTrigger trigger) +// { +// switch (@event) +// { +// case RuleDeleted _: +// await usageTrackerGrain.RemoveTargetAsync(@event.AppId); +// break; +// case RuleEnabled _: +// await usageTrackerGrain.ActivateTargetAsync(@event.AppId); +// break; +// case RuleDisabled _: +// await usageTrackerGrain.DeactivateTargetAsync(@event.AppId); +// break; +// case RuleCreated ruleCreated: +// if (ruleCreated.Trigger is UsageTrigger createdTrigger) +// { +// await usageTrackerGrain.AddTargetAsync(ruleCreated.AppId, createdTrigger.Limit); +// } - break; - case RuleUpdated ruleUpdated: - if (ruleUpdated.Trigger is UsageTrigger updatedTrigger) - { - await usageTrackerGrain.AddTargetAsync(ruleUpdated.AppId, updatedTrigger.Limit); - } +// break; +// case RuleUpdated ruleUpdated: +// if (ruleUpdated.Trigger is UsageTrigger updatedTrigger) +// { +// await usageTrackerGrain.AddTargetAsync(ruleUpdated.AppId, updatedTrigger.Limit); +// } - break; - } +// break; +// } - return @event is AppUsageExceeded; - } +// return @event is AppUsageExceeded; +// } - protected override Task TriggersAsync(EnrichedUsageExceededEvent @event, UsageTrigger trigger) - { - return TaskHelper.True; - } - } -} +// protected override Task Trigger(EnrichedUsageExceededEvent @event, UsageTrigger trigger) +// { +// return TaskHelper.True; +// } +// } +//} diff --git a/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs b/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs index a8a81df4b..16da5a476 100644 --- a/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs +++ b/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs @@ -47,7 +47,7 @@ namespace Squidex.Infrastructure.Commands protected sealed override Task ReadAsync(Type type, Guid id) { - persistence = store.WithSnapshotsAndEventSourcing(GetType(), id, ApplySnapshot, ApplyEvent); + persistence = store.WithSnapshotsAndEventSourcing(GetType(), id, new HandleSnapshot(ApplySnapshot), ApplyEvent); return persistence.ReadAsync(); } diff --git a/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs b/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs index 417b4f1e3..cece08391 100644 --- a/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs +++ b/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs @@ -64,7 +64,7 @@ namespace Squidex.Infrastructure.Commands protected sealed override Task ReadAsync(Type type, Guid id) { - persistence = store.WithEventSourcing(type, id, ApplyEvent); + persistence = store.WithEventSourcing(type, id, ApplyEvent); return persistence.ReadAsync(); } diff --git a/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs b/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs index 4c916683a..51e584d12 100644 --- a/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs +++ b/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs @@ -17,18 +17,15 @@ using Squidex.Infrastructure.Tasks; namespace Squidex.Infrastructure.EventSourcing.Grains { - public class EventConsumerGrain : GrainOfString, IEventConsumerGrain + public class EventConsumerGrain : GrainOfString, IEventConsumerGrain { private readonly EventConsumerFactory eventConsumerFactory; - private readonly IStore store; private readonly IEventDataFormatter eventDataFormatter; private readonly IEventStore eventStore; private readonly ISemanticLog log; private TaskScheduler scheduler; - private IPersistence persistence; private IEventSubscription currentSubscription; private IEventConsumer eventConsumer; - private EventConsumerState state = new EventConsumerState(); public EventConsumerGrain( EventConsumerFactory eventConsumerFactory, @@ -36,18 +33,18 @@ namespace Squidex.Infrastructure.EventSourcing.Grains IEventStore eventStore, IEventDataFormatter eventDataFormatter, ISemanticLog log) + : base(store) { - Guard.NotNull(log, nameof(log)); - Guard.NotNull(store, nameof(store)); Guard.NotNull(eventStore, nameof(eventStore)); Guard.NotNull(eventDataFormatter, nameof(eventDataFormatter)); Guard.NotNull(eventConsumerFactory, nameof(eventConsumerFactory)); + Guard.NotNull(log, nameof(log)); - this.log = log; - this.store = store; this.eventStore = eventStore; this.eventDataFormatter = eventDataFormatter; this.eventConsumerFactory = eventConsumerFactory; + + this.log = log; } protected override Task OnActivateAsync(string key) @@ -56,14 +53,12 @@ namespace Squidex.Infrastructure.EventSourcing.Grains eventConsumer = eventConsumerFactory(key); - persistence = store.WithSnapshots(GetType(), eventConsumer.Name, s => state = s); - - return persistence.ReadAsync(); + return Task.CompletedTask; } public Task> GetStateAsync() { - return Task.FromResult(state.ToInfo(eventConsumer.Name).AsImmutable()); + return Task.FromResult(State.ToInfo(eventConsumer.Name).AsImmutable()); } public Task OnEventAsync(Immutable subscription, Immutable storedEvent) @@ -82,7 +77,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains await DispatchConsumerAsync(@event); } - state = state.Handled(storedEvent.Value.EventPosition); + State = State.Handled(storedEvent.Value.EventPosition); }); } @@ -97,15 +92,15 @@ namespace Squidex.Infrastructure.EventSourcing.Grains { Unsubscribe(); - state = state.Failed(exception.Value); + State = State.Failed(exception.Value); }); } public Task ActivateAsync() { - if (!state.IsStopped) + if (!State.IsStopped) { - Subscribe(state.Position); + Subscribe(State.Position); } return TaskHelper.Done; @@ -113,22 +108,22 @@ namespace Squidex.Infrastructure.EventSourcing.Grains public Task StartAsync() { - if (!state.IsStopped) + if (!State.IsStopped) { return TaskHelper.Done; } return DoAndUpdateStateAsync(() => { - Subscribe(state.Position); + Subscribe(State.Position); - state = state.Started(); + State = State.Started(); }); } public Task StopAsync() { - if (state.IsStopped) + if (State.IsStopped) { return TaskHelper.Done; } @@ -137,7 +132,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains { Unsubscribe(); - state = state.Stopped(); + State = State.Stopped(); }); } @@ -151,7 +146,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains Subscribe(null); - state = state.Reset(); + State = State.Reset(); }); } @@ -182,10 +177,10 @@ namespace Squidex.Infrastructure.EventSourcing.Grains .WriteProperty("status", "Failed") .WriteProperty("eventConsumer", eventConsumer.Name)); - state = state.Failed(ex); + State = State.Failed(ex); } - await persistence.WriteSnapshotAsync(state); + await WriteStateAsync(); } private async Task ClearAsync() diff --git a/src/Squidex.Infrastructure/None.cs b/src/Squidex.Infrastructure/None.cs new file mode 100644 index 000000000..76fe93d4a --- /dev/null +++ b/src/Squidex.Infrastructure/None.cs @@ -0,0 +1,20 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Infrastructure +{ + public sealed class None + { + public static readonly Type Type = typeof(None); + + private None() + { + } + } +} diff --git a/src/Squidex.Infrastructure/Orleans/GrainOfGuid{T}.cs b/src/Squidex.Infrastructure/Orleans/GrainOfGuid{T}.cs index 315130710..87dd5724e 100644 --- a/src/Squidex.Infrastructure/Orleans/GrainOfGuid{T}.cs +++ b/src/Squidex.Infrastructure/Orleans/GrainOfGuid{T}.cs @@ -43,7 +43,7 @@ namespace Squidex.Infrastructure.Orleans { Key = key; - persistence = store.WithSnapshots(GetType(), key, ApplyState); + persistence = store.WithSnapshots(GetType(), key, new HandleSnapshot(ApplyState)); await persistence.ReadAsync(); diff --git a/src/Squidex.Infrastructure/Orleans/GrainOfString{T}.cs b/src/Squidex.Infrastructure/Orleans/GrainOfString{T}.cs index bbbd61b4e..241493c64 100644 --- a/src/Squidex.Infrastructure/Orleans/GrainOfString{T}.cs +++ b/src/Squidex.Infrastructure/Orleans/GrainOfString{T}.cs @@ -42,7 +42,7 @@ namespace Squidex.Infrastructure.Orleans { Key = key; - persistence = store.WithSnapshots(GetType(), key, ApplyState); + persistence = store.WithSnapshots(GetType(), key, new HandleSnapshot(ApplyState)); await persistence.ReadAsync(); diff --git a/src/Squidex.Infrastructure/States/IPersistence.cs b/src/Squidex.Infrastructure/States/IPersistence.cs index 523a9dd0b..e07b75b38 100644 --- a/src/Squidex.Infrastructure/States/IPersistence.cs +++ b/src/Squidex.Infrastructure/States/IPersistence.cs @@ -7,7 +7,7 @@ namespace Squidex.Infrastructure.States { - public interface IPersistence : IPersistence + public interface IPersistence : IPersistence { } } diff --git a/src/Squidex.Infrastructure/States/IStore.cs b/src/Squidex.Infrastructure/States/IStore.cs index 28c44fd78..e94d94558 100644 --- a/src/Squidex.Infrastructure/States/IStore.cs +++ b/src/Squidex.Infrastructure/States/IStore.cs @@ -6,18 +6,21 @@ // ========================================================================== using System; -using System.Threading.Tasks; using Squidex.Infrastructure.EventSourcing; namespace Squidex.Infrastructure.States { + public delegate void HandleEvent(Envelope @event); + + public delegate void HandleSnapshot(T state); + public interface IStore { - IPersistence WithEventSourcing(Type owner, TKey key, Func, Task> applyEvent); + IPersistence WithEventSourcing(Type owner, TKey key, HandleEvent applyEvent); - IPersistence WithSnapshots(Type owner, TKey key, Func applySnapshot); + IPersistence WithSnapshots(Type owner, TKey key, HandleSnapshot applySnapshot); - IPersistence WithSnapshotsAndEventSourcing(Type owner, TKey key, Func applySnapshot, Func, Task> applyEvent); + IPersistence WithSnapshotsAndEventSourcing(Type owner, TKey key, HandleSnapshot applySnapshot, HandleEvent applyEvent); ISnapshotStore GetSnapshotStore(); } diff --git a/src/Squidex.Infrastructure/States/Persistence.cs b/src/Squidex.Infrastructure/States/Persistence.cs index 5c0d79037..edf253026 100644 --- a/src/Squidex.Infrastructure/States/Persistence.cs +++ b/src/Squidex.Infrastructure/States/Persistence.cs @@ -6,19 +6,18 @@ // ========================================================================== using System; -using System.Threading.Tasks; using Squidex.Infrastructure.EventSourcing; namespace Squidex.Infrastructure.States { - internal sealed class Persistence : Persistence, IPersistence + internal sealed class Persistence : Persistence, IPersistence { public Persistence(TKey ownerKey, Type ownerType, IEventStore eventStore, IEventDataFormatter eventDataFormatter, - ISnapshotStore snapshotStore, + ISnapshotStore snapshotStore, IStreamNameResolver streamNameResolver, - Func, Task> applyEvent) + HandleEvent applyEvent) : base(ownerKey, ownerType, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, PersistenceMode.EventSourcing, null, applyEvent) { } diff --git a/src/Squidex.Infrastructure/States/Persistence{TSnapshot,TKey}.cs b/src/Squidex.Infrastructure/States/Persistence{TSnapshot,TKey}.cs index 5b7f103e7..48a30f22c 100644 --- a/src/Squidex.Infrastructure/States/Persistence{TSnapshot,TKey}.cs +++ b/src/Squidex.Infrastructure/States/Persistence{TSnapshot,TKey}.cs @@ -24,8 +24,8 @@ namespace Squidex.Infrastructure.States private readonly IEventStore eventStore; private readonly IEventDataFormatter eventDataFormatter; private readonly PersistenceMode persistenceMode; - private readonly Func applyState; - private readonly Func, Task> applyEvent; + private readonly HandleSnapshot applyState; + private readonly HandleEvent applyEvent; private long versionSnapshot = EtagVersion.Empty; private long versionEvents = EtagVersion.Empty; private long version; @@ -41,8 +41,8 @@ namespace Squidex.Infrastructure.States ISnapshotStore snapshotStore, IStreamNameResolver streamNameResolver, PersistenceMode persistenceMode, - Func applyState, - Func, Task> applyEvent) + HandleSnapshot applyState, + HandleEvent applyEvent) { this.ownerKey = ownerKey; this.ownerType = ownerType; @@ -94,7 +94,7 @@ namespace Squidex.Infrastructure.States if (applyState != null && position >= 0) { - await applyState(state); + applyState(state); } } } @@ -116,9 +116,9 @@ namespace Squidex.Infrastructure.States var parsedEvent = ParseKnownEvent(@event); - if (parsedEvent != null && applyEvent != null) + if (applyEvent != null && parsedEvent != null) { - await applyEvent(parsedEvent); + applyEvent(parsedEvent); } } } diff --git a/src/Squidex.Infrastructure/States/Store.cs b/src/Squidex.Infrastructure/States/Store.cs index 3bbacc36d..6d5e77772 100644 --- a/src/Squidex.Infrastructure/States/Store.cs +++ b/src/Squidex.Infrastructure/States/Store.cs @@ -30,26 +30,31 @@ namespace Squidex.Infrastructure.States this.streamNameResolver = streamNameResolver; } - public IPersistence WithSnapshots(Type owner, TKey key, Func applySnapshot) + public IPersistence WithEventSourcing(Type owner, TKey key, HandleEvent applyEvent) + { + return CreatePersistence(owner, key, applyEvent); + } + + public IPersistence WithSnapshots(Type owner, TKey key, HandleSnapshot applySnapshot) { return CreatePersistence(owner, key, PersistenceMode.Snapshots, applySnapshot, null); } - public IPersistence WithSnapshotsAndEventSourcing(Type owner, TKey key, Func applySnapshot, Func, Task> applyEvent) + public IPersistence WithSnapshotsAndEventSourcing(Type owner, TKey key, HandleSnapshot applySnapshot, HandleEvent applyEvent) { return CreatePersistence(owner, key, PersistenceMode.SnapshotsAndEventSourcing, applySnapshot, applyEvent); } - public IPersistence WithEventSourcing(Type owner, TKey key, Func, Task> applyEvent) + private IPersistence CreatePersistence(Type owner, TKey key, HandleEvent applyEvent) { Guard.NotNull(key, nameof(key)); - var snapshotStore = GetSnapshotStore(); + var snapshotStore = GetSnapshotStore(); return new Persistence(key, owner, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, applyEvent); } - private IPersistence CreatePersistence(Type owner, TKey key, PersistenceMode mode, Func applySnapshot, Func, Task> applyEvent) + private IPersistence CreatePersistence(Type owner, TKey key, PersistenceMode mode, HandleSnapshot applySnapshot, HandleEvent applyEvent) { Guard.NotNull(key, nameof(key)); diff --git a/src/Squidex.Infrastructure/States/StoreExtensions.cs b/src/Squidex.Infrastructure/States/StoreExtensions.cs index 1c82d57dd..789c94ce1 100644 --- a/src/Squidex.Infrastructure/States/StoreExtensions.cs +++ b/src/Squidex.Infrastructure/States/StoreExtensions.cs @@ -5,83 +5,36 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using System.Threading.Tasks; using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Tasks; namespace Squidex.Infrastructure.States { public static class StoreExtensions { - public static IPersistence WithEventSourcing(this IStore store, TKey key, Func, Task> applyEvent) - { - return store.WithEventSourcing(typeof(TOwner), key, applyEvent); - } - - public static IPersistence WithSnapshots(this IStore store, TKey key, Func applySnapshot) - { - return store.WithSnapshots(typeof(TOwner), key, applySnapshot); - } - - public static IPersistence WithSnapshotsAndEventSourcing(this IStore store, TKey key, Func applySnapshot, Func, Task> applyEvent) - { - return store.WithSnapshotsAndEventSourcing(typeof(TOwner), key, applySnapshot, applyEvent); - } - - public static IPersistence WithEventSourcing(this IStore store, Type owner, TKey key, Action> applyEvent) - { - return store.WithEventSourcing(owner, key, applyEvent.ToAsync()); - } - - public static IPersistence WithSnapshots(this IStore store, Type owner, TKey key, Action applySnapshot) - { - return store.WithSnapshots(owner, key, applySnapshot.ToAsync()); - } - - public static IPersistence WithSnapshotsAndEventSourcing(this IStore store, Type owner, TKey key, Action applySnapshot, Action> applyEvent) - { - return store.WithSnapshotsAndEventSourcing(owner, key, applySnapshot.ToAsync(), applyEvent.ToAsync()); - } - - public static IPersistence WithEventSourcing(this IStore store, TKey key, Action> applyEvent) - { - return store.WithEventSourcing(typeof(TOwner), key, applyEvent.ToAsync()); - } - - public static IPersistence WithSnapshots(this IStore store, TKey key, Action applySnapshot) - { - return store.WithSnapshots(typeof(TOwner), key, applySnapshot.ToAsync()); - } - - public static IPersistence WithSnapshotsAndEventSourcing(this IStore store, TKey key, Action applySnapshot, Action> applyEvent) - { - return store.WithSnapshotsAndEventSourcing(typeof(TOwner), key, applySnapshot.ToAsync(), applyEvent.ToAsync()); - } - - public static Task WriteEventAsync(IPersistence persistence, Envelope @event) + public static Task WriteEventAsync(this IPersistence persistence, Envelope @event) { return persistence.WriteEventsAsync(new[] { @event }); } - public static Task WriteEventAsync(IPersistence persistence, IEvent @event) + public static Task WriteEventAsync(this IPersistence persistence, IEvent @event) { return persistence.WriteEventsAsync(new[] { Envelope.Create(@event) }); } - public static Task ClearSnapshotsAsync(this IStore store) + public static Task ClearSnapshotsAsync(this IStore store) { - return store.GetSnapshotStore().ClearAsync(); + return store.GetSnapshotStore().ClearAsync(); } - public static Task RemoveSnapshotAsync(this IStore store, TKey key) + public static Task RemoveSnapshotAsync(this IStore store, TKey key) { - return store.GetSnapshotStore().RemoveAsync(key); + return store.GetSnapshotStore().RemoveAsync(key); } - public static async Task GetSnapshotAsync(this IStore store, TKey key) + public static async Task GetSnapshotAsync(this IStore store, TKey key) { - var result = await store.GetSnapshotStore().ReadAsync(key); + var result = await store.GetSnapshotStore().ReadAsync(key); return result.Value; } diff --git a/src/Squidex/Config/Domain/RuleServices.cs b/src/Squidex/Config/Domain/RuleServices.cs index 517cccd33..0b30fda37 100644 --- a/src/Squidex/Config/Domain/RuleServices.cs +++ b/src/Squidex/Config/Domain/RuleServices.cs @@ -27,6 +27,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs index 1b9229222..d69dddfcc 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs @@ -146,7 +146,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules var ruleConfig = ValidRule(); var ruleEnvelope = Envelope.Create(new ContentCreated()); - A.CallTo(() => ruleTriggerHandler.TriggersAsync(A.Ignored, ruleConfig.Trigger)) + A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, ruleConfig.Trigger)) .Returns(false); var job = await sut.CreateJobAsync(ruleConfig, ruleEnvelope); @@ -163,10 +163,10 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules var ruleConfig = ValidRule(); var ruleEnvelope = Envelope.Create(new ContentCreated()); - A.CallTo(() => ruleTriggerHandler.TriggersAsync(A.Ignored, ruleConfig.Trigger)) + A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, ruleConfig.Trigger)) .Returns(true); - A.CallTo(() => ruleTriggerHandler.TriggersAsync(A.Ignored, ruleConfig.Trigger)) + A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, ruleConfig.Trigger)) .Returns(false); var job = await sut.CreateJobAsync(ruleConfig, ruleEnvelope); @@ -215,10 +215,10 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules A.CallTo(() => clock.GetCurrentInstant()) .Returns(now); - A.CallTo(() => ruleTriggerHandler.TriggersAsync(A.Ignored, ruleConfig.Trigger)) + A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, ruleConfig.Trigger)) .Returns(true); - A.CallTo(() => ruleTriggerHandler.TriggersAsync(A.Ignored, ruleConfig.Trigger)) + A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, ruleConfig.Trigger)) .Returns(true); A.CallTo(() => ruleActionHandler.CreateJobAsync(A.Ignored, ruleConfig.Action)) diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/AssetChangedTriggerTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/AssetChangedTriggerTests.cs index 4a82ec138..4bc0ff28f 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/AssetChangedTriggerTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/AssetChangedTriggerTests.cs @@ -6,7 +6,6 @@ // ========================================================================== using System; -using System.Threading.Tasks; using FakeItEasy; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; @@ -36,76 +35,76 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers } [Fact] - public Task Should_not_trigger_precheck_when_event_type_not_correct() + public void Should_not_trigger_precheck_when_event_type_not_correct() { - return TestForConditionAsync(string.Empty, async trigger => + TestForCondition(string.Empty, trigger => { - var result = await sut.TriggersAsync(new ContentCreated(), trigger); + var result = sut.Trigger(new ContentCreated(), trigger); Assert.False(result); }); } [Fact] - public Task Should_trigger_precheck_when_event_type_correct() + public void Should_trigger_precheck_when_event_type_correct() { - return TestForConditionAsync(string.Empty, async trigger => + TestForCondition(string.Empty, trigger => { - var result = await sut.TriggersAsync(new AssetCreated(), trigger); + var result = sut.Trigger(new AssetCreated(), trigger); Assert.True(result); }); } [Fact] - public Task Should_not_trigger_check_when_event_type_not_correct() + public void Should_not_trigger_check_when_event_type_not_correct() { - return TestForConditionAsync(string.Empty, async trigger => + TestForCondition(string.Empty, trigger => { - var result = await sut.TriggersAsync(new EnrichedContentEvent(), trigger); + var result = sut.Trigger(new EnrichedContentEvent(), trigger); Assert.False(result); }); } [Fact] - public Task Should_trigger_check_when_condition_is_empty() + public void Should_trigger_check_when_condition_is_empty() { - return TestForConditionAsync(string.Empty, async trigger => + TestForCondition(string.Empty, trigger => { - var result = await sut.TriggersAsync(new EnrichedAssetEvent(), trigger); + var result = sut.Trigger(new EnrichedAssetEvent(), trigger); Assert.True(result); }); } [Fact] - public Task Should_trigger_check_when_condition_matchs() + public void Should_trigger_check_when_condition_matchs() { - return TestForConditionAsync("true", async trigger => + TestForCondition("true", trigger => { - var result = await sut.TriggersAsync(new EnrichedAssetEvent(), trigger); + var result = sut.Trigger(new EnrichedAssetEvent(), trigger); Assert.True(result); }); } [Fact] - public Task Should_not_trigger_check_when_condition_does_not_matchs() + public void Should_not_trigger_check_when_condition_does_not_matchs() { - return TestForConditionAsync("false", async trigger => + TestForCondition("false", trigger => { - var result = await sut.TriggersAsync(new EnrichedAssetEvent(), trigger); + var result = sut.Trigger(new EnrichedAssetEvent(), trigger); Assert.False(result); }); } - private async Task TestForConditionAsync(string condition, Func action) + private void TestForCondition(string condition, Action action) { var trigger = new AssetChangedTriggerV2 { Condition = condition }; - await action(trigger); + action(trigger); if (string.IsNullOrWhiteSpace(condition)) { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/ContentChangedTriggerTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/ContentChangedTriggerTests.cs index e363a418b..a9e5bc937 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/ContentChangedTriggerTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/ContentChangedTriggerTests.cs @@ -8,7 +8,6 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Threading.Tasks; using FakeItEasy; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; @@ -44,138 +43,138 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers } [Fact] - public Task Should_not_trigger_precheck_when_event_type_not_correct() + public void Should_not_trigger_precheck_when_event_type_not_correct() { - return TestForTriggerAsync(handleAll: true, schemaId: null, condition: null, action: async trigger => + TestForTrigger(handleAll: true, schemaId: null, condition: null, action: trigger => { - var result = await sut.TriggersAsync(new AssetCreated(), trigger); + var result = sut.Trigger(new AssetCreated(), trigger); Assert.False(result); }); } [Fact] - public Task Should_not_trigger_precheck_when_trigger_contains_no_schemas() + public void Should_not_trigger_precheck_when_trigger_contains_no_schemas() { - return TestForTriggerAsync(handleAll: false, schemaId: null, condition: null, action: async trigger => + TestForTrigger(handleAll: false, schemaId: null, condition: null, action: trigger => { - var result = await sut.TriggersAsync(new ContentCreated { SchemaId = SchemaMatch }, trigger); + var result = sut.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger); Assert.False(result); }); } [Fact] - public Task Should_trigger_precheck_when_handling_all_events() + public void Should_trigger_precheck_when_handling_all_events() { - return TestForTriggerAsync(handleAll: true, schemaId: SchemaMatch, condition: null, action: async trigger => + TestForTrigger(handleAll: true, schemaId: SchemaMatch, condition: null, action: trigger => { - var result = await sut.TriggersAsync(new ContentCreated { SchemaId = SchemaMatch }, trigger); + var result = sut.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger); Assert.True(result); }); } [Fact] - public Task Should_trigger_precheck_when_condition_is_empty() + public void Should_trigger_precheck_when_condition_is_empty() { - return TestForTriggerAsync(handleAll: false, schemaId: SchemaMatch, condition: string.Empty, action: async trigger => + TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: string.Empty, action: trigger => { - var result = await sut.TriggersAsync(new ContentCreated { SchemaId = SchemaMatch }, trigger); + var result = sut.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger); Assert.True(result); }); } [Fact] - public Task Should_not_trigger_precheck_when_schema_id_does_not_match() + public void Should_not_trigger_precheck_when_schema_id_does_not_match() { - return TestForTriggerAsync(handleAll: false, schemaId: SchemaNonMatch, condition: null, action: async trigger => + TestForTrigger(handleAll: false, schemaId: SchemaNonMatch, condition: null, action: trigger => { - var result = await sut.TriggersAsync(new ContentCreated { SchemaId = SchemaMatch }, trigger); + var result = sut.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger); Assert.False(result); }); } [Fact] - public Task Should_not_trigger_check_when_event_type_not_correct() + public void Should_not_trigger_check_when_event_type_not_correct() { - return TestForTriggerAsync(handleAll: true, schemaId: null, condition: null, action: async trigger => + TestForTrigger(handleAll: true, schemaId: null, condition: null, action: trigger => { - var result = await sut.TriggersAsync(new EnrichedAssetEvent(), trigger); + var result = sut.Trigger(new EnrichedAssetEvent(), trigger); Assert.False(result); }); } [Fact] - public Task Should_not_trigger_check_when_trigger_contains_no_schemas() + public void Should_not_trigger_check_when_trigger_contains_no_schemas() { - return TestForTriggerAsync(handleAll: false, schemaId: null, condition: null, action: async trigger => + TestForTrigger(handleAll: false, schemaId: null, condition: null, action: trigger => { - var result = await sut.TriggersAsync(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); + var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); Assert.False(result); }); } [Fact] - public Task Should_trigger_check_when_handling_all_events() + public void Should_trigger_check_when_handling_all_events() { - return TestForTriggerAsync(handleAll: true, schemaId: SchemaMatch, condition: null, action: async trigger => + TestForTrigger(handleAll: true, schemaId: SchemaMatch, condition: null, action: trigger => { - var result = await sut.TriggersAsync(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); + var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); Assert.True(result); }); } [Fact] - public Task Should_trigger_check_when_condition_is_empty() + public void Should_trigger_check_when_condition_is_empty() { - return TestForTriggerAsync(handleAll: false, schemaId: SchemaMatch, condition: string.Empty, action: async trigger => + TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: string.Empty, action: trigger => { - var result = await sut.TriggersAsync(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); + var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); Assert.True(result); }); } [Fact] - public Task Should_trigger_check_when_condition_matchs() + public void Should_trigger_check_when_condition_matchs() { - return TestForTriggerAsync(handleAll: false, schemaId: SchemaMatch, condition: "true", action: async trigger => + TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: "true", action: trigger => { - var result = await sut.TriggersAsync(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); + var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); Assert.True(result); }); } [Fact] - public Task Should_not_trigger_check_when_schema_id_does_not_match() + public void Should_not_trigger_check_when_schema_id_does_not_match() { - return TestForTriggerAsync(handleAll: false, schemaId: SchemaNonMatch, condition: null, action: async trigger => + TestForTrigger(handleAll: false, schemaId: SchemaNonMatch, condition: null, action: trigger => { - var result = await sut.TriggersAsync(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); + var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); Assert.False(result); }); } [Fact] - public Task Should_not_trigger_check_when_condition_does_not_matchs() + public void Should_not_trigger_check_when_condition_does_not_matchs() { - return TestForTriggerAsync(handleAll: false, schemaId: SchemaMatch, condition: "false", action: async trigger => + TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: "false", action: trigger => { - var result = await sut.TriggersAsync(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); + var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); Assert.False(result); }); } - private async Task TestForTriggerAsync(bool handleAll, NamedId schemaId, string condition, Func action) + private void TestForTrigger(bool handleAll, NamedId schemaId, string condition, Action action) { var trigger = new ContentChangedTriggerV2 { HandleAll = handleAll }; @@ -190,7 +189,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers }); } - await action(trigger); + action(trigger); if (string.IsNullOrWhiteSpace(condition)) { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/UsageTriggerHandlerTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/UsageTriggerHandlerTests.cs new file mode 100644 index 000000000..b3eb86013 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/UsageTriggerHandlerTests.cs @@ -0,0 +1,54 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.HandleRules.Triggers; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Contents; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers +{ + public class UsageTriggerHandlerTests + { + private readonly IRuleTriggerHandler sut = new UsageTriggerHandler(); + + [Fact] + public void Should_not_trigger_precheck_when_event_type_not_correct() + { + var result = sut.Trigger(new ContentCreated(), new UsageTrigger()); + + Assert.False(result); + } + + [Fact] + public void Should_trigger_precheck_when_event_type_correct() + { + var result = sut.Trigger(new AppUsageExceeded(), new UsageTrigger()); + + Assert.True(result); + } + + [Fact] + public void Should_not_trigger_check_when_event_type_not_correct() + { + var result = sut.Trigger(new EnrichedContentEvent(), new UsageTrigger()); + + Assert.False(result); + } + + [Fact] + public void Should_trigger_check_when_type_correct() + { + var result = sut.Trigger(new AppUsageExceeded(), new UsageTrigger()); + + Assert.True(result); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsGrainTests.cs index 9935f7ebb..7859d6107 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsGrainTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsGrainTests.cs @@ -23,7 +23,7 @@ namespace Squidex.Domain.Apps.Entities.Apps public AppUISettingsGrainTests() { - A.CallTo(() => store.WithSnapshots(A.Ignored, A.Ignored, A>.Ignored)) + A.CallTo(() => store.WithSnapshots(typeof(AppUISettingsGrain), Guid.Empty, A>.Ignored)) .Returns(persistence); sut = new AppUISettingsGrain(store); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByNameIndexGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByNameIndexGrainTests.cs index e3092cf06..f5cf63deb 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByNameIndexGrainTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByNameIndexGrainTests.cs @@ -27,7 +27,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes public AppsByNameIndexGrainTests() { - A.CallTo(() => store.WithSnapshots(A.Ignored, A.Ignored, A>.Ignored)) + A.CallTo(() => store.WithSnapshots(typeof(AppsByNameIndexGrain), SingleGrain.Id, A>.Ignored)) .Returns(persistence); sut = new AppsByNameIndexGrain(store); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByUserIndexGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByUserIndexGrainTests.cs index 78ca858e3..7bb9f1bad 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByUserIndexGrainTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByUserIndexGrainTests.cs @@ -21,15 +21,16 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes private readonly IPersistence persistence = A.Fake>(); private readonly Guid appId1 = Guid.NewGuid(); private readonly Guid appId2 = Guid.NewGuid(); + private readonly string userId = "user"; private readonly AppsByUserIndexGrain sut; public AppsByUserIndexGrainTests() { - A.CallTo(() => store.WithSnapshots(A.Ignored, A.Ignored, A>.Ignored)) + A.CallTo(() => store.WithSnapshots(typeof(AppsByUserIndexGrain), userId, A>.Ignored)) .Returns(persistence); sut = new AppsByUserIndexGrain(store); - sut.ActivateAsync("user").Wait(); + sut.ActivateAsync(userId).Wait(); } [Fact] diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesByAppIndexGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesByAppIndexGrainTests.cs index db4558e2f..9d252b869 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesByAppIndexGrainTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesByAppIndexGrainTests.cs @@ -18,17 +18,18 @@ namespace Squidex.Domain.Apps.Entities.Rules.Indexes { private readonly IStore store = A.Fake>(); private readonly IPersistence persistence = A.Fake>(); + private readonly Guid appId = Guid.NewGuid(); private readonly Guid ruleId1 = Guid.NewGuid(); private readonly Guid ruleId2 = Guid.NewGuid(); private readonly RulesByAppIndexGrain sut; public RulesByAppIndexGrainTests() { - A.CallTo(() => store.WithSnapshots(A.Ignored, A.Ignored, A>.Ignored)) + A.CallTo(() => store.WithSnapshots(typeof(RulesByAppIndexGrain), appId, A>.Ignored)) .Returns(persistence); sut = new RulesByAppIndexGrain(store); - sut.ActivateAsync(Guid.NewGuid()).Wait(); + sut.ActivateAsync(appId).Wait(); } [Fact] diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasByAppIndexGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasByAppIndexGrainTests.cs index fcb735274..5ee8bb093 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasByAppIndexGrainTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasByAppIndexGrainTests.cs @@ -18,6 +18,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes { private readonly IStore store = A.Fake>(); private readonly IPersistence persistence = A.Fake>(); + private readonly Guid appId = Guid.NewGuid(); private readonly Guid schemaId1 = Guid.NewGuid(); private readonly Guid schemaId2 = Guid.NewGuid(); private readonly string schemaName1 = "my-schema1"; @@ -26,11 +27,11 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes public SchemasByAppIndexGrainTests() { - A.CallTo(() => store.WithSnapshots(A.Ignored, A.Ignored, A>.Ignored)) + A.CallTo(() => store.WithSnapshots(typeof(SchemasByAppIndexGrain), appId, A>.Ignored)) .Returns(persistence); sut = new SchemasByAppIndexGrain(store); - sut.ActivateAsync(Guid.NewGuid()).Wait(); + sut.ActivateAsync(appId).Wait(); } [Fact] diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagGrainTests.cs index dfcd5ad9d..9642dd244 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagGrainTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagGrainTests.cs @@ -20,15 +20,16 @@ namespace Squidex.Domain.Apps.Entities.Tags { private readonly IStore store = A.Fake>(); private readonly IPersistence persistence = A.Fake>(); + private readonly string id = Guid.NewGuid().ToString(); private readonly TagGrain sut; public TagGrainTests() { - A.CallTo(() => store.WithSnapshots(A.Ignored, A.Ignored, A>.Ignored)) + A.CallTo(() => store.WithSnapshots(typeof(TagGrain), id, A>.Ignored)) .Returns(persistence); sut = new TagGrain(store); - sut.ActivateAsync(string.Empty).Wait(); + sut.ActivateAsync(id).Wait(); } [Fact] diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs b/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs index af1f88032..b48fb4f24 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs @@ -8,7 +8,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using FakeItEasy; using Squidex.Domain.Apps.Events; using Squidex.Infrastructure; @@ -58,10 +57,10 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers protected HandlerTestBase() { - A.CallTo(() => store.WithSnapshotsAndEventSourcing(A.Ignored, Id, A>.Ignored, A, Task>>.Ignored)) + A.CallTo(() => store.WithSnapshotsAndEventSourcing(A.Ignored, Id, A>.Ignored, A.Ignored)) .Returns(persistence1); - A.CallTo(() => store.WithEventSourcing(A.Ignored, Id, A, Task>>.Ignored)) + A.CallTo(() => store.WithEventSourcing(A.Ignored, Id, A.Ignored)) .Returns(persistence2); A.CallTo(() => persistence1.WriteEventsAsync(A>>.Ignored)) diff --git a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs index 91996181d..4810221d3 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs @@ -109,7 +109,7 @@ namespace Squidex.Infrastructure.Commands public DomainObjectGrainTests() { - A.CallTo(() => store.WithSnapshotsAndEventSourcing(typeof(MyDomainObject), id, A>.Ignored, A, Task>>.Ignored)) + A.CallTo(() => store.WithSnapshotsAndEventSourcing(typeof(MyDomainObject), id, A>.Ignored, A.Ignored)) .Returns(persistence); sut = new MyDomainObject(store); diff --git a/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectGrainTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectGrainTests.cs index 71d509a1e..68ed4cf06 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectGrainTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectGrainTests.cs @@ -104,7 +104,7 @@ namespace Squidex.Infrastructure.Commands public LogSnapshotDomainObjectGrainTests() { - A.CallTo(() => store.WithEventSourcing(typeof(MyDomainObject), id, A, Task>>.Ignored)) + A.CallTo(() => store.WithEventSourcing(typeof(MyDomainObject), id, A.Ignored)) .Returns(persistence); A.CallTo(() => store.GetSnapshotStore()) diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs index 9d4c9cc0e..d1423f98c 100644 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs +++ b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs @@ -54,7 +54,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains private readonly EventConsumerGrain sut; private readonly string consumerName; private readonly string initialPosition = Guid.NewGuid().ToString(); - private Func apply; + private HandleSnapshot apply; private EventConsumerState state = new EventConsumerState(); public EventConsumerGrainTests() @@ -63,8 +63,8 @@ namespace Squidex.Infrastructure.EventSourcing.Grains consumerName = eventConsumer.GetType().Name; - A.CallTo(() => store.WithSnapshots(A.Ignored, consumerName, A>.Ignored)) - .Invokes(new Action>((t, key, a) => + A.CallTo(() => store.WithSnapshots(A.Ignored, consumerName, A>.Ignored)) + .Invokes(new Action>((t, key, a) => { apply = a; })) diff --git a/tests/Squidex.Infrastructure.Tests/Orleans/GrainOfGuidTests.cs b/tests/Squidex.Infrastructure.Tests/Orleans/GrainOfGuidTests.cs new file mode 100644 index 000000000..4a12b4c94 --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/Orleans/GrainOfGuidTests.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.Threading.Tasks; +using FakeItEasy; +using Squidex.Infrastructure.States; +using Xunit; + +namespace Squidex.Infrastructure.Orleans +{ + public class GrainOfGuidTests + { + private readonly IPersistence persistence = A.Fake>(); + private readonly IStore store = A.Fake>(); + private readonly Guid id = Guid.NewGuid(); + private readonly MyGrain sut; + private HandleSnapshot read; + + public sealed class MyGrain : GrainOfGuid + { + public sealed class GrainState + { + public Guid Id { get; set; } + } + + public GrainState PublicState + { + get { return State; } + } + + public MyGrain(IStore store) + : base(store) + { + } + + public Task PublicWriteAsync() + { + return WriteStateAsync(); + } + + public Task PublicClearAsync() + { + return ClearStateAsync(); + } + } + + public GrainOfGuidTests() + { + A.CallTo(() => persistence.ReadAsync(EtagVersion.Any)) + .Invokes(_ => + { + read(new MyGrain.GrainState { Id = id }); + }); + + A.CallTo(() => store.WithSnapshots(typeof(MyGrain), id, A>.Ignored)) + .Invokes(new Action>((type, id, callback) => + { + read = callback; + })) + .Returns(persistence); + + sut = new MyGrain(store); + } + + [Fact] + public async Task Should_read_on_activate() + { + await sut.ActivateAsync(id); + + Assert.Equal(id, sut.PublicState.Id); + + A.CallTo(() => persistence.ReadAsync(EtagVersion.Any)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_invoke_persistence_on_write() + { + await sut.ActivateAsync(id); + await sut.PublicWriteAsync(); + + A.CallTo(() => persistence.WriteSnapshotAsync(sut.PublicState)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_invoke_persistence_on_clear() + { + await sut.ActivateAsync(id); + await sut.PublicClearAsync(); + + A.CallTo(() => persistence.DeleteAsync()) + .MustHaveHappened(); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Orleans/GrainOfStringTests.cs b/tests/Squidex.Infrastructure.Tests/Orleans/GrainOfStringTests.cs new file mode 100644 index 000000000..40e3b6a4c --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/Orleans/GrainOfStringTests.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.Threading.Tasks; +using FakeItEasy; +using Squidex.Infrastructure.States; +using Xunit; + +namespace Squidex.Infrastructure.Orleans +{ + public class GrainOfStringTests + { + private readonly IPersistence persistence = A.Fake>(); + private readonly IStore store = A.Fake>(); + private readonly string id = Guid.NewGuid().ToString(); + private readonly MyGrain sut; + private HandleSnapshot read; + + public sealed class MyGrain : GrainOfString + { + public sealed class GrainState + { + public string Id { get; set; } + } + + public GrainState PublicState + { + get { return State; } + } + + public MyGrain(IStore store) + : base(store) + { + } + + public Task PublicWriteAsync() + { + return WriteStateAsync(); + } + + public Task PublicClearAsync() + { + return ClearStateAsync(); + } + } + + public GrainOfStringTests() + { + A.CallTo(() => persistence.ReadAsync(EtagVersion.Any)) + .Invokes(_ => + { + read(new MyGrain.GrainState { Id = id }); + }); + + A.CallTo(() => store.WithSnapshots(typeof(MyGrain), id, A>.Ignored)) + .Invokes(new Action>((type, id, callback) => + { + read = callback; + })) + .Returns(persistence); + + sut = new MyGrain(store); + } + + [Fact] + public async Task Should_read_on_activate() + { + await sut.ActivateAsync(id); + + Assert.Equal(id, sut.PublicState.Id); + + A.CallTo(() => persistence.ReadAsync(EtagVersion.Any)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_invoke_persistence_on_write() + { + await sut.ActivateAsync(id); + await sut.PublicWriteAsync(); + + A.CallTo(() => persistence.WriteSnapshotAsync(sut.PublicState)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_invoke_persistence_on_clear() + { + await sut.ActivateAsync(id); + await sut.PublicClearAsync(); + + A.CallTo(() => persistence.DeleteAsync()) + .MustHaveHappened(); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs b/tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs index 72e37048b..686a57c95 100644 --- a/tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs +++ b/tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs @@ -22,16 +22,19 @@ namespace Squidex.Infrastructure.States private readonly IEventDataFormatter eventDataFormatter = A.Fake(); private readonly IEventStore eventStore = A.Fake(); private readonly IServiceProvider services = A.Fake(); - private readonly ISnapshotStore snapshotStore = A.Fake>(); + private readonly ISnapshotStore snapshotStore = A.Fake>(); + private readonly ISnapshotStore snapshotStoreNone = A.Fake>(); private readonly IStreamNameResolver streamNameResolver = A.Fake(); private readonly IStore sut; public PersistenceEventSourcingTests() { - A.CallTo(() => services.GetService(typeof(ISnapshotStore))) + A.CallTo(() => services.GetService(typeof(ISnapshotStore))) .Returns(snapshotStore); + A.CallTo(() => services.GetService(typeof(ISnapshotStore))) + .Returns(snapshotStoreNone); - A.CallTo(() => streamNameResolver.GetStreamName(typeof(object), key)) + A.CallTo(() => streamNameResolver.GetStreamName(None.Type, key)) .Returns(key); sut = new Store(eventStore, eventDataFormatter, services, streamNameResolver); @@ -46,7 +49,7 @@ namespace Squidex.Infrastructure.States SetupEventStore(event1, event2); var persistedEvents = new List(); - var persistence = sut.WithEventSourcing(key, x => persistedEvents.Add(x.Payload)); + var persistence = sut.WithEventSourcing(None.Type, key, x => persistedEvents.Add(x.Payload)); await persistence.ReadAsync(); @@ -65,7 +68,7 @@ namespace Squidex.Infrastructure.States .Throws(new TypeNameNotFoundException()); var persistedEvents = new List(); - var persistence = sut.WithEventSourcing(key, x => persistedEvents.Add(x.Payload)); + var persistence = sut.WithEventSourcing(None.Type, key, x => persistedEvents.Add(x.Payload)); await persistence.ReadAsync(); @@ -81,9 +84,9 @@ namespace Squidex.Infrastructure.States SetupEventStore(3, 2); - var persistedState = (object)null; + var persistedState = -1; var persistedEvents = new List(); - var persistence = sut.WithSnapshotsAndEventSourcing(key, x => persistedState = x, x => persistedEvents.Add(x.Payload)); + var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, (int x) => persistedState = x, x => persistedEvents.Add(x.Payload)); await persistence.ReadAsync(); @@ -99,9 +102,9 @@ namespace Squidex.Infrastructure.States SetupEventStore(3, 0, 3); - var persistedState = (object)null; + var persistedState = -1; var persistedEvents = new List(); - var persistence = sut.WithSnapshotsAndEventSourcing(key, x => persistedState = x, x => persistedEvents.Add(x.Payload)); + var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, (int x) => persistedState = x, x => persistedEvents.Add(x.Payload)); await Assert.ThrowsAsync(() => persistence.ReadAsync()); } @@ -114,9 +117,9 @@ namespace Squidex.Infrastructure.States SetupEventStore(3, 4, 3); - var persistedState = (object)null; + var persistedState = -1; var persistedEvents = new List(); - var persistence = sut.WithSnapshotsAndEventSourcing(key, x => persistedState = x, x => persistedEvents.Add(x.Payload)); + var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, (int x) => persistedState = x, x => persistedEvents.Add(x.Payload)); await Assert.ThrowsAsync(() => persistence.ReadAsync()); } @@ -127,7 +130,7 @@ namespace Squidex.Infrastructure.States SetupEventStore(0); var persistedEvents = new List(); - var persistence = sut.WithEventSourcing(key, x => persistedEvents.Add(x.Payload)); + var persistence = sut.WithEventSourcing(None.Type, key, x => persistedEvents.Add(x.Payload)); await Assert.ThrowsAsync(() => persistence.ReadAsync(1)); } @@ -138,7 +141,7 @@ namespace Squidex.Infrastructure.States SetupEventStore(3); var persistedEvents = new List(); - var persistence = sut.WithEventSourcing(key, x => persistedEvents.Add(x.Payload)); + var persistence = sut.WithEventSourcing(None.Type, key, x => persistedEvents.Add(x.Payload)); await Assert.ThrowsAsync(() => persistence.ReadAsync(1)); } @@ -151,9 +154,9 @@ namespace Squidex.Infrastructure.States SetupEventStore(0); - var persistedState = (object)null; + var persistedState = -1; var persistedEvents = new List(); - var persistence = sut.WithSnapshotsAndEventSourcing(key, x => persistedState = x, x => persistedEvents.Add(x.Payload)); + var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, (int x) => persistedState = x, x => persistedEvents.Add(x.Payload)); await Assert.ThrowsAsync(() => persistence.ReadAsync(1)); } @@ -163,9 +166,9 @@ namespace Squidex.Infrastructure.States { SetupEventStore(0); - var persistedState = (object)null; + var persistedState = -1; var persistedEvents = new List(); - var persistence = sut.WithSnapshotsAndEventSourcing(key, x => persistedState = x, x => persistedEvents.Add(x.Payload)); + var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, (int x) => persistedState = x, x => persistedEvents.Add(x.Payload)); await persistence.ReadAsync(); } @@ -176,7 +179,7 @@ namespace Squidex.Infrastructure.States SetupEventStore(3); var persistedEvents = new List(); - var persistence = sut.WithEventSourcing(key, x => persistedEvents.Add(x.Payload)); + var persistence = sut.WithEventSourcing(None.Type, key, x => persistedEvents.Add(x.Payload)); await persistence.ReadAsync(); @@ -195,7 +198,7 @@ namespace Squidex.Infrastructure.States SetupEventStore(3); var persistedEvents = new List(); - var persistence = sut.WithEventSourcing(key, x => persistedEvents.Add(x.Payload)); + var persistence = sut.WithEventSourcing(None.Type, key, x => persistedEvents.Add(x.Payload)); await persistence.ReadAsync(); @@ -208,7 +211,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_delete_events_but_not_snapshot_when_deleted_snapshot_only() { - var persistence = sut.WithEventSourcing(key, x => { }); + var persistence = sut.WithEventSourcing(None.Type, key, null); await persistence.DeleteAsync(); @@ -222,7 +225,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_delete_events_and_snapshot_when_deleted() { - var persistence = sut.WithSnapshotsAndEventSourcing(key, x => { }, x => { }); + var persistence = sut.WithSnapshotsAndEventSourcing(None.Type, key, null, null); await persistence.DeleteAsync(); diff --git a/tests/Squidex.Infrastructure.Tests/States/PersistenceSnapshotTests.cs b/tests/Squidex.Infrastructure.Tests/States/PersistenceSnapshotTests.cs index 06701c721..f23ce4d40 100644 --- a/tests/Squidex.Infrastructure.Tests/States/PersistenceSnapshotTests.cs +++ b/tests/Squidex.Infrastructure.Tests/States/PersistenceSnapshotTests.cs @@ -40,7 +40,7 @@ namespace Squidex.Infrastructure.States .Returns((20, 10)); var persistedState = 0; - var persistence = sut.WithSnapshots(key, x => persistedState = x); + var persistence = sut.WithSnapshots(None.Type, key, (int x) => persistedState = x); await persistence.ReadAsync(); @@ -55,7 +55,7 @@ namespace Squidex.Infrastructure.States .Returns((20, -10)); var persistedState = 0; - var persistence = sut.WithSnapshots(key, x => persistedState = x); + var persistence = sut.WithSnapshots(None.Type, key, (int x) => persistedState = x); await persistence.ReadAsync(); @@ -69,7 +69,7 @@ namespace Squidex.Infrastructure.States .Returns((20, EtagVersion.Empty)); var persistedState = 0; - var persistence = sut.WithSnapshots(key, x => persistedState = x); + var persistence = sut.WithSnapshots(None.Type, key, (int x) => persistedState = x); await persistence.ReadAsync(); @@ -84,7 +84,7 @@ namespace Squidex.Infrastructure.States .Returns((123, EtagVersion.Empty)); var persistedState = 0; - var persistence = sut.WithSnapshots(key, x => persistedState = x); + var persistence = sut.WithSnapshots(None.Type, key, (int x) => persistedState = x); await Assert.ThrowsAsync(() => persistence.ReadAsync(1)); } @@ -96,7 +96,7 @@ namespace Squidex.Infrastructure.States .Returns((123, 2)); var persistedState = 0; - var persistence = sut.WithSnapshots(key, x => persistedState = x); + var persistence = sut.WithSnapshots(None.Type, key, (int x) => persistedState = x); await Assert.ThrowsAsync(() => persistence.ReadAsync(1)); } @@ -108,7 +108,7 @@ namespace Squidex.Infrastructure.States .Returns((20, 10)); var persistedState = 0; - var persistence = sut.WithSnapshots(key, x => persistedState = x); + var persistence = sut.WithSnapshots(None.Type, key, (int x) => persistedState = x); await persistence.ReadAsync(); @@ -131,7 +131,7 @@ namespace Squidex.Infrastructure.States .Throws(new InconsistentStateException(1, 1, new InvalidOperationException())); var persistedState = 0; - var persistence = sut.WithSnapshots(key, x => persistedState = x); + var persistence = sut.WithSnapshots(None.Type, key, (int x) => persistedState = x); await persistence.ReadAsync(); @@ -141,7 +141,8 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_delete_snapshot_but_not_events_when_deleted() { - var persistence = sut.WithSnapshots(key, x => { }); + var persistedState = 0; + var persistence = sut.WithSnapshots(None.Type, key, (int x) => persistedState = x); await persistence.DeleteAsync(); diff --git a/tools/Migrate_01/Rebuilder.cs b/tools/Migrate_01/Rebuilder.cs index 030257d8e..39fac9933 100644 --- a/tools/Migrate_01/Rebuilder.cs +++ b/tools/Migrate_01/Rebuilder.cs @@ -126,7 +126,7 @@ namespace Migrate_01 Version = EtagVersion.Empty }; - var persistence = store.WithSnapshotsAndEventSourcing(typeof(TGrain), key, s => state = s, e => + var persistence = store.WithSnapshotsAndEventSourcing(typeof(TGrain), key, (TState s) => state = s, e => { state = func(e, state); From 404acb31adb14ff67dea9fd46efe687baa945a18 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Fri, 11 Jan 2019 22:15:34 +0100 Subject: [PATCH 3/7] Grain improved. --- .../HandleRules/IRuleTriggerHandler.cs | 2 +- .../HandleRules/RuleService.cs | 4 +- .../HandleRules/RuleTriggerHandler.cs | 7 +- .../Triggers/AssetChangedTriggerHandler.cs | 1 - .../Triggers/ContentChangedTriggerHandler.cs | 2 +- .../Triggers/UsageTriggerHandler.cs | 4 +- .../Apps/InviteUserCommandMiddleware.cs | 2 +- .../Rules/EventEnricher.cs | 2 +- .../Rules/RuleEnqueuer.cs | 2 +- .../Rules/UsageTracking/IUsageTrackerGrain.cs | 10 +-- .../Rules/UsageTracking/UsageTrackerGrain.cs | 54 ++++++++++----- .../UsageTracking/UsageTriggerHandler.cs | 69 ------------------- .../AppUsageExceeded.cs | 7 +- .../Commands/CommandContext.cs | 5 ++ .../Apps/AppContributorsController.cs | 5 +- .../Controllers/Plans/AppPlansController.cs | 2 +- .../ETagCommandMiddleware.cs | 2 +- .../HandleRules/RuleServiceTests.cs | 69 ++++++++++--------- .../Triggers/AssetChangedTriggerTests.cs | 4 +- .../Triggers/ContentChangedTriggerTests.cs | 11 +-- .../Triggers/UsageTriggerHandlerTests.cs | 20 +++--- .../Rules/RuleEnqueuerTests.cs | 18 ++--- .../Commands/CommandContextTests.cs | 19 ++++- 23 files changed, 144 insertions(+), 177 deletions(-) delete mode 100644 src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs index 023d0908d..5b27a05b9 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs @@ -18,6 +18,6 @@ namespace Squidex.Domain.Apps.Core.HandleRules bool Trigger(EnrichedEvent @event, RuleTrigger trigger); - bool Trigger(IEvent @event, RuleTrigger trigger); + bool Trigger(IEvent @event, RuleTrigger trigger, Guid ruleId); } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs index 14db51bf2..b7a0050b7 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs @@ -56,7 +56,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules this.clock = clock; } - public virtual async Task CreateJobAsync(Rule rule, Envelope @event) + public virtual async Task CreateJobAsync(Rule rule, Guid ruleId, Envelope @event) { Guard.NotNull(rule, nameof(rule)); Guard.NotNull(@event, nameof(@event)); @@ -83,7 +83,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules return null; } - if (!triggerHandler.Trigger(@event.Payload, rule.Trigger)) + if (!triggerHandler.Trigger(@event.Payload, rule.Trigger, ruleId)) { return null; } diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs index 334e759d1..7e62cad86 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs @@ -6,7 +6,6 @@ // ========================================================================== using System; -using System.Threading.Tasks; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules; using Squidex.Infrastructure.EventSourcing; @@ -28,14 +27,14 @@ namespace Squidex.Domain.Apps.Core.HandleRules return @event is TEnrichedEvent e && Trigger(e, (TTrigger)trigger); } - bool IRuleTriggerHandler.Trigger(IEvent @event, RuleTrigger trigger) + bool IRuleTriggerHandler.Trigger(IEvent @event, RuleTrigger trigger, Guid ruleId) { - return @event is TEvent e && Trigger(e, (TTrigger)trigger); + return @event is TEvent e && Trigger(e, (TTrigger)trigger, ruleId); } protected abstract bool Trigger(TEnrichedEvent @event, TTrigger trigger); - protected virtual bool Trigger(TEvent @event, TTrigger trigger) + protected virtual bool Trigger(TEvent @event, TTrigger trigger, Guid ruleId) { return true; } diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/AssetChangedTriggerHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/AssetChangedTriggerHandler.cs index 8daa7db20..e75e41b8a 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/AssetChangedTriggerHandler.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/AssetChangedTriggerHandler.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Threading.Tasks; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Domain.Apps.Core.Scripting; diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs index 21bc4124c..1c95e6734 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs @@ -25,7 +25,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Triggers this.scriptEngine = scriptEngine; } - protected override bool Trigger(ContentEvent @event, ContentChangedTriggerV2 trigger) + protected override bool Trigger(ContentEvent @event, ContentChangedTriggerV2 trigger, Guid ruleId) { if (trigger.HandleAll) { diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/UsageTriggerHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/UsageTriggerHandler.cs index f3bb0965f..71ed317f4 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/UsageTriggerHandler.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/UsageTriggerHandler.cs @@ -5,11 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Threading.Tasks; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Domain.Apps.Events; -using Squidex.Infrastructure.EventSourcing; namespace Squidex.Domain.Apps.Core.HandleRules.Triggers { @@ -17,7 +15,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Triggers { protected override bool Trigger(EnrichedUsageExceededEvent @event, UsageTrigger trigger) { - return true; + return @event.Limit == trigger.Limit; } } } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/InviteUserCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Apps/InviteUserCommandMiddleware.cs index 9372b80b5..a82031519 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/InviteUserCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/InviteUserCommandMiddleware.cs @@ -35,7 +35,7 @@ namespace Squidex.Domain.Apps.Entities.Apps await next(); - if (isInvited && context.Result() is EntityCreatedResult id) + if (isInvited && context.PlainResult is EntityCreatedResult id) { context.Complete(new InvitedResult { Id = id }); } diff --git a/src/Squidex.Domain.Apps.Entities/Rules/EventEnricher.cs b/src/Squidex.Domain.Apps.Entities/Rules/EventEnricher.cs index 99e915073..803d90884 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/EventEnricher.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/EventEnricher.cs @@ -72,7 +72,7 @@ namespace Squidex.Domain.Apps.Entities.Rules case AppUsageExceeded usageExceeded: { - var result = new EnrichedUsageExceededEvent { Current = usageExceeded.Current, Limit = usageExceeded.Limit }; + var result = new EnrichedUsageExceededEvent { Current = usageExceeded.CallsCurrent, Limit = usageExceeded.CallsLimit }; await EnrichDefaultAsync(result, @event); diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs index e1710013e..e485d324e 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs @@ -65,7 +65,7 @@ namespace Squidex.Domain.Apps.Entities.Rules foreach (var ruleEntity in rules) { - var job = await ruleService.CreateJobAsync(ruleEntity.RuleDef, @event); + var job = await ruleService.CreateJobAsync(ruleEntity.RuleDef, ruleEntity.Id, @event); if (job != null) { diff --git a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/IUsageTrackerGrain.cs b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/IUsageTrackerGrain.cs index 19deb0be8..812b2862b 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/IUsageTrackerGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/IUsageTrackerGrain.cs @@ -14,12 +14,14 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking { public interface IUsageTrackerGrain : IGrainWithStringKey { - Task AddTargetAsync(NamedId appId, int limits); + Task AddTargetAsync(Guid ruleId, NamedId appId, int limits); - Task ActivateTargetAsync(NamedId appId); + Task ActivateTargetAsync(Guid ruleId); - Task DeactivateTargetAsync(NamedId appId); + Task DeactivateTargetAsync(Guid ruleId); - Task RemoveTargetAsync(NamedId appId); + Task RemoveTargetAsync(Guid ruleId); + + Task UpdateTargetAsync(Guid ruleId, int limits); } } \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs index d1607d097..1ebc89e37 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs @@ -24,17 +24,19 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking public sealed class Target { - public int Limit { get; set; } + public int Limits { get; set; } - public bool Enabled { get; set; } + public bool Disabled { get; set; } public DateTime Triggered { get; set; } + + public NamedId AppId { get; set; } } [CollectionName("UsageTracker")] public sealed class GrainState { - public Dictionary, Target> Targets { get; set; } = new Dictionary, Target>(); + public Dictionary Targets { get; set; } = new Dictionary(); } public UsageTrackerGrain(IStore store, IUsageTracker usageTracker) @@ -60,19 +62,25 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking foreach (var kvp in State.Targets) { - var appId = kvp.Key; + var appId = kvp.Value.AppId; if (!IsSameMonth(today, kvp.Value.Triggered)) { var usage = await usageTracker.GetMonthlyCallsAsync(appId.Id.ToString(), today); - var limit = kvp.Value.Limit; + var limit = kvp.Value.Limits; if (usage > limit) { kvp.Value.Triggered = today; - var @event = new AppUsageExceeded { AppId = appId, Current = usage, Limit = limit }; + var @event = new AppUsageExceeded + { + AppId = appId, + CallsCurrent = usage, + CallsLimit = limit, + RuleId = kvp.Key + }; await Persistence.WriteEventAsync(@event); } @@ -87,37 +95,51 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking return lhs.Year == rhs.Year && lhs.Month == rhs.Month; } - public Task ActivateTargetAsync(NamedId appId) + public Task AddTargetAsync(Guid ruleId, NamedId appId, int limits) + { + UpdateTarget(ruleId, t => { t.Limits = limits; t.AppId = appId; }); + + return WriteStateAsync(); + } + + public Task UpdateTargetAsync(Guid ruleId, int limits) + { + UpdateTarget(ruleId, t => t.Limits = limits); + + return WriteStateAsync(); + } + + public Task ActivateTargetAsync(Guid ruleId) { - UpdateTarget(appId, t => t.Enabled = true); + UpdateTarget(ruleId, t => t.Disabled = false); return WriteStateAsync(); } - public Task DeactivateTargetAsync(NamedId appId) + public Task DeactivateTargetAsync(Guid ruleId) { - UpdateTarget(appId, t => t.Enabled = false); + UpdateTarget(ruleId, t => t.Disabled = true); return WriteStateAsync(); } - public Task AddTargetAsync(NamedId appId, int limits) + public Task AddTargetAsync(Guid ruleId, int limits) { - UpdateTarget(appId, t => t.Limit = limits); + UpdateTarget(ruleId, t => t.Limits = limits); return WriteStateAsync(); } - public Task RemoveTargetAsync(NamedId appId) + public Task RemoveTargetAsync(Guid ruleId) { - State.Targets.Remove(appId); + State.Targets.Remove(ruleId); return WriteStateAsync(); } - private void UpdateTarget(NamedId appId, Action updater) + private void UpdateTarget(Guid ruleId, Action updater) { - updater(State.Targets.GetOrAddNew(appId));; + updater(State.Targets.GetOrAddNew(ruleId)); } } } diff --git a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs deleted file mode 100644 index c33e903a8..000000000 --- a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs +++ /dev/null @@ -1,69 +0,0 @@ -//// ========================================================================== -//// Squidex Headless CMS -//// ========================================================================== -//// Copyright (c) Squidex UG (haftungsbeschraenkt) -//// All rights reserved. Licensed under the MIT license. -//// ========================================================================== - -//using System.Threading.Tasks; -//using Orleans; -//using Squidex.Domain.Apps.Core.HandleRules; -//using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -//using Squidex.Domain.Apps.Core.Rules.Triggers; -//using Squidex.Domain.Apps.Events; -//using Squidex.Domain.Apps.Events.Rules; -//using Squidex.Infrastructure; -//using Squidex.Infrastructure.Orleans; -//using Squidex.Infrastructure.Tasks; - -//namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking -//{ -// public sealed class UsageTriggerHandler : RuleTriggerHandler -// { -// private readonly IUsageTrackerGrain usageTrackerGrain; - -// public UsageTriggerHandler(IGrainFactory grainFactory) -// { -// Guard.NotNull(grainFactory, nameof(grainFactory)); - -// usageTrackerGrain = grainFactory.GetGrain(SingleGrain.Id); -// } - -// protected override async Task Trigger(AppEvent @event, UsageTrigger trigger) -// { -// switch (@event) -// { -// case RuleDeleted _: -// await usageTrackerGrain.RemoveTargetAsync(@event.AppId); -// break; -// case RuleEnabled _: -// await usageTrackerGrain.ActivateTargetAsync(@event.AppId); -// break; -// case RuleDisabled _: -// await usageTrackerGrain.DeactivateTargetAsync(@event.AppId); -// break; -// case RuleCreated ruleCreated: -// if (ruleCreated.Trigger is UsageTrigger createdTrigger) -// { -// await usageTrackerGrain.AddTargetAsync(ruleCreated.AppId, createdTrigger.Limit); -// } - -// break; -// case RuleUpdated ruleUpdated: -// if (ruleUpdated.Trigger is UsageTrigger updatedTrigger) -// { -// await usageTrackerGrain.AddTargetAsync(ruleUpdated.AppId, updatedTrigger.Limit); -// } - -// break; -// } - -// return @event is AppUsageExceeded; -// } - -// protected override Task Trigger(EnrichedUsageExceededEvent @event, UsageTrigger trigger) -// { -// return TaskHelper.True; -// } -// } -//} diff --git a/src/Squidex.Domain.Apps.Events/AppUsageExceeded.cs b/src/Squidex.Domain.Apps.Events/AppUsageExceeded.cs index dc1313ee6..c7b6c6d8d 100644 --- a/src/Squidex.Domain.Apps.Events/AppUsageExceeded.cs +++ b/src/Squidex.Domain.Apps.Events/AppUsageExceeded.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using Squidex.Infrastructure.EventSourcing; namespace Squidex.Domain.Apps.Events @@ -12,8 +13,10 @@ namespace Squidex.Domain.Apps.Events [EventType(nameof(AppUsageExceeded))] public sealed class AppUsageExceeded : AppEvent { - public long Current { get; set; } + public long CallsCurrent { get; set; } - public long Limit { get; set; } + public long CallsLimit { get; set; } + + public Guid RuleId { get; set; } } } diff --git a/src/Squidex.Infrastructure/Commands/CommandContext.cs b/src/Squidex.Infrastructure/Commands/CommandContext.cs index 22cea0840..0b50bb495 100644 --- a/src/Squidex.Infrastructure/Commands/CommandContext.cs +++ b/src/Squidex.Infrastructure/Commands/CommandContext.cs @@ -19,6 +19,11 @@ namespace Squidex.Infrastructure.Commands public ICommandBus CommandBus { get; } + public object PlainResult + { + get { return result?.Item1; } + } + public bool IsCompleted { get { return result != null; } diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs index 30baf7b0b..571d9be66 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs @@ -75,14 +75,13 @@ namespace Squidex.Areas.Api.Controllers.Apps var command = request.ToCommand(); var context = await CommandBus.PublishAsync(command); - var result = context.Result(); var response = (ContributorAssignedDto)null; - if (result is EntityCreatedResult idOrValue) + if (context.PlainResult is EntityCreatedResult idOrValue) { response = ContributorAssignedDto.FromId(idOrValue.IdOrValue, false); } - else if (result is InvitedResult invited) + else if (context.PlainResult is InvitedResult invited) { response = ContributorAssignedDto.FromId(invited.Id.IdOrValue, true); } diff --git a/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs b/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs index caf36e07a..5b5a8fe65 100644 --- a/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs +++ b/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs @@ -80,7 +80,7 @@ namespace Squidex.Areas.Api.Controllers.Plans string redirectUri = null; - if (context.Result() is RedirectToCheckoutResult result) + if (context.PlainResult is RedirectToCheckoutResult result) { redirectUri = result.Url.ToString(); } diff --git a/src/Squidex/Pipeline/CommandMiddlewares/ETagCommandMiddleware.cs b/src/Squidex/Pipeline/CommandMiddlewares/ETagCommandMiddleware.cs index fb4c74dfa..ef14a8bb0 100644 --- a/src/Squidex/Pipeline/CommandMiddlewares/ETagCommandMiddleware.cs +++ b/src/Squidex/Pipeline/CommandMiddlewares/ETagCommandMiddleware.cs @@ -54,7 +54,7 @@ namespace Squidex.Pipeline.CommandMiddlewares await next(); - if (context.Result() is EntitySavedResult result) + if (context.PlainResult is EntitySavedResult result) { httpContextAccessor.HttpContext.Response.Headers[HeaderNames.ETag] = result.Version.ToString(); } diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs index d69dddfcc..537de9762 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs @@ -33,6 +33,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules private readonly string actionDump = "MyDump"; private readonly string actionName = "ValidAction"; private readonly string actionDescription = "MyDescription"; + private readonly Guid ruleId = Guid.NewGuid(); private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); private readonly TypeNameRegistry typeNameRegistry = new TypeNameRegistry(); @@ -87,10 +88,10 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Fact] public async Task Should_not_create_if_rule_disabled() { - var ruleConfig = ValidRule().Disable(); - var ruleEnvelope = Envelope.Create(new ContentCreated()); + var rule = ValidRule().Disable(); + var ruleEvent = Envelope.Create(new ContentCreated()); - var job = await sut.CreateJobAsync(ruleConfig, ruleEnvelope); + var job = await sut.CreateJobAsync(rule, ruleId, ruleEvent); Assert.Null(job); @@ -101,10 +102,10 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Fact] public async Task Should_not_create_job_for_invalid_event() { - var ruleConfig = ValidRule(); - var ruleEnvelope = Envelope.Create(new InvalidEvent()); + var rule = ValidRule(); + var ruleEvent = Envelope.Create(new InvalidEvent()); - var job = await sut.CreateJobAsync(ruleConfig, ruleEnvelope); + var job = await sut.CreateJobAsync(rule, ruleId, ruleEvent); Assert.Null(job); @@ -115,10 +116,10 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Fact] public async Task Should_not_create_job_if_no_trigger_handler_registered() { - var ruleConfig = new Rule(new InvalidTrigger(), new ValidAction()); - var ruleEnvelope = Envelope.Create(new ContentCreated()); + var rule = new Rule(new InvalidTrigger(), new ValidAction()); + var ruleEvent = Envelope.Create(new ContentCreated()); - var job = await sut.CreateJobAsync(ruleConfig, ruleEnvelope); + var job = await sut.CreateJobAsync(rule, ruleId, ruleEvent); Assert.Null(job); @@ -129,10 +130,10 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Fact] public async Task Should_not_create_job_if_no_action_handler_registered() { - var ruleConfig = new Rule(new ContentChangedTriggerV2(), new InvalidAction()); - var ruleEnvelope = Envelope.Create(new ContentCreated()); + var rule = new Rule(new ContentChangedTriggerV2(), new InvalidAction()); + var ruleEvent = Envelope.Create(new ContentCreated()); - var job = await sut.CreateJobAsync(ruleConfig, ruleEnvelope); + var job = await sut.CreateJobAsync(rule, ruleId, ruleEvent); Assert.Null(job); @@ -143,13 +144,13 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Fact] public async Task Should_not_create_job_if_not_triggered_with_precheck() { - var ruleConfig = ValidRule(); - var ruleEnvelope = Envelope.Create(new ContentCreated()); + var rule = ValidRule(); + var ruleEvent = Envelope.Create(new ContentCreated()); - A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, ruleConfig.Trigger)) + A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, rule.Trigger, ruleId)) .Returns(false); - var job = await sut.CreateJobAsync(ruleConfig, ruleEnvelope); + var job = await sut.CreateJobAsync(rule, ruleId, ruleEvent); Assert.Null(job); @@ -160,16 +161,16 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Fact] public async Task Should_not_create_job_if_not_triggered() { - var ruleConfig = ValidRule(); - var ruleEnvelope = Envelope.Create(new ContentCreated()); + var rule = ValidRule(); + var ruleEvent = Envelope.Create(new ContentCreated()); - A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, ruleConfig.Trigger)) + A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, rule.Trigger, ruleId)) .Returns(true); - A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, ruleConfig.Trigger)) + A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, rule.Trigger)) .Returns(false); - var job = await sut.CreateJobAsync(ruleConfig, ruleEnvelope); + var job = await sut.CreateJobAsync(rule, ruleId, ruleEvent); Assert.Null(job); } @@ -177,22 +178,22 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Fact] public async Task Should_not_create_job_if_too_old() { - var @event = new ContentCreated { SchemaId = schemaId, AppId = appId }; + var ruleEvent = new ContentCreated { SchemaId = schemaId, AppId = appId }; var now = SystemClock.Instance.GetCurrentInstant(); - var ruleConfig = ValidRule(); - var ruleEnvelope = Envelope.Create(@event); + var rule = ValidRule(); + var ruleEnvelope = Envelope.Create(ruleEvent); ruleEnvelope.SetTimestamp(now.Minus(Duration.FromDays(3))); A.CallTo(() => clock.GetCurrentInstant()) .Returns(now); - A.CallTo(() => ruleActionHandler.CreateJobAsync(A.Ignored, ruleConfig.Action)) + A.CallTo(() => ruleActionHandler.CreateJobAsync(A.Ignored, rule.Action)) .Returns((actionDescription, actionData)); - var job = await sut.CreateJobAsync(ruleConfig, ruleEnvelope); + var job = await sut.CreateJobAsync(rule, ruleId, ruleEnvelope); Assert.Null(job); @@ -203,28 +204,28 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Fact] public async Task Should_create_job_if_triggered() { - var @event = new ContentCreated { SchemaId = schemaId, AppId = appId }; + var ruleEvent = new ContentCreated { SchemaId = schemaId, AppId = appId }; var now = Instant.FromUnixTimeSeconds(SystemClock.Instance.GetCurrentInstant().ToUnixTimeSeconds()); - var ruleConfig = ValidRule(); - var ruleEnvelope = Envelope.Create(@event); + var rule = ValidRule(); + var ruleEnvelope = Envelope.Create(ruleEvent); ruleEnvelope.SetTimestamp(now); A.CallTo(() => clock.GetCurrentInstant()) .Returns(now); - A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, ruleConfig.Trigger)) + A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, rule.Trigger)) .Returns(true); - A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, ruleConfig.Trigger)) + A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, rule.Trigger, ruleId)) .Returns(true); - A.CallTo(() => ruleActionHandler.CreateJobAsync(A.Ignored, ruleConfig.Action)) + A.CallTo(() => ruleActionHandler.CreateJobAsync(A.Ignored, rule.Action)) .Returns((actionDescription, new ValidData { Value = 10 })); - var job = await sut.CreateJobAsync(ruleConfig, ruleEnvelope); + var job = await sut.CreateJobAsync(rule, ruleId, ruleEnvelope); Assert.Equal(actionData, job.ActionData); Assert.Equal(actionName, job.ActionName); @@ -233,7 +234,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules Assert.Equal(now, job.Created); Assert.Equal(now.Plus(Duration.FromDays(2)), job.Expires); - Assert.Equal(@event.AppId.Id, job.AppId); + Assert.Equal(ruleEvent.AppId.Id, job.AppId); Assert.NotEqual(Guid.Empty, job.JobId); } diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/AssetChangedTriggerTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/AssetChangedTriggerTests.cs index 4bc0ff28f..3ea7cdeb2 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/AssetChangedTriggerTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/AssetChangedTriggerTests.cs @@ -39,7 +39,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers { TestForCondition(string.Empty, trigger => { - var result = sut.Trigger(new ContentCreated(), trigger); + var result = sut.Trigger(new ContentCreated(), trigger, Guid.NewGuid()); Assert.False(result); }); @@ -50,7 +50,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers { TestForCondition(string.Empty, trigger => { - var result = sut.Trigger(new AssetCreated(), trigger); + var result = sut.Trigger(new AssetCreated(), trigger, Guid.NewGuid()); Assert.True(result); }); diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/ContentChangedTriggerTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/ContentChangedTriggerTests.cs index a9e5bc937..467cb9378 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/ContentChangedTriggerTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/ContentChangedTriggerTests.cs @@ -28,6 +28,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers { private readonly IScriptEngine scriptEngine = A.Fake(); private readonly IRuleTriggerHandler sut; + private readonly Guid ruleId = Guid.NewGuid(); private static readonly NamedId SchemaMatch = NamedId.Of(Guid.NewGuid(), "my-schema1"); private static readonly NamedId SchemaNonMatch = NamedId.Of(Guid.NewGuid(), "my-schema2"); @@ -47,7 +48,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers { TestForTrigger(handleAll: true, schemaId: null, condition: null, action: trigger => { - var result = sut.Trigger(new AssetCreated(), trigger); + var result = sut.Trigger(new AssetCreated(), trigger, ruleId); Assert.False(result); }); @@ -58,7 +59,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers { TestForTrigger(handleAll: false, schemaId: null, condition: null, action: trigger => { - var result = sut.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger); + var result = sut.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger, ruleId); Assert.False(result); }); @@ -69,7 +70,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers { TestForTrigger(handleAll: true, schemaId: SchemaMatch, condition: null, action: trigger => { - var result = sut.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger); + var result = sut.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger, ruleId); Assert.True(result); }); @@ -80,7 +81,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers { TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: string.Empty, action: trigger => { - var result = sut.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger); + var result = sut.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger, ruleId); Assert.True(result); }); @@ -91,7 +92,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers { TestForTrigger(handleAll: false, schemaId: SchemaNonMatch, condition: null, action: trigger => { - var result = sut.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger); + var result = sut.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger, ruleId); Assert.False(result); }); diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/UsageTriggerHandlerTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/UsageTriggerHandlerTests.cs index b3eb86013..def006016 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/UsageTriggerHandlerTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/UsageTriggerHandlerTests.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; using Squidex.Domain.Apps.Core.HandleRules.Triggers; @@ -17,38 +18,39 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers { public class UsageTriggerHandlerTests { + private readonly Guid ruleId = Guid.NewGuid(); private readonly IRuleTriggerHandler sut = new UsageTriggerHandler(); [Fact] public void Should_not_trigger_precheck_when_event_type_not_correct() { - var result = sut.Trigger(new ContentCreated(), new UsageTrigger()); + var result = sut.Trigger(new ContentCreated(), new UsageTrigger(), ruleId); Assert.False(result); } [Fact] - public void Should_trigger_precheck_when_event_type_correct() + public void Should_not_trigger_precheck_when_rule_id_not_matchs() { - var result = sut.Trigger(new AppUsageExceeded(), new UsageTrigger()); + var result = sut.Trigger(new AppUsageExceeded { RuleId = Guid.NewGuid() }, new UsageTrigger(), ruleId); Assert.True(result); } [Fact] - public void Should_not_trigger_check_when_event_type_not_correct() + public void Should_trigger_precheck_when_event_type_correct_and_rule_id_matchs() { - var result = sut.Trigger(new EnrichedContentEvent(), new UsageTrigger()); + var result = sut.Trigger(new AppUsageExceeded { RuleId = ruleId }, new UsageTrigger(), ruleId); - Assert.False(result); + Assert.True(result); } [Fact] - public void Should_trigger_check_when_type_correct() + public void Should_not_trigger_check_when_event_type_not_correct() { - var result = sut.Trigger(new AppUsageExceeded(), new UsageTrigger()); + var result = sut.Trigger(new EnrichedContentEvent(), new UsageTrigger()); - Assert.True(result); + Assert.False(result); } } } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs index 0822d1b77..a38e3a2da 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs @@ -28,9 +28,9 @@ namespace Squidex.Domain.Apps.Entities.Rules private readonly IAppProvider appProvider = A.Fake(); private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); private readonly IRuleEventRepository ruleEventRepository = A.Fake(); - private readonly RuleService ruleService = A.Fake(); private readonly Instant now = SystemClock.Instance.GetCurrentInstant(); private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); + private readonly RuleService ruleService = A.Fake(); private readonly RuleEnqueuer sut; public sealed class TestAction : RuleAction @@ -72,38 +72,28 @@ namespace Squidex.Domain.Apps.Entities.Rules var rule1 = new Rule(new ContentChangedTriggerV2(), new TestAction { Url = new Uri("https://squidex.io") }); var rule2 = new Rule(new ContentChangedTriggerV2(), new TestAction { Url = new Uri("https://squidex.io") }); - var rule3 = new Rule(new ContentChangedTriggerV2(), new TestAction { Url = new Uri("https://squidex.io") }); var job1 = new RuleJob { Created = now }; - var job2 = new RuleJob { Created = now }; var ruleEntity1 = A.Fake(); var ruleEntity2 = A.Fake(); - var ruleEntity3 = A.Fake(); A.CallTo(() => ruleEntity1.RuleDef).Returns(rule1); A.CallTo(() => ruleEntity2.RuleDef).Returns(rule2); - A.CallTo(() => ruleEntity3.RuleDef).Returns(rule3); A.CallTo(() => appProvider.GetRulesAsync(appId.Id)) - .Returns(new List { ruleEntity1, ruleEntity2, ruleEntity3 }); + .Returns(new List { ruleEntity1, ruleEntity2 }); - A.CallTo(() => ruleService.CreateJobAsync(rule1, @event)) + A.CallTo(() => ruleService.CreateJobAsync(rule1, ruleEntity1.Id, @event)) .Returns(job1); - A.CallTo(() => ruleService.CreateJobAsync(rule2, @event)) - .Returns(job2); - - A.CallTo(() => ruleService.CreateJobAsync(rule3, @event)) + A.CallTo(() => ruleService.CreateJobAsync(rule2, ruleEntity2.Id, @event)) .Returns(Task.FromResult(null)); await sut.On(@event); A.CallTo(() => ruleEventRepository.EnqueueAsync(job1, now)) .MustHaveHappened(); - - A.CallTo(() => ruleEventRepository.EnqueueAsync(job2, now)) - .MustHaveHappened(); } } } \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/Commands/CommandContextTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/CommandContextTests.cs index 3d97d2a33..8d8793984 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/CommandContextTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Commands/CommandContextTests.cs @@ -26,8 +26,13 @@ namespace Squidex.Infrastructure.Commands public void Should_instantiate_and_provide_command() { Assert.Equal(command, sut.Command); - Assert.False(sut.IsCompleted); + + Assert.Null(sut.PlainResult); + Assert.Null(sut.Result()); + Assert.NotEqual(Guid.Empty, sut.ContextId); + + Assert.False(sut.IsCompleted); } [Fact] @@ -39,9 +44,19 @@ namespace Squidex.Infrastructure.Commands } [Fact] - public void Should_provide_result_valid_when_succeeded_with_value() + public void Should_provide_result_when_succeeded_with_value() + { + sut.Complete("RESULT"); + + Assert.Equal("RESULT", sut.Result()); + } + + [Fact] + public void Should_provide_plain_result_when_succeeded_with_value() { sut.Complete("RESULT"); + + Assert.Equal("RESULT", sut.PlainResult); } } } From 9b06a1a7efb829bd230d818e655d2078bf10aa86 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sat, 12 Jan 2019 13:20:50 +0100 Subject: [PATCH 4/7] Refactorings. --- .../HandleRules/ClientPool.cs | 4 +- .../EnrichedUsageExceededEvent.cs | 4 +- .../HandleRules/EventEnricher.cs | 78 +++++++ .../HandleRules/IEventEnricher.cs | 2 +- .../HandleRules/IRuleTriggerHandler.cs | 6 +- .../HandleRules/RuleService.cs | 34 +-- .../HandleRules/RuleTriggerHandler.cs | 33 ++- .../Triggers/AssetChangedTriggerHandler.cs | 32 --- .../Assets/AssetChangedTriggerHandler.cs | 73 ++++++ .../Contents}/ContentChangedTriggerHandler.cs | 64 +++++- .../Rules/EventEnricher.cs | 207 ------------------ .../UsageTrackerCommandMiddleware.cs | 65 ++++++ .../Rules/UsageTracking/UsageTrackerGrain.cs | 3 +- .../UsageTracking}/UsageTriggerHandler.cs | 21 +- .../SquidexHeaderExtensions.cs | 2 +- .../EventSourcing/Envelope.cs | 4 +- .../EventSourcing/EnvelopeExtensions.cs | 12 +- .../EventSourcing/Envelope{T}.cs | 9 +- .../InstantExtensions.cs | 19 ++ .../Log/IArrayWriter.cs | 5 +- .../Log/IObjectWriter.cs | 5 +- .../Log/Internal/FileLogProcessor.cs | 3 +- .../Log/JsonLogWriter.cs | 29 +-- .../Log/TimestampLogAppender.cs | 17 +- src/Squidex.Infrastructure/RetryWindow.cs | 21 +- .../States/StoreExtensions.cs | 5 - src/Squidex/Config/Domain/RuleServices.cs | 4 +- .../EnrichContent/ContentEnrichmentTests.cs | 2 +- .../HandleRules/RuleEventFormatterTests.cs | 4 +- .../HandleRules/RuleServiceTests.cs | 131 ++++++----- .../ValidateContent/DateTimeFieldTests.cs | 4 +- .../Assets}/AssetChangedTriggerTests.cs | 41 +++- .../Comments/Guards/GuardCommentsTests.cs | 18 +- .../Contents}/ContentChangedTriggerTests.cs | 42 +++- .../Contents/GraphQL/GraphQLTestBase.cs | 6 +- .../Rules/RuleDequeuerTests.cs | 17 +- .../Rules/RuleEnqueuerTests.cs | 2 +- .../UsageTriggerHandlerTests.cs | 16 +- .../DateTimeFieldPropertiesTests.cs | 3 +- ...richWithTimestampCommandMiddlewareTests.cs | 22 +- .../CompoundEventConsumerTests.cs | 2 +- .../DefaultEventDataFormatterTests.cs | 6 +- .../EventSourcing/EnvelopeExtensionsTests.cs | 6 +- .../EventSourcing/EnvelopeTests.cs | 4 +- .../InstantExtensions.cs | 23 ++ .../Json/InstantConverterTests.cs | 5 +- .../ConverterContractResolverTests.cs | 6 +- .../Log/JsonLogWriterTests.cs | 37 +--- .../Log/SemanticLogTests.cs | 10 +- .../RetryWindowTests.cs | 42 ++-- .../States/PersistenceEventSourcingTests.cs | 12 +- 51 files changed, 718 insertions(+), 504 deletions(-) create mode 100644 src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventEnricher.cs delete mode 100644 src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/AssetChangedTriggerHandler.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs rename src/{Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers => Squidex.Domain.Apps.Entities/Contents}/ContentChangedTriggerHandler.cs (51%) delete mode 100644 src/Squidex.Domain.Apps.Entities/Rules/EventEnricher.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs rename src/{Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers => Squidex.Domain.Apps.Entities/Rules/UsageTracking}/UsageTriggerHandler.cs (51%) create mode 100644 src/Squidex.Infrastructure/InstantExtensions.cs rename tests/{Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers => Squidex.Domain.Apps.Entities.Tests/Assets}/AssetChangedTriggerTests.cs (70%) rename tests/{Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers => Squidex.Domain.Apps.Entities.Tests/Contents}/ContentChangedTriggerTests.cs (78%) rename tests/{Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers => Squidex.Domain.Apps.Entities.Tests/Rules/UsageTracking}/UsageTriggerHandlerTests.cs (76%) create mode 100644 tests/Squidex.Infrastructure.Tests/InstantExtensions.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/ClientPool.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/ClientPool.cs index b93a45a25..049de10ab 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/ClientPool.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/ClientPool.cs @@ -16,7 +16,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules { internal sealed class ClientPool { - private static readonly TimeSpan TTL = TimeSpan.FromMinutes(30); + private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(30); private readonly MemoryCache memoryCache = new MemoryCache(Options.Create(new MemoryCacheOptions())); private readonly Func> factory; @@ -41,7 +41,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules { client = await factory(key); - memoryCache.Set(key, client, TTL); + memoryCache.Set(key, client, CacheDuration); } return client; diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUsageExceededEvent.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUsageExceededEvent.cs index ec4d672da..616db38f7 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUsageExceededEvent.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUsageExceededEvent.cs @@ -9,9 +9,9 @@ namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents { public sealed class EnrichedUsageExceededEvent : EnrichedEvent { - public long Current { get; set; } + public long CallsCurrent { get; set; } - public long Limit { get; set; } + public long CallsLimit { get; set; } public override long Partition { diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventEnricher.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventEnricher.cs new file mode 100644 index 000000000..3597d90c0 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventEnricher.cs @@ -0,0 +1,78 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Memory; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public sealed class EventEnricher : IEventEnricher + { + private static readonly TimeSpan UserCacheDuration = TimeSpan.FromMinutes(10); + private readonly IMemoryCache userCache; + private readonly IUserResolver userResolver; + + public EventEnricher(IMemoryCache userCache, IUserResolver userResolver) + { + Guard.NotNull(userCache, nameof(userCache)); + Guard.NotNull(userResolver, nameof(userResolver)); + + this.userCache = userCache; + this.userResolver = userResolver; + } + + public async Task EnrichAsync(EnrichedEvent enrichedEvent, Envelope @event) + { + enrichedEvent.Timestamp = @event.Headers.Timestamp(); + + if (enrichedEvent is EnrichedUserEvent userEvent) + { + if (@event.Payload is SquidexEvent squidexEvent) + { + userEvent.Actor = squidexEvent.Actor; + } + + userEvent.User = await FindUserAsync(userEvent.Actor); + } + + enrichedEvent.AppId = @event.Payload.AppId; + } + + private Task FindUserAsync(RefToken actor) + { + var key = $"EventEnrichers_Users_${actor.Identifier}"; + + return userCache.GetOrCreateAsync(key, async x => + { + x.AbsoluteExpirationRelativeToNow = UserCacheDuration; + + IUser user; + try + { + user = await userResolver.FindByIdOrEmailAsync(actor.Identifier); + } + catch + { + user = null; + } + + if (user == null && actor.Type.Equals(RefTokenType.Client, StringComparison.OrdinalIgnoreCase)) + { + user = new ClientUser(actor); + } + + return user; + }); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IEventEnricher.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IEventEnricher.cs index 6d2e7961d..1346f9b80 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IEventEnricher.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IEventEnricher.cs @@ -14,6 +14,6 @@ namespace Squidex.Domain.Apps.Core.HandleRules { public interface IEventEnricher { - Task EnrichAsync(Envelope @event); + Task EnrichAsync(EnrichedEvent enrichedEvent, Envelope @event); } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs index 5b27a05b9..b7cd99f60 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs @@ -6,8 +6,10 @@ // ========================================================================== using System; +using System.Threading.Tasks; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Events; using Squidex.Infrastructure.EventSourcing; namespace Squidex.Domain.Apps.Core.HandleRules @@ -16,8 +18,10 @@ namespace Squidex.Domain.Apps.Core.HandleRules { Type TriggerType { get; } + Task CreateEnrichedEventAsync(Envelope @event); + bool Trigger(EnrichedEvent @event, RuleTrigger trigger); - bool Trigger(IEvent @event, RuleTrigger trigger, Guid ruleId); + bool Trigger(AppEvent @event, RuleTrigger trigger, Guid ruleId); } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs index b7a0050b7..a1f5e3b7b 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs @@ -7,7 +7,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -71,6 +70,8 @@ namespace Squidex.Domain.Apps.Core.HandleRules return null; } + var typed = @event.To(); + var actionType = rule.Action.GetType(); if (!ruleTriggerHandlers.TryGetValue(rule.Trigger.GetType(), out var triggerHandler)) @@ -83,11 +84,6 @@ namespace Squidex.Domain.Apps.Core.HandleRules return null; } - if (!triggerHandler.Trigger(@event.Payload, rule.Trigger, ruleId)) - { - return null; - } - var now = clock.GetCurrentInstant(); var eventTime = @@ -102,9 +98,21 @@ namespace Squidex.Domain.Apps.Core.HandleRules return null; } + if (!triggerHandler.Trigger(typed.Payload, rule.Trigger, ruleId)) + { + return null; + } + var appEventEnvelope = @event.To(); - var enrichedEvent = await eventEnricher.EnrichAsync(appEventEnvelope); + var enrichedEvent = await triggerHandler.CreateEnrichedEventAsync(appEventEnvelope); + + if (enrichedEvent == null) + { + return null; + } + + await eventEnricher.EnrichAsync(enrichedEvent, typed); if (!triggerHandler.Trigger(enrichedEvent, rule.Trigger)) { @@ -137,7 +145,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules try { var actionType = typeNameRegistry.GetType(actionName); - var actionWatch = Stopwatch.StartNew(); + var actionWatch = ValueStopwatch.StartNew(); var actionHandler = ruleActionHandlers[actionType]; @@ -145,12 +153,12 @@ namespace Squidex.Domain.Apps.Core.HandleRules var result = await actionHandler.ExecuteJobAsync(deserialized); - actionWatch.Stop(); + var elapsed = TimeSpan.FromMilliseconds(actionWatch.Stop()); var dumpBuilder = new StringBuilder(result.Dump); dumpBuilder.AppendLine(); - dumpBuilder.AppendFormat("Elapsed {0}.", actionWatch.Elapsed); + dumpBuilder.AppendFormat("Elapsed {0}.", elapsed); dumpBuilder.AppendLine(); if (result.Exception is TimeoutException || result.Exception is OperationCanceledException) @@ -158,15 +166,15 @@ namespace Squidex.Domain.Apps.Core.HandleRules dumpBuilder.AppendLine(); dumpBuilder.AppendLine("Action timed out."); - return (dumpBuilder.ToString(), RuleResult.Timeout, actionWatch.Elapsed); + return (dumpBuilder.ToString(), RuleResult.Timeout, elapsed); } else if (result.Exception != null) { - return (dumpBuilder.ToString(), RuleResult.Failed, actionWatch.Elapsed); + return (dumpBuilder.ToString(), RuleResult.Failed, elapsed); } else { - return (dumpBuilder.ToString(), RuleResult.Success, actionWatch.Elapsed); + return (dumpBuilder.ToString(), RuleResult.Success, elapsed); } } catch (Exception ex) diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs index 7e62cad86..d5f8d47df 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs @@ -6,15 +6,19 @@ // ========================================================================== using System; +using System.Threading.Tasks; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Events; using Squidex.Infrastructure.EventSourcing; +#pragma warning disable IDE0019 // Use pattern matching + namespace Squidex.Domain.Apps.Core.HandleRules { public abstract class RuleTriggerHandler : IRuleTriggerHandler where TTrigger : RuleTrigger - where TEvent : IEvent + where TEvent : AppEvent where TEnrichedEvent : EnrichedEvent { public Type TriggerType @@ -22,16 +26,37 @@ namespace Squidex.Domain.Apps.Core.HandleRules get { return typeof(TTrigger); } } + async Task IRuleTriggerHandler.CreateEnrichedEventAsync(Envelope @event) + { + return await CreateEnrichedEventAsync(@event.To()); + } + bool IRuleTriggerHandler.Trigger(EnrichedEvent @event, RuleTrigger trigger) { - return @event is TEnrichedEvent e && Trigger(e, (TTrigger)trigger); + var typed = @event as TEnrichedEvent; + + if (typed != null) + { + return Trigger(typed, (TTrigger)trigger); + } + + return false; } - bool IRuleTriggerHandler.Trigger(IEvent @event, RuleTrigger trigger, Guid ruleId) + bool IRuleTriggerHandler.Trigger(AppEvent @event, RuleTrigger trigger, Guid ruleId) { - return @event is TEvent e && Trigger(e, (TTrigger)trigger, ruleId); + var typed = @event as TEvent; + + if (typed != null) + { + return Trigger(typed, (TTrigger)trigger, ruleId); + } + + return false; } + protected abstract Task CreateEnrichedEventAsync(Envelope @event); + protected abstract bool Trigger(TEnrichedEvent @event, TTrigger trigger); protected virtual bool Trigger(TEvent @event, TTrigger trigger, Guid ruleId) diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/AssetChangedTriggerHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/AssetChangedTriggerHandler.cs deleted file mode 100644 index e75e41b8a..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/AssetChangedTriggerHandler.cs +++ /dev/null @@ -1,32 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Events.Assets; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.HandleRules.Triggers -{ - public sealed class AssetChangedTriggerHandler : RuleTriggerHandler - { - private readonly IScriptEngine scriptEngine; - - public AssetChangedTriggerHandler(IScriptEngine scriptEngine) - { - Guard.NotNull(scriptEngine, nameof(scriptEngine)); - - this.scriptEngine = scriptEngine; - } - - protected override bool Trigger(EnrichedAssetEvent @event, AssetChangedTriggerV2 trigger) - { - return string.IsNullOrWhiteSpace(trigger.Condition) || scriptEngine.Evaluate("event", @event, trigger.Condition); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs new file mode 100644 index 000000000..7c23af11e --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs @@ -0,0 +1,73 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Orleans; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Events.Assets; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public sealed class AssetChangedTriggerHandler : RuleTriggerHandler + { + private readonly IScriptEngine scriptEngine; + private readonly IGrainFactory grainFactory; + + public AssetChangedTriggerHandler(IScriptEngine scriptEngine, IGrainFactory grainFactory) + { + Guard.NotNull(scriptEngine, nameof(scriptEngine)); + Guard.NotNull(grainFactory, nameof(grainFactory)); + + this.scriptEngine = scriptEngine; + + this.grainFactory = grainFactory; + } + + protected override async Task CreateEnrichedEventAsync(Envelope @event) + { + var result = new EnrichedAssetEvent(); + + var asset = + (await grainFactory + .GetGrain(@event.Payload.AssetId) + .GetStateAsync(@event.Headers.EventStreamNumber())).Value; + + SimpleMapper.Map(asset, result); + + switch (@event.Payload) + { + case AssetCreated _: + result.Type = EnrichedAssetEventType.Created; + break; + case AssetRenamed _: + result.Type = EnrichedAssetEventType.Renamed; + break; + case AssetUpdated _: + result.Type = EnrichedAssetEventType.Updated; + break; + case AssetDeleted _: + result.Type = EnrichedAssetEventType.Deleted; + break; + } + + result.Name = $"Asset{result.Type}"; + + return result; + } + + protected override bool Trigger(EnrichedAssetEvent @event, AssetChangedTriggerV2 trigger) + { + return string.IsNullOrWhiteSpace(trigger.Condition) || scriptEngine.Evaluate("event", @event, trigger.Condition); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs similarity index 51% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs rename to src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs index 1c95e6734..60726b883 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs @@ -6,23 +6,83 @@ // ========================================================================== using System; +using System.Threading.Tasks; +using Orleans; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; 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.EventSourcing; +using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Core.HandleRules.Triggers +namespace Squidex.Domain.Apps.Entities.Contents { public sealed class ContentChangedTriggerHandler : RuleTriggerHandler { private readonly IScriptEngine scriptEngine; + private readonly IGrainFactory grainFactory; - public ContentChangedTriggerHandler(IScriptEngine scriptEngine) + public ContentChangedTriggerHandler(IScriptEngine scriptEngine, IGrainFactory grainFactory) { Guard.NotNull(scriptEngine, nameof(scriptEngine)); + Guard.NotNull(grainFactory, nameof(grainFactory)); this.scriptEngine = scriptEngine; + + this.grainFactory = grainFactory; + } + + protected override async Task CreateEnrichedEventAsync(Envelope @event) + { + var result = new EnrichedContentEvent(); + + var content = + (await grainFactory + .GetGrain(@event.Payload.ContentId) + .GetStateAsync(@event.Headers.EventStreamNumber())).Value; + + SimpleMapper.Map(content, result); + + result.Data = content.Data ?? content.DataDraft; + + switch (@event.Payload) + { + case ContentCreated _: + result.Type = EnrichedContentEventType.Created; + break; + case ContentDeleted _: + result.Type = EnrichedContentEventType.Deleted; + break; + case ContentChangesPublished _: + case ContentUpdated _: + result.Type = EnrichedContentEventType.Updated; + break; + case ContentStatusChanged contentStatusChanged: + switch (contentStatusChanged.Change) + { + case StatusChange.Published: + result.Type = EnrichedContentEventType.Published; + break; + case StatusChange.Unpublished: + result.Type = EnrichedContentEventType.Unpublished; + break; + case StatusChange.Archived: + result.Type = EnrichedContentEventType.Archived; + break; + case StatusChange.Restored: + result.Type = EnrichedContentEventType.Restored; + break; + } + + break; + } + + result.Name = $"{content.SchemaId.Name.ToPascalCase()}{result.Type}"; + + return result; } protected override bool Trigger(ContentEvent @event, ContentChangedTriggerV2 trigger, Guid ruleId) diff --git a/src/Squidex.Domain.Apps.Entities/Rules/EventEnricher.cs b/src/Squidex.Domain.Apps.Entities/Rules/EventEnricher.cs deleted file mode 100644 index 803d90884..000000000 --- a/src/Squidex.Domain.Apps.Entities/Rules/EventEnricher.cs +++ /dev/null @@ -1,207 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Microsoft.Extensions.Caching.Memory; -using Orleans; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Domain.Apps.Entities.Contents; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Assets; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Reflection; -using Squidex.Shared.Users; - -namespace Squidex.Domain.Apps.Entities.Rules -{ - public sealed class EventEnricher : IEventEnricher - { - private static readonly TimeSpan UserCacheDuration = TimeSpan.FromMinutes(10); - private readonly IGrainFactory grainFactory; - private readonly IMemoryCache userCache; - private readonly IUserResolver userResolver; - - public EventEnricher(IGrainFactory grainFactory, IMemoryCache userCache, IUserResolver userResolver) - { - Guard.NotNull(grainFactory, nameof(grainFactory)); - Guard.NotNull(userCache, nameof(userCache)); - Guard.NotNull(userResolver, nameof(userResolver)); - - this.grainFactory = grainFactory; - this.userCache = userCache; - this.userResolver = userResolver; - } - - public async Task EnrichAsync(Envelope @event) - { - Guard.NotNull(@event, nameof(@event)); - - switch (@event.Payload) - { - case ContentEvent contentEvent: - { - var result = new EnrichedContentEvent(); - - await Task.WhenAll( - EnrichContentAsync(result, contentEvent, @event), - EnrichDefaultAsync(result, @event)); - - return result; - } - - case AssetEvent assetEvent: - { - var result = new EnrichedAssetEvent(); - - await Task.WhenAll( - EnrichAssetAsync(result, assetEvent, @event), - EnrichDefaultAsync(result, @event)); - - return result; - } - - case AppUsageExceeded usageExceeded: - { - var result = new EnrichedUsageExceededEvent { Current = usageExceeded.CallsCurrent, Limit = usageExceeded.CallsLimit }; - - await EnrichDefaultAsync(result, @event); - - return result; - } - } - - return null; - } - - private async Task EnrichAssetAsync(EnrichedAssetEvent result, AssetEvent assetEvent, Envelope @event) - { - var asset = - (await grainFactory - .GetGrain(assetEvent.AssetId) - .GetStateAsync(@event.Headers.EventStreamNumber())).Value; - - SimpleMapper.Map(asset, result); - - switch (assetEvent) - { - case AssetCreated _: - result.Type = EnrichedAssetEventType.Created; - break; - case AssetRenamed _: - result.Type = EnrichedAssetEventType.Renamed; - break; - case AssetUpdated _: - result.Type = EnrichedAssetEventType.Updated; - break; - case AssetDeleted _: - result.Type = EnrichedAssetEventType.Deleted; - break; - } - - result.Name = $"Asset{result.Type}"; - } - - private async Task EnrichContentAsync(EnrichedContentEvent result, ContentEvent contentEvent, Envelope @event) - { - var content = - (await grainFactory - .GetGrain(contentEvent.ContentId) - .GetStateAsync(@event.Headers.EventStreamNumber())).Value; - - SimpleMapper.Map(content, result); - - result.Data = content.Data ?? content.DataDraft; - - switch (contentEvent) - { - case ContentCreated _: - result.Type = EnrichedContentEventType.Created; - break; - case ContentDeleted _: - result.Type = EnrichedContentEventType.Deleted; - break; - case ContentChangesPublished _: - case ContentUpdated _: - result.Type = EnrichedContentEventType.Updated; - break; - case ContentStatusChanged contentStatusChanged: - switch (contentStatusChanged.Change) - { - case StatusChange.Published: - result.Type = EnrichedContentEventType.Published; - break; - case StatusChange.Unpublished: - result.Type = EnrichedContentEventType.Unpublished; - break; - case StatusChange.Archived: - result.Type = EnrichedContentEventType.Archived; - break; - case StatusChange.Restored: - result.Type = EnrichedContentEventType.Restored; - break; - } - - break; - } - - result.Name = $"{content.SchemaId.Name.ToPascalCase()}{result.Type}"; - } - - private async Task EnrichDefaultAsync(EnrichedEvent result, Envelope @event) - { - result.Timestamp = @event.Headers.Timestamp(); - - if (result is EnrichedUserEvent userEvent) - { - if (@event.Payload is SquidexEvent squidexEvent) - { - userEvent.Actor = squidexEvent.Actor; - } - - userEvent.User = await FindUserAsync(userEvent.Actor); - } - - if (@event.Payload is AppEvent appEvent) - { - result.AppId = appEvent.AppId; - } - } - - private Task FindUserAsync(RefToken actor) - { - var key = $"EventEnrichers_Users_${actor.Identifier}"; - - return userCache.GetOrCreateAsync(key, async x => - { - x.AbsoluteExpirationRelativeToNow = UserCacheDuration; - - IUser user; - try - { - user = await userResolver.FindByIdOrEmailAsync(actor.Identifier); - } - catch - { - user = null; - } - - if (user == null && actor.Type.Equals(RefTokenType.Client, StringComparison.OrdinalIgnoreCase)) - { - user = new ClientUser(actor); - } - - return user; - }); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs new file mode 100644 index 000000000..3556a8f47 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs @@ -0,0 +1,65 @@ +// ========================================================================== +// 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.Rules.Triggers; +using Squidex.Domain.Apps.Entities.Rules.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Orleans; + +namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking +{ + public sealed class UsageTrackerCommandMiddleware : ICommandMiddleware + { + private readonly IUsageTrackerGrain usageTrackerGrain; + + public UsageTrackerCommandMiddleware(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory, nameof(grainFactory)); + + usageTrackerGrain = grainFactory.GetGrain(SingleGrain.Id); + } + + public async Task HandleAsync(CommandContext context, Func next) + { + switch (context.Command) + { + case DeleteRule deleteRule: + await usageTrackerGrain.RemoveTargetAsync(deleteRule.RuleId); + break; + case EnableRule enableRule: + await usageTrackerGrain.ActivateTargetAsync(enableRule.RuleId); + break; + case DisableRule disableRule: + await usageTrackerGrain.DeactivateTargetAsync(disableRule.RuleId); + break; + case CreateRule createRule: + { + if (createRule.Trigger is UsageTrigger createdTrigger) + { + await usageTrackerGrain.AddTargetAsync(createRule.RuleId, createRule.AppId, createdTrigger.Limit); + } + + break; + } + + case UpdateRule ruleUpdated: + if (ruleUpdated.Trigger is UsageTrigger updatedTrigger) + { + await usageTrackerGrain.UpdateTargetAsync(ruleUpdated.RuleId, updatedTrigger.Limit); + } + + break; + } + + await next(); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs index 1ebc89e37..c1cda0185 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs @@ -12,6 +12,7 @@ using Orleans; using Orleans.Runtime; using Squidex.Domain.Apps.Events; using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.States; using Squidex.Infrastructure.UsageTracking; @@ -82,7 +83,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking RuleId = kvp.Key }; - await Persistence.WriteEventAsync(@event); + await Persistence.WriteEventAsync(Envelope.Create(@event)); } } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/UsageTriggerHandler.cs b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs similarity index 51% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/UsageTriggerHandler.cs rename to src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs index 71ed317f4..f5b17df98 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/UsageTriggerHandler.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs @@ -5,17 +5,34 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Core.HandleRules.Triggers +namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking { public sealed class UsageTriggerHandler : RuleTriggerHandler { + private const string EventName = "Usage exceeeded"; + + protected override Task CreateEnrichedEventAsync(Envelope @event) + { + var result = new EnrichedUsageExceededEvent + { + CallsCurrent = @event.Payload.CallsCurrent, + CallsLimit = @event.Payload.CallsLimit, + Name = EventName + }; + + return Task.FromResult(result); + } + protected override bool Trigger(EnrichedUsageExceededEvent @event, UsageTrigger trigger) { - return @event.Limit == trigger.Limit; + return @event.CallsLimit == trigger.Limit; } } } diff --git a/src/Squidex.Domain.Apps.Events/SquidexHeaderExtensions.cs b/src/Squidex.Domain.Apps.Events/SquidexHeaderExtensions.cs index dc2e05ad2..d4139ae71 100644 --- a/src/Squidex.Domain.Apps.Events/SquidexHeaderExtensions.cs +++ b/src/Squidex.Domain.Apps.Events/SquidexHeaderExtensions.cs @@ -17,7 +17,7 @@ namespace Squidex.Domain.Apps.Events return headers.GetGuid(SquidexHeaders.AppId); } - public static Envelope SetAppId(this Envelope envelope, Guid value) where T : class + public static Envelope SetAppId(this Envelope envelope, Guid value) where T : class, IEvent { envelope.Headers.Add(SquidexHeaders.AppId, value.ToString()); diff --git a/src/Squidex.Infrastructure/EventSourcing/Envelope.cs b/src/Squidex.Infrastructure/EventSourcing/Envelope.cs index aa4af3878..458fc7991 100644 --- a/src/Squidex.Infrastructure/EventSourcing/Envelope.cs +++ b/src/Squidex.Infrastructure/EventSourcing/Envelope.cs @@ -12,12 +12,12 @@ namespace Squidex.Infrastructure.EventSourcing { public static class Envelope { - public static Envelope Create(TPayload payload) where TPayload : IEvent + public static Envelope Create(TPayload payload) where TPayload : class, IEvent { var eventId = Guid.NewGuid(); var envelope = - new Envelope(payload) + new Envelope(payload) .SetEventId(eventId) .SetTimestamp(SystemClock.Instance.GetCurrentInstant()); diff --git a/src/Squidex.Infrastructure/EventSourcing/EnvelopeExtensions.cs b/src/Squidex.Infrastructure/EventSourcing/EnvelopeExtensions.cs index e08a6ec4f..82719fe96 100644 --- a/src/Squidex.Infrastructure/EventSourcing/EnvelopeExtensions.cs +++ b/src/Squidex.Infrastructure/EventSourcing/EnvelopeExtensions.cs @@ -20,7 +20,7 @@ namespace Squidex.Infrastructure.EventSourcing return headers.GetString(CommonHeaders.EventNumber); } - public static Envelope SetEventPosition(this Envelope envelope, string value) where T : class + public static Envelope SetEventPosition(this Envelope envelope, string value) where T : class, IEvent { envelope.Headers.Add(CommonHeaders.EventNumber, value); @@ -32,7 +32,7 @@ namespace Squidex.Infrastructure.EventSourcing return headers.GetLong(CommonHeaders.EventStreamNumber); } - public static Envelope SetEventStreamNumber(this Envelope envelope, long value) where T : class + public static Envelope SetEventStreamNumber(this Envelope envelope, long value) where T : class, IEvent { envelope.Headers.Add(CommonHeaders.EventStreamNumber, value); @@ -44,7 +44,7 @@ namespace Squidex.Infrastructure.EventSourcing return headers.GetGuid(CommonHeaders.CommitId); } - public static Envelope SetCommitId(this Envelope envelope, Guid value) where T : class + public static Envelope SetCommitId(this Envelope envelope, Guid value) where T : class, IEvent { envelope.Headers.Add(CommonHeaders.CommitId, value.ToString()); @@ -56,7 +56,7 @@ namespace Squidex.Infrastructure.EventSourcing return headers.GetGuid(CommonHeaders.AggregateId); } - public static Envelope SetAggregateId(this Envelope envelope, Guid value) where T : class + public static Envelope SetAggregateId(this Envelope envelope, Guid value) where T : class, IEvent { envelope.Headers.Add(CommonHeaders.AggregateId, value.ToString()); @@ -68,7 +68,7 @@ namespace Squidex.Infrastructure.EventSourcing return headers.GetGuid(CommonHeaders.EventId); } - public static Envelope SetEventId(this Envelope envelope, Guid value) where T : class + public static Envelope SetEventId(this Envelope envelope, Guid value) where T : class, IEvent { envelope.Headers.Add(CommonHeaders.EventId, value.ToString()); @@ -80,7 +80,7 @@ namespace Squidex.Infrastructure.EventSourcing return headers.GetInstant(CommonHeaders.Timestamp); } - public static Envelope SetTimestamp(this Envelope envelope, Instant value) where T : class + public static Envelope SetTimestamp(this Envelope envelope, Instant value) where T : class, IEvent { envelope.Headers.Add(CommonHeaders.Timestamp, value.ToString()); diff --git a/src/Squidex.Infrastructure/EventSourcing/Envelope{T}.cs b/src/Squidex.Infrastructure/EventSourcing/Envelope{T}.cs index de6ac36ae..ba1b59a34 100644 --- a/src/Squidex.Infrastructure/EventSourcing/Envelope{T}.cs +++ b/src/Squidex.Infrastructure/EventSourcing/Envelope{T}.cs @@ -7,7 +7,7 @@ namespace Squidex.Infrastructure.EventSourcing { - public class Envelope where T : class + public class Envelope where T : class, IEvent { private readonly EnvelopeHeaders headers; private readonly T payload; @@ -30,9 +30,14 @@ namespace Squidex.Infrastructure.EventSourcing this.headers = headers ?? new EnvelopeHeaders(); } - public Envelope To() where TOther : class + public Envelope To() where TOther : class, IEvent { return new Envelope(payload as TOther, headers.Clone()); } + + public static implicit operator Envelope(Envelope source) + { + return source == null ? source : new Envelope(source.payload, source.headers); + } } } diff --git a/src/Squidex.Infrastructure/InstantExtensions.cs b/src/Squidex.Infrastructure/InstantExtensions.cs new file mode 100644 index 000000000..3b9e37d8b --- /dev/null +++ b/src/Squidex.Infrastructure/InstantExtensions.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using NodaTime; + +namespace Squidex.Infrastructure +{ + public static class InstantExtensions + { + public static Instant WithoutMs(this Instant value) + { + return Instant.FromUnixTimeSeconds(value.ToUnixTimeSeconds()); + } + } +} diff --git a/src/Squidex.Infrastructure/Log/IArrayWriter.cs b/src/Squidex.Infrastructure/Log/IArrayWriter.cs index 8ec8b803a..d80dae6ed 100644 --- a/src/Squidex.Infrastructure/Log/IArrayWriter.cs +++ b/src/Squidex.Infrastructure/Log/IArrayWriter.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using NodaTime; namespace Squidex.Infrastructure.Log { @@ -21,9 +22,7 @@ namespace Squidex.Infrastructure.Log IArrayWriter WriteValue(TimeSpan value); - IArrayWriter WriteValue(DateTime value); - - IArrayWriter WriteValue(DateTimeOffset value); + IArrayWriter WriteValue(Instant value); IArrayWriter WriteObject(Action objectWriter); } diff --git a/src/Squidex.Infrastructure/Log/IObjectWriter.cs b/src/Squidex.Infrastructure/Log/IObjectWriter.cs index fc133875e..b1f7cbad1 100644 --- a/src/Squidex.Infrastructure/Log/IObjectWriter.cs +++ b/src/Squidex.Infrastructure/Log/IObjectWriter.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using NodaTime; namespace Squidex.Infrastructure.Log { @@ -21,9 +22,7 @@ namespace Squidex.Infrastructure.Log IObjectWriter WriteProperty(string property, TimeSpan value); - IObjectWriter WriteProperty(string property, DateTime value); - - IObjectWriter WriteProperty(string property, DateTimeOffset value); + IObjectWriter WriteProperty(string property, Instant value); IObjectWriter WriteObject(string property, Action objectWriter); diff --git a/src/Squidex.Infrastructure/Log/Internal/FileLogProcessor.cs b/src/Squidex.Infrastructure/Log/Internal/FileLogProcessor.cs index 491f32964..80ad22c26 100644 --- a/src/Squidex.Infrastructure/Log/Internal/FileLogProcessor.cs +++ b/src/Squidex.Infrastructure/Log/Internal/FileLogProcessor.cs @@ -10,6 +10,7 @@ using System.Collections.Concurrent; using System.IO; using System.Text; using System.Threading.Tasks; +using NodaTime; namespace Squidex.Infrastructure.Log.Internal { @@ -70,7 +71,7 @@ namespace Squidex.Infrastructure.Log.Internal AutoFlush = true }; - writer.WriteLine($"--- Started Logging {DateTime.UtcNow} ---", 1); + writer.WriteLine($"--- Started Logging {SystemClock.Instance.GetCurrentInstant()} ---", 1); } catch (Exception ex) { diff --git a/src/Squidex.Infrastructure/Log/JsonLogWriter.cs b/src/Squidex.Infrastructure/Log/JsonLogWriter.cs index b90cf64eb..413dbd136 100644 --- a/src/Squidex.Infrastructure/Log/JsonLogWriter.cs +++ b/src/Squidex.Infrastructure/Log/JsonLogWriter.cs @@ -6,9 +6,9 @@ // ========================================================================== using System; -using System.Globalization; using System.IO; using Newtonsoft.Json; +using NodaTime; namespace Squidex.Infrastructure.Log { @@ -73,23 +73,16 @@ namespace Squidex.Infrastructure.Log return this; } - IArrayWriter IArrayWriter.WriteValue(DateTime value) + IArrayWriter IArrayWriter.WriteValue(Instant value) { - jsonWriter.WriteValue(value.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture)); - - return this; - } - - IArrayWriter IArrayWriter.WriteValue(DateTimeOffset value) - { - jsonWriter.WriteValue(value.ToString("o", CultureInfo.InvariantCulture)); + jsonWriter.WriteValue(value.ToString()); return this; } IArrayWriter IArrayWriter.WriteValue(TimeSpan value) { - jsonWriter.WriteValue(value); + jsonWriter.WriteValue(value.ToString()); return this; } @@ -126,18 +119,10 @@ namespace Squidex.Infrastructure.Log return this; } - IObjectWriter IObjectWriter.WriteProperty(string property, DateTime value) + IObjectWriter IObjectWriter.WriteProperty(string property, Instant value) { jsonWriter.WritePropertyName(Format(property)); - jsonWriter.WriteValue(value.ToString("o", CultureInfo.InvariantCulture)); - - return this; - } - - IObjectWriter IObjectWriter.WriteProperty(string property, DateTimeOffset value) - { - jsonWriter.WritePropertyName(Format(property)); - jsonWriter.WriteValue(value.ToString("o", CultureInfo.InvariantCulture)); + jsonWriter.WriteValue(value.ToString()); return this; } @@ -145,7 +130,7 @@ namespace Squidex.Infrastructure.Log IObjectWriter IObjectWriter.WriteProperty(string property, TimeSpan value) { jsonWriter.WritePropertyName(Format(property)); - jsonWriter.WriteValue(value); + jsonWriter.WriteValue(value.ToString()); return this; } diff --git a/src/Squidex.Infrastructure/Log/TimestampLogAppender.cs b/src/Squidex.Infrastructure/Log/TimestampLogAppender.cs index f1ab37473..2e1b6bf30 100644 --- a/src/Squidex.Infrastructure/Log/TimestampLogAppender.cs +++ b/src/Squidex.Infrastructure/Log/TimestampLogAppender.cs @@ -5,29 +5,22 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; +using NodaTime; namespace Squidex.Infrastructure.Log { public sealed class TimestampLogAppender : ILogAppender { - private readonly Func timestamp; + private readonly IClock clock; - public TimestampLogAppender() - : this(() => DateTime.UtcNow) + public TimestampLogAppender(IClock clock = null) { - } - - public TimestampLogAppender(Func timestamp) - { - Guard.NotNull(timestamp, nameof(timestamp)); - - this.timestamp = timestamp; + this.clock = clock ?? SystemClock.Instance; } public void Append(IObjectWriter writer) { - writer.WriteProperty("timestamp", timestamp()); + writer.WriteProperty("timestamp", clock.GetCurrentInstant()); } } } diff --git a/src/Squidex.Infrastructure/RetryWindow.cs b/src/Squidex.Infrastructure/RetryWindow.cs index 0541b90db..ed155d2d8 100644 --- a/src/Squidex.Infrastructure/RetryWindow.cs +++ b/src/Squidex.Infrastructure/RetryWindow.cs @@ -7,19 +7,23 @@ using System; using System.Collections.Generic; +using NodaTime; namespace Squidex.Infrastructure { public sealed class RetryWindow { - private readonly TimeSpan windowDuration; + private readonly Duration windowDuration; private readonly int windowSize; - private readonly Queue retries = new Queue(); + private readonly Queue retries = new Queue(); + private readonly IClock clock; - public RetryWindow(TimeSpan windowDuration, int windowSize) + public RetryWindow(TimeSpan windowDuration, int windowSize, IClock clock = null) { - this.windowDuration = windowDuration; + this.windowDuration = Duration.FromTimeSpan(windowDuration); this.windowSize = windowSize + 1; + + this.clock = clock ?? SystemClock.Instance; } public void Reset() @@ -29,19 +33,16 @@ namespace Squidex.Infrastructure public bool CanRetryAfterFailure() { - return CanRetryAfterFailure(DateTime.UtcNow); - } + var now = clock.GetCurrentInstant(); - public bool CanRetryAfterFailure(DateTime utcNow) - { - retries.Enqueue(utcNow); + retries.Enqueue(now); while (retries.Count > windowSize) { retries.Dequeue(); } - return retries.Count < windowSize || (retries.Count > 0 && (utcNow - retries.Peek()) > windowDuration); + return retries.Count < windowSize || (retries.Count > 0 && (now - retries.Peek()) > windowDuration); } } } diff --git a/src/Squidex.Infrastructure/States/StoreExtensions.cs b/src/Squidex.Infrastructure/States/StoreExtensions.cs index 789c94ce1..8fd4d1dbd 100644 --- a/src/Squidex.Infrastructure/States/StoreExtensions.cs +++ b/src/Squidex.Infrastructure/States/StoreExtensions.cs @@ -17,11 +17,6 @@ namespace Squidex.Infrastructure.States return persistence.WriteEventsAsync(new[] { @event }); } - public static Task WriteEventAsync(this IPersistence persistence, IEvent @event) - { - return persistence.WriteEventsAsync(new[] { Envelope.Create(@event) }); - } - public static Task ClearSnapshotsAsync(this IStore store) { return store.GetSnapshotStore().ClearAsync(); diff --git a/src/Squidex/Config/Domain/RuleServices.cs b/src/Squidex/Config/Domain/RuleServices.cs index 0b30fda37..f178db4de 100644 --- a/src/Squidex/Config/Domain/RuleServices.cs +++ b/src/Squidex/Config/Domain/RuleServices.cs @@ -7,8 +7,10 @@ using Microsoft.Extensions.DependencyInjection; using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.HandleRules.Triggers; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Rules; +using Squidex.Domain.Apps.Entities.Rules.UsageTracking; using Squidex.Extensions.Actions; using Squidex.Infrastructure.EventSourcing; diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs index fff6720c4..13a33e087 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs @@ -193,7 +193,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EnrichContent private static Instant FutureDays(int days) { - return Instant.FromDateTimeUtc(DateTime.UtcNow.Date.AddDays(days)); + return SystemClock.Instance.GetCurrentInstant().WithoutMs().Plus(Duration.FromDays(days)); } } } 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 53bcf4106..3e5712af7 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs @@ -105,9 +105,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [InlineData("Script(`Date: ${formatDate(event.timestamp, 'yyyy-MM-dd')}, Full: ${formatDate(event.timestamp, 'yyyy-MM-dd-hh-mm-ss')}`)")] public void Should_replace_timestamp_information_from_event(string script) { - var now = DateTime.UtcNow; + var now = SystemClock.Instance.GetCurrentInstant(); - var envelope = new EnrichedContentEvent { Timestamp = Instant.FromDateTimeUtc(now) }; + var envelope = new EnrichedContentEvent { Timestamp = now }; var result = sut.Format(script, envelope); diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs index 537de9762..e4d693829 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs @@ -49,7 +49,6 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules public sealed class ValidAction : RuleAction { - public int Value { get; set; } } public sealed class ValidData @@ -70,8 +69,8 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules typeNameRegistry.Map(typeof(ContentCreated)); typeNameRegistry.Map(typeof(ValidAction), actionName); - A.CallTo(() => eventEnricher.EnrichAsync(A>.Ignored)) - .Returns(new EnrichedContentEvent { AppId = appId }); + A.CallTo(() => clock.GetCurrentInstant()) + .Returns(SystemClock.Instance.GetCurrentInstant().WithoutMs()); A.CallTo(() => ruleActionHandler.ActionType) .Returns(typeof(ValidAction)); @@ -86,58 +85,67 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules } [Fact] - public async Task Should_not_create_if_rule_disabled() + public async Task Should_not_create_job_if_rule_disabled() { - var rule = ValidRule().Disable(); - var ruleEvent = Envelope.Create(new ContentCreated()); + var @event = Envelope.Create(new ContentCreated()); - var job = await sut.CreateJobAsync(rule, ruleId, ruleEvent); + var job = await sut.CreateJobAsync(ValidRule().Disable(), ruleId, @event); Assert.Null(job); - A.CallTo(() => eventEnricher.EnrichAsync(A>.Ignored)) + A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, A.Ignored, ruleId)) .MustNotHaveHappened(); } [Fact] public async Task Should_not_create_job_for_invalid_event() { - var rule = ValidRule(); - var ruleEvent = Envelope.Create(new InvalidEvent()); + var @event = Envelope.Create(new InvalidEvent()); - var job = await sut.CreateJobAsync(rule, ruleId, ruleEvent); + var job = await sut.CreateJobAsync(ValidRule(), ruleId, @event); Assert.Null(job); - A.CallTo(() => eventEnricher.EnrichAsync(A>.Ignored)) + A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, A.Ignored, ruleId)) .MustNotHaveHappened(); } [Fact] public async Task Should_not_create_job_if_no_trigger_handler_registered() { - var rule = new Rule(new InvalidTrigger(), new ValidAction()); - var ruleEvent = Envelope.Create(new ContentCreated()); + var @event = Envelope.Create(new ContentCreated()); - var job = await sut.CreateJobAsync(rule, ruleId, ruleEvent); + var job = await sut.CreateJobAsync(RuleInvalidTrigger(), ruleId, @event); Assert.Null(job); - A.CallTo(() => eventEnricher.EnrichAsync(A>.Ignored)) + A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, A.Ignored, ruleId)) .MustNotHaveHappened(); } [Fact] public async Task Should_not_create_job_if_no_action_handler_registered() { - var rule = new Rule(new ContentChangedTriggerV2(), new InvalidAction()); - var ruleEvent = Envelope.Create(new ContentCreated()); + var @event = Envelope.Create(new ContentCreated()); - var job = await sut.CreateJobAsync(rule, ruleId, ruleEvent); + var job = await sut.CreateJobAsync(RuleInvalidAction(), ruleId, @event); Assert.Null(job); - A.CallTo(() => eventEnricher.EnrichAsync(A>.Ignored)) + A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, A.Ignored, ruleId)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_not_create_job_if_too_old() + { + var @event = Envelope.Create(new ContentCreated()).SetTimestamp(clock.GetCurrentInstant().Minus(Duration.FromDays(3))); + + var job = await sut.CreateJobAsync(ValidRule(), ruleId, @event); + + Assert.Null(job); + + A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, A.Ignored, ruleId)) .MustNotHaveHappened(); } @@ -145,87 +153,85 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules public async Task Should_not_create_job_if_not_triggered_with_precheck() { var rule = ValidRule(); - var ruleEvent = Envelope.Create(new ContentCreated()); - A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, rule.Trigger, ruleId)) + var @event = Envelope.Create(new ContentCreated()); + + A.CallTo(() => ruleTriggerHandler.Trigger(@event.Payload, rule.Trigger, ruleId)) .Returns(false); - var job = await sut.CreateJobAsync(rule, ruleId, ruleEvent); + var job = await sut.CreateJobAsync(rule, ruleId, @event); Assert.Null(job); - A.CallTo(() => eventEnricher.EnrichAsync(A>.Ignored)) + A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventAsync(A>.Ignored)) .MustNotHaveHappened(); } [Fact] - public async Task Should_not_create_job_if_not_triggered() + public async Task Should_not_create_job_if_enriched_event_not_created() { var rule = ValidRule(); - var ruleEvent = Envelope.Create(new ContentCreated()); - A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, rule.Trigger, ruleId)) + var @event = Envelope.Create(new ContentCreated()); + + A.CallTo(() => ruleTriggerHandler.Trigger(@event.Payload, rule.Trigger, ruleId)) .Returns(true); - A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, rule.Trigger)) - .Returns(false); + A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventAsync(A>.That.Matches(x => x.Payload == @event.Payload))) + .Returns(Task.FromResult(null)); - var job = await sut.CreateJobAsync(rule, ruleId, ruleEvent); + var job = await sut.CreateJobAsync(rule, ruleId, @event); Assert.Null(job); } [Fact] - public async Task Should_not_create_job_if_too_old() + public async Task Should_not_create_job_if_not_triggered() { - var ruleEvent = new ContentCreated { SchemaId = schemaId, AppId = appId }; + var rule = ValidRule(); - var now = SystemClock.Instance.GetCurrentInstant(); + var enrichedEvent = new EnrichedContentEvent { AppId = appId }; - var rule = ValidRule(); - var ruleEnvelope = Envelope.Create(ruleEvent); + var @event = Envelope.Create(new ContentCreated()); - ruleEnvelope.SetTimestamp(now.Minus(Duration.FromDays(3))); + A.CallTo(() => ruleTriggerHandler.Trigger(@event.Payload, rule.Trigger, ruleId)) + .Returns(true); - A.CallTo(() => clock.GetCurrentInstant()) - .Returns(now); + A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventAsync(A>.That.Matches(x => x.Payload == @event.Payload))) + .Returns(enrichedEvent); - A.CallTo(() => ruleActionHandler.CreateJobAsync(A.Ignored, rule.Action)) - .Returns((actionDescription, actionData)); + A.CallTo(() => ruleTriggerHandler.Trigger(enrichedEvent, rule.Trigger)) + .Returns(false); - var job = await sut.CreateJobAsync(rule, ruleId, ruleEnvelope); + var job = await sut.CreateJobAsync(rule, ruleId, @event); Assert.Null(job); - - A.CallTo(() => eventEnricher.EnrichAsync(A>.Ignored)) - .MustNotHaveHappened(); } [Fact] public async Task Should_create_job_if_triggered() { - var ruleEvent = new ContentCreated { SchemaId = schemaId, AppId = appId }; - - var now = Instant.FromUnixTimeSeconds(SystemClock.Instance.GetCurrentInstant().ToUnixTimeSeconds()); + var now = clock.GetCurrentInstant(); var rule = ValidRule(); - var ruleEnvelope = Envelope.Create(ruleEvent); - ruleEnvelope.SetTimestamp(now); + var enrichedEvent = new EnrichedContentEvent { AppId = appId }; - A.CallTo(() => clock.GetCurrentInstant()) - .Returns(now); + var @event = Envelope.Create(new ContentCreated()).SetTimestamp(now); - A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, rule.Trigger)) + A.CallTo(() => ruleTriggerHandler.Trigger(@event.Payload, rule.Trigger, ruleId)) .Returns(true); - A.CallTo(() => ruleTriggerHandler.Trigger(A.Ignored, rule.Trigger, ruleId)) + A.CallTo(() => ruleTriggerHandler.Trigger(enrichedEvent, rule.Trigger)) .Returns(true); + A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventAsync(A>.That.Matches(x => x.Payload == @event.Payload))) + .Returns(enrichedEvent); + A.CallTo(() => ruleActionHandler.CreateJobAsync(A.Ignored, rule.Action)) .Returns((actionDescription, new ValidData { Value = 10 })); - var job = await sut.CreateJobAsync(rule, ruleId, ruleEnvelope); + var job = await sut.CreateJobAsync(rule, ruleId, @event); Assert.Equal(actionData, job.ActionData); Assert.Equal(actionName, job.ActionName); @@ -234,9 +240,12 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules Assert.Equal(now, job.Created); Assert.Equal(now.Plus(Duration.FromDays(2)), job.Expires); - Assert.Equal(ruleEvent.AppId.Id, job.AppId); + Assert.Equal(enrichedEvent.AppId.Id, job.AppId); Assert.NotEqual(Guid.Empty, job.JobId); + + A.CallTo(() => eventEnricher.EnrichAsync(enrichedEvent, A>.That.Matches(x => x.Payload == @event.Payload))) + .MustHaveHappened(); } [Fact] @@ -250,7 +259,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules Assert.Equal(RuleResult.Success, result.Result); Assert.True(result.Elapsed >= TimeSpan.Zero); - Assert.StartsWith(actionDump, result.Dump, StringComparison.OrdinalIgnoreCase); + Assert.True(result.Dump.StartsWith(actionDump, StringComparison.OrdinalIgnoreCase)); } [Fact] @@ -296,6 +305,16 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules Assert.Equal((ruleError.ToString(), RuleResult.Failed, TimeSpan.Zero), result); } + private static Rule RuleInvalidAction() + { + return new Rule(new ContentChangedTriggerV2(), new InvalidAction()); + } + + private static Rule RuleInvalidTrigger() + { + return new Rule(new InvalidTrigger(), new ValidAction()); + } + private static Rule ValidRule() { return new Rule(new ContentChangedTriggerV2(), new ValidAction()); diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs index 6ae3fd19b..dfb26997c 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs @@ -5,12 +5,12 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; using NodaTime; using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; using Xunit; @@ -95,7 +95,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent private static Instant FutureDays(int days) { - return Instant.FromDateTimeUtc(DateTime.UtcNow.Date.AddDays(days)); + return SystemClock.Instance.GetCurrentInstant().WithoutMs().Plus(Duration.FromDays(days)); } private static IJsonValue CreateValue(Instant v) diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/AssetChangedTriggerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerTests.cs similarity index 70% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/AssetChangedTriggerTests.cs rename to tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerTests.cs index 3ea7cdeb2..19b3f2fd7 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/AssetChangedTriggerTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerTests.cs @@ -6,26 +6,34 @@ // ========================================================================== using System; +using System.Collections.Generic; +using System.Threading.Tasks; using FakeItEasy; +using Orleans; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Domain.Apps.Core.HandleRules.Triggers; using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Assets; using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Orleans; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers +#pragma warning disable SA1401 // Fields must be private + +namespace Squidex.Domain.Apps.Entities.Assets { public class AssetChangedTriggerTests { private readonly IScriptEngine scriptEngine = A.Fake(); + private readonly IGrainFactory grainFactory = A.Fake(); private readonly IRuleTriggerHandler sut; public AssetChangedTriggerTests() { - sut = new AssetChangedTriggerHandler(scriptEngine); + sut = new AssetChangedTriggerHandler(scriptEngine, grainFactory); A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, "true")) .Returns(true); @@ -34,6 +42,33 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers .Returns(false); } + public static IEnumerable TestEvents = new[] + { + new object[] { new AssetCreated(), EnrichedAssetEventType.Created }, + new object[] { new AssetUpdated(), EnrichedAssetEventType.Updated }, + new object[] { new AssetRenamed(), EnrichedAssetEventType.Renamed }, + new object[] { new AssetDeleted(), EnrichedAssetEventType.Deleted } + }; + + [Theory] + [MemberData(nameof(TestEvents))] + public async Task Should_enrich_events(AssetEvent @event, EnrichedAssetEventType type) + { + var envelope = Envelope.Create(@event).SetEventStreamNumber(12); + + var assetGrain = A.Fake(); + + A.CallTo(() => grainFactory.GetGrain(@event.AssetId, null)) + .Returns(assetGrain); + + A.CallTo(() => assetGrain.GetStateAsync(12)) + .Returns(A.Fake().AsJ()); + + var result = await sut.CreateEnrichedEventAsync(envelope); + + Assert.Equal(type, ((EnrichedAssetEvent)result).Type); + } + [Fact] public void Should_not_trigger_precheck_when_event_type_not_correct() { diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Comments/Guards/GuardCommentsTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Comments/Guards/GuardCommentsTests.cs index cb665e45a..fd142c7f2 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Comments/Guards/GuardCommentsTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Comments/Guards/GuardCommentsTests.cs @@ -46,7 +46,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.Guards var events = new List> { - Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To() + Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To() }; ValidationAssert.Throws(() => GuardComments.CanUpdate(events, command), @@ -61,7 +61,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.Guards var events = new List> { - Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To() + Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To() }; Assert.Throws(() => GuardComments.CanUpdate(events, command)); @@ -86,8 +86,8 @@ namespace Squidex.Domain.Apps.Entities.Comments.Guards var events = new List> { - Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To(), - Envelope.Create(new CommentDeleted { CommentId = commentId }).To() + Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To(), + Envelope.Create(new CommentDeleted { CommentId = commentId }).To() }; Assert.Throws(() => GuardComments.CanUpdate(events, command)); @@ -101,7 +101,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.Guards var events = new List> { - Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To() + Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To() }; GuardComments.CanUpdate(events, command); @@ -115,7 +115,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.Guards var events = new List> { - Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To() + Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To() }; Assert.Throws(() => GuardComments.CanDelete(events, command)); @@ -140,8 +140,8 @@ namespace Squidex.Domain.Apps.Entities.Comments.Guards var events = new List> { - Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To(), - Envelope.Create(new CommentDeleted { CommentId = commentId }).To() + Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }), + Envelope.Create(new CommentDeleted { CommentId = commentId }) }; Assert.Throws(() => GuardComments.CanDelete(events, command)); @@ -155,7 +155,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.Guards var events = new List> { - Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To() + Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To() }; GuardComments.CanDelete(events, command); diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/ContentChangedTriggerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerTests.cs similarity index 78% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/ContentChangedTriggerTests.cs rename to tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerTests.cs index 467cb9378..a2c601af5 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/ContentChangedTriggerTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerTests.cs @@ -8,25 +8,31 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Threading.Tasks; using FakeItEasy; +using Orleans; +using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Domain.Apps.Core.HandleRules.Triggers; using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Assets; using Squidex.Domain.Apps.Events.Contents; using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Orleans; using Xunit; #pragma warning disable SA1401 // Fields must be private #pragma warning disable RECS0070 -namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers +namespace Squidex.Domain.Apps.Entities.Contents { public class ContentChangedTriggerTests { private readonly IScriptEngine scriptEngine = A.Fake(); + private readonly IGrainFactory grainFactory = A.Fake(); private readonly IRuleTriggerHandler sut; private readonly Guid ruleId = Guid.NewGuid(); private static readonly NamedId SchemaMatch = NamedId.Of(Guid.NewGuid(), "my-schema1"); @@ -34,7 +40,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers public ContentChangedTriggerTests() { - sut = new ContentChangedTriggerHandler(scriptEngine); + sut = new ContentChangedTriggerHandler(scriptEngine, grainFactory); A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, "true")) .Returns(true); @@ -43,6 +49,36 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers .Returns(false); } + public static IEnumerable TestEvents = new[] + { + new object[] { new ContentCreated(), EnrichedContentEventType.Created }, + new object[] { new ContentUpdated(), EnrichedContentEventType.Updated }, + new object[] { new ContentDeleted(), EnrichedContentEventType.Deleted }, + new object[] { new ContentStatusChanged { Change = StatusChange.Archived }, EnrichedContentEventType.Archived }, + new object[] { new ContentStatusChanged { Change = StatusChange.Published }, EnrichedContentEventType.Published }, + new object[] { new ContentStatusChanged { Change = StatusChange.Restored }, EnrichedContentEventType.Restored }, + new object[] { new ContentStatusChanged { Change = StatusChange.Unpublished }, EnrichedContentEventType.Unpublished } + }; + + [Theory] + [MemberData(nameof(TestEvents))] + public async Task Should_enrich_events(ContentEvent @event, EnrichedContentEventType type) + { + var envelope = Envelope.Create(@event).SetEventStreamNumber(12); + + var contentGrain = A.Fake(); + + A.CallTo(() => grainFactory.GetGrain(@event.ContentId, null)) + .Returns(contentGrain); + + A.CallTo(() => contentGrain.GetStateAsync(12)) + .Returns(A.Fake().AsJ()); + + var result = await sut.CreateEnrichedEventAsync(envelope); + + Assert.Equal(type, ((EnrichedContentEvent)result).Type); + } + [Fact] public void Should_not_trigger_precheck_when_event_type_not_correct() { diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs index 6988c899d..7e1639b8e 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs @@ -12,7 +12,7 @@ using FakeItEasy; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using Newtonsoft.Json; -using NodaTime.Extensions; +using NodaTime; using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Contents; @@ -99,7 +99,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL protected static IContentEntity CreateContent(Guid id, Guid refId, Guid assetId, NamedContentData data = null) { - var now = DateTime.UtcNow.ToInstant(); + var now = SystemClock.Instance.GetCurrentInstant(); data = data ?? new NamedContentData() @@ -159,7 +159,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL protected static IAssetEntity CreateAsset(Guid id) { - var now = DateTime.UtcNow.ToInstant(); + var now = SystemClock.Instance.GetCurrentInstant(); var asset = new FakeAssetEntity { diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerTests.cs index 68a3a2eb8..ee1183609 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerTests.cs @@ -12,6 +12,7 @@ using NodaTime; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Entities.Rules.Repositories; +using Squidex.Infrastructure; using Squidex.Infrastructure.Log; using Xunit; @@ -24,19 +25,15 @@ namespace Squidex.Domain.Apps.Entities.Rules private readonly IClock clock = A.Fake(); private readonly ISemanticLog log = A.Dummy(); private readonly IRuleEventRepository ruleEventRepository = A.Fake(); - private readonly Instant now = SystemClock.Instance.GetCurrentInstant(); private readonly RuleService ruleService = A.Fake(); private readonly RuleDequeuerGrain sut; public RuleDequeuerTests() { - A.CallTo(() => clock.GetCurrentInstant()).Returns(now); + A.CallTo(() => clock.GetCurrentInstant()) + .Returns(SystemClock.Instance.GetCurrentInstant().WithoutMs()); - sut = new RuleDequeuerGrain( - ruleService, - ruleEventRepository, - log, - clock); + sut = new RuleDequeuerGrain(ruleService, ruleEventRepository, log, clock); } [Theory] @@ -63,7 +60,7 @@ namespace Squidex.Domain.Apps.Entities.Rules if (minutes > 0) { - nextCall = now.Plus(Duration.FromMinutes(minutes)); + nextCall = clock.GetCurrentInstant().Plus(Duration.FromMinutes(minutes)); } await sut.HandleAsync(@event); @@ -81,12 +78,12 @@ namespace Squidex.Domain.Apps.Entities.Rules JobId = Guid.NewGuid(), ActionData = actionData, ActionName = actionName, - Created = now + Created = clock.GetCurrentInstant() }; A.CallTo(() => @event.Id).Returns(Guid.NewGuid()); A.CallTo(() => @event.Job).Returns(job); - A.CallTo(() => @event.Created).Returns(now); + A.CallTo(() => @event.Created).Returns(clock.GetCurrentInstant()); A.CallTo(() => @event.NumCalls).Returns(numCalls); return @event; diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs index a38e3a2da..b8fcb0d48 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs @@ -68,7 +68,7 @@ namespace Squidex.Domain.Apps.Entities.Rules [Fact] public async Task Should_update_repositories_on_with_jobs_from_sender() { - var @event = Envelope.Create(new ContentCreated { AppId = appId }); + var @event = Envelope.Create(new ContentCreated { AppId = appId }); var rule1 = new Rule(new ContentChangedTriggerV2(), new TestAction { Url = new Uri("https://squidex.io") }); var rule2 = new Rule(new ContentChangedTriggerV2(), new TestAction { Url = new Uri("https://squidex.io") }); diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/UsageTriggerHandlerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/UsageTracking/UsageTriggerHandlerTests.cs similarity index 76% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/UsageTriggerHandlerTests.cs rename to tests/Squidex.Domain.Apps.Entities.Tests/Rules/UsageTracking/UsageTriggerHandlerTests.cs index def006016..b8ca9421b 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/UsageTriggerHandlerTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/UsageTracking/UsageTriggerHandlerTests.cs @@ -6,15 +6,16 @@ // ========================================================================== using System; +using System.Threading.Tasks; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Domain.Apps.Core.HandleRules.Triggers; using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure.EventSourcing; using Xunit; -namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers +namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking { public class UsageTriggerHandlerTests { @@ -52,5 +53,16 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers Assert.False(result); } + + [Fact] + public async Task Should_create_enriched_event() + { + var @event = new AppUsageExceeded { CallsCurrent = 80, CallsLimit = 120 }; + + var result = (EnrichedUsageExceededEvent)await sut.CreateEnrichedEventAsync(Envelope.Create(@event)); + + Assert.Equal(@event.CallsCurrent, result.CallsCurrent); + Assert.Equal(@event.CallsLimit, result.CallsLimit); + } } } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/DateTimeFieldPropertiesTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/DateTimeFieldPropertiesTests.cs index 7137b4527..6cfa3feef 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/DateTimeFieldPropertiesTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/DateTimeFieldPropertiesTests.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using System.Collections.Generic; using System.Linq; using FluentAssertions; @@ -119,7 +118,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards.FieldProperties private static Instant FutureDays(int days) { - return Instant.FromDateTimeUtc(DateTime.UtcNow.Date.AddDays(days)); + return SystemClock.Instance.GetCurrentInstant().WithoutMs().Plus(Duration.FromDays(days)); } } } \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/Commands/EnrichWithTimestampCommandMiddlewareTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/EnrichWithTimestampCommandMiddlewareTests.cs index 491f9e7d1..4bd06870e 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/EnrichWithTimestampCommandMiddlewareTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Commands/EnrichWithTimestampCommandMiddlewareTests.cs @@ -17,31 +17,33 @@ namespace Squidex.Infrastructure.Commands { private readonly IClock clock = A.Fake(); private readonly ICommandBus commandBus = A.Dummy(); + private readonly EnrichWithTimestampCommandMiddleware sut; - [Fact] - public async Task Should_set_timestamp_for_timestamp_command() + public EnrichWithTimestampCommandMiddlewareTests() { - var utc = Instant.FromUnixTimeSeconds(1000); - var sut = new EnrichWithTimestampCommandMiddleware(clock); - A.CallTo(() => clock.GetCurrentInstant()) - .Returns(utc); + .Returns(SystemClock.Instance.GetCurrentInstant().WithoutMs()); + + sut = new EnrichWithTimestampCommandMiddleware(clock); + } + [Fact] + public async Task Should_set_timestamp_for_timestamp_command() + { var command = new MyCommand(); await sut.HandleAsync(new CommandContext(command, commandBus)); - Assert.Equal(utc, command.Timestamp); + Assert.Equal(clock.GetCurrentInstant(), command.Timestamp); } [Fact] public async Task Should_do_nothing_for_normal_command() { - var sut = new EnrichWithTimestampCommandMiddleware(clock); - await sut.HandleAsync(new CommandContext(A.Dummy(), commandBus)); - A.CallTo(() => clock.GetCurrentInstant()).MustNotHaveHappened(); + A.CallTo(() => clock.GetCurrentInstant()) + .MustNotHaveHappened(); } } } diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/CompoundEventConsumerTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/CompoundEventConsumerTests.cs index 594564274..9494ff7b7 100644 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/CompoundEventConsumerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/EventSourcing/CompoundEventConsumerTests.cs @@ -82,7 +82,7 @@ namespace Squidex.Infrastructure.EventSourcing [Fact] public async Task Should_invoke_all_consumers() { - var @event = Envelope.Create(new MyEvent()); + var @event = Envelope.Create(new MyEvent()); var sut = new CompoundEventConsumer("consumer-name", consumer1, consumer2); diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/DefaultEventDataFormatterTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/DefaultEventDataFormatterTests.cs index 74529224f..ead614257 100644 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/DefaultEventDataFormatterTests.cs +++ b/tests/Squidex.Infrastructure.Tests/EventSourcing/DefaultEventDataFormatterTests.cs @@ -51,7 +51,7 @@ namespace Squidex.Infrastructure.EventSourcing inputEvent.SetEventStreamNumber(1); inputEvent.SetTimestamp(SystemClock.Instance.GetCurrentInstant()); - var eventData = sut.ToEventData(inputEvent.To(), commitId); + var eventData = sut.ToEventData(inputEvent, commitId); var outputEvent = sut.Parse(eventData).To(); @@ -64,7 +64,7 @@ namespace Squidex.Infrastructure.EventSourcing { var inputEvent = new Envelope(new MyOldEvent { MyProperty = "My-Property" }); - var eventData = sut.ToEventData(inputEvent.To(), Guid.NewGuid()); + var eventData = sut.ToEventData(inputEvent, Guid.NewGuid()); var outputEvent = sut.Parse(eventData).To(); @@ -76,7 +76,7 @@ namespace Squidex.Infrastructure.EventSourcing { var inputEvent = new Envelope(new MyOldEvent { MyProperty = "My-Property" }); - var eventData = sut.ToEventData(inputEvent.To(), Guid.NewGuid(), false); + var eventData = sut.ToEventData(inputEvent, Guid.NewGuid(), false); var outputEvent = sut.Parse(eventData).To(); diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeExtensionsTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeExtensionsTests.cs index 464585379..dbba8b617 100644 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeExtensionsTests.cs +++ b/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeExtensionsTests.cs @@ -13,7 +13,11 @@ namespace Squidex.Infrastructure.EventSourcing { public class EnvelopeExtensionsTests { - private readonly Envelope sut = new Envelope(string.Empty); + private readonly Envelope sut = new Envelope(new MyEvent()); + + public sealed class MyEvent : IEvent + { + } [Fact] public void Should_set_and_get_timestamp() diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeTests.cs index f9d3a75e5..8a573c40f 100644 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeTests.cs +++ b/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeTests.cs @@ -20,11 +20,11 @@ namespace Squidex.Infrastructure.EventSourcing [Fact] public void Should_serialize_and_deserialize() { - var value = new Envelope(new MyEvent { Value = 1 }); + var value = Envelope.Create(new MyEvent { Value = 1 }); var deserialized = value.SerializeAndDeserialize(); - Assert.Equal(1, deserialized.To().Payload.Value); + Assert.Equal(1, deserialized.Payload.Value); } } } diff --git a/tests/Squidex.Infrastructure.Tests/InstantExtensions.cs b/tests/Squidex.Infrastructure.Tests/InstantExtensions.cs new file mode 100644 index 000000000..eb30be491 --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/InstantExtensions.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using NodaTime; +using Xunit; + +namespace Squidex.Infrastructure +{ + public class InstantExtensions + { + [Fact] + public void Should_remove_ms_from_instant() + { + var source = Instant.FromUnixTimeMilliseconds((30 * 1000) + 100); + + Assert.Equal(Instant.FromUnixTimeSeconds(30), source.WithoutMs()); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Json/InstantConverterTests.cs b/tests/Squidex.Infrastructure.Tests/Json/InstantConverterTests.cs index aba2d8269..6ef80c6fd 100644 --- a/tests/Squidex.Infrastructure.Tests/Json/InstantConverterTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Json/InstantConverterTests.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using NodaTime; using Squidex.Infrastructure.TestHelpers; using Xunit; @@ -17,7 +16,7 @@ namespace Squidex.Infrastructure.Json [Fact] public void Should_serialize_and_deserialize() { - var value = Instant.FromDateTimeUtc(DateTime.UtcNow.Date); + var value = Instant.FromUtc(2012, 12, 10, 9, 8, 45); var serialized = value.SerializeAndDeserialize(); @@ -27,7 +26,7 @@ namespace Squidex.Infrastructure.Json [Fact] public void Should_serialize_and_deserialize_nullable_with_value() { - Instant? value = Instant.FromDateTimeUtc(DateTime.UtcNow.Date); + Instant? value = Instant.FromUtc(2012, 12, 10, 9, 8, 45); var serialized = value.SerializeAndDeserialize(); diff --git a/tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ConverterContractResolverTests.cs b/tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ConverterContractResolverTests.cs index 4ea3d7c0f..aee6e6966 100644 --- a/tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ConverterContractResolverTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ConverterContractResolverTests.cs @@ -42,7 +42,7 @@ namespace Squidex.Infrastructure.Json.Newtonsoft [Fact] public void Should_respect_property_converter() { - var value = Instant.FromDateTimeUtc(DateTime.UtcNow.Date); + var value = Instant.FromUtc(2012, 12, 10, 9, 8, 45); var serializerSettings = new JsonSerializerSettings { @@ -57,7 +57,7 @@ namespace Squidex.Infrastructure.Json.Newtonsoft [Fact] public void Should_ignore_other_converters() { - var value = Instant.FromDateTimeUtc(DateTime.UtcNow.Date); + var value = Instant.FromUtc(2012, 12, 10, 9, 8, 45); var serializerSettings = new JsonSerializerSettings { @@ -75,7 +75,7 @@ namespace Squidex.Infrastructure.Json.Newtonsoft [Fact] public void Should_serialize_and_deserialize_instant() { - var value = Instant.FromDateTimeUtc(DateTime.UtcNow.Date); + var value = Instant.FromUtc(2012, 12, 10, 9, 8, 45); var serialized = value.SerializeAndDeserialize(); diff --git a/tests/Squidex.Infrastructure.Tests/Log/JsonLogWriterTests.cs b/tests/Squidex.Infrastructure.Tests/Log/JsonLogWriterTests.cs index 069be90c7..5d3b00d6c 100644 --- a/tests/Squidex.Infrastructure.Tests/Log/JsonLogWriterTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Log/JsonLogWriterTests.cs @@ -7,6 +7,7 @@ using System; using Newtonsoft.Json; +using NodaTime; using Xunit; namespace Squidex.Infrastructure.Log @@ -48,31 +49,21 @@ namespace Squidex.Infrastructure.Log } [Fact] - public void Should_write_timespan_property() + public void Should_write_duration_property() { - var result = sut.WriteProperty("property", new TimeSpan(1, 40, 30, 20, 100)).ToString(); + var result = sut.WriteProperty("property", new TimeSpan(2, 16, 30, 20, 100)).ToString(); Assert.Equal(@"{""property"":""2.16:30:20.1000000""}", result); } - [Fact] - public void Should_write_datetimeoffset_property() - { - var value = DateTimeOffset.UtcNow; - - var result = sut.WriteProperty("property", value).ToString(); - - Assert.Equal($"{{\"property\":\"{value:o}\"}}", result); - } - [Fact] public void Should_write_date_property() { - var value = DateTime.UtcNow; + var value = Instant.FromUtc(2012, 11, 10, 9, 8, 45); var result = sut.WriteProperty("property", value).ToString(); - Assert.Equal($"{{\"property\":\"{value:o}\"}}", result); + Assert.Equal(@"{""property"":""2012-11-10T09:08:45Z""}", result); } [Fact] @@ -108,9 +99,9 @@ namespace Squidex.Infrastructure.Log } [Fact] - public void Should_write_timespan_value() + public void Should_write_duration_value() { - var result = sut.WriteArray("property", a => a.WriteValue(new TimeSpan(1, 40, 30, 20, 100))).ToString(); + var result = sut.WriteArray("property", a => a.WriteValue(new TimeSpan(2, 16, 30, 20, 100))).ToString(); Assert.Equal(@"{""property"":[""2.16:30:20.1000000""]}", result); } @@ -123,24 +114,14 @@ namespace Squidex.Infrastructure.Log Assert.Equal(@"{""property1"":[{""property2"":120}]}", result); } - [Fact] - public void Should_write_datetimeoffset_value() - { - var value = DateTimeOffset.UtcNow; - - var result = sut.WriteArray("property", a => a.WriteValue(value)).ToString(); - - Assert.Equal($"{{\"property\":[\"{value:o}\"]}}", result); - } - [Fact] public void Should_write_date_value() { - var value = DateTime.UtcNow; + var value = Instant.FromUtc(2012, 11, 10, 9, 8, 45); var result = sut.WriteArray("property", a => a.WriteValue(value)).ToString(); - Assert.Equal($"{{\"property\":[\"{value:yyyy-MM-ddTHH:mm:ssZ}\"]}}", result); + Assert.Equal(@"{""property"":[""2012-11-10T09:08:45Z""]}", result); } [Fact] diff --git a/tests/Squidex.Infrastructure.Tests/Log/SemanticLogTests.cs b/tests/Squidex.Infrastructure.Tests/Log/SemanticLogTests.cs index 7f82046ff..cd5685739 100644 --- a/tests/Squidex.Infrastructure.Tests/Log/SemanticLogTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Log/SemanticLogTests.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using System.Linq; using FakeItEasy; using Microsoft.Extensions.Logging; +using NodaTime; using Squidex.Infrastructure.Log.Adapter; using Xunit; @@ -63,16 +64,19 @@ namespace Squidex.Infrastructure.Log [Fact] public void Should_log_timestamp() { - var now = DateTime.UtcNow; + var clock = A.Fake(); - appenders.Add(new TimestampLogAppender(() => now)); + A.CallTo(() => clock.GetCurrentInstant()) + .Returns(SystemClock.Instance.GetCurrentInstant().WithoutMs()); + + appenders.Add(new TimestampLogAppender(clock)); Log.LogFatal(w => { /* Do Nothing */ }); var expected = LogTest(w => w .WriteProperty("logLevel", "Fatal") - .WriteProperty("timestamp", now)); + .WriteProperty("timestamp", clock.GetCurrentInstant())); Assert.Equal(expected, output); } diff --git a/tests/Squidex.Infrastructure.Tests/RetryWindowTests.cs b/tests/Squidex.Infrastructure.Tests/RetryWindowTests.cs index a32164846..dcb6a2322 100644 --- a/tests/Squidex.Infrastructure.Tests/RetryWindowTests.cs +++ b/tests/Squidex.Infrastructure.Tests/RetryWindowTests.cs @@ -6,20 +6,28 @@ // ========================================================================== using System; +using FakeItEasy; +using NodaTime; using Xunit; namespace Squidex.Infrastructure { public class RetryWindowTests { - private const int WindowSize = 5; + private readonly IClock clock = A.Fake(); + + public RetryWindowTests() + { + A.CallTo(() => clock.GetCurrentInstant()) + .Returns(SystemClock.Instance.GetCurrentInstant().WithoutMs()); + } [Fact] public void Should_allow_to_retry_after_reset() { - var sut = new RetryWindow(TimeSpan.FromSeconds(1), WindowSize); + var sut = new RetryWindow(TimeSpan.FromSeconds(1), 5); - for (var i = 0; i < WindowSize * 2; i++) + for (var i = 0; i < 5 * 2; i++) { sut.CanRetryAfterFailure(); } @@ -34,19 +42,18 @@ namespace Squidex.Infrastructure [InlineData(7)] public void Should_not_allow_to_retry_after_many_errors(int errors) { - var sut = new RetryWindow(TimeSpan.FromSeconds(1), WindowSize); - var now = DateTime.UtcNow; + var sut = new RetryWindow(TimeSpan.FromSeconds(1), 5, clock); - for (var i = 0; i < WindowSize; i++) + for (var i = 0; i < 5; i++) { - Assert.True(sut.CanRetryAfterFailure(now)); + Assert.True(sut.CanRetryAfterFailure()); } - var remaining = errors - WindowSize; + var remaining = errors - 5; for (var i = 0; i < remaining; i++) { - Assert.False(sut.CanRetryAfterFailure(now)); + Assert.False(sut.CanRetryAfterFailure()); } } @@ -57,12 +64,11 @@ namespace Squidex.Infrastructure [InlineData(4)] public void Should_allow_to_retry_after_few_errors(int errors) { - var sut = new RetryWindow(TimeSpan.FromSeconds(1), WindowSize); - var now = DateTime.UtcNow; + var sut = new RetryWindow(TimeSpan.FromSeconds(1), 5, clock); for (var i = 0; i < errors; i++) { - Assert.True(sut.CanRetryAfterFailure(now)); + Assert.True(sut.CanRetryAfterFailure()); } } @@ -77,12 +83,18 @@ namespace Squidex.Infrastructure [InlineData(8)] public void Should_allow_to_retry_after_few_errors_in_window(int errors) { - var sut = new RetryWindow(TimeSpan.FromSeconds(1), WindowSize); - var now = DateTime.UtcNow; + var sut = new RetryWindow(TimeSpan.FromSeconds(1), 5, clock); + + var now = SystemClock.Instance.GetCurrentInstant(); + + A.CallTo(() => clock.GetCurrentInstant()) + .ReturnsLazily(() => now); for (var i = 0; i < errors; i++) { - Assert.True(sut.CanRetryAfterFailure(now.AddMilliseconds(i * 300))); + now = now.Plus(Duration.FromMilliseconds(300)); + + Assert.True(sut.CanRetryAfterFailure()); } } } diff --git a/tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs b/tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs index 686a57c95..5e44de36f 100644 --- a/tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs +++ b/tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs @@ -183,12 +183,12 @@ namespace Squidex.Infrastructure.States await persistence.ReadAsync(); - await persistence.WriteEventsAsync(new[] { new MyEvent(), new MyEvent() }.Select(Envelope.Create)); - await persistence.WriteEventsAsync(new[] { new MyEvent(), new MyEvent() }.Select(Envelope.Create)); + await persistence.WriteEventAsync(Envelope.Create(new MyEvent())); + await persistence.WriteEventAsync(Envelope.Create(new MyEvent())); - A.CallTo(() => eventStore.AppendAsync(A.Ignored, key, 2, A>.That.Matches(x => x.Count == 2))) + A.CallTo(() => eventStore.AppendAsync(A.Ignored, key, 2, A>.That.Matches(x => x.Count == 1))) .MustHaveHappened(); - A.CallTo(() => eventStore.AppendAsync(A.Ignored, key, 4, A>.That.Matches(x => x.Count == 2))) + A.CallTo(() => eventStore.AppendAsync(A.Ignored, key, 3, A>.That.Matches(x => x.Count == 1))) .MustHaveHappened(); } @@ -202,10 +202,10 @@ namespace Squidex.Infrastructure.States await persistence.ReadAsync(); - A.CallTo(() => eventStore.AppendAsync(A.Ignored, key, 2, A>.That.Matches(x => x.Count == 2))) + A.CallTo(() => eventStore.AppendAsync(A.Ignored, key, 2, A>.That.Matches(x => x.Count == 1))) .Throws(new WrongEventVersionException(1, 1)); - await Assert.ThrowsAsync(() => persistence.WriteEventsAsync(new[] { new MyEvent(), new MyEvent() }.Select(Envelope.Create))); + await Assert.ThrowsAsync(() => persistence.WriteEventAsync(Envelope.Create(new MyEvent()))); } [Fact] From 5a9310f2369ea0cc680bd170ac2c83bab16aafe0 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 13 Jan 2019 09:50:54 +0100 Subject: [PATCH 5/7] Usage triggers finalized. --- .../Actions/RuleElementRegistry.cs | 6 ++-- .../Rules/UsageTracking/IUsageTrackerGrain.cs | 7 ++-- .../UsageTrackerCommandMiddleware.cs | 6 ---- .../Rules/UsageTracking/UsageTrackerGrain.cs | 36 +++++++++---------- .../UsageTracking/UsageTriggerHandler.cs | 2 +- .../Converters/RuleTriggerDtoFactory.cs | 2 +- ...geTriggerDto.cs => UsageRuleTriggerDto.cs} | 2 +- src/Squidex/Config/Domain/EntitiesServices.cs | 4 +++ src/Squidex/Config/Orleans/SiloWrapper.cs | 2 ++ .../app/features/rules/declarations.ts | 1 + src/Squidex/app/features/rules/module.ts | 2 ++ .../pages/rules/rule-wizard.component.html | 7 ++++ .../triggers/usage-trigger.component.html | 13 +++++++ .../triggers/usage-trigger.component.scss | 6 ++++ .../rules/triggers/usage-trigger.component.ts | 30 ++++++++++++++++ ....cs => AssetChangedTriggerHandlerTests.cs} | 8 ++--- ...s => ContentChangedTriggerHandlerTests.cs} | 8 ++--- 17 files changed, 98 insertions(+), 44 deletions(-) rename src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/{UsageTriggerDto.cs => UsageRuleTriggerDto.cs} (93%) create mode 100644 src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.html create mode 100644 src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.scss create mode 100644 src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.ts rename tests/Squidex.Domain.Apps.Entities.Tests/Assets/{AssetChangedTriggerTests.cs => AssetChangedTriggerHandlerTests.cs} (98%) rename tests/Squidex.Domain.Apps.Entities.Tests/Contents/{ContentChangedTriggerTests.cs => ContentChangedTriggerHandlerTests.cs} (98%) diff --git a/extensions/Squidex.Extensions/Actions/RuleElementRegistry.cs b/extensions/Squidex.Extensions/Actions/RuleElementRegistry.cs index 72eaa0db7..0c2bdd610 100644 --- a/extensions/Squidex.Extensions/Actions/RuleElementRegistry.cs +++ b/extensions/Squidex.Extensions/Actions/RuleElementRegistry.cs @@ -34,14 +34,14 @@ namespace Squidex.Extensions.Actions IconImage = "", IconColor = "#3389ff", Display = "Asset changed", - Description = "For asset changes like uploaded, updated, renamed, deleted..." + Description = "For asset changes like uploaded, updated (reuploaded), renamed, deleted..." }, [UsageTrigger.Name] = new RuleElement { IconImage = "", IconColor = "#3389ff", - Display = "Usage limitations", - Description = "When monthly API calls exceeed a specified limit..." + Display = "Usage exceeded", + Description = "When monthly API calls exceed a specified limit for one time a month..." } }; diff --git a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/IUsageTrackerGrain.cs b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/IUsageTrackerGrain.cs index 812b2862b..87582bcce 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/IUsageTrackerGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/IUsageTrackerGrain.cs @@ -9,17 +9,14 @@ using System; using System.Threading.Tasks; using Orleans; using Squidex.Infrastructure; +using Squidex.Infrastructure.Orleans; namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking { - public interface IUsageTrackerGrain : IGrainWithStringKey + public interface IUsageTrackerGrain : IGrainWithStringKey, IBackgroundGrain { Task AddTargetAsync(Guid ruleId, NamedId appId, int limits); - Task ActivateTargetAsync(Guid ruleId); - - Task DeactivateTargetAsync(Guid ruleId); - Task RemoveTargetAsync(Guid ruleId); Task UpdateTargetAsync(Guid ruleId, int limits); diff --git a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs index 3556a8f47..a287add24 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs @@ -34,12 +34,6 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking case DeleteRule deleteRule: await usageTrackerGrain.RemoveTargetAsync(deleteRule.RuleId); break; - case EnableRule enableRule: - await usageTrackerGrain.ActivateTargetAsync(enableRule.RuleId); - break; - case DisableRule disableRule: - await usageTrackerGrain.DeactivateTargetAsync(disableRule.RuleId); - break; case CreateRule createRule: { if (createRule.Trigger is UsageTrigger createdTrigger) diff --git a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs index c1cda0185..38b4cdcf8 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs @@ -9,16 +9,19 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using Orleans; +using Orleans.Concurrency; using Orleans.Runtime; using Squidex.Domain.Apps.Events; using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.States; +using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.UsageTracking; namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking { + [Reentrant] public sealed class UsageTrackerGrain : GrainOfString, IRemindable, IUsageTrackerGrain { private readonly IUsageTracker usageTracker; @@ -27,9 +30,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking { public int Limits { get; set; } - public bool Disabled { get; set; } - - public DateTime Triggered { get; set; } + public DateTime? Triggered { get; set; } public NamedId AppId { get; set; } } @@ -53,11 +54,22 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking DelayDeactivation(TimeSpan.FromDays(1)); RegisterOrUpdateReminder("Default", TimeSpan.Zero, TimeSpan.FromMinutes(10)); + RegisterTimer(x => CheckUsagesAsync(), null, TimeSpan.Zero, TimeSpan.FromMinutes(10)); return Task.CompletedTask; } - public async Task ReceiveReminder(string reminderName, TickStatus status) + public Task ActivateAsync() + { + return TaskHelper.Done; + } + + public Task ReceiveReminder(string reminderName, TickStatus status) + { + return TaskHelper.Done; + } + + public async Task CheckUsagesAsync() { var today = DateTime.Today; @@ -65,7 +77,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking { var appId = kvp.Value.AppId; - if (!IsSameMonth(today, kvp.Value.Triggered)) + if (!kvp.Value.Triggered.HasValue || !IsSameMonth(today, kvp.Value.Triggered.Value)) { var usage = await usageTracker.GetMonthlyCallsAsync(appId.Id.ToString(), today); @@ -110,20 +122,6 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking return WriteStateAsync(); } - public Task ActivateTargetAsync(Guid ruleId) - { - UpdateTarget(ruleId, t => t.Disabled = false); - - return WriteStateAsync(); - } - - public Task DeactivateTargetAsync(Guid ruleId) - { - UpdateTarget(ruleId, t => t.Disabled = true); - - return WriteStateAsync(); - } - public Task AddTargetAsync(Guid ruleId, int limits) { UpdateTarget(ruleId, t => t.Limits = limits); diff --git a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs index f5b17df98..42bcb40e2 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs @@ -16,7 +16,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking { public sealed class UsageTriggerHandler : RuleTriggerHandler { - private const string EventName = "Usage exceeeded"; + private const string EventName = "Usage exceeded"; protected override Task CreateEnrichedEventAsync(Envelope @event) { diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs index 3b74047de..25fb14496 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs @@ -33,7 +33,7 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models.Converters public RuleTriggerDto Visit(UsageTrigger trigger) { - return SimpleMapper.Map(trigger, new UsageTriggerDto()); + return SimpleMapper.Map(trigger, new UsageRuleTriggerDto()); } public RuleTriggerDto Visit(ContentChangedTriggerV2 trigger) diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageTriggerDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageRuleTriggerDto.cs similarity index 93% rename from src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageTriggerDto.cs rename to src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageRuleTriggerDto.cs index f298b6ef6..00cc24b41 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageTriggerDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageRuleTriggerDto.cs @@ -11,7 +11,7 @@ using Squidex.Infrastructure.Reflection; namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers { - public sealed class UsageTriggerDto : RuleTriggerDto + public sealed class UsageRuleTriggerDto : RuleTriggerDto { /// /// The number of monthly api calls. diff --git a/src/Squidex/Config/Domain/EntitiesServices.cs b/src/Squidex/Config/Domain/EntitiesServices.cs index faa19dd39..4401d09b8 100644 --- a/src/Squidex/Config/Domain/EntitiesServices.cs +++ b/src/Squidex/Config/Domain/EntitiesServices.cs @@ -35,6 +35,7 @@ using Squidex.Domain.Apps.Entities.History; using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Rules.Commands; using Squidex.Domain.Apps.Entities.Rules.Indexes; +using Squidex.Domain.Apps.Entities.Rules.UsageTracking; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Domain.Apps.Entities.Schemas.Indexes; @@ -207,6 +208,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + + services.AddSingletonAs() + .As(); } private static void AddBackupHandlers(this IServiceCollection services) diff --git a/src/Squidex/Config/Orleans/SiloWrapper.cs b/src/Squidex/Config/Orleans/SiloWrapper.cs index 334993f1e..55f8b6d11 100644 --- a/src/Squidex/Config/Orleans/SiloWrapper.cs +++ b/src/Squidex/Config/Orleans/SiloWrapper.cs @@ -18,6 +18,7 @@ using Orleans.Configuration; using Orleans.Hosting; using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Rules; +using Squidex.Domain.Apps.Entities.Rules.UsageTracking; using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing.Grains; using Squidex.Infrastructure.Log; @@ -68,6 +69,7 @@ namespace Squidex.Config.Orleans .AddStartupTask>() .AddStartupTask>() .AddStartupTask>() + .AddStartupTask>() .Configure(options => { options.Configure(); diff --git a/src/Squidex/app/features/rules/declarations.ts b/src/Squidex/app/features/rules/declarations.ts index c14638e0f..eb57c8359 100644 --- a/src/Squidex/app/features/rules/declarations.ts +++ b/src/Squidex/app/features/rules/declarations.ts @@ -19,6 +19,7 @@ export * from './pages/rules/actions/webhook-action.component'; export * from './pages/rules/triggers/asset-changed-trigger.component'; export * from './pages/rules/triggers/content-changed-trigger.component'; +export * from './pages/rules/triggers/usage-trigger.component'; export * from './pages/rules/rule-element.component'; export * from './pages/rules/rule-wizard.component'; diff --git a/src/Squidex/app/features/rules/module.ts b/src/Squidex/app/features/rules/module.ts index 99a9f48db..55d204251 100644 --- a/src/Squidex/app/features/rules/module.ts +++ b/src/Squidex/app/features/rules/module.ts @@ -32,6 +32,7 @@ import { RuleWizardComponent, SlackActionComponent, TweetActionComponent, + UsageTriggerComponent, WebhookActionComponent } from './declarations'; @@ -79,6 +80,7 @@ const routes: Routes = [ RuleWizardComponent, SlackActionComponent, TweetActionComponent, + UsageTriggerComponent, WebhookActionComponent ] }) diff --git a/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html b/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html index c9b29629e..54a41851e 100644 --- a/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html +++ b/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html @@ -49,6 +49,13 @@ [triggerFormSubmitted]="triggerForm.submitted | async"> + + + + diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.html b/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.html new file mode 100644 index 000000000..9b18d5467 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.html @@ -0,0 +1,13 @@ +
+
+ + + + + + + + The monthly api calls to trigger. + +
+
\ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.scss b/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.scss new file mode 100644 index 000000000..6e1eef5ec --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.scss @@ -0,0 +1,6 @@ +@import '_vars'; +@import '_mixins'; + +textarea { + height: 100px; +} \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.ts b/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.ts new file mode 100644 index 000000000..d08c1d15c --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.ts @@ -0,0 +1,30 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Component, Input, OnInit } from '@angular/core'; +import { FormControl, FormGroup } from '@angular/forms'; + +@Component({ + selector: 'sqx-usage-trigger', + styleUrls: ['./usage-trigger.component.scss'], + templateUrl: './usage-trigger.component.html' +}) +export class UsageTriggerComponent implements OnInit { + @Input() + public trigger: any; + + @Input() + public triggerForm: FormGroup; + + @Input() + public triggerFormSubmitted = false; + + public ngOnInit() { + this.triggerForm.setControl('limit', + new FormControl(this.trigger.limit || 20000)); + } +} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs similarity index 98% rename from tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerTests.cs rename to tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs index 19b3f2fd7..584a1c50e 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs @@ -25,21 +25,21 @@ using Xunit; namespace Squidex.Domain.Apps.Entities.Assets { - public class AssetChangedTriggerTests + public class AssetChangedTriggerHandlerTests { private readonly IScriptEngine scriptEngine = A.Fake(); private readonly IGrainFactory grainFactory = A.Fake(); private readonly IRuleTriggerHandler sut; - public AssetChangedTriggerTests() + public AssetChangedTriggerHandlerTests() { - sut = new AssetChangedTriggerHandler(scriptEngine, grainFactory); - A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, "true")) .Returns(true); A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, "false")) .Returns(false); + + sut = new AssetChangedTriggerHandler(scriptEngine, grainFactory); } public static IEnumerable TestEvents = new[] diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs similarity index 98% rename from tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerTests.cs rename to tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs index a2c601af5..03b547993 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs @@ -29,7 +29,7 @@ using Xunit; namespace Squidex.Domain.Apps.Entities.Contents { - public class ContentChangedTriggerTests + public class ContentChangedTriggerHandlerTests { private readonly IScriptEngine scriptEngine = A.Fake(); private readonly IGrainFactory grainFactory = A.Fake(); @@ -38,15 +38,15 @@ namespace Squidex.Domain.Apps.Entities.Contents private static readonly NamedId SchemaMatch = NamedId.Of(Guid.NewGuid(), "my-schema1"); private static readonly NamedId SchemaNonMatch = NamedId.Of(Guid.NewGuid(), "my-schema2"); - public ContentChangedTriggerTests() + public ContentChangedTriggerHandlerTests() { - sut = new ContentChangedTriggerHandler(scriptEngine, grainFactory); - A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, "true")) .Returns(true); A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, "false")) .Returns(false); + + sut = new ContentChangedTriggerHandler(scriptEngine, grainFactory); } public static IEnumerable TestEvents = new[] From 4bf3badffde3abd52489dee668fbb4cfdecd9479 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Mon, 14 Jan 2019 14:26:47 +0100 Subject: [PATCH 6/7] Define day range. --- .../Rules/Triggers/UsageTrigger.cs | 2 ++ .../Rules/UsageTracking/IUsageTrackerGrain.cs | 4 +-- .../UsageTrackerCommandMiddleware.cs | 14 ++++---- .../Rules/UsageTracking/UsageTrackerGrain.cs | 32 +++++++++++++------ .../UsageTracking/BackgroundUsageTracker.cs | 12 ++++--- .../UsageTracking/CachingUsageTracker.cs | 18 +++++++++-- .../UsageTracking/IUsageTracker.cs | 2 ++ .../Models/Triggers/UsageRuleTriggerDto.cs | 5 +++ .../BackgroundUsageTrackerTests.cs | 24 +++++++++++++- .../UsageTracking/CachingUsageTrackerTests.cs | 19 +++++++++++ 10 files changed, 106 insertions(+), 26 deletions(-) diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/UsageTrigger.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/UsageTrigger.cs index 6e4ac5104..60f266d78 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/UsageTrigger.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/UsageTrigger.cs @@ -16,6 +16,8 @@ namespace Squidex.Domain.Apps.Core.Rules.Triggers public int Limit { get; set; } + public int? NumDays { get; set; } + public override T Accept(IRuleTriggerVisitor visitor) { return visitor.Visit(this); diff --git a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/IUsageTrackerGrain.cs b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/IUsageTrackerGrain.cs index 87582bcce..9027be9b4 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/IUsageTrackerGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/IUsageTrackerGrain.cs @@ -15,10 +15,10 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking { public interface IUsageTrackerGrain : IGrainWithStringKey, IBackgroundGrain { - Task AddTargetAsync(Guid ruleId, NamedId appId, int limits); + Task AddTargetAsync(Guid ruleId, NamedId appId, int limits, int? numDays); Task RemoveTargetAsync(Guid ruleId); - Task UpdateTargetAsync(Guid ruleId, int limits); + Task UpdateTargetAsync(Guid ruleId, int limits, int? numDays); } } \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs index a287add24..9cd7202b7 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs @@ -36,21 +36,23 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking break; case CreateRule createRule: { - if (createRule.Trigger is UsageTrigger createdTrigger) + if (createRule.Trigger is UsageTrigger usage) { - await usageTrackerGrain.AddTargetAsync(createRule.RuleId, createRule.AppId, createdTrigger.Limit); + await usageTrackerGrain.AddTargetAsync(createRule.RuleId, createRule.AppId, usage.Limit, usage.NumDays); } break; } case UpdateRule ruleUpdated: - if (ruleUpdated.Trigger is UsageTrigger updatedTrigger) { - await usageTrackerGrain.UpdateTargetAsync(ruleUpdated.RuleId, updatedTrigger.Limit); - } + if (ruleUpdated.Trigger is UsageTrigger usage) + { + await usageTrackerGrain.UpdateTargetAsync(ruleUpdated.RuleId, usage.Limit, usage.NumDays); + } - break; + break; + } } await next(); diff --git a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs index 38b4cdcf8..3eec572b0 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs @@ -24,12 +24,15 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking [Reentrant] public sealed class UsageTrackerGrain : GrainOfString, IRemindable, IUsageTrackerGrain { + private const int MaxDays = 30; private readonly IUsageTracker usageTracker; public sealed class Target { public int Limits { get; set; } + public int? NumDays { get; set; } + public DateTime? Triggered { get; set; } public NamedId AppId { get; set; } @@ -75,11 +78,13 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking foreach (var kvp in State.Targets) { - var appId = kvp.Value.AppId; + var target = kvp.Value; + + var (from, to) = GetDateRange(today, target.NumDays); - if (!kvp.Value.Triggered.HasValue || !IsSameMonth(today, kvp.Value.Triggered.Value)) + if (!target.Triggered.HasValue || target.Triggered < from) { - var usage = await usageTracker.GetMonthlyCallsAsync(appId.Id.ToString(), today); + var usage = await usageTracker.GetMonthlyCallsAsync(target.AppId.Id.ToString(), today); var limit = kvp.Value.Limits; @@ -89,7 +94,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking var @event = new AppUsageExceeded { - AppId = appId, + AppId = target.AppId, CallsCurrent = usage, CallsLimit = limit, RuleId = kvp.Key @@ -103,21 +108,28 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking await WriteStateAsync(); } - private static bool IsSameMonth(DateTime lhs, DateTime rhs) + private (DateTime, DateTime) GetDateRange(DateTime today, int? numDays) { - return lhs.Year == rhs.Year && lhs.Month == rhs.Month; + if (numDays > 0 && numDays < MaxDays) + { + return (today.AddDays(-numDays.Value).AddDays(1), today); + } + else + { + return (new DateTime(today.Year, today.Month, 1), today); + } } - public Task AddTargetAsync(Guid ruleId, NamedId appId, int limits) + public Task AddTargetAsync(Guid ruleId, NamedId appId, int limits, int? numDays) { - UpdateTarget(ruleId, t => { t.Limits = limits; t.AppId = appId; }); + UpdateTarget(ruleId, t => { t.Limits = limits; t.AppId = appId; t.NumDays = numDays; }); return WriteStateAsync(); } - public Task UpdateTargetAsync(Guid ruleId, int limits) + public Task UpdateTargetAsync(Guid ruleId, int limits, int? numDays) { - UpdateTarget(ruleId, t => t.Limits = limits); + UpdateTarget(ruleId, t => { t.Limits = limits; t.NumDays = numDays; }); return WriteStateAsync(); } diff --git a/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs b/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs index 2cb0240f7..e82fabe02 100644 --- a/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs +++ b/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs @@ -167,16 +167,18 @@ namespace Squidex.Infrastructure.UsageTracking return result; } - public async Task GetMonthlyCallsAsync(string key, DateTime date) + public Task GetMonthlyCallsAsync(string key, DateTime date) + { + return GetPreviousCallsAsync(key, new DateTime(date.Year, date.Month, 1), date); + } + + public async Task GetPreviousCallsAsync(string key, DateTime fromDate, DateTime toDate) { key = GetKey(key); ThrowIfDisposed(); - var dateFrom = new DateTime(date.Year, date.Month, 1); - var dateTo = dateFrom.AddMonths(1).AddDays(-1); - - var originalUsages = await usageRepository.QueryAsync(key, dateFrom, dateTo); + var originalUsages = await usageRepository.QueryAsync(key, fromDate, toDate); return originalUsages.Sum(x => (long)x.Counters.Get(CounterTotalCalls)); } diff --git a/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs b/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs index 7998c422a..ddd32e051 100644 --- a/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs +++ b/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs @@ -14,7 +14,7 @@ namespace Squidex.Infrastructure.UsageTracking { public sealed class CachingUsageTracker : CachingProviderBase, IUsageTracker { - private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10); + private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5); private readonly IUsageTracker inner; public CachingUsageTracker(IUsageTracker inner, IMemoryCache cache) @@ -43,7 +43,7 @@ namespace Squidex.Infrastructure.UsageTracking { Guard.NotNull(key, nameof(key)); - var cacheKey = string.Concat(key, date); + var cacheKey = string.Join("$", "Usage", nameof(GetMonthlyCallsAsync), key, date); return Cache.GetOrCreateAsync(cacheKey, entry => { @@ -52,5 +52,19 @@ namespace Squidex.Infrastructure.UsageTracking return inner.GetMonthlyCallsAsync(key, date); }); } + + public Task GetPreviousCallsAsync(string key, DateTime fromDate, DateTime toDate) + { + Guard.NotNull(key, nameof(key)); + + var cacheKey = string.Join("$", "Usage", nameof(GetPreviousCallsAsync), key, fromDate, toDate); + + return Cache.GetOrCreateAsync(cacheKey, entry => + { + entry.AbsoluteExpirationRelativeToNow = CacheDuration; + + return inner.GetPreviousCallsAsync(key, fromDate, toDate); + }); + } } } diff --git a/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs b/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs index f0945d1e1..bb94d83ff 100644 --- a/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs +++ b/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs @@ -17,6 +17,8 @@ namespace Squidex.Infrastructure.UsageTracking Task GetMonthlyCallsAsync(string key, DateTime date); + Task GetPreviousCallsAsync(string key, DateTime fromDate, DateTime toDate); + Task>> QueryAsync(string key, DateTime fromDate, DateTime toDate); } } diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageRuleTriggerDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageRuleTriggerDto.cs index 00cc24b41..97e24e087 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageRuleTriggerDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageRuleTriggerDto.cs @@ -18,6 +18,11 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers /// public int Limit { get; set; } + /// + /// The number of days to check or null for the current month. + /// + public int? NumDays { get; set; } + public override RuleTrigger ToTrigger() { return SimpleMapper.Map(this, new UsageTrigger()); diff --git a/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs b/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs index f8fd85490..cf9692bcb 100644 --- a/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs @@ -64,7 +64,7 @@ namespace Squidex.Infrastructure.UsageTracking new StoredUsage("category1", date.AddDays(7), Counters(17, 22)) }; - A.CallTo(() => usageStore.QueryAsync($"{key}_API", new DateTime(2016, 1, 1), new DateTime(2016, 1, 31))) + A.CallTo(() => usageStore.QueryAsync($"{key}_API", new DateTime(2016, 1, 1), new DateTime(2016, 1, 15))) .Returns(originalData); var result = await sut.GetMonthlyCallsAsync(key, date); @@ -72,6 +72,28 @@ namespace Squidex.Infrastructure.UsageTracking Assert.Equal(55, result); } + [Fact] + public async Task Should_sum_up_when_getting_last_calls_calls() + { + var f = DateTime.Today; + var t = DateTime.Today.AddDays(10); + + IReadOnlyList originalData = new List + { + new StoredUsage("category1", f.AddDays(1), Counters(10, 15)), + new StoredUsage("category1", f.AddDays(3), Counters(13, 18)), + new StoredUsage("category1", f.AddDays(5), Counters(15, 20)), + new StoredUsage("category1", f.AddDays(7), Counters(17, 22)) + }; + + A.CallTo(() => usageStore.QueryAsync($"{key}_API", f, t)) + .Returns(originalData); + + var result = await sut.GetPreviousCallsAsync(key, f, t); + + Assert.Equal(55, result); + } + [Fact] public async Task Should_fill_missing_days() { diff --git a/tests/Squidex.Infrastructure.Tests/UsageTracking/CachingUsageTrackerTests.cs b/tests/Squidex.Infrastructure.Tests/UsageTracking/CachingUsageTrackerTests.cs index 4ceb57a93..a8fa75b46 100644 --- a/tests/Squidex.Infrastructure.Tests/UsageTracking/CachingUsageTrackerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/UsageTracking/CachingUsageTrackerTests.cs @@ -59,5 +59,24 @@ namespace Squidex.Infrastructure.UsageTracking A.CallTo(() => inner.GetMonthlyCallsAsync(key, DateTime.Today)) .MustHaveHappened(Repeated.Exactly.Once); } + + [Fact] + public async Task Should_cache_days_usage() + { + var f = DateTime.Today; + var t = DateTime.Today.AddDays(10); + + A.CallTo(() => inner.GetPreviousCallsAsync(key, f, t)) + .Returns(120); + + var result1 = await sut.GetPreviousCallsAsync(key, f, t); + var result2 = await sut.GetPreviousCallsAsync(key, f, t); + + Assert.Equal(120, result1); + Assert.Equal(120, result2); + + A.CallTo(() => inner.GetPreviousCallsAsync(key, f, t)) + .MustHaveHappened(Repeated.Exactly.Once); + } } } From 9ce798bffe73e58fa8b1f26e9bb24ccb0d2e92f0 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Mon, 14 Jan 2019 15:36:12 +0100 Subject: [PATCH 7/7] UI for num days. --- .../Rules/Guards/RuleTriggerValidator.cs | 45 +++++++++--- .../Rules/UsageTracking/UsageTrackerGrain.cs | 3 +- .../Models/Triggers/UsageRuleTriggerDto.cs | 2 + .../triggers/usage-trigger.component.html | 16 ++++- .../rules/triggers/usage-trigger.component.ts | 13 +++- .../schema-preview-urls-form.component.html | 4 +- .../angular/forms/control-errors.component.ts | 4 +- .../angular/forms/validators.spec.ts | 16 +++++ .../app/framework/angular/forms/validators.ts | 14 ++-- .../Triggers/ContentChangedTriggerTests.cs | 40 ++++++++--- .../Triggers/UsageTriggerValidationTests.cs | 72 +++++++++++++++++++ 11 files changed, 197 insertions(+), 32 deletions(-) create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/UsageTriggerValidationTests.cs diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs b/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs index f709c24f1..e855d1c21 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs @@ -42,23 +42,52 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards public Task> Visit(UsageTrigger trigger) { - return Task.FromResult(Enumerable.Empty()); + var errors = new List(); + + if (trigger.NumDays.HasValue && (trigger.NumDays < 1 || trigger.NumDays > 30)) + { + errors.Add(new ValidationError("Num days must be between 1 and 30.", nameof(trigger.NumDays))); + } + + return Task.FromResult< IEnumerable>(errors); } public async Task> Visit(ContentChangedTriggerV2 trigger) { + var errors = new List(); + if (trigger.Schemas != null) { - var schemaErrors = await Task.WhenAll( - trigger.Schemas.Select(async s => - await SchemaProvider(s.SchemaId) == null - ? new ValidationError($"Schema {s.SchemaId} does not exist.", nameof(trigger.Schemas)) - : null)); + var tasks = new List>(); + + foreach (var schema in trigger.Schemas) + { + if (schema.SchemaId == Guid.Empty) + { + errors.Add(new ValidationError("Schema id is required.", nameof(trigger.Schemas))); + } + else + { + tasks.Add(CheckSchemaAsync(schema)); + } + } - return schemaErrors.Where(x => x != null).ToList(); + var checkErrors = await Task.WhenAll(tasks); + + errors.AddRange(checkErrors.Where(x => x != null)); + } + + return errors; + } + + private async Task CheckSchemaAsync(ContentChangedTriggerSchemaV2 schema) + { + if (await SchemaProvider(schema.SchemaId) == null) + { + return new ValidationError($"Schema {schema.SchemaId} does not exist.", nameof(ContentChangedTriggerV2.Schemas)); } - return new List(); + return null; } } } diff --git a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs index 3eec572b0..a0cb3b57b 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs @@ -24,7 +24,6 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking [Reentrant] public sealed class UsageTrackerGrain : GrainOfString, IRemindable, IUsageTrackerGrain { - private const int MaxDays = 30; private readonly IUsageTracker usageTracker; public sealed class Target @@ -110,7 +109,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking private (DateTime, DateTime) GetDateRange(DateTime today, int? numDays) { - if (numDays > 0 && numDays < MaxDays) + if (numDays.HasValue) { return (today.AddDays(-numDays.Value).AddDays(1), today); } diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageRuleTriggerDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageRuleTriggerDto.cs index 97e24e087..e5dbce7bc 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageRuleTriggerDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageRuleTriggerDto.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.ComponentModel.DataAnnotations; using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Infrastructure.Reflection; @@ -21,6 +22,7 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers /// /// The number of days to check or null for the current month. /// + [Range(1, 30)] public int? NumDays { get; set; } public override RuleTrigger ToTrigger() diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.html b/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.html index 9b18d5467..d2637ac1f 100644 --- a/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.html +++ b/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.html @@ -2,12 +2,24 @@
- + - + The monthly api calls to trigger.
+ +
+ + + + + + + + The number of days to check or empty to check the current month. + +
\ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.ts b/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.ts index d08c1d15c..6fa94dd64 100644 --- a/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.ts +++ b/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.ts @@ -6,7 +6,9 @@ */ import { Component, Input, OnInit } from '@angular/core'; -import { FormControl, FormGroup } from '@angular/forms'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; + +import { ValidatorsEx } from '@app/shared'; @Component({ selector: 'sqx-usage-trigger', @@ -25,6 +27,13 @@ export class UsageTriggerComponent implements OnInit { public ngOnInit() { this.triggerForm.setControl('limit', - new FormControl(this.trigger.limit || 20000)); + new FormControl(this.trigger.limit || 20000, [ + Validators.required + ])); + + this.triggerForm.setControl('numDays', + new FormControl(this.trigger.numDays, [ + ValidatorsEx.between(1, 30) + ])); } } \ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-preview-urls-form.component.html b/src/Squidex/app/features/schemas/pages/schema/schema-preview-urls-form.component.html index 48ea86ad0..fdd06b41c 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-preview-urls-form.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/schema-preview-urls-form.component.html @@ -33,13 +33,13 @@
- +
- + diff --git a/src/Squidex/app/framework/angular/forms/control-errors.component.ts b/src/Squidex/app/framework/angular/forms/control-errors.component.ts index caeb9e437..86a8fed4e 100644 --- a/src/Squidex/app/framework/angular/forms/control-errors.component.ts +++ b/src/Squidex/app/framework/angular/forms/control-errors.component.ts @@ -15,8 +15,8 @@ const DEFAULT_ERRORS: { [key: string]: string } = { required: '{field} is required.', pattern: '{field} does not follow the pattern.', patternmessage: '{message}', - minvalue: '{field} must be larger than {minValue}.', - maxvalue: '{field} must be smaller than {maxValue}.', + minvalue: '{field} must be larger or equals to {minValue}.', + maxvalue: '{field} must be smaller or equals to {maxValue}.', minmax: '{field} must have a length of more than {requiredLength}.', maxlength: '{field} must have a length of less than {requiredLength}.', match: '{message}', diff --git a/src/Squidex/app/framework/angular/forms/validators.spec.ts b/src/Squidex/app/framework/angular/forms/validators.spec.ts index 5a9126443..51d538b51 100644 --- a/src/Squidex/app/framework/angular/forms/validators.spec.ts +++ b/src/Squidex/app/framework/angular/forms/validators.spec.ts @@ -26,6 +26,22 @@ describe('ValidatorsEx.between', () => { expect(error).toBeNull(); }); + it('should return null when value is null', () => { + const input = new FormControl(null); + + const error = ValidatorsEx.between(1, 5)(input); + + expect(error).toBeNull(); + }); + + it('should return null when value is undefined', () => { + const input = new FormControl(undefined); + + const error = ValidatorsEx.between(1, 5)(input); + + expect(error).toBeNull(); + }); + it('should return error when not a number', () => { const input = new FormControl('text'); diff --git a/src/Squidex/app/framework/angular/forms/validators.ts b/src/Squidex/app/framework/angular/forms/validators.ts index 32c6557bf..84ca3959b 100644 --- a/src/Squidex/app/framework/angular/forms/validators.ts +++ b/src/Squidex/app/framework/angular/forms/validators.ts @@ -97,12 +97,14 @@ export module ValidatorsEx { return (control: AbstractControl) => { const value: number = control.value; - if (!Types.isNumber(value)) { - return { validnumber: false }; - } else if (minValue && value < minValue) { - return { minvalue: { minValue, actualValue: value } }; - } else if (maxValue && value > maxValue) { - return { maxvalue: { maxValue, actualValue: value } }; + if (!Types.isUndefined(value) && !Types.isNull(value)) { + if (!Types.isNumber(value)) { + return { validnumber: false }; + } else if (minValue && value < minValue) { + return { minvalue: { minValue, actualValue: value } }; + } else if (maxValue && value > maxValue) { + return { maxvalue: { maxValue, actualValue: value } }; + } } return null; diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs index 5d386c6ac..a4eb93efd 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs @@ -6,10 +6,13 @@ // ========================================================================== using System; +using System.Collections.Generic; using System.Threading.Tasks; using FakeItEasy; +using FluentAssertions; using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; using Squidex.Infrastructure.Collections; using Xunit; @@ -19,23 +22,46 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards.Triggers { private readonly IAppProvider appProvider = A.Fake(); private readonly Guid appId = Guid.NewGuid(); + private readonly Guid schemaId = Guid.NewGuid(); [Fact] - public async Task Should_add_error_if_schemas_ids_are_not_valid() + public async Task Should_add_error_if_schema_id_is_not_defined() { + var trigger = new ContentChangedTriggerV2 + { + Schemas = ReadOnlyCollection.Create(new ContentChangedTriggerSchemaV2()) + }; + + var errors = await RuleTriggerValidator.ValidateAsync(appId, trigger, appProvider); + + errors.Should().BeEquivalentTo( + new List + { + new ValidationError("Schema id is required.", "Schemas") + }); + A.CallTo(() => appProvider.GetSchemaAsync(appId, A.Ignored, false)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_add_error_if_schemas_ids_are_not_valid() + { + A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false)) .Returns(Task.FromResult(null)); var trigger = new ContentChangedTriggerV2 { - Schemas = ReadOnlyCollection.Create( - new ContentChangedTriggerSchemaV2() - ) + Schemas = ReadOnlyCollection.Create(new ContentChangedTriggerSchemaV2 { SchemaId = schemaId }) }; var errors = await RuleTriggerValidator.ValidateAsync(appId, trigger, appProvider); - Assert.NotEmpty(errors); + errors.Should().BeEquivalentTo( + new List + { + new ValidationError($"Schema {schemaId} does not exist.", "Schemas") + }); } [Fact] @@ -69,9 +95,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards.Triggers var trigger = new ContentChangedTriggerV2 { - Schemas = ReadOnlyCollection.Create( - new ContentChangedTriggerSchemaV2() - ) + Schemas = ReadOnlyCollection.Create(new ContentChangedTriggerSchemaV2 { SchemaId = schemaId }) }; var errors = await RuleTriggerValidator.ValidateAsync(appId, trigger, appProvider); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/UsageTriggerValidationTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/UsageTriggerValidationTests.cs new file mode 100644 index 000000000..827d8a8b4 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/UsageTriggerValidationTests.cs @@ -0,0 +1,72 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using FluentAssertions; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Rules.Guards.Triggers +{ + public class UsageTriggerValidationTests + { + private readonly IAppProvider appProvider = A.Fake(); + private readonly Guid appId = Guid.NewGuid(); + + [Fact] + public async Task Should_add_error_if_num_days_less_than_1() + { + var trigger = new UsageTrigger { NumDays = 0 }; + + var errors = await RuleTriggerValidator.ValidateAsync(appId, trigger, appProvider); + + errors.Should().BeEquivalentTo( + new List + { + new ValidationError("Num days must be between 1 and 30.", "NumDays") + }); + } + + [Fact] + public async Task Should_add_error_if_num_days_greater_than_30() + { + var trigger = new UsageTrigger { NumDays = 32 }; + + var errors = await RuleTriggerValidator.ValidateAsync(appId, trigger, appProvider); + + errors.Should().BeEquivalentTo( + new List + { + new ValidationError("Num days must be between 1 and 30.", "NumDays") + }); + } + + [Fact] + public async Task Should_not_add_error_if_num_days_is_valid() + { + var trigger = new UsageTrigger { NumDays = 20 }; + + var errors = await RuleTriggerValidator.ValidateAsync(appId, trigger, appProvider); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_num_days_is_not_defined() + { + var trigger = new UsageTrigger(); + + var errors = await RuleTriggerValidator.ValidateAsync(appId, trigger, appProvider); + + Assert.Empty(errors); + } + } +}