From bfc4c9c5ccd57c3f78ed91b9bd34c85a8ad159ff Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Thu, 26 Oct 2017 08:59:35 +0200 Subject: [PATCH 01/14] 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 + } +} From bbc7ad278989260ace333659e05dc9cc81c3116c Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 29 Oct 2017 17:45:59 +0100 Subject: [PATCH 02/14] Packages updated Tests fixed Json support for Rules. --- .../Rules/Json/JsonRule.cs | 46 +++++++ .../Rules/Json/RuleConverter.cs | 27 +++++ .../Rules/Rule.cs | 2 +- .../HandleRules/RuleService.cs | 2 +- .../Model/Rules/RuleTests.cs | 113 ++++++++++++++++++ .../Squidex.Domain.Apps.Core.Tests.csproj | 5 +- .../TestData.cs | 2 + .../Squidex.Domain.Apps.Read.Tests.csproj | 5 +- .../Squidex.Domain.Apps.Write.Tests.csproj | 5 +- .../Squidex.Domain.Users.Tests.csproj | 5 +- .../Squidex.Infrastructure.Tests.csproj | 5 +- 11 files changed, 195 insertions(+), 22 deletions(-) create mode 100644 src/Squidex.Domain.Apps.Core.Model/Rules/Json/JsonRule.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Rules/Json/RuleConverter.cs create mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Model/Rules/RuleTests.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Json/JsonRule.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Json/JsonRule.cs new file mode 100644 index 000000000..163a66661 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Json/JsonRule.cs @@ -0,0 +1,46 @@ +// ========================================================================== +// JsonRule.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Newtonsoft.Json; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Core.Rules.Json +{ + public sealed class JsonRule + { + [JsonProperty] + public RuleTrigger Trigger { get; set; } + + [JsonProperty] + public RuleAction Action { get; set; } + + [JsonProperty] + public bool IsEnabled { get; set; } + + public JsonRule() + { + } + + public JsonRule(Rule rule) + { + SimpleMapper.Map(rule, this); + } + + public Rule ToRule() + { + var rule = new Rule(Trigger, Action); + + if (!IsEnabled) + { + rule.Disable(); + } + + return rule; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Json/RuleConverter.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Json/RuleConverter.cs new file mode 100644 index 000000000..27f7f10c5 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Json/RuleConverter.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// RuleConverter.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Newtonsoft.Json; +using Squidex.Infrastructure.Json; + +namespace Squidex.Domain.Apps.Core.Rules.Json +{ + public sealed class RuleConverter : JsonClassConverter + { + protected override Rule ReadValue(JsonReader reader, JsonSerializer serializer) + { + return serializer.Deserialize(reader).ToRule(); + } + + protected override void WriteValue(JsonWriter writer, Rule value, JsonSerializer serializer) + { + serializer.Serialize(writer, new JsonRule(value)); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs index 877ab8885..8595d84a8 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs @@ -67,7 +67,7 @@ namespace Squidex.Domain.Apps.Core.Rules { Guard.NotNull(newAction, nameof(newAction)); - if (newAction.GetType() != trigger.GetType()) + if (newAction.GetType() != action.GetType()) { throw new ArgumentException("New action has another type.", nameof(newAction)); } diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs index 632f05d72..3d236b815 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs @@ -100,7 +100,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules return job; } - public async Task<(string Dump, RuleResult Result, TimeSpan Elapsed)> InvokeAsync(string actionName, Dictionary job) + public async Task<(string Dump, RuleResult Result, TimeSpan Elapsed)> InvokeAsync(string actionName, RuleJobData job) { try { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Rules/RuleTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Rules/RuleTests.cs new file mode 100644 index 000000000..1d83454fc --- /dev/null +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/Rules/RuleTests.cs @@ -0,0 +1,113 @@ +// ========================================================================== +// RuleTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using FluentAssertions; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Actions; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Model.Rules +{ + public class RuleTests + { + private readonly JsonSerializer serializer = TestData.DefaultSerializer(); + private readonly Rule sut = new Rule(new ContentChangedTrigger(), new WebhookAction()); + + public sealed class OtherTrigger : RuleTrigger + { + public override T Accept(IRuleTriggerVisitor visitor) + { + throw new NotSupportedException(); + } + } + + public sealed class OtherAction : RuleAction + { + public override T Accept(IRuleActionVisitor visitor) + { + throw new NotSupportedException(); + } + } + + [Fact] + public void Should_create_with_trigger_and_action() + { + var ruleTrigger = new ContentChangedTrigger(); + var ruleAction = new WebhookAction(); + + var newRule = new Rule(ruleTrigger, ruleAction); + + Assert.Equal(ruleTrigger, newRule.Trigger); + Assert.Equal(ruleAction, newRule.Action); + Assert.True(newRule.IsEnabled); + } + + [Fact] + public void Should_set_enabled_to_true_when_enabling() + { + sut.Enable(); + + Assert.True(sut.IsEnabled); + } + + [Fact] + public void Should_set_enabled_to_false_when_disabling() + { + sut.Enable(); + sut.Disable(); + + Assert.False(sut.IsEnabled); + } + + [Fact] + public void Should_replace_trigger_when_updating() + { + var newTrigger = new ContentChangedTrigger(); + + sut.Update(newTrigger); + + Assert.Same(newTrigger, sut.Trigger); + } + + [Fact] + public void Should_throw_exception_when_new_trigger_has_other_type() + { + Assert.Throws(() => sut.Update(new OtherTrigger())); + } + + [Fact] + public void Should_replace_action_when_updating() + { + var newAction = new WebhookAction(); + + sut.Update(newAction); + + Assert.Same(newAction, sut.Action); + } + + [Fact] + public void Should_throw_exception_when_new_action_has_other_type() + { + Assert.Throws(() => sut.Update(new OtherAction())); + } + + [Fact] + public void Should_serialize_and_deserialize() + { + sut.Disable(); + + var appClients = JToken.FromObject(sut, serializer).ToObject(serializer); + + appClients.ShouldBeEquivalentTo(sut); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj b/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj index 6314f5295..db37388f0 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj +++ b/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj @@ -19,10 +19,7 @@ - - - - + ..\..\Squidex.ruleset diff --git a/tests/Squidex.Domain.Apps.Core.Tests/TestData.cs b/tests/Squidex.Domain.Apps.Core.Tests/TestData.cs index 15b4449a0..ad21aafa0 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/TestData.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/TestData.cs @@ -9,6 +9,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Squidex.Domain.Apps.Core.Apps.Json; +using Squidex.Domain.Apps.Core.Rules.Json; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas.Json; using Squidex.Infrastructure; @@ -36,6 +37,7 @@ namespace Squidex.Domain.Apps.Core new NamedLongIdConverter(), new NamedStringIdConverter(), new RefTokenConverter(), + new RuleConverter(), new SchemaConverter(new FieldRegistry(typeNameRegistry)), new StringEnumConverter()), diff --git a/tests/Squidex.Domain.Apps.Read.Tests/Squidex.Domain.Apps.Read.Tests.csproj b/tests/Squidex.Domain.Apps.Read.Tests/Squidex.Domain.Apps.Read.Tests.csproj index 48bba6f42..06b97a592 100644 --- a/tests/Squidex.Domain.Apps.Read.Tests/Squidex.Domain.Apps.Read.Tests.csproj +++ b/tests/Squidex.Domain.Apps.Read.Tests/Squidex.Domain.Apps.Read.Tests.csproj @@ -29,10 +29,7 @@ - - - - + ..\..\Squidex.ruleset diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Squidex.Domain.Apps.Write.Tests.csproj b/tests/Squidex.Domain.Apps.Write.Tests/Squidex.Domain.Apps.Write.Tests.csproj index b049b5e7c..ddbddff8e 100644 --- a/tests/Squidex.Domain.Apps.Write.Tests/Squidex.Domain.Apps.Write.Tests.csproj +++ b/tests/Squidex.Domain.Apps.Write.Tests/Squidex.Domain.Apps.Write.Tests.csproj @@ -21,10 +21,7 @@ - - - - + ..\..\Squidex.ruleset diff --git a/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj b/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj index f54f04636..41bc3752e 100644 --- a/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj +++ b/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj @@ -21,10 +21,7 @@ - - - - + ..\..\Squidex.ruleset diff --git a/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj b/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj index feee70e93..1ae104ea4 100644 --- a/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj +++ b/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj @@ -24,10 +24,7 @@ - - - - + ..\..\Squidex.ruleset From 9e1540e909c67331a9ecf47bcdb8e71ce127b2b7 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 29 Oct 2017 21:08:03 +0100 Subject: [PATCH 03/14] Write logic updated and tested. --- .../Rules/Json/RuleConverter.cs | 1 - .../Webhooks/WebhookSchema.cs | 25 -- .../Rules/Utils/RuleEventDispatcher.cs | 37 --- .../Webhooks/WebhookCreated.cs | 18 -- .../Webhooks/WebhookDeleted.cs | 17 -- .../Webhooks/WebhookEditEvent.cs | 21 -- .../Webhooks/WebhookUpdated.cs | 17 -- .../Apps/MongoAppRepository_EventHandling.cs | 1 - .../MongoAssetRepository_EventHandling.cs | 1 - .../MongoContentRepository_EventHandling.cs | 1 - .../{Utils => }/EntityMapper.cs | 2 +- .../History/MongoHistoryEventRepository.cs | 1 - .../{Utils => }/MongoCollectionExtensions.cs | 2 +- .../Rules/MongoRuleEntity.cs | 41 +++ .../MongoRuleEventEntity.cs} | 45 +--- .../MongoRuleEventRepository.cs} | 50 ++-- .../Rules/MongoRuleRepository.cs | 90 +++++++ .../MongoRuleRepository_EventHandling.cs | 97 +++++++ .../MongoSchemaRepository_EventHandling.cs | 1 - .../Webhooks/MongoWebhookEntity.cs | 74 ------ .../Webhooks/MongoWebhookRepository.cs | 119 --------- .../MongoWebhookRepository_EventHandling.cs | 99 -------- .../Implementations/CachingAppProvider.cs | 1 - .../{Utils => }/CachingProviderBase.cs | 2 +- .../Contents/Edm/EdmModelBuilder.cs | 1 - .../Contents/GraphQL/CachingGraphQLService.cs | 1 - .../Rules/IRuleEventEntity.cs | 1 + .../Repositories/IRuleEventRepository.cs | 2 +- .../Rules/RuleDequeuer.cs | 4 +- .../Implementations/CachingSchemaProvider.cs | 1 - .../Webhooks/IWebhookEntity.cs | 31 --- .../Webhooks/IWebhookEventEntity.cs | 27 -- .../Repositories/IWebhookEventRepository.cs | 35 --- .../Repositories/IWebhookRepository.cs | 23 -- .../Webhooks/WebhookDequeuer.cs | 160 ------------ .../Webhooks/WebhookEnqueuer.cs | 140 ---------- .../Webhooks/WebhookJob.cs | 32 --- .../Webhooks/WebhookSender.cs | 98 ------- .../Commands/CreateRule.cs} | 6 +- .../Commands/DeleteRule.cs} | 6 +- .../Rules/Commands/DisableRule.cs} | 10 +- .../Rules/Commands/EnableRule.cs} | 10 +- .../Commands/RuleAggregateCommand.cs} | 10 +- .../Rules/Commands/RuleEditCommand.cs} | 12 +- .../Rules/Commands/UpdateRule.cs | 14 + .../Rules/Guards/GuardRule.cs | 107 ++++++++ .../Rules/Guards/RuleActionValidator.cs | 40 +++ .../Rules/Guards/RuleTriggerValidator.cs | 54 ++++ .../Rules/RuleCommandMiddleware.cs | 92 +++++++ .../Rules/RuleDomainObject.cs | 118 +++++++++ .../Webhooks/Commands/CreateWebhook.cs | 23 -- .../Webhooks/Commands/WebhookEditCommand.cs | 21 -- .../Webhooks/Guards/GuardWebhook.cs | 61 ----- .../Webhooks/WebhookCommandMiddleware.cs | 72 ------ .../Webhooks/WebhookDomainObject.cs | 82 ------ src/Squidex/Squidex.csproj | 4 +- .../Guards/Actions/WebhookActionTests.cs | 48 ++++ .../Rules/Guards/GuardRuleTests.cs | 158 ++++++++++++ .../Triggers/ContentChangedTriggerTests.cs | 85 +++++++ .../Rules/RuleCommandMiddlewareTests.cs | 117 +++++++++ .../Rules/RuleDomainObjectTests.cs | 240 ++++++++++++++++++ .../Webhooks/Guards/GuardWebhookTests.cs | 138 ---------- .../Webhooks/WebhookCommandMiddlewareTests.cs | 115 --------- .../Webhooks/WebhookDomainObjectTests.cs | 159 ------------ 64 files changed, 1374 insertions(+), 1747 deletions(-) delete mode 100644 src/Squidex.Domain.Apps.Core.Model/Webhooks/WebhookSchema.cs delete mode 100644 src/Squidex.Domain.Apps.Events/Webhooks/WebhookCreated.cs delete mode 100644 src/Squidex.Domain.Apps.Events/Webhooks/WebhookDeleted.cs delete mode 100644 src/Squidex.Domain.Apps.Events/Webhooks/WebhookEditEvent.cs delete mode 100644 src/Squidex.Domain.Apps.Events/Webhooks/WebhookUpdated.cs rename src/Squidex.Domain.Apps.Read.MongoDb/{Utils => }/EntityMapper.cs (98%) rename src/Squidex.Domain.Apps.Read.MongoDb/{Utils => }/MongoCollectionExtensions.cs (98%) create mode 100644 src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEntity.cs rename src/Squidex.Domain.Apps.Read.MongoDb/{Webhooks/MongoWebhookEventEntity.cs => Rules/MongoRuleEventEntity.cs} (59%) rename src/Squidex.Domain.Apps.Read.MongoDb/{Webhooks/MongoWebhookEventRepository.cs => Rules/MongoRuleEventRepository.cs} (66%) create mode 100644 src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository.cs create mode 100644 src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository_EventHandling.cs delete mode 100644 src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookEntity.cs delete mode 100644 src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookRepository.cs delete mode 100644 src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookRepository_EventHandling.cs rename src/Squidex.Domain.Apps.Read/{Utils => }/CachingProviderBase.cs (95%) delete mode 100644 src/Squidex.Domain.Apps.Read/Webhooks/IWebhookEntity.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Webhooks/IWebhookEventEntity.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Webhooks/Repositories/IWebhookEventRepository.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Webhooks/Repositories/IWebhookRepository.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Webhooks/WebhookDequeuer.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Webhooks/WebhookEnqueuer.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Webhooks/WebhookJob.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Webhooks/WebhookSender.cs rename src/Squidex.Domain.Apps.Write/{Webhooks/Commands/UpdateWebhook.cs => Rules/Commands/CreateRule.cs} (71%) rename src/Squidex.Domain.Apps.Write/{Webhooks/Commands/DeleteWebhook.cs => Rules/Commands/DeleteRule.cs} (70%) rename src/{Squidex.Domain.Apps.Read/Webhooks/WebhookResult.cs => Squidex.Domain.Apps.Write/Rules/Commands/DisableRule.cs} (67%) rename src/{Squidex.Domain.Apps.Read/Webhooks/WebhookJobResult.cs => Squidex.Domain.Apps.Write/Rules/Commands/EnableRule.cs} (67%) rename src/Squidex.Domain.Apps.Write/{Webhooks/Commands/WebhookAggregateCommand.cs => Rules/Commands/RuleAggregateCommand.cs} (64%) rename src/{Squidex.Domain.Apps.Events/Webhooks/WebhookEvent.cs => Squidex.Domain.Apps.Write/Rules/Commands/RuleEditCommand.cs} (55%) create mode 100644 src/Squidex.Domain.Apps.Write/Rules/Commands/UpdateRule.cs create mode 100644 src/Squidex.Domain.Apps.Write/Rules/Guards/GuardRule.cs create mode 100644 src/Squidex.Domain.Apps.Write/Rules/Guards/RuleActionValidator.cs create mode 100644 src/Squidex.Domain.Apps.Write/Rules/Guards/RuleTriggerValidator.cs create mode 100644 src/Squidex.Domain.Apps.Write/Rules/RuleCommandMiddleware.cs create mode 100644 src/Squidex.Domain.Apps.Write/Rules/RuleDomainObject.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Webhooks/Commands/CreateWebhook.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Webhooks/Commands/WebhookEditCommand.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Webhooks/Guards/GuardWebhook.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Webhooks/WebhookCommandMiddleware.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Webhooks/WebhookDomainObject.cs create mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Rules/Guards/Actions/WebhookActionTests.cs create mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Rules/Guards/GuardRuleTests.cs create mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs create mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Rules/RuleCommandMiddlewareTests.cs create mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Rules/RuleDomainObjectTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Webhooks/Guards/GuardWebhookTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Webhooks/WebhookCommandMiddlewareTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Webhooks/WebhookDomainObjectTests.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Json/RuleConverter.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Json/RuleConverter.cs index 27f7f10c5..cd3e9707f 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Rules/Json/RuleConverter.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Json/RuleConverter.cs @@ -6,7 +6,6 @@ // All rights reserved. // ========================================================================== -using System; using Newtonsoft.Json; using Squidex.Infrastructure.Json; diff --git a/src/Squidex.Domain.Apps.Core.Model/Webhooks/WebhookSchema.cs b/src/Squidex.Domain.Apps.Core.Model/Webhooks/WebhookSchema.cs deleted file mode 100644 index 169e85b7d..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Webhooks/WebhookSchema.cs +++ /dev/null @@ -1,25 +0,0 @@ -// ========================================================================== -// WebhookSchema.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; - -namespace Squidex.Domain.Apps.Core.Webhooks -{ - public sealed class WebhookSchema - { - 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.Events/Rules/Utils/RuleEventDispatcher.cs b/src/Squidex.Domain.Apps.Events/Rules/Utils/RuleEventDispatcher.cs index 45b03161d..8de79c181 100644 --- a/src/Squidex.Domain.Apps.Events/Rules/Utils/RuleEventDispatcher.cs +++ b/src/Squidex.Domain.Apps.Events/Rules/Utils/RuleEventDispatcher.cs @@ -6,12 +6,7 @@ // 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 { @@ -22,21 +17,6 @@ namespace Squidex.Domain.Apps.Events.Rules.Utils 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) @@ -59,22 +39,5 @@ namespace Squidex.Domain.Apps.Events.Rules.Utils { 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.Events/Webhooks/WebhookCreated.cs b/src/Squidex.Domain.Apps.Events/Webhooks/WebhookCreated.cs deleted file mode 100644 index 1cab7fdce..000000000 --- a/src/Squidex.Domain.Apps.Events/Webhooks/WebhookCreated.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ========================================================================== -// WebhookCreated.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Infrastructure.CQRS.Events; - -namespace Squidex.Domain.Apps.Events.Webhooks -{ - [EventType(nameof(WebhookCreated))] - public sealed class WebhookCreated : WebhookEditEvent - { - public string SharedSecret { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Events/Webhooks/WebhookDeleted.cs b/src/Squidex.Domain.Apps.Events/Webhooks/WebhookDeleted.cs deleted file mode 100644 index a04e86ab6..000000000 --- a/src/Squidex.Domain.Apps.Events/Webhooks/WebhookDeleted.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ========================================================================== -// WebhookDeleted.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Infrastructure.CQRS.Events; - -namespace Squidex.Domain.Apps.Events.Webhooks -{ - [EventType(nameof(WebhookDeleted), 2)] - public sealed class WebhookDeleted : WebhookEvent - { - } -} diff --git a/src/Squidex.Domain.Apps.Events/Webhooks/WebhookEditEvent.cs b/src/Squidex.Domain.Apps.Events/Webhooks/WebhookEditEvent.cs deleted file mode 100644 index 02dde09b3..000000000 --- a/src/Squidex.Domain.Apps.Events/Webhooks/WebhookEditEvent.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ========================================================================== -// WebhookEditEvent.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using Squidex.Domain.Apps.Core.Webhooks; - -namespace Squidex.Domain.Apps.Events.Webhooks -{ - public abstract class WebhookEditEvent : WebhookEvent - { - public Uri Url { get; set; } - - public List Schemas { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Events/Webhooks/WebhookUpdated.cs b/src/Squidex.Domain.Apps.Events/Webhooks/WebhookUpdated.cs deleted file mode 100644 index c40793935..000000000 --- a/src/Squidex.Domain.Apps.Events/Webhooks/WebhookUpdated.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ========================================================================== -// WebhookUpdated.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Infrastructure.CQRS.Events; - -namespace Squidex.Domain.Apps.Events.Webhooks -{ - [EventType(nameof(WebhookUpdated))] - public sealed class WebhookUpdated : WebhookEditEvent - { - } -} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs index 0fdab29f8..51c627fec 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs @@ -13,7 +13,6 @@ using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Apps; using Squidex.Domain.Apps.Events.Apps.Utils; -using Squidex.Domain.Apps.Read.MongoDb.Utils; using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.Dispatching; diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository_EventHandling.cs index 1915a40bd..2685c24fc 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository_EventHandling.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository_EventHandling.cs @@ -9,7 +9,6 @@ using System.Threading.Tasks; using MongoDB.Driver; using Squidex.Domain.Apps.Events.Assets; -using Squidex.Domain.Apps.Read.MongoDb.Utils; using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.Reflection; diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs index b76240365..077fa6cd1 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs @@ -13,7 +13,6 @@ using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Domain.Apps.Events.Apps; using Squidex.Domain.Apps.Events.Assets; using Squidex.Domain.Apps.Events.Contents; -using Squidex.Domain.Apps.Read.MongoDb.Utils; using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.Reflection; diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Utils/EntityMapper.cs b/src/Squidex.Domain.Apps.Read.MongoDb/EntityMapper.cs similarity index 98% rename from src/Squidex.Domain.Apps.Read.MongoDb/Utils/EntityMapper.cs rename to src/Squidex.Domain.Apps.Read.MongoDb/EntityMapper.cs index d4b999cf3..0557c28f4 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Utils/EntityMapper.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/EntityMapper.cs @@ -10,7 +10,7 @@ using Squidex.Domain.Apps.Events; using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.MongoDb; -namespace Squidex.Domain.Apps.Read.MongoDb.Utils +namespace Squidex.Domain.Apps.Read.MongoDb { public static class EntityMapper { diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventRepository.cs b/src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventRepository.cs index 15610b5c8..53368fd51 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventRepository.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventRepository.cs @@ -14,7 +14,6 @@ using MongoDB.Driver; using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Read.History; using Squidex.Domain.Apps.Read.History.Repositories; -using Squidex.Domain.Apps.Read.MongoDb.Utils; using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.MongoDb; diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Utils/MongoCollectionExtensions.cs b/src/Squidex.Domain.Apps.Read.MongoDb/MongoCollectionExtensions.cs similarity index 98% rename from src/Squidex.Domain.Apps.Read.MongoDb/Utils/MongoCollectionExtensions.cs rename to src/Squidex.Domain.Apps.Read.MongoDb/MongoCollectionExtensions.cs index 78cd17175..c9cc5cd8c 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Utils/MongoCollectionExtensions.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/MongoCollectionExtensions.cs @@ -14,7 +14,7 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.MongoDb; -namespace Squidex.Domain.Apps.Read.MongoDb.Utils +namespace Squidex.Domain.Apps.Read.MongoDb { public static class MongoCollectionExtensions { diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEntity.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEntity.cs new file mode 100644 index 000000000..be9369804 --- /dev/null +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEntity.cs @@ -0,0 +1,41 @@ +// ========================================================================== +// MongoRuleEntity.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using MongoDB.Bson.Serialization.Attributes; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Read.Rules; +using Squidex.Infrastructure; +using Squidex.Infrastructure.MongoDb; + +namespace Squidex.Domain.Apps.Read.MongoDb.Rules +{ + public class MongoRuleEntity : MongoEntity, IRuleEntity + { + [BsonRequired] + [BsonElement] + public Guid AppId { get; set; } + + [BsonRequired] + [BsonElement] + public RefToken CreatedBy { get; set; } + + [BsonRequired] + [BsonElement] + public RefToken LastModifiedBy { get; set; } + + [BsonRequired] + [BsonElement] + public long Version { get; set; } + + [BsonRequired] + [BsonElement] + [BsonJson] + public Rule Rule { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookEventEntity.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventEntity.cs similarity index 59% rename from src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookEventEntity.cs rename to src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventEntity.cs index 18f212967..0b31e6a12 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookEventEntity.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventEntity.cs @@ -1,5 +1,5 @@ // ========================================================================== -// MongoWebhookEventEntity.cs +// MongoRuleEventEntity.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -9,43 +9,30 @@ using System; using MongoDB.Bson.Serialization.Attributes; using NodaTime; -using Squidex.Domain.Apps.Read.Webhooks; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Read.Rules; using Squidex.Infrastructure.MongoDb; -using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Read.MongoDb.Webhooks +namespace Squidex.Domain.Apps.Read.MongoDb.Rules { - public sealed class MongoWebhookEventEntity : MongoEntity, IWebhookEventEntity + public sealed class MongoRuleEventEntity : MongoEntity, IRuleEventEntity { - private WebhookJob job; - [BsonRequired] [BsonElement] public Guid AppId { get; set; } [BsonRequired] [BsonElement] - public long Version { get; set; } - - [BsonRequired] - [BsonElement] - public Uri RequestUrl { get; set; } - - [BsonRequired] - [BsonElement] - public string RequestBody { get; set; } - - [BsonRequired] - [BsonElement] - public string RequestSignature { get; set; } + public string EventName { get; set; } [BsonRequired] [BsonElement] - public string EventName { get; set; } + public string LastDump { get; set; } [BsonRequired] [BsonElement] - public string LastDump { get; set; } + public int NumCalls { get; set; } [BsonRequired] [BsonElement] @@ -57,23 +44,19 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Webhooks [BsonRequired] [BsonElement] - public int NumCalls { get; set; } + public bool IsSending { get; set; } [BsonRequired] [BsonElement] - public bool IsSending { get; set; } + public RuleResult Result { get; set; } [BsonRequired] [BsonElement] - public WebhookResult Result { get; set; } + public RuleJobResult JobResult { get; set; } [BsonRequired] [BsonElement] - public WebhookJobResult JobResult { get; set; } - - public WebhookJob Job - { - get { return job ?? (job = SimpleMapper.Map(this, new WebhookJob())); } - } + [BsonJson] + public RuleJob Job { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookEventRepository.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventRepository.cs similarity index 66% rename from src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookEventRepository.cs rename to src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventRepository.cs index d6a4325fc..0bdaa4198 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookEventRepository.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventRepository.cs @@ -1,5 +1,5 @@ // ========================================================================== -// MongoWebhookEventRepository.cs +// MongoRuleEventRepository.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -12,19 +12,21 @@ using System.Threading; using System.Threading.Tasks; using MongoDB.Driver; using NodaTime; -using Squidex.Domain.Apps.Read.Webhooks; -using Squidex.Domain.Apps.Read.Webhooks.Repositories; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Read.Rules; +using Squidex.Domain.Apps.Read.Rules.Repositories; using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Read.MongoDb.Webhooks +namespace Squidex.Domain.Apps.Read.MongoDb.Rules { - public sealed class MongoWebhookEventRepository : MongoRepositoryBase, IWebhookEventRepository + public sealed class MongoRuleEventRepository : MongoRepositoryBase, IRuleEventRepository { private readonly IClock clock; - public MongoWebhookEventRepository(IMongoDatabase database, IClock clock) + public MongoRuleEventRepository(IMongoDatabase database, IClock clock) : base(database) { Guard.NotNull(clock, nameof(clock)); @@ -34,10 +36,10 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Webhooks protected override string CollectionName() { - return "WebhookEvents"; + return "RuleEvents"; } - protected override Task SetupCollectionAsync(IMongoCollection collection) + protected override Task SetupCollectionAsync(IMongoCollection collection) { return Task.WhenAll( collection.Indexes.CreateOneAsync(Index.Ascending(x => x.NextAttempt).Descending(x => x.IsSending)), @@ -45,14 +47,14 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Webhooks collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Expires), new CreateIndexOptions { ExpireAfter = TimeSpan.Zero })); } - public Task QueryPendingAsync(Func callback, CancellationToken cancellationToken = default(CancellationToken)) + public Task QueryPendingAsync(Func callback, CancellationToken cancellationToken = default(CancellationToken)) { var now = clock.GetCurrentInstant(); return Collection.Find(x => x.NextAttempt < now && !x.IsSending).ForEachAsync(callback, cancellationToken); } - public async Task> QueryByAppAsync(Guid appId, int skip = 0, int take = 20) + public async Task> QueryByAppAsync(Guid appId, int skip = 0, int take = 20) { var webhookEventEntities = await Collection.Find(x => x.AppId == appId).Skip(skip).Limit(take).SortByDescending(x => x.Created) @@ -61,7 +63,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Webhooks return webhookEventEntities; } - public async Task FindAsync(Guid id) + public async Task FindAsync(Guid id) { var webhookEventEntity = await Collection.Find(x => x.Id == id) @@ -80,33 +82,33 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Webhooks return Collection.UpdateOneAsync(x => x.Id == id, Update.Set(x => x.NextAttempt, nextAttempt)); } - public Task TraceSendingAsync(Guid jobId) + public Task EnqueueAsync(RuleJob job, Instant nextAttempt) { - return Collection.UpdateOneAsync(x => x.Id == jobId, Update.Set(x => x.IsSending, true)); + var entity = SimpleMapper.Map(job, new MongoRuleEventEntity { Created = clock.GetCurrentInstant(), NextAttempt = nextAttempt }); + + return Collection.InsertOneIfNotExistsAsync(entity); } - public Task EnqueueAsync(WebhookJob job, Instant nextAttempt) + public Task MarkSendingAsync(Guid jobId) { - var entity = SimpleMapper.Map(job, new MongoWebhookEventEntity { Created = clock.GetCurrentInstant(), NextAttempt = nextAttempt }); - - return Collection.InsertOneIfNotExistsAsync(entity); + return Collection.UpdateOneAsync(x => x.Id == jobId, Update.Set(x => x.IsSending, true)); } - public Task TraceSentAsync(Guid jobId, string dump, WebhookResult result, TimeSpan elapsed, Instant? nextAttempt) + public Task MarkSentAsync(Guid jobId, string dump, RuleResult result, TimeSpan elapsed, Instant? nextAttempt) { - WebhookJobResult jobResult; + RuleJobResult jobResult; - if (result != WebhookResult.Success && nextAttempt == null) + if (result != RuleResult.Success && nextAttempt == null) { - jobResult = WebhookJobResult.Failed; + jobResult = RuleJobResult.Failed; } - else if (result != WebhookResult.Success && nextAttempt.HasValue) + else if (result != RuleResult.Success && nextAttempt.HasValue) { - jobResult = WebhookJobResult.Retry; + jobResult = RuleJobResult.Retry; } else { - jobResult = WebhookJobResult.Success; + jobResult = RuleJobResult.Success; } return Collection.UpdateOneAsync(x => x.Id == jobId, diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository.cs new file mode 100644 index 000000000..c4dc37beb --- /dev/null +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository.cs @@ -0,0 +1,90 @@ +// ========================================================================== +// MongoRuleRepository.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; +using Squidex.Domain.Apps.Read.Rules; +using Squidex.Domain.Apps.Read.Rules.Repositories; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Events; +using Squidex.Infrastructure.MongoDb; + +namespace Squidex.Domain.Apps.Read.MongoDb.Rules +{ + public partial class MongoRuleRepository : MongoRepositoryBase, IRuleRepository, IEventConsumer + { + private static readonly List EmptyRules = new List(); + private readonly SemaphoreSlim lockObject = new SemaphoreSlim(1); + private Dictionary> inMemoryWebhooks; + + public MongoRuleRepository(IMongoDatabase database) + : base(database) + { + } + + protected override string CollectionName() + { + return "Projections_Rules"; + } + + protected override Task SetupCollectionAsync(IMongoCollection collection) + { + return Task.WhenAll(collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AppId))); + } + + public async Task> QueryByAppAsync(Guid appId) + { + var entities = + await Collection.Find(x => x.AppId == appId) + .ToListAsync(); + + return entities.OfType().ToList(); + } + + public async Task> QueryCachedByAppAsync(Guid appId) + { + await EnsureRulesLoadedAsync(); + + return inMemoryWebhooks.GetOrDefault(appId) ?? EmptyRules; + } + + private async Task EnsureRulesLoadedAsync() + { + if (inMemoryWebhooks == null) + { + try + { + await lockObject.WaitAsync(); + + if (inMemoryWebhooks == null) + { + inMemoryWebhooks = new Dictionary>(); + + var webhooks = + await Collection.Find(new BsonDocument()) + .ToListAsync(); + + foreach (var webhook in webhooks) + { + inMemoryWebhooks.GetOrAddNew(webhook.AppId).Add(webhook); + } + } + } + finally + { + lockObject.Release(); + } + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository_EventHandling.cs new file mode 100644 index 000000000..4de792863 --- /dev/null +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository_EventHandling.cs @@ -0,0 +1,97 @@ +// ========================================================================== +// MongoRuleRepository_EventHandling.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Threading.Tasks; +using MongoDB.Driver; +using Squidex.Domain.Apps.Events.Rules; +using Squidex.Domain.Apps.Events.Rules.Utils; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Events; +using Squidex.Infrastructure.Dispatching; + +namespace Squidex.Domain.Apps.Read.MongoDb.Rules +{ + public partial class MongoRuleRepository + { + public string Name + { + get { return GetType().Name; } + } + + public string EventsFilter + { + get { return "^rules-"; } + } + + public Task On(Envelope @event) + { + return this.DispatchActionAsync(@event.Payload, @event.Headers); + } + + protected async Task On(RuleCreated @event, EnvelopeHeaders headers) + { + await EnsureRulesLoadedAsync(); + + await Collection.CreateAsync(@event, headers, w => + { + w.Rule = RuleEventDispatcher.Create(@event); + + inMemoryWebhooks.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id); + inMemoryWebhooks.GetOrAddNew(w.AppId).Add(w); + }); + } + + protected async Task On(RuleUpdated @event, EnvelopeHeaders headers) + { + await EnsureRulesLoadedAsync(); + + await Collection.UpdateAsync(@event, headers, w => + { + w.Rule.Apply(@event); + + inMemoryWebhooks.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id); + inMemoryWebhooks.GetOrAddNew(w.AppId).Add(w); + }); + } + + protected async Task On(RuleEnabled @event, EnvelopeHeaders headers) + { + await EnsureRulesLoadedAsync(); + + await Collection.UpdateAsync(@event, headers, w => + { + w.Rule.Apply(@event); + + inMemoryWebhooks.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id); + inMemoryWebhooks.GetOrAddNew(w.AppId).Add(w); + }); + } + + protected async Task On(RuleDisabled @event, EnvelopeHeaders headers) + { + await EnsureRulesLoadedAsync(); + + await Collection.UpdateAsync(@event, headers, w => + { + w.Rule.Apply(@event); + + inMemoryWebhooks.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id); + inMemoryWebhooks.GetOrAddNew(w.AppId).Add(w); + }); + } + + protected async Task On(RuleDeleted @event, EnvelopeHeaders headers) + { + await EnsureRulesLoadedAsync(); + + inMemoryWebhooks.GetOrAddNew(@event.AppId.Id).RemoveAll(x => x.Id == @event.RuleId); + + await Collection.DeleteManyAsync(x => x.Id == @event.RuleId); + } + } +} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs index b4a662186..772620b38 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs @@ -12,7 +12,6 @@ using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Schemas; using Squidex.Domain.Apps.Events.Schemas.Old; using Squidex.Domain.Apps.Events.Schemas.Utils; -using Squidex.Domain.Apps.Read.MongoDb.Utils; using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.Reflection; diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookEntity.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookEntity.cs deleted file mode 100644 index 239ea346a..000000000 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookEntity.cs +++ /dev/null @@ -1,74 +0,0 @@ -// ========================================================================== -// MongoWebhookEntity.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using MongoDB.Bson.Serialization.Attributes; -using Squidex.Domain.Apps.Core.Webhooks; -using Squidex.Domain.Apps.Read.Webhooks; -using Squidex.Infrastructure; -using Squidex.Infrastructure.MongoDb; - -namespace Squidex.Domain.Apps.Read.MongoDb.Webhooks -{ - public class MongoWebhookEntity : MongoEntity, IWebhookEntity - { - [BsonRequired] - [BsonElement] - public Uri Url { get; set; } - - [BsonRequired] - [BsonElement] - public Guid AppId { get; set; } - - [BsonRequired] - [BsonElement] - public long Version { get; set; } - - [BsonRequired] - [BsonElement] - public RefToken CreatedBy { get; set; } - - [BsonRequired] - [BsonElement] - public RefToken LastModifiedBy { get; set; } - - [BsonRequired] - [BsonElement] - public string SharedSecret { get; set; } - - [BsonRequired] - [BsonElement] - public long TotalSucceeded { get; set; } - - [BsonRequired] - [BsonElement] - public long TotalFailed { get; set; } - - [BsonRequired] - [BsonElement] - public long TotalTimedout { get; set; } - - [BsonRequired] - [BsonElement] - public long TotalRequestTime { get; set; } - - [BsonRequired] - [BsonElement] - public List Schemas { get; set; } - - [BsonRequired] - [BsonElement] - public List SchemaIds { get; set; } - - IEnumerable IWebhookEntity.Schemas - { - get { return Schemas; } - } - } -} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookRepository.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookRepository.cs deleted file mode 100644 index d4dc24ac9..000000000 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookRepository.cs +++ /dev/null @@ -1,119 +0,0 @@ -// ========================================================================== -// MongoWebhookRepository.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MongoDB.Bson; -using MongoDB.Driver; -using Squidex.Domain.Apps.Read.Webhooks; -using Squidex.Domain.Apps.Read.Webhooks.Repositories; -using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS.Events; -using Squidex.Infrastructure.MongoDb; - -namespace Squidex.Domain.Apps.Read.MongoDb.Webhooks -{ - public partial class MongoWebhookRepository : MongoRepositoryBase, IWebhookRepository, IEventConsumer - { - private static readonly List EmptyWebhooks = new List(); - private readonly SemaphoreSlim lockObject = new SemaphoreSlim(1); - private Dictionary> inMemoryWebhooks; - - public MongoWebhookRepository(IMongoDatabase database) - : base(database) - { - } - - protected override string CollectionName() - { - return "Projections_SchemaWebhooks"; - } - - protected override Task SetupCollectionAsync(IMongoCollection collection) - { - return Task.WhenAll( - collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AppId)), - collection.Indexes.CreateOneAsync(Index.Ascending(x => x.SchemaIds))); - } - - public async Task> QueryByAppAsync(Guid appId) - { - var entities = - await Collection.Find(x => x.AppId == appId) - .ToListAsync(); - - return entities.OfType().ToList(); - } - - public async Task> QueryCachedByAppAsync(Guid appId) - { - await EnsureWebooksLoadedAsync(); - - return inMemoryWebhooks.GetOrDefault(appId) ?? EmptyWebhooks; - } - - public async Task TraceSentAsync(Guid webhookId, WebhookResult result, TimeSpan elapsed) - { - var webhookEntity = - await Collection.Find(x => x.Id == webhookId) - .FirstOrDefaultAsync(); - - if (webhookEntity != null) - { - switch (result) - { - case WebhookResult.Success: - webhookEntity.TotalSucceeded++; - break; - case WebhookResult.Failed: - webhookEntity.TotalFailed++; - break; - case WebhookResult.Timeout: - webhookEntity.TotalTimedout++; - break; - } - - webhookEntity.TotalRequestTime += (long)elapsed.TotalMilliseconds; - - await Collection.ReplaceOneAsync(x => x.Id == webhookId, webhookEntity); - } - } - - private async Task EnsureWebooksLoadedAsync() - { - if (inMemoryWebhooks == null) - { - try - { - await lockObject.WaitAsync(); - - if (inMemoryWebhooks == null) - { - inMemoryWebhooks = new Dictionary>(); - - var webhooks = - await Collection.Find(new BsonDocument()) - .ToListAsync(); - - foreach (var webhook in webhooks) - { - inMemoryWebhooks.GetOrAddNew(webhook.AppId).Add(webhook); - } - } - } - finally - { - lockObject.Release(); - } - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookRepository_EventHandling.cs deleted file mode 100644 index 9646e8d62..000000000 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookRepository_EventHandling.cs +++ /dev/null @@ -1,99 +0,0 @@ -// ========================================================================== -// MongoSchemaWebhookRepository_EventHandling.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Linq; -using System.Threading.Tasks; -using MongoDB.Driver; -using Squidex.Domain.Apps.Events.Schemas; -using Squidex.Domain.Apps.Events.Webhooks; -using Squidex.Domain.Apps.Read.MongoDb.Utils; -using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS.Events; -using Squidex.Infrastructure.Dispatching; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Read.MongoDb.Webhooks -{ - public partial class MongoWebhookRepository - { - public string Name - { - get { return GetType().Name; } - } - - public string EventsFilter - { - get { return "(^webhook-)|(^schema-)"; } - } - - public Task On(Envelope @event) - { - return this.DispatchActionAsync(@event.Payload, @event.Headers); - } - - protected async Task On(WebhookCreated @event, EnvelopeHeaders headers) - { - await EnsureWebooksLoadedAsync(); - - await Collection.CreateAsync(@event, headers, w => - { - SimpleMapper.Map(@event, w); - - w.SchemaIds = w.Schemas.Select(x => x.SchemaId).ToList(); - - inMemoryWebhooks.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id); - inMemoryWebhooks.GetOrAddNew(w.AppId).Add(w); - }); - } - - protected async Task On(WebhookUpdated @event, EnvelopeHeaders headers) - { - await EnsureWebooksLoadedAsync(); - - await Collection.UpdateAsync(@event, headers, w => - { - SimpleMapper.Map(@event, w); - - w.SchemaIds = w.Schemas.Select(x => x.SchemaId).ToList(); - - inMemoryWebhooks.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id); - inMemoryWebhooks.GetOrAddNew(w.AppId).Add(w); - }); - } - - protected async Task On(SchemaDeleted @event, EnvelopeHeaders headers) - { - await EnsureWebooksLoadedAsync(); - - var webhooks = - await Collection.Find(t => t.SchemaIds.Contains(@event.SchemaId.Id)) - .ToListAsync(); - - foreach (var webhook in webhooks) - { - webhook.Schemas.RemoveAll(s => s.SchemaId == @event.SchemaId.Id); - - webhook.SchemaIds = webhook.Schemas.Select(x => x.SchemaId).ToList(); - - inMemoryWebhooks.GetOrAddNew(webhook.AppId).RemoveAll(x => x.Id == webhook.Id); - inMemoryWebhooks.GetOrAddNew(webhook.AppId).Add(webhook); - - await Collection.ReplaceOneAsync(x => x.Id == webhook.Id, webhook); - } - } - - protected async Task On(WebhookDeleted @event, EnvelopeHeaders headers) - { - await EnsureWebooksLoadedAsync(); - - inMemoryWebhooks.GetOrAddNew(@event.AppId.Id).RemoveAll(x => x.Id == @event.WebhookId); - - await Collection.DeleteManyAsync(x => x.Id == @event.WebhookId); - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Apps/Services/Implementations/CachingAppProvider.cs b/src/Squidex.Domain.Apps.Read/Apps/Services/Implementations/CachingAppProvider.cs index 8e82a4a31..becf14af4 100644 --- a/src/Squidex.Domain.Apps.Read/Apps/Services/Implementations/CachingAppProvider.cs +++ b/src/Squidex.Domain.Apps.Read/Apps/Services/Implementations/CachingAppProvider.cs @@ -11,7 +11,6 @@ using System.Threading.Tasks; using Microsoft.Extensions.Caching.Memory; using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Read.Apps.Repositories; -using Squidex.Domain.Apps.Read.Utils; using Squidex.Infrastructure; using Squidex.Infrastructure.Caching; using Squidex.Infrastructure.CQRS.Events; diff --git a/src/Squidex.Domain.Apps.Read/Utils/CachingProviderBase.cs b/src/Squidex.Domain.Apps.Read/CachingProviderBase.cs similarity index 95% rename from src/Squidex.Domain.Apps.Read/Utils/CachingProviderBase.cs rename to src/Squidex.Domain.Apps.Read/CachingProviderBase.cs index 6e5330c04..5883b29ab 100644 --- a/src/Squidex.Domain.Apps.Read/Utils/CachingProviderBase.cs +++ b/src/Squidex.Domain.Apps.Read/CachingProviderBase.cs @@ -9,7 +9,7 @@ using Microsoft.Extensions.Caching.Memory; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Read.Utils +namespace Squidex.Domain.Apps.Read { public abstract class CachingProviderBase { diff --git a/src/Squidex.Domain.Apps.Read/Contents/Edm/EdmModelBuilder.cs b/src/Squidex.Domain.Apps.Read/Contents/Edm/EdmModelBuilder.cs index 14fa93c78..7f3fd63ce 100644 --- a/src/Squidex.Domain.Apps.Read/Contents/Edm/EdmModelBuilder.cs +++ b/src/Squidex.Domain.Apps.Read/Contents/Edm/EdmModelBuilder.cs @@ -14,7 +14,6 @@ using Squidex.Domain.Apps.Core.GenerateEdmSchema; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Read.Apps; using Squidex.Domain.Apps.Read.Schemas; -using Squidex.Domain.Apps.Read.Utils; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Read.Contents.Edm diff --git a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/CachingGraphQLService.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/CachingGraphQLService.cs index cfabcb279..f20d7cf4c 100644 --- a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/CachingGraphQLService.cs +++ b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/CachingGraphQLService.cs @@ -15,7 +15,6 @@ using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Read.Apps; using Squidex.Domain.Apps.Read.Assets.Repositories; using Squidex.Domain.Apps.Read.Schemas.Repositories; -using Squidex.Domain.Apps.Read.Utils; using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.Tasks; diff --git a/src/Squidex.Domain.Apps.Read/Rules/IRuleEventEntity.cs b/src/Squidex.Domain.Apps.Read/Rules/IRuleEventEntity.cs index 43dea2b55..631ad2322 100644 --- a/src/Squidex.Domain.Apps.Read/Rules/IRuleEventEntity.cs +++ b/src/Squidex.Domain.Apps.Read/Rules/IRuleEventEntity.cs @@ -8,6 +8,7 @@ using NodaTime; using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; namespace Squidex.Domain.Apps.Read.Rules { diff --git a/src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleEventRepository.cs b/src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleEventRepository.cs index ded48c233..f67c074e8 100644 --- a/src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleEventRepository.cs +++ b/src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleEventRepository.cs @@ -24,7 +24,7 @@ namespace Squidex.Domain.Apps.Read.Rules.Repositories Task MarkSendingAsync(Guid jobId); - Task TraceSentAsync(Guid jobId, string dump, RuleResult result, TimeSpan elapsed, Instant? nextCall); + Task MarkSentAsync(Guid jobId, string dump, RuleResult result, TimeSpan elapsed, Instant? nextCall); Task QueryPendingAsync(Func callback, CancellationToken cancellationToken = default(CancellationToken)); diff --git a/src/Squidex.Domain.Apps.Read/Rules/RuleDequeuer.cs b/src/Squidex.Domain.Apps.Read/Rules/RuleDequeuer.cs index bcaa3d718..3edb332c4 100644 --- a/src/Squidex.Domain.Apps.Read/Rules/RuleDequeuer.cs +++ b/src/Squidex.Domain.Apps.Read/Rules/RuleDequeuer.cs @@ -110,7 +110,7 @@ namespace Squidex.Domain.Apps.Read.Rules { var job = @event.Job; - var response = await ruleService.InvokeAsync(job.ActionName, job.Details); + var response = await ruleService.InvokeAsync(job.ActionName, job.ActionData); Instant? nextCall = null; @@ -133,7 +133,7 @@ namespace Squidex.Domain.Apps.Read.Rules } } - await ruleEventRepository.TraceSentAsync(@event.Id, response.Dump, response.Result, response.Elapsed, nextCall); + await ruleEventRepository.MarkSentAsync(@event.Id, response.Dump, response.Result, response.Elapsed, nextCall); } catch (Exception ex) { diff --git a/src/Squidex.Domain.Apps.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs b/src/Squidex.Domain.Apps.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs index e7d0ef9e1..ab0a609bd 100644 --- a/src/Squidex.Domain.Apps.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs +++ b/src/Squidex.Domain.Apps.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs @@ -11,7 +11,6 @@ using System.Threading.Tasks; using Microsoft.Extensions.Caching.Memory; using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Read.Schemas.Repositories; -using Squidex.Domain.Apps.Read.Utils; using Squidex.Infrastructure; using Squidex.Infrastructure.Caching; using Squidex.Infrastructure.CQRS.Events; diff --git a/src/Squidex.Domain.Apps.Read/Webhooks/IWebhookEntity.cs b/src/Squidex.Domain.Apps.Read/Webhooks/IWebhookEntity.cs deleted file mode 100644 index 62ba20035..000000000 --- a/src/Squidex.Domain.Apps.Read/Webhooks/IWebhookEntity.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ========================================================================== -// IWebhookEntity.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using Squidex.Domain.Apps.Core.Webhooks; - -namespace Squidex.Domain.Apps.Read.Webhooks -{ - public interface IWebhookEntity : IAppRefEntity, IEntityWithCreatedBy, IEntityWithLastModifiedBy, IEntityWithVersion - { - Uri Url { get; } - - long TotalSucceeded { get; } - - long TotalFailed { get; } - - long TotalTimedout { get; } - - long TotalRequestTime { get; } - - string SharedSecret { get; } - - IEnumerable Schemas { get; } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Webhooks/IWebhookEventEntity.cs b/src/Squidex.Domain.Apps.Read/Webhooks/IWebhookEventEntity.cs deleted file mode 100644 index e35b23747..000000000 --- a/src/Squidex.Domain.Apps.Read/Webhooks/IWebhookEventEntity.cs +++ /dev/null @@ -1,27 +0,0 @@ -// ========================================================================== -// IWebhookEventEntity.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using NodaTime; - -namespace Squidex.Domain.Apps.Read.Webhooks -{ - public interface IWebhookEventEntity : IEntity - { - WebhookJob Job { get; } - - Instant? NextAttempt { get; } - - WebhookResult Result { get; } - - WebhookJobResult JobResult { get; } - - int NumCalls { get; } - - string LastDump { get; } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Webhooks/Repositories/IWebhookEventRepository.cs b/src/Squidex.Domain.Apps.Read/Webhooks/Repositories/IWebhookEventRepository.cs deleted file mode 100644 index 26d6d0ffa..000000000 --- a/src/Squidex.Domain.Apps.Read/Webhooks/Repositories/IWebhookEventRepository.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ========================================================================== -// IWebhookEventRepository.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; - -namespace Squidex.Domain.Apps.Read.Webhooks.Repositories -{ - public interface IWebhookEventRepository - { - Task EnqueueAsync(WebhookJob job, Instant nextAttempt); - - Task EnqueueAsync(Guid id, Instant nextAttempt); - - Task TraceSendingAsync(Guid jobId); - - Task TraceSentAsync(Guid jobId, string dump, WebhookResult 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/Webhooks/Repositories/IWebhookRepository.cs b/src/Squidex.Domain.Apps.Read/Webhooks/Repositories/IWebhookRepository.cs deleted file mode 100644 index b0a4dde1f..000000000 --- a/src/Squidex.Domain.Apps.Read/Webhooks/Repositories/IWebhookRepository.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ========================================================================== -// ISchemaWebhookRepository.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.Webhooks.Repositories -{ - public interface IWebhookRepository - { - Task TraceSentAsync(Guid webhookId, WebhookResult result, TimeSpan elapsed); - - Task> QueryByAppAsync(Guid appId); - - Task> QueryCachedByAppAsync(Guid appId); - } -} diff --git a/src/Squidex.Domain.Apps.Read/Webhooks/WebhookDequeuer.cs b/src/Squidex.Domain.Apps.Read/Webhooks/WebhookDequeuer.cs deleted file mode 100644 index 1d585018d..000000000 --- a/src/Squidex.Domain.Apps.Read/Webhooks/WebhookDequeuer.cs +++ /dev/null @@ -1,160 +0,0 @@ -// ========================================================================== -// WebhookDequeuer.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.Read.Webhooks.Repositories; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Timers; - -namespace Squidex.Domain.Apps.Read.Webhooks -{ - public sealed class WebhookDequeuer : DisposableObjectBase, IExternalSystem - { - private readonly ActionBlock requestBlock; - private readonly TransformBlock blockBlock; - private readonly IWebhookEventRepository webhookEventRepository; - private readonly IWebhookRepository webhookRepository; - private readonly WebhookSender webhookSender; - private readonly CompletionTimer timer; - private readonly ISemanticLog log; - private readonly IClock clock; - - public WebhookDequeuer(WebhookSender webhookSender, - IWebhookEventRepository webhookEventRepository, - IWebhookRepository webhookRepository, - IClock clock, - ISemanticLog log) - { - Guard.NotNull(webhookEventRepository, nameof(webhookEventRepository)); - Guard.NotNull(webhookRepository, nameof(webhookRepository)); - Guard.NotNull(webhookSender, nameof(webhookSender)); - Guard.NotNull(clock, nameof(clock)); - Guard.NotNull(log, nameof(log)); - - this.webhookEventRepository = webhookEventRepository; - this.webhookRepository = webhookRepository; - this.webhookSender = webhookSender; - - this.clock = clock; - - 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 webhookEventRepository.QueryPendingAsync(blockBlock.SendAsync, cancellationToken); - } - catch (Exception ex) - { - log.LogError(ex, w => w - .WriteProperty("action", "QueueWebhookEvents") - .WriteProperty("status", "Failed")); - } - } - - private async Task BlockAsync(IWebhookEventEntity @event) - { - try - { - await webhookEventRepository.TraceSendingAsync(@event.Id); - - return @event; - } - catch (Exception ex) - { - log.LogError(ex, w => w - .WriteProperty("action", "BlockWebhookEvent") - .WriteProperty("status", "Failed")); - - throw; - } - } - - private async Task MakeRequestAsync(IWebhookEventEntity @event) - { - try - { - var response = await webhookSender.SendAsync(@event.Job); - - Instant? nextCall = null; - - if (response.Result != WebhookResult.Success) - { - var now = clock.GetCurrentInstant(); - - switch (@event.NumCalls) - { - case 0: - nextCall = now.Plus(Duration.FromMinutes(5)); - break; - case 1: - nextCall = now.Plus(Duration.FromHours(1)); - break; - case 2: - nextCall = now.Plus(Duration.FromHours(5)); - break; - case 3: - nextCall = now.Plus(Duration.FromHours(6)); - break; - } - } - - await Task.WhenAll( - webhookRepository.TraceSentAsync(@event.Job.WebhookId, response.Result, response.Elapsed), - webhookEventRepository.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/Webhooks/WebhookEnqueuer.cs b/src/Squidex.Domain.Apps.Read/Webhooks/WebhookEnqueuer.cs deleted file mode 100644 index 0e3be5546..000000000 --- a/src/Squidex.Domain.Apps.Read/Webhooks/WebhookEnqueuer.cs +++ /dev/null @@ -1,140 +0,0 @@ -// ========================================================================== -// WebhookEnqueuer.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Linq; -using System.Threading.Tasks; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using NodaTime; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Webhooks; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Domain.Apps.Read.Webhooks.Repositories; -using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS.Events; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Read.Webhooks -{ - public sealed class WebhookEnqueuer : IEventConsumer - { - private const string ContentPrefix = "Content"; - private static readonly Duration ExpirationTime = Duration.FromDays(2); - private readonly IWebhookEventRepository webhookEventRepository; - private readonly IWebhookRepository webhookRepository; - private readonly IClock clock; - private readonly TypeNameRegistry typeNameRegistry; - private readonly JsonSerializer webhookSerializer; - - public string Name - { - get { return GetType().Name; } - } - - public string EventsFilter - { - get { return "^content-"; } - } - - public WebhookEnqueuer(TypeNameRegistry typeNameRegistry, - IWebhookEventRepository webhookEventRepository, - IWebhookRepository webhookRepository, - IClock clock, - JsonSerializer webhookSerializer) - { - Guard.NotNull(webhookEventRepository, nameof(webhookEventRepository)); - Guard.NotNull(webhookSerializer, nameof(webhookSerializer)); - Guard.NotNull(webhookRepository, nameof(webhookRepository)); - Guard.NotNull(typeNameRegistry, nameof(typeNameRegistry)); - Guard.NotNull(clock, nameof(clock)); - - this.webhookEventRepository = webhookEventRepository; - this.webhookSerializer = webhookSerializer; - this.webhookRepository = webhookRepository; - - this.clock = clock; - - this.typeNameRegistry = typeNameRegistry; - } - - public Task ClearAsync() - { - return TaskHelper.Done; - } - - public async Task On(Envelope @event) - { - if (@event.Payload is ContentEvent contentEvent) - { - var eventType = typeNameRegistry.GetName(@event.Payload.GetType()); - - var webhooks = await webhookRepository.QueryCachedByAppAsync(contentEvent.AppId.Id); - - var matchingWebhooks = webhooks.Where(w => w.Schemas.Any(s => Matchs(s, contentEvent))).ToList(); - - if (matchingWebhooks.Count > 0) - { - var now = clock.GetCurrentInstant(); - - var eventPayload = CreatePayload(@event, eventType); - var eventName = $"{contentEvent.SchemaId.Name.ToPascalCase()}{CreateContentEventName(eventType)}"; - - foreach (var webhook in matchingWebhooks) - { - await EnqueueJobAsync(eventPayload, webhook, contentEvent, eventName, now); - } - } - } - } - - private async Task EnqueueJobAsync(string payload, IWebhookEntity webhook, AppEvent contentEvent, string eventName, Instant now) - { - var signature = $"{payload}{webhook.SharedSecret}".Sha256Base64(); - - var job = new WebhookJob - { - Id = Guid.NewGuid(), - AppId = contentEvent.AppId.Id, - RequestUrl = webhook.Url, - RequestBody = payload, - RequestSignature = signature, - EventName = eventName, - Expires = now.Plus(ExpirationTime), - WebhookId = webhook.Id - }; - - await webhookEventRepository.EnqueueAsync(job, now); - } - - private static bool Matchs(WebhookSchema webhookSchema, SchemaEvent @event) - { - return - (@event.SchemaId.Id == webhookSchema.SchemaId) && - (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); - } - - private string CreatePayload(Envelope @event, string eventType) - { - return new JObject( - new JProperty("type", eventType), - new JProperty("payload", JObject.FromObject(@event.Payload, webhookSerializer)), - new JProperty("timestamp", @event.Headers.Timestamp().ToString())) - .ToString(Formatting.Indented); - } - - private static string CreateContentEventName(string eventType) - { - return eventType.StartsWith(ContentPrefix, StringComparison.Ordinal) ? eventType.Substring(ContentPrefix.Length) : eventType; - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Webhooks/WebhookJob.cs b/src/Squidex.Domain.Apps.Read/Webhooks/WebhookJob.cs deleted file mode 100644 index 8951c3da6..000000000 --- a/src/Squidex.Domain.Apps.Read/Webhooks/WebhookJob.cs +++ /dev/null @@ -1,32 +0,0 @@ -// ========================================================================== -// WebhookJob.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using NodaTime; - -namespace Squidex.Domain.Apps.Read.Webhooks -{ - public sealed class WebhookJob - { - public Guid Id { get; set; } - - public Guid AppId { get; set; } - - public Guid WebhookId { get; set; } - - public Uri RequestUrl { get; set; } - - public string RequestBody { get; set; } - - public string RequestSignature { get; set; } - - public string EventName { get; set; } - - public Instant Expires { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Webhooks/WebhookSender.cs b/src/Squidex.Domain.Apps.Read/Webhooks/WebhookSender.cs deleted file mode 100644 index 6a1b07993..000000000 --- a/src/Squidex.Domain.Apps.Read/Webhooks/WebhookSender.cs +++ /dev/null @@ -1,98 +0,0 @@ -// ========================================================================== -// WebhookSender.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Diagnostics; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; -using Squidex.Infrastructure.Http; - -namespace Squidex.Domain.Apps.Read.Webhooks -{ - public class WebhookSender - { - private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(2); - - public virtual async Task<(string Dump, WebhookResult Result, TimeSpan Elapsed)> SendAsync(WebhookJob job) - { - try - { - var request = BuildRequest(job); - HttpResponseMessage response = null; - - var responseString = string.Empty; - - var isTimeout = false; - - var watch = Stopwatch.StartNew(); - - try - { - using (var client = new HttpClient { Timeout = Timeout }) - { - response = await client.SendAsync(request); - } - } - catch (TimeoutException) - { - isTimeout = true; - } - catch (OperationCanceledException) - { - isTimeout = true; - } - catch (Exception ex) - { - responseString = ex.Message; - } - finally - { - watch.Stop(); - } - - if (response != null) - { - responseString = await response.Content.ReadAsStringAsync(); - } - - var dump = DumpFormatter.BuildDump(request, response, job.RequestBody, responseString, watch.Elapsed, isTimeout); - - var result = WebhookResult.Failed; - - if (isTimeout) - { - result = WebhookResult.Timeout; - } - else if (response?.IsSuccessStatusCode == true) - { - result = WebhookResult.Success; - } - - return (dump, result, watch.Elapsed); - } - catch (Exception ex) - { - return (ex.Message, WebhookResult.Failed, TimeSpan.Zero); - } - } - - private static HttpRequestMessage BuildRequest(WebhookJob job) - { - 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("User-Agent", "Squidex Webhook"); - - return request; - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Webhooks/Commands/UpdateWebhook.cs b/src/Squidex.Domain.Apps.Write/Rules/Commands/CreateRule.cs similarity index 71% rename from src/Squidex.Domain.Apps.Write/Webhooks/Commands/UpdateWebhook.cs rename to src/Squidex.Domain.Apps.Write/Rules/Commands/CreateRule.cs index 22c33345f..34aaa4d21 100644 --- a/src/Squidex.Domain.Apps.Write/Webhooks/Commands/UpdateWebhook.cs +++ b/src/Squidex.Domain.Apps.Write/Rules/Commands/CreateRule.cs @@ -1,14 +1,14 @@ // ========================================================================== -// UpdateWebhook.cs +// CreateRule.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -namespace Squidex.Domain.Apps.Write.Webhooks.Commands +namespace Squidex.Domain.Apps.Write.Rules.Commands { - public sealed class UpdateWebhook : WebhookEditCommand + public sealed class CreateRule : RuleEditCommand { } } diff --git a/src/Squidex.Domain.Apps.Write/Webhooks/Commands/DeleteWebhook.cs b/src/Squidex.Domain.Apps.Write/Rules/Commands/DeleteRule.cs similarity index 70% rename from src/Squidex.Domain.Apps.Write/Webhooks/Commands/DeleteWebhook.cs rename to src/Squidex.Domain.Apps.Write/Rules/Commands/DeleteRule.cs index 462532dfe..c97f2c6b1 100644 --- a/src/Squidex.Domain.Apps.Write/Webhooks/Commands/DeleteWebhook.cs +++ b/src/Squidex.Domain.Apps.Write/Rules/Commands/DeleteRule.cs @@ -1,14 +1,14 @@ // ========================================================================== -// DeleteWebhook.cs +// DeleteRule.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -namespace Squidex.Domain.Apps.Write.Webhooks.Commands +namespace Squidex.Domain.Apps.Write.Rules.Commands { - public sealed class DeleteWebhook : WebhookAggregateCommand + public sealed class DeleteRule : RuleAggregateCommand { } } diff --git a/src/Squidex.Domain.Apps.Read/Webhooks/WebhookResult.cs b/src/Squidex.Domain.Apps.Write/Rules/Commands/DisableRule.cs similarity index 67% rename from src/Squidex.Domain.Apps.Read/Webhooks/WebhookResult.cs rename to src/Squidex.Domain.Apps.Write/Rules/Commands/DisableRule.cs index bc8584b5b..ccfa2a9be 100644 --- a/src/Squidex.Domain.Apps.Read/Webhooks/WebhookResult.cs +++ b/src/Squidex.Domain.Apps.Write/Rules/Commands/DisableRule.cs @@ -1,18 +1,14 @@ // ========================================================================== -// WebhookResult.cs +// DisableRule.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -namespace Squidex.Domain.Apps.Read.Webhooks +namespace Squidex.Domain.Apps.Write.Rules.Commands { - public enum WebhookResult + public sealed class DisableRule : RuleAggregateCommand { - Pending, - Success, - Failed, - Timeout } } diff --git a/src/Squidex.Domain.Apps.Read/Webhooks/WebhookJobResult.cs b/src/Squidex.Domain.Apps.Write/Rules/Commands/EnableRule.cs similarity index 67% rename from src/Squidex.Domain.Apps.Read/Webhooks/WebhookJobResult.cs rename to src/Squidex.Domain.Apps.Write/Rules/Commands/EnableRule.cs index 57d68fc19..ac3bf7f4c 100644 --- a/src/Squidex.Domain.Apps.Read/Webhooks/WebhookJobResult.cs +++ b/src/Squidex.Domain.Apps.Write/Rules/Commands/EnableRule.cs @@ -1,18 +1,14 @@ // ========================================================================== -// WebhookJobResult.cs +// EnableRule.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -namespace Squidex.Domain.Apps.Read.Webhooks +namespace Squidex.Domain.Apps.Write.Rules.Commands { - public enum WebhookJobResult + public sealed class EnableRule : RuleAggregateCommand { - Pending, - Success, - Retry, - Failed } } diff --git a/src/Squidex.Domain.Apps.Write/Webhooks/Commands/WebhookAggregateCommand.cs b/src/Squidex.Domain.Apps.Write/Rules/Commands/RuleAggregateCommand.cs similarity index 64% rename from src/Squidex.Domain.Apps.Write/Webhooks/Commands/WebhookAggregateCommand.cs rename to src/Squidex.Domain.Apps.Write/Rules/Commands/RuleAggregateCommand.cs index ec13cb8ef..4af936af5 100644 --- a/src/Squidex.Domain.Apps.Write/Webhooks/Commands/WebhookAggregateCommand.cs +++ b/src/Squidex.Domain.Apps.Write/Rules/Commands/RuleAggregateCommand.cs @@ -1,5 +1,5 @@ // ========================================================================== -// WebhookAggregateCommand.cs +// RuleAggregateCommand.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -9,15 +9,15 @@ using System; using Squidex.Infrastructure.CQRS.Commands; -namespace Squidex.Domain.Apps.Write.Webhooks.Commands +namespace Squidex.Domain.Apps.Write.Rules.Commands { - public abstract class WebhookAggregateCommand : AppCommand, IAggregateCommand + public abstract class RuleAggregateCommand : AppCommand, IAggregateCommand { - public Guid WebhookId { get; set; } + public Guid RuleId { get; set; } Guid IAggregateCommand.AggregateId { - get { return WebhookId; } + get { return RuleId; } } } } diff --git a/src/Squidex.Domain.Apps.Events/Webhooks/WebhookEvent.cs b/src/Squidex.Domain.Apps.Write/Rules/Commands/RuleEditCommand.cs similarity index 55% rename from src/Squidex.Domain.Apps.Events/Webhooks/WebhookEvent.cs rename to src/Squidex.Domain.Apps.Write/Rules/Commands/RuleEditCommand.cs index 99fc08697..b63be728a 100644 --- a/src/Squidex.Domain.Apps.Events/Webhooks/WebhookEvent.cs +++ b/src/Squidex.Domain.Apps.Write/Rules/Commands/RuleEditCommand.cs @@ -1,17 +1,19 @@ // ========================================================================== -// WebhookEvent.cs +// RuleEditCommand.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -using System; +using Squidex.Domain.Apps.Core.Rules; -namespace Squidex.Domain.Apps.Events.Webhooks +namespace Squidex.Domain.Apps.Write.Rules.Commands { - public abstract class WebhookEvent : AppEvent + public abstract class RuleEditCommand : RuleAggregateCommand { - public Guid WebhookId { get; set; } + public RuleTrigger Trigger { get; set; } + + public RuleAction Action { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Write/Rules/Commands/UpdateRule.cs b/src/Squidex.Domain.Apps.Write/Rules/Commands/UpdateRule.cs new file mode 100644 index 000000000..6fe43df4d --- /dev/null +++ b/src/Squidex.Domain.Apps.Write/Rules/Commands/UpdateRule.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// UpdateRule.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Write.Rules.Commands +{ + public sealed class UpdateRule : RuleEditCommand + { + } +} diff --git a/src/Squidex.Domain.Apps.Write/Rules/Guards/GuardRule.cs b/src/Squidex.Domain.Apps.Write/Rules/Guards/GuardRule.cs new file mode 100644 index 000000000..47a6cc129 --- /dev/null +++ b/src/Squidex.Domain.Apps.Write/Rules/Guards/GuardRule.cs @@ -0,0 +1,107 @@ +// ========================================================================== +// GuardRule.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Read.Schemas.Services; +using Squidex.Domain.Apps.Write.Rules.Commands; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Write.Rules.Guards +{ + public static class GuardRule + { + public static Task CanCreate(CreateRule command, ISchemaProvider schemas) + { + Guard.NotNull(command, nameof(command)); + + return Validate.It(() => "Cannot create rule.", async error => + { + if (command.Trigger == null) + { + error(new ValidationError("Trigger must be defined.", nameof(command.Trigger))); + } + else + { + var errors = await RuleTriggerValidator.ValidateAsync(command.Trigger, schemas); + + errors.Foreach(error); + } + + if (command.Action == null) + { + error(new ValidationError("Trigger must be defined.", nameof(command.Action))); + } + else + { + var errors = await RuleActionValidator.ValidateAsync(command.Action); + + errors.Foreach(error); + } + }); + } + + public static Task CanUpdate(UpdateRule command, ISchemaProvider schemas) + { + Guard.NotNull(command, nameof(command)); + + return Validate.It(() => "Cannot update rule.", async error => + { + if (command.Trigger == null && command.Action == null) + { + error(new ValidationError("Either trigger or action must be defined.", nameof(command.Trigger), nameof(command.Action))); + } + + if (command.Trigger != null) + { + var errors = await RuleTriggerValidator.ValidateAsync(command.Trigger, schemas); + + errors.Foreach(error); + } + + if (command.Action != null) + { + var errors = await RuleActionValidator.ValidateAsync(command.Action); + + errors.Foreach(error); + } + }); + } + + public static void CanEnable(EnableRule command, Rule rule) + { + Guard.NotNull(command, nameof(command)); + + Validate.It(() => "Cannot enable rule.", error => + { + if (rule.IsEnabled) + { + error(new ValidationError("Rule is already enabled.")); + } + }); + } + + public static void CanDisable(DisableRule command, Rule rule) + { + Guard.NotNull(command, nameof(command)); + + Validate.It(() => "Cannot disable rule.", error => + { + if (!rule.IsEnabled) + { + error(new ValidationError("Rule is already disabled.")); + } + }); + } + + public static void CanDelete(DeleteRule command) + { + Guard.NotNull(command, nameof(command)); + } + } +} diff --git a/src/Squidex.Domain.Apps.Write/Rules/Guards/RuleActionValidator.cs b/src/Squidex.Domain.Apps.Write/Rules/Guards/RuleActionValidator.cs new file mode 100644 index 000000000..4ead12956 --- /dev/null +++ b/src/Squidex.Domain.Apps.Write/Rules/Guards/RuleActionValidator.cs @@ -0,0 +1,40 @@ +// ========================================================================== +// RuleActionValidator.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Actions; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Write.Rules.Guards +{ + public sealed class RuleActionValidator : IRuleActionVisitor>> + { + public static Task> ValidateAsync(RuleAction action) + { + Guard.NotNull(action, nameof(action)); + + var visitor = new RuleActionValidator(); + + return action.Accept(visitor); + } + + public Task> Visit(WebhookAction action) + { + var errors = new List(); + + if (action.Url == null || !action.Url.IsAbsoluteUri) + { + errors.Add(new ValidationError("Url must be specified and absolute.", nameof(action.Url))); + } + + return Task.FromResult>(errors); + } + } +} diff --git a/src/Squidex.Domain.Apps.Write/Rules/Guards/RuleTriggerValidator.cs b/src/Squidex.Domain.Apps.Write/Rules/Guards/RuleTriggerValidator.cs new file mode 100644 index 000000000..f9594c26f --- /dev/null +++ b/src/Squidex.Domain.Apps.Write/Rules/Guards/RuleTriggerValidator.cs @@ -0,0 +1,54 @@ +// ========================================================================== +// RuleTriggerValidator.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Read.Schemas.Services; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Write.Rules.Guards +{ + public sealed class RuleTriggerValidator : IRuleTriggerVisitor>> + { + public ISchemaProvider Schemas { get; } + + public RuleTriggerValidator(ISchemaProvider schemas) + { + Schemas = schemas; + } + + public static Task> ValidateAsync(RuleTrigger action, ISchemaProvider schemas) + { + Guard.NotNull(action, nameof(action)); + Guard.NotNull(schemas, nameof(schemas)); + + var visitor = new RuleTriggerValidator(schemas); + + return action.Accept(visitor); + } + + public async Task> Visit(ContentChangedTrigger trigger) + { + if (trigger.Schemas != null) + { + var schemaErrors = await Task.WhenAll( + trigger.Schemas.Select(async s => + await Schemas.FindSchemaByIdAsync(s.SchemaId) == null + ? new ValidationError($"Schema {s.SchemaId} does not exist.", nameof(trigger.Schemas)) + : null)); + + return schemaErrors.Where(x => x != null).ToList(); + } + + return new List(); + } + } +} diff --git a/src/Squidex.Domain.Apps.Write/Rules/RuleCommandMiddleware.cs b/src/Squidex.Domain.Apps.Write/Rules/RuleCommandMiddleware.cs new file mode 100644 index 000000000..b0b2ed082 --- /dev/null +++ b/src/Squidex.Domain.Apps.Write/Rules/RuleCommandMiddleware.cs @@ -0,0 +1,92 @@ +// ========================================================================== +// RuleCommandMiddleware.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Read.Schemas.Services; +using Squidex.Domain.Apps.Write.Rules.Commands; +using Squidex.Domain.Apps.Write.Rules.Guards; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Commands; +using Squidex.Infrastructure.Dispatching; + +namespace Squidex.Domain.Apps.Write.Rules +{ + public class RuleCommandMiddleware : ICommandMiddleware + { + private readonly IAggregateHandler handler; + private readonly ISchemaProvider schemas; + + public RuleCommandMiddleware(IAggregateHandler handler, ISchemaProvider schemas) + { + Guard.NotNull(handler, nameof(handler)); + Guard.NotNull(schemas, nameof(schemas)); + + this.handler = handler; + this.schemas = schemas; + } + + protected Task On(CreateRule command, CommandContext context) + { + return handler.CreateAsync(context, async w => + { + await GuardRule.CanCreate(command, schemas); + + w.Create(command); + }); + } + + protected Task On(UpdateRule command, CommandContext context) + { + return handler.UpdateAsync(context, async c => + { + await GuardRule.CanUpdate(command, schemas); + + c.Update(command); + }); + } + + protected Task On(EnableRule command, CommandContext context) + { + return handler.UpdateAsync(context, r => + { + GuardRule.CanEnable(command, r.Rule); + + r.Enable(command); + }); + } + + protected Task On(DisableRule command, CommandContext context) + { + return handler.UpdateAsync(context, r => + { + GuardRule.CanDisable(command, r.Rule); + + r.Disable(command); + }); + } + + protected Task On(DeleteRule command, CommandContext context) + { + return handler.UpdateAsync(context, c => + { + GuardRule.CanDelete(command); + + c.Delete(command); + }); + } + + public async Task HandleAsync(CommandContext context, Func next) + { + if (!await this.DispatchActionAsync(context.Command, context)) + { + await next(); + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Write/Rules/RuleDomainObject.cs b/src/Squidex.Domain.Apps.Write/Rules/RuleDomainObject.cs new file mode 100644 index 000000000..ccf2e4a57 --- /dev/null +++ b/src/Squidex.Domain.Apps.Write/Rules/RuleDomainObject.cs @@ -0,0 +1,118 @@ +// ========================================================================== +// RuleDomainObject.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Events.Rules; +using Squidex.Domain.Apps.Events.Rules.Utils; +using Squidex.Domain.Apps.Write.Rules.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS; +using Squidex.Infrastructure.CQRS.Events; +using Squidex.Infrastructure.Dispatching; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Write.Rules +{ + public class RuleDomainObject : DomainObjectBase + { + private Rule rule; + private bool isDeleted; + + public Rule Rule + { + get { return rule; } + } + + public RuleDomainObject(Guid id, int version) + : base(id, version) + { + } + + protected void On(RuleCreated @event) + { + rule = RuleEventDispatcher.Create(@event); + } + + protected void On(RuleUpdated @event) + { + rule.Apply(@event); + } + + protected void On(RuleEnabled @event) + { + rule.Apply(@event); + } + + protected void On(RuleDisabled @event) + { + rule.Apply(@event); + } + + protected void On(RuleDeleted @event) + { + isDeleted = true; + } + + public void Create(CreateRule command) + { + VerifyNotCreated(); + + RaiseEvent(SimpleMapper.Map(command, new RuleCreated())); + } + + public void Update(UpdateRule command) + { + VerifyCreatedAndNotDeleted(); + + RaiseEvent(SimpleMapper.Map(command, new RuleUpdated())); + } + + public void Enable(EnableRule command) + { + VerifyCreatedAndNotDeleted(); + + RaiseEvent(SimpleMapper.Map(command, new RuleEnabled())); + } + + public void Disable(DisableRule command) + { + VerifyCreatedAndNotDeleted(); + + RaiseEvent(SimpleMapper.Map(command, new RuleDisabled())); + } + + public void Delete(DeleteRule command) + { + VerifyCreatedAndNotDeleted(); + + RaiseEvent(SimpleMapper.Map(command, new RuleDeleted())); + } + + private void VerifyNotCreated() + { + if (rule != null) + { + throw new DomainException("Webhook has already been created."); + } + } + + private void VerifyCreatedAndNotDeleted() + { + if (isDeleted || rule == null) + { + throw new DomainException("Webhook has already been deleted or not created yet."); + } + } + + protected override void DispatchEvent(Envelope @event) + { + this.DispatchAction(@event.Payload); + } + } +} diff --git a/src/Squidex.Domain.Apps.Write/Webhooks/Commands/CreateWebhook.cs b/src/Squidex.Domain.Apps.Write/Webhooks/Commands/CreateWebhook.cs deleted file mode 100644 index d6a76ea84..000000000 --- a/src/Squidex.Domain.Apps.Write/Webhooks/Commands/CreateWebhook.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ========================================================================== -// CreateWebhook.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Write.Webhooks.Commands -{ - public sealed class CreateWebhook : WebhookEditCommand - { - public string SharedSecret { get; } = RandomHash.New(); - - public CreateWebhook() - { - WebhookId = Guid.NewGuid(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Webhooks/Commands/WebhookEditCommand.cs b/src/Squidex.Domain.Apps.Write/Webhooks/Commands/WebhookEditCommand.cs deleted file mode 100644 index 2f8cc5d3f..000000000 --- a/src/Squidex.Domain.Apps.Write/Webhooks/Commands/WebhookEditCommand.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ========================================================================== -// WebhookEditCommand.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using Squidex.Domain.Apps.Core.Webhooks; - -namespace Squidex.Domain.Apps.Write.Webhooks.Commands -{ - public abstract class WebhookEditCommand : WebhookAggregateCommand - { - public Uri Url { get; set; } - - public List Schemas { get; set; } = new List(); - } -} diff --git a/src/Squidex.Domain.Apps.Write/Webhooks/Guards/GuardWebhook.cs b/src/Squidex.Domain.Apps.Write/Webhooks/Guards/GuardWebhook.cs deleted file mode 100644 index 13359a531..000000000 --- a/src/Squidex.Domain.Apps.Write/Webhooks/Guards/GuardWebhook.cs +++ /dev/null @@ -1,61 +0,0 @@ -// ========================================================================== -// GuardWebhook.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Read.Schemas.Services; -using Squidex.Domain.Apps.Write.Webhooks.Commands; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Write.Webhooks.Guards -{ - public static class GuardWebhook - { - public static Task CanCreate(CreateWebhook command, ISchemaProvider schemas) - { - Guard.NotNull(command, nameof(command)); - - return Validate.It(() => "Cannot create webhook.", error => ValidateCommandAsync(command, error, schemas)); - } - - public static Task CanUpdate(UpdateWebhook command, ISchemaProvider schemas) - { - Guard.NotNull(command, nameof(command)); - - return Validate.It(() => "Cannot update webhook.", error => ValidateCommandAsync(command, error, schemas)); - } - - public static void CanDelete(DeleteWebhook command) - { - Guard.NotNull(command, nameof(command)); - } - - private static async Task ValidateCommandAsync(WebhookEditCommand command, Action error, ISchemaProvider schemas) - { - if (command.Url == null || !command.Url.IsAbsoluteUri) - { - error(new ValidationError("Url must be specified and absolute.", nameof(command.Url))); - } - - if (command.Schemas != null) - { - var schemaErrors = await Task.WhenAll( - command.Schemas.Select(async s => - await schemas.FindSchemaByIdAsync(s.SchemaId) == null - ? new ValidationError($"Schema {s.SchemaId} does not exist.", nameof(command.Schemas)) - : null)); - - foreach (var schemaError in schemaErrors.Where(x => x != null)) - { - error(schemaError); - } - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Webhooks/WebhookCommandMiddleware.cs b/src/Squidex.Domain.Apps.Write/Webhooks/WebhookCommandMiddleware.cs deleted file mode 100644 index 13cee7474..000000000 --- a/src/Squidex.Domain.Apps.Write/Webhooks/WebhookCommandMiddleware.cs +++ /dev/null @@ -1,72 +0,0 @@ -// ========================================================================== -// WebhookCommandMiddleware.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Read.Schemas.Services; -using Squidex.Domain.Apps.Write.Webhooks.Commands; -using Squidex.Domain.Apps.Write.Webhooks.Guards; -using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS.Commands; -using Squidex.Infrastructure.Dispatching; - -namespace Squidex.Domain.Apps.Write.Webhooks -{ - public class WebhookCommandMiddleware : ICommandMiddleware - { - private readonly IAggregateHandler handler; - private readonly ISchemaProvider schemas; - - public WebhookCommandMiddleware(IAggregateHandler handler, ISchemaProvider schemas) - { - Guard.NotNull(handler, nameof(handler)); - Guard.NotNull(schemas, nameof(schemas)); - - this.handler = handler; - this.schemas = schemas; - } - - protected async Task On(CreateWebhook command, CommandContext context) - { - await handler.CreateAsync(context, async w => - { - await GuardWebhook.CanCreate(command, schemas); - - w.Create(command); - }); - } - - protected async Task On(UpdateWebhook command, CommandContext context) - { - await handler.UpdateAsync(context, async c => - { - await GuardWebhook.CanUpdate(command, schemas); - - c.Update(command); - }); - } - - protected Task On(DeleteWebhook command, CommandContext context) - { - return handler.UpdateAsync(context, c => - { - GuardWebhook.CanDelete(command); - - c.Delete(command); - }); - } - - public async Task HandleAsync(CommandContext context, Func next) - { - if (!await this.DispatchActionAsync(context.Command, context)) - { - await next(); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Webhooks/WebhookDomainObject.cs b/src/Squidex.Domain.Apps.Write/Webhooks/WebhookDomainObject.cs deleted file mode 100644 index fee952cf7..000000000 --- a/src/Squidex.Domain.Apps.Write/Webhooks/WebhookDomainObject.cs +++ /dev/null @@ -1,82 +0,0 @@ -// ========================================================================== -// WebhookDomainObject.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Squidex.Domain.Apps.Events.Webhooks; -using Squidex.Domain.Apps.Write.Webhooks.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS; -using Squidex.Infrastructure.CQRS.Events; -using Squidex.Infrastructure.Dispatching; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Write.Webhooks -{ - public class WebhookDomainObject : DomainObjectBase - { - private bool isDeleted; - private bool isCreated; - - public WebhookDomainObject(Guid id, int version) - : base(id, version) - { - } - - protected void On(WebhookCreated @event) - { - isCreated = true; - } - - protected void On(WebhookDeleted @event) - { - isDeleted = true; - } - - public void Create(CreateWebhook command) - { - VerifyNotCreated(); - - RaiseEvent(SimpleMapper.Map(command, new WebhookCreated())); - } - - public void Update(UpdateWebhook command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(SimpleMapper.Map(command, new WebhookUpdated())); - } - - public void Delete(DeleteWebhook command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(SimpleMapper.Map(command, new WebhookDeleted())); - } - - private void VerifyNotCreated() - { - if (isCreated) - { - throw new DomainException("Webhook has already been created."); - } - } - - private void VerifyCreatedAndNotDeleted() - { - if (isDeleted || !isCreated) - { - throw new DomainException("Webhook has already been deleted or not created yet."); - } - } - - protected override void DispatchEvent(Envelope @event) - { - this.DispatchAction(@event.Payload); - } - } -} diff --git a/src/Squidex/Squidex.csproj b/src/Squidex/Squidex.csproj index a7ffd5aa4..eff3313f2 100644 --- a/src/Squidex/Squidex.csproj +++ b/src/Squidex/Squidex.csproj @@ -62,11 +62,11 @@ - + - + diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Rules/Guards/Actions/WebhookActionTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Rules/Guards/Actions/WebhookActionTests.cs new file mode 100644 index 000000000..fe3487cce --- /dev/null +++ b/tests/Squidex.Domain.Apps.Write.Tests/Rules/Guards/Actions/WebhookActionTests.cs @@ -0,0 +1,48 @@ +// ========================================================================== +// WebhookActionTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Rules.Actions; +using Xunit; + +namespace Squidex.Domain.Apps.Write.Rules.Guards.Actions +{ + public sealed class WebhookActionTests + { + [Fact] + public async Task Should_add_error_if_url_is_null() + { + var action = new WebhookAction { Url = null }; + + var errors = await RuleActionValidator.ValidateAsync(action); + + Assert.NotEmpty(errors); + } + + [Fact] + public async Task Should_add_error_if_url_is_relative() + { + var action = new WebhookAction { Url = new Uri("/invalid", UriKind.Relative) }; + + var errors = await RuleActionValidator.ValidateAsync(action); + + Assert.NotEmpty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_url_is_absolute() + { + var action = new WebhookAction { Url = new Uri("https://squidex.io", UriKind.Absolute) }; + + var errors = await RuleActionValidator.ValidateAsync(action); + + Assert.Empty(errors); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Rules/Guards/GuardRuleTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Rules/Guards/GuardRuleTests.cs new file mode 100644 index 000000000..1699e64d8 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Write.Tests/Rules/Guards/GuardRuleTests.cs @@ -0,0 +1,158 @@ +// ========================================================================== +// GuardRuleTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Actions; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Read.Schemas; +using Squidex.Domain.Apps.Read.Schemas.Services; +using Squidex.Domain.Apps.Write.Rules.Commands; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Write.Rules.Guards +{ + public class GuardRuleTests + { + private readonly Uri validUrl = new Uri("https://squidex.io"); + private readonly Rule rule = new Rule(new ContentChangedTrigger(), new WebhookAction()); + private readonly ISchemaProvider schemas = A.Fake(); + + public GuardRuleTests() + { + A.CallTo(() => schemas.FindSchemaByIdAsync(A.Ignored, false)) + .Returns(A.Fake()); + } + + [Fact] + public async Task CanCreate_should_throw_exception_if_trigger_null() + { + var command = new CreateRule + { + Trigger = null, + Action = new WebhookAction + { + Url = validUrl + } + }; + + await Assert.ThrowsAsync(() => GuardRule.CanCreate(command, schemas)); + } + + [Fact] + public async Task CanCreate_should_throw_exception_if_action_null() + { + var command = new CreateRule + { + Trigger = new ContentChangedTrigger + { + Schemas = new List() + }, + Action = null + }; + + await Assert.ThrowsAsync(() => GuardRule.CanCreate(command, schemas)); + } + + [Fact] + public async Task CanCreate_should_not_throw_exception_if_trigger_and_action_valid() + { + var command = new CreateRule + { + Trigger = new ContentChangedTrigger + { + Schemas = new List() + }, + Action = new WebhookAction + { + Url = validUrl + } + }; + + await GuardRule.CanCreate(command, schemas); + } + + [Fact] + public async Task CanUpdate_should_throw_exception_if_action_and_trigger_are_null() + { + var command = new UpdateRule(); + + await Assert.ThrowsAsync(() => GuardRule.CanUpdate(command, schemas)); + } + + [Fact] + public async Task CanUpdate_should_not_throw_exception_if_trigger_and_action_valid() + { + var command = new UpdateRule + { + Trigger = new ContentChangedTrigger + { + Schemas = new List() + }, + Action = new WebhookAction + { + Url = validUrl + } + }; + + await GuardRule.CanUpdate(command, schemas); + } + + [Fact] + public void CanEnable_should_throw_exception_if_rule_enabled() + { + var command = new EnableRule(); + + rule.Enable(); + + Assert.Throws(() => GuardRule.CanEnable(command, rule)); + } + + [Fact] + public void CanEnable_should_not_throw_exception_if_rule_disabled() + { + var command = new EnableRule(); + + rule.Disable(); + + GuardRule.CanEnable(command, rule); + } + + [Fact] + public void CanDisable_should_throw_exception_if_rule_disabled() + { + var command = new DisableRule(); + + rule.Disable(); + + Assert.Throws(() => GuardRule.CanDisable(command, rule)); + } + + [Fact] + public void CanDisable_should_not_throw_exception_if_rule_enabled() + { + var command = new DisableRule(); + + rule.Enable(); + + GuardRule.CanDisable(command, rule); + } + + [Fact] + public void CanDelete_should_not_throw_exception() + { + var command = new DeleteRule(); + + GuardRule.CanDelete(command); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs new file mode 100644 index 000000000..87cfdec3d --- /dev/null +++ b/tests/Squidex.Domain.Apps.Write.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs @@ -0,0 +1,85 @@ +// ========================================================================== +// ContentChangedTriggerTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Read.Schemas; +using Squidex.Domain.Apps.Read.Schemas.Services; +using Xunit; + +namespace Squidex.Domain.Apps.Write.Rules.Guards.Triggers +{ + public class ContentChangedTriggerTests + { + private readonly ISchemaProvider schemas = A.Fake(); + + [Fact] + public async Task Should_add_error_if_schemas_ids_are_not_valid() + { + A.CallTo(() => schemas.FindSchemaByIdAsync(A.Ignored, false)) + .Returns(Task.FromResult(null)); + + var trigger = new ContentChangedTrigger + { + Schemas = new List + { + new ContentChangedTriggerSchema() + } + }; + + var errors = await RuleTriggerValidator.ValidateAsync(trigger, schemas); + + Assert.NotEmpty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_schemas_is_null() + { + var trigger = new ContentChangedTrigger(); + + var errors = await RuleTriggerValidator.ValidateAsync(trigger, schemas); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_schemas_is_empty() + { + var trigger = new ContentChangedTrigger + { + Schemas = new List() + }; + + var errors = await RuleTriggerValidator.ValidateAsync(trigger, schemas); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_schemas_ids_are_valid() + { + A.CallTo(() => schemas.FindSchemaByIdAsync(A.Ignored, false)) + .Returns(A.Fake()); + + var trigger = new ContentChangedTrigger + { + Schemas = new List + { + new ContentChangedTriggerSchema() + } + }; + + var errors = await RuleTriggerValidator.ValidateAsync(trigger, schemas); + + Assert.Empty(errors); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Rules/RuleCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Rules/RuleCommandMiddlewareTests.cs new file mode 100644 index 000000000..87d38c924 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Write.Tests/Rules/RuleCommandMiddlewareTests.cs @@ -0,0 +1,117 @@ +// ========================================================================== +// RuleCommandMiddlewareTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Actions; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Read.Schemas; +using Squidex.Domain.Apps.Read.Schemas.Services; +using Squidex.Domain.Apps.Write.Rules.Commands; +using Squidex.Domain.Apps.Write.TestHelpers; +using Squidex.Infrastructure.CQRS.Commands; +using Xunit; + +namespace Squidex.Domain.Apps.Write.Rules +{ + public class RuleCommandMiddlewareTests : HandlerTestBase + { + private readonly ISchemaProvider schemas = A.Fake(); + private readonly RuleCommandMiddleware sut; + private readonly RuleDomainObject rule; + private readonly RuleTrigger ruleTrigger = new ContentChangedTrigger(); + private readonly RuleAction ruleAction = new WebhookAction { Url = new Uri("https://squidex.io") }; + private readonly Guid ruleId = Guid.NewGuid(); + + public RuleCommandMiddlewareTests() + { + A.CallTo(() => schemas.FindSchemaByIdAsync(A.Ignored, false)) + .Returns(A.Fake()); + + rule = new RuleDomainObject(ruleId, -1); + + sut = new RuleCommandMiddleware(Handler, schemas); + } + + [Fact] + public async Task Create_should_create_domain_object() + { + var context = CreateContextForCommand(new CreateRule { Trigger = ruleTrigger, Action = ruleAction }); + + await TestCreate(rule, async _ => + { + await sut.HandleAsync(context); + }); + } + + [Fact] + public async Task Update_should_update_domain_object() + { + var context = CreateContextForCommand(new UpdateRule { Trigger = ruleTrigger, Action = ruleAction }); + + CreateRule(); + + await TestUpdate(rule, async _ => + { + await sut.HandleAsync(context); + }); + } + + [Fact] + public async Task Enable_should_update_domain_object() + { + CreateRule(); + DisableRule(); + + var command = CreateContextForCommand(new EnableRule { RuleId = ruleId }); + + await TestUpdate(rule, async _ => + { + await sut.HandleAsync(command); + }); + } + + [Fact] + public async Task Disable_should_update_domain_object() + { + CreateRule(); + + var command = CreateContextForCommand(new DisableRule { RuleId = ruleId }); + + await TestUpdate(rule, async _ => + { + await sut.HandleAsync(command); + }); + } + + [Fact] + public async Task Delete_should_update_domain_object() + { + CreateRule(); + + var command = CreateContextForCommand(new DeleteRule { RuleId = ruleId }); + + await TestUpdate(rule, async _ => + { + await sut.HandleAsync(command); + }); + } + + private void DisableRule() + { + rule.Disable(new DisableRule()); + } + + private void CreateRule() + { + rule.Create(new CreateRule { Trigger = ruleTrigger, Action = ruleAction }); + } + } +} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Rules/RuleDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Rules/RuleDomainObjectTests.cs new file mode 100644 index 000000000..2bc45e1b3 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Write.Tests/Rules/RuleDomainObjectTests.cs @@ -0,0 +1,240 @@ +// ========================================================================== +// RuleDomainObjectTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +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.Rules; +using Squidex.Domain.Apps.Write.Rules.Commands; +using Squidex.Domain.Apps.Write.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS; +using Xunit; + +namespace Squidex.Domain.Apps.Write.Rules +{ + public class RuleDomainObjectTests : HandlerTestBase + { + private readonly RuleTrigger ruleTrigger = new ContentChangedTrigger(); + private readonly RuleAction ruleAction = new WebhookAction { Url = new Uri("https://squidex.io") }; + private readonly RuleDomainObject sut; + + public Guid RuleId { get; } = Guid.NewGuid(); + + public RuleDomainObjectTests() + { + sut = new RuleDomainObject(RuleId, 0); + } + + [Fact] + public void Create_should_throw_exception_if_created() + { + sut.Create(new CreateRule { Trigger = ruleTrigger, Action = ruleAction }); + + Assert.Throws(() => + { + sut.Create(CreateRuleCommand(new CreateRule { Trigger = ruleTrigger, Action = ruleAction })); + }); + } + + [Fact] + public void Create_should_create_events() + { + var command = new CreateRule { Trigger = ruleTrigger, Action = ruleAction }; + + sut.Create(CreateRuleCommand(command)); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateRuleEvent(new RuleCreated { Trigger = ruleTrigger, Action = ruleAction }) + ); + } + + [Fact] + public void Update_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.Update(CreateRuleCommand(new UpdateRule { Trigger = ruleTrigger, Action = ruleAction })); + }); + } + + [Fact] + public void Update_should_throw_exception_if_rule_is_deleted() + { + CreateRule(); + DeleteRule(); + + Assert.Throws(() => + { + sut.Update(CreateRuleCommand(new UpdateRule { Trigger = ruleTrigger, Action = ruleAction })); + }); + } + + [Fact] + public void Update_should_create_events() + { + var newTrigger = new ContentChangedTrigger + { + Schemas = new List() + }; + + var newAction = new WebhookAction + { + Url = new Uri("https://squidex.io/v2") + }; + + CreateRule(); + + var command = new UpdateRule { Trigger = newTrigger, Action = newAction }; + + sut.Update(CreateRuleCommand(command)); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateRuleEvent(new RuleUpdated { Trigger = newTrigger, Action = newAction }) + ); + } + + [Fact] + public void Enable_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.Enable(CreateRuleCommand(new EnableRule())); + }); + } + + [Fact] + public void Enable_should_throw_exception_if_rule_is_deleted() + { + CreateRule(); + DeleteRule(); + + Assert.Throws(() => + { + sut.Enable(CreateRuleCommand(new EnableRule())); + }); + } + + [Fact] + public void Enable_should_create_events() + { + CreateRule(); + + var command = new EnableRule(); + + sut.Enable(CreateRuleCommand(command)); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateRuleEvent(new RuleEnabled()) + ); + } + + [Fact] + public void Disable_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.Disable(CreateRuleCommand(new DisableRule())); + }); + } + + [Fact] + public void Disable_should_throw_exception_if_rule_is_deleted() + { + CreateRule(); + DeleteRule(); + + Assert.Throws(() => + { + sut.Disable(CreateRuleCommand(new DisableRule())); + }); + } + + [Fact] + public void Disable_should_create_events() + { + CreateRule(); + + var command = new DisableRule(); + + sut.Disable(CreateRuleCommand(command)); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateRuleEvent(new RuleDisabled()) + ); + } + + [Fact] + public void Delete_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.Delete(CreateRuleCommand(new DeleteRule())); + }); + } + + [Fact] + public void Delete_should_throw_exception_if_already_deleted() + { + CreateRule(); + DeleteRule(); + + Assert.Throws(() => + { + sut.Delete(CreateRuleCommand(new DeleteRule())); + }); + } + + [Fact] + public void Delete_should_update_create_events() + { + CreateRule(); + + sut.Delete(CreateRuleCommand(new DeleteRule())); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateRuleEvent(new RuleDeleted()) + ); + } + + private void CreateRule() + { + sut.Create(CreateRuleCommand(new CreateRule { Trigger = ruleTrigger, Action = ruleAction })); + + ((IAggregate)sut).ClearUncommittedEvents(); + } + + private void DeleteRule() + { + sut.Delete(CreateRuleCommand(new DeleteRule())); + + ((IAggregate)sut).ClearUncommittedEvents(); + } + + protected T CreateRuleEvent(T @event) where T : RuleEvent + { + @event.RuleId = RuleId; + + return CreateEvent(@event); + } + + protected T CreateRuleCommand(T command) where T : RuleAggregateCommand + { + command.RuleId = RuleId; + + return CreateCommand(command); + } + } +} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Webhooks/Guards/GuardWebhookTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Webhooks/Guards/GuardWebhookTests.cs deleted file mode 100644 index 567e4a241..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Webhooks/Guards/GuardWebhookTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -// ========================================================================== -// GuardWebhookTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Core.Webhooks; -using Squidex.Domain.Apps.Read.Schemas; -using Squidex.Domain.Apps.Read.Schemas.Services; -using Squidex.Domain.Apps.Write.Webhooks.Commands; -using Squidex.Infrastructure; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Webhooks.Guards -{ - public class GuardWebhookTests - { - private readonly ISchemaProvider schemas = A.Fake(); - - public GuardWebhookTests() - { - A.CallTo(() => schemas.FindSchemaByIdAsync(A.Ignored, false)) - .Returns(A.Fake()); - } - - [Fact] - public async Task CanCreate_should_throw_exception_if_url_defined() - { - var command = new CreateWebhook(); - - await Assert.ThrowsAsync(() => GuardWebhook.CanCreate(command, schemas)); - } - - [Fact] - public async Task CanCreate_should_throw_exception_if_url_not_valid() - { - var command = new CreateWebhook { Url = new Uri("/invalid", UriKind.Relative) }; - - await Assert.ThrowsAsync(() => GuardWebhook.CanCreate(command, schemas)); - } - - [Fact] - public async Task CanCreate_should_throw_exception_if_schema_id_not_found() - { - A.CallTo(() => schemas.FindSchemaByIdAsync(A.Ignored, false)) - .Returns(Task.FromResult(null)); - - var command = new CreateWebhook - { - Schemas = new List - { - new WebhookSchema() - }, - Url = new Uri("/invalid", UriKind.Relative) - }; - - await Assert.ThrowsAsync(() => GuardWebhook.CanCreate(command, schemas)); - } - - [Fact] - public async Task CanCreate_should_not_throw_exception_if_schema_id_found() - { - var command = new CreateWebhook - { - Schemas = new List - { - new WebhookSchema() - }, - Url = new Uri("/invalid", UriKind.Relative) - }; - - await Assert.ThrowsAsync(() => GuardWebhook.CanCreate(command, schemas)); - } - - [Fact] - public async Task CanUpdate_should_throw_exception_if_url_not_defined() - { - var command = new UpdateWebhook(); - - await Assert.ThrowsAsync(() => GuardWebhook.CanUpdate(command, schemas)); - } - - [Fact] - public async Task CanUpdate_should_throw_exception_if_url_not_valid() - { - var command = new UpdateWebhook { Url = new Uri("/invalid", UriKind.Relative) }; - - await Assert.ThrowsAsync(() => GuardWebhook.CanUpdate(command, schemas)); - } - - [Fact] - public async Task CanUpdate_should_throw_exception_if_schema_id_not_found() - { - A.CallTo(() => schemas.FindSchemaByIdAsync(A.Ignored, false)) - .Returns(Task.FromResult(null)); - - var command = new UpdateWebhook - { - Schemas = new List - { - new WebhookSchema() - }, - Url = new Uri("/invalid", UriKind.Relative) - }; - - await Assert.ThrowsAsync(() => GuardWebhook.CanUpdate(command, schemas)); - } - - [Fact] - public async Task CanUpdate_should_not_throw_exception_if_schema_id_found() - { - var command = new UpdateWebhook - { - Schemas = new List - { - new WebhookSchema() - }, - Url = new Uri("/invalid", UriKind.Relative) - }; - - await Assert.ThrowsAsync(() => GuardWebhook.CanUpdate(command, schemas)); - } - - [Fact] - public void CanDelete_should_not_throw_exception() - { - var command = new DeleteWebhook(); - - GuardWebhook.CanDelete(command); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Webhooks/WebhookCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Webhooks/WebhookCommandMiddlewareTests.cs deleted file mode 100644 index 37e786b7d..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Webhooks/WebhookCommandMiddlewareTests.cs +++ /dev/null @@ -1,115 +0,0 @@ -// ========================================================================== -// WebhookCommandMiddlewareTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Core.Webhooks; -using Squidex.Domain.Apps.Read.Schemas; -using Squidex.Domain.Apps.Read.Schemas.Services; -using Squidex.Domain.Apps.Write.TestHelpers; -using Squidex.Domain.Apps.Write.Webhooks.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS.Commands; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Webhooks -{ - public class WebhookCommandMiddlewareTests : HandlerTestBase - { - private readonly ISchemaProvider schemas = A.Fake(); - private readonly WebhookCommandMiddleware sut; - private readonly WebhookDomainObject webhook; - private readonly Uri url = new Uri("http://squidex.io"); - private readonly Guid schemaId = Guid.NewGuid(); - private readonly Guid webhookId = Guid.NewGuid(); - private readonly List webhookSchemas; - - public WebhookCommandMiddlewareTests() - { - A.CallTo(() => schemas.FindSchemaByIdAsync(schemaId, false)) - .Returns(A.Fake()); - - webhook = new WebhookDomainObject(webhookId, -1); - - webhookSchemas = new List - { - new WebhookSchema { SchemaId = schemaId } - }; - - sut = new WebhookCommandMiddleware(Handler, schemas); - } - - [Fact] - public async Task Create_should_create_domain_object() - { - var context = CreateContextForCommand(new CreateWebhook { Schemas = webhookSchemas, Url = url, WebhookId = webhookId }); - - await TestCreate(webhook, async _ => - { - await sut.HandleAsync(context); - }); - - A.CallTo(() => schemas.FindSchemaByIdAsync(schemaId, false)).MustHaveHappened(); - } - - [Fact] - public async Task Update_should_update_domain_object() - { - var context = CreateContextForCommand(new UpdateWebhook { Schemas = webhookSchemas, Url = url, WebhookId = webhookId }); - - A.CallTo(() => schemas.FindSchemaByIdAsync(schemaId, false)).Returns(A.Fake()); - - CreateWebhook(); - - await TestUpdate(webhook, async _ => - { - await sut.HandleAsync(context); - }); - - A.CallTo(() => schemas.FindSchemaByIdAsync(schemaId, false)).MustHaveHappened(); - } - - [Fact] - public async Task Update_should_throw_exception_when_schema_is_not_found() - { - var context = CreateContextForCommand(new UpdateWebhook { Schemas = webhookSchemas, Url = url, WebhookId = webhookId }); - - A.CallTo(() => schemas.FindSchemaByIdAsync(schemaId, false)).Returns((ISchemaEntity)null); - - CreateWebhook(); - - await Assert.ThrowsAsync(async () => - { - await TestCreate(webhook, async _ => - { - await sut.HandleAsync(context); - }); - }); - } - - [Fact] - public async Task Delete_should_update_domain_object() - { - CreateWebhook(); - - var command = CreateContextForCommand(new DeleteWebhook { WebhookId = webhookId }); - - await TestUpdate(webhook, async _ => - { - await sut.HandleAsync(command); - }); - } - - private void CreateWebhook() - { - webhook.Create(new CreateWebhook { Url = url }); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Webhooks/WebhookDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Webhooks/WebhookDomainObjectTests.cs deleted file mode 100644 index 182f00d84..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Webhooks/WebhookDomainObjectTests.cs +++ /dev/null @@ -1,159 +0,0 @@ -// ========================================================================== -// WebhookDomainObjectTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Squidex.Domain.Apps.Events.Webhooks; -using Squidex.Domain.Apps.Write.TestHelpers; -using Squidex.Domain.Apps.Write.Webhooks.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Webhooks -{ - public class WebhookDomainObjectTests : HandlerTestBase - { - private readonly Uri url = new Uri("http://squidex.io"); - private readonly WebhookDomainObject sut; - - public Guid WebhookId { get; } = Guid.NewGuid(); - - public WebhookDomainObjectTests() - { - sut = new WebhookDomainObject(WebhookId, 0); - } - - [Fact] - public void Create_should_throw_exception_if_created() - { - sut.Create(new CreateWebhook { Url = url }); - - Assert.Throws(() => - { - sut.Create(CreateWebhookCommand(new CreateWebhook { Url = url })); - }); - } - - [Fact] - public void Create_should_create_events() - { - var command = new CreateWebhook { Url = url }; - - sut.Create(CreateWebhookCommand(command)); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateWebhookEvent(new WebhookCreated - { - Url = url, - Schemas = command.Schemas, - SharedSecret = command.SharedSecret, - WebhookId = command.WebhookId - }) - ); - } - - [Fact] - public void Update_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.Update(CreateWebhookCommand(new UpdateWebhook { Url = url })); - }); - } - - [Fact] - public void Update_should_throw_exception_if_webhook_is_deleted() - { - CreateWebhook(); - DeleteWebhook(); - - Assert.Throws(() => - { - sut.Update(CreateWebhookCommand(new UpdateWebhook { Url = url })); - }); - } - - [Fact] - public void Update_should_create_events() - { - CreateWebhook(); - - var command = new UpdateWebhook { Url = url }; - - sut.Update(CreateWebhookCommand(command)); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateWebhookEvent(new WebhookUpdated { Url = url, Schemas = command.Schemas }) - ); - } - - [Fact] - public void Delete_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.Delete(CreateWebhookCommand(new DeleteWebhook())); - }); - } - - [Fact] - public void Delete_should_throw_exception_if_already_deleted() - { - CreateWebhook(); - DeleteWebhook(); - - Assert.Throws(() => - { - sut.Delete(CreateWebhookCommand(new DeleteWebhook())); - }); - } - - [Fact] - public void Delete_should_update_properties_create_events() - { - CreateWebhook(); - - sut.Delete(CreateWebhookCommand(new DeleteWebhook())); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateWebhookEvent(new WebhookDeleted()) - ); - } - - private void CreateWebhook() - { - sut.Create(CreateWebhookCommand(new CreateWebhook { Url = url })); - - ((IAggregate)sut).ClearUncommittedEvents(); - } - - private void DeleteWebhook() - { - sut.Delete(CreateWebhookCommand(new DeleteWebhook())); - - ((IAggregate)sut).ClearUncommittedEvents(); - } - - protected T CreateWebhookEvent(T @event) where T : WebhookEvent - { - @event.WebhookId = WebhookId; - - return CreateEvent(@event); - } - - protected T CreateWebhookCommand(T command) where T : WebhookAggregateCommand - { - command.WebhookId = WebhookId; - - return CreateCommand(command); - } - } -} From 43081693d818e0bef5517993b308f6451b260538 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 29 Oct 2017 21:46:14 +0100 Subject: [PATCH 04/14] Tests for dequeuer. --- .../Rules/RuleJob.cs | 2 +- .../HandleRules/RuleService.cs | 8 +- .../Rules/MongoRuleEventRepository.cs | 40 +----- .../Repositories/IRuleEventRepository.cs | 6 +- .../Rules/RuleDequeuer.cs | 27 +++- .../Webhooks/WebhookDequeuerTests.cs | 128 ------------------ .../Webhooks/WebhookEnqueuerTests.cs | 2 + 7 files changed, 37 insertions(+), 176 deletions(-) delete mode 100644 tests/Squidex.Domain.Apps.Read.Tests/Webhooks/WebhookDequeuerTests.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs index 5e2d877a9..3117fd8e2 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs @@ -13,7 +13,7 @@ namespace Squidex.Domain.Apps.Core.Rules { public sealed class RuleJob { - public Guid Id { get; set; } + public Guid RuleId { get; set; } public Guid AppId { get; set; } diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs index 3d236b815..913ce09d5 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs @@ -21,7 +21,7 @@ using Squidex.Infrastructure.CQRS.Events; namespace Squidex.Domain.Apps.Core.HandleRules { - public sealed class RuleService + public class RuleService { private const string ContentPrefix = "Content"; private static readonly Duration ExpirationTime = Duration.FromDays(2); @@ -49,7 +49,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules this.clock = clock; } - public RuleJob CreateJob(Rule rule, Envelope @event) + public virtual RuleJob CreateJob(Rule rule, Envelope @event) { Guard.NotNull(rule, nameof(rule)); Guard.NotNull(@event, nameof(@event)); @@ -87,7 +87,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules var job = new RuleJob { - Id = Guid.NewGuid(), + RuleId = Guid.NewGuid(), ActionName = actionName, ActionData = actionData.Data, AppId = appEvent.AppId.Id, @@ -100,7 +100,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules return job; } - public async Task<(string Dump, RuleResult Result, TimeSpan Elapsed)> InvokeAsync(string actionName, RuleJobData job) + public virtual async Task<(string Dump, RuleResult Result, TimeSpan Elapsed)> InvokeAsync(string actionName, RuleJobData job) { try { diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventRepository.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventRepository.cs index 0bdaa4198..1d0ce4859 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventRepository.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventRepository.cs @@ -16,7 +16,6 @@ using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Read.Rules; using Squidex.Domain.Apps.Read.Rules.Repositories; -using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.Reflection; @@ -24,14 +23,9 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Rules { public sealed class MongoRuleEventRepository : MongoRepositoryBase, IRuleEventRepository { - private readonly IClock clock; - - public MongoRuleEventRepository(IMongoDatabase database, IClock clock) + public MongoRuleEventRepository(IMongoDatabase database) : base(database) { - Guard.NotNull(clock, nameof(clock)); - - this.clock = clock; } protected override string CollectionName() @@ -47,10 +41,8 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Rules collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Expires), new CreateIndexOptions { ExpireAfter = TimeSpan.Zero })); } - public Task QueryPendingAsync(Func callback, CancellationToken cancellationToken = default(CancellationToken)) + public Task QueryPendingAsync(Instant now, Func callback, CancellationToken cancellationToken = default(CancellationToken)) { - var now = clock.GetCurrentInstant(); - return Collection.Find(x => x.NextAttempt < now && !x.IsSending).ForEachAsync(callback, cancellationToken); } @@ -63,15 +55,6 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Rules return webhookEventEntities; } - public async Task FindAsync(Guid id) - { - var webhookEventEntity = - await Collection.Find(x => x.Id == id) - .FirstOrDefaultAsync(); - - return webhookEventEntity; - } - public async Task CountByAppAsync(Guid appId) { return (int)await Collection.CountAsync(x => x.AppId == appId); @@ -84,7 +67,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Rules public Task EnqueueAsync(RuleJob job, Instant nextAttempt) { - var entity = SimpleMapper.Map(job, new MongoRuleEventEntity { Created = clock.GetCurrentInstant(), NextAttempt = nextAttempt }); + var entity = SimpleMapper.Map(job, new MongoRuleEventEntity { Created = nextAttempt, NextAttempt = nextAttempt }); return Collection.InsertOneIfNotExistsAsync(entity); } @@ -94,23 +77,8 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Rules return Collection.UpdateOneAsync(x => x.Id == jobId, Update.Set(x => x.IsSending, true)); } - public Task MarkSentAsync(Guid jobId, string dump, RuleResult result, TimeSpan elapsed, Instant? nextAttempt) + public Task MarkSentAsync(Guid jobId, string dump, RuleResult result, RuleJobResult jobResult, TimeSpan elapsed, Instant? nextAttempt) { - RuleJobResult jobResult; - - if (result != RuleResult.Success && nextAttempt == null) - { - jobResult = RuleJobResult.Failed; - } - else if (result != RuleResult.Success && nextAttempt.HasValue) - { - jobResult = RuleJobResult.Retry; - } - else - { - jobResult = RuleJobResult.Success; - } - return Collection.UpdateOneAsync(x => x.Id == jobId, Update.Set(x => x.Result, result) .Set(x => x.LastDump, dump) diff --git a/src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleEventRepository.cs b/src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleEventRepository.cs index f67c074e8..b34448f4e 100644 --- a/src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleEventRepository.cs +++ b/src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleEventRepository.cs @@ -24,14 +24,12 @@ namespace Squidex.Domain.Apps.Read.Rules.Repositories Task MarkSendingAsync(Guid jobId); - Task MarkSentAsync(Guid jobId, string dump, RuleResult result, TimeSpan elapsed, Instant? nextCall); + Task MarkSentAsync(Guid jobId, string dump, RuleResult result, RuleJobResult jobResult, TimeSpan elapsed, Instant? nextCall); - Task QueryPendingAsync(Func callback, CancellationToken cancellationToken = default(CancellationToken)); + Task QueryPendingAsync(Instant now, 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/RuleDequeuer.cs b/src/Squidex.Domain.Apps.Read/Rules/RuleDequeuer.cs index 3edb332c4..3f6f6e7c6 100644 --- a/src/Squidex.Domain.Apps.Read/Rules/RuleDequeuer.cs +++ b/src/Squidex.Domain.Apps.Read/Rules/RuleDequeuer.cs @@ -26,17 +26,21 @@ namespace Squidex.Domain.Apps.Read.Rules private readonly IRuleEventRepository ruleEventRepository; private readonly RuleService ruleService; private readonly CompletionTimer timer; + private readonly IClock clock; private readonly ISemanticLog log; - public RuleDequeuer(RuleService ruleService, IRuleEventRepository ruleEventRepository, ISemanticLog log) + public RuleDequeuer(RuleService ruleService, IRuleEventRepository ruleEventRepository, ISemanticLog log, IClock clock) { Guard.NotNull(ruleEventRepository, nameof(ruleEventRepository)); Guard.NotNull(ruleService, nameof(ruleService)); + Guard.NotNull(clock, nameof(clock)); Guard.NotNull(log, nameof(log)); this.ruleEventRepository = ruleEventRepository; this.ruleService = ruleService; + this.clock = clock; + this.log = log; requestBlock = @@ -76,7 +80,9 @@ namespace Squidex.Domain.Apps.Read.Rules { try { - await ruleEventRepository.QueryPendingAsync(blockBlock.SendAsync, cancellationToken); + var now = clock.GetCurrentInstant(); + + await ruleEventRepository.QueryPendingAsync(now, blockBlock.SendAsync, cancellationToken); } catch (Exception ex) { @@ -133,7 +139,22 @@ namespace Squidex.Domain.Apps.Read.Rules } } - await ruleEventRepository.MarkSentAsync(@event.Id, response.Dump, response.Result, response.Elapsed, nextCall); + RuleJobResult jobResult; + + if (response.Result != RuleResult.Success && !nextCall.HasValue) + { + jobResult = RuleJobResult.Failed; + } + else if (response.Result != RuleResult.Success && nextCall.HasValue) + { + jobResult = RuleJobResult.Retry; + } + else + { + jobResult = RuleJobResult.Success; + } + + await ruleEventRepository.MarkSentAsync(@event.Id, response.Dump, response.Result, jobResult, response.Elapsed, nextCall); } catch (Exception ex) { diff --git a/tests/Squidex.Domain.Apps.Read.Tests/Webhooks/WebhookDequeuerTests.cs b/tests/Squidex.Domain.Apps.Read.Tests/Webhooks/WebhookDequeuerTests.cs deleted file mode 100644 index d860bf55a..000000000 --- a/tests/Squidex.Domain.Apps.Read.Tests/Webhooks/WebhookDequeuerTests.cs +++ /dev/null @@ -1,128 +0,0 @@ -// ========================================================================== -// WebhookDequeuerTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Threading; -using System.Threading.Tasks; -using FakeItEasy; -using NodaTime; -using Squidex.Domain.Apps.Read.Webhooks.Repositories; -using Squidex.Infrastructure.Log; -using Xunit; - -#pragma warning disable RECS0165 // Asynchronous methods should return a Task instead of void - -namespace Squidex.Domain.Apps.Read.Webhooks -{ - public class WebhookDequeuerTests - { - private readonly IClock clock = A.Fake(); - private readonly IWebhookRepository webhookRepository = A.Fake(); - private readonly IWebhookEventRepository webhookEventRepository = A.Fake(); - private readonly WebhookSender webhookSender = A.Fake(); - private readonly Instant now = SystemClock.Instance.GetCurrentInstant(); - - public WebhookDequeuerTests() - { - A.CallTo(() => clock.GetCurrentInstant()).Returns(now); - } - - [Fact] - public void Should_update_repositories_on_successful_requests() - { - var @event = CreateEvent(0); - - var requestResult = WebhookResult.Success; - var requestTime = TimeSpan.FromMinutes(1); - var requestDump = "Dump"; - - SetupSender(@event, requestDump, requestResult, requestTime); - SetupPendingEvents(@event); - - var sut = new WebhookDequeuer( - webhookSender, - webhookEventRepository, - webhookRepository, - clock, A.Fake()); - - sut.Next(); - sut.Dispose(); - - VerifyRepositories(@event, requestDump, requestResult, requestTime, null); - } - - [Theory] - [InlineData(0, 5)] - [InlineData(1, 60)] - [InlineData(2, 300)] - [InlineData(3, 360)] - public void Should_set_next_attempt_based_on_num_calls(int calls, int minutes) - { - var @event = CreateEvent(calls); - - var requestResult = WebhookResult.Failed; - var requestTime = TimeSpan.FromMinutes(1); - var requestDump = "Dump"; - - SetupSender(@event, requestDump, requestResult, requestTime); - SetupPendingEvents(@event); - - var sut = new WebhookDequeuer( - webhookSender, - webhookEventRepository, - webhookRepository, - clock, A.Fake()); - - sut.Next(); - sut.Dispose(); - - VerifyRepositories(@event, requestDump, requestResult, requestTime, now.Plus(Duration.FromMinutes(minutes))); - } - - private void SetupSender(IWebhookEventEntity @event, string requestDump, WebhookResult requestResult, TimeSpan requestTime) - { - A.CallTo(() => webhookSender.SendAsync(@event.Job)) - .Returns((requestDump, requestResult, requestTime)); - } - - private void SetupPendingEvents(IWebhookEventEntity @event) - { - A.CallTo(() => webhookEventRepository.QueryPendingAsync(A>.Ignored, A.Ignored)) - .Invokes(async (Func callback, CancellationToken ct) => - { - await callback(@event); - }); - } - - private void VerifyRepositories(IWebhookEventEntity @event, string requestDump, WebhookResult requestResult, TimeSpan requestTime, Instant? nextAttempt) - { - A.CallTo(() => webhookEventRepository.TraceSendingAsync(@event.Id)) - .MustHaveHappened(); - - A.CallTo(() => webhookEventRepository.TraceSendingAsync(@event.Id)) - .MustHaveHappened(); - - A.CallTo(() => webhookEventRepository.TraceSentAsync(@event.Id, requestDump, requestResult, requestTime, nextAttempt)) - .MustHaveHappened(); - - A.CallTo(() => webhookRepository.TraceSentAsync(@event.Job.WebhookId, requestResult, requestTime)) - .MustHaveHappened(); - } - - private static IWebhookEventEntity CreateEvent(int numCalls) - { - var @event = A.Fake(); - - A.CallTo(() => @event.Id).Returns(Guid.NewGuid()); - A.CallTo(() => @event.Job).Returns(new WebhookJob { WebhookId = Guid.NewGuid() }); - A.CallTo(() => @event.NumCalls).Returns(numCalls); - - return @event; - } - } -} diff --git a/tests/Squidex.Domain.Apps.Read.Tests/Webhooks/WebhookEnqueuerTests.cs b/tests/Squidex.Domain.Apps.Read.Tests/Webhooks/WebhookEnqueuerTests.cs index 6caec39ce..d33bf4c8c 100644 --- a/tests/Squidex.Domain.Apps.Read.Tests/Webhooks/WebhookEnqueuerTests.cs +++ b/tests/Squidex.Domain.Apps.Read.Tests/Webhooks/WebhookEnqueuerTests.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +/* using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -119,3 +120,4 @@ namespace Squidex.Domain.Apps.Read.Webhooks } } } +*/ \ No newline at end of file From d81c2d2cd7492b9a8861fe65acd4f7532b13bbe4 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 29 Oct 2017 21:46:30 +0100 Subject: [PATCH 05/14] Test added, damn --- .../Rules/RuleDequeuerTests.cs | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 tests/Squidex.Domain.Apps.Read.Tests/Rules/RuleDequeuerTests.cs diff --git a/tests/Squidex.Domain.Apps.Read.Tests/Rules/RuleDequeuerTests.cs b/tests/Squidex.Domain.Apps.Read.Tests/Rules/RuleDequeuerTests.cs new file mode 100644 index 000000000..8914346d7 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Read.Tests/Rules/RuleDequeuerTests.cs @@ -0,0 +1,124 @@ +// ========================================================================== +// RuleDequeuerTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading; +using System.Threading.Tasks; +using FakeItEasy; +using NodaTime; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Read.Rules.Repositories; +using Squidex.Infrastructure.Log; +using Xunit; + +#pragma warning disable RECS0165 // Asynchronous methods should return a Task instead of void + +namespace Squidex.Domain.Apps.Read.Rules +{ + public class RuleDequeuerTests + { + private readonly IClock clock = A.Fake(); + private readonly ISemanticLog log = A.Fake(); + private readonly IRuleRepository ruleRepository = A.Fake(); + private readonly IRuleEventRepository ruleEventRepository = A.Fake(); + private readonly RuleService ruleService = A.Fake(); + private readonly Instant now = SystemClock.Instance.GetCurrentInstant(); + + public RuleDequeuerTests() + { + A.CallTo(() => clock.GetCurrentInstant()).Returns(now); + } + + [Theory] + [InlineData(0, 0, RuleResult.Success, RuleJobResult.Success)] + [InlineData(0, 5, RuleResult.Timeout, RuleJobResult.Retry)] + [InlineData(1, 60, RuleResult.Timeout, RuleJobResult.Retry)] + [InlineData(2, 360, RuleResult.Failed, RuleJobResult.Retry)] + [InlineData(3, 720, RuleResult.Failed, RuleJobResult.Retry)] + [InlineData(4, 0, RuleResult.Failed, RuleJobResult.Failed)] + public void Should_set_next_attempt_based_on_num_calls(int calls, int minutes, RuleResult result, RuleJobResult jobResult) + { + var actionData = new RuleJobData(); + var actionName = "MyAction"; + + var @event = CreateEvent(calls, actionName, actionData); + + var requestElapsed = TimeSpan.FromMinutes(1); + var requestDump = "Dump"; + + SetupSender(@event, requestDump, result, requestElapsed); + SetupPendingEvents(@event); + + var sut = new RuleDequeuer( + ruleService, + ruleEventRepository, + log, + clock); + + sut.Next(); + sut.Dispose(); + + Instant? nextCall = null; + + if (minutes > 0) + { + nextCall = now.Plus(Duration.FromMinutes(minutes)); + } + + VerifyRepositories(@event, requestDump, result, jobResult, requestElapsed, nextCall); + } + + private void SetupSender(IRuleEventEntity @event, string requestDump, RuleResult requestResult, TimeSpan requestTime) + { + A.CallTo(() => ruleService.InvokeAsync(@event.Job.ActionName, @event.Job.ActionData)) + .Returns((requestDump, requestResult, requestTime)); + } + + private void SetupPendingEvents(IRuleEventEntity @event) + { + A.CallTo(() => ruleEventRepository.QueryPendingAsync( + now, + A>.Ignored, + A.Ignored)) + .Invokes(async (Instant n, Func callback, CancellationToken ct) => + { + await callback(@event); + }); + } + + private void VerifyRepositories(IRuleEventEntity @event, string dump, RuleResult result, RuleJobResult jobResult, TimeSpan elapsed, Instant? nextCall) + { + A.CallTo(() => ruleEventRepository.MarkSendingAsync(@event.Id)) + .MustHaveHappened(); + + A.CallTo(() => ruleEventRepository.MarkSentAsync(@event.Id, dump, result, jobResult, elapsed, nextCall)) + .MustHaveHappened(); + } + + private IRuleEventEntity CreateEvent(int numCalls, string actionName, RuleJobData actionData) + { + var @event = A.Fake(); + + var job = new RuleJob + { + RuleId = Guid.NewGuid(), + ActionData = actionData, + ActionName = actionName, + Created = now + }; + + A.CallTo(() => @event.Id).Returns(Guid.NewGuid()); + A.CallTo(() => @event.Job).Returns(job); + A.CallTo(() => @event.Created).Returns(now); + A.CallTo(() => @event.NumCalls).Returns(numCalls); + + return @event; + } + } +} From 33f7d4470418b4f3727117098240ffcf3f5f28db Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 29 Oct 2017 22:05:58 +0100 Subject: [PATCH 06/14] Tests for rule enqueuer. --- .../Rules/RuleEnqueuer.cs | 8 +- .../Webhooks/WebhookEnqueuerTests.cs | 123 ------------------ 2 files changed, 3 insertions(+), 128 deletions(-) delete mode 100644 tests/Squidex.Domain.Apps.Read.Tests/Webhooks/WebhookEnqueuerTests.cs diff --git a/src/Squidex.Domain.Apps.Read/Rules/RuleEnqueuer.cs b/src/Squidex.Domain.Apps.Read/Rules/RuleEnqueuer.cs index ad962157c..01870edb7 100644 --- a/src/Squidex.Domain.Apps.Read/Rules/RuleEnqueuer.cs +++ b/src/Squidex.Domain.Apps.Read/Rules/RuleEnqueuer.cs @@ -29,7 +29,7 @@ namespace Squidex.Domain.Apps.Read.Rules public string EventsFilter { - get { return "^content-"; } + get { return ".*"; } } public RuleEnqueuer( @@ -61,12 +61,10 @@ namespace Squidex.Domain.Apps.Read.Rules { var job = ruleService.CreateJob(ruleEntity.Rule, @event); - if (job == null) + if (job != null) { - continue; + await ruleEventRepository.EnqueueAsync(job, job.Created); } - - await ruleEventRepository.EnqueueAsync(job, job.Created); } } } diff --git a/tests/Squidex.Domain.Apps.Read.Tests/Webhooks/WebhookEnqueuerTests.cs b/tests/Squidex.Domain.Apps.Read.Tests/Webhooks/WebhookEnqueuerTests.cs deleted file mode 100644 index d33bf4c8c..000000000 --- a/tests/Squidex.Domain.Apps.Read.Tests/Webhooks/WebhookEnqueuerTests.cs +++ /dev/null @@ -1,123 +0,0 @@ -// ========================================================================== -// WebhookEnqueuerTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -/* -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FakeItEasy; -using Newtonsoft.Json; -using NodaTime; -using Squidex.Domain.Apps.Core.Webhooks; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Domain.Apps.Read.Webhooks.Repositories; -using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS.Events; -using Xunit; - -namespace Squidex.Domain.Apps.Read.Webhooks -{ - public class WebhookEnqueuerTests - { - private readonly IClock clock = A.Fake(); - private readonly IWebhookRepository webhookRepository = A.Fake(); - private readonly IWebhookEventRepository webhookEventRepository = A.Fake(); - private readonly TypeNameRegistry typeNameRegisty = new TypeNameRegistry(); - private readonly Instant now = SystemClock.Instance.GetCurrentInstant(); - private readonly NamedId appId = new NamedId(Guid.NewGuid(), "my-app"); - private readonly NamedId schemaId = new NamedId(Guid.NewGuid(), "my-schema"); - private readonly WebhookEnqueuer sut; - - public WebhookEnqueuerTests() - { - A.CallTo(() => clock.GetCurrentInstant()).Returns(now); - - typeNameRegisty.Map(typeof(ContentCreated)); - - sut = new WebhookEnqueuer( - typeNameRegisty, - webhookEventRepository, - webhookRepository, - clock, new JsonSerializer()); - } - - [Fact] - public void Should_return_contents_filter_for_events_filter() - { - Assert.Equal("^content-", sut.EventsFilter); - } - - [Fact] - public void Should_return_type_name_for_name() - { - Assert.Equal(typeof(WebhookEnqueuer).Name, sut.Name); - } - - [Fact] - public Task Should_do_nothing_on_clear() - { - return sut.ClearAsync(); - } - - [Fact] - public async Task Should_update_repositories_on_successful_requests() - { - var @event = Envelope.Create(new ContentCreated { AppId = appId, SchemaId = schemaId }); - - var webhook1 = CreateWebhook(1); - var webhook2 = CreateWebhook(2); - - A.CallTo(() => webhookRepository.QueryCachedByAppAsync(appId.Id)) - .Returns(new List { webhook1, webhook2 }); - - await sut.On(@event); - - A.CallTo(() => webhookEventRepository.EnqueueAsync( - A.That.Matches(webhookJob => - !string.IsNullOrWhiteSpace(webhookJob.RequestSignature) - && !string.IsNullOrWhiteSpace(webhookJob.RequestBody) - && webhookJob.Id != Guid.Empty - && webhookJob.Expires == now.Plus(Duration.FromDays(2)) - && webhookJob.AppId == appId.Id - && webhookJob.EventName == "MySchemaCreatedEvent" - && webhookJob.RequestUrl == webhook1.Url - && webhookJob.WebhookId == webhook1.Id), now)).MustHaveHappened(); - - A.CallTo(() => webhookEventRepository.EnqueueAsync( - A.That.Matches(webhookJob => - !string.IsNullOrWhiteSpace(webhookJob.RequestSignature) - && !string.IsNullOrWhiteSpace(webhookJob.RequestBody) - && webhookJob.Id != Guid.Empty - && webhookJob.Expires == now.Plus(Duration.FromDays(2)) - && webhookJob.AppId == appId.Id - && webhookJob.EventName == "MySchemaCreatedEvent" - && webhookJob.RequestUrl == webhook2.Url - && webhookJob.WebhookId == webhook2.Id), now)).MustHaveHappened(); - } - - private IWebhookEntity CreateWebhook(int offset) - { - var webhook = A.Dummy(); - - var schema = new WebhookSchema - { - SchemaId = schemaId.Id, - SendCreate = true, - SendUpdate = true - }; - - A.CallTo(() => webhook.Id).Returns(Guid.NewGuid()); - A.CallTo(() => webhook.Url).Returns(new Uri($"http://domain{offset}.com")); - A.CallTo(() => webhook.Schemas).Returns(new[] { schema }); - A.CallTo(() => webhook.SharedSecret).Returns($"secret{offset}"); - - return webhook; - } - } -} -*/ \ No newline at end of file From 46ee7459ada064dcba8390442819d794284bbcab Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Mon, 30 Oct 2017 20:22:59 +0100 Subject: [PATCH 07/14] API updated. --- .../HandleRules/RuleService.cs | 3 +- .../Triggers/ContentChangedTriggerHandler.cs | 14 +- .../Rules/MongoRuleEventRepository.cs | 13 +- .../Rules/MongoRuleRepository.cs | 12 +- .../MongoRuleRepository_EventHandling.cs | 18 +- .../Repositories/IRuleEventRepository.cs | 2 + src/Squidex/Config/Domain/ReadModule.cs | 21 +- .../Config/Domain/StoreMongoDbModule.cs | 12 +- src/Squidex/Config/Domain/WriteModule.cs | 6 +- .../Models/Actions/WebhookActionDto.cs} | 25 +- .../Models/Converters/RuleActionDtoFactory.cs | 34 +++ .../Rules/Models/Converters/RuleConverter.cs | 57 ++++ .../Converters/RuleTriggerDtoFactory.cs | 38 +++ .../Models/CreateRuleDto.cs} | 16 +- .../Api/Rules/Models/RuleActionDto.cs | 23 ++ .../Controllers/Api/Rules/Models/RuleDto.cs | 67 +++++ .../Models/RuleEventDto.cs} | 17 +- .../Models/RuleEventsDto.cs} | 12 +- .../Api/Rules/Models/RuleTriggerDto.cs | 23 ++ .../Triggers/ContentChangedTriggerDto.cs | 36 +++ .../ContentChangedTriggerSchemaDto.cs} | 6 +- .../Models/UpdateRuleDto.cs} | 20 +- .../Controllers/Api/Rules/RulesController.cs | 245 ++++++++++++++++++ .../Converters/FieldPropertiesDtoFactory.cs | 1 + .../Api/Schemas/Models/FieldPropertiesDto.cs | 3 +- .../{ => Fields}/AssetsFieldPropertiesDto.cs | 2 +- .../{ => Fields}/BooleanFieldPropertiesDto.cs | 2 +- .../DateTimeFieldPropertiesDto.cs | 2 +- .../GeolocationFieldPropertiesDto.cs | 2 +- .../{ => Fields}/JsonFieldPropertiesDto.cs | 2 +- .../{ => Fields}/NumberFieldPropertiesDto.cs | 2 +- .../ReferencesFieldPropertiesDto.cs | 2 +- .../{ => Fields}/StringFieldPropertiesDto.cs | 2 +- .../{ => Fields}/TagsFieldPropertiesDto.cs | 2 +- .../Api/Webhooks/Models/WebhookDto.cs | 89 ------- .../Api/Webhooks/WebhooksController.cs | 220 ---------------- ...verter.cs => JsonInheritanceConverter2.cs} | 6 +- .../Rules/RuleEnqueuerTests.cs | 102 ++++++++ 38 files changed, 749 insertions(+), 410 deletions(-) rename src/Squidex/Controllers/Api/{Webhooks/Models/WebhookCreatedDto.cs => Rules/Models/Actions/WebhookActionDto.cs} (54%) create mode 100644 src/Squidex/Controllers/Api/Rules/Models/Converters/RuleActionDtoFactory.cs create mode 100644 src/Squidex/Controllers/Api/Rules/Models/Converters/RuleConverter.cs create mode 100644 src/Squidex/Controllers/Api/Rules/Models/Converters/RuleTriggerDtoFactory.cs rename src/Squidex/Controllers/Api/{Webhooks/Models/UpdateWebhookDto.cs => Rules/Models/CreateRuleDto.cs} (60%) create mode 100644 src/Squidex/Controllers/Api/Rules/Models/RuleActionDto.cs create mode 100644 src/Squidex/Controllers/Api/Rules/Models/RuleDto.cs rename src/Squidex/Controllers/Api/{Webhooks/Models/WebhookEventDto.cs => Rules/Models/RuleEventDto.cs} (79%) rename src/Squidex/Controllers/Api/{Webhooks/Models/WebhookEventsDto.cs => Rules/Models/RuleEventsDto.cs} (65%) create mode 100644 src/Squidex/Controllers/Api/Rules/Models/RuleTriggerDto.cs create mode 100644 src/Squidex/Controllers/Api/Rules/Models/Triggers/ContentChangedTriggerDto.cs rename src/Squidex/Controllers/Api/{Webhooks/Models/WebhookSchemaDto.cs => Rules/Models/Triggers/ContentChangedTriggerSchemaDto.cs} (87%) rename src/Squidex/Controllers/Api/{Webhooks/Models/CreateWebhookDto.cs => Rules/Models/UpdateRuleDto.cs} (50%) create mode 100644 src/Squidex/Controllers/Api/Rules/RulesController.cs rename src/Squidex/Controllers/Api/Schemas/Models/{ => Fields}/AssetsFieldPropertiesDto.cs (94%) rename src/Squidex/Controllers/Api/Schemas/Models/{ => Fields}/BooleanFieldPropertiesDto.cs (95%) rename src/Squidex/Controllers/Api/Schemas/Models/{ => Fields}/DateTimeFieldPropertiesDto.cs (96%) rename src/Squidex/Controllers/Api/Schemas/Models/{ => Fields}/GeolocationFieldPropertiesDto.cs (95%) rename src/Squidex/Controllers/Api/Schemas/Models/{ => Fields}/JsonFieldPropertiesDto.cs (92%) rename src/Squidex/Controllers/Api/Schemas/Models/{ => Fields}/NumberFieldPropertiesDto.cs (96%) rename src/Squidex/Controllers/Api/Schemas/Models/{ => Fields}/ReferencesFieldPropertiesDto.cs (95%) rename src/Squidex/Controllers/Api/Schemas/Models/{ => Fields}/StringFieldPropertiesDto.cs (97%) rename src/Squidex/Controllers/Api/Schemas/Models/{ => Fields}/TagsFieldPropertiesDto.cs (94%) delete mode 100644 src/Squidex/Controllers/Api/Webhooks/Models/WebhookDto.cs delete mode 100644 src/Squidex/Controllers/Api/Webhooks/WebhooksController.cs rename src/Squidex/Controllers/{Api/Schemas/Models/Converters/JsonInheritanceConverter.cs => JsonInheritanceConverter2.cs} (95%) create mode 100644 tests/Squidex.Domain.Apps.Read.Tests/Rules/RuleEnqueuerTests.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs index 913ce09d5..35fecb5ac 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs @@ -12,7 +12,6 @@ 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; @@ -114,7 +113,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules var dumpBuilder = new StringBuilder(result.Dump); dumpBuilder.AppendLine(); - dumpBuilder.AppendFormat("Elapesed {0}.", actionWatch.Elapsed); + dumpBuilder.AppendFormat("Elapsed {0}.", actionWatch.Elapsed); dumpBuilder.AppendLine(); if (result.Exception is TimeoutException || result.Exception is OperationCanceledException) diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs index 5a3c542d4..e0b8ef424 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs @@ -32,18 +32,18 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Triggers return false; } - private static bool MatchsSchema(ContentChangedTriggerSchema webhookSchema, SchemaEvent @event) + private static bool MatchsSchema(ContentChangedTriggerSchema schema, SchemaEvent @event) { - return @event.SchemaId.Id == webhookSchema.SchemaId; + return @event.SchemaId.Id == schema.SchemaId; } - private static bool MatchsType(ContentChangedTriggerSchema webhookSchema, SchemaEvent @event) + private static bool MatchsType(ContentChangedTriggerSchema schema, 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); + (schema.SendCreate && @event is ContentCreated) || + (schema.SendUpdate && @event is ContentUpdated) || + (schema.SendDelete && @event is ContentDeleted) || + (schema.SendPublish && @event is ContentStatusChanged statusChanged && statusChanged.Status == Status.Published); } } } diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventRepository.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventRepository.cs index 1d0ce4859..d7016942c 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventRepository.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventRepository.cs @@ -48,11 +48,20 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Rules public async Task> QueryByAppAsync(Guid appId, int skip = 0, int take = 20) { - var webhookEventEntities = + var ruleEventEntities = await Collection.Find(x => x.AppId == appId).Skip(skip).Limit(take).SortByDescending(x => x.Created) .ToListAsync(); - return webhookEventEntities; + return ruleEventEntities; + } + + public async Task FindAsync(Guid id) + { + var ruleEvent = + await Collection.Find(x => x.Id == id) + .FirstOrDefaultAsync(); + + return ruleEvent; } public async Task CountByAppAsync(Guid appId) diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository.cs index c4dc37beb..378b512f9 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository.cs @@ -25,7 +25,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Rules { private static readonly List EmptyRules = new List(); private readonly SemaphoreSlim lockObject = new SemaphoreSlim(1); - private Dictionary> inMemoryWebhooks; + private Dictionary> inMemoryRules; public MongoRuleRepository(IMongoDatabase database) : base(database) @@ -55,20 +55,20 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Rules { await EnsureRulesLoadedAsync(); - return inMemoryWebhooks.GetOrDefault(appId) ?? EmptyRules; + return inMemoryRules.GetOrDefault(appId) ?? EmptyRules; } private async Task EnsureRulesLoadedAsync() { - if (inMemoryWebhooks == null) + if (inMemoryRules == null) { try { await lockObject.WaitAsync(); - if (inMemoryWebhooks == null) + if (inMemoryRules == null) { - inMemoryWebhooks = new Dictionary>(); + inMemoryRules = new Dictionary>(); var webhooks = await Collection.Find(new BsonDocument()) @@ -76,7 +76,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Rules foreach (var webhook in webhooks) { - inMemoryWebhooks.GetOrAddNew(webhook.AppId).Add(webhook); + inMemoryRules.GetOrAddNew(webhook.AppId).Add(webhook); } } } diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository_EventHandling.cs index 4de792863..db2e3db37 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository_EventHandling.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository_EventHandling.cs @@ -41,8 +41,8 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Rules { w.Rule = RuleEventDispatcher.Create(@event); - inMemoryWebhooks.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id); - inMemoryWebhooks.GetOrAddNew(w.AppId).Add(w); + inMemoryRules.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id); + inMemoryRules.GetOrAddNew(w.AppId).Add(w); }); } @@ -54,8 +54,8 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Rules { w.Rule.Apply(@event); - inMemoryWebhooks.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id); - inMemoryWebhooks.GetOrAddNew(w.AppId).Add(w); + inMemoryRules.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id); + inMemoryRules.GetOrAddNew(w.AppId).Add(w); }); } @@ -67,8 +67,8 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Rules { w.Rule.Apply(@event); - inMemoryWebhooks.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id); - inMemoryWebhooks.GetOrAddNew(w.AppId).Add(w); + inMemoryRules.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id); + inMemoryRules.GetOrAddNew(w.AppId).Add(w); }); } @@ -80,8 +80,8 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Rules { w.Rule.Apply(@event); - inMemoryWebhooks.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id); - inMemoryWebhooks.GetOrAddNew(w.AppId).Add(w); + inMemoryRules.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id); + inMemoryRules.GetOrAddNew(w.AppId).Add(w); }); } @@ -89,7 +89,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Rules { await EnsureRulesLoadedAsync(); - inMemoryWebhooks.GetOrAddNew(@event.AppId.Id).RemoveAll(x => x.Id == @event.RuleId); + inMemoryRules.GetOrAddNew(@event.AppId.Id).RemoveAll(x => x.Id == @event.RuleId); await Collection.DeleteManyAsync(x => x.Id == @event.RuleId); } diff --git a/src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleEventRepository.cs b/src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleEventRepository.cs index b34448f4e..256aa9b71 100644 --- a/src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleEventRepository.cs +++ b/src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleEventRepository.cs @@ -31,5 +31,7 @@ namespace Squidex.Domain.Apps.Read.Rules.Repositories Task CountByAppAsync(Guid appId); Task> QueryByAppAsync(Guid appId, int skip = 0, int take = 20); + + Task FindAsync(Guid id); } } diff --git a/src/Squidex/Config/Domain/ReadModule.cs b/src/Squidex/Config/Domain/ReadModule.cs index c9cd02801..fca1204f7 100644 --- a/src/Squidex/Config/Domain/ReadModule.cs +++ b/src/Squidex/Config/Domain/ReadModule.cs @@ -11,6 +11,9 @@ using System.Linq; using Autofac; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.HandleRules.ActionHandlers; +using Squidex.Domain.Apps.Core.HandleRules.Triggers; using Squidex.Domain.Apps.Read.Apps; using Squidex.Domain.Apps.Read.Apps.Services; using Squidex.Domain.Apps.Read.Apps.Services.Implementations; @@ -18,10 +21,10 @@ using Squidex.Domain.Apps.Read.Contents; using Squidex.Domain.Apps.Read.Contents.Edm; using Squidex.Domain.Apps.Read.Contents.GraphQL; using Squidex.Domain.Apps.Read.History; +using Squidex.Domain.Apps.Read.Rules; using Squidex.Domain.Apps.Read.Schemas; using Squidex.Domain.Apps.Read.Schemas.Services; using Squidex.Domain.Apps.Read.Schemas.Services.Implementations; -using Squidex.Domain.Apps.Read.Webhooks; using Squidex.Domain.Users; using Squidex.Infrastructure; using Squidex.Infrastructure.Assets; @@ -104,17 +107,27 @@ namespace Squidex.Config.Domain .AsSelf() .InstancePerDependency(); - builder.RegisterType() + builder.RegisterType() .As() .AsSelf() .InstancePerDependency(); - builder.RegisterType() + builder.RegisterType() .As() .AsSelf() .InstancePerDependency(); - builder.RegisterType() + builder.RegisterType() + .As() + .AsSelf() + .SingleInstance(); + + builder.RegisterType() + .As() + .AsSelf() + .SingleInstance(); + + builder.RegisterType() .AsSelf() .SingleInstance(); diff --git a/src/Squidex/Config/Domain/StoreMongoDbModule.cs b/src/Squidex/Config/Domain/StoreMongoDbModule.cs index f232207f3..faa0c355f 100644 --- a/src/Squidex/Config/Domain/StoreMongoDbModule.cs +++ b/src/Squidex/Config/Domain/StoreMongoDbModule.cs @@ -22,11 +22,11 @@ using Squidex.Domain.Apps.Read.MongoDb.Apps; using Squidex.Domain.Apps.Read.MongoDb.Assets; using Squidex.Domain.Apps.Read.MongoDb.Contents; using Squidex.Domain.Apps.Read.MongoDb.History; +using Squidex.Domain.Apps.Read.MongoDb.Rules; using Squidex.Domain.Apps.Read.MongoDb.Schemas; -using Squidex.Domain.Apps.Read.MongoDb.Webhooks; +using Squidex.Domain.Apps.Read.Rules.Repositories; using Squidex.Domain.Apps.Read.Schemas.Repositories; using Squidex.Domain.Apps.Read.Schemas.Services.Implementations; -using Squidex.Domain.Apps.Read.Webhooks.Repositories; using Squidex.Domain.Users; using Squidex.Domain.Users.MongoDb; using Squidex.Domain.Users.MongoDb.Infrastructure; @@ -136,9 +136,9 @@ namespace Squidex.Config.Domain .AsSelf() .SingleInstance(); - builder.RegisterType() + builder.RegisterType() .WithParameter(ResolvedParameter.ForNamed(MongoDatabaseRegistration)) - .As() + .As() .As() .AsSelf() .SingleInstance(); @@ -171,9 +171,9 @@ namespace Squidex.Config.Domain .AsSelf() .SingleInstance(); - builder.RegisterType() + builder.RegisterType() .WithParameter(ResolvedParameter.ForNamed(MongoDatabaseRegistration)) - .As() + .As() .As() .As() .AsSelf() diff --git a/src/Squidex/Config/Domain/WriteModule.cs b/src/Squidex/Config/Domain/WriteModule.cs index 24389f8e9..f4b7ee25f 100644 --- a/src/Squidex/Config/Domain/WriteModule.cs +++ b/src/Squidex/Config/Domain/WriteModule.cs @@ -13,8 +13,8 @@ using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Write.Apps; using Squidex.Domain.Apps.Write.Assets; using Squidex.Domain.Apps.Write.Contents; +using Squidex.Domain.Apps.Write.Rules; using Squidex.Domain.Apps.Write.Schemas; -using Squidex.Domain.Apps.Write.Webhooks; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Pipeline.CommandMiddlewares; @@ -75,7 +75,7 @@ namespace Squidex.Config.Domain .As() .SingleInstance(); - builder.RegisterType() + builder.RegisterType() .As() .SingleInstance(); @@ -95,7 +95,7 @@ namespace Squidex.Config.Domain .AsSelf() .SingleInstance(); - builder.Register>(c => (id => new WebhookDomainObject(id, -1))) + builder.Register>(c => (id => new RuleDomainObject(id, -1))) .AsSelf() .SingleInstance(); diff --git a/src/Squidex/Controllers/Api/Webhooks/Models/WebhookCreatedDto.cs b/src/Squidex/Controllers/Api/Rules/Models/Actions/WebhookActionDto.cs similarity index 54% rename from src/Squidex/Controllers/Api/Webhooks/Models/WebhookCreatedDto.cs rename to src/Squidex/Controllers/Api/Rules/Models/Actions/WebhookActionDto.cs index f00234562..d7f1c909e 100644 --- a/src/Squidex/Controllers/Api/Webhooks/Models/WebhookCreatedDto.cs +++ b/src/Squidex/Controllers/Api/Rules/Models/Actions/WebhookActionDto.cs @@ -1,5 +1,5 @@ // ========================================================================== -// WebhookCreatedDto.cs +// WebhookActionDto.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -8,25 +8,30 @@ using System; using System.ComponentModel.DataAnnotations; +using NJsonSchema.Annotations; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Actions; +using Squidex.Infrastructure.Reflection; -namespace Squidex.Controllers.Api.Webhooks.Models +namespace Squidex.Controllers.Api.Rules.Models.Actions { - public sealed class WebhookCreatedDto + [JsonSchema("Webhook")] + public sealed class WebhookActionDto : RuleActionDto { /// - /// The id of the webhook. + /// The url of the rule. /// - public Guid Id { get; set; } + [Required] + public Uri Url { get; set; } /// /// The shared secret that is used to calculate the signature. /// - [Required] public string SharedSecret { get; set; } - /// - /// The version of the schema. - /// - public long Version { get; set; } + public override RuleAction ToAction() + { + return SimpleMapper.Map(this, new WebhookAction()); + } } } diff --git a/src/Squidex/Controllers/Api/Rules/Models/Converters/RuleActionDtoFactory.cs b/src/Squidex/Controllers/Api/Rules/Models/Converters/RuleActionDtoFactory.cs new file mode 100644 index 000000000..fe28cd931 --- /dev/null +++ b/src/Squidex/Controllers/Api/Rules/Models/Converters/RuleActionDtoFactory.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// RuleActionDtoFactory.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Controllers.Api.Rules.Models.Actions; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Actions; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Controllers.Api.Rules.Models.Converters +{ + public sealed class RuleActionDtoFactory : IRuleActionVisitor + { + private static readonly RuleActionDtoFactory Instance = new RuleActionDtoFactory(); + + private RuleActionDtoFactory() + { + } + + public static RuleActionDto Create(RuleAction properties) + { + return properties.Accept(Instance); + } + + public RuleActionDto Visit(WebhookAction action) + { + return SimpleMapper.Map(action, new WebhookActionDto()); + } + } +} diff --git a/src/Squidex/Controllers/Api/Rules/Models/Converters/RuleConverter.cs b/src/Squidex/Controllers/Api/Rules/Models/Converters/RuleConverter.cs new file mode 100644 index 000000000..6d4a3b54d --- /dev/null +++ b/src/Squidex/Controllers/Api/Rules/Models/Converters/RuleConverter.cs @@ -0,0 +1,57 @@ +// ========================================================================== +// RuleConverter.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Read.Rules; +using Squidex.Domain.Apps.Write.Rules.Commands; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Controllers.Api.Rules.Models.Converters +{ + public static class RuleConverter + { + public static RuleDto ToModel(this IRuleEntity entity) + { + var dto = new RuleDto(); + + SimpleMapper.Map(entity, dto); + SimpleMapper.Map(entity.Rule, dto); + + if (entity.Rule.Trigger != null) + { + dto.Trigger = RuleTriggerDtoFactory.Create(entity.Rule.Trigger); + } + + if (entity.Rule.Action != null) + { + dto.Action = RuleActionDtoFactory.Create(entity.Rule.Action); + } + + return dto; + } + + public static UpdateRule ToCommand(this UpdateRuleDto dto) + { + var command = new UpdateRule + { + Trigger = dto.Trigger?.ToTrigger(), Action = dto.Action?.ToAction() + }; + + return command; + } + + public static CreateRule ToCommand(this CreateRuleDto dto) + { + var command = new CreateRule + { + Trigger = dto.Trigger.ToTrigger(), Action = dto.Action.ToAction() + }; + + return command; + } + } +} diff --git a/src/Squidex/Controllers/Api/Rules/Models/Converters/RuleTriggerDtoFactory.cs b/src/Squidex/Controllers/Api/Rules/Models/Converters/RuleTriggerDtoFactory.cs new file mode 100644 index 000000000..a9d38a6bf --- /dev/null +++ b/src/Squidex/Controllers/Api/Rules/Models/Converters/RuleTriggerDtoFactory.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// RuleTriggerDtoFactory.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Linq; +using Squidex.Controllers.Api.Rules.Models.Triggers; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Controllers.Api.Rules.Models.Converters +{ + public sealed class RuleTriggerDtoFactory : IRuleTriggerVisitor + { + private static readonly RuleTriggerDtoFactory Instance = new RuleTriggerDtoFactory(); + + private RuleTriggerDtoFactory() + { + } + + public static RuleTriggerDto Create(RuleTrigger properties) + { + return properties.Accept(Instance); + } + + public RuleTriggerDto Visit(ContentChangedTrigger trigger) + { + return new ContentChangedTriggerDto + { + Schemas = trigger.Schemas.Select(x => SimpleMapper.Map(x, new ContentChangedTriggerSchemaDto())).ToList() + }; + } + } +} diff --git a/src/Squidex/Controllers/Api/Webhooks/Models/UpdateWebhookDto.cs b/src/Squidex/Controllers/Api/Rules/Models/CreateRuleDto.cs similarity index 60% rename from src/Squidex/Controllers/Api/Webhooks/Models/UpdateWebhookDto.cs rename to src/Squidex/Controllers/Api/Rules/Models/CreateRuleDto.cs index d5b7da231..060ff6461 100644 --- a/src/Squidex/Controllers/Api/Webhooks/Models/UpdateWebhookDto.cs +++ b/src/Squidex/Controllers/Api/Rules/Models/CreateRuleDto.cs @@ -1,29 +1,27 @@ // ========================================================================== -// UpdateWebhookDto.cs +// CreateRuleDto.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace Squidex.Controllers.Api.Webhooks.Models +namespace Squidex.Controllers.Api.Rules.Models { - public sealed class UpdateWebhookDto + public sealed class CreateRuleDto { /// - /// The url of the webhook. + /// The trigger properties. /// [Required] - public Uri Url { get; set; } + public RuleTriggerDto Trigger { get; set; } /// - /// The schema settings. + /// The action properties. /// [Required] - public List Schemas { get; set; } + public RuleActionDto Action { get; set; } } } diff --git a/src/Squidex/Controllers/Api/Rules/Models/RuleActionDto.cs b/src/Squidex/Controllers/Api/Rules/Models/RuleActionDto.cs new file mode 100644 index 000000000..e2bc33439 --- /dev/null +++ b/src/Squidex/Controllers/Api/Rules/Models/RuleActionDto.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// RuleActionDto.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Runtime.Serialization; +using Newtonsoft.Json; +using NJsonSchema.Converters; +using Squidex.Controllers.Api.Rules.Models.Actions; +using Squidex.Domain.Apps.Core.Rules; + +namespace Squidex.Controllers.Api.Rules.Models +{ + [JsonConverter(typeof(JsonInheritanceConverter), "actionType")] + [KnownType(typeof(WebhookActionDto))] + public abstract class RuleActionDto + { + public abstract RuleAction ToAction(); + } +} diff --git a/src/Squidex/Controllers/Api/Rules/Models/RuleDto.cs b/src/Squidex/Controllers/Api/Rules/Models/RuleDto.cs new file mode 100644 index 000000000..d6c3ade48 --- /dev/null +++ b/src/Squidex/Controllers/Api/Rules/Models/RuleDto.cs @@ -0,0 +1,67 @@ +// ========================================================================== +// RuleDto.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.ComponentModel.DataAnnotations; +using NodaTime; +using Squidex.Infrastructure; + +namespace Squidex.Controllers.Api.Rules.Models +{ + public sealed class RuleDto + { + /// + /// The id of the rule. + /// + public Guid Id { get; set; } + + /// + /// The user that has created the rule. + /// + [Required] + public RefToken CreatedBy { get; set; } + + /// + /// The user that has updated the rule. + /// + [Required] + public RefToken LastModifiedBy { get; set; } + + /// + /// The date and time when the rule has been created. + /// + public Instant Created { get; set; } + + /// + /// The date and time when the rule has been modified last. + /// + public Instant LastModified { get; set; } + + /// + /// The version of the rule. + /// + public int Version { get; set; } + + /// + /// The trigger properties. + /// + [Required] + public RuleTriggerDto Trigger { get; set; } + + /// + /// The action properties. + /// + [Required] + public RuleActionDto Action { get; set; } + + /// + /// Determines if the rule is enabled. + /// + public bool IsEnabled { get; set; } + } +} diff --git a/src/Squidex/Controllers/Api/Webhooks/Models/WebhookEventDto.cs b/src/Squidex/Controllers/Api/Rules/Models/RuleEventDto.cs similarity index 79% rename from src/Squidex/Controllers/Api/Webhooks/Models/WebhookEventDto.cs rename to src/Squidex/Controllers/Api/Rules/Models/RuleEventDto.cs index 4473fe749..5513d5a25 100644 --- a/src/Squidex/Controllers/Api/Webhooks/Models/WebhookEventDto.cs +++ b/src/Squidex/Controllers/Api/Rules/Models/RuleEventDto.cs @@ -1,5 +1,5 @@ // ========================================================================== -// WebhookEventDto.cs +// RuleEventDto.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -9,11 +9,12 @@ using System; using System.ComponentModel.DataAnnotations; using NodaTime; -using Squidex.Domain.Apps.Read.Webhooks; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Read.Rules; -namespace Squidex.Controllers.Api.Webhooks.Models +namespace Squidex.Controllers.Api.Rules.Models { - public sealed class WebhookEventDto + public sealed class RuleEventDto { /// /// The id of the event. @@ -26,10 +27,10 @@ namespace Squidex.Controllers.Api.Webhooks.Models public Instant Created { get; set; } /// - /// The request url. + /// The description /// [Required] - public Uri RequestUrl { get; set; } + public string Description { get; set; } /// /// The name of the event. @@ -55,11 +56,11 @@ namespace Squidex.Controllers.Api.Webhooks.Models /// /// The result of the event. /// - public WebhookResult Result { get; set; } + public RuleResult Result { get; set; } /// /// The result of the job. /// - public WebhookJobResult JobResult { get; set; } + public RuleJobResult JobResult { get; set; } } } diff --git a/src/Squidex/Controllers/Api/Webhooks/Models/WebhookEventsDto.cs b/src/Squidex/Controllers/Api/Rules/Models/RuleEventsDto.cs similarity index 65% rename from src/Squidex/Controllers/Api/Webhooks/Models/WebhookEventsDto.cs rename to src/Squidex/Controllers/Api/Rules/Models/RuleEventsDto.cs index 977f44c04..f0cda8a38 100644 --- a/src/Squidex/Controllers/Api/Webhooks/Models/WebhookEventsDto.cs +++ b/src/Squidex/Controllers/Api/Rules/Models/RuleEventsDto.cs @@ -1,23 +1,23 @@ // ========================================================================== -// WebhookEventsDto.cs +// RuleEventsDto.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -namespace Squidex.Controllers.Api.Webhooks.Models +namespace Squidex.Controllers.Api.Rules.Models { - public sealed class WebhookEventsDto + public sealed class RuleEventsDto { /// - /// The total number of webhook events. + /// The total number of rule events. /// public long Total { get; set; } /// - /// The webhook events. + /// The rule events. /// - public WebhookEventDto[] Items { get; set; } + public RuleEventDto[] Items { get; set; } } } diff --git a/src/Squidex/Controllers/Api/Rules/Models/RuleTriggerDto.cs b/src/Squidex/Controllers/Api/Rules/Models/RuleTriggerDto.cs new file mode 100644 index 000000000..3d009cd50 --- /dev/null +++ b/src/Squidex/Controllers/Api/Rules/Models/RuleTriggerDto.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// RuleTriggerDto.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Runtime.Serialization; +using Newtonsoft.Json; +using NJsonSchema.Converters; +using Squidex.Controllers.Api.Rules.Models.Triggers; +using Squidex.Domain.Apps.Core.Rules; + +namespace Squidex.Controllers.Api.Rules.Models +{ + [JsonConverter(typeof(JsonInheritanceConverter), "triggerType")] + [KnownType(typeof(ContentChangedTriggerDto))] + public abstract class RuleTriggerDto + { + public abstract RuleTrigger ToTrigger(); + } +} diff --git a/src/Squidex/Controllers/Api/Rules/Models/Triggers/ContentChangedTriggerDto.cs b/src/Squidex/Controllers/Api/Rules/Models/Triggers/ContentChangedTriggerDto.cs new file mode 100644 index 000000000..05af3cf26 --- /dev/null +++ b/src/Squidex/Controllers/Api/Rules/Models/Triggers/ContentChangedTriggerDto.cs @@ -0,0 +1,36 @@ +// ========================================================================== +// ContentChangedTriggerDto.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using NJsonSchema.Annotations; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Controllers.Api.Rules.Models.Triggers +{ + [JsonSchema("ContentChanged")] + public sealed class ContentChangedTriggerDto : RuleTriggerDto + { + /// + /// The schema settings. + /// + [Required] + public List Schemas { get; set; } + + public override RuleTrigger ToTrigger() + { + return new ContentChangedTrigger + { + Schemas = Schemas.Select(x => SimpleMapper.Map(x, new ContentChangedTriggerSchema())).ToList() + }; + } + } +} diff --git a/src/Squidex/Controllers/Api/Webhooks/Models/WebhookSchemaDto.cs b/src/Squidex/Controllers/Api/Rules/Models/Triggers/ContentChangedTriggerSchemaDto.cs similarity index 87% rename from src/Squidex/Controllers/Api/Webhooks/Models/WebhookSchemaDto.cs rename to src/Squidex/Controllers/Api/Rules/Models/Triggers/ContentChangedTriggerSchemaDto.cs index 251afb1e6..29e0c71fd 100644 --- a/src/Squidex/Controllers/Api/Webhooks/Models/WebhookSchemaDto.cs +++ b/src/Squidex/Controllers/Api/Rules/Models/Triggers/ContentChangedTriggerSchemaDto.cs @@ -1,5 +1,5 @@ // ========================================================================== -// WebhookSchemaDto.cs +// ContentChangedTriggerSchemaDto.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -8,9 +8,9 @@ using System; -namespace Squidex.Controllers.Api.Webhooks.Models +namespace Squidex.Controllers.Api.Rules.Models.Triggers { - public sealed class WebhookSchemaDto + public sealed class ContentChangedTriggerSchemaDto { /// /// The id of the schema. diff --git a/src/Squidex/Controllers/Api/Webhooks/Models/CreateWebhookDto.cs b/src/Squidex/Controllers/Api/Rules/Models/UpdateRuleDto.cs similarity index 50% rename from src/Squidex/Controllers/Api/Webhooks/Models/CreateWebhookDto.cs rename to src/Squidex/Controllers/Api/Rules/Models/UpdateRuleDto.cs index d7dcd7f46..bad710cc8 100644 --- a/src/Squidex/Controllers/Api/Webhooks/Models/CreateWebhookDto.cs +++ b/src/Squidex/Controllers/Api/Rules/Models/UpdateRuleDto.cs @@ -1,29 +1,23 @@ // ========================================================================== -// CreateWebhookDto.cs +// UpdateRuleDto.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; - -namespace Squidex.Controllers.Api.Webhooks.Models +namespace Squidex.Controllers.Api.Rules.Models { - public sealed class CreateWebhookDto + public sealed class UpdateRuleDto { /// - /// The url of the webhook. + /// The trigger properties. /// - [Required] - public Uri Url { get; set; } + public RuleTriggerDto Trigger { get; set; } /// - /// The schema settings. + /// The action properties. /// - [Required] - public List Schemas { get; set; } + public RuleActionDto Action { get; set; } } } diff --git a/src/Squidex/Controllers/Api/Rules/RulesController.cs b/src/Squidex/Controllers/Api/Rules/RulesController.cs new file mode 100644 index 000000000..7c05ac840 --- /dev/null +++ b/src/Squidex/Controllers/Api/Rules/RulesController.cs @@ -0,0 +1,245 @@ +// ========================================================================== +// RulesController.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using NodaTime; +using NSwag.Annotations; +using Squidex.Controllers.Api.Rules.Models; +using Squidex.Controllers.Api.Rules.Models.Converters; +using Squidex.Domain.Apps.Read.Rules.Repositories; +using Squidex.Domain.Apps.Write.Rules.Commands; +using Squidex.Infrastructure.CQRS.Commands; +using Squidex.Infrastructure.Reflection; +using Squidex.Pipeline; + +namespace Squidex.Controllers.Api.Rules +{ + /// + /// Manages and retrieves information about schemas. + /// + [ApiAuthorize] + [ApiExceptionFilter] + [AppApi] + [SwaggerTag(nameof(Rules))] + [MustBeAppDeveloper] + public sealed class RulesController : ControllerBase + { + private readonly IRuleRepository rulesRepository; + private readonly IRuleEventRepository ruleEventsRepository; + + public RulesController(ICommandBus commandBus, + IRuleRepository rulesRepository, + IRuleEventRepository ruleEventsRepository) + : base(commandBus) + { + this.rulesRepository = rulesRepository; + this.ruleEventsRepository = ruleEventsRepository; + } + + /// + /// Get rules. + /// + /// The name of the app. + /// + /// 200 => Rules returned. + /// 404 => App not found. + /// + [HttpGet] + [Route("apps/{app}/rules/")] + [ProducesResponseType(typeof(RuleDto[]), 200)] + [ApiCosts(1)] + public async Task GetRules(string app) + { + var rules = await rulesRepository.QueryByAppAsync(App.Id); + + var response = rules.Select(r => r.ToModel()); + + return Ok(response); + } + + /// + /// Create a new rule. + /// + /// The name of the app. + /// The rule object that needs to be added to the app. + /// + /// 201 => Rule created. + /// 400 => Rule is not valid. + /// 404 => App not found. + /// + [HttpPost] + [Route("apps/{app}/rules/")] + [ProducesResponseType(typeof(EntityCreatedDto), 201)] + [ProducesResponseType(typeof(ErrorDto), 400)] + [ApiCosts(1)] + public async Task PostRule(string app, [FromBody] CreateRuleDto request) + { + var command = request.ToCommand(); + + var context = await CommandBus.PublishAsync(command); + + var result = context.Result>(); + var response = new EntityCreatedDto { Id = result.IdOrValue.ToString(), Version = result.Version }; + + return CreatedAtAction(nameof(GetRules), new { app }, response); + } + + /// + /// Update a rule. + /// + /// The name of the app. + /// The id of the rule to update. + /// The rule object that needs to be added to the app. + /// + /// 204 => Rule updated. + /// 400 => Rule is not valid. + /// 404 => Rule or app not found. + /// + /// + /// All events for the specified schemas will be sent to the url. The timeout is 2 seconds. + /// + [HttpPut] + [Route("apps/{app}/rules/{id}/")] + [ProducesResponseType(typeof(ErrorDto), 400)] + [ApiCosts(1)] + public async Task PutRule(string app, Guid id, [FromBody] UpdateRuleDto request) + { + var command = request.ToCommand(); + + await CommandBus.PublishAsync(command); + + return NoContent(); + } + + /// + /// Enable a rule. + /// + /// The name of the app. + /// The id of the rule to enable. + /// + /// 204 => Rule enabled. + /// 400 => Rule already enabled. + /// 404 => Rule or app not found. + /// + [HttpPut] + [Route("apps/{app}/rules/{id}/enable/")] + [ApiCosts(1)] + public async Task EnableRule(string app, Guid id) + { + await CommandBus.PublishAsync(new EnableRule { RuleId = id }); + + return NoContent(); + } + + /// + /// Disable a rule. + /// + /// The name of the app. + /// The id of the rule to disable. + /// + /// 204 => Rule disabled. + /// 400 => Rule already disabled. + /// 404 => Rule or app not found. + /// + [HttpPut] + [Route("apps/{app}/rules/{id}/disable/")] + [ApiCosts(1)] + public async Task DisableRule(string app, Guid id) + { + await CommandBus.PublishAsync(new DisableRule { RuleId = id }); + + return NoContent(); + } + + /// + /// Delete a rule. + /// + /// The name of the app. + /// The id of the rule to delete. + /// + /// 204 => Rule has been deleted. + /// 404 => Rule or app not found. + /// + [HttpDelete] + [Route("apps/{app}/rules/{id}/")] + [ApiCosts(1)] + public async Task DeleteRule(string app, Guid id) + { + await CommandBus.PublishAsync(new DeleteRule { RuleId = id }); + + return NoContent(); + } + + /// + /// Get rule events. + /// + /// The name of the app. + /// The number of events to skip. + /// The number of events to take. + /// + /// 200 => Rule events returned. + /// 404 => App not found. + /// + [HttpGet] + [Route("apps/{app}/rules/events/")] + [ProducesResponseType(typeof(RuleEventsDto), 200)] + [ApiCosts(0)] + public async Task GetEvents(string app, [FromQuery] int skip = 0, [FromQuery] int take = 20) + { + var taskForItems = ruleEventsRepository.QueryByAppAsync(App.Id, skip, take); + var taskForCount = ruleEventsRepository.CountByAppAsync(App.Id); + + await Task.WhenAll(taskForItems, taskForCount); + + var response = new RuleEventsDto + { + Total = taskForCount.Result, + Items = taskForItems.Result.Select(x => + { + var itemModel = new RuleEventDto(); + + SimpleMapper.Map(x, itemModel); + SimpleMapper.Map(x.Job, itemModel); + + return itemModel; + }).ToArray() + }; + + return Ok(response); + } + + /// + /// Enqueue the event to be send. + /// + /// The name of the app. + /// The event to enqueue. + /// + /// 200 => Rule enqueued. + /// 404 => App or rule event not found. + /// + [HttpPut] + [Route("apps/{app}/rules/events/{id}/")] + [ApiCosts(0)] + public async Task PutEvent(string app, Guid id) + { + var entity = await ruleEventsRepository.FindAsync(id); + + if (entity == null) + { + return NotFound(); + } + + await ruleEventsRepository.EnqueueAsync(id, SystemClock.Instance.GetCurrentInstant()); + + return Ok(); + } + } +} diff --git a/src/Squidex/Controllers/Api/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs b/src/Squidex/Controllers/Api/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs index 12db361af..5b803f707 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs @@ -7,6 +7,7 @@ // ========================================================================== using System.Linq; +using Squidex.Controllers.Api.Schemas.Models.Fields; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Reflection; diff --git a/src/Squidex/Controllers/Api/Schemas/Models/FieldPropertiesDto.cs b/src/Squidex/Controllers/Api/Schemas/Models/FieldPropertiesDto.cs index 4271dd057..f7ac054e7 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/FieldPropertiesDto.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/FieldPropertiesDto.cs @@ -9,7 +9,8 @@ using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; using Newtonsoft.Json; -using Squidex.Controllers.Api.Schemas.Models.Converters; +using NJsonSchema.Converters; +using Squidex.Controllers.Api.Schemas.Models.Fields; using Squidex.Domain.Apps.Core.Schemas; namespace Squidex.Controllers.Api.Schemas.Models diff --git a/src/Squidex/Controllers/Api/Schemas/Models/AssetsFieldPropertiesDto.cs b/src/Squidex/Controllers/Api/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs similarity index 94% rename from src/Squidex/Controllers/Api/Schemas/Models/AssetsFieldPropertiesDto.cs rename to src/Squidex/Controllers/Api/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs index 1becd319c..b2e7a3d50 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/AssetsFieldPropertiesDto.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs @@ -10,7 +10,7 @@ using NJsonSchema.Annotations; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Reflection; -namespace Squidex.Controllers.Api.Schemas.Models +namespace Squidex.Controllers.Api.Schemas.Models.Fields { [JsonSchema("Assets")] public sealed class AssetsFieldPropertiesDto : FieldPropertiesDto diff --git a/src/Squidex/Controllers/Api/Schemas/Models/BooleanFieldPropertiesDto.cs b/src/Squidex/Controllers/Api/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs similarity index 95% rename from src/Squidex/Controllers/Api/Schemas/Models/BooleanFieldPropertiesDto.cs rename to src/Squidex/Controllers/Api/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs index 82c5c081d..8516d4b92 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/BooleanFieldPropertiesDto.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs @@ -12,7 +12,7 @@ using NJsonSchema.Annotations; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Reflection; -namespace Squidex.Controllers.Api.Schemas.Models +namespace Squidex.Controllers.Api.Schemas.Models.Fields { [JsonSchema("Boolean")] public sealed class BooleanFieldPropertiesDto : FieldPropertiesDto diff --git a/src/Squidex/Controllers/Api/Schemas/Models/DateTimeFieldPropertiesDto.cs b/src/Squidex/Controllers/Api/Schemas/Models/Fields/DateTimeFieldPropertiesDto.cs similarity index 96% rename from src/Squidex/Controllers/Api/Schemas/Models/DateTimeFieldPropertiesDto.cs rename to src/Squidex/Controllers/Api/Schemas/Models/Fields/DateTimeFieldPropertiesDto.cs index 2c2c68b13..4cdc78c49 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/DateTimeFieldPropertiesDto.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/Fields/DateTimeFieldPropertiesDto.cs @@ -13,7 +13,7 @@ using NodaTime; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Reflection; -namespace Squidex.Controllers.Api.Schemas.Models +namespace Squidex.Controllers.Api.Schemas.Models.Fields { [JsonSchema("DateTime")] public sealed class DateTimeFieldPropertiesDto : FieldPropertiesDto diff --git a/src/Squidex/Controllers/Api/Schemas/Models/GeolocationFieldPropertiesDto.cs b/src/Squidex/Controllers/Api/Schemas/Models/Fields/GeolocationFieldPropertiesDto.cs similarity index 95% rename from src/Squidex/Controllers/Api/Schemas/Models/GeolocationFieldPropertiesDto.cs rename to src/Squidex/Controllers/Api/Schemas/Models/Fields/GeolocationFieldPropertiesDto.cs index 4ad558c5c..76e1a1ce0 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/GeolocationFieldPropertiesDto.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/Fields/GeolocationFieldPropertiesDto.cs @@ -12,7 +12,7 @@ using NJsonSchema.Annotations; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Reflection; -namespace Squidex.Controllers.Api.Schemas.Models +namespace Squidex.Controllers.Api.Schemas.Models.Fields { [JsonSchema("Geolocation")] public sealed class GeolocationFieldPropertiesDto : FieldPropertiesDto diff --git a/src/Squidex/Controllers/Api/Schemas/Models/JsonFieldPropertiesDto.cs b/src/Squidex/Controllers/Api/Schemas/Models/Fields/JsonFieldPropertiesDto.cs similarity index 92% rename from src/Squidex/Controllers/Api/Schemas/Models/JsonFieldPropertiesDto.cs rename to src/Squidex/Controllers/Api/Schemas/Models/Fields/JsonFieldPropertiesDto.cs index 2957c8cce..68f9fe290 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/JsonFieldPropertiesDto.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/Fields/JsonFieldPropertiesDto.cs @@ -10,7 +10,7 @@ using NJsonSchema.Annotations; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Reflection; -namespace Squidex.Controllers.Api.Schemas.Models +namespace Squidex.Controllers.Api.Schemas.Models.Fields { [JsonSchema("Json")] public sealed class JsonFieldPropertiesDto : FieldPropertiesDto diff --git a/src/Squidex/Controllers/Api/Schemas/Models/NumberFieldPropertiesDto.cs b/src/Squidex/Controllers/Api/Schemas/Models/Fields/NumberFieldPropertiesDto.cs similarity index 96% rename from src/Squidex/Controllers/Api/Schemas/Models/NumberFieldPropertiesDto.cs rename to src/Squidex/Controllers/Api/Schemas/Models/Fields/NumberFieldPropertiesDto.cs index 3bd914770..ff5b8fc14 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/NumberFieldPropertiesDto.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/Fields/NumberFieldPropertiesDto.cs @@ -12,7 +12,7 @@ using NJsonSchema.Annotations; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Reflection; -namespace Squidex.Controllers.Api.Schemas.Models +namespace Squidex.Controllers.Api.Schemas.Models.Fields { [JsonSchema("Number")] public sealed class NumberFieldPropertiesDto : FieldPropertiesDto diff --git a/src/Squidex/Controllers/Api/Schemas/Models/ReferencesFieldPropertiesDto.cs b/src/Squidex/Controllers/Api/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs similarity index 95% rename from src/Squidex/Controllers/Api/Schemas/Models/ReferencesFieldPropertiesDto.cs rename to src/Squidex/Controllers/Api/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs index 38fbcb992..e35541086 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/ReferencesFieldPropertiesDto.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs @@ -11,7 +11,7 @@ using NJsonSchema.Annotations; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Reflection; -namespace Squidex.Controllers.Api.Schemas.Models +namespace Squidex.Controllers.Api.Schemas.Models.Fields { [JsonSchema("References")] public sealed class ReferencesFieldPropertiesDto : FieldPropertiesDto diff --git a/src/Squidex/Controllers/Api/Schemas/Models/StringFieldPropertiesDto.cs b/src/Squidex/Controllers/Api/Schemas/Models/Fields/StringFieldPropertiesDto.cs similarity index 97% rename from src/Squidex/Controllers/Api/Schemas/Models/StringFieldPropertiesDto.cs rename to src/Squidex/Controllers/Api/Schemas/Models/Fields/StringFieldPropertiesDto.cs index 32a3e00da..ebf41e4e7 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/StringFieldPropertiesDto.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/Fields/StringFieldPropertiesDto.cs @@ -12,7 +12,7 @@ using NJsonSchema.Annotations; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Reflection; -namespace Squidex.Controllers.Api.Schemas.Models +namespace Squidex.Controllers.Api.Schemas.Models.Fields { [JsonSchema("String")] public sealed class StringFieldPropertiesDto : FieldPropertiesDto diff --git a/src/Squidex/Controllers/Api/Schemas/Models/TagsFieldPropertiesDto.cs b/src/Squidex/Controllers/Api/Schemas/Models/Fields/TagsFieldPropertiesDto.cs similarity index 94% rename from src/Squidex/Controllers/Api/Schemas/Models/TagsFieldPropertiesDto.cs rename to src/Squidex/Controllers/Api/Schemas/Models/Fields/TagsFieldPropertiesDto.cs index 45e91f5f0..0769967ad 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/TagsFieldPropertiesDto.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/Fields/TagsFieldPropertiesDto.cs @@ -10,7 +10,7 @@ using NJsonSchema.Annotations; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Reflection; -namespace Squidex.Controllers.Api.Schemas.Models +namespace Squidex.Controllers.Api.Schemas.Models.Fields { [JsonSchema("Tags")] public sealed class TagsFieldPropertiesDto : FieldPropertiesDto diff --git a/src/Squidex/Controllers/Api/Webhooks/Models/WebhookDto.cs b/src/Squidex/Controllers/Api/Webhooks/Models/WebhookDto.cs deleted file mode 100644 index 29bb7670e..000000000 --- a/src/Squidex/Controllers/Api/Webhooks/Models/WebhookDto.cs +++ /dev/null @@ -1,89 +0,0 @@ -// ========================================================================== -// WebhookDto.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using NodaTime; -using Squidex.Infrastructure; - -namespace Squidex.Controllers.Api.Webhooks.Models -{ - public sealed class WebhookDto - { - /// - /// The id of the webhook. - /// - public Guid Id { get; set; } - - /// - /// The user that has created the webhook. - /// - [Required] - public RefToken CreatedBy { get; set; } - - /// - /// The user that has updated the webhook. - /// - [Required] - public RefToken LastModifiedBy { get; set; } - - /// - /// The date and time when the webhook has been created. - /// - public Instant Created { get; set; } - - /// - /// The date and time when the webhook has been modified last. - /// - public Instant LastModified { get; set; } - - /// - /// The version of the webhook. - /// - public int Version { get; set; } - - /// - /// The number of succceeded calls. - /// - public long TotalSucceeded { get; set; } - - /// - /// The number of failed calls. - /// - public long TotalFailed { get; set; } - - /// - /// The number of timedout calls. - /// - public long TotalTimedout { get; set; } - - /// - /// The average response time in milliseconds. - /// - public long AverageRequestTimeMs { get; set; } - - /// - /// The url of the webhook. - /// - [Required] - public Uri Url { get; set; } - - /// - /// The shared secret that is used to calculate the signature. - /// - [Required] - public string SharedSecret { get; set; } - - /// - /// The schema settings. - /// - [Required] - public List Schemas { get; set; } - } -} diff --git a/src/Squidex/Controllers/Api/Webhooks/WebhooksController.cs b/src/Squidex/Controllers/Api/Webhooks/WebhooksController.cs deleted file mode 100644 index e672333b5..000000000 --- a/src/Squidex/Controllers/Api/Webhooks/WebhooksController.cs +++ /dev/null @@ -1,220 +0,0 @@ -// ========================================================================== -// WebhooksController.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using NodaTime; -using NSwag.Annotations; -using Squidex.Controllers.Api.Webhooks.Models; -using Squidex.Domain.Apps.Core.Webhooks; -using Squidex.Domain.Apps.Read.Webhooks.Repositories; -using Squidex.Domain.Apps.Write.Webhooks.Commands; -using Squidex.Infrastructure.CQRS.Commands; -using Squidex.Infrastructure.Reflection; -using Squidex.Pipeline; - -namespace Squidex.Controllers.Api.Webhooks -{ - /// - /// Manages and retrieves information about schemas. - /// - [ApiAuthorize] - [ApiExceptionFilter] - [AppApi] - [SwaggerTag(nameof(Webhooks))] - [MustBeAppDeveloper] - public sealed class WebhooksController : ControllerBase - { - private readonly IWebhookRepository webhooksRepository; - private readonly IWebhookEventRepository webhookEventsRepository; - - public WebhooksController(ICommandBus commandBus, - IWebhookRepository webhooksRepository, - IWebhookEventRepository webhookEventsRepository) - : base(commandBus) - { - this.webhooksRepository = webhooksRepository; - this.webhookEventsRepository = webhookEventsRepository; - } - - /// - /// Get webhooks. - /// - /// The name of the app. - /// - /// 200 => Webhooks returned. - /// 404 => App not found. - /// - [HttpGet] - [Route("apps/{app}/webhooks/")] - [ProducesResponseType(typeof(WebhookDto[]), 200)] - [ApiCosts(1)] - public async Task GetWebhooks(string app) - { - var webhooks = await webhooksRepository.QueryByAppAsync(App.Id); - - var response = webhooks.Select(w => - { - var totalCount = w.TotalTimedout + w.TotalSucceeded + w.TotalFailed; - var totalAverage = totalCount == 0 ? 0 : w.TotalRequestTime / totalCount; - - var schemas = w.Schemas.Select(s => SimpleMapper.Map(s, new WebhookSchemaDto())).ToList(); - - return SimpleMapper.Map(w, new WebhookDto { AverageRequestTimeMs = totalAverage, Schemas = schemas }); - }); - - return Ok(response); - } - - /// - /// Create a new webhook. - /// - /// The name of the app. - /// The webhook object that needs to be added to the app. - /// - /// 201 => Webhook created. - /// 400 => Webhook is not valid. - /// 404 => App not found. - /// - /// - /// All events for the specified schemas will be sent to the url. The timeout is 2 seconds. - /// - [HttpPost] - [Route("apps/{app}/webhooks/")] - [ProducesResponseType(typeof(EntityCreatedDto), 201)] - [ProducesResponseType(typeof(ErrorDto), 400)] - [ApiCosts(1)] - public async Task PostWebhook(string app, [FromBody] CreateWebhookDto request) - { - var schemas = request.Schemas.Select(s => SimpleMapper.Map(s, new WebhookSchema())).ToList(); - - var command = new CreateWebhook { Url = request.Url, Schemas = schemas }; - - var context = await CommandBus.PublishAsync(command); - - var result = context.Result>(); - var response = new WebhookCreatedDto { Id = result.IdOrValue, SharedSecret = command.SharedSecret, Version = result.Version }; - - return CreatedAtAction(nameof(GetWebhooks), new { app }, response); - } - - /// - /// Update a webhook. - /// - /// The name of the app. - /// The id of the webhook to update. - /// The webhook object that needs to be added to the app. - /// - /// 203 => Webhook updated. - /// 400 => Webhook is not valid. - /// 404 => Webhook or app not found. - /// - /// - /// All events for the specified schemas will be sent to the url. The timeout is 2 seconds. - /// - [HttpPut] - [Route("apps/{app}/webhooks/{id}/")] - [ProducesResponseType(typeof(ErrorDto), 400)] - [ApiCosts(1)] - public async Task PutWebhook(string app, Guid id, [FromBody] CreateWebhookDto request) - { - var schemas = request.Schemas.Select(s => SimpleMapper.Map(s, new WebhookSchema())).ToList(); - - var command = new UpdateWebhook { WebhookId = id, Url = request.Url, Schemas = schemas }; - - await CommandBus.PublishAsync(command); - - return NoContent(); - } - - /// - /// Delete a webhook. - /// - /// The name of the app. - /// The id of the webhook to delete. - /// - /// 204 => Webhook has been deleted. - /// 404 => Webhook or app not found. - /// - [HttpDelete] - [Route("apps/{app}/webhooks/{id}/")] - [ApiCosts(1)] - public async Task DeleteWebhook(string app, Guid id) - { - await CommandBus.PublishAsync(new DeleteWebhook { WebhookId = id }); - - return NoContent(); - } - - /// - /// Get webhook events. - /// - /// The name of the app. - /// The number of events to skip. - /// The number of events to take. - /// - /// 200 => Webhook events returned. - /// 404 => App not found. - /// - [HttpGet] - [Route("apps/{app}/webhooks/events/")] - [ProducesResponseType(typeof(WebhookEventsDto), 200)] - [ApiCosts(0)] - public async Task GetEvents(string app, [FromQuery] int skip = 0, [FromQuery] int take = 20) - { - var taskForItems = webhookEventsRepository.QueryByAppAsync(App.Id, skip, take); - var taskForCount = webhookEventsRepository.CountByAppAsync(App.Id); - - await Task.WhenAll(taskForItems, taskForCount); - - var response = new WebhookEventsDto - { - Total = taskForCount.Result, - Items = taskForItems.Result.Select(x => - { - var itemModel = new WebhookEventDto(); - - SimpleMapper.Map(x, itemModel); - SimpleMapper.Map(x.Job, itemModel); - - return itemModel; - }).ToArray() - }; - - return Ok(response); - } - - /// - /// Enqueue the event to be send. - /// - /// The name of the app. - /// The event to enqueue. - /// - /// 200 => Webhook enqueued. - /// 404 => App or webhook event not found. - /// - [HttpPut] - [Route("apps/{app}/webhooks/events/{id}/")] - [ApiCosts(0)] - public async Task PutEvent(string app, Guid id) - { - var entity = await webhookEventsRepository.FindAsync(id); - - if (entity == null) - { - return NotFound(); - } - - await webhookEventsRepository.EnqueueAsync(id, SystemClock.Instance.GetCurrentInstant()); - - return Ok(); - } - } -} diff --git a/src/Squidex/Controllers/Api/Schemas/Models/Converters/JsonInheritanceConverter.cs b/src/Squidex/Controllers/JsonInheritanceConverter2.cs similarity index 95% rename from src/Squidex/Controllers/Api/Schemas/Models/Converters/JsonInheritanceConverter.cs rename to src/Squidex/Controllers/JsonInheritanceConverter2.cs index e55080547..5cd6ac724 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/Converters/JsonInheritanceConverter.cs +++ b/src/Squidex/Controllers/JsonInheritanceConverter2.cs @@ -16,9 +16,9 @@ using NJsonSchema.Annotations; #pragma warning disable SA1306 // Field names must begin with lower-case letter -namespace Squidex.Controllers.Api.Schemas.Models.Converters +namespace Squidex.Controllers { - public sealed class JsonInheritanceConverter : JsonConverter + public sealed class JsonInheritanceConverter2 : JsonConverter { private readonly string discriminator; @@ -54,7 +54,7 @@ namespace Squidex.Controllers.Api.Schemas.Models.Converters } } - public JsonInheritanceConverter(string discriminator) + public JsonInheritanceConverter2(string discriminator) { this.discriminator = discriminator; } diff --git a/tests/Squidex.Domain.Apps.Read.Tests/Rules/RuleEnqueuerTests.cs b/tests/Squidex.Domain.Apps.Read.Tests/Rules/RuleEnqueuerTests.cs new file mode 100644 index 000000000..500bc2a44 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Read.Tests/Rules/RuleEnqueuerTests.cs @@ -0,0 +1,102 @@ +// ========================================================================== +// RuleEnqueuerTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using NodaTime; +using Squidex.Domain.Apps.Core.HandleRules; +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.Contents; +using Squidex.Domain.Apps.Read.Rules.Repositories; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Events; +using Xunit; + +namespace Squidex.Domain.Apps.Read.Rules +{ + public class RuleEnqueuerTests + { + private readonly IRuleRepository ruleRepository = A.Fake(); + private readonly IRuleEventRepository ruleEventRepository = A.Fake(); + private readonly RuleService ruleService = A.Fake(); + private readonly Instant now = SystemClock.Instance.GetCurrentInstant(); + private readonly NamedId appId = new NamedId(Guid.NewGuid(), "my-app"); + private readonly RuleEnqueuer sut; + + public RuleEnqueuerTests() + { + sut = new RuleEnqueuer( + ruleEventRepository, + ruleRepository, + ruleService); + } + + [Fact] + public void Should_return_contents_filter_for_events_filter() + { + Assert.Equal(".*", sut.EventsFilter); + } + + [Fact] + public void Should_return_type_name_for_name() + { + Assert.Equal(typeof(RuleEnqueuer).Name, sut.Name); + } + + [Fact] + public Task Should_do_nothing_on_clear() + { + return sut.ClearAsync(); + } + + [Fact] + public async Task Should_update_repositories_on_with_jobs_from_sender() + { + var @event = Envelope.Create(new ContentCreated { AppId = appId }); + + var rule1 = new Rule(new ContentChangedTrigger(), new WebhookAction { Url = new Uri("https://squidex.io") }); + var rule2 = new Rule(new ContentChangedTrigger(), new WebhookAction { Url = new Uri("https://squidex.io") }); + var rule3 = new Rule(new ContentChangedTrigger(), new WebhookAction { Url = new Uri("https://squidex.io") }); + + var job1 = new RuleJob { Created = now }; + var job2 = new RuleJob { Created = now }; + + var ruleEntity1 = A.Fake(); + var ruleEntity2 = A.Fake(); + var ruleEntity3 = A.Fake(); + + A.CallTo(() => ruleEntity1.Rule).Returns(rule1); + A.CallTo(() => ruleEntity2.Rule).Returns(rule2); + A.CallTo(() => ruleEntity3.Rule).Returns(rule3); + + A.CallTo(() => ruleRepository.QueryCachedByAppAsync(appId.Id)) + .Returns(new List { ruleEntity1, ruleEntity2, ruleEntity3 }); + + A.CallTo(() => ruleService.CreateJob(rule1, @event)) + .Returns(job1); + + A.CallTo(() => ruleService.CreateJob(rule2, @event)) + .Returns(job2); + + A.CallTo(() => ruleService.CreateJob(rule3, @event)) + .Returns(null); + + await sut.On(@event); + + A.CallTo(() => ruleEventRepository.EnqueueAsync(job1, now)) + .MustHaveHappened(); + + A.CallTo(() => ruleEventRepository.EnqueueAsync(job2, now)) + .MustHaveHappened(); + } + } +} \ No newline at end of file From 32d877039fe0fda5c61b7f010460f571b587b6b1 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 31 Oct 2017 17:27:38 +0100 Subject: [PATCH 08/14] Converter reverted --- src/Squidex/Controllers/Api/Rules/Models/RuleActionDto.cs | 1 - src/Squidex/Controllers/Api/Rules/Models/RuleTriggerDto.cs | 1 - .../Controllers/Api/Schemas/Models/FieldPropertiesDto.cs | 1 - ...onInheritanceConverter2.cs => JsonInheritanceConverter.cs} | 4 ++-- 4 files changed, 2 insertions(+), 5 deletions(-) rename src/Squidex/Controllers/{JsonInheritanceConverter2.cs => JsonInheritanceConverter.cs} (96%) diff --git a/src/Squidex/Controllers/Api/Rules/Models/RuleActionDto.cs b/src/Squidex/Controllers/Api/Rules/Models/RuleActionDto.cs index e2bc33439..2b95ed5b6 100644 --- a/src/Squidex/Controllers/Api/Rules/Models/RuleActionDto.cs +++ b/src/Squidex/Controllers/Api/Rules/Models/RuleActionDto.cs @@ -8,7 +8,6 @@ using System.Runtime.Serialization; using Newtonsoft.Json; -using NJsonSchema.Converters; using Squidex.Controllers.Api.Rules.Models.Actions; using Squidex.Domain.Apps.Core.Rules; diff --git a/src/Squidex/Controllers/Api/Rules/Models/RuleTriggerDto.cs b/src/Squidex/Controllers/Api/Rules/Models/RuleTriggerDto.cs index 3d009cd50..f65ea3f46 100644 --- a/src/Squidex/Controllers/Api/Rules/Models/RuleTriggerDto.cs +++ b/src/Squidex/Controllers/Api/Rules/Models/RuleTriggerDto.cs @@ -8,7 +8,6 @@ using System.Runtime.Serialization; using Newtonsoft.Json; -using NJsonSchema.Converters; using Squidex.Controllers.Api.Rules.Models.Triggers; using Squidex.Domain.Apps.Core.Rules; diff --git a/src/Squidex/Controllers/Api/Schemas/Models/FieldPropertiesDto.cs b/src/Squidex/Controllers/Api/Schemas/Models/FieldPropertiesDto.cs index f7ac054e7..31e33fbad 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/FieldPropertiesDto.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/FieldPropertiesDto.cs @@ -9,7 +9,6 @@ using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; using Newtonsoft.Json; -using NJsonSchema.Converters; using Squidex.Controllers.Api.Schemas.Models.Fields; using Squidex.Domain.Apps.Core.Schemas; diff --git a/src/Squidex/Controllers/JsonInheritanceConverter2.cs b/src/Squidex/Controllers/JsonInheritanceConverter.cs similarity index 96% rename from src/Squidex/Controllers/JsonInheritanceConverter2.cs rename to src/Squidex/Controllers/JsonInheritanceConverter.cs index 5cd6ac724..5076a3791 100644 --- a/src/Squidex/Controllers/JsonInheritanceConverter2.cs +++ b/src/Squidex/Controllers/JsonInheritanceConverter.cs @@ -18,7 +18,7 @@ using NJsonSchema.Annotations; namespace Squidex.Controllers { - public sealed class JsonInheritanceConverter2 : JsonConverter + public sealed class JsonInheritanceConverter : JsonConverter { private readonly string discriminator; @@ -54,7 +54,7 @@ namespace Squidex.Controllers } } - public JsonInheritanceConverter2(string discriminator) + public JsonInheritanceConverter(string discriminator) { this.discriminator = discriminator; } From 486da51899deb3661f44ae115da839ce92c559f8 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 31 Oct 2017 20:37:07 +0100 Subject: [PATCH 09/14] Started with UI --- .../Triggers/ContentChangedTriggerSchema.cs | 2 +- .../Triggers/ContentChangedTriggerHandler.cs | 2 +- .../MongoRuleRepository_EventHandling.cs | 2 +- .../Rules/Commands/CreateRule.cs | 6 + .../ContentChangedTriggerSchemaDto.cs | 2 +- src/Squidex/app/app.routes.ts | 4 +- .../apps/pages/apps-page.component.html | 3 +- .../app/features/rules/declarations.ts | 13 + .../app/features/{webhooks => rules}/index.ts | 0 .../features/{webhooks => rules}/module.ts | 30 +- .../events/rule-events-page.component.html} | 4 +- .../events/rule-events-page.component.scss} | 0 .../events/rule-events-page.component.ts} | 24 +- .../actions/webhook-action.component.html | 29 ++ .../actions/webhook-action.component.scss | 2 + .../rules/actions/webhook-action.component.ts | 55 ++++ .../pages/rules/rule-wizard.component.html | 79 +++++ .../pages/rules/rule-wizard.component.scss | 38 +++ .../pages/rules/rule-wizard.component.ts | 96 ++++++ .../pages/rules/rules-page.component.html | 51 +++ .../pages/rules/rules-page.component.scss} | 0 .../rules/pages/rules/rules-page.component.ts | 66 ++++ .../content-changed-trigger.component.html | 71 ++++ .../content-changed-trigger.component.scss | 8 + .../content-changed-trigger.component.ts | 147 +++++++++ .../pages/schemas/schemas-page.component.html | 5 +- .../app/features/webhooks/declarations.ts | 10 - .../webhooks/pages/webhook.component.html | 149 --------- .../webhooks/pages/webhook.component.scss | 13 - .../webhooks/pages/webhook.component.ts | 174 ---------- .../pages/webhooks-page.component.html | 72 ---- .../webhooks/pages/webhooks-page.component.ts | 136 -------- .../app/framework/angular/keys.pipe.spec.ts | 26 ++ .../app/framework/angular/keys.pipe.ts | 18 + src/Squidex/app/framework/declarations.ts | 1 + src/Squidex/app/framework/module.ts | 3 + src/Squidex/app/shared/declarations-base.ts | 2 +- src/Squidex/app/shared/module.ts | 6 +- .../app/shared/services/rules.service.spec.ts | 310 ++++++++++++++++++ .../app/shared/services/rules.service.ts | 260 +++++++++++++++ .../shared/services/webhooks.service.spec.ts | 245 -------------- .../app/shared/services/webhooks.service.ts | 230 ------------- .../shell/pages/app/left-menu.component.html | 4 +- .../pages/internal/apps-menu.component.html | 5 +- src/Squidex/app/theme/_mixins.scss | 6 + src/Squidex/app/theme/_rules.scss | 56 ++++ src/Squidex/app/theme/icomoon/demo.html | 48 +++ .../app/theme/icomoon/fonts/icomoon.eot | Bin 21860 -> 21860 bytes .../app/theme/icomoon/fonts/icomoon.svg | 4 +- .../app/theme/icomoon/fonts/icomoon.ttf | Bin 21696 -> 21696 bytes .../app/theme/icomoon/fonts/icomoon.woff | Bin 21772 -> 21772 bytes src/Squidex/app/theme/icomoon/selection.json | 4 +- src/Squidex/app/theme/icomoon/style.css | 19 +- src/Squidex/app/theme/theme.scss | 1 + 54 files changed, 1457 insertions(+), 1084 deletions(-) create mode 100644 src/Squidex/app/features/rules/declarations.ts rename src/Squidex/app/features/{webhooks => rules}/index.ts (100%) rename src/Squidex/app/features/{webhooks => rules}/module.ts (53%) rename src/Squidex/app/features/{webhooks/pages/webhook-events-page.component.html => rules/pages/events/rule-events-page.component.html} (96%) rename src/Squidex/app/features/{webhooks/pages/webhook-events-page.component.scss => rules/pages/events/rule-events-page.component.scss} (100%) rename src/Squidex/app/features/{webhooks/pages/webhook-events-page.component.ts => rules/pages/events/rule-events-page.component.ts} (71%) create mode 100644 src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.html create mode 100644 src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.scss create mode 100644 src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.ts create mode 100644 src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html create mode 100644 src/Squidex/app/features/rules/pages/rules/rule-wizard.component.scss create mode 100644 src/Squidex/app/features/rules/pages/rules/rule-wizard.component.ts create mode 100644 src/Squidex/app/features/rules/pages/rules/rules-page.component.html rename src/Squidex/app/features/{webhooks/pages/webhooks-page.component.scss => rules/pages/rules/rules-page.component.scss} (100%) create mode 100644 src/Squidex/app/features/rules/pages/rules/rules-page.component.ts create mode 100644 src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html create mode 100644 src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.scss create mode 100644 src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts delete mode 100644 src/Squidex/app/features/webhooks/declarations.ts delete mode 100644 src/Squidex/app/features/webhooks/pages/webhook.component.html delete mode 100644 src/Squidex/app/features/webhooks/pages/webhook.component.scss delete mode 100644 src/Squidex/app/features/webhooks/pages/webhook.component.ts delete mode 100644 src/Squidex/app/features/webhooks/pages/webhooks-page.component.html delete mode 100644 src/Squidex/app/features/webhooks/pages/webhooks-page.component.ts create mode 100644 src/Squidex/app/framework/angular/keys.pipe.spec.ts create mode 100644 src/Squidex/app/framework/angular/keys.pipe.ts create mode 100644 src/Squidex/app/shared/services/rules.service.spec.ts create mode 100644 src/Squidex/app/shared/services/rules.service.ts delete mode 100644 src/Squidex/app/shared/services/webhooks.service.spec.ts delete mode 100644 src/Squidex/app/shared/services/webhooks.service.ts create mode 100644 src/Squidex/app/theme/_rules.scss diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchema.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchema.cs index 600fcaf6e..439ead886 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchema.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchema.cs @@ -20,6 +20,6 @@ namespace Squidex.Domain.Apps.Core.Rules.Triggers public bool SendDelete { get; set; } - public bool SendPublish { get; set; } + public bool SendChange { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs index e0b8ef424..e65686d8e 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs @@ -43,7 +43,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Triggers (schema.SendCreate && @event is ContentCreated) || (schema.SendUpdate && @event is ContentUpdated) || (schema.SendDelete && @event is ContentDeleted) || - (schema.SendPublish && @event is ContentStatusChanged statusChanged && statusChanged.Status == Status.Published); + (schema.SendChange && @event is ContentStatusChanged statusChanged && statusChanged.Status == Status.Published); } } } diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository_EventHandling.cs index db2e3db37..26be2002a 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository_EventHandling.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository_EventHandling.cs @@ -25,7 +25,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Rules public string EventsFilter { - get { return "^rules-"; } + get { return "^rule-"; } } public Task On(Envelope @event) diff --git a/src/Squidex.Domain.Apps.Write/Rules/Commands/CreateRule.cs b/src/Squidex.Domain.Apps.Write/Rules/Commands/CreateRule.cs index 34aaa4d21..1c6bafd7e 100644 --- a/src/Squidex.Domain.Apps.Write/Rules/Commands/CreateRule.cs +++ b/src/Squidex.Domain.Apps.Write/Rules/Commands/CreateRule.cs @@ -6,9 +6,15 @@ // All rights reserved. // ========================================================================== +using System; + namespace Squidex.Domain.Apps.Write.Rules.Commands { public sealed class CreateRule : RuleEditCommand { + public CreateRule() + { + RuleId = Guid.NewGuid(); + } } } diff --git a/src/Squidex/Controllers/Api/Rules/Models/Triggers/ContentChangedTriggerSchemaDto.cs b/src/Squidex/Controllers/Api/Rules/Models/Triggers/ContentChangedTriggerSchemaDto.cs index 29e0c71fd..05215c376 100644 --- a/src/Squidex/Controllers/Api/Rules/Models/Triggers/ContentChangedTriggerSchemaDto.cs +++ b/src/Squidex/Controllers/Api/Rules/Models/Triggers/ContentChangedTriggerSchemaDto.cs @@ -35,6 +35,6 @@ namespace Squidex.Controllers.Api.Rules.Models.Triggers /// /// True, when to send a message for published events. /// - public bool SendPublish { get; set; } + public bool SendChange { get; set; } } } diff --git a/src/Squidex/app/app.routes.ts b/src/Squidex/app/app.routes.ts index 3b4e8fea7..2505c716a 100644 --- a/src/Squidex/app/app.routes.ts +++ b/src/Squidex/app/app.routes.ts @@ -64,8 +64,8 @@ export const routes: Routes = [ loadChildren: './features/assets/module#SqxFeatureAssetsModule' }, { - path: 'webhooks', - loadChildren: './features/webhooks/module#SqxFeatureWebhooksModule' + path: 'rules', + loadChildren: './features/rules/module#SqxFeatureRulesModule' }, { path: 'settings', diff --git a/src/Squidex/app/features/apps/pages/apps-page.component.html b/src/Squidex/app/features/apps/pages/apps-page.component.html index e485a137e..bb08afd66 100644 --- a/src/Squidex/app/features/apps/pages/apps-page.component.html +++ b/src/Squidex/app/features/apps/pages/apps-page.component.html @@ -31,7 +31,8 @@ diff --git a/src/Squidex/app/features/rules/declarations.ts b/src/Squidex/app/features/rules/declarations.ts new file mode 100644 index 000000000..c2f70d3ef --- /dev/null +++ b/src/Squidex/app/features/rules/declarations.ts @@ -0,0 +1,13 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +export * from './pages/rules/actions/webhook-action.component'; +export * from './pages/rules/triggers/content-changed-trigger.component'; +export * from './pages/rules/rule-wizard.component'; +export * from './pages/rules/rules-page.component'; + +export * from './pages/events/rule-events-page.component'; \ No newline at end of file diff --git a/src/Squidex/app/features/webhooks/index.ts b/src/Squidex/app/features/rules/index.ts similarity index 100% rename from src/Squidex/app/features/webhooks/index.ts rename to src/Squidex/app/features/rules/index.ts diff --git a/src/Squidex/app/features/webhooks/module.ts b/src/Squidex/app/features/rules/module.ts similarity index 53% rename from src/Squidex/app/features/webhooks/module.ts rename to src/Squidex/app/features/rules/module.ts index 12a58e960..23a869ede 100644 --- a/src/Squidex/app/features/webhooks/module.ts +++ b/src/Squidex/app/features/rules/module.ts @@ -9,32 +9,26 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { - HelpComponent, SqxFrameworkModule, SqxSharedModule } from 'shared'; import { - WebhookComponent, - WebhookEventsPageComponent, - WebhooksPageComponent + ContentChangedTriggerComponent, + RuleEventsPageComponent, + RulesPageComponent, + RuleWizardComponent, + WebhookActionComponent } from './declarations'; const routes: Routes = [ { path: '', - component: WebhooksPageComponent, + component: RulesPageComponent, children: [ { path: 'events', - component: WebhookEventsPageComponent - }, - { - path: 'help', - component: HelpComponent, - data: { - helpPage: '05-integrated/webhooks' - } + component: RuleEventsPageComponent } ] } @@ -47,9 +41,11 @@ const routes: Routes = [ RouterModule.forChild(routes) ], declarations: [ - WebhookComponent, - WebhookEventsPageComponent, - WebhooksPageComponent + ContentChangedTriggerComponent, + RuleEventsPageComponent, + RulesPageComponent, + RuleWizardComponent, + WebhookActionComponent ] }) -export class SqxFeatureWebhooksModule { } \ No newline at end of file +export class SqxFeatureRulesModule { } \ No newline at end of file diff --git a/src/Squidex/app/features/webhooks/pages/webhook-events-page.component.html b/src/Squidex/app/features/rules/pages/events/rule-events-page.component.html similarity index 96% rename from src/Squidex/app/features/webhooks/pages/webhook-events-page.component.html rename to src/Squidex/app/features/rules/pages/events/rule-events-page.component.html index b68b5b51c..9bb9ebd9f 100644 --- a/src/Squidex/app/features/webhooks/pages/webhook-events-page.component.html +++ b/src/Squidex/app/features/rules/pages/events/rule-events-page.component.html @@ -1,4 +1,4 @@ - +
@@ -57,7 +57,7 @@ {{event.eventName}} - {{event.requestUrl}} + {{event.description}} {{event.created | sqxFromNow}} diff --git a/src/Squidex/app/features/webhooks/pages/webhook-events-page.component.scss b/src/Squidex/app/features/rules/pages/events/rule-events-page.component.scss similarity index 100% rename from src/Squidex/app/features/webhooks/pages/webhook-events-page.component.scss rename to src/Squidex/app/features/rules/pages/events/rule-events-page.component.scss diff --git a/src/Squidex/app/features/webhooks/pages/webhook-events-page.component.ts b/src/Squidex/app/features/rules/pages/events/rule-events-page.component.ts similarity index 71% rename from src/Squidex/app/features/webhooks/pages/webhook-events-page.component.ts rename to src/Squidex/app/features/rules/pages/events/rule-events-page.component.ts index 53bed278e..6e5fe2657 100644 --- a/src/Squidex/app/features/webhooks/pages/webhook-events-page.component.ts +++ b/src/Squidex/app/features/rules/pages/events/rule-events-page.component.ts @@ -14,23 +14,23 @@ import { DialogService, ImmutableArray, Pager, - WebhookEventDto, - WebhooksService + RuleEventDto, + RulesService } from 'shared'; @Component({ - selector: 'sqx-webhook-events-page', - styleUrls: ['./webhook-events-page.component.scss'], - templateUrl: './webhook-events-page.component.html' + selector: 'sqx-rule-events-page', + styleUrls: ['./rule-events-page.component.scss'], + templateUrl: './rule-events-page.component.html' }) -export class WebhookEventsPageComponent extends AppComponentBase implements OnInit { - public eventsItems = ImmutableArray.empty(); +export class RuleEventsPageComponent extends AppComponentBase implements OnInit { + public eventsItems = ImmutableArray.empty(); public eventsPager = new Pager(0); public selectedEventId: string | null = null; constructor(dialogs: DialogService, appsStore: AppsStoreService, authService: AuthService, - private readonly webhooksService: WebhooksService + private readonly rulesService: RulesService ) { super(dialogs, appsStore, authService); } @@ -41,7 +41,7 @@ export class WebhookEventsPageComponent extends AppComponentBase implements OnIn public load(showInfo = false) { this.appNameOnce() - .switchMap(app => this.webhooksService.getEvents(app, this.eventsPager.pageSize, this.eventsPager.skip)) + .switchMap(app => this.rulesService.getEvents(app, this.eventsPager.pageSize, this.eventsPager.skip)) .subscribe(dtos => { this.eventsItems = ImmutableArray.of(dtos.items); this.eventsPager = this.eventsPager.setCount(dtos.total); @@ -54,11 +54,11 @@ export class WebhookEventsPageComponent extends AppComponentBase implements OnIn }); } - public enqueueEvent(event: WebhookEventDto) { + public enqueueEvent(event: RuleEventDto) { this.appNameOnce() - .switchMap(app => this.webhooksService.enqueueEvent(app, event.id)) + .switchMap(app => this.rulesService.enqueueEvent(app, event.id)) .subscribe(() => { - this.notifyInfo('Events enqueued. Will be send in a few seconds.'); + this.notifyInfo('Events enqueued. Will be resend in a few seconds.'); }, error => { this.notifyError(error); }); diff --git a/src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.html b/src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.html new file mode 100644 index 000000000..62f2231d5 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.html @@ -0,0 +1,29 @@ +
+
+ + +
+ + + + + + The url where the events will be sent to. + +
+
+ +
+ + +
+ + + + + + The shared secret will be used to add a header X-Signature=Sha256(RequestBody + Secret) + +
+
+
\ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.scss b/src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.scss new file mode 100644 index 000000000..fbb752506 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.scss @@ -0,0 +1,2 @@ +@import '_vars'; +@import '_mixins'; \ No newline at end of file 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 new file mode 100644 index 000000000..d11b3e8c8 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.ts @@ -0,0 +1,55 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { FormBuilder, Validators } from '@angular/forms'; + +@Component({ + selector: 'sqx-webhook-action', + styleUrls: ['./webhook-action.component.scss'], + templateUrl: './webhook-action.component.html' +}) +export class WebhookActionComponent implements OnInit { + @Input() + public action: any; + + @Output() + public actionChanged = new EventEmitter(); + + public actionFormSubmitted = false; + public actionForm = + this.formBuilder.group({ + url: ['', + [ + Validators.required + ]], + sharedSecret: [''] + }); + + constructor( + private readonly formBuilder: FormBuilder + ) { + } + + public ngOnInit() { + this.action = Object.assign({}, { url: '', sharedSecret: '' }, this.action || {}); + + this.actionFormSubmitted = false; + this.actionForm.reset(); + this.actionForm.setValue(this.action); + } + + public save() { + this.actionFormSubmitted = true; + + if (this.actionForm.valid) { + const action = this.actionForm.value; + + this.actionChanged.emit(action); + } + } +} \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html b/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html new file mode 100644 index 000000000..bf9f21d62 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html @@ -0,0 +1,79 @@ + \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.scss b/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.scss new file mode 100644 index 000000000..d17243924 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.scss @@ -0,0 +1,38 @@ +@import '_vars'; +@import '_mixins'; + +.modal { + &-dialog { + height: 70%; + } + + &-content { + min-height: 100%; + max-height: 100%; + } + + &-body { + overflow-y: auto; + } + + &-header { + @include flex-shrink(0); + } + + &-footer { + @include flex-shrink(0); + } + + &-form { + padding-top: 2em; + padding-bottom: 0; + } +} + +.clearfix { + width: 100%; +} + +.rule-element { + margin-right: .5rem; +} \ 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 new file mode 100644 index 000000000..019951711 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.ts @@ -0,0 +1,96 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'; + +import { + AppComponentBase, + AppsStoreService, + AuthService, + CreateRuleDto, + DateTime, + DialogService, + fadeAnimation, + ruleActions, + ruleTriggers, + RuleDto, + RulesService, + SchemaDto +} from 'shared'; + +@Component({ + selector: 'sqx-rule-wizard', + styleUrls: ['./rule-wizard.component.scss'], + templateUrl: './rule-wizard.component.html', + animations: [ + fadeAnimation + ] +}) +export class RuleWizardComponent extends AppComponentBase { + public ruleActions = ruleActions; + public ruleTriggers = ruleTriggers; + + public triggerType: string; + public trigger: any = {}; + public actionType: string; + public action: any = {}; + public step = 1; + + @ViewChild('triggerControl') + public triggerControl: any; + + @ViewChild('actionControl') + public actionControl: any; + + @Output() + public cancelled = new EventEmitter(); + + @Output() + public created = new EventEmitter(); + + @Input() + public schemas: SchemaDto[]; + + constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService, + private readonly rulesService: RulesService + ) { + super(dialogs, apps, authService); + } + + public selectTriggerType(type: string) { + this.triggerType = type; + this.step++; + } + + public selectTrigger(value: any) { + this.trigger = Object.assign({}, value, { triggerType: this.triggerType }); + this.step++; + } + + public selectActionType(type: string) { + this.actionType = type; + this.step++; + } + + public selectAction(value: any) { + this.action = Object.assign({}, value, { actionType: this.actionType }); + + const requestDto = new CreateRuleDto(this.trigger, this.action); + + this.appNameOnce() + .switchMap(app => this.rulesService.postRule(app, requestDto, this.authService.user!.id, DateTime.now())) + .subscribe(dto => { + this.created.emit(dto); + }, error => { + this.notifyError(error); + }); + } + + public cancel() { + this.cancelled.emit(); + } +} \ No newline at end of file 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 new file mode 100644 index 000000000..57dc83714 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/rules-page.component.html @@ -0,0 +1,51 @@ + + + +
+
+
+ + + + + + +
+ +

Rules

+
+ + + + +
+ +
+
+
+ No Rule created yet. +
+
+
+ + +
+ + + +
+
+ + + + \ No newline at end of file diff --git a/src/Squidex/app/features/webhooks/pages/webhooks-page.component.scss b/src/Squidex/app/features/rules/pages/rules/rules-page.component.scss similarity index 100% rename from src/Squidex/app/features/webhooks/pages/webhooks-page.component.scss rename to src/Squidex/app/features/rules/pages/rules/rules-page.component.scss 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 new file mode 100644 index 000000000..212be1d14 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/rules-page.component.ts @@ -0,0 +1,66 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Component, OnInit } from '@angular/core'; + +import { + AppComponentBase, + AppsStoreService, + AuthService, + DialogService, + fadeAnimation, + ImmutableArray, + ModalView, + RuleDto, + RulesService, + SchemaDto, + SchemasService +} from 'shared'; + +@Component({ + selector: 'sqx-rules-page', + styleUrls: ['./rules-page.component.scss'], + templateUrl: './rules-page.component.html', + animations: [ + fadeAnimation + ] +}) +export class RulesPageComponent extends AppComponentBase implements OnInit { + public addRuleDialog = new ModalView(true, false); + + public rules: ImmutableArray; + public schemas: SchemaDto[]; + + constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService, + private readonly schemasService: SchemasService, + private readonly rulesService: RulesService + ) { + super(dialogs, apps, authService); + } + + public ngOnInit() { + this.load(); + } + + public load(showInfo = false) { + this.appNameOnce() + .switchMap(app => + this.schemasService.getSchemas(app) + .combineLatest(this.rulesService.getRules(app), + (s, w) => { return { rules: w, schemas: s }; })) + .subscribe(dtos => { + this.schemas = dtos.schemas; + this.rules = ImmutableArray.of(dtos.rules); + + if (showInfo) { + this.notifyInfo('Rules reloaded.'); + } + }, error => { + this.notifyError(error); + }); + } +} diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html b/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html new file mode 100644 index 000000000..ff7321f40 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Schema + + All + + C + + U + + D + + C +
+ {{schema.schema.name}} + + + + + + + + + + + + +
+ +
+
+
+ +
+ + +
+
\ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.scss b/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.scss new file mode 100644 index 000000000..4869322fb --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.scss @@ -0,0 +1,8 @@ +@import '_vars'; +@import '_mixins'; + +.section { + border-top: 1px solid $color-border; + padding-top: 1rem; + padding-bottom: 0; +} \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts b/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts new file mode 100644 index 000000000..cecb4385f --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts @@ -0,0 +1,147 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; + +import { + ImmutableArray, + SchemaDto +} from 'shared'; + +export interface TriggerSchemaForm { + schema: SchemaDto; + sendAll: boolean; + sendCreate: boolean; + sendUpdate: boolean; + sendDelete: boolean; + sendChange: boolean; +} + +@Component({ + selector: 'sqx-content-changed-trigger', + styleUrls: ['./content-changed-trigger.component.scss'], + templateUrl: './content-changed-trigger.component.html' +}) +export class ContentChangedTriggerComponent implements OnInit { + @Input() + public schemas: SchemaDto[]; + + @Input() + public trigger: any; + + @Output() + public triggerChanged = new EventEmitter(); + + public triggerSchemas: ImmutableArray; + + public schemaToAdd: SchemaDto; + public schemasToAdd: ImmutableArray; + + public get hasSchema() { + return !!this.schemaToAdd; + } + + public ngOnInit() { + const triggerSchemas: any[] = (this.trigger.schemas = this.trigger.schemas || []); + + this.triggerSchemas = + ImmutableArray.of( + triggerSchemas.map(triggerSchema => { + const schema = this.schemas.find(s => s.id === triggerSchema.schemaId); + + if (schema) { + return this.updateSendAll({ + schema: schema, + sendAll: false, + sendCreate: triggerSchema.sendCreate, + sendUpdate: triggerSchema.sendUpdate, + sendDelete: triggerSchema.sendDelete, + sendChange: triggerSchema.sendChange + }); + } else { + return null; + } + }).filter(s => s !== null).map(s => s!)).sortByStringAsc(s => s.schema.name); + + this.schemasToAdd = + ImmutableArray.of( + this.schemas.filter(schema => + !triggerSchemas.find(s => s.schemaId === schema.id))) + .sortByStringAsc(x => x.name); + + this.schemaToAdd = this.schemasToAdd.values[0]; + } + + public save() { + const schemas = + this.triggerSchemas.values.map(s => { + return { + schemaId: s.schema.id, + sendCreate: s.sendCreate, + sendUpdate: s.sendUpdate, + sendDelete: s.sendDelete, + sendChange: s.sendChange + }; + }); + + this.triggerChanged.emit({ schemas }); + } + + public removeSchema(schemaForm: TriggerSchemaForm) { + this.triggerSchemas = this.triggerSchemas.remove(schemaForm); + + this.schemasToAdd = this.schemasToAdd.push(schemaForm.schema).sortByStringAsc(x => x.name); + this.schemaToAdd = this.schemasToAdd.values[0]; + } + + public addSchema() { + this.triggerSchemas = + this.triggerSchemas.push( + this.updateSendAll({ + schema: this.schemaToAdd, + sendAll: false, + sendCreate: false, + sendUpdate: false, + sendDelete: false, + sendChange: false + })).sortByStringAsc(x => x.schema.name); + + this.schemasToAdd = this.schemasToAdd.remove(this.schemaToAdd).sortByStringAsc(x => x.name); + this.schemaToAdd = this.schemasToAdd.values[0]; + } + + public toggle(schemaForm: TriggerSchemaForm, property: string) { + const newSchema = this.updateSendAll(Object.assign({}, schemaForm, { [property]: !schemaForm[property] })); + + this.triggerSchemas = this.triggerSchemas.replace(schemaForm, newSchema); + } + + public toggleAll(schemaForm: TriggerSchemaForm) { + const newSchema = this.updateAll({ schema: schemaForm.schema }, !schemaForm.sendAll); + + this.triggerSchemas = this.triggerSchemas.replace(schemaForm, newSchema); + } + + private updateAll(schemaForm: TriggerSchemaForm, value: boolean): TriggerSchemaForm { + schemaForm.sendAll = value; + schemaForm.sendCreate = value; + schemaForm.sendUpdate = value; + schemaForm.sendDelete = value; + schemaForm.sendChange = value; + return schemaForm; + } + + private updateSendAll(schemaForm: TriggerSchemaForm): TriggerSchemaForm { + schemaForm.sendAll = + schemaForm.sendCreate && + schemaForm.sendUpdate && + schemaForm.sendDelete && + schemaForm.sendChange; + + return schemaForm; + } +} \ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html index 29831ee93..05c321f29 100644 --- a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html +++ b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html @@ -66,7 +66,10 @@ diff --git a/src/Squidex/app/features/webhooks/declarations.ts b/src/Squidex/app/features/webhooks/declarations.ts deleted file mode 100644 index 84b17769c..000000000 --- a/src/Squidex/app/features/webhooks/declarations.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Sebastian Stehle. All rights reserved - */ - -export * from './pages/webhook-events-page.component'; -export * from './pages/webhook.component'; -export * from './pages/webhooks-page.component'; \ No newline at end of file diff --git a/src/Squidex/app/features/webhooks/pages/webhook.component.html b/src/Squidex/app/features/webhooks/pages/webhook.component.html deleted file mode 100644 index 2f015bbb6..000000000 --- a/src/Squidex/app/features/webhooks/pages/webhook.component.html +++ /dev/null @@ -1,149 +0,0 @@ -
-
- - - - - - - - - - - - - - - - - - - - - -
- - - -
Url: - - - -
Secret: - - - -
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Schema - - All - - C - - U - - D - - P -
- {{schema.schema.name}} - - - - - - - - - - - - -
-
- -
-
-
- -
- - -
-
- -
-
-
- - {{webhook.totalSucceeded}} - -
-
- - {{webhook.totalFailed}} - -
-
- - {{webhook.totalTimedout}} - -
-
- - {{webhook.averageRequestTimeMs}} ms - -
-
-
-
\ No newline at end of file diff --git a/src/Squidex/app/features/webhooks/pages/webhook.component.scss b/src/Squidex/app/features/webhooks/pages/webhook.component.scss deleted file mode 100644 index 61873b1a2..000000000 --- a/src/Squidex/app/features/webhooks/pages/webhook.component.scss +++ /dev/null @@ -1,13 +0,0 @@ -@import '_vars'; -@import '_mixins'; - -.schemas-control { - width: 18rem; -} - -.webhook-section { - border-top: 1px solid $color-border; - margin-left: -1.25rem; - margin-right: -1.25rem; - padding: 1rem 1.25rem 0; -} \ No newline at end of file diff --git a/src/Squidex/app/features/webhooks/pages/webhook.component.ts b/src/Squidex/app/features/webhooks/pages/webhook.component.ts deleted file mode 100644 index 7ac843b2d..000000000 --- a/src/Squidex/app/features/webhooks/pages/webhook.component.ts +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Sebastian Stehle. All rights reserved - */ - -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { FormBuilder, Validators } from '@angular/forms'; - -import { - ImmutableArray, - SchemaDto, - UpdateWebhookDto, - WebhookDto, - WebhookSchemaDto -} from 'shared'; - -export interface WebhookSchemaForm { - schema: SchemaDto; - sendAll: boolean; - sendCreate: boolean; - sendUpdate: boolean; - sendDelete: boolean; - sendPublish: boolean; -} - -@Component({ - selector: 'sqx-webhook', - styleUrls: ['./webhook.component.scss'], - templateUrl: './webhook.component.html' -}) -export class WebhookComponent implements OnInit { - @Output() - public deleting = new EventEmitter(); - - @Output() - public updating = new EventEmitter(); - - @Input() - public allSchemas: SchemaDto[]; - - @Input() - public webhook: WebhookDto; - - public schemas: ImmutableArray; - - public schemaToAdd: SchemaDto; - public schemasToAdd: ImmutableArray; - - public webhookForm = - this.formBuilder.group({ - url: ['', - [ - Validators.required - ]] - }); - - public get hasUrl() { - return this.webhookForm.controls['url'].value && this.webhookForm.controls['url'].value.length > 0; - } - - public get hasSchema() { - return !!this.schemaToAdd; - } - - constructor( - private readonly formBuilder: FormBuilder - ) { - } - - public ngOnInit() { - this.webhookForm.controls['url'].setValue(this.webhook.url); - - this.schemas = - ImmutableArray.of( - this.webhook.schemas.map(webhookSchema => { - const schema = this.allSchemas.find(s => s.id === webhookSchema.schemaId); - - if (schema) { - return this.updateSendAll({ - schema: schema, - sendAll: false, - sendCreate: webhookSchema.sendCreate, - sendUpdate: webhookSchema.sendUpdate, - sendDelete: webhookSchema.sendDelete, - sendPublish: webhookSchema.sendPublish - }); - } else { - return null; - } - }).filter(w => w !== null).map(w => w!)).sortByStringAsc(x => x.schema.name); - - this.schemasToAdd = - ImmutableArray.of( - this.allSchemas.filter(schema => - !this.webhook.schemas.find(w => w.schemaId === schema.id))) - .sortByStringAsc(x => x.name); - this.schemaToAdd = this.schemasToAdd.values[0]; - } - - public removeSchema(schemaForm: WebhookSchemaForm) { - this.schemas = this.schemas.remove(schemaForm); - - this.schemasToAdd = this.schemasToAdd.push(schemaForm.schema).sortByStringAsc(x => x.name); - this.schemaToAdd = this.schemasToAdd.values[0]; - } - - public addSchema() { - this.schemas = - this.schemas.push( - this.updateSendAll({ - schema: this.schemaToAdd, - sendAll: false, - sendCreate: false, - sendUpdate: false, - sendDelete: false, - sendPublish: false - })).sortByStringAsc(x => x.schema.name); - - this.schemasToAdd = this.schemasToAdd.remove(this.schemaToAdd).sortByStringAsc(x => x.name); - this.schemaToAdd = this.schemasToAdd.values[0]; - } - - public save() { - const requestDto = - new UpdateWebhookDto( - this.webhookForm.controls['url'].value, - this.schemas.values.map(schema => - new WebhookSchemaDto( - schema.schema.id, - schema.sendCreate, - schema.sendUpdate, - schema.sendDelete, - schema.sendPublish))); - - this.emitUpdating(requestDto); - } - - public toggle(schemaForm: WebhookSchemaForm, property: string) { - const newSchema = this.updateSendAll(Object.assign({}, schemaForm, { [property]: !schemaForm[property] })); - - this.schemas = this.schemas.replace(schemaForm, newSchema); - } - - public toggleAll(schemaForm: WebhookSchemaForm) { - const newSchema = this.updateAll({ schema: schemaForm.schema }, !schemaForm.sendAll); - - this.schemas = this.schemas.replace(schemaForm, newSchema); - } - - private emitUpdating(dto: UpdateWebhookDto) { - this.updating.emit(dto); - } - - private updateAll(schemaForm: WebhookSchemaForm, value: boolean): WebhookSchemaForm { - schemaForm.sendAll = value; - schemaForm.sendCreate = value; - schemaForm.sendUpdate = value; - schemaForm.sendDelete = value; - schemaForm.sendPublish = value; - return schemaForm; - } - - private updateSendAll(schemaForm: WebhookSchemaForm): WebhookSchemaForm { - schemaForm.sendAll = - schemaForm.sendCreate && - schemaForm.sendUpdate && - schemaForm.sendDelete && - schemaForm.sendPublish; - - return schemaForm; - } -} diff --git a/src/Squidex/app/features/webhooks/pages/webhooks-page.component.html b/src/Squidex/app/features/webhooks/pages/webhooks-page.component.html deleted file mode 100644 index e49cc91eb..000000000 --- a/src/Squidex/app/features/webhooks/pages/webhooks-page.component.html +++ /dev/null @@ -1,72 +0,0 @@ - - - -
-
-
- - - -
- -

Webhooks

-
- - - - -
- -
-
-
- No Webhook created yet. -
- -
- - - -
-
- -
- - - - - - - - - The sidebar navigation contains useful context specific links. Here you can view the history how this schema has changed over time. - - - - Click the help icon to show a context specific help page. Go to https://docs.squidex.io for the full documentation. - -
-
-
- - \ No newline at end of file diff --git a/src/Squidex/app/features/webhooks/pages/webhooks-page.component.ts b/src/Squidex/app/features/webhooks/pages/webhooks-page.component.ts deleted file mode 100644 index 6661a4dd3..000000000 --- a/src/Squidex/app/features/webhooks/pages/webhooks-page.component.ts +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Sebastian Stehle. All rights reserved - */ - -import { Component, OnInit } from '@angular/core'; -import { FormBuilder, Validators } from '@angular/forms'; - -import { - AppComponentBase, - AppsStoreService, - AuthService, - CreateWebhookDto, - DateTime, - DialogService, - ImmutableArray, - SchemaDto, - SchemasService, - WebhookDto, - WebhooksService, - UpdateWebhookDto -} from 'shared'; - -@Component({ - selector: 'sqx-webhooks-page', - styleUrls: ['./webhooks-page.component.scss'], - templateUrl: './webhooks-page.component.html' -}) -export class WebhooksPageComponent extends AppComponentBase implements OnInit { - public webhooks: ImmutableArray; - public schemas: SchemaDto[]; - - public addWebhookFormSubmitted = false; - public addWebhookForm = - this.formBuilder.group({ - url: ['', - [ - Validators.required - ]] - }); - - public get hasUrl() { - return this.addWebhookForm.controls['url'].value && this.addWebhookForm.controls['url'].value.length > 0; - } - - constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService, - private readonly schemasService: SchemasService, - private readonly webhooksService: WebhooksService, - private readonly formBuilder: FormBuilder - ) { - super(dialogs, apps, authService); - } - - public ngOnInit() { - this.load(); - } - - public load(showInfo = false) { - this.appNameOnce() - .switchMap(app => - this.schemasService.getSchemas(app) - .combineLatest(this.webhooksService.getWebhooks(app), - (s, w) => { return { webhooks: w, schemas: s }; })) - .subscribe(dtos => { - this.schemas = dtos.schemas; - this.webhooks = ImmutableArray.of(dtos.webhooks); - - if (showInfo) { - this.notifyInfo('Webhooks reloaded.'); - } - }, error => { - this.notifyError(error); - }); - } - - public deleteWebhook(webhook: WebhookDto) { - this.appNameOnce() - .switchMap(app => this.webhooksService.deleteWebhook(app, webhook.id, webhook.version)) - .subscribe(dto => { - this.webhooks = this.webhooks.remove(webhook); - }, error => { - this.notifyError(error); - }); - } - - public updateWebhook(webhook: WebhookDto, requestDto: UpdateWebhookDto) { - this.appNameOnce() - .switchMap(app => this.webhooksService.putWebhook(app, webhook.id, requestDto, webhook.version)) - .subscribe(dto => { - this.webhooks = this.webhooks.replace(webhook, webhook.update(requestDto, this.userToken, dto.version)); - - this.notifyInfo('Webhook saved.'); - }, error => { - this.notifyError(error); - }); - } - - public addWebhook() { - this.addWebhookFormSubmitted = true; - - if (this.addWebhookForm.valid) { - this.addWebhookForm.disable(); - - const requestDto = new CreateWebhookDto(this.addWebhookForm.controls['url'].value, []); - - const me = this.userToken; - - this.appNameOnce() - .switchMap(app => this.webhooksService.postWebhook(app, requestDto, me, DateTime.now())) - .subscribe(dto => { - this.webhooks = this.webhooks.push(dto); - - this.resetWebhookForm(); - }, error => { - this.notifyError(error); - this.enableWebhookForm(); - }); - } - } - - public cancelAddWebhook() { - this.resetWebhookForm(); - } - - private enableWebhookForm() { - this.addWebhookForm.enable(); - } - - private resetWebhookForm() { - this.addWebhookFormSubmitted = false; - this.addWebhookForm.enable(); - this.addWebhookForm.reset(); - } -} diff --git a/src/Squidex/app/framework/angular/keys.pipe.spec.ts b/src/Squidex/app/framework/angular/keys.pipe.spec.ts new file mode 100644 index 000000000..09de18928 --- /dev/null +++ b/src/Squidex/app/framework/angular/keys.pipe.spec.ts @@ -0,0 +1,26 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { + KeysPipe +} from './keys.pipe'; + +describe('KeysPipe', () => { + it('should return keys', () => { + const value = { + key1: 1, + key2: 2 + }; + + const pipe = new KeysPipe(); + + const actual = pipe.transform(value); + const expected = ['key1', 'key2']; + + expect(actual).toBe(expected); + }); +}); \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/keys.pipe.ts b/src/Squidex/app/framework/angular/keys.pipe.ts new file mode 100644 index 000000000..8b3b91334 --- /dev/null +++ b/src/Squidex/app/framework/angular/keys.pipe.ts @@ -0,0 +1,18 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'sqxKeys', + pure: true +}) +export class KeysPipe implements PipeTransform { + public transform(value: any, args: any[] = null): any { + return Object.keys(value); + } +} \ No newline at end of file diff --git a/src/Squidex/app/framework/declarations.ts b/src/Squidex/app/framework/declarations.ts index 40c8b1d87..f2883f9b8 100644 --- a/src/Squidex/app/framework/declarations.ts +++ b/src/Squidex/app/framework/declarations.ts @@ -23,6 +23,7 @@ export * from './angular/image-source.directive'; export * from './angular/indeterminate-value.directive'; export * from './angular/jscript-editor.component'; export * from './angular/json-editor.component'; +export * from './angular/keys.pipe'; export * from './angular/lowercase-input.directive'; export * from './angular/markdown-editor.component'; export * from './angular/modal-target.directive'; diff --git a/src/Squidex/app/framework/module.ts b/src/Squidex/app/framework/module.ts index 973635904..ad8b62490 100644 --- a/src/Squidex/app/framework/module.ts +++ b/src/Squidex/app/framework/module.ts @@ -36,6 +36,7 @@ import { IndeterminateValueDirective, JscriptEditorComponent, JsonEditorComponent, + KeysPipe, KNumberPipe, LocalCacheService, LocalStoreService, @@ -102,6 +103,7 @@ import { IndeterminateValueDirective, JscriptEditorComponent, JsonEditorComponent, + KeysPipe, KNumberPipe, LowerCaseInputDirective, MarkdownEditorComponent, @@ -151,6 +153,7 @@ import { IndeterminateValueDirective, JscriptEditorComponent, JsonEditorComponent, + KeysPipe, KNumberPipe, LowerCaseInputDirective, MarkdownEditorComponent, diff --git a/src/Squidex/app/shared/declarations-base.ts b/src/Squidex/app/shared/declarations-base.ts index 01d4a0bdc..7b73101d9 100644 --- a/src/Squidex/app/shared/declarations-base.ts +++ b/src/Squidex/app/shared/declarations-base.ts @@ -30,12 +30,12 @@ export * from './services/help.service'; export * from './services/history.service'; export * from './services/languages.service'; export * from './services/plans.service'; +export * from './services/rules.service'; export * from './services/schemas.service'; export * from './services/ui.service'; export * from './services/usages.service'; export * from './services/users-provider.service'; export * from './services/users.service'; -export * from './services/webhooks.service'; export * from './utils/messages'; diff --git a/src/Squidex/app/shared/module.ts b/src/Squidex/app/shared/module.ts index f67368770..890bfb326 100644 --- a/src/Squidex/app/shared/module.ts +++ b/src/Squidex/app/shared/module.ts @@ -44,6 +44,7 @@ import { ResolveSchemaGuard, SchemasService, ResolveUserGuard, + RulesService, UIService, UsagesService, UserDtoPicture, @@ -56,8 +57,7 @@ import { UserPictureRefPipe, UserManagementService, UsersProviderService, - UsersService, - WebhooksService + UsersService } from './declarations'; @NgModule({ @@ -129,13 +129,13 @@ export class SqxSharedModule { ResolvePublishedSchemaGuard, ResolveSchemaGuard, ResolveUserGuard, + RulesService, SchemasService, UIService, UsagesService, UserManagementService, UsersProviderService, UsersService, - WebhooksService, { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, diff --git a/src/Squidex/app/shared/services/rules.service.spec.ts b/src/Squidex/app/shared/services/rules.service.spec.ts new file mode 100644 index 000000000..0e1e7570a --- /dev/null +++ b/src/Squidex/app/shared/services/rules.service.spec.ts @@ -0,0 +1,310 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + + +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { inject, TestBed } from '@angular/core/testing'; + +import { + AnalyticsService, + ApiUrlConfig, + CreateRuleDto, + DateTime, + UpdateRuleDto, + Version, + RuleDto, + RuleEventDto, + RuleEventsDto, + RulesService +} from './../'; + +describe('RuleDto', () => { + const creation = DateTime.today(); + const creator = 'not-me'; + const modified = DateTime.now(); + const modifier = 'me'; + const version = new Version('1'); + const newVersion = new Version('2'); + + it('should update trigger and action', () => { + const update = new UpdateRuleDto({ param1: 1, triggerType: 'NewType' }, { param2: 2, actionType: 'NewType' }); + + const rule_1 = new RuleDto('id1', creator, creator, creation, creation, version, true, {}, 'contentChanged', {}, 'webhook'); + const rule_2 = rule_1.update(update, modifier, newVersion, modified); + + expect(rule_2.trigger).toEqual(update.trigger); + expect(rule_2.triggerType).toEqual(update.trigger.triggerType); + expect(rule_2.action).toEqual(update.action); + expect(rule_2.actionType).toEqual(update.action.actionType); + expect(rule_2.lastModified).toEqual(modified); + expect(rule_2.lastModifiedBy).toEqual(modifier); + expect(rule_2.version).toEqual(newVersion); + }); + + it('should enable', () => { + const rule_1 = new RuleDto('id1', creator, creator, creation, creation, version, true, {}, 'contentChanged', {}, 'webhook'); + const rule_2 = rule_1.enable(modifier, newVersion, modified); + + expect(rule_2.isEnabled).toBeTruthy(); + expect(rule_2.lastModified).toEqual(modified); + expect(rule_2.lastModifiedBy).toEqual(modifier); + expect(rule_2.version).toEqual(newVersion); + }); + + it('should disable', () => { + const rule_1 = new RuleDto('id1', creator, creator, creation, creation, version, true, {}, 'contentChanged', {}, 'webhook'); + const rule_2 = rule_1.disable(modifier, newVersion, modified); + + expect(rule_2.isEnabled).toBeFalsy(); + expect(rule_2.lastModified).toEqual(modified); + expect(rule_2.lastModifiedBy).toEqual(modifier); + expect(rule_2.version).toEqual(newVersion); + }); +}); + +describe('RulesService', () => { + const now = DateTime.now(); + const user = 'me'; + const version = new Version('1'); + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule + ], + providers: [ + RulesService, + { provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }, + { provide: AnalyticsService, useValue: new AnalyticsService() } + ] + }); + }); + + afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => { + httpMock.verify(); + })); + + it('should make get request to get app rules', + inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { + + let rules: RuleDto[] | null = null; + + rulesService.getRules('my-app').subscribe(result => { + rules = result; + }); + + const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules'); + + expect(req.request.method).toEqual('GET'); + expect(req.request.headers.get('If-Match')).toBeNull(); + + req.flush([ + { + id: 'id1', + created: '2016-12-12T10:10', + createdBy: 'CreatedBy1', + lastModified: '2017-12-12T10:10', + lastModifiedBy: 'LastModifiedBy1', + url: 'http://squidex.io/hook', + version: '1', + trigger: { + param1: 1, + param2: 2, + triggerType: 'ContentChanged' + }, + action: { + param3: 3, + param4: 4, + actionType: 'Webhook' + }, + isEnabled: true + } + ]); + + expect(rules).toEqual([ + new RuleDto('id1', 'CreatedBy1', 'LastModifiedBy1', + DateTime.parseISO_UTC('2016-12-12T10:10'), + DateTime.parseISO_UTC('2017-12-12T10:10'), + version, + true, + { + param1: 1, + param2: 2 + }, + 'ContentChanged', + { + param3: 3, + param4: 4 + }, + 'Webhook') + ]); + })); + + it('should make post request to create rule', + inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { + + const dto = new CreateRuleDto({ + param1: 1, + param2: 2, + triggerType: 'ContentChanged' + }, { + param3: 3, + param4: 4, + actionType: 'Webhook' + }); + + let rule: RuleDto | null = null; + + rulesService.postRule('my-app', dto, user, now).subscribe(result => { + rule = result; + }); + + const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules'); + + expect(req.request.method).toEqual('POST'); + expect(req.request.headers.get('If-Match')).toBeNull(); + + req.flush({ id: 'id1', sharedSecret: 'token1', schemaId: 'schema1' }, { + headers: { + etag: '1' + } + }); + + expect(rule).toEqual( + new RuleDto('id1', 'CreatedBy1', 'LastModifiedBy1', + DateTime.parseISO_UTC('2016-12-12T10:10'), + DateTime.parseISO_UTC('2017-12-12T10:10'), + version, + true, + { + param1: 1, + param2: 2 + }, + 'ContentChanged', + { + param3: 3, + param4: 4 + }, + 'Webhook')); + })); + + it('should make put request to update rule', + inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { + + const dto = new UpdateRuleDto({ param1: 1 }, { param2: 2 }); + + rulesService.putRule('my-app', '123', dto, version).subscribe(); + + const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules/123'); + + expect(req.request.method).toEqual('PUT'); + expect(req.request.headers.get('If-Match')).toEqual(version.value); + + req.flush({}); + })); + + it('should make put request to enable rule', + inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { + + rulesService.enableRule('my-app', '123', version).subscribe(); + + const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules/123/enable'); + + expect(req.request.method).toEqual('PUT'); + expect(req.request.headers.get('If-Match')).toEqual(version.value); + + req.flush({}); + })); + + it('should make put request to disable rule', + inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { + + rulesService.disableRule('my-app', '123', version).subscribe(); + + const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules/123/disable'); + + expect(req.request.method).toEqual('PUT'); + expect(req.request.headers.get('If-Match')).toEqual(version.value); + + req.flush({}); + })); + + it('should make delete request to delete rule', + inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { + + rulesService.deleteRule('my-app', '123', version).subscribe(); + + const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules/123'); + + expect(req.request.method).toEqual('DELETE'); + expect(req.request.headers.get('If-Match')).toEqual(version.value); + })); + + it('should make get request to get app rule events', + inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { + + let rules: RuleEventsDto | null = null; + + rulesService.getEvents('my-app', 10, 20).subscribe(result => { + rules = result; + }); + + const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules/events?take=10&skip=20'); + + expect(req.request.method).toEqual('GET'); + + req.flush({ + total: 20, + items: [ + { + id: 'id1', + created: '2017-12-12T10:10', + eventName: 'event1', + nextAttempt: '2017-12-12T12:10', + jobResult: 'Failed', + lastDump: 'dump1', + numCalls: 1, + description: 'url1', + result: 'Failed' + }, + { + id: 'id2', + created: '2017-12-13T10:10', + eventName: 'event2', + nextAttempt: '2017-12-13T12:10', + jobResult: 'Failed', + lastDump: 'dump2', + numCalls: 2, + description: 'url2', + result: 'Failed' + } + ] + }); + + expect(rules).toEqual( + new RuleEventsDto(20, [ + new RuleEventDto('id1', + DateTime.parseISO_UTC('2017-12-12T10:10'), + DateTime.parseISO_UTC('2017-12-12T12:10'), + 'event1', 'url1', 'dump1', 'Failed', 'Failed', 1), + new RuleEventDto('id2', + DateTime.parseISO_UTC('2017-12-13T10:10'), + DateTime.parseISO_UTC('2017-12-13T12:10'), + 'event2', 'url2', 'dump2', 'Failed', 'Failed', 2) + ])); + })); + + it('should make put request to enqueue rule event', + inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { + + rulesService.enqueueEvent('my-app', '123').subscribe(); + + const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules/events/123'); + + expect(req.request.method).toEqual('PUT'); + })); +}); \ No newline at end of file diff --git a/src/Squidex/app/shared/services/rules.service.ts b/src/Squidex/app/shared/services/rules.service.ts new file mode 100644 index 000000000..8981dfbcd --- /dev/null +++ b/src/Squidex/app/shared/services/rules.service.ts @@ -0,0 +1,260 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; + +import 'framework/angular/http-extensions'; + +import { + AnalyticsService, + ApiUrlConfig, + DateTime, + HTTP, + Version, + Versioned +} from 'framework'; + +export const ruleTriggers: any = { + 'ContentChanged': 'Content changed' +}; + +export const ruleActions: any = { + 'Webhook': 'Send Webhooks' +}; + +export class RuleDto { + constructor( + public readonly id: string, + public readonly createdBy: string, + public readonly lastModifiedBy: string, + public readonly created: DateTime, + public readonly lastModified: DateTime, + public readonly version: Version, + public readonly isEnabled: boolean, + public readonly trigger: any, + public readonly triggerType: string, + public readonly action: any, + public readonly actionType: string + ) { + } + + public update(update: UpdateRuleDto, user: string, version: Version, now?: DateTime): RuleDto { + return new RuleDto( + this.id, + this.createdBy, user, + this.created, now || DateTime.now(), + version, + this.isEnabled, + update.trigger, + update.trigger['triggerType'], + update.action, + update.action['actionType']); + } + + public enable(user: string, version: Version, now?: DateTime): RuleDto { + return new RuleDto( + this.id, + this.createdBy, user, + this.created, now || DateTime.now(), + version, + true, + this.trigger, + this.triggerType, + this.action, + this.actionType); + } + + public disable(user: string, version: Version, now?: DateTime): RuleDto { + return new RuleDto( + this.id, + this.createdBy, user, + this.created, now || DateTime.now(), + version, + true, + this.trigger, + this.triggerType, + this.action, + this.actionType); + } +} + +export class RuleEventDto { + constructor( + public readonly id: string, + public readonly created: DateTime, + public readonly nextAttempt: DateTime | null, + public readonly eventName: string, + public readonly description: string, + public readonly lastDump: string, + public readonly result: string, + public readonly jobResult: string, + public readonly numCalls: number + ) { + } +} + +export class RuleEventsDto { + constructor( + public readonly total: number, + public readonly items: RuleEventDto[] + ) { + } +} + +export class CreateRuleDto { + constructor( + public readonly trigger: any, + public readonly action: any + ) { + } +} + +export class UpdateRuleDto { + constructor( + public readonly trigger: any, + public readonly action: any + ) { + } +} + +@Injectable() +export class RulesService { + constructor( + private readonly http: HttpClient, + private readonly apiUrl: ApiUrlConfig, + private readonly analytics: AnalyticsService + ) { + } + + public getRules(appName: string): Observable { + const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules`); + + return HTTP.getVersioned(this.http, url) + .map(response => { + const items: any[] = response.payload.body; + + return items.map(item => { + return new RuleDto( + item.id, + item.createdBy, + item.lastModifiedBy, + DateTime.parseISO_UTC(item.created), + DateTime.parseISO_UTC(item.lastModified), + new Version(item.version.toString()), + item.isEnabled, + item.trigger, + item.trigger.triggerType, + item.action, + item.action.actionType); + }); + }) + .pretifyError('Failed to load Rules. Please reload.'); + } + + public postRule(appName: string, dto: CreateRuleDto, user: string, now: DateTime): Observable { + const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules`); + + return HTTP.postVersioned(this.http, url, dto) + .map(response => { + const body = response.payload.body; + + return new RuleDto( + body.id, + user, + user, + now, + now, + response.version, + true, + dto.trigger, + dto.trigger.triggerType, + dto.action, + dto.action.actionType); + }) + .do(() => { + this.analytics.trackEvent('Rule', 'Created', appName); + }) + .pretifyError('Failed to create rule. Please reload.'); + } + + public putRule(appName: string, id: string, dto: UpdateRuleDto, version: Version): Observable> { + const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/${id}`); + + return HTTP.putVersioned(this.http, url, dto, version) + .do(() => { + this.analytics.trackEvent('Rule', 'Updated', appName); + }) + .pretifyError('Failed to update rule. Please reload.'); + } + + public enableRule(appName: string, id: string, version: Version): Observable> { + const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/${id}/enable`); + + return HTTP.putVersioned(this.http, url, {}, version) + .do(() => { + this.analytics.trackEvent('Rule', 'Updated', appName); + }) + .pretifyError('Failed to enable rule. Please reload.'); + } + + public disableRule(appName: string, id: string, version: Version): Observable> { + const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/${id}/disable`); + + return HTTP.putVersioned(this.http, url, {}, version) + .do(() => { + this.analytics.trackEvent('Rule', 'Updated', appName); + }) + .pretifyError('Failed to disable rule. Please reload.'); + } + + public deleteRule(appName: string, id: string, version: Version): Observable { + const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/${id}`); + + return HTTP.deleteVersioned(this.http, url, version) + .do(() => { + this.analytics.trackEvent('Rule', 'Deleted', appName); + }) + .pretifyError('Failed to delete rule. Please reload.'); + } + + public getEvents(appName: string, take: number, skip: number): Observable { + const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/events?take=${take}&skip=${skip}`); + + return HTTP.getVersioned(this.http, url) + .map(response => { + const body = response.payload.body; + + const items: any[] = body.items; + + return new RuleEventsDto(body.total, items.map(item => { + return new RuleEventDto( + item.id, + DateTime.parseISO_UTC(item.created), + item.nextAttempt ? DateTime.parseISO_UTC(item.nextAttempt) : null, + item.eventName, + item.description, + item.lastDump, + item.result, + item.jobResult, + item.numCalls); + })); + }) + .pretifyError('Failed to load events. Please reload.'); + } + + public enqueueEvent(appName: string, id: string): Observable { + const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/events/${id}`); + + return HTTP.putVersioned(this.http, url, {}) + .do(() => { + this.analytics.trackEvent('Rule', 'EventEnqueued', appName); + }) + .pretifyError('Failed to enqueue rule event. Please reload.'); + } +} \ No newline at end of file diff --git a/src/Squidex/app/shared/services/webhooks.service.spec.ts b/src/Squidex/app/shared/services/webhooks.service.spec.ts deleted file mode 100644 index d25b39c94..000000000 --- a/src/Squidex/app/shared/services/webhooks.service.spec.ts +++ /dev/null @@ -1,245 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Sebastian Stehle. All rights reserved - */ - - -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { inject, TestBed } from '@angular/core/testing'; - -import { - AnalyticsService, - ApiUrlConfig, - CreateWebhookDto, - DateTime, - UpdateWebhookDto, - Version, - WebhookDto, - WebhookEventDto, - WebhookEventsDto, - WebhookSchemaDto, - WebhooksService -} from './../'; - -describe('WebhookDto', () => { - const creation = DateTime.today(); - const creator = 'not-me'; - const modified = DateTime.now(); - const modifier = 'me'; - const version = new Version('1'); - const newVersion = new Version('2'); - - it('should update url and schemas', () => { - const webhook_1 = new WebhookDto('id1', 'token1', creator, creator, creation, creation, version, [], 'http://squidex.io/hook', 1, 2, 3, 4); - const webhook_2 = - webhook_1.update(new UpdateWebhookDto('http://squidex.io/hook2', - [ - new WebhookSchemaDto('1', true, true, true, true), - new WebhookSchemaDto('2', true, true, true, true) - ]), modifier, newVersion, modified); - - expect(webhook_2.url).toEqual('http://squidex.io/hook2'); - expect(webhook_2.schemas.length).toEqual(2); - expect(webhook_2.lastModified).toEqual(modified); - expect(webhook_2.lastModifiedBy).toEqual(modifier); - expect(webhook_2.version).toEqual(newVersion); - }); -}); - -describe('WebhooksService', () => { - const now = DateTime.now(); - const user = 'me'; - const version = new Version('1'); - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule - ], - providers: [ - WebhooksService, - { provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }, - { provide: AnalyticsService, useValue: new AnalyticsService() } - ] - }); - }); - - afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => { - httpMock.verify(); - })); - - it('should make get request to get app webhooks', - inject([WebhooksService, HttpTestingController], (webhooksService: WebhooksService, httpMock: HttpTestingController) => { - - let webhooks: WebhookDto[] | null = null; - - webhooksService.getWebhooks('my-app').subscribe(result => { - webhooks = result; - }); - - const req = httpMock.expectOne('http://service/p/api/apps/my-app/webhooks'); - - expect(req.request.method).toEqual('GET'); - expect(req.request.headers.get('If-Match')).toBeNull(); - - req.flush([ - { - id: 'id1', - sharedSecret: 'token1', - created: '2016-12-12T10:10', - createdBy: 'CreatedBy1', - lastModified: '2017-12-12T10:10', - lastModifiedBy: 'LastModifiedBy1', - url: 'http://squidex.io/hook', - version: '1', - totalSucceeded: 1, - totalFailed: 2, - totalTimedout: 3, - averageRequestTimeMs: 4, - schemas: [{ - schemaId: '1', - sendCreate: true, - sendUpdate: true, - sendDelete: true, - sendPublish: true - }, { - schemaId: '2', - sendCreate: true, - sendUpdate: true, - sendDelete: true, - sendPublish: true - }] - } - ]); - - expect(webhooks).toEqual([ - new WebhookDto('id1', 'token1', 'CreatedBy1', 'LastModifiedBy1', - DateTime.parseISO_UTC('2016-12-12T10:10'), - DateTime.parseISO_UTC('2017-12-12T10:10'), - version, - [ - new WebhookSchemaDto('1', true, true, true, true), - new WebhookSchemaDto('2', true, true, true, true) - ], - 'http://squidex.io/hook', 1, 2, 3, 4) - ]); - })); - - it('should make post request to create webhook', - inject([WebhooksService, HttpTestingController], (webhooksService: WebhooksService, httpMock: HttpTestingController) => { - - const dto = new CreateWebhookDto('http://squidex.io/hook', []); - - let webhook: WebhookDto | null = null; - - webhooksService.postWebhook('my-app', dto, user, now).subscribe(result => { - webhook = result; - }); - - const req = httpMock.expectOne('http://service/p/api/apps/my-app/webhooks'); - - expect(req.request.method).toEqual('POST'); - expect(req.request.headers.get('If-Match')).toBeNull(); - - req.flush({ id: 'id1', sharedSecret: 'token1', schemaId: 'schema1' }, { - headers: { - etag: '1' - } - }); - - expect(webhook).toEqual( - new WebhookDto('id1', 'token1', user, user, now, now, version, [], 'http://squidex.io/hook', 0, 0, 0, 0)); - })); - - it('should make put request to update webhook', - inject([WebhooksService, HttpTestingController], (webhooksService: WebhooksService, httpMock: HttpTestingController) => { - - const dto = new UpdateWebhookDto('http://squidex.io/hook', []); - - webhooksService.putWebhook('my-app', '123', dto, version).subscribe(); - - const req = httpMock.expectOne('http://service/p/api/apps/my-app/webhooks/123'); - - expect(req.request.method).toEqual('PUT'); - expect(req.request.headers.get('If-Match')).toEqual(version.value); - - req.flush({}); - })); - - it('should make delete request to delete webhook', - inject([WebhooksService, HttpTestingController], (webhooksService: WebhooksService, httpMock: HttpTestingController) => { - - webhooksService.deleteWebhook('my-app', '123', version).subscribe(); - - const req = httpMock.expectOne('http://service/p/api/apps/my-app/webhooks/123'); - - expect(req.request.method).toEqual('DELETE'); - expect(req.request.headers.get('If-Match')).toEqual(version.value); - })); - - it('should make get request to get app webhook events', - inject([WebhooksService, HttpTestingController], (webhooksService: WebhooksService, httpMock: HttpTestingController) => { - - let webhooks: WebhookEventsDto | null = null; - - webhooksService.getEvents('my-app', 10, 20).subscribe(result => { - webhooks = result; - }); - - const req = httpMock.expectOne('http://service/p/api/apps/my-app/webhooks/events?take=10&skip=20'); - - expect(req.request.method).toEqual('GET'); - - req.flush({ - total: 20, - items: [ - { - id: 'id1', - created: '2017-12-12T10:10', - eventName: 'event1', - nextAttempt: '2017-12-12T12:10', - jobResult: 'Failed', - lastDump: 'dump1', - numCalls: 1, - requestUrl: 'url1', - result: 'Failed' - }, - { - id: 'id2', - created: '2017-12-13T10:10', - eventName: 'event2', - nextAttempt: '2017-12-13T12:10', - jobResult: 'Failed', - lastDump: 'dump2', - numCalls: 2, - requestUrl: 'url2', - result: 'Failed' - } - ] - }); - - expect(webhooks).toEqual( - new WebhookEventsDto(20, [ - new WebhookEventDto('id1', - DateTime.parseISO_UTC('2017-12-12T10:10'), - DateTime.parseISO_UTC('2017-12-12T12:10'), - 'event1', 'url1', 'dump1', 'Failed', 'Failed', 1), - new WebhookEventDto('id2', - DateTime.parseISO_UTC('2017-12-13T10:10'), - DateTime.parseISO_UTC('2017-12-13T12:10'), - 'event2', 'url2', 'dump2', 'Failed', 'Failed', 2) - ])); - })); - - it('should make put request to enqueue webhook event', - inject([WebhooksService, HttpTestingController], (webhooksService: WebhooksService, httpMock: HttpTestingController) => { - - webhooksService.enqueueEvent('my-app', '123').subscribe(); - - const req = httpMock.expectOne('http://service/p/api/apps/my-app/webhooks/events/123'); - - expect(req.request.method).toEqual('PUT'); - })); -}); \ No newline at end of file diff --git a/src/Squidex/app/shared/services/webhooks.service.ts b/src/Squidex/app/shared/services/webhooks.service.ts deleted file mode 100644 index 596409cf7..000000000 --- a/src/Squidex/app/shared/services/webhooks.service.ts +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Sebastian Stehle. All rights reserved - */ - -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs'; - -import 'framework/angular/http-extensions'; - -import { - AnalyticsService, - ApiUrlConfig, - DateTime, - HTTP, - Version, - Versioned -} from 'framework'; - -export class WebhookDto { - constructor( - public readonly id: string, - public readonly sharedSecret: string, - public readonly createdBy: string, - public readonly lastModifiedBy: string, - public readonly created: DateTime, - public readonly lastModified: DateTime, - public readonly version: Version, - public readonly schemas: WebhookSchemaDto[], - public readonly url: string, - public readonly totalSucceeded: number, - public readonly totalFailed: number, - public readonly totalTimedout: number, - public readonly averageRequestTimeMs: number - ) { - } - - public update(update: UpdateWebhookDto, user: string, version: Version, now?: DateTime): WebhookDto { - return new WebhookDto( - this.id, - this.sharedSecret, - this.createdBy, user, - this.created, now || DateTime.now(), - version, - update.schemas, - update.url, - this.totalSucceeded, - this.totalFailed, - this.totalTimedout, - this.averageRequestTimeMs); - } -} - -export class WebhookSchemaDto { - constructor( - public readonly schemaId: string, - public readonly sendCreate: boolean, - public readonly sendUpdate: boolean, - public readonly sendDelete: boolean, - public readonly sendPublish: boolean - ) { - } -} - -export class WebhookEventDto { - constructor( - public readonly id: string, - public readonly created: DateTime, - public readonly nextAttempt: DateTime | null, - public readonly eventName: string, - public readonly requestUrl: string, - public readonly lastDump: string, - public readonly result: string, - public readonly jobResult: string, - public readonly numCalls: number - ) { - } -} - -export class WebhookEventsDto { - constructor( - public readonly total: number, - public readonly items: WebhookEventDto[] - ) { - } -} - -export class CreateWebhookDto { - constructor( - public readonly url: string, - public readonly schemas: WebhookSchemaDto[] - ) { - } -} - -export class UpdateWebhookDto { - constructor( - public readonly url: string, - public readonly schemas: WebhookSchemaDto[] - ) { - } -} - -@Injectable() -export class WebhooksService { - constructor( - private readonly http: HttpClient, - private readonly apiUrl: ApiUrlConfig, - private readonly analytics: AnalyticsService - ) { - } - - public getWebhooks(appName: string): Observable { - const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks`); - - return HTTP.getVersioned(this.http, url) - .map(response => { - const items: any[] = response.payload.body; - - return items.map(item => { - const schemas = item.schemas.map((schema: any) => - new WebhookSchemaDto( - schema.schemaId, - schema.sendCreate, - schema.sendUpdate, - schema.sendDelete, - schema.sendPublish)); - - return new WebhookDto( - item.id, - item.sharedSecret, - item.createdBy, - item.lastModifiedBy, - DateTime.parseISO_UTC(item.created), - DateTime.parseISO_UTC(item.lastModified), - new Version(item.version.toString()), - schemas, - item.url, - item.totalSucceeded, - item.totalFailed, - item.totalTimedout, - item.averageRequestTimeMs); - }); - }) - .pretifyError('Failed to load webhooks. Please reload.'); - } - - public postWebhook(appName: string, dto: CreateWebhookDto, user: string, now: DateTime): Observable { - const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks`); - - return HTTP.postVersioned(this.http, url, dto) - .map(response => { - const body = response.payload.body; - - return new WebhookDto( - body.id, - body.sharedSecret, - user, - user, - now, - now, - response.version, - dto.schemas, - dto.url, - 0, 0, 0, 0); - }) - .do(() => { - this.analytics.trackEvent('Webhook', 'Created', appName); - }) - .pretifyError('Failed to create webhook. Please reload.'); - } - - public putWebhook(appName: string, id: string, dto: UpdateWebhookDto, version: Version): Observable> { - const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks/${id}`); - - return HTTP.putVersioned(this.http, url, dto, version) - .do(() => { - this.analytics.trackEvent('Webhook', 'Updated', appName); - }) - .pretifyError('Failed to update webhook. Please reload.'); - } - - public deleteWebhook(appName: string, id: string, version: Version): Observable { - const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks/${id}`); - - return HTTP.deleteVersioned(this.http, url, version) - .do(() => { - this.analytics.trackEvent('Webhook', 'Deleted', appName); - }) - .pretifyError('Failed to delete webhook. Please reload.'); - } - - public getEvents(appName: string, take: number, skip: number): Observable { - const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks/events?take=${take}&skip=${skip}`); - - return HTTP.getVersioned(this.http, url) - .map(response => { - const body = response.payload.body; - - const items: any[] = body.items; - - return new WebhookEventsDto(body.total, items.map(item => { - return new WebhookEventDto( - item.id, - DateTime.parseISO_UTC(item.created), - item.nextAttempt ? DateTime.parseISO_UTC(item.nextAttempt) : null, - item.eventName, - item.requestUrl, - item.lastDump, - item.result, - item.jobResult, - item.numCalls); - })); - }) - .pretifyError('Failed to load events. Please reload.'); - } - - public enqueueEvent(appName: string, id: string): Observable { - const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks/events/${id}`); - - return HTTP.putVersioned(this.http, url, {}) - .do(() => { - this.analytics.trackEvent('Webhook', 'EventEnqueued', appName); - }) - .pretifyError('Failed to enqueue webhook event. Please reload.'); - } -} \ No newline at end of file diff --git a/src/Squidex/app/shell/pages/app/left-menu.component.html b/src/Squidex/app/shell/pages/app/left-menu.component.html index f420ad2a4..70a4b5359 100644 --- a/src/Squidex/app/shell/pages/app/left-menu.component.html +++ b/src/Squidex/app/shell/pages/app/left-menu.component.html @@ -16,8 +16,8 @@