From bfc4c9c5ccd57c3f78ed91b9bd34c85a8ad159ff Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Thu, 26 Oct 2017 08:59:35 +0200 Subject: [PATCH] Started with rules. --- .../Rules/Actions/WebhookAction.cs | 26 +++ .../Rules/IRuleActionVisitor.cs | 17 ++ .../Rules/IRuleTriggerVisitor.cs | 17 ++ .../Rules/Rule.cs | 78 +++++++++ .../Rules/RuleAction.cs | 15 ++ .../Rules/RuleJob.cs | 32 ++++ .../Rules/RuleJobData.cs | 17 ++ .../Rules/RuleTrigger.cs | 15 ++ .../Rules/Triggers/ContentChangedTrigger.cs | 24 +++ .../Triggers/ContentChangedTriggerSchema.cs | 25 +++ .../Actions/WebhookActionHandler.cs | 115 +++++++++++++ .../HandleRules/IRuleActionHandler.cs | 25 +++ .../HandleRules/IRuleTriggerHandler.cs | 22 +++ .../HandleRules/RuleActionHandler.cs | 33 ++++ .../HandleRules/RuleResult.cs | 18 ++ .../HandleRules/RuleService.cs | 159 ++++++++++++++++++ .../HandleRules/RuleTriggerHandler.cs | 30 ++++ .../Triggers/ContentChangedTriggerHandler.cs | 49 ++++++ ...Squidex.Domain.Apps.Core.Operations.csproj | 1 + .../Rules/RuleCreated.cs | 21 +++ .../Rules/RuleDeleted.cs | 17 ++ .../Rules/RuleDisabled.cs | 17 ++ .../Rules/RuleEnabled.cs | 17 ++ .../Rules/RuleEvent.cs | 17 ++ .../Rules/RuleUpdated.cs | 21 +++ .../Rules/Utils/RuleEventDispatcher.cs | 80 +++++++++ .../Rules/IRuleEntity.cs | 17 ++ .../Rules/IRuleEventEntity.cs | 28 +++ .../Repositories/IRuleEventRepository.cs | 37 ++++ .../Rules/Repositories/IRuleRepository.cs | 21 +++ .../Rules/RuleDequeuer.cs | 148 ++++++++++++++++ .../Rules/RuleEnqueuer.cs | 74 ++++++++ .../Rules/RuleJobResult.cs | 18 ++ 33 files changed, 1251 insertions(+) create mode 100644 src/Squidex.Domain.Apps.Core.Model/Rules/Actions/WebhookAction.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Rules/IRuleActionVisitor.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Rules/RuleJobData.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTrigger.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchema.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleActionHandler.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleResult.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs create mode 100644 src/Squidex.Domain.Apps.Events/Rules/RuleCreated.cs create mode 100644 src/Squidex.Domain.Apps.Events/Rules/RuleDeleted.cs create mode 100644 src/Squidex.Domain.Apps.Events/Rules/RuleDisabled.cs create mode 100644 src/Squidex.Domain.Apps.Events/Rules/RuleEnabled.cs create mode 100644 src/Squidex.Domain.Apps.Events/Rules/RuleEvent.cs create mode 100644 src/Squidex.Domain.Apps.Events/Rules/RuleUpdated.cs create mode 100644 src/Squidex.Domain.Apps.Events/Rules/Utils/RuleEventDispatcher.cs create mode 100644 src/Squidex.Domain.Apps.Read/Rules/IRuleEntity.cs create mode 100644 src/Squidex.Domain.Apps.Read/Rules/IRuleEventEntity.cs create mode 100644 src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleEventRepository.cs create mode 100644 src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleRepository.cs create mode 100644 src/Squidex.Domain.Apps.Read/Rules/RuleDequeuer.cs create mode 100644 src/Squidex.Domain.Apps.Read/Rules/RuleEnqueuer.cs create mode 100644 src/Squidex.Domain.Apps.Read/Rules/RuleJobResult.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/WebhookAction.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/WebhookAction.cs new file mode 100644 index 000000000..09718ad8f --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/WebhookAction.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// WebhookAction.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Rules.Actions +{ + [TypeName(nameof(WebhookAction))] + public sealed class WebhookAction : RuleAction + { + public Uri Url { get; set; } + + public string SharedSecret { get; set; } + + public override T Accept(IRuleActionVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleActionVisitor.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleActionVisitor.cs new file mode 100644 index 000000000..8c999b7ae --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleActionVisitor.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// IActionVisitor.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Rules.Actions; + +namespace Squidex.Domain.Apps.Core.Rules +{ + public interface IRuleActionVisitor + { + T Visit(WebhookAction action); + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs new file mode 100644 index 000000000..b91c7e604 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// IRuleTriggerVisitor.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Rules.Triggers; + +namespace Squidex.Domain.Apps.Core.Rules +{ + public interface IRuleTriggerVisitor + { + T Visit(ContentChangedTrigger trigger); + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs new file mode 100644 index 000000000..877ab8885 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs @@ -0,0 +1,78 @@ +// ========================================================================== +// Rule.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Rules +{ + public sealed class Rule + { + private RuleTrigger trigger; + private RuleAction action; + private bool isEnabled = true; + + public RuleTrigger Trigger + { + get { return trigger; } + } + + public RuleAction Action + { + get { return action; } + } + + public bool IsEnabled + { + get { return isEnabled; } + } + + public Rule(RuleTrigger trigger, RuleAction action) + { + Guard.NotNull(trigger, nameof(trigger)); + Guard.NotNull(action, nameof(action)); + + this.trigger = trigger; + this.action = action; + } + + public void Enable() + { + this.isEnabled = true; + } + + public void Disable() + { + this.isEnabled = false; + } + + public void Update(RuleTrigger newTrigger) + { + Guard.NotNull(newTrigger, nameof(newTrigger)); + + if (newTrigger.GetType() != trigger.GetType()) + { + throw new ArgumentException("New trigger has another type.", nameof(newTrigger)); + } + + trigger = newTrigger; + } + + public void Update(RuleAction newAction) + { + Guard.NotNull(newAction, nameof(newAction)); + + if (newAction.GetType() != trigger.GetType()) + { + throw new ArgumentException("New action has another type.", nameof(newAction)); + } + + action = newAction; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs new file mode 100644 index 000000000..483abdff9 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs @@ -0,0 +1,15 @@ +// ========================================================================== +// RuleAction.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Rules +{ + public abstract class RuleAction + { + public abstract T Accept(IRuleActionVisitor visitor); + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs new file mode 100644 index 000000000..5e2d877a9 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs @@ -0,0 +1,32 @@ +// ========================================================================== +// RuleJob.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using NodaTime; + +namespace Squidex.Domain.Apps.Core.Rules +{ + public sealed class RuleJob + { + public Guid Id { get; set; } + + public Guid AppId { get; set; } + + public string EventName { get; set; } + + public string ActionName { get; set; } + + public string Description { get; set; } + + public Instant Created { get; set; } + + public Instant Expires { get; set; } + + public RuleJobData ActionData { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJobData.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJobData.cs new file mode 100644 index 000000000..ad9cef6c1 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJobData.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// RuleJobData.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using Newtonsoft.Json.Linq; + +namespace Squidex.Domain.Apps.Core.Rules +{ + public sealed class RuleJobData : Dictionary + { + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs new file mode 100644 index 000000000..15284960c --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs @@ -0,0 +1,15 @@ +// ========================================================================== +// RuleTrigger.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Rules +{ + public abstract class RuleTrigger + { + public abstract T Accept(IRuleTriggerVisitor visitor); + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTrigger.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTrigger.cs new file mode 100644 index 000000000..cc11d969c --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTrigger.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// ContentChangedTrigger.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Rules.Triggers +{ + [TypeName(nameof(ContentChangedTrigger))] + public sealed class ContentChangedTrigger : RuleTrigger + { + public List Schemas { get; set; } + + public override T Accept(IRuleTriggerVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchema.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchema.cs new file mode 100644 index 000000000..600fcaf6e --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchema.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// ContentChangedTriggerSchema.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; + +namespace Squidex.Domain.Apps.Core.Rules.Triggers +{ + public sealed class ContentChangedTriggerSchema + { + public Guid SchemaId { get; set; } + + public bool SendCreate { get; set; } + + public bool SendUpdate { get; set; } + + public bool SendDelete { get; set; } + + public bool SendPublish { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs new file mode 100644 index 000000000..aae33d8ca --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs @@ -0,0 +1,115 @@ +// ========================================================================== +// WebhookActionHandler.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Actions; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Events; +using Squidex.Infrastructure.Http; + +namespace Squidex.Domain.Apps.Core.HandleRules.ActionHandlers +{ + public sealed class WebhookActionHandler : RuleActionHandler + { + private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(2); + + private readonly JsonSerializer serializer; + + public WebhookActionHandler(JsonSerializer serializer) + { + Guard.NotNull(serializer, nameof(serializer)); + + this.serializer = serializer; + } + + protected override (string Description, RuleJobData Data) CreateJob(Envelope @event, string eventName, WebhookAction action) + { + var body = CreatePayload(@event, eventName); + + var signature = $"{body.ToString(Formatting.Indented)}{action.SharedSecret}".Sha256Base64(); + + var ruleDescription = $"Send event to webhook {action.Url}"; + var ruleData = new RuleJobData + { + ["RequestUrl"] = action.Url, + ["RequestBody"] = body, + ["RequestSignature"] = signature + }; + + return (ruleDescription, ruleData); + } + + private JObject CreatePayload(Envelope @event, string eventName) + { + return new JObject( + new JProperty("type", eventName), + new JProperty("payload", JObject.FromObject(@event.Payload, serializer)), + new JProperty("timestamp", @event.Headers.Timestamp().ToString())); + } + + public override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(RuleJobData job) + { + var requestBody = job["RequestBody"].ToString(Formatting.Indented); + var request = BuildRequest(job, requestBody); + + HttpResponseMessage response = null; + + try + { + using (var client = new HttpClient { Timeout = Timeout }) + { + response = await client.SendAsync(request); + + var responseString = await response.Content.ReadAsStringAsync(); + var requestDump = DumpFormatter.BuildDump(request, response, requestBody, responseString, TimeSpan.Zero, false); + + return (requestDump, null); + } + } + catch (Exception ex) + { + if (request != null) + { + var requestDump = DumpFormatter.BuildDump(request, response, requestBody, ex.ToString(), TimeSpan.Zero, false); + + return (requestDump, ex); + } + else + { + var requestDump = ex.ToString(); + + return (requestDump, ex); + } + } + } + + private static HttpRequestMessage BuildRequest(Dictionary job, string requestBody) + { + var requestUrl = job["RequestUrl"].ToString(); + var requestSignature = job["RequestSignature"].ToString(); + + var request = new HttpRequestMessage(HttpMethod.Post, requestUrl) + { + Content = new StringContent(requestBody, Encoding.UTF8, "application/json") + }; + + request.Headers.Add("X-Signature", requestSignature); + request.Headers.Add("User-Agent", "Squidex Webhook"); + + return request; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleActionHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleActionHandler.cs new file mode 100644 index 000000000..e2078e988 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleActionHandler.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// IRuleActionHandler.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure.CQRS.Events; + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public interface IRuleActionHandler + { + Type ActionType { get; } + + (string Description, RuleJobData Data) CreateJob(Envelope @event, string eventName, RuleAction action); + + Task<(string Dump, Exception Exception)> ExecuteJobAsync(RuleJobData data); + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs new file mode 100644 index 000000000..a1507bc5a --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// IRuleTriggerHandler.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure.CQRS.Events; + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public interface IRuleTriggerHandler + { + Type TriggerType { get; } + + bool Triggers(Envelope @event, RuleTrigger trigger); + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs new file mode 100644 index 000000000..a97957bdc --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs @@ -0,0 +1,33 @@ +// ========================================================================== +// RuleActionHandler.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure.CQRS.Events; + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public abstract class RuleActionHandler : IRuleActionHandler where T : RuleAction + { + Type IRuleActionHandler.ActionType + { + get { return typeof(T); } + } + + (string Description, RuleJobData Data) IRuleActionHandler.CreateJob(Envelope @event, string eventName, RuleAction action) + { + return CreateJob(@event, eventName, (T)action); + } + + protected abstract (string Description, RuleJobData Data) CreateJob(Envelope @event, string eventName, T action); + + public abstract Task<(string Dump, Exception Exception)> ExecuteJobAsync(RuleJobData job); + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleResult.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleResult.cs new file mode 100644 index 000000000..76316497a --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleResult.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// RuleResult.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public enum RuleResult + { + Pending, + Success, + Failed, + Timeout + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs new file mode 100644 index 000000000..632f05d72 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs @@ -0,0 +1,159 @@ +// ========================================================================== +// RuleService.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using NodaTime; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Events; + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public sealed class RuleService + { + private const string ContentPrefix = "Content"; + private static readonly Duration ExpirationTime = Duration.FromDays(2); + private readonly Dictionary ruleActionHandlers; + private readonly Dictionary ruleTriggerHandlers; + private readonly TypeNameRegistry typeNameRegistry; + private readonly IClock clock; + + public RuleService( + IEnumerable ruleTriggerHandlers, + IEnumerable ruleActionHandlers, + IClock clock, + TypeNameRegistry typeNameRegistry) + { + Guard.NotNull(ruleTriggerHandlers, nameof(ruleTriggerHandlers)); + Guard.NotNull(ruleActionHandlers, nameof(ruleActionHandlers)); + Guard.NotNull(typeNameRegistry, nameof(typeNameRegistry)); + Guard.NotNull(clock, nameof(clock)); + + this.typeNameRegistry = typeNameRegistry; + + this.ruleTriggerHandlers = ruleTriggerHandlers.ToDictionary(x => x.TriggerType); + this.ruleActionHandlers = ruleActionHandlers.ToDictionary(x => x.ActionType); + + this.clock = clock; + } + + public RuleJob CreateJob(Rule rule, Envelope @event) + { + Guard.NotNull(rule, nameof(rule)); + Guard.NotNull(@event, nameof(@event)); + + if (!(@event.Payload is AppEvent appEvent)) + { + return null; + } + + var actionType = rule.Action.GetType(); + + if (!ruleTriggerHandlers.TryGetValue(rule.Trigger.GetType(), out var triggerHandler)) + { + return null; + } + + if (!ruleActionHandlers.TryGetValue(actionType, out var actionHandler)) + { + return null; + } + + var appEventEnvelope = @event.To(); + + if (!triggerHandler.Triggers(appEventEnvelope, rule.Trigger)) + { + return null; + } + + var eventName = CreateEventName(appEvent); + + var now = clock.GetCurrentInstant(); + + var actionName = typeNameRegistry.GetName(actionType); + var actionData = actionHandler.CreateJob(appEventEnvelope, eventName, rule.Action); + + var job = new RuleJob + { + Id = Guid.NewGuid(), + ActionName = actionName, + ActionData = actionData.Data, + AppId = appEvent.AppId.Id, + Created = now, + EventName = eventName, + Expires = now.Plus(ExpirationTime), + Description = actionData.Description + }; + + return job; + } + + public async Task<(string Dump, RuleResult Result, TimeSpan Elapsed)> InvokeAsync(string actionName, Dictionary job) + { + try + { + var actionType = typeNameRegistry.GetType(actionName); + var actionWatch = Stopwatch.StartNew(); + + var result = await ruleActionHandlers[actionType].ExecuteJobAsync(job); + + actionWatch.Stop(); + + var dumpBuilder = new StringBuilder(result.Dump); + + dumpBuilder.AppendLine(); + dumpBuilder.AppendFormat("Elapesed {0}.", actionWatch.Elapsed); + dumpBuilder.AppendLine(); + + if (result.Exception is TimeoutException || result.Exception is OperationCanceledException) + { + dumpBuilder.AppendLine(); + dumpBuilder.AppendLine("Action timed out."); + + return (dumpBuilder.ToString(), RuleResult.Timeout, actionWatch.Elapsed); + } + else if (result.Exception != null) + { + return (dumpBuilder.ToString(), RuleResult.Failed, actionWatch.Elapsed); + } + else + { + return (dumpBuilder.ToString(), RuleResult.Success, actionWatch.Elapsed); + } + } + catch (Exception ex) + { + return (ex.ToString(), RuleResult.Failed, TimeSpan.Zero); + } + } + + private string CreateEventName(AppEvent appEvent) + { + var eventName = typeNameRegistry.GetName(appEvent.GetType()); + + if (appEvent is SchemaEvent schemaEvent) + { + if (eventName.StartsWith(ContentPrefix, StringComparison.Ordinal)) + { + eventName = eventName.Substring(ContentPrefix.Length); + } + + return $"{schemaEvent.SchemaId.Name.ToPascalCase()}{eventName}"; + } + + return eventName; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs new file mode 100644 index 000000000..131bf9ac1 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// RuleTriggerHandler.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure.CQRS.Events; + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public abstract class RuleTriggerHandler : IRuleTriggerHandler where T : RuleTrigger + { + public Type TriggerType + { + get { return typeof(T); } + } + + bool IRuleTriggerHandler.Triggers(Envelope @event, RuleTrigger trigger) + { + return Triggers(@event, (T)trigger); + } + + protected abstract bool Triggers(Envelope @event, T trigger); + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs new file mode 100644 index 000000000..5a3c542d4 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs @@ -0,0 +1,49 @@ +// ========================================================================== +// ContentChangedTriggerHandler.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure.CQRS.Events; + +namespace Squidex.Domain.Apps.Core.HandleRules.Triggers +{ + public sealed class ContentChangedTriggerHandler : RuleTriggerHandler + { + protected override bool Triggers(Envelope @event, ContentChangedTrigger trigger) + { + if (trigger.Schemas != null && @event.Payload is SchemaEvent schemaEvent) + { + foreach (var schema in trigger.Schemas) + { + if (MatchsSchema(schema, schemaEvent) && MatchsType(schema, schemaEvent)) + { + return true; + } + } + } + + return false; + } + + private static bool MatchsSchema(ContentChangedTriggerSchema webhookSchema, SchemaEvent @event) + { + return @event.SchemaId.Id == webhookSchema.SchemaId; + } + + private static bool MatchsType(ContentChangedTriggerSchema webhookSchema, SchemaEvent @event) + { + return + (webhookSchema.SendCreate && @event is ContentCreated) || + (webhookSchema.SendUpdate && @event is ContentUpdated) || + (webhookSchema.SendDelete && @event is ContentDeleted) || + (webhookSchema.SendPublish && @event is ContentStatusChanged statusChanged && statusChanged.Status == Status.Published); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj b/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj index ec9a21f4c..e5d74893a 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj +++ b/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj @@ -9,6 +9,7 @@ + diff --git a/src/Squidex.Domain.Apps.Events/Rules/RuleCreated.cs b/src/Squidex.Domain.Apps.Events/Rules/RuleCreated.cs new file mode 100644 index 000000000..9128687ff --- /dev/null +++ b/src/Squidex.Domain.Apps.Events/Rules/RuleCreated.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// RuleCreated.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Infrastructure.CQRS.Events; + +namespace Squidex.Domain.Apps.Events.Rules +{ + [EventType(nameof(RuleCreated))] + public sealed class RuleCreated : RuleEvent + { + public RuleTrigger Trigger { get; set; } + + public RuleAction Action { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Events/Rules/RuleDeleted.cs b/src/Squidex.Domain.Apps.Events/Rules/RuleDeleted.cs new file mode 100644 index 000000000..696acb36b --- /dev/null +++ b/src/Squidex.Domain.Apps.Events/Rules/RuleDeleted.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// RuleDeleted.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Infrastructure.CQRS.Events; + +namespace Squidex.Domain.Apps.Events.Rules +{ + [EventType(nameof(RuleDeleted))] + public sealed class RuleDeleted : RuleEvent + { + } +} diff --git a/src/Squidex.Domain.Apps.Events/Rules/RuleDisabled.cs b/src/Squidex.Domain.Apps.Events/Rules/RuleDisabled.cs new file mode 100644 index 000000000..265d716bc --- /dev/null +++ b/src/Squidex.Domain.Apps.Events/Rules/RuleDisabled.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// RuleDisabled.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Infrastructure.CQRS.Events; + +namespace Squidex.Domain.Apps.Events.Rules +{ + [EventType(nameof(RuleDisabled))] + public sealed class RuleDisabled : RuleEvent + { + } +} diff --git a/src/Squidex.Domain.Apps.Events/Rules/RuleEnabled.cs b/src/Squidex.Domain.Apps.Events/Rules/RuleEnabled.cs new file mode 100644 index 000000000..f94d1f58f --- /dev/null +++ b/src/Squidex.Domain.Apps.Events/Rules/RuleEnabled.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// RuleEnabled.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Infrastructure.CQRS.Events; + +namespace Squidex.Domain.Apps.Events.Rules +{ + [EventType(nameof(RuleEnabled))] + public sealed class RuleEnabled : RuleEvent + { + } +} diff --git a/src/Squidex.Domain.Apps.Events/Rules/RuleEvent.cs b/src/Squidex.Domain.Apps.Events/Rules/RuleEvent.cs new file mode 100644 index 000000000..d0ee5604c --- /dev/null +++ b/src/Squidex.Domain.Apps.Events/Rules/RuleEvent.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// RuleEvent.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; + +namespace Squidex.Domain.Apps.Events.Rules +{ + public abstract class RuleEvent : AppEvent + { + public Guid RuleId { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Events/Rules/RuleUpdated.cs b/src/Squidex.Domain.Apps.Events/Rules/RuleUpdated.cs new file mode 100644 index 000000000..e4d828138 --- /dev/null +++ b/src/Squidex.Domain.Apps.Events/Rules/RuleUpdated.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// RuleUpdated.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Infrastructure.CQRS.Events; + +namespace Squidex.Domain.Apps.Events.Rules +{ + [EventType(nameof(RuleUpdated))] + public sealed class RuleUpdated : RuleEvent + { + public RuleTrigger Trigger { get; set; } + + public RuleAction Action { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Events/Rules/Utils/RuleEventDispatcher.cs b/src/Squidex.Domain.Apps.Events/Rules/Utils/RuleEventDispatcher.cs new file mode 100644 index 000000000..45b03161d --- /dev/null +++ b/src/Squidex.Domain.Apps.Events/Rules/Utils/RuleEventDispatcher.cs @@ -0,0 +1,80 @@ +// ========================================================================== +// RuleEventDispatcher.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Linq; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Actions; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Events.Webhooks; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Events.Rules.Utils +{ + public static class RuleEventDispatcher + { + public static Rule Create(RuleCreated @event) + { + return new Rule(@event.Trigger, @event.Action); + } + + public static Rule Create(WebhookCreated @event) + { + return new Rule(CreateTrigger(@event), CreateAction(@event)); + } + + public static void Apply(this Rule rule, WebhookUpdated @event) + { + rule.Update(CreateTrigger(@event)); + + if (rule.Action is WebhookAction webhookAction) + { + webhookAction.Url = @event.Url; + } + } + + public static void Apply(this Rule rule, RuleUpdated @event) + { + if (@event.Trigger != null) + { + rule.Update(@event.Trigger); + } + + if (@event.Action != null) + { + rule.Update(@event.Action); + } + } + + public static void Apply(this Rule rule, RuleEnabled @event) + { + rule.Enable(); + } + + public static void Apply(this Rule rule, RuleDisabled @event) + { + rule.Disable(); + } + + private static WebhookAction CreateAction(WebhookCreated @event) + { + var action = new WebhookAction { Url = @event.Url, SharedSecret = @event.SharedSecret }; + + return action; + } + + private static ContentChangedTrigger CreateTrigger(WebhookEditEvent @event) + { + var trigger = new ContentChangedTrigger + { + Schemas = @event.Schemas.Select(x => SimpleMapper.Map(x, new ContentChangedTriggerSchema())).ToList() + }; + + return trigger; + } + } +} diff --git a/src/Squidex.Domain.Apps.Read/Rules/IRuleEntity.cs b/src/Squidex.Domain.Apps.Read/Rules/IRuleEntity.cs new file mode 100644 index 000000000..6de2db9dd --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/Rules/IRuleEntity.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// IRuleEntity.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Rules; + +namespace Squidex.Domain.Apps.Read.Rules +{ + public interface IRuleEntity : IAppRefEntity, IEntityWithCreatedBy, IEntityWithLastModifiedBy, IEntityWithVersion + { + Rule Rule { get; } + } +} diff --git a/src/Squidex.Domain.Apps.Read/Rules/IRuleEventEntity.cs b/src/Squidex.Domain.Apps.Read/Rules/IRuleEventEntity.cs new file mode 100644 index 000000000..43dea2b55 --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/Rules/IRuleEventEntity.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// IRuleEventEntity.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using NodaTime; +using Squidex.Domain.Apps.Core.HandleRules; + +namespace Squidex.Domain.Apps.Read.Rules +{ + public interface IRuleEventEntity : IEntity + { + RuleJob Job { get; } + + Instant? NextAttempt { get; } + + RuleJobResult JobResult { get; } + + RuleResult Result { get; } + + int NumCalls { get; } + + string LastDump { get; } + } +} diff --git a/src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleEventRepository.cs b/src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleEventRepository.cs new file mode 100644 index 000000000..ded48c233 --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleEventRepository.cs @@ -0,0 +1,37 @@ +// ========================================================================== +// IRuleEventRepository.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using NodaTime; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; + +namespace Squidex.Domain.Apps.Read.Rules.Repositories +{ + public interface IRuleEventRepository + { + Task EnqueueAsync(RuleJob job, Instant nextAttempt); + + Task EnqueueAsync(Guid id, Instant nextAttempt); + + Task MarkSendingAsync(Guid jobId); + + Task TraceSentAsync(Guid jobId, string dump, RuleResult result, TimeSpan elapsed, Instant? nextCall); + + Task QueryPendingAsync(Func callback, CancellationToken cancellationToken = default(CancellationToken)); + + Task CountByAppAsync(Guid appId); + + Task> QueryByAppAsync(Guid appId, int skip = 0, int take = 20); + + Task FindAsync(Guid id); + } +} diff --git a/src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleRepository.cs b/src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleRepository.cs new file mode 100644 index 000000000..5f24b1a4e --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleRepository.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// IRuleRepository.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Squidex.Domain.Apps.Read.Rules.Repositories +{ + public interface IRuleRepository + { + Task> QueryByAppAsync(Guid appId); + + Task> QueryCachedByAppAsync(Guid appId); + } +} diff --git a/src/Squidex.Domain.Apps.Read/Rules/RuleDequeuer.cs b/src/Squidex.Domain.Apps.Read/Rules/RuleDequeuer.cs new file mode 100644 index 000000000..bcaa3d718 --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/Rules/RuleDequeuer.cs @@ -0,0 +1,148 @@ +// ========================================================================== +// RuleDequeuer.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Threading.Tasks.Dataflow; +using NodaTime; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Read.Rules.Repositories; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Timers; + +namespace Squidex.Domain.Apps.Read.Rules +{ + public sealed class RuleDequeuer : DisposableObjectBase, IExternalSystem + { + private readonly ActionBlock requestBlock; + private readonly TransformBlock blockBlock; + private readonly IRuleEventRepository ruleEventRepository; + private readonly RuleService ruleService; + private readonly CompletionTimer timer; + private readonly ISemanticLog log; + + public RuleDequeuer(RuleService ruleService, IRuleEventRepository ruleEventRepository, ISemanticLog log) + { + Guard.NotNull(ruleEventRepository, nameof(ruleEventRepository)); + Guard.NotNull(ruleService, nameof(ruleService)); + Guard.NotNull(log, nameof(log)); + + this.ruleEventRepository = ruleEventRepository; + this.ruleService = ruleService; + + this.log = log; + + requestBlock = + new ActionBlock(MakeRequestAsync, + new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 32, BoundedCapacity = 32 }); + + blockBlock = + new TransformBlock(x => BlockAsync(x), + new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 1, BoundedCapacity = 1 }); + + blockBlock.LinkTo(requestBlock, new DataflowLinkOptions { PropagateCompletion = true }); + + timer = new CompletionTimer(5000, QueryAsync); + } + + protected override void DisposeObject(bool disposing) + { + if (disposing) + { + timer.StopAsync().Wait(); + + blockBlock.Complete(); + requestBlock.Completion.Wait(); + } + } + + public void Connect() + { + } + + public void Next() + { + timer.SkipCurrentDelay(); + } + + private async Task QueryAsync(CancellationToken cancellationToken) + { + try + { + await ruleEventRepository.QueryPendingAsync(blockBlock.SendAsync, cancellationToken); + } + catch (Exception ex) + { + log.LogError(ex, w => w + .WriteProperty("action", "QueueWebhookEvents") + .WriteProperty("status", "Failed")); + } + } + + private async Task BlockAsync(IRuleEventEntity @event) + { + try + { + await ruleEventRepository.MarkSendingAsync(@event.Id); + + return @event; + } + catch (Exception ex) + { + log.LogError(ex, w => w + .WriteProperty("action", "BlockWebhookEvent") + .WriteProperty("status", "Failed")); + + throw; + } + } + + private async Task MakeRequestAsync(IRuleEventEntity @event) + { + try + { + var job = @event.Job; + + var response = await ruleService.InvokeAsync(job.ActionName, job.Details); + + Instant? nextCall = null; + + if (response.Result != RuleResult.Success) + { + switch (@event.NumCalls) + { + case 0: + nextCall = job.Created.Plus(Duration.FromMinutes(5)); + break; + case 1: + nextCall = job.Created.Plus(Duration.FromHours(1)); + break; + case 2: + nextCall = job.Created.Plus(Duration.FromHours(6)); + break; + case 3: + nextCall = job.Created.Plus(Duration.FromHours(12)); + break; + } + } + + await ruleEventRepository.TraceSentAsync(@event.Id, response.Dump, response.Result, response.Elapsed, nextCall); + } + catch (Exception ex) + { + log.LogError(ex, w => w + .WriteProperty("action", "SendWebhookEvent") + .WriteProperty("status", "Failed")); + + throw; + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Read/Rules/RuleEnqueuer.cs b/src/Squidex.Domain.Apps.Read/Rules/RuleEnqueuer.cs new file mode 100644 index 000000000..ad962157c --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/Rules/RuleEnqueuer.cs @@ -0,0 +1,74 @@ +// ========================================================================== +// RuleEnqueuer.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Read.Rules.Repositories; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Events; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Read.Rules +{ + public sealed class RuleEnqueuer : IEventConsumer + { + private readonly IRuleEventRepository ruleEventRepository; + private readonly IRuleRepository ruleRepository; + private readonly RuleService ruleService; + + public string Name + { + get { return GetType().Name; } + } + + public string EventsFilter + { + get { return "^content-"; } + } + + public RuleEnqueuer( + IRuleEventRepository ruleEventRepository, + IRuleRepository ruleRepository, + RuleService ruleService) + { + Guard.NotNull(ruleEventRepository, nameof(ruleEventRepository)); + Guard.NotNull(ruleRepository, nameof(ruleRepository)); + Guard.NotNull(ruleService, nameof(ruleService)); + + this.ruleEventRepository = ruleEventRepository; + this.ruleRepository = ruleRepository; + this.ruleService = ruleService; + } + + public Task ClearAsync() + { + return TaskHelper.Done; + } + + public async Task On(Envelope @event) + { + if (@event.Payload is AppEvent appEvent) + { + var rules = await ruleRepository.QueryCachedByAppAsync(appEvent.AppId.Id); + + foreach (var ruleEntity in rules) + { + var job = ruleService.CreateJob(ruleEntity.Rule, @event); + + if (job == null) + { + continue; + } + + await ruleEventRepository.EnqueueAsync(job, job.Created); + } + } + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Read/Rules/RuleJobResult.cs b/src/Squidex.Domain.Apps.Read/Rules/RuleJobResult.cs new file mode 100644 index 000000000..d8fb997e7 --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/Rules/RuleJobResult.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// RuleJobResult.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Read.Rules +{ + public enum RuleJobResult + { + Pending, + Success, + Retry, + Failed + } +}