diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionAttribute.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionAttribute.cs new file mode 100644 index 000000000..adb1d1064 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionAttribute.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + public sealed class RuleActionAttribute : Attribute + { + public string Link { get; set; } + + public string Display { get; set; } + + public string Description { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs index ddab38183..d0e6009b4 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using Newtonsoft.Json.Linq; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules; +using Squidex.Infrastructure; #pragma warning disable RECS0083 // Shows NotImplementedException throws in the quick task bar @@ -17,11 +18,50 @@ namespace Squidex.Domain.Apps.Core.HandleRules { public abstract class RuleActionHandler : IRuleActionHandler where TAction : RuleAction { + private readonly RuleEventFormatter formatter; + Type IRuleActionHandler.ActionType { get { return typeof(TAction); } } + protected RuleActionHandler(RuleEventFormatter formatter) + { + Guard.NotNull(formatter, nameof(formatter)); + + this.formatter = formatter; + } + + protected virtual string ToPayloadJson(T @event) + { + return formatter.ToPayload(@event).ToString(); + } + + protected virtual string ToEnvelopeJson(EnrichedEvent @event) + { + return formatter.ToEnvelope(@event).ToString(); + } + + protected virtual JObject ToPayload(T @event) + { + return formatter.ToPayload(@event); + } + + protected virtual JObject ToEnvelope(EnrichedEvent @event) + { + return formatter.ToEnvelope(@event); + } + + protected string Format(Uri uri, EnrichedEvent @event) + { + return formatter.Format(uri.ToString(), @event); + } + + protected string Format(string text, EnrichedEvent @event) + { + return formatter.Format(text, @event); + } + async Task<(string Description, JObject Data)> IRuleActionHandler.CreateJobAsync(EnrichedEvent @event, RuleAction action) { var (description, data) = await CreateJobAsync(@event, (TAction)action); diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandlerAttribute.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandlerAttribute.cs new file mode 100644 index 000000000..de70fcc3d --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandlerAttribute.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + public sealed class RuleActionHandlerAttribute : Attribute + { + public Type HandlerType { get; } + + public RuleActionHandlerAttribute(Type handlerType) + { + Guard.NotNull(handlerType, nameof(handlerType)); + + HandlerType = handlerType; + + if (!typeof(IRuleActionHandler).IsAssignableFrom(handlerType)) + { + throw new ArgumentException($"Handler type must implement {typeof(IRuleActionHandler)}.", nameof(handlerType)); + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Rules/Actions/Algolia/AlgoliaAction.cs b/src/Squidex.Domain.Apps.Rules/Actions/Algolia/AlgoliaAction.cs index 230dea494..e7f19e23c 100644 --- a/src/Squidex.Domain.Apps.Rules/Actions/Algolia/AlgoliaAction.cs +++ b/src/Squidex.Domain.Apps.Rules/Actions/Algolia/AlgoliaAction.cs @@ -6,10 +6,13 @@ // ========================================================================== using System.ComponentModel.DataAnnotations; +using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules; namespace Squidex.Domain.Apps.Rules.Action.Algolia { + [RuleActionHandler(typeof(AlgoliaActionHandler))] + [RuleAction(Description = "")] public sealed class AlgoliaAction : RuleAction { [Required] diff --git a/src/Squidex.Domain.Apps.Rules/Actions/Algolia/AlgoliaActionHandler.cs b/src/Squidex.Domain.Apps.Rules/Actions/Algolia/AlgoliaActionHandler.cs index ceceb843c..d15d307dc 100644 --- a/src/Squidex.Domain.Apps.Rules/Actions/Algolia/AlgoliaActionHandler.cs +++ b/src/Squidex.Domain.Apps.Rules/Actions/Algolia/AlgoliaActionHandler.cs @@ -12,38 +12,18 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Infrastructure; - -#pragma warning disable SA1649 // File name must match first type name namespace Squidex.Domain.Apps.Rules.Action.Algolia { - public sealed class AlgoliaJob - { - public string AppId { get; set; } - - public string ApiKey { get; set; } - - public string ContentId { get; set; } - - public string IndexName { get; set; } - - public JObject Content { get; set; } - } - public sealed class AlgoliaActionHandler : RuleActionHandler { private const string DescriptionIgnore = "Ignore"; private readonly ClientPool<(string AppId, string ApiKey, string IndexName), Index> clients; - private readonly RuleEventFormatter formatter; public AlgoliaActionHandler(RuleEventFormatter formatter) + : base(formatter) { - 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); @@ -64,7 +44,7 @@ namespace Squidex.Domain.Apps.Rules.Action.Algolia AppId = action.AppId, ApiKey = action.ApiKey, ContentId = contentId, - IndexName = formatter.Format(action.IndexName, @event) + IndexName = Format(action.IndexName, @event) }; if (contentEvent.Type == EnrichedContentEventType.Deleted || @@ -76,7 +56,7 @@ namespace Squidex.Domain.Apps.Rules.Action.Algolia { ruleDescription = $"Add entry to Algolia index: {action.IndexName}"; - ruleJob.Content = formatter.ToPayload(contentEvent); + ruleJob.Content = ToPayload(contentEvent); ruleJob.Content["objectID"] = contentId; } @@ -116,4 +96,17 @@ namespace Squidex.Domain.Apps.Rules.Action.Algolia } } } + + public sealed class AlgoliaJob + { + public string AppId { get; set; } + + public string ApiKey { get; set; } + + public string ContentId { get; set; } + + public string IndexName { get; set; } + + public JObject Content { get; set; } + } } diff --git a/src/Squidex.Domain.Apps.Rules/Actions/AzureQueue/AzureQueueAction.cs b/src/Squidex.Domain.Apps.Rules/Actions/AzureQueue/AzureQueueAction.cs index 1b06c55ca..441abba82 100644 --- a/src/Squidex.Domain.Apps.Rules/Actions/AzureQueue/AzureQueueAction.cs +++ b/src/Squidex.Domain.Apps.Rules/Actions/AzureQueue/AzureQueueAction.cs @@ -8,11 +8,14 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Text.RegularExpressions; +using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Rules.Action.AzureQueue { + [RuleActionHandler(typeof(AzureQueueActionHandler))] + [RuleAction(Description = "")] public sealed class AzureQueueAction : RuleAction { [Required] diff --git a/src/Squidex.Domain.Apps.Rules/Actions/AzureQueue/AzureQueueActionHandler.cs b/src/Squidex.Domain.Apps.Rules/Actions/AzureQueue/AzureQueueActionHandler.cs index 1863c9ad5..dca1aa230 100644 --- a/src/Squidex.Domain.Apps.Rules/Actions/AzureQueue/AzureQueueActionHandler.cs +++ b/src/Squidex.Domain.Apps.Rules/Actions/AzureQueue/AzureQueueActionHandler.cs @@ -9,46 +9,18 @@ using System; using System.Threading.Tasks; using Microsoft.WindowsAzure.Storage; using Microsoft.WindowsAzure.Storage.Queue; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Infrastructure; - -#pragma warning disable SA1649 // File name must match first type name namespace Squidex.Domain.Apps.Rules.Action.AzureQueue { - public sealed class AzureQueueJob - { - public string QueueConnectionString { get; set; } - - public string QueueName { get; set; } - - public string MessageBodyV2 { get; set; } - - public JObject MessageBody { get; set; } - - public string Body - { - get - { - return MessageBodyV2 ?? MessageBody.ToString(Formatting.Indented); - } - } - } - public sealed class AzureQueueActionHandler : RuleActionHandler { private readonly ClientPool<(string ConnectionString, string QueueName), CloudQueue> clients; - private readonly RuleEventFormatter formatter; public AzureQueueActionHandler(RuleEventFormatter formatter) + : base(formatter) { - Guard.NotNull(formatter, nameof(formatter)); - - this.formatter = formatter; - clients = new ClientPool<(string ConnectionString, string QueueName), CloudQueue>(key => { var storageAccount = CloudStorageAccount.Parse(key.ConnectionString); @@ -62,16 +34,14 @@ namespace Squidex.Domain.Apps.Rules.Action.AzureQueue protected override (string Description, AzureQueueJob Data) CreateJob(EnrichedEvent @event, AzureQueueAction action) { - var body = formatter.ToEnvelope(@event).ToString(Formatting.Indented); - - var queueName = formatter.Format(action.Queue, @event); + var queueName = Format(action.Queue, @event); - var ruleDescription = $"Send AzureQueueJob to azure queue '{action.Queue}'"; + var ruleDescription = $"Send AzureQueueJob to azure queue '{queueName}'"; var ruleJob = new AzureQueueJob { QueueConnectionString = action.ConnectionString, QueueName = queueName, - MessageBodyV2 = body + MessageBodyV2 = ToEnvelopeJson(@event) }; return (ruleDescription, ruleJob); @@ -81,9 +51,18 @@ namespace Squidex.Domain.Apps.Rules.Action.AzureQueue { var queue = clients.GetClient((job.QueueConnectionString, job.QueueName)); - await queue.AddMessageAsync(new CloudQueueMessage(job.Body)); + await queue.AddMessageAsync(new CloudQueueMessage(job.MessageBodyV2)); return ("Completed", null); } } + + public sealed class AzureQueueJob + { + public string QueueConnectionString { get; set; } + + public string QueueName { get; set; } + + public string MessageBodyV2 { get; set; } + } } diff --git a/src/Squidex.Domain.Apps.Rules/Actions/Discourse/DiscourseAction.cs b/src/Squidex.Domain.Apps.Rules/Actions/Discourse/DiscourseAction.cs new file mode 100644 index 000000000..b49d11455 --- /dev/null +++ b/src/Squidex.Domain.Apps.Rules/Actions/Discourse/DiscourseAction.cs @@ -0,0 +1,42 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.ComponentModel.DataAnnotations; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Rules.Actions.Discourse +{ + [RuleActionHandler(typeof(DiscourseActionHandler))] + [RuleAction(Description = "")] + public sealed class DiscourseAction : RuleAction + { + [AbsoluteUrl] + [Required] + [Display(Name = "Url", Description = "he url to the discourse server.")] + public Uri Url { get; set; } + + [Required] + [Display(Name = "Api Key", Description = "The api key.")] + public string ApiKey { get; set; } + + [Required] + [Display(Name = "Text", Description = "The text as markdown.")] + public string Text { get; set; } + + [Display(Name = "Title", Description = "The optional title when creating new topics.")] + public string Title { get; set; } + + [Display(Name = "Topic", Description = "The optional topic id.")] + public int? Topic { get; set; } + + [Display(Name = "Category", Description = "The optional category id.")] + public int? Category { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Rules/Actions/Discourse/DiscourseActionHandler.cs b/src/Squidex.Domain.Apps.Rules/Actions/Discourse/DiscourseActionHandler.cs new file mode 100644 index 000000000..799d4315e --- /dev/null +++ b/src/Squidex.Domain.Apps.Rules/Actions/Discourse/DiscourseActionHandler.cs @@ -0,0 +1,48 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; + +namespace Squidex.Domain.Apps.Rules.Actions.Discourse +{ + public sealed class DiscourseActionHandler : RuleActionHandler + { + private readonly IHttpClientFactory httpClientFactory; + + public DiscourseActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory) + : base(formatter) + { + this.httpClientFactory = httpClientFactory; + } + + protected override Task<(string Description, DiscourseJob Data)> CreateJobAsync(EnrichedEvent @event, DiscourseAction action) + { + return base.CreateJobAsync(@event, action); + } + + protected override Task<(string Dump, Exception Exception)> ExecuteJobAsync(DiscourseJob job) + { + using (var client = httpClientFactory.CreateClient()) + { + // Foo + } + + return Task.FromResult<(string Dump, Exception Exception)>((string.Empty, null)); + } + } + + public sealed class DiscourseJob + { + public string RequestUrl { get; set; } + + public string RequestBody { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Rules/Actions/ElasticSearch/ElasticSearchAction.cs b/src/Squidex.Domain.Apps.Rules/Actions/ElasticSearch/ElasticSearchAction.cs index 4903c3a33..f07cf3e7d 100644 --- a/src/Squidex.Domain.Apps.Rules/Actions/ElasticSearch/ElasticSearchAction.cs +++ b/src/Squidex.Domain.Apps.Rules/Actions/ElasticSearch/ElasticSearchAction.cs @@ -7,11 +7,14 @@ using System; using System.ComponentModel.DataAnnotations; +using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Rules.Action.ElasticSearch { + [RuleActionHandler(typeof(ElasticSearchActionHandler))] + [RuleAction(Description = "")] public sealed class ElasticSearchAction : RuleAction { [AbsoluteUrl] diff --git a/src/Squidex.Domain.Apps.Rules/Actions/ElasticSearch/ElasticSearchActionHandler.cs b/src/Squidex.Domain.Apps.Rules/Actions/ElasticSearch/ElasticSearchActionHandler.cs index 90023f661..dce818f7b 100644 --- a/src/Squidex.Domain.Apps.Rules/Actions/ElasticSearch/ElasticSearchActionHandler.cs +++ b/src/Squidex.Domain.Apps.Rules/Actions/ElasticSearch/ElasticSearchActionHandler.cs @@ -11,42 +11,18 @@ using Elasticsearch.Net; using Newtonsoft.Json.Linq; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; -using Squidex.Infrastructure; - -#pragma warning disable SA1649 // File name must match first type name namespace Squidex.Domain.Apps.Rules.Action.ElasticSearch { - public sealed class ElasticSearchJob - { - public string Host { get; set; } - - public string Username { get; set; } - - public string Password { get; set; } - - public string ContentId { get; set; } - - public string IndexName { get; set; } - - public string IndexType { get; set; } - - public JObject Content { get; set; } - } - public sealed class ElasticSearchActionHandler : RuleActionHandler { private const string DescriptionIgnore = "Ignore"; private readonly ClientPool<(Uri Host, string Username, string Password), ElasticLowLevelClient> clients; - private readonly RuleEventFormatter formatter; public ElasticSearchActionHandler(RuleEventFormatter formatter) + : base(formatter) { - Guard.NotNull(formatter, nameof(formatter)); - - this.formatter = formatter; - clients = new ClientPool<(Uri Host, string Username, string Password), ElasticLowLevelClient>(key => { var config = new ConnectionConfiguration(key.Host); @@ -73,8 +49,8 @@ namespace Squidex.Domain.Apps.Rules.Action.ElasticSearch Username = action.Username, Password = action.Password, ContentId = contentId, - IndexName = formatter.Format(action.IndexName, @event), - IndexType = formatter.Format(action.IndexType, @event) + IndexName = Format(action.IndexName, @event), + IndexType = Format(action.IndexType, @event) }; if (contentEvent.Type == EnrichedContentEventType.Deleted || @@ -86,7 +62,7 @@ namespace Squidex.Domain.Apps.Rules.Action.ElasticSearch { ruleDescription = $"Upsert to index: {action.IndexName}"; - ruleJob.Content = formatter.ToPayload(contentEvent); + ruleJob.Content = ToPayload(contentEvent); ruleJob.Content["objectID"] = contentId; } } @@ -121,4 +97,21 @@ namespace Squidex.Domain.Apps.Rules.Action.ElasticSearch } } } + + public sealed class ElasticSearchJob + { + public string Host { get; set; } + + public string Username { get; set; } + + public string Password { get; set; } + + public string ContentId { get; set; } + + public string IndexName { get; set; } + + public string IndexType { get; set; } + + public JObject Content { get; set; } + } } diff --git a/src/Squidex.Domain.Apps.Rules/Actions/Fastly/FastlyAction.cs b/src/Squidex.Domain.Apps.Rules/Actions/Fastly/FastlyAction.cs index 096eca62b..d15d6e053 100644 --- a/src/Squidex.Domain.Apps.Rules/Actions/Fastly/FastlyAction.cs +++ b/src/Squidex.Domain.Apps.Rules/Actions/Fastly/FastlyAction.cs @@ -6,10 +6,13 @@ // ========================================================================== using System.ComponentModel.DataAnnotations; +using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules; namespace Squidex.Domain.Apps.Rules.Action.Fastly { + [RuleActionHandler(typeof(FastlyActionHandler))] + [RuleAction(Description = "")] public sealed class FastlyAction : RuleAction { [Required] diff --git a/src/Squidex.Domain.Apps.Rules/Actions/Fastly/FastlyActionHandler.cs b/src/Squidex.Domain.Apps.Rules/Actions/Fastly/FastlyActionHandler.cs index 55a33f993..5ae76f0c3 100644 --- a/src/Squidex.Domain.Apps.Rules/Actions/Fastly/FastlyActionHandler.cs +++ b/src/Squidex.Domain.Apps.Rules/Actions/Fastly/FastlyActionHandler.cs @@ -11,35 +11,22 @@ using System.Threading.Tasks; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules.Actions.Utils; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; - -#pragma warning disable SA1649 // File name must match first type name +using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Rules.Action.Fastly { - public sealed class FastlyJob - { - public string FastlyApiKey { get; set; } - - public string FastlyServiceID { get; set; } - - public string Key { get; set; } - } - public sealed class FastlyActionHandler : RuleActionHandler { private const string Description = "Purge key in fastly"; - private readonly ClientPool clients; + private readonly IHttpClientFactory httpClientFactory; - public FastlyActionHandler() + public FastlyActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory) + : base(formatter) { - clients = new ClientPool(key => - { - return new HttpClient - { - Timeout = TimeSpan.FromSeconds(2) - }; - }); + Guard.NotNull(httpClientFactory, nameof(httpClientFactory)); + + this.httpClientFactory = httpClientFactory; } protected override (string Description, FastlyJob Data) CreateJob(EnrichedEvent @event, FastlyAction action) @@ -56,19 +43,26 @@ namespace Squidex.Domain.Apps.Rules.Action.Fastly protected override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(FastlyJob job) { - var httpClient = clients.GetClient(string.Empty); + using (var httpClient = httpClientFactory.CreateClient()) + { + httpClient.Timeout = TimeSpan.FromSeconds(2); + + var requestUrl = $"https://api.fastly.com/service/{job.FastlyServiceID}/purge/{job.Key}"; + var request = new HttpRequestMessage(HttpMethod.Post, requestUrl); + + request.Headers.Add("Fastly-Key", job.FastlyApiKey); - return await httpClient.OneWayRequestAsync(BuildRequest(job), null); + return await httpClient.OneWayRequestAsync(request, null); + } } + } - private static HttpRequestMessage BuildRequest(FastlyJob job) - { - var requestUrl = $"https://api.fastly.com/service/{job.FastlyServiceID}/purge/{job.Key}"; - var request = new HttpRequestMessage(HttpMethod.Post, requestUrl); + public sealed class FastlyJob + { + public string FastlyApiKey { get; set; } - request.Headers.Add("Fastly-Key", job.FastlyApiKey); + public string FastlyServiceID { get; set; } - return request; - } + public string Key { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Rules/Actions/Medium/MediumAction.cs b/src/Squidex.Domain.Apps.Rules/Actions/Medium/MediumAction.cs index 2350ebaea..97c9f3b66 100644 --- a/src/Squidex.Domain.Apps.Rules/Actions/Medium/MediumAction.cs +++ b/src/Squidex.Domain.Apps.Rules/Actions/Medium/MediumAction.cs @@ -6,10 +6,13 @@ // ========================================================================== using System.ComponentModel.DataAnnotations; +using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules; namespace Squidex.Domain.Apps.Rules.Action.Medium { + [RuleActionHandler(typeof(MediumActionHandler))] + [RuleAction(Description = "")] public sealed class MediumAction : RuleAction { [Required] diff --git a/src/Squidex.Domain.Apps.Rules/Actions/Medium/MediumActionHandler.cs b/src/Squidex.Domain.Apps.Rules/Actions/Medium/MediumActionHandler.cs index 95e91b08c..050e1fc7e 100644 --- a/src/Squidex.Domain.Apps.Rules/Actions/Medium/MediumActionHandler.cs +++ b/src/Squidex.Domain.Apps.Rules/Actions/Medium/MediumActionHandler.cs @@ -32,38 +32,22 @@ namespace Squidex.Domain.Apps.Rules.Action.Medium { private const string Description = "Post to medium"; - private readonly RuleEventFormatter formatter; - private readonly ClientPool clients; + private readonly IHttpClientFactory httpClientFactory; - public MediumActionHandler(RuleEventFormatter formatter) + public MediumActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory) + : base(formatter) { - Guard.NotNull(formatter, nameof(formatter)); - - this.formatter = formatter; - - clients = new ClientPool(key => - { - var client = new HttpClient - { - Timeout = TimeSpan.FromSeconds(4) - }; - - client.DefaultRequestHeaders.Add("Accept", "application/json"); - client.DefaultRequestHeaders.Add("Accept-Charset", "utf-8"); - client.DefaultRequestHeaders.Add("User-Agent", "Squidex Headless CMS"); - - return client; - }); + this.httpClientFactory = httpClientFactory; } protected override (string Description, MediumJob Data) CreateJob(EnrichedEvent @event, MediumAction action) { var requestBody = new JObject( - new JProperty("title", formatter.Format(action.Title, @event)), + new JProperty("title", Format(action.Title, @event)), new JProperty("contentFormat", action.IsHtml ? "html" : "markdown"), - new JProperty("content", formatter.Format(action.Content, @event)), - new JProperty("canonicalUrl", formatter.Format(action.CanonicalUrl, @event)), + new JProperty("content", Format(action.Content, @event)), + new JProperty("canonicalUrl", Format(action.CanonicalUrl, @event)), new JProperty("tags", ParseTags(@event, action))); var ruleJob = new MediumJob { AccessToken = action.AccessToken, RequestBody = requestBody.ToString(Formatting.Indented) }; @@ -81,7 +65,7 @@ namespace Squidex.Domain.Apps.Rules.Action.Medium string[] tags; try { - var jsonTags = formatter.Format(action.Tags, @event); + var jsonTags = Format(action.Tags, @event); tags = JsonConvert.DeserializeObject(jsonTags); } @@ -95,30 +79,36 @@ namespace Squidex.Domain.Apps.Rules.Action.Medium protected override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(MediumJob job) { - var httpClient = clients.GetClient(string.Empty); + using (var httpClient = httpClientFactory.CreateClient()) + { + httpClient.Timeout = TimeSpan.FromSeconds(4); + httpClient.DefaultRequestHeaders.Add("Accept", "application/json"); + httpClient.DefaultRequestHeaders.Add("Accept-Charset", "utf-8"); + httpClient.DefaultRequestHeaders.Add("User-Agent", "Squidex Headless CMS"); - string id; + string id; - HttpResponseMessage response = null; + HttpResponseMessage response = null; - var meRequest = BuildMeRequest(job); - try - { - response = await httpClient.SendAsync(meRequest); + var meRequest = BuildMeRequest(job); + try + { + response = await httpClient.SendAsync(meRequest); - var responseString = await response.Content.ReadAsStringAsync(); - var responseJson = JToken.Parse(responseString); + var responseString = await response.Content.ReadAsStringAsync(); + var responseJson = JToken.Parse(responseString); - id = responseJson["data"]["id"].ToString(); - } - catch (Exception ex) - { - var requestDump = DumpFormatter.BuildDump(meRequest, response, ex.ToString()); + id = responseJson["data"]["id"].ToString(); + } + catch (Exception ex) + { + var requestDump = DumpFormatter.BuildDump(meRequest, response, ex.ToString()); - return (requestDump, ex); - } + return (requestDump, ex); + } - return await httpClient.OneWayRequestAsync(BuildPostRequest(job, id), job.RequestBody); + return await httpClient.OneWayRequestAsync(BuildPostRequest(job, id), job.RequestBody); + } } private static HttpRequestMessage BuildPostRequest(MediumJob job, string id) diff --git a/src/Squidex.Domain.Apps.Rules/Actions/RuleActionRegistry.cs b/src/Squidex.Domain.Apps.Rules/Actions/RuleActionRegistry.cs deleted file mode 100644 index 1ddab84b3..000000000 --- a/src/Squidex.Domain.Apps.Rules/Actions/RuleActionRegistry.cs +++ /dev/null @@ -1,106 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Rules.Action.Algolia; -using Squidex.Domain.Apps.Rules.Action.AzureQueue; -using Squidex.Domain.Apps.Rules.Action.ElasticSearch; -using Squidex.Domain.Apps.Rules.Action.Fastly; -using Squidex.Domain.Apps.Rules.Action.Medium; -using Squidex.Domain.Apps.Rules.Action.Slack; -using Squidex.Domain.Apps.Rules.Action.Twitter; -using Squidex.Domain.Apps.Rules.Action.Webhook; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Rules.Actions -{ - public static class RuleActionRegistry - { - private const string Suffix = "Action"; - private static readonly HashSet ActionHandlerTypes = new HashSet(); - private static readonly Dictionary ActionTypes = new Dictionary(); - - public static IReadOnlyDictionary Actions - { - get { return ActionTypes; } - } - - public static IReadOnlyCollection ActionHandlers - { - get { return ActionHandlerTypes; } - } - - static RuleActionRegistry() - { - Register< - AlgoliaAction, - AlgoliaActionHandler>(); - - Register< - AzureQueueAction, - AzureQueueActionHandler>(); - - Register< - ElasticSearchAction, - ElasticSearchActionHandler>(); - - Register< - FastlyAction, - FastlyActionHandler>(); - - Register< - MediumAction, - MediumActionHandler>(); - - Register< - SlackAction, - SlackActionHandler>(); - - Register< - TweetAction, - TweetActionHandler>(); - - Register< - WebhookAction, - WebhookActionHandler>(); - } - - public static void Register() where TAction : RuleAction where THandler : IRuleActionHandler - { - AddActionType(); - AddActionHandler(); - } - - private static void AddActionHandler() where THandler : IRuleActionHandler - { - ActionHandlerTypes.Add(typeof(THandler)); - } - - private static void AddActionType() where TAction : RuleAction - { - var name = typeof(TAction).Name; - - if (name.EndsWith(Suffix, StringComparison.Ordinal)) - { - name = name.Substring(0, name.Length - Suffix.Length); - } - - ActionTypes.Add(name, typeof(TAction)); - } - - public static void RegisterTypes(TypeNameRegistry typeNameRegistry) - { - foreach (var actionType in ActionTypes.Values) - { - typeNameRegistry.Map(actionType, actionType.Name); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Rules/Actions/RuleElement.cs b/src/Squidex.Domain.Apps.Rules/Actions/RuleElement.cs new file mode 100644 index 000000000..37cc7f689 --- /dev/null +++ b/src/Squidex.Domain.Apps.Rules/Actions/RuleElement.cs @@ -0,0 +1,32 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Domain.Apps.Rules.Actions +{ + public sealed class RuleElement + { + public Type Type { get; } + + public string Link { get; set; } + + public string Display { get; } + + public string Description { get; } + + public RuleElement(Type type, string display, string description, string link = null) + { + Type = type; + + Display = display; + Description = description; + + Link = link; + } + } +} diff --git a/src/Squidex.Domain.Apps.Rules/Actions/RuleElementRegistry.cs b/src/Squidex.Domain.Apps.Rules/Actions/RuleElementRegistry.cs new file mode 100644 index 000000000..934ff4933 --- /dev/null +++ b/src/Squidex.Domain.Apps.Rules/Actions/RuleElementRegistry.cs @@ -0,0 +1,92 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Rules.Actions +{ + public static class RuleElementRegistry + { + private const string Suffix = "Action"; + private static readonly HashSet ActionHandlerTypes = new HashSet(); + private static readonly Dictionary ActionTypes = new Dictionary(); + private static readonly Dictionary TriggerTypes = new Dictionary(); + + public static IReadOnlyDictionary Actions + { + get { return ActionTypes; } + } + + public static IReadOnlyDictionary Triggers + { + get { return TriggerTypes; } + } + + public static IReadOnlyCollection ActionHandlers + { + get { return ActionHandlerTypes; } + } + + static RuleElementRegistry() + { + TriggerTypes["ContentChanged"] = + new RuleElement( + typeof(ContentChangedTrigger), + "Content changed", + "Content changed like created, updated, published, unpublished..."); + + TriggerTypes["AssetChanged"] = + new RuleElement( + typeof(AssetChangedTrigger), + "Asset changed", + "Asset changed like created, updated, renamed..."); + + var actionTypes = + typeof(RuleElementRegistry).Assembly + .GetTypes() + .Where(x => typeof(RuleAction).IsAssignableFrom(x)) + .Where(x => x.GetCustomAttribute() != null) + .Where(x => x.GetCustomAttribute() != null) + .ToList(); + + foreach (var actionType in actionTypes) + { + var name = actionType.Name; + + if (name.EndsWith(Suffix, StringComparison.Ordinal)) + { + name = name.Substring(0, name.Length - Suffix.Length); + } + + var metadata = actionType.GetCustomAttribute(); + + ActionTypes[name] = + new RuleElement(actionType, + metadata.Display, + metadata.Description, + metadata.Link); + + ActionHandlerTypes.Add(actionType.GetCustomAttribute().HandlerType); + } + } + + public static void RegisterTypes(TypeNameRegistry typeNameRegistry) + { + foreach (var actionType in ActionTypes.Values) + { + typeNameRegistry.Map(actionType.Type, actionType.Type.Name); + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Rules/Actions/Slack/SlackAction.cs b/src/Squidex.Domain.Apps.Rules/Actions/Slack/SlackAction.cs index c799efcfb..b8e1d6cd1 100644 --- a/src/Squidex.Domain.Apps.Rules/Actions/Slack/SlackAction.cs +++ b/src/Squidex.Domain.Apps.Rules/Actions/Slack/SlackAction.cs @@ -7,11 +7,14 @@ using System; using System.ComponentModel.DataAnnotations; +using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Rules.Action.Slack { + [RuleActionHandler(typeof(SlackActionHandler))] + [RuleAction(Description = "")] public sealed class SlackAction : RuleAction { [AbsoluteUrl] diff --git a/src/Squidex.Domain.Apps.Rules/Actions/Slack/SlackActionHandler.cs b/src/Squidex.Domain.Apps.Rules/Actions/Slack/SlackActionHandler.cs index 246004808..e82e00ced 100644 --- a/src/Squidex.Domain.Apps.Rules/Actions/Slack/SlackActionHandler.cs +++ b/src/Squidex.Domain.Apps.Rules/Actions/Slack/SlackActionHandler.cs @@ -16,79 +16,57 @@ using Squidex.Domain.Apps.Core.HandleRules.Actions.Utils; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; using Squidex.Infrastructure; -#pragma warning disable SA1649 // File name must match first type name - namespace Squidex.Domain.Apps.Rules.Action.Slack { - public sealed class SlackJob - { - public string RequestUrl { get; set; } - - public string RequestBodyV2 { get; set; } - - public JObject RequestBody { get; set; } - - public string Body - { - get - { - return RequestBodyV2 ?? RequestBody.ToString(Formatting.Indented); - } - } - } - public sealed class SlackActionHandler : RuleActionHandler { private const string Description = "Send message to slack"; - private readonly RuleEventFormatter formatter; - private readonly ClientPool clients; + private readonly IHttpClientFactory httpClientFactory; - public SlackActionHandler(RuleEventFormatter formatter) + public SlackActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory) + : base(formatter) { - Guard.NotNull(formatter, nameof(formatter)); - - this.formatter = formatter; + Guard.NotNull(httpClientFactory, nameof(httpClientFactory)); - clients = new ClientPool(key => - { - return new HttpClient - { - Timeout = TimeSpan.FromSeconds(2) - }; - }); + this.httpClientFactory = httpClientFactory; } protected override (string Description, SlackJob Data) CreateJob(EnrichedEvent @event, SlackAction action) { var body = new JObject( - new JProperty("text", formatter.Format(action.Text, @event))); + new JProperty("text", Format(action.Text, @event))); var ruleJob = new SlackJob { RequestUrl = action.WebhookUrl.ToString(), - RequestBodyV2 = body.ToString(Formatting.Indented) + RequestBody = body.ToString(Formatting.Indented) }; return (Description, ruleJob); } - protected override Task<(string Dump, Exception Exception)> ExecuteJobAsync(SlackJob job) + protected override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(SlackJob job) { - var httpClient = clients.GetClient(string.Empty); + using (var httpClient = httpClientFactory.CreateClient()) + { + httpClient.Timeout = TimeSpan.FromSeconds(2); + + var request = new HttpRequestMessage(HttpMethod.Post, job.RequestUrl) + { + Content = new StringContent(job.RequestBody, Encoding.UTF8, "application/json") + }; - return httpClient.OneWayRequestAsync(BuildRequest(job, job.Body), job.Body); + return await httpClient.OneWayRequestAsync(request, job.RequestBody); + } } + } - private static HttpRequestMessage BuildRequest(SlackJob job, string requestBody) - { - var request = new HttpRequestMessage(HttpMethod.Post, job.RequestUrl) - { - Content = new StringContent(requestBody, Encoding.UTF8, "application/json") - }; + public sealed class SlackJob + { + public string RequestUrl { get; set; } - return request; - } + public string RequestBody { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Rules/Actions/Twitter/TweetAction.cs b/src/Squidex.Domain.Apps.Rules/Actions/Twitter/TweetAction.cs index c3455906a..8915aeaed 100644 --- a/src/Squidex.Domain.Apps.Rules/Actions/Twitter/TweetAction.cs +++ b/src/Squidex.Domain.Apps.Rules/Actions/Twitter/TweetAction.cs @@ -6,10 +6,13 @@ // ========================================================================== using System.ComponentModel.DataAnnotations; +using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules; namespace Squidex.Domain.Apps.Rules.Action.Twitter { + [RuleActionHandler(typeof(TweetActionHandler))] + [RuleAction(Description = "")] public sealed class TweetAction : RuleAction { [Required] diff --git a/src/Squidex.Domain.Apps.Rules/Actions/Twitter/TweetActionHandler.cs b/src/Squidex.Domain.Apps.Rules/Actions/Twitter/TweetActionHandler.cs index e3cf80f7d..1c60d6888 100644 --- a/src/Squidex.Domain.Apps.Rules/Actions/Twitter/TweetActionHandler.cs +++ b/src/Squidex.Domain.Apps.Rules/Actions/Twitter/TweetActionHandler.cs @@ -13,43 +13,27 @@ using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; using Squidex.Infrastructure; -#pragma warning disable SA1649 // File name must match first type name - namespace Squidex.Domain.Apps.Rules.Action.Twitter { - public sealed class TweetJob - { - public string AccessToken { get; set; } - - public string AccessSecret { get; set; } - - public string Text { get; set; } - } - public sealed class TweetActionHandler : RuleActionHandler { private const string Description = "Send a tweet"; - private readonly RuleEventFormatter formatter; private readonly TwitterOptions twitterOptions; public TweetActionHandler(RuleEventFormatter formatter, IOptions twitterOptions) + : base(formatter) { - Guard.NotNull(formatter, nameof(formatter)); Guard.NotNull(twitterOptions, nameof(twitterOptions)); - this.formatter = formatter; - this.twitterOptions = twitterOptions.Value; } protected override (string Description, TweetJob Data) CreateJob(EnrichedEvent @event, TweetAction action) { - var text = formatter.Format(action.Text, @event); - var ruleJob = new TweetJob { - Text = text, + Text = Format(action.Text, @event), AccessToken = action.AccessToken, AccessSecret = action.AccessSecret }; @@ -59,22 +43,24 @@ namespace Squidex.Domain.Apps.Rules.Action.Twitter protected override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(TweetJob job) { - try - { - var tokens = Tokens.Create( - twitterOptions.ClientId, - twitterOptions.ClientSecret, - job.AccessToken, - job.AccessSecret); + var tokens = Tokens.Create( + twitterOptions.ClientId, + twitterOptions.ClientSecret, + job.AccessToken, + job.AccessSecret); - var response = await tokens.Statuses.UpdateAsync(status => job.Text); + var response = await tokens.Statuses.UpdateAsync(status => job.Text); - return ($"Tweeted: {job.Text}", null); - } - catch (Exception ex) - { - return (ex.Message, ex); - } + return ($"Tweeted: {job.Text}", null); } } + + public sealed class TweetJob + { + public string AccessToken { get; set; } + + public string AccessSecret { get; set; } + + public string Text { get; set; } + } } diff --git a/src/Squidex.Domain.Apps.Rules/Actions/WebhookAction/WebhookAction.cs b/src/Squidex.Domain.Apps.Rules/Actions/WebhookAction/WebhookAction.cs index 3e98b2434..124dbc28e 100644 --- a/src/Squidex.Domain.Apps.Rules/Actions/WebhookAction/WebhookAction.cs +++ b/src/Squidex.Domain.Apps.Rules/Actions/WebhookAction/WebhookAction.cs @@ -7,16 +7,19 @@ using System; using System.ComponentModel.DataAnnotations; +using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Rules.Action.Webhook { + [RuleActionHandler(typeof(WebhookActionHandler))] + [RuleAction(Description = "")] public sealed class WebhookAction : RuleAction { [AbsoluteUrl] [Required] - [Display(Name = "Url", Description = "he url of the webhook.")] + [Display(Name = "Url", Description = "he url to the webhook.")] public Uri Url { get; set; } [Display(Name = "Shared Secret", Description = "The shared secret that is used to calculate the signature.")] diff --git a/src/Squidex.Domain.Apps.Rules/Actions/WebhookAction/WebhookActionHandler.cs b/src/Squidex.Domain.Apps.Rules/Actions/WebhookAction/WebhookActionHandler.cs index 0fe594fe9..21a79953c 100644 --- a/src/Squidex.Domain.Apps.Rules/Actions/WebhookAction/WebhookActionHandler.cs +++ b/src/Squidex.Domain.Apps.Rules/Actions/WebhookAction/WebhookActionHandler.cs @@ -9,93 +9,65 @@ using System; using System.Net.Http; using System.Text; using System.Threading.Tasks; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules.Actions.Utils; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; using Squidex.Infrastructure; -#pragma warning disable SA1649 // File name must match first type name - namespace Squidex.Domain.Apps.Rules.Action.Webhook { - public sealed class WebhookJob - { - public string RequestUrl { get; set; } - - public string RequestSignature { get; set; } - - public string RequestBodyV2 { get; set; } - - public JObject RequestBody { get; set; } - - public string Body - { - get - { - return RequestBodyV2 ?? RequestBody.ToString(Formatting.Indented); - } - } - } - public sealed class WebhookActionHandler : RuleActionHandler { - private readonly RuleEventFormatter formatter; - private readonly ClientPool clients; + private readonly IHttpClientFactory httpClientFactory; - public WebhookActionHandler(RuleEventFormatter formatter) + public WebhookActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory) + : base(formatter) { - Guard.NotNull(formatter, nameof(formatter)); - - this.formatter = formatter; + Guard.NotNull(httpClientFactory, nameof(httpClientFactory)); - clients = new ClientPool(key => - { - var client = new HttpClient - { - Timeout = TimeSpan.FromSeconds(4) - }; - - client.DefaultRequestHeaders.Add("User-Agent", "Squidex Webhook"); - - return client; - }); + this.httpClientFactory = httpClientFactory; } protected override (string Description, WebhookJob Data) CreateJob(EnrichedEvent @event, WebhookAction action) { - var requestBody = formatter.ToEnvelope(@event).ToString(Formatting.Indented); - var requestUrl = formatter.Format(action.Url.ToString(), @event); + var requestBody = ToEnvelopeJson(@event); + var requestUrl = Format(action.Url, @event); var ruleDescription = $"Send event to webhook '{requestUrl}'"; var ruleJob = new WebhookJob { - RequestUrl = requestUrl, + RequestUrl = Format(action.Url.ToString(), @event), RequestSignature = $"{requestBody}{action.SharedSecret}".Sha256Base64(), - RequestBodyV2 = requestBody + RequestBody = requestBody }; return (ruleDescription, ruleJob); } - protected override Task<(string Dump, Exception Exception)> ExecuteJobAsync(WebhookJob job) + protected override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(WebhookJob job) { - var httpClient = clients.GetClient(string.Empty); + using (var httpClient = httpClientFactory.CreateClient()) + { + var request = new HttpRequestMessage(HttpMethod.Post, job.RequestUrl) + { + Content = new StringContent(job.RequestBody, Encoding.UTF8, "application/json") + }; + + request.Headers.Add("X-Signature", job.RequestSignature); + request.Headers.Add("X-Application", "Squidex Webhook"); + request.Headers.Add("User-Agent", "Squidex Webhook"); - return httpClient.OneWayRequestAsync(BuildRequest(job, job.Body), job.Body); + return await httpClient.OneWayRequestAsync(request, job.RequestBody); + } } + } - private static HttpRequestMessage BuildRequest(WebhookJob job, string requestBody) - { - var request = new HttpRequestMessage(HttpMethod.Post, job.RequestUrl) - { - Content = new StringContent(requestBody, Encoding.UTF8, "application/json") - }; + public sealed class WebhookJob + { + public string RequestUrl { get; set; } - request.Headers.Add("X-Signature", job.RequestSignature); + public string RequestSignature { get; set; } - return request; - } + public string RequestBody { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Rules/Squidex.Domain.Apps.Rules.csproj b/src/Squidex.Domain.Apps.Rules/Squidex.Domain.Apps.Rules.csproj index bc521b28e..1bf1f6a65 100644 --- a/src/Squidex.Domain.Apps.Rules/Squidex.Domain.Apps.Rules.csproj +++ b/src/Squidex.Domain.Apps.Rules/Squidex.Domain.Apps.Rules.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs index b6a61fd24..b82491af0 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs @@ -36,9 +36,9 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models IsRequired = true }; - foreach (var derived in RuleActionRegistry.Actions) + foreach (var derived in RuleElementRegistry.Actions) { - var derivedSchema = await context.SchemaGenerator.GenerateAsync(derived.Value, context.SchemaResolver); + var derivedSchema = await context.SchemaGenerator.GenerateAsync(derived.Value.Type, context.SchemaResolver); var oldName = context.Document.Definitions.FirstOrDefault(x => x.Value == derivedSchema).Key; diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionSerializer.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionSerializer.cs index 7e47ebeb1..7ad9d69f1 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionSerializer.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionSerializer.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Linq; using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Rules.Actions; @@ -13,7 +14,7 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models public sealed class RuleActionSerializer : JsonInheritanceConverter { public RuleActionSerializer() - : base("actionType", typeof(RuleAction), RuleActionRegistry.Actions) + : base("actionType", typeof(RuleAction), RuleElementRegistry.Actions.ToDictionary(x => x.Key, x => x.Value.Type)) { } } diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementDto.cs new file mode 100644 index 000000000..92861b8c8 --- /dev/null +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementDto.cs @@ -0,0 +1,31 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.ComponentModel.DataAnnotations; + +namespace Squidex.Areas.Api.Controllers.Rules.Models +{ + public sealed class RuleElementDto + { + /// + /// Describes the action or trigger type. + /// + [Required] + public string Description { get; set; } + + /// + /// The label for the action or trigger type. + /// + [Required] + public string Display { get; set; } + + /// + /// The optional link to the product that is integrated. + /// + public string Link { get; set; } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs b/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs index 9de9634d9..1a9c4ea01 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; @@ -15,7 +16,9 @@ using Squidex.Areas.Api.Controllers.Rules.Models; using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Rules.Commands; using Squidex.Domain.Apps.Entities.Rules.Repositories; +using Squidex.Domain.Apps.Rules.Actions; using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Reflection; using Squidex.Pipeline; namespace Squidex.Areas.Api.Controllers.Rules @@ -42,6 +45,53 @@ namespace Squidex.Areas.Api.Controllers.Rules this.ruleEventsRepository = ruleEventsRepository; } + /// + /// Get the supported rule actions. + /// + /// + /// 200 => Rule actions returned. + /// + [HttpGet] + [Route("rules/actions/")] + [ProducesResponseType(typeof(Dictionary), 200)] + [ApiCosts(0)] + public IActionResult GetActions() + { + var response = RuleElementRegistry.Actions.ToDictionary(x => x.Key, x => SimpleMapper.Map(x.Value, new RuleElementDto())); + + return Ok(response); + } + + /// + /// Get the supported rule triggers. + /// + /// + /// 200 => Rule triggers returned. + /// + [HttpGet] + [Route("rules/actions/")] + [ProducesResponseType(typeof(Dictionary), 200)] + [ApiCosts(0)] + public IActionResult GetTriggers() + { + var response = new Dictionary + { + ["ContentChanged"] = new RuleElementDto + { + Display = "Content changed", + Description = "Content changed like created, updated, published, unpublished..." + }, + + ["AssetChanged"] = new RuleElementDto + { + Display = "Asset changed", + Description = "Asset changed like created, updated, renamed..." + } + }; + + return Ok(response); + } + /// /// Get rules. /// diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs index f250e1352..ed0e6accc 100644 --- a/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs @@ -60,7 +60,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models public static Type[] Subtypes() { - var type = typeof(SchemaPropertiesDto); + var type = typeof(FieldPropertiesDto); return type.Assembly.GetTypes().Where(type.IsAssignableFrom).ToArray(); } diff --git a/src/Squidex/Config/Domain/RuleServices.cs b/src/Squidex/Config/Domain/RuleServices.cs index f889ff8af..720450e83 100644 --- a/src/Squidex/Config/Domain/RuleServices.cs +++ b/src/Squidex/Config/Domain/RuleServices.cs @@ -36,7 +36,7 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .AsSelf(); - foreach (var actionHandler in RuleActionRegistry.ActionHandlers) + foreach (var actionHandler in RuleElementRegistry.ActionHandlers) { services.AddSingleton(typeof(IRuleActionHandler), actionHandler); } diff --git a/src/Squidex/Config/Domain/SerializationServices.cs b/src/Squidex/Config/Domain/SerializationServices.cs index fef266019..9990d9197 100644 --- a/src/Squidex/Config/Domain/SerializationServices.cs +++ b/src/Squidex/Config/Domain/SerializationServices.cs @@ -39,7 +39,7 @@ namespace Squidex.Config.Domain private static void ConfigureJson(JsonSerializerSettings settings, TypeNameHandling typeNameHandling) { - RuleActionRegistry.RegisterTypes(TypeNameRegistry); + RuleElementRegistry.RegisterTypes(TypeNameRegistry); settings.SerializationBinder = new TypeNameSerializationBinder(TypeNameRegistry); diff --git a/src/Squidex/app/features/rules/pages/rules/actions/algolia-action.component.ts b/src/Squidex/app/features/rules/pages/rules/actions/algolia-action.component.ts index 0ac2c1641..5d98ac62d 100644 --- a/src/Squidex/app/features/rules/pages/rules/actions/algolia-action.component.ts +++ b/src/Squidex/app/features/rules/pages/rules/actions/algolia-action.component.ts @@ -14,6 +14,8 @@ import { FormControl, FormGroup, Validators } from '@angular/forms'; templateUrl: './algolia-action.component.html' }) export class AlgoliaActionComponent implements OnInit { + public static key = 'Algolia'; + @Input() public action: any; diff --git a/src/Squidex/app/features/rules/pages/rules/actions/azure-queue-action.component.ts b/src/Squidex/app/features/rules/pages/rules/actions/azure-queue-action.component.ts index 8df30f90e..eeb0f568c 100644 --- a/src/Squidex/app/features/rules/pages/rules/actions/azure-queue-action.component.ts +++ b/src/Squidex/app/features/rules/pages/rules/actions/azure-queue-action.component.ts @@ -16,6 +16,8 @@ import { ValidatorsEx } from '@app/shared'; templateUrl: './azure-queue-action.component.html' }) export class AzureQueueActionComponent implements OnInit { + public static key = 'AzureQueue'; + @Input() public action: any; diff --git a/src/Squidex/app/features/rules/pages/rules/actions/elastic-search-action.component.ts b/src/Squidex/app/features/rules/pages/rules/actions/elastic-search-action.component.ts index 8e6e0c8f2..7a2ce7ba9 100644 --- a/src/Squidex/app/features/rules/pages/rules/actions/elastic-search-action.component.ts +++ b/src/Squidex/app/features/rules/pages/rules/actions/elastic-search-action.component.ts @@ -14,6 +14,8 @@ import { FormControl, FormGroup, Validators } from '@angular/forms'; templateUrl: './elastic-search-action.component.html' }) export class ElasticSearchActionComponent implements OnInit { + public static key = 'ElasticSearch'; + @Input() public action: any; diff --git a/src/Squidex/app/features/rules/pages/rules/actions/fastly-action.component.ts b/src/Squidex/app/features/rules/pages/rules/actions/fastly-action.component.ts index 192216769..d66dc64b0 100644 --- a/src/Squidex/app/features/rules/pages/rules/actions/fastly-action.component.ts +++ b/src/Squidex/app/features/rules/pages/rules/actions/fastly-action.component.ts @@ -14,6 +14,8 @@ import { FormControl, FormGroup, Validators } from '@angular/forms'; templateUrl: './fastly-action.component.html' }) export class FastlyActionComponent implements OnInit { + public static key = 'Fastly'; + @Input() public action: any; diff --git a/src/Squidex/app/features/rules/pages/rules/actions/medium-action.component.ts b/src/Squidex/app/features/rules/pages/rules/actions/medium-action.component.ts index a29f274df..0975594a5 100644 --- a/src/Squidex/app/features/rules/pages/rules/actions/medium-action.component.ts +++ b/src/Squidex/app/features/rules/pages/rules/actions/medium-action.component.ts @@ -14,6 +14,8 @@ import { FormControl, FormGroup, Validators } from '@angular/forms'; templateUrl: './medium-action.component.html' }) export class MediumActionComponent implements OnInit { + public static key = 'Medium'; + @Input() public action: any; diff --git a/src/Squidex/app/features/rules/pages/rules/actions/slack-action.component.ts b/src/Squidex/app/features/rules/pages/rules/actions/slack-action.component.ts index 9a425591b..c58e1b291 100644 --- a/src/Squidex/app/features/rules/pages/rules/actions/slack-action.component.ts +++ b/src/Squidex/app/features/rules/pages/rules/actions/slack-action.component.ts @@ -14,6 +14,8 @@ import { FormControl, FormGroup, Validators } from '@angular/forms'; templateUrl: './slack-action.component.html' }) export class SlackActionComponent implements OnInit { + public static key = 'Slack'; + @Input() public action: any; diff --git a/src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.ts b/src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.ts index 84b8b802c..e14dcc771 100644 --- a/src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.ts +++ b/src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.ts @@ -17,6 +17,8 @@ import { DialogService } from '@app/shared'; templateUrl: './tweet-action.component.html' }) export class TweetActionComponent implements OnInit { + public static key = 'Tweet'; + private request: any; @Input() diff --git a/src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.ts b/src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.ts index 767ed1442..190eb249f 100644 --- a/src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.ts +++ b/src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.ts @@ -14,6 +14,8 @@ import { FormControl, FormGroup, Validators } from '@angular/forms'; templateUrl: './webhook-action.component.html' }) export class WebhookActionComponent implements OnInit { + public static key = 'Webhook'; + @Input() public action: any; diff --git a/src/Squidex/app/features/rules/pages/rules/rule-action.container.ts b/src/Squidex/app/features/rules/pages/rules/rule-action.container.ts new file mode 100644 index 000000000..dfcb28558 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/rule-action.container.ts @@ -0,0 +1,40 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { ComponentFactoryResolver, ComponentRef, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +export class RuleActionContainer implements OnInit { + @Input() + public actionType: string; + + @Input() + public action: any; + + @Input() + public actionForm: FormGroup; + + @Input() + public actionFormSubmitted = false; + + @ViewChild('container', { read: ViewContainerRef }) + public entry: ViewContainerRef; + + private component: ComponentRef; + + constructor( + private readonly componentFactoryResolver: ComponentFactoryResolver + ) { + } + + public ngOnInit() { + const factories = Array.from(this.componentFactoryResolver['_factories'].values()); + const factory: any = factories.find((x: any) => x.selector === this.actionType); + + this.component = this.entry.createComponent(factory); + } +} \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.ts b/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.ts index 891232a55..52ba5ad1f 100644 --- a/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.ts +++ b/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.ts @@ -12,10 +12,9 @@ import { CreateRuleDto, Form, ImmutableArray, - ruleActions, RuleDto, + RuleElementDto, RulesState, - ruleTriggers, SchemaDto } from '@app/shared'; @@ -29,9 +28,6 @@ export const MODE_EDIT_ACTION = 'EditAction'; templateUrl: './rule-wizard.component.html' }) export class RuleWizardComponent implements OnInit { - public ruleActions = ruleActions; - public ruleTriggers = ruleTriggers; - public actionForm = new Form(new FormGroup({})); public actionType: string; public action: any = {}; @@ -45,6 +41,12 @@ export class RuleWizardComponent implements OnInit { @Output() public completed = new EventEmitter(); + @Input() + public ruleActions: { [name: string]: RuleElementDto }; + + @Input() + public ruleTriggers: { [name: string]: RuleElementDto }; + @Input() public schemas: ImmutableArray; diff --git a/src/Squidex/app/features/rules/pages/rules/rules-page.component.html b/src/Squidex/app/features/rules/pages/rules/rules-page.component.html index b6c112f2c..c873338e7 100644 --- a/src/Squidex/app/features/rules/pages/rules/rules-page.component.html +++ b/src/Squidex/app/features/rules/pages/rules/rules-page.component.html @@ -20,58 +20,68 @@ -
- No Rule created yet. + +
+ No Rule created yet. - -
+ +
- - - - - + + + + + +
-

If

-
- - - + + + + + + + - - - - - - - -
+

If

+
+ + + + + + {{ruleTriggers[rule.triggerType].display}} + - - {{ruleTriggers[rule.triggerType].name}} + +

then

+
+ + + + + + {{ruleActions[rule.actionType].display}} + - - -

then

-
- - - - - - {{ruleActions[rule.actionType].name}} - - - - - - -
+
+ + + +
+ + + + + +
@@ -90,10 +100,4 @@ - - - - - \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/rules-page.component.ts b/src/Squidex/app/features/rules/pages/rules/rules-page.component.ts index 0e6f54017..d2798e192 100644 --- a/src/Squidex/app/features/rules/pages/rules/rules-page.component.ts +++ b/src/Squidex/app/features/rules/pages/rules/rules-page.component.ts @@ -11,10 +11,10 @@ import { onErrorResumeNext } from 'rxjs/operators'; import { AppsState, DialogModel, - ruleActions, RuleDto, + RuleElementDto, + RulesService, RulesState, - ruleTriggers, SchemasState } from '@app/shared'; @@ -24,25 +24,37 @@ import { templateUrl: './rules-page.component.html' }) export class RulesPageComponent implements OnInit { - public ruleActions = ruleActions; - public ruleTriggers = ruleTriggers; - public addRuleDialog = new DialogModel(); public wizardMode = 'Wizard'; public wizardRule: RuleDto | null; + public ruleActions: { [name: string]: RuleElementDto }; + public ruleTriggers: { [name: string]: RuleElementDto }; + constructor( public readonly appsState: AppsState, public readonly rulesState: RulesState, + public readonly rulesService: RulesService, public readonly schemasState: SchemasState ) { } public ngOnInit() { - this.schemasState.load().pipe(onErrorResumeNext()).subscribe(); this.rulesState.load().pipe(onErrorResumeNext()).subscribe(); + + this.rulesService.getActions() + .subscribe(actions => { + this.ruleActions = actions; + }); + + this.rulesService.getTriggers() + .subscribe(triggers => { + this.ruleTriggers = triggers; + }); + + this.schemasState.load().pipe(onErrorResumeNext()).subscribe(); } public reload() { diff --git a/src/Squidex/app/shared/services/rules.service.ts b/src/Squidex/app/shared/services/rules.service.ts index 219c56137..298a08219 100644 --- a/src/Squidex/app/shared/services/rules.service.ts +++ b/src/Squidex/app/shared/services/rules.service.ts @@ -21,41 +21,14 @@ import { Versioned } from '@app/framework'; -export const ruleTriggers: any = { - 'AssetChanged': { - name: 'Asset changed' - }, - 'ContentChanged': { - name: 'Content changed' - } -}; - -export const ruleActions: any = { - 'Algolia': { - name: 'Populate Algolia Index' - }, - 'AzureQueue': { - name: 'Send to Azure Queue' - }, - 'ElasticSearch': { - name: 'Populate ElasticSearch Index' - }, - 'Fastly': { - name: 'Purge fastly Cache' - }, - 'Medium': { - name: 'Post to Medium' - }, - 'Slack': { - name: 'Send to Slack' - }, - 'Tweet': { - name: 'Tweet' - }, - 'Webhook': { - name: 'Send Webhook' +export class RuleElementDto { + constructor( + public readonly display: string, + public readonly description: string, + public readonly link: string + ) { } -}; +} export class RuleDto extends Model { constructor( @@ -129,6 +102,46 @@ export class RulesService { ) { } + public getActions(): Observable<{ [name: string]: RuleElementDto }> { + return HTTP.getVersioned(this.http, 'rules/action').pipe( + map(response => { + const items: { [name: string]: any } = response.payload.body; + + const result: { [name: string]: RuleElementDto } = {}; + + for (let key in items) { + if (items.hasOwnProperty(key)) { + const value = items[key]; + + result[key] = new RuleElementDto(value.display, value.description, value.link); + } + } + + return result; + }), + pretifyError('Failed to load Rules. Please reload.')); + } + + public getTriggers(): Observable<{ [name: string]: RuleElementDto }> { + return HTTP.getVersioned(this.http, 'rules/triggers').pipe( + map(response => { + const items: { [name: string]: any } = response.payload.body; + + const result: { [name: string]: RuleElementDto } = {}; + + for (let key in items) { + if (items.hasOwnProperty(key)) { + const value = items[key]; + + result[key] = new RuleElementDto(value.display, value.description, value.link); + } + } + + return result; + }), + pretifyError('Failed to load Rules. Please reload.')); + } + public getRules(appName: string): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules`);