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