diff --git a/extensions/Squidex.Extensions/Actions/RuleElementRegistry.cs b/extensions/Squidex.Extensions/Actions/RuleElementRegistry.cs index b8e3dae0a..0c2bdd610 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..." + Description = "For asset changes like uploaded, updated (reuploaded), renamed, deleted..." + }, + [UsageTrigger.Name] = new RuleElement + { + IconImage = "", + IconColor = "#3389ff", + Display = "Usage exceeded", + Description = "When monthly API calls exceed a specified limit for one time a month..." } }; 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..60f266d78 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/UsageTrigger.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// 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 int? NumDays { get; set; } + + public override T Accept(IRuleTriggerVisitor visitor) + { + return visitor.Visit(this); + } + } +} 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/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..616db38f7 --- /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 CallsCurrent { get; set; } + + public long CallsLimit { 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/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 5c8cff42e..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; } - bool Triggers(EnrichedEvent @event, RuleTrigger trigger); + Task CreateEnrichedEventAsync(Envelope @event); - bool Triggers(IEvent @event, RuleTrigger trigger); + bool Trigger(EnrichedEvent @event, RuleTrigger trigger); + + bool Trigger(AppEvent @event, RuleTrigger trigger, Guid ruleId); } } 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..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; @@ -56,7 +55,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)); @@ -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.Triggers(@event.Payload, rule.Trigger)) - { - return null; - } - var now = clock.GetCurrentInstant(); var eventTime = @@ -102,11 +98,23 @@ 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 (!triggerHandler.Triggers(enrichedEvent, rule.Trigger)) + if (enrichedEvent == null) + { + return null; + } + + await eventEnricher.EnrichAsync(enrichedEvent, typed); + + if (!triggerHandler.Trigger(enrichedEvent, rule.Trigger)) { return null; } @@ -116,14 +124,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, @@ -139,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]; @@ -147,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) @@ -160,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 b6ae11c04..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,19 +26,40 @@ namespace Squidex.Domain.Apps.Core.HandleRules get { return typeof(TTrigger); } } - bool IRuleTriggerHandler.Triggers(EnrichedEvent @event, RuleTrigger trigger) + async Task IRuleTriggerHandler.CreateEnrichedEventAsync(Envelope @event) { - return @event is TEnrichedEvent e && Triggers(e, (TTrigger)trigger); + return await CreateEnrichedEventAsync(@event.To()); } - bool IRuleTriggerHandler.Triggers(IEvent @event, RuleTrigger trigger) + bool IRuleTriggerHandler.Trigger(EnrichedEvent @event, RuleTrigger trigger) { - return @event is TEvent e && Triggers(e, (TTrigger)trigger); + var typed = @event as TEnrichedEvent; + + if (typed != null) + { + return Trigger(typed, (TTrigger)trigger); + } + + return false; } - protected abstract bool Triggers(TEnrichedEvent @event, TTrigger trigger); + bool IRuleTriggerHandler.Trigger(AppEvent @event, RuleTrigger trigger, Guid 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 Triggers(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 deleted file mode 100644 index 3aa2b107b..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 Triggers(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.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs deleted file mode 100644 index 25bd8ccfb..000000000 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs +++ /dev/null @@ -1,80 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -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; - -namespace Squidex.Domain.Apps.Core.HandleRules.Triggers -{ - public sealed class ContentChangedTriggerHandler : RuleTriggerHandler - { - private readonly IScriptEngine scriptEngine; - - public ContentChangedTriggerHandler(IScriptEngine scriptEngine) - { - Guard.NotNull(scriptEngine, nameof(scriptEngine)); - - this.scriptEngine = scriptEngine; - } - - protected override bool Triggers(ContentEvent @event, ContentChangedTriggerV2 trigger) - { - if (trigger.HandleAll) - { - return true; - } - - if (trigger.Schemas != null) - { - foreach (var schema in trigger.Schemas) - { - if (MatchsSchema(schema, @event.SchemaId)) - { - return true; - } - } - } - - return false; - } - - protected override bool Triggers(EnrichedContentEvent @event, ContentChangedTriggerV2 trigger) - { - if (trigger.HandleAll) - { - return true; - } - - if (trigger.Schemas != null) - { - foreach (var schema in trigger.Schemas) - { - if (MatchsSchema(schema, @event.SchemaId) && MatchsCondition(schema, @event)) - { - return true; - } - } - } - - return false; - } - - private static bool MatchsSchema(ContentChangedTriggerSchemaV2 schema, NamedId eventId) - { - return eventId.Id == schema.SchemaId; - } - - private bool MatchsCondition(ContentChangedTriggerSchemaV2 schema, EnrichedSchemaEvent @event) - { - return string.IsNullOrWhiteSpace(schema.Condition) || scriptEngine.Evaluate("event", @event, schema.Condition); - } - } -} 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/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.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/Contents/ContentChangedTriggerHandler.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs new file mode 100644 index 000000000..60726b883 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs @@ -0,0 +1,140 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +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.Entities.Contents +{ + public sealed class ContentChangedTriggerHandler : RuleTriggerHandler + { + private readonly IScriptEngine scriptEngine; + private readonly IGrainFactory grainFactory; + + 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) + { + if (trigger.HandleAll) + { + return true; + } + + if (trigger.Schemas != null) + { + foreach (var schema in trigger.Schemas) + { + if (MatchsSchema(schema, @event.SchemaId)) + { + return true; + } + } + } + + return false; + } + + protected override bool Trigger(EnrichedContentEvent @event, ContentChangedTriggerV2 trigger) + { + if (trigger.HandleAll) + { + return true; + } + + if (trigger.Schemas != null) + { + foreach (var schema in trigger.Schemas) + { + if (MatchsSchema(schema, @event.SchemaId) && MatchsCondition(schema, @event)) + { + return true; + } + } + } + + return false; + } + + private static bool MatchsSchema(ContentChangedTriggerSchemaV2 schema, NamedId eventId) + { + return eventId.Id == schema.SchemaId; + } + + private bool MatchsCondition(ContentChangedTriggerSchemaV2 schema, EnrichedSchemaEvent @event) + { + return string.IsNullOrWhiteSpace(schema.Condition) || scriptEngine.Evaluate("event", @event, schema.Condition); + } + } +} 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 a9532e97f..000000000 --- a/src/Squidex.Domain.Apps.Entities/Rules/EventEnricher.cs +++ /dev/null @@ -1,192 +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)); - - if (@event.Payload is ContentEvent contentEvent) - { - var result = new EnrichedContentEvent(); - - await Task.WhenAll( - EnrichContentAsync(result, contentEvent, @event), - EnrichDefaultAsync(result, @event)); - - return result; - } - - if (@event.Payload is AssetEvent assetEvent) - { - var result = new EnrichedAssetEvent(); - - await Task.WhenAll( - EnrichAssetAsync(result, assetEvent, @event), - 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 (@event.Payload is SquidexEvent squidexEvent) - { - result.Actor = squidexEvent.Actor; - } - - if (@event.Payload is AppEvent appEvent) - { - result.AppId = appEvent.AppId; - } - - result.User = await FindUserAsync(result.Actor); - } - - 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/Guards/RuleTriggerValidator.cs b/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs index 1e50e9606..e855d1c21 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs @@ -40,20 +40,54 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards return Task.FromResult(Enumerable.Empty()); } + public Task> Visit(UsageTrigger trigger) + { + 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>(); - return schemaErrors.Where(x => x != null).ToList(); + 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)); + } + } + + 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/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 new file mode 100644 index 000000000..9027be9b4 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/IUsageTrackerGrain.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// 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; +using Squidex.Infrastructure.Orleans; + +namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking +{ + public interface IUsageTrackerGrain : IGrainWithStringKey, IBackgroundGrain + { + Task AddTargetAsync(Guid ruleId, NamedId appId, int limits, int? numDays); + + Task RemoveTargetAsync(Guid ruleId); + + 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 new file mode 100644 index 000000000..9cd7202b7 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs @@ -0,0 +1,61 @@ +// ========================================================================== +// 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 CreateRule createRule: + { + if (createRule.Trigger is UsageTrigger usage) + { + await usageTrackerGrain.AddTargetAsync(createRule.RuleId, createRule.AppId, usage.Limit, usage.NumDays); + } + + break; + } + + case UpdateRule ruleUpdated: + { + if (ruleUpdated.Trigger is UsageTrigger usage) + { + await usageTrackerGrain.UpdateTargetAsync(ruleUpdated.RuleId, usage.Limit, usage.NumDays); + } + + 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 new file mode 100644 index 000000000..a0cb3b57b --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs @@ -0,0 +1,155 @@ +// ========================================================================== +// 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.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; + + public sealed class Target + { + public int Limits { get; set; } + + public int? NumDays { get; set; } + + public DateTime? Triggered { get; set; } + + public NamedId AppId { get; set; } + } + + [CollectionName("UsageTracker")] + public sealed class GrainState + { + public Dictionary Targets { get; set; } = new Dictionary(); + } + + public UsageTrackerGrain(IStore store, IUsageTracker usageTracker) + : base(store) + { + Guard.NotNull(usageTracker, nameof(usageTracker)); + + this.usageTracker = usageTracker; + } + + protected override Task OnActivateAsync(string key) + { + DelayDeactivation(TimeSpan.FromDays(1)); + + RegisterOrUpdateReminder("Default", TimeSpan.Zero, TimeSpan.FromMinutes(10)); + RegisterTimer(x => CheckUsagesAsync(), null, TimeSpan.Zero, TimeSpan.FromMinutes(10)); + + return Task.CompletedTask; + } + + public Task ActivateAsync() + { + return TaskHelper.Done; + } + + public Task ReceiveReminder(string reminderName, TickStatus status) + { + return TaskHelper.Done; + } + + public async Task CheckUsagesAsync() + { + var today = DateTime.Today; + + foreach (var kvp in State.Targets) + { + var target = kvp.Value; + + var (from, to) = GetDateRange(today, target.NumDays); + + if (!target.Triggered.HasValue || target.Triggered < from) + { + var usage = await usageTracker.GetMonthlyCallsAsync(target.AppId.Id.ToString(), today); + + var limit = kvp.Value.Limits; + + if (usage > limit) + { + kvp.Value.Triggered = today; + + var @event = new AppUsageExceeded + { + AppId = target.AppId, + CallsCurrent = usage, + CallsLimit = limit, + RuleId = kvp.Key + }; + + await Persistence.WriteEventAsync(Envelope.Create(@event)); + } + } + } + + await WriteStateAsync(); + } + + private (DateTime, DateTime) GetDateRange(DateTime today, int? numDays) + { + if (numDays.HasValue) + { + 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, int? numDays) + { + UpdateTarget(ruleId, t => { t.Limits = limits; t.AppId = appId; t.NumDays = numDays; }); + + return WriteStateAsync(); + } + + public Task UpdateTargetAsync(Guid ruleId, int limits, int? numDays) + { + UpdateTarget(ruleId, t => { t.Limits = limits; t.NumDays = numDays; }); + + return WriteStateAsync(); + } + + public Task AddTargetAsync(Guid ruleId, int limits) + { + UpdateTarget(ruleId, t => t.Limits = limits); + + return WriteStateAsync(); + } + + public Task RemoveTargetAsync(Guid ruleId) + { + State.Targets.Remove(ruleId); + + return WriteStateAsync(); + } + + private void UpdateTarget(Guid ruleId, Action updater) + { + 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 new file mode 100644 index 000000000..42bcb40e2 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// 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; +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.Entities.Rules.UsageTracking +{ + public sealed class UsageTriggerHandler : RuleTriggerHandler + { + private const string EventName = "Usage exceeded"; + + 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.CallsLimit == trigger.Limit; + } + } +} diff --git a/src/Squidex.Domain.Apps.Events/AppUsageExceeded.cs b/src/Squidex.Domain.Apps.Events/AppUsageExceeded.cs new file mode 100644 index 000000000..c7b6c6d8d --- /dev/null +++ b/src/Squidex.Domain.Apps.Events/AppUsageExceeded.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Events +{ + [EventType(nameof(AppUsageExceeded))] + public sealed class AppUsageExceeded : AppEvent + { + public long CallsCurrent { get; set; } + + public long CallsLimit { get; set; } + + public Guid RuleId { get; set; } + } +} 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/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.Infrastructure/Commands/DomainObjectGrain.cs b/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs index 04b05efd3..16da5a476 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, new HandleSnapshot(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.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/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/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/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/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/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/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..8fd4d1dbd 100644 --- a/src/Squidex.Infrastructure/States/StoreExtensions.cs +++ b/src/Squidex.Infrastructure/States/StoreExtensions.cs @@ -5,83 +5,31 @@ // 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) - { - 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.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/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/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs index ab6339b9d..25fb14496 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 UsageRuleTriggerDto()); + } + 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/UsageRuleTriggerDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageRuleTriggerDto.cs new file mode 100644 index 000000000..e5dbce7bc --- /dev/null +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageRuleTriggerDto.cs @@ -0,0 +1,33 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// 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; + +namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers +{ + public sealed class UsageRuleTriggerDto : RuleTriggerDto + { + /// + /// The number of monthly api calls. + /// + public int Limit { get; set; } + + /// + /// The number of days to check or null for the current month. + /// + [Range(1, 30)] + public int? NumDays { get; set; } + + public override RuleTrigger ToTrigger() + { + return SimpleMapper.Map(this, new UsageTrigger()); + } + } +} 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/Domain/RuleServices.cs b/src/Squidex/Config/Domain/RuleServices.cs index 517cccd33..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; @@ -27,6 +29,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); 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/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/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..d2637ac1f --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.html @@ -0,0 +1,25 @@ +
+
+ + + + + + + + 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.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..6fa94dd64 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.ts @@ -0,0 +1,39 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Component, Input, OnInit } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; + +import { ValidatorsEx } from '@app/shared'; + +@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, [ + 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.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 24397e3cb..e4d693829 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,9 @@ 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(); private readonly RuleService sut; @@ -46,7 +49,6 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules public sealed class ValidAction : RuleAction { - public int Value { get; set; } } public sealed class ValidData @@ -67,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()); + A.CallTo(() => clock.GetCurrentInstant()) + .Returns(SystemClock.Instance.GetCurrentInstant().WithoutMs()); A.CallTo(() => ruleActionHandler.ActionType) .Returns(typeof(ValidAction)); @@ -83,146 +85,153 @@ 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 ruleConfig = ValidRule().Disable(); - var ruleEnvelope = Envelope.Create(new ContentCreated()); + var @event = Envelope.Create(new ContentCreated()); - var job = await sut.CreateJobAsync(ruleConfig, ruleEnvelope); + 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 ruleConfig = ValidRule(); - var ruleEnvelope = Envelope.Create(new InvalidEvent()); + var @event = Envelope.Create(new InvalidEvent()); - var job = await sut.CreateJobAsync(ruleConfig, ruleEnvelope); + 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 ruleConfig = new Rule(new InvalidTrigger(), new ValidAction()); - var ruleEnvelope = Envelope.Create(new ContentCreated()); + var @event = Envelope.Create(new ContentCreated()); - var job = await sut.CreateJobAsync(ruleConfig, ruleEnvelope); + 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 ruleConfig = new Rule(new ContentChangedTriggerV2(), new InvalidAction()); - var ruleEnvelope = Envelope.Create(new ContentCreated()); + var @event = Envelope.Create(new ContentCreated()); - var job = await sut.CreateJobAsync(ruleConfig, ruleEnvelope); + 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(); } [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 @event = Envelope.Create(new ContentCreated()); - A.CallTo(() => ruleTriggerHandler.Triggers(A.Ignored, ruleConfig.Trigger)) + A.CallTo(() => ruleTriggerHandler.Trigger(@event.Payload, rule.Trigger, ruleId)) .Returns(false); - var job = await sut.CreateJobAsync(ruleConfig, ruleEnvelope); + 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 ruleConfig = ValidRule(); - var ruleEnvelope = Envelope.Create(new ContentCreated()); + var rule = ValidRule(); + + var @event = Envelope.Create(new ContentCreated()); - A.CallTo(() => ruleTriggerHandler.Triggers(A.Ignored, ruleConfig.Trigger)) + A.CallTo(() => ruleTriggerHandler.Trigger(@event.Payload, rule.Trigger, ruleId)) .Returns(true); - A.CallTo(() => ruleTriggerHandler.Triggers(A.Ignored, ruleConfig.Trigger)) - .Returns(false); + A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventAsync(A>.That.Matches(x => x.Payload == @event.Payload))) + .Returns(Task.FromResult(null)); - var job = await sut.CreateJobAsync(ruleConfig, ruleEnvelope); + 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 @event = new ContentCreated { SchemaId = NamedId.Of(Guid.NewGuid(), "my-schema"), AppId = NamedId.Of(Guid.NewGuid(), "my-event") }; + var rule = ValidRule(); - var now = SystemClock.Instance.GetCurrentInstant(); + var enrichedEvent = new EnrichedContentEvent { AppId = appId }; - var ruleConfig = ValidRule(); - var ruleEnvelope = Envelope.Create(@event); + 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, ruleConfig.Action)) - .Returns((actionDescription, actionData)); + A.CallTo(() => ruleTriggerHandler.Trigger(enrichedEvent, rule.Trigger)) + .Returns(false); - var job = await sut.CreateJobAsync(ruleConfig, 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 @event = new ContentCreated { SchemaId = NamedId.Of(Guid.NewGuid(), "my-schema"), AppId = NamedId.Of(Guid.NewGuid(), "my-event") }; - - var now = Instant.FromUnixTimeSeconds(SystemClock.Instance.GetCurrentInstant().ToUnixTimeSeconds()); + var now = clock.GetCurrentInstant(); - var ruleConfig = ValidRule(); - var ruleEnvelope = Envelope.Create(@event); + var rule = ValidRule(); - 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.Triggers(A.Ignored, ruleConfig.Trigger)) + A.CallTo(() => ruleTriggerHandler.Trigger(@event.Payload, rule.Trigger, ruleId)) .Returns(true); - A.CallTo(() => ruleTriggerHandler.Triggers(A.Ignored, ruleConfig.Trigger)) + A.CallTo(() => ruleTriggerHandler.Trigger(enrichedEvent, rule.Trigger)) .Returns(true); - A.CallTo(() => ruleActionHandler.CreateJobAsync(A.Ignored, ruleConfig.Action)) + 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(ruleConfig, ruleEnvelope); + var job = await sut.CreateJobAsync(rule, ruleId, @event); Assert.Equal(actionData, job.ActionData); Assert.Equal(actionName, job.ActionName); @@ -231,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(@event.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] @@ -247,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] @@ -293,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.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.Core.Tests/Operations/HandleRules/Triggers/AssetChangedTriggerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs similarity index 59% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/AssetChangedTriggerTests.cs rename to tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs index 976d24ac3..584a1c50e 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/AssetChangedTriggerTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs @@ -6,32 +6,67 @@ // ========================================================================== 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 + 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); - 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[] + { + 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] @@ -39,7 +74,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers { TestForCondition(string.Empty, trigger => { - var result = sut.Triggers(new ContentCreated(), trigger); + var result = sut.Trigger(new ContentCreated(), trigger, Guid.NewGuid()); Assert.False(result); }); @@ -50,7 +85,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers { TestForCondition(string.Empty, trigger => { - var result = sut.Triggers(new AssetCreated(), trigger); + var result = sut.Trigger(new AssetCreated(), trigger, Guid.NewGuid()); Assert.True(result); }); @@ -61,7 +96,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers { TestForCondition(string.Empty, trigger => { - var result = sut.Triggers(new EnrichedContentEvent(), trigger); + var result = sut.Trigger(new EnrichedContentEvent(), trigger); Assert.False(result); }); @@ -72,7 +107,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers { TestForCondition(string.Empty, trigger => { - var result = sut.Triggers(new EnrichedAssetEvent(), trigger); + var result = sut.Trigger(new EnrichedAssetEvent(), trigger); Assert.True(result); }); @@ -83,7 +118,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers { TestForCondition("true", trigger => { - var result = sut.Triggers(new EnrichedAssetEvent(), trigger); + var result = sut.Trigger(new EnrichedAssetEvent(), trigger); Assert.True(result); }); @@ -94,7 +129,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers { TestForCondition("false", trigger => { - var result = sut.Triggers(new EnrichedAssetEvent(), trigger); + var result = sut.Trigger(new EnrichedAssetEvent(), trigger); Assert.False(result); }); 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/ContentChangedTriggerHandlerTests.cs similarity index 64% rename from tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/ContentChangedTriggerTests.cs rename to tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs index 297495f6f..03b547993 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/Triggers/ContentChangedTriggerTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs @@ -8,38 +8,75 @@ 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 + public class ContentChangedTriggerHandlerTests { 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"); private static readonly NamedId SchemaNonMatch = NamedId.Of(Guid.NewGuid(), "my-schema2"); - public ContentChangedTriggerTests() + public ContentChangedTriggerHandlerTests() { - sut = new ContentChangedTriggerHandler(scriptEngine); - 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[] + { + 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] @@ -47,7 +84,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers { TestForTrigger(handleAll: true, schemaId: null, condition: null, action: trigger => { - var result = sut.Triggers(new AssetCreated(), trigger); + var result = sut.Trigger(new AssetCreated(), trigger, ruleId); Assert.False(result); }); @@ -58,7 +95,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers { TestForTrigger(handleAll: false, schemaId: null, condition: null, action: trigger => { - var result = sut.Triggers(new ContentCreated { SchemaId = SchemaMatch }, trigger); + var result = sut.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger, ruleId); Assert.False(result); }); @@ -69,7 +106,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers { TestForTrigger(handleAll: true, schemaId: SchemaMatch, condition: null, action: trigger => { - var result = sut.Triggers(new ContentCreated { SchemaId = SchemaMatch }, trigger); + var result = sut.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger, ruleId); Assert.True(result); }); @@ -80,7 +117,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers { TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: string.Empty, action: trigger => { - var result = sut.Triggers(new ContentCreated { SchemaId = SchemaMatch }, trigger); + var result = sut.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger, ruleId); Assert.True(result); }); @@ -91,7 +128,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers { TestForTrigger(handleAll: false, schemaId: SchemaNonMatch, condition: null, action: trigger => { - var result = sut.Triggers(new ContentCreated { SchemaId = SchemaMatch }, trigger); + var result = sut.Trigger(new ContentCreated { SchemaId = SchemaMatch }, trigger, ruleId); Assert.False(result); }); @@ -102,7 +139,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers { TestForTrigger(handleAll: true, schemaId: null, condition: null, action: trigger => { - var result = sut.Triggers(new EnrichedAssetEvent(), trigger); + var result = sut.Trigger(new EnrichedAssetEvent(), trigger); Assert.False(result); }); @@ -113,7 +150,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers { TestForTrigger(handleAll: false, schemaId: null, condition: null, action: trigger => { - var result = sut.Triggers(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); + var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); Assert.False(result); }); @@ -124,7 +161,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers { TestForTrigger(handleAll: true, schemaId: SchemaMatch, condition: null, action: trigger => { - var result = sut.Triggers(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); + var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); Assert.True(result); }); @@ -135,7 +172,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers { TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: string.Empty, action: trigger => { - var result = sut.Triggers(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); + var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); Assert.True(result); }); @@ -146,7 +183,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers { TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: "true", action: trigger => { - var result = sut.Triggers(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); + var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); Assert.True(result); }); @@ -157,7 +194,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers { TestForTrigger(handleAll: false, schemaId: SchemaNonMatch, condition: null, action: trigger => { - var result = sut.Triggers(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); + var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); Assert.False(result); }); @@ -168,7 +205,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules.Triggers { TestForTrigger(handleAll: false, schemaId: SchemaMatch, condition: "false", action: trigger => { - var result = sut.Triggers(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); + var result = sut.Trigger(new EnrichedContentEvent { SchemaId = SchemaMatch }, trigger); Assert.False(result); }); 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/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); + } + } +} 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/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 0822d1b77..b8fcb0d48 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 @@ -68,42 +68,32 @@ 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") }); - 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.Domain.Apps.Entities.Tests/Rules/UsageTracking/UsageTriggerHandlerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/UsageTracking/UsageTriggerHandlerTests.cs new file mode 100644 index 000000000..b8ca9421b --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/UsageTracking/UsageTriggerHandlerTests.cs @@ -0,0 +1,68 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +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.Domain.Apps.Events.Contents; +using Squidex.Infrastructure.EventSourcing; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking +{ + 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(), ruleId); + + Assert.False(result); + } + + [Fact] + public void Should_not_trigger_precheck_when_rule_id_not_matchs() + { + var result = sut.Trigger(new AppUsageExceeded { RuleId = Guid.NewGuid() }, new UsageTrigger(), ruleId); + + Assert.True(result); + } + + [Fact] + public void Should_trigger_precheck_when_event_type_correct_and_rule_id_matchs() + { + var result = sut.Trigger(new AppUsageExceeded { RuleId = ruleId }, new UsageTrigger(), ruleId); + + 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 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.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/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); } } } 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/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/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/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/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/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/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/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 72e37048b..5e44de36f 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,16 +179,16 @@ 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(); - 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(); } @@ -195,20 +198,20 @@ 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(); - 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] 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/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); + } } } 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);