mirror of https://github.com/Squidex/squidex.git
51 changed files with 718 additions and 504 deletions
@ -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 Trigger(EnrichedAssetEvent @event, AssetChangedTriggerV2 trigger) |
|
||||
{ |
|
||||
return string.IsNullOrWhiteSpace(trigger.Condition) || scriptEngine.Evaluate("event", @event, trigger.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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,207 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.Threading.Tasks; |
|
||||
using Microsoft.Extensions.Caching.Memory; |
|
||||
using Orleans; |
|
||||
using Squidex.Domain.Apps.Core.Contents; |
|
||||
using Squidex.Domain.Apps.Core.HandleRules; |
|
||||
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; |
|
||||
using Squidex.Domain.Apps.Entities.Assets; |
|
||||
using Squidex.Domain.Apps.Entities.Contents; |
|
||||
using Squidex.Domain.Apps.Events; |
|
||||
using Squidex.Domain.Apps.Events.Assets; |
|
||||
using Squidex.Domain.Apps.Events.Contents; |
|
||||
using Squidex.Infrastructure; |
|
||||
using Squidex.Infrastructure.EventSourcing; |
|
||||
using Squidex.Infrastructure.Reflection; |
|
||||
using Squidex.Shared.Users; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Entities.Rules |
|
||||
{ |
|
||||
public sealed class EventEnricher : IEventEnricher |
|
||||
{ |
|
||||
private static readonly TimeSpan UserCacheDuration = TimeSpan.FromMinutes(10); |
|
||||
private readonly IGrainFactory grainFactory; |
|
||||
private readonly IMemoryCache userCache; |
|
||||
private readonly IUserResolver userResolver; |
|
||||
|
|
||||
public EventEnricher(IGrainFactory grainFactory, IMemoryCache userCache, IUserResolver userResolver) |
|
||||
{ |
|
||||
Guard.NotNull(grainFactory, nameof(grainFactory)); |
|
||||
Guard.NotNull(userCache, nameof(userCache)); |
|
||||
Guard.NotNull(userResolver, nameof(userResolver)); |
|
||||
|
|
||||
this.grainFactory = grainFactory; |
|
||||
this.userCache = userCache; |
|
||||
this.userResolver = userResolver; |
|
||||
} |
|
||||
|
|
||||
public async Task<EnrichedEvent> EnrichAsync(Envelope<AppEvent> @event) |
|
||||
{ |
|
||||
Guard.NotNull(@event, nameof(@event)); |
|
||||
|
|
||||
switch (@event.Payload) |
|
||||
{ |
|
||||
case ContentEvent contentEvent: |
|
||||
{ |
|
||||
var result = new EnrichedContentEvent(); |
|
||||
|
|
||||
await Task.WhenAll( |
|
||||
EnrichContentAsync(result, contentEvent, @event), |
|
||||
EnrichDefaultAsync(result, @event)); |
|
||||
|
|
||||
return result; |
|
||||
} |
|
||||
|
|
||||
case AssetEvent assetEvent: |
|
||||
{ |
|
||||
var result = new EnrichedAssetEvent(); |
|
||||
|
|
||||
await Task.WhenAll( |
|
||||
EnrichAssetAsync(result, assetEvent, @event), |
|
||||
EnrichDefaultAsync(result, @event)); |
|
||||
|
|
||||
return result; |
|
||||
} |
|
||||
|
|
||||
case AppUsageExceeded usageExceeded: |
|
||||
{ |
|
||||
var result = new EnrichedUsageExceededEvent { Current = usageExceeded.CallsCurrent, Limit = usageExceeded.CallsLimit }; |
|
||||
|
|
||||
await EnrichDefaultAsync(result, @event); |
|
||||
|
|
||||
return result; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return null; |
|
||||
} |
|
||||
|
|
||||
private async Task EnrichAssetAsync(EnrichedAssetEvent result, AssetEvent assetEvent, Envelope<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 (result is EnrichedUserEvent userEvent) |
|
||||
{ |
|
||||
if (@event.Payload is SquidexEvent squidexEvent) |
|
||||
{ |
|
||||
userEvent.Actor = squidexEvent.Actor; |
|
||||
} |
|
||||
|
|
||||
userEvent.User = await FindUserAsync(userEvent.Actor); |
|
||||
} |
|
||||
|
|
||||
if (@event.Payload is AppEvent appEvent) |
|
||||
{ |
|
||||
result.AppId = appEvent.AppId; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private Task<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,65 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Orleans; |
||||
|
using Squidex.Domain.Apps.Core.Rules.Triggers; |
||||
|
using Squidex.Domain.Apps.Entities.Rules.Commands; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Commands; |
||||
|
using Squidex.Infrastructure.Orleans; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking |
||||
|
{ |
||||
|
public sealed class UsageTrackerCommandMiddleware : ICommandMiddleware |
||||
|
{ |
||||
|
private readonly IUsageTrackerGrain usageTrackerGrain; |
||||
|
|
||||
|
public UsageTrackerCommandMiddleware(IGrainFactory grainFactory) |
||||
|
{ |
||||
|
Guard.NotNull(grainFactory, nameof(grainFactory)); |
||||
|
|
||||
|
usageTrackerGrain = grainFactory.GetGrain<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 EnableRule enableRule: |
||||
|
await usageTrackerGrain.ActivateTargetAsync(enableRule.RuleId); |
||||
|
break; |
||||
|
case DisableRule disableRule: |
||||
|
await usageTrackerGrain.DeactivateTargetAsync(disableRule.RuleId); |
||||
|
break; |
||||
|
case CreateRule createRule: |
||||
|
{ |
||||
|
if (createRule.Trigger is UsageTrigger createdTrigger) |
||||
|
{ |
||||
|
await usageTrackerGrain.AddTargetAsync(createRule.RuleId, createRule.AppId, createdTrigger.Limit); |
||||
|
} |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
case UpdateRule ruleUpdated: |
||||
|
if (ruleUpdated.Trigger is UsageTrigger updatedTrigger) |
||||
|
{ |
||||
|
await usageTrackerGrain.UpdateTargetAsync(ruleUpdated.RuleId, updatedTrigger.Limit); |
||||
|
} |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
await next(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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,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()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue