diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs index 10d10fd0a..0793615af 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs @@ -11,6 +11,8 @@ namespace Squidex.Domain.Apps.Core.Rules { public interface IRuleTriggerVisitor { + T Visit(AssetChangedTrigger trigger); + T Visit(ContentChangedTrigger trigger); } } diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/AssetChangedTrigger.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/AssetChangedTrigger.cs new file mode 100644 index 000000000..67f099873 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/AssetChangedTrigger.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Rules.Triggers +{ + [TypeName(nameof(AssetChangedTrigger))] + public sealed class AssetChangedTrigger : RuleTrigger + { + public bool SendCreate { get; set; } + + public bool SendUpdate { get; set; } + + public bool SendRename { get; set; } + + public bool SendDelete { get; set; } + + public override T Accept(IRuleTriggerVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AlgoliaActionHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AlgoliaActionHandler.cs index 398f01c91..3a0114be4 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AlgoliaActionHandler.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AlgoliaActionHandler.cs @@ -6,7 +6,6 @@ // ========================================================================== using System; -using System.Collections.Concurrent; using System.Threading.Tasks; using Algolia.Search; using Newtonsoft.Json; @@ -23,7 +22,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions public sealed class AlgoliaActionHandler : RuleActionHandler { private const string SchemaNamePlaceholder = "$SCHEMA_NAME"; - private readonly ConcurrentDictionary<(string AppId, string ApiKey, string IndexName), Index> clients = new ConcurrentDictionary<(string AppId, string ApiKey, string IndexName), Index>(); + private readonly ClientPool<(string AppId, string ApiKey, string IndexName), Index> clients; private readonly RuleEventFormatter formatter; public AlgoliaActionHandler(RuleEventFormatter formatter) @@ -31,6 +30,13 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions Guard.NotNull(formatter, nameof(formatter)); this.formatter = formatter; + + clients = new ClientPool<(string AppId, string ApiKey, string IndexName), Index>(key => + { + var client = new AlgoliaClient(key.AppId, key.ApiKey); + + return client.InitIndex(key.IndexName); + }); } protected override (string Description, RuleJobData Data) CreateJob(Envelope @event, string eventName, AlgoliaAction action) @@ -102,16 +108,16 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions public override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(RuleJobData job) { + if (job["Operation"] == null) + { + return (null, new InvalidOperationException("The action cannot handle this event.")); + } + var appId = job["AppId"].Value(); var apiKey = job["ApiKey"].Value(); var indexName = job["IndexName"].Value(); - var index = clients.GetOrAdd((appId, apiKey, indexName), s => - { - var client = new AlgoliaClient(appId, apiKey); - - return client.InitIndex(indexName); - }); + var index = clients.GetClient((appId, apiKey, indexName)); var operation = job["Operation"].Value(); var content = job["Content"].Value(); @@ -138,9 +144,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions } default: - { - return ("Nothing to do!", null); - } + return (null, null); } } catch (AlgoliaException ex) diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AzureQueueActionHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AzureQueueActionHandler.cs index 92e28ba1b..56dd3154a 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AzureQueueActionHandler.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AzureQueueActionHandler.cs @@ -6,7 +6,6 @@ // ========================================================================== using System; -using System.Collections.Concurrent; using System.Threading.Tasks; using Microsoft.WindowsAzure.Storage; using Microsoft.WindowsAzure.Storage.Queue; @@ -22,7 +21,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions { public sealed class AzureQueueActionHandler : RuleActionHandler { - private readonly ConcurrentDictionary<(string ConnectionString, string QueueName), CloudQueue> queues = new ConcurrentDictionary<(string ConnectionString, string QueueName), CloudQueue>(); + private readonly ClientPool<(string ConnectionString, string QueueName), CloudQueue> clients; private readonly RuleEventFormatter formatter; public AzureQueueActionHandler(RuleEventFormatter formatter) @@ -30,6 +29,16 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions Guard.NotNull(formatter, nameof(formatter)); this.formatter = formatter; + + clients = new ClientPool<(string ConnectionString, string QueueName), CloudQueue>(key => + { + var storageAccount = CloudStorageAccount.Parse(key.ConnectionString); + + var queueClient = storageAccount.CreateCloudQueueClient(); + var queueRef = queueClient.GetQueueReference(key.QueueName); + + return queueRef; + }); } protected override (string Description, RuleJobData Data) CreateJob(Envelope @event, string eventName, AzureQueueAction action) @@ -52,15 +61,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions var queueConnectionString = job["QueueConnectionString"].Value(); var queueName = job["QueueName"].Value(); - var queue = queues.GetOrAdd((queueConnectionString, queueName), s => - { - var storageAccount = CloudStorageAccount.Parse(queueConnectionString); - - var queueClient = storageAccount.CreateCloudQueueClient(); - var queueRef = queueClient.GetQueueReference(queueName); - - return queueRef; - }); + var queue = clients.GetClient((queueConnectionString, queueName)); var messageBody = job["MessageBody"].ToString(Formatting.Indented); diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/SlackActionHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/SlackActionHandler.cs index dc10c3afd..d5becff40 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/SlackActionHandler.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/SlackActionHandler.cs @@ -23,7 +23,6 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions { public sealed class SlackActionHandler : RuleActionHandler { - private static readonly HttpClient Client = new HttpClient { Timeout = TimeSpan.FromSeconds(2) }; private readonly RuleEventFormatter formatter; public SlackActionHandler(RuleEventFormatter formatter) @@ -61,7 +60,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions try { - response = await Client.SendAsync(requestMsg); + response = await HttpClientPool.GetHttpClient().SendAsync(requestMsg); var responseString = await response.Content.ReadAsStringAsync(); var requestDump = DumpFormatter.BuildDump(requestMsg, response, requestBody, responseString, TimeSpan.Zero, false); diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs index bb0d00cbd..9fbecd68d 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs @@ -23,7 +23,6 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions { public sealed class WebhookActionHandler : RuleActionHandler { - private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(2); private readonly RuleEventFormatter formatter; public WebhookActionHandler(RuleEventFormatter formatter) @@ -59,15 +58,12 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions try { - using (var client = new HttpClient { Timeout = Timeout }) - { - response = await client.SendAsync(requestMsg); + response = await HttpClientPool.GetHttpClient().SendAsync(requestMsg); - var responseString = await response.Content.ReadAsStringAsync(); - var requestDump = DumpFormatter.BuildDump(requestMsg, response, requestBody, responseString, TimeSpan.Zero, false); + var responseString = await response.Content.ReadAsStringAsync(); + var requestDump = DumpFormatter.BuildDump(requestMsg, response, requestBody, responseString, TimeSpan.Zero, false); - return (requestDump, null); - } + return (requestDump, null); } catch (Exception ex) { diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/ClientPool.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/ClientPool.cs new file mode 100644 index 000000000..b126e4bf3 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/ClientPool.cs @@ -0,0 +1,39 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; + +#pragma warning disable RECS0108 // Warns about static fields in generic types + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + internal sealed class ClientPool + { + private static readonly TimeSpan TTL = TimeSpan.FromMinutes(30); + private readonly MemoryCache memoryCache = new MemoryCache(Options.Create(new MemoryCacheOptions())); + private readonly Func factory; + + public ClientPool(Func factory) + { + this.factory = factory; + } + + public TClient GetClient(TKey key) + { + if (!memoryCache.TryGetValue(key, out var client)) + { + client = factory(key); + + memoryCache.Set(key, client, TTL); + } + + return client; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/HttpClientPool.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/HttpClientPool.cs new file mode 100644 index 000000000..231920699 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/HttpClientPool.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Net.Http; + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public static class HttpClientPool + { + private static readonly ClientPool Pool = new ClientPool(key => + { + return new HttpClient { Timeout = TimeSpan.FromSeconds(2) }; + }); + + public static HttpClient GetHttpClient() + { + return Pool.GetClient(string.Empty); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/AssetChangedTriggerHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/AssetChangedTriggerHandler.cs new file mode 100644 index 000000000..22555b6e2 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/AssetChangedTriggerHandler.cs @@ -0,0 +1,31 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Assets; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Core.HandleRules.Triggers +{ + public sealed class AssetChangedTriggerHandler : RuleTriggerHandler + { + protected override bool Triggers(Envelope @event, AssetChangedTrigger trigger) + { + return @event.Payload is AssetEvent assetEvent && MatchsType(trigger, assetEvent); + } + + private static bool MatchsType(AssetChangedTrigger trigger, AssetEvent @event) + { + return + (trigger.SendCreate && @event is AssetCreated) || + (trigger.SendUpdate && @event is AssetUpdated) || + (trigger.SendDelete && @event is AssetDeleted) || + (trigger.SendRename && @event is AssetRenamed); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs b/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs index f4e8f1b0e..ea513751d 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs @@ -35,6 +35,11 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards return action.Accept(visitor); } + public Task> Visit(AssetChangedTrigger trigger) + { + return Task.FromResult(Enumerable.Empty()); + } + public async Task> Visit(ContentChangedTrigger trigger) { if (trigger.Schemas != null) 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 6d40e73b1..00ada885a 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs @@ -26,6 +26,11 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models.Converters return properties.Accept(Instance); } + public RuleTriggerDto Visit(AssetChangedTrigger trigger) + { + return SimpleMapper.Map(trigger, new AssetChangedTriggerDto()); + } + public RuleTriggerDto Visit(ContentChangedTrigger trigger) { return new ContentChangedTriggerDto diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleTriggerDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleTriggerDto.cs index b2ed18047..16a181e6c 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleTriggerDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleTriggerDto.cs @@ -13,6 +13,7 @@ using Squidex.Domain.Apps.Core.Rules; namespace Squidex.Areas.Api.Controllers.Rules.Models { [JsonConverter(typeof(JsonInheritanceConverter), "triggerType")] + [KnownType(typeof(AssetChangedTriggerDto))] [KnownType(typeof(ContentChangedTriggerDto))] public abstract class RuleTriggerDto { diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/AssetChangedTriggerDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/AssetChangedTriggerDto.cs new file mode 100644 index 000000000..740e13fea --- /dev/null +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/AssetChangedTriggerDto.cs @@ -0,0 +1,43 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using NJsonSchema.Annotations; +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 +{ + [JsonSchema("AssetChanged")] + public sealed class AssetChangedTriggerDto : RuleTriggerDto + { + /// + /// Determines whether to handle the event when an asset is created. + /// + public bool SendCreate { get; set; } + + /// + /// Determines whether to handle the event when an asset is updated. + /// + public bool SendUpdate { get; set; } + + /// + /// Determines whether to handle the event when an asset is renamed. + /// + public bool SendRename { get; set; } + + /// + /// Determines whether to handle the event when an asset is deleted. + /// + public bool SendDelete { get; set; } + + public override RuleTrigger ToTrigger() + { + return SimpleMapper.Map(this, new AssetChangedTrigger()); + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedTriggerSchemaDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedTriggerSchemaDto.cs index dc8b503db..dacd359b3 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedTriggerSchemaDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedTriggerSchemaDto.cs @@ -17,22 +17,22 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers public Guid SchemaId { get; set; } /// - /// True, when to send a message for created events. + /// Determines whether to handle the event when a content is created. /// public bool SendCreate { get; set; } /// - /// True, when to send a message for updated events. + /// Determines whether to handle the event when a content is updated. /// public bool SendUpdate { get; set; } /// - /// True, when to send a message for deleted events. + /// Determines whether to handle the event when a content is deleted. /// public bool SendDelete { get; set; } /// - /// True, when to send a message for published events. + /// Determines whether to handle the event when a content is published. /// public bool SendPublish { get; set; } } diff --git a/src/Squidex/Config/Domain/ReadServices.cs b/src/Squidex/Config/Domain/ReadServices.cs index d6e8e4979..e51951702 100644 --- a/src/Squidex/Config/Domain/ReadServices.cs +++ b/src/Squidex/Config/Domain/ReadServices.cs @@ -92,6 +92,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .AsSelf(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); diff --git a/src/Squidex/app/features/rules/declarations.ts b/src/Squidex/app/features/rules/declarations.ts index e4ead5d30..56bb26dbf 100644 --- a/src/Squidex/app/features/rules/declarations.ts +++ b/src/Squidex/app/features/rules/declarations.ts @@ -9,6 +9,7 @@ export * from './pages/rules/actions/algolia-action.component'; export * from './pages/rules/actions/azure-queue-action.component'; export * from './pages/rules/actions/slack-action.component'; 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/rule-wizard.component'; export * from './pages/rules/rules-page.component'; diff --git a/src/Squidex/app/features/rules/module.ts b/src/Squidex/app/features/rules/module.ts index 6a95936fb..a386b55c2 100644 --- a/src/Squidex/app/features/rules/module.ts +++ b/src/Squidex/app/features/rules/module.ts @@ -16,6 +16,7 @@ import { import { AlgoliaActionComponent, + AssetChangedTriggerComponent, AzureQueueActionComponent, ContentChangedTriggerComponent, RuleEventsPageComponent, @@ -53,6 +54,7 @@ const routes: Routes = [ ], declarations: [ AlgoliaActionComponent, + AssetChangedTriggerComponent, AzureQueueActionComponent, ContentChangedTriggerComponent, RuleEventsPageComponent, 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 77cad1927..1ea0805f2 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 @@ -39,6 +39,12 @@