mirror of https://github.com/Squidex/squidex.git
committed by
GitHub
119 changed files with 1949 additions and 826 deletions
@ -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<T>(IRuleTriggerVisitor<T> visitor) |
||||
|
{ |
||||
|
return visitor.Visit(this); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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(); } |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; } |
||||
|
} |
||||
|
} |
||||
@ -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<AppEvent> @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<IUser> 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; |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<AssetChangedTriggerV2, AssetEvent, EnrichedAssetEvent> |
|
||||
{ |
|
||||
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); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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<ContentChangedTriggerV2, ContentEvent, EnrichedContentEvent> |
|
||||
{ |
|
||||
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<Guid> eventId) |
|
||||
{ |
|
||||
return eventId.Id == schema.SchemaId; |
|
||||
} |
|
||||
|
|
||||
private bool MatchsCondition(ContentChangedTriggerSchemaV2 schema, EnrichedSchemaEvent @event) |
|
||||
{ |
|
||||
return string.IsNullOrWhiteSpace(schema.Condition) || scriptEngine.Evaluate("event", @event, schema.Condition); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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<AssetChangedTriggerV2, AssetEvent, EnrichedAssetEvent> |
||||
|
{ |
||||
|
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<EnrichedAssetEvent> CreateEnrichedEventAsync(Envelope<AssetEvent> @event) |
||||
|
{ |
||||
|
var result = new EnrichedAssetEvent(); |
||||
|
|
||||
|
var asset = |
||||
|
(await grainFactory |
||||
|
.GetGrain<IAssetGrain>(@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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<ContentChangedTriggerV2, ContentEvent, EnrichedContentEvent> |
||||
|
{ |
||||
|
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<EnrichedContentEvent> CreateEnrichedEventAsync(Envelope<ContentEvent> @event) |
||||
|
{ |
||||
|
var result = new EnrichedContentEvent(); |
||||
|
|
||||
|
var content = |
||||
|
(await grainFactory |
||||
|
.GetGrain<IContentGrain>(@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<Guid> eventId) |
||||
|
{ |
||||
|
return eventId.Id == schema.SchemaId; |
||||
|
} |
||||
|
|
||||
|
private bool MatchsCondition(ContentChangedTriggerSchemaV2 schema, EnrichedSchemaEvent @event) |
||||
|
{ |
||||
|
return string.IsNullOrWhiteSpace(schema.Condition) || scriptEngine.Evaluate("event", @event, schema.Condition); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<EnrichedEvent> EnrichAsync(Envelope<AppEvent> @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<AppEvent> @event) |
|
||||
{ |
|
||||
var asset = |
|
||||
(await grainFactory |
|
||||
.GetGrain<IAssetGrain>(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<AppEvent> @event) |
|
||||
{ |
|
||||
var content = |
|
||||
(await grainFactory |
|
||||
.GetGrain<IContentGrain>(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<AppEvent> @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<IUser> 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; |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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<Guid> appId, int limits, int? numDays); |
||||
|
|
||||
|
Task RemoveTargetAsync(Guid ruleId); |
||||
|
|
||||
|
Task UpdateTargetAsync(Guid ruleId, int limits, int? numDays); |
||||
|
} |
||||
|
} |
||||
@ -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<IUsageTrackerGrain>(SingleGrain.Id); |
||||
|
} |
||||
|
|
||||
|
public async Task HandleAsync(CommandContext context, Func<Task> 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(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<UsageTrackerGrain.GrainState>, 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<Guid> AppId { get; set; } |
||||
|
} |
||||
|
|
||||
|
[CollectionName("UsageTracker")] |
||||
|
public sealed class GrainState |
||||
|
{ |
||||
|
public Dictionary<Guid, Target> Targets { get; set; } = new Dictionary<Guid, Target>(); |
||||
|
} |
||||
|
|
||||
|
public UsageTrackerGrain(IStore<string> 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<IEvent>(@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<Guid> 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<Target> updater) |
||||
|
{ |
||||
|
updater(State.Targets.GetOrAddNew(ruleId)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<UsageTrigger, AppUsageExceeded, EnrichedUsageExceededEvent> |
||||
|
{ |
||||
|
private const string EventName = "Usage exceeded"; |
||||
|
|
||||
|
protected override Task<EnrichedUsageExceededEvent> CreateEnrichedEventAsync(Envelope<AppUsageExceeded> @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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; } |
||||
|
} |
||||
|
} |
||||
@ -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()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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() |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The number of monthly api calls.
|
||||
|
/// </summary>
|
||||
|
public int Limit { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The number of days to check or null for the current month.
|
||||
|
/// </summary>
|
||||
|
[Range(1, 30)] |
||||
|
public int? NumDays { get; set; } |
||||
|
|
||||
|
public override RuleTrigger ToTrigger() |
||||
|
{ |
||||
|
return SimpleMapper.Map(this, new UsageTrigger()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
<div [formGroup]="triggerForm" class="form-horizontal"> |
||||
|
<div class="form-group"> |
||||
|
<label for="condition">Limit</label> |
||||
|
|
||||
|
<sqx-control-errors for="limit" [submitted]="triggerFormSubmitted"></sqx-control-errors> |
||||
|
|
||||
|
<input type="number" step="1" class="form-control" id="limit" formControlName="limit" /> |
||||
|
|
||||
|
<small class="form-text text-muted"> |
||||
|
The monthly api calls to trigger. |
||||
|
</small> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group"> |
||||
|
<label for="condition">Days</label> |
||||
|
|
||||
|
<sqx-control-errors for="numDays" fieldName="Days" [submitted]="triggerFormSubmitted"></sqx-control-errors> |
||||
|
|
||||
|
<input type="number" step="1" class="form-control" id="numDays" formControlName="numDays" /> |
||||
|
|
||||
|
<small class="form-text text-muted"> |
||||
|
The number of days to check or empty to check the current month. |
||||
|
</small> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,6 @@ |
|||||
|
@import '_vars'; |
||||
|
@import '_mixins'; |
||||
|
|
||||
|
textarea { |
||||
|
height: 100px; |
||||
|
} |
||||
@ -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) |
||||
|
])); |
||||
|
} |
||||
|
} |
||||
@ -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<IAppProvider>(); |
||||
|
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<ValidationError> |
||||
|
{ |
||||
|
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<ValidationError> |
||||
|
{ |
||||
|
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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<AppEvent>(@event)); |
||||
|
|
||||
|
Assert.Equal(@event.CallsCurrent, result.CallsCurrent); |
||||
|
Assert.Equal(@event.CallsLimit, result.CallsLimit); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue