diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs index 57f857877..d9c2c24e9 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs @@ -99,7 +99,7 @@ namespace Squidex.Domain.Apps.Core.Apps var newMaster = state.Master.Language != language ? state.Master : - state.Languages.Values.FirstOrDefault(); + newLanguages.Values.FirstOrDefault(); state = new State(newLanguages, newMaster); } diff --git a/src/Squidex.Domain.Apps.Write/Webhooks/Commands/CreateWebhook.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/WebhookAction.cs similarity index 53% rename from src/Squidex.Domain.Apps.Write/Webhooks/Commands/CreateWebhook.cs rename to src/Squidex.Domain.Apps.Core.Model/Rules/Actions/WebhookAction.cs index d6a76ea84..09718ad8f 100644 --- a/src/Squidex.Domain.Apps.Write/Webhooks/Commands/CreateWebhook.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/WebhookAction.cs @@ -1,5 +1,5 @@ // ========================================================================== -// CreateWebhook.cs +// WebhookAction.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -9,15 +9,18 @@ using System; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Write.Webhooks.Commands +namespace Squidex.Domain.Apps.Core.Rules.Actions { - public sealed class CreateWebhook : WebhookEditCommand + [TypeName(nameof(WebhookAction))] + public sealed class WebhookAction : RuleAction { - public string SharedSecret { get; } = RandomHash.New(); + public Uri Url { get; set; } - public CreateWebhook() + public string SharedSecret { get; set; } + + public override T Accept(IRuleActionVisitor visitor) { - WebhookId = Guid.NewGuid(); + 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/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..cd3e9707f --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Json/RuleConverter.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// RuleConverter.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +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 new file mode 100644 index 000000000..8595d84a8 --- /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() != action.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.Read/Webhooks/WebhookJob.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs similarity index 60% rename from src/Squidex.Domain.Apps.Read/Webhooks/WebhookJob.cs rename to src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs index 8951c3da6..3117fd8e2 100644 --- a/src/Squidex.Domain.Apps.Read/Webhooks/WebhookJob.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs @@ -1,5 +1,5 @@ // ========================================================================== -// WebhookJob.cs +// RuleJob.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -9,24 +9,24 @@ using System; using NodaTime; -namespace Squidex.Domain.Apps.Read.Webhooks +namespace Squidex.Domain.Apps.Core.Rules { - public sealed class WebhookJob + public sealed class RuleJob { - public Guid Id { get; set; } + public Guid RuleId { get; set; } public Guid AppId { get; set; } - public Guid WebhookId { get; set; } - - public Uri RequestUrl { get; set; } + public string EventName { get; set; } - public string RequestBody { get; set; } + public string ActionName { get; set; } - public string RequestSignature { get; set; } + public string Description { get; set; } - public string EventName { 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/Webhooks/WebhookSchema.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchema.cs similarity index 80% rename from src/Squidex.Domain.Apps.Core.Model/Webhooks/WebhookSchema.cs rename to src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchema.cs index 169e85b7d..600fcaf6e 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Webhooks/WebhookSchema.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchema.cs @@ -1,5 +1,5 @@ // ========================================================================== -// WebhookSchema.cs +// ContentChangedTriggerSchema.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -8,9 +8,9 @@ using System; -namespace Squidex.Domain.Apps.Core.Webhooks +namespace Squidex.Domain.Apps.Core.Rules.Triggers { - public sealed class WebhookSchema + public sealed class ContentChangedTriggerSchema { public Guid SchemaId { get; set; } diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs index bf94a3a13..710581ef1 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs @@ -84,7 +84,6 @@ namespace Squidex.Domain.Apps.Core.Schemas new TagsField(id, name, partitioning, (TagsFieldProperties)properties)); typeNameRegistry.MapObsolete(typeof(ReferencesFieldProperties), "DateTime"); - typeNameRegistry.MapObsolete(typeof(DateTimeFieldProperties), "References"); } diff --git a/src/Squidex.Domain.Apps.Write/Webhooks/Commands/UpdateWebhook.cs b/src/Squidex.Domain.Apps.Core.Model/SquidexCoreModel.cs similarity index 71% rename from src/Squidex.Domain.Apps.Write/Webhooks/Commands/UpdateWebhook.cs rename to src/Squidex.Domain.Apps.Core.Model/SquidexCoreModel.cs index 22c33345f..5ed5bdea6 100644 --- a/src/Squidex.Domain.Apps.Write/Webhooks/Commands/UpdateWebhook.cs +++ b/src/Squidex.Domain.Apps.Core.Model/SquidexCoreModel.cs @@ -1,14 +1,14 @@ // ========================================================================== -// UpdateWebhook.cs +// SquidexCoreModel.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -namespace Squidex.Domain.Apps.Write.Webhooks.Commands +namespace Squidex.Domain.Apps.Core { - public sealed class UpdateWebhook : WebhookEditCommand + public static class SquidexCoreModel { } } 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.Read/Webhooks/WebhookResult.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleResult.cs similarity index 80% rename from src/Squidex.Domain.Apps.Read/Webhooks/WebhookResult.cs rename to src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleResult.cs index bc8584b5b..76316497a 100644 --- a/src/Squidex.Domain.Apps.Read/Webhooks/WebhookResult.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleResult.cs @@ -1,14 +1,14 @@ // ========================================================================== -// WebhookResult.cs +// RuleResult.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -namespace Squidex.Domain.Apps.Read.Webhooks +namespace Squidex.Domain.Apps.Core.HandleRules { - public enum WebhookResult + public enum RuleResult { Pending, Success, 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..35fecb5ac --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs @@ -0,0 +1,158 @@ +// ========================================================================== +// 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 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 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 virtual 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 + { + RuleId = 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 virtual async Task<(string Dump, RuleResult Result, TimeSpan Elapsed)> InvokeAsync(string actionName, RuleJobData 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("Elapsed {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..e0b8ef424 --- /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 schema, SchemaEvent @event) + { + return @event.SchemaId.Id == schema.SchemaId; + } + + private static bool MatchsType(ContentChangedTriggerSchema schema, SchemaEvent @event) + { + return + (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.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/Webhooks/WebhookCreated.cs b/src/Squidex.Domain.Apps.Events/Rules/RuleCreated.cs similarity index 57% rename from src/Squidex.Domain.Apps.Events/Webhooks/WebhookCreated.cs rename to src/Squidex.Domain.Apps.Events/Rules/RuleCreated.cs index 1cab7fdce..9128687ff 100644 --- a/src/Squidex.Domain.Apps.Events/Webhooks/WebhookCreated.cs +++ b/src/Squidex.Domain.Apps.Events/Rules/RuleCreated.cs @@ -1,18 +1,21 @@ // ========================================================================== -// WebhookCreated.cs +// 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.Webhooks +namespace Squidex.Domain.Apps.Events.Rules { - [EventType(nameof(WebhookCreated))] - public sealed class WebhookCreated : WebhookEditEvent + [EventType(nameof(RuleCreated))] + public sealed class RuleCreated : RuleEvent { - public string SharedSecret { get; set; } + public RuleTrigger Trigger { get; set; } + + public RuleAction Action { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Events/Webhooks/WebhookDeleted.cs b/src/Squidex.Domain.Apps.Events/Rules/RuleDeleted.cs similarity index 69% rename from src/Squidex.Domain.Apps.Events/Webhooks/WebhookDeleted.cs rename to src/Squidex.Domain.Apps.Events/Rules/RuleDeleted.cs index a04e86ab6..696acb36b 100644 --- a/src/Squidex.Domain.Apps.Events/Webhooks/WebhookDeleted.cs +++ b/src/Squidex.Domain.Apps.Events/Rules/RuleDeleted.cs @@ -1,5 +1,5 @@ // ========================================================================== -// WebhookDeleted.cs +// RuleDeleted.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -8,10 +8,10 @@ using Squidex.Infrastructure.CQRS.Events; -namespace Squidex.Domain.Apps.Events.Webhooks +namespace Squidex.Domain.Apps.Events.Rules { - [EventType(nameof(WebhookDeleted), 2)] - public sealed class WebhookDeleted : WebhookEvent + [EventType(nameof(RuleDeleted))] + public sealed class RuleDeleted : RuleEvent { } } diff --git a/src/Squidex.Domain.Apps.Events/Webhooks/WebhookUpdated.cs b/src/Squidex.Domain.Apps.Events/Rules/RuleDisabled.cs similarity index 69% rename from src/Squidex.Domain.Apps.Events/Webhooks/WebhookUpdated.cs rename to src/Squidex.Domain.Apps.Events/Rules/RuleDisabled.cs index c40793935..265d716bc 100644 --- a/src/Squidex.Domain.Apps.Events/Webhooks/WebhookUpdated.cs +++ b/src/Squidex.Domain.Apps.Events/Rules/RuleDisabled.cs @@ -1,5 +1,5 @@ // ========================================================================== -// WebhookUpdated.cs +// RuleDisabled.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -8,10 +8,10 @@ using Squidex.Infrastructure.CQRS.Events; -namespace Squidex.Domain.Apps.Events.Webhooks +namespace Squidex.Domain.Apps.Events.Rules { - [EventType(nameof(WebhookUpdated))] - public sealed class WebhookUpdated : WebhookEditEvent + [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/Webhooks/WebhookEvent.cs b/src/Squidex.Domain.Apps.Events/Rules/RuleEvent.cs similarity index 68% rename from src/Squidex.Domain.Apps.Events/Webhooks/WebhookEvent.cs rename to src/Squidex.Domain.Apps.Events/Rules/RuleEvent.cs index 99fc08697..d0ee5604c 100644 --- a/src/Squidex.Domain.Apps.Events/Webhooks/WebhookEvent.cs +++ b/src/Squidex.Domain.Apps.Events/Rules/RuleEvent.cs @@ -1,5 +1,5 @@ // ========================================================================== -// WebhookEvent.cs +// RuleEvent.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -8,10 +8,10 @@ using System; -namespace Squidex.Domain.Apps.Events.Webhooks +namespace Squidex.Domain.Apps.Events.Rules { - public abstract class WebhookEvent : AppEvent + public abstract class RuleEvent : AppEvent { - public Guid WebhookId { get; set; } + 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..8de79c181 --- /dev/null +++ b/src/Squidex.Domain.Apps.Events/Rules/Utils/RuleEventDispatcher.cs @@ -0,0 +1,43 @@ +// ========================================================================== +// RuleEventDispatcher.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Rules; + +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 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(); + } + } +} diff --git a/src/Squidex.Domain.Apps.Write/Webhooks/Commands/DeleteWebhook.cs b/src/Squidex.Domain.Apps.Events/SquidexEvents.cs similarity index 70% rename from src/Squidex.Domain.Apps.Write/Webhooks/Commands/DeleteWebhook.cs rename to src/Squidex.Domain.Apps.Events/SquidexEvents.cs index 462532dfe..ab5d1ec98 100644 --- a/src/Squidex.Domain.Apps.Write/Webhooks/Commands/DeleteWebhook.cs +++ b/src/Squidex.Domain.Apps.Events/SquidexEvents.cs @@ -1,14 +1,14 @@ // ========================================================================== -// DeleteWebhook.cs +// SquidexEvents.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -namespace Squidex.Domain.Apps.Write.Webhooks.Commands +namespace Squidex.Domain.Apps.Events { - public sealed class DeleteWebhook : WebhookAggregateCommand + public static class SquidexEvents { } } 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 54% rename from src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookEventEntity.cs rename to src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventEntity.cs index 18f212967..f060b9294 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,26 @@ 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; } - - [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 +40,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 56% rename from src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookEventRepository.cs rename to src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventRepository.cs index d6a4325fc..db39b6675 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,32 +12,28 @@ 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.Infrastructure; +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.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) : base(database) { - Guard.NotNull(clock, nameof(clock)); - - this.clock = clock; } 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,29 +41,27 @@ 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(Instant now, 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 = + 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) + public async Task FindAsync(Guid id) { - var webhookEventEntity = + var ruleEvent = await Collection.Find(x => x.Id == id) .FirstOrDefaultAsync(); - return webhookEventEntity; + return ruleEvent; } public async Task CountByAppAsync(Guid appId) @@ -80,35 +74,20 @@ 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)); - } - - public Task EnqueueAsync(WebhookJob job, Instant nextAttempt) - { - var entity = SimpleMapper.Map(job, new MongoWebhookEventEntity { Created = clock.GetCurrentInstant(), NextAttempt = nextAttempt }); + var entity = SimpleMapper.Map(job, new MongoRuleEventEntity { Job = job, Created = nextAttempt, NextAttempt = nextAttempt }); return Collection.InsertOneIfNotExistsAsync(entity); } - public Task TraceSentAsync(Guid jobId, string dump, WebhookResult result, TimeSpan elapsed, Instant? nextAttempt) + public Task MarkSendingAsync(Guid jobId) { - WebhookJobResult jobResult; - - if (result != WebhookResult.Success && nextAttempt == null) - { - jobResult = WebhookJobResult.Failed; - } - else if (result != WebhookResult.Success && nextAttempt.HasValue) - { - jobResult = WebhookJobResult.Retry; - } - else - { - jobResult = WebhookJobResult.Success; - } + return Collection.UpdateOneAsync(x => x.Id == jobId, Update.Set(x => x.IsSending, true)); + } + public Task MarkSentAsync(Guid jobId, string dump, RuleResult result, RuleJobResult jobResult, TimeSpan elapsed, Instant? nextAttempt) + { 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.MongoDb/Rules/MongoRuleRepository.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository.cs new file mode 100644 index 000000000..ab9ff0204 --- /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> inMemoryRules; + + 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 inMemoryRules.GetOrDefault(appId)?.ToList() ?? EmptyRules; + } + + private async Task EnsureRulesLoadedAsync() + { + if (inMemoryRules == null) + { + try + { + await lockObject.WaitAsync(); + + if (inMemoryRules == null) + { + inMemoryRules = new Dictionary>(); + + var webhooks = + await Collection.Find(new BsonDocument()) + .ToListAsync(); + + foreach (var webhook in webhooks) + { + inMemoryRules.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..26be2002a --- /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 "^rule-"; } + } + + 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); + + inMemoryRules.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id); + inMemoryRules.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); + + inMemoryRules.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id); + inMemoryRules.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); + + inMemoryRules.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id); + inMemoryRules.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); + + inMemoryRules.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id); + inMemoryRules.GetOrAddNew(w.AppId).Add(w); + }); + } + + protected async Task On(RuleDeleted @event, EnvelopeHeaders headers) + { + await EnsureRulesLoadedAsync(); + + 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.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/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/Webhooks/IWebhookEventEntity.cs b/src/Squidex.Domain.Apps.Read/Rules/IRuleEventEntity.cs similarity index 60% rename from src/Squidex.Domain.Apps.Read/Webhooks/IWebhookEventEntity.cs rename to src/Squidex.Domain.Apps.Read/Rules/IRuleEventEntity.cs index e35b23747..631ad2322 100644 --- a/src/Squidex.Domain.Apps.Read/Webhooks/IWebhookEventEntity.cs +++ b/src/Squidex.Domain.Apps.Read/Rules/IRuleEventEntity.cs @@ -1,5 +1,5 @@ // ========================================================================== -// IWebhookEventEntity.cs +// IRuleEventEntity.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -7,18 +7,20 @@ // ========================================================================== using NodaTime; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; -namespace Squidex.Domain.Apps.Read.Webhooks +namespace Squidex.Domain.Apps.Read.Rules { - public interface IWebhookEventEntity : IEntity + public interface IRuleEventEntity : IEntity { - WebhookJob Job { get; } + RuleJob Job { get; } Instant? NextAttempt { get; } - WebhookResult Result { get; } + RuleJobResult JobResult { get; } - WebhookJobResult JobResult { get; } + RuleResult Result { get; } int NumCalls { 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..256aa9b71 --- /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 MarkSentAsync(Guid jobId, string dump, RuleResult result, RuleJobResult jobResult, TimeSpan elapsed, Instant? nextCall); + + 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.Events/Webhooks/WebhookEditEvent.cs b/src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleRepository.cs similarity index 57% rename from src/Squidex.Domain.Apps.Events/Webhooks/WebhookEditEvent.cs rename to src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleRepository.cs index 02dde09b3..5f24b1a4e 100644 --- a/src/Squidex.Domain.Apps.Events/Webhooks/WebhookEditEvent.cs +++ b/src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleRepository.cs @@ -1,5 +1,5 @@ // ========================================================================== -// WebhookEditEvent.cs +// IRuleRepository.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -8,14 +8,14 @@ using System; using System.Collections.Generic; -using Squidex.Domain.Apps.Core.Webhooks; +using System.Threading.Tasks; -namespace Squidex.Domain.Apps.Events.Webhooks +namespace Squidex.Domain.Apps.Read.Rules.Repositories { - public abstract class WebhookEditEvent : WebhookEvent + public interface IRuleRepository { - public Uri Url { get; set; } + Task> QueryByAppAsync(Guid appId); - public List Schemas { get; set; } + Task> QueryCachedByAppAsync(Guid appId); } } diff --git a/src/Squidex.Domain.Apps.Read/Webhooks/WebhookDequeuer.cs b/src/Squidex.Domain.Apps.Read/Rules/RuleDequeuer.cs similarity index 55% rename from src/Squidex.Domain.Apps.Read/Webhooks/WebhookDequeuer.cs rename to src/Squidex.Domain.Apps.Read/Rules/RuleDequeuer.cs index 1d585018d..3f6f6e7c6 100644 --- a/src/Squidex.Domain.Apps.Read/Webhooks/WebhookDequeuer.cs +++ b/src/Squidex.Domain.Apps.Read/Rules/RuleDequeuer.cs @@ -1,5 +1,5 @@ // ========================================================================== -// WebhookDequeuer.cs +// RuleDequeuer.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -11,50 +11,44 @@ using System.Threading; using System.Threading.Tasks; using System.Threading.Tasks.Dataflow; using NodaTime; -using Squidex.Domain.Apps.Read.Webhooks.Repositories; +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.Webhooks +namespace Squidex.Domain.Apps.Read.Rules { - public sealed class WebhookDequeuer : DisposableObjectBase, IExternalSystem + public sealed class RuleDequeuer : DisposableObjectBase, IExternalSystem { - private readonly ActionBlock requestBlock; - private readonly TransformBlock blockBlock; - private readonly IWebhookEventRepository webhookEventRepository; - private readonly IWebhookRepository webhookRepository; - private readonly WebhookSender webhookSender; + private readonly ActionBlock requestBlock; + private readonly TransformBlock blockBlock; + private readonly IRuleEventRepository ruleEventRepository; + private readonly RuleService ruleService; private readonly CompletionTimer timer; - private readonly ISemanticLog log; private readonly IClock clock; + private readonly ISemanticLog log; - public WebhookDequeuer(WebhookSender webhookSender, - IWebhookEventRepository webhookEventRepository, - IWebhookRepository webhookRepository, - IClock clock, - ISemanticLog log) + public RuleDequeuer(RuleService ruleService, IRuleEventRepository ruleEventRepository, ISemanticLog log, IClock clock) { - Guard.NotNull(webhookEventRepository, nameof(webhookEventRepository)); - Guard.NotNull(webhookRepository, nameof(webhookRepository)); - Guard.NotNull(webhookSender, nameof(webhookSender)); + Guard.NotNull(ruleEventRepository, nameof(ruleEventRepository)); + Guard.NotNull(ruleService, nameof(ruleService)); Guard.NotNull(clock, nameof(clock)); Guard.NotNull(log, nameof(log)); - this.webhookEventRepository = webhookEventRepository; - this.webhookRepository = webhookRepository; - this.webhookSender = webhookSender; + this.ruleEventRepository = ruleEventRepository; + this.ruleService = ruleService; this.clock = clock; this.log = log; requestBlock = - new ActionBlock(MakeRequestAsync, + new ActionBlock(MakeRequestAsync, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 32, BoundedCapacity = 32 }); blockBlock = - new TransformBlock(x => BlockAsync(x), + new TransformBlock(x => BlockAsync(x), new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 1, BoundedCapacity = 1 }); blockBlock.LinkTo(requestBlock, new DataflowLinkOptions { PropagateCompletion = true }); @@ -86,7 +80,9 @@ namespace Squidex.Domain.Apps.Read.Webhooks { try { - await webhookEventRepository.QueryPendingAsync(blockBlock.SendAsync, cancellationToken); + var now = clock.GetCurrentInstant(); + + await ruleEventRepository.QueryPendingAsync(now, blockBlock.SendAsync, cancellationToken); } catch (Exception ex) { @@ -96,11 +92,11 @@ namespace Squidex.Domain.Apps.Read.Webhooks } } - private async Task BlockAsync(IWebhookEventEntity @event) + private async Task BlockAsync(IRuleEventEntity @event) { try { - await webhookEventRepository.TraceSendingAsync(@event.Id); + await ruleEventRepository.MarkSendingAsync(@event.Id); return @event; } @@ -114,38 +110,51 @@ namespace Squidex.Domain.Apps.Read.Webhooks } } - private async Task MakeRequestAsync(IWebhookEventEntity @event) + private async Task MakeRequestAsync(IRuleEventEntity @event) { try { - var response = await webhookSender.SendAsync(@event.Job); + var job = @event.Job; + + var response = await ruleService.InvokeAsync(job.ActionName, job.ActionData); Instant? nextCall = null; - if (response.Result != WebhookResult.Success) + if (response.Result != RuleResult.Success) { - var now = clock.GetCurrentInstant(); - switch (@event.NumCalls) { case 0: - nextCall = now.Plus(Duration.FromMinutes(5)); + nextCall = job.Created.Plus(Duration.FromMinutes(5)); break; case 1: - nextCall = now.Plus(Duration.FromHours(1)); + nextCall = job.Created.Plus(Duration.FromHours(1)); break; case 2: - nextCall = now.Plus(Duration.FromHours(5)); + nextCall = job.Created.Plus(Duration.FromHours(6)); break; case 3: - nextCall = now.Plus(Duration.FromHours(6)); + nextCall = job.Created.Plus(Duration.FromHours(12)); break; } } - await Task.WhenAll( - webhookRepository.TraceSentAsync(@event.Job.WebhookId, response.Result, response.Elapsed), - webhookEventRepository.TraceSentAsync(@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/src/Squidex.Domain.Apps.Read/Rules/RuleEnqueuer.cs b/src/Squidex.Domain.Apps.Read/Rules/RuleEnqueuer.cs new file mode 100644 index 000000000..01870edb7 --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/Rules/RuleEnqueuer.cs @@ -0,0 +1,72 @@ +// ========================================================================== +// 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 ".*"; } + } + + 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) + { + await ruleEventRepository.EnqueueAsync(job, job.Created); + } + } + } + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Read/Webhooks/WebhookJobResult.cs b/src/Squidex.Domain.Apps.Read/Rules/RuleJobResult.cs similarity index 79% rename from src/Squidex.Domain.Apps.Read/Webhooks/WebhookJobResult.cs rename to src/Squidex.Domain.Apps.Read/Rules/RuleJobResult.cs index 57d68fc19..d8fb997e7 100644 --- a/src/Squidex.Domain.Apps.Read/Webhooks/WebhookJobResult.cs +++ b/src/Squidex.Domain.Apps.Read/Rules/RuleJobResult.cs @@ -1,14 +1,14 @@ // ========================================================================== -// WebhookJobResult.cs +// RuleJobResult.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -namespace Squidex.Domain.Apps.Read.Webhooks +namespace Squidex.Domain.Apps.Read.Rules { - public enum WebhookJobResult + public enum RuleJobResult { Pending, Success, 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/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/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/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/Rules/Commands/CreateRule.cs b/src/Squidex.Domain.Apps.Write/Rules/Commands/CreateRule.cs new file mode 100644 index 000000000..1c6bafd7e --- /dev/null +++ b/src/Squidex.Domain.Apps.Write/Rules/Commands/CreateRule.cs @@ -0,0 +1,20 @@ +// ========================================================================== +// CreateRule.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// 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.Domain.Apps.Write/Rules/Commands/DeleteRule.cs b/src/Squidex.Domain.Apps.Write/Rules/Commands/DeleteRule.cs new file mode 100644 index 000000000..c97f2c6b1 --- /dev/null +++ b/src/Squidex.Domain.Apps.Write/Rules/Commands/DeleteRule.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// DeleteRule.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Write.Rules.Commands +{ + public sealed class DeleteRule : RuleAggregateCommand + { + } +} diff --git a/src/Squidex.Domain.Apps.Write/Rules/Commands/DisableRule.cs b/src/Squidex.Domain.Apps.Write/Rules/Commands/DisableRule.cs new file mode 100644 index 000000000..ccfa2a9be --- /dev/null +++ b/src/Squidex.Domain.Apps.Write/Rules/Commands/DisableRule.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// DisableRule.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Write.Rules.Commands +{ + public sealed class DisableRule : RuleAggregateCommand + { + } +} diff --git a/src/Squidex.Domain.Apps.Write/Rules/Commands/EnableRule.cs b/src/Squidex.Domain.Apps.Write/Rules/Commands/EnableRule.cs new file mode 100644 index 000000000..ac3bf7f4c --- /dev/null +++ b/src/Squidex.Domain.Apps.Write/Rules/Commands/EnableRule.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// EnableRule.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Write.Rules.Commands +{ + public sealed class EnableRule : RuleAggregateCommand + { + } +} 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.Write/Rules/Commands/RuleEditCommand.cs b/src/Squidex.Domain.Apps.Write/Rules/Commands/RuleEditCommand.cs new file mode 100644 index 000000000..b63be728a --- /dev/null +++ b/src/Squidex.Domain.Apps.Write/Rules/Commands/RuleEditCommand.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// RuleEditCommand.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Rules; + +namespace Squidex.Domain.Apps.Write.Rules.Commands +{ + public abstract class RuleEditCommand : RuleAggregateCommand + { + 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/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.Infrastructure/Dispatching/ActionContextDispatcher.cs b/src/Squidex.Infrastructure/Dispatching/ActionContextDispatcher.cs index c0dabb460..9f7ef7d67 100644 --- a/src/Squidex.Infrastructure/Dispatching/ActionContextDispatcher.cs +++ b/src/Squidex.Infrastructure/Dispatching/ActionContextDispatcher.cs @@ -10,9 +10,11 @@ using System; using System.Linq; using System.Reflection; +#pragma warning disable IDE0033 // Use explicitly provided tuple name + namespace Squidex.Infrastructure.Dispatching { - public sealed class ActionContextDispatcher + public static class ActionContextDispatcher { public delegate void ActionContextDelegate(TTarget target, T input, TContext context) where T : TIn; diff --git a/src/Squidex.Infrastructure/Dispatching/ActionDispatcher.cs b/src/Squidex.Infrastructure/Dispatching/ActionDispatcher.cs index 5663a5e52..4902f0f44 100644 --- a/src/Squidex.Infrastructure/Dispatching/ActionDispatcher.cs +++ b/src/Squidex.Infrastructure/Dispatching/ActionDispatcher.cs @@ -10,9 +10,11 @@ using System; using System.Linq; using System.Reflection; +#pragma warning disable IDE0033 // Use explicitly provided tuple name + namespace Squidex.Infrastructure.Dispatching { - public sealed class ActionDispatcher + public static class ActionDispatcher { public delegate void ActionDelegate(TTarget target, T input) where T : TIn; diff --git a/src/Squidex.Infrastructure/Dispatching/FuncContextDispatcher.cs b/src/Squidex.Infrastructure/Dispatching/FuncContextDispatcher.cs index 294488276..00042fe62 100644 --- a/src/Squidex.Infrastructure/Dispatching/FuncContextDispatcher.cs +++ b/src/Squidex.Infrastructure/Dispatching/FuncContextDispatcher.cs @@ -9,9 +9,11 @@ using System.Linq; using System.Reflection; +#pragma warning disable IDE0033 // Use explicitly provided tuple name + namespace Squidex.Infrastructure.Dispatching { - public sealed class FuncContextDispatcher + public static class FuncContextDispatcher { public delegate TOut FuncContextDelegate(TTarget target, T input, TContext context) where T : TIn; diff --git a/src/Squidex.Infrastructure/Dispatching/FuncDispatcher.cs b/src/Squidex.Infrastructure/Dispatching/FuncDispatcher.cs index d600e5b33..08e1fd48e 100644 --- a/src/Squidex.Infrastructure/Dispatching/FuncDispatcher.cs +++ b/src/Squidex.Infrastructure/Dispatching/FuncDispatcher.cs @@ -9,9 +9,11 @@ using System.Linq; using System.Reflection; +#pragma warning disable IDE0033 // Use explicitly provided tuple name + namespace Squidex.Infrastructure.Dispatching { - public sealed class FuncDispatcher + public static class FuncDispatcher { public delegate TOut FuncDelegate(TTarget target, T input) where T : TIn; diff --git a/src/Squidex.Infrastructure/SquidexInfrastructure.cs b/src/Squidex.Infrastructure/SquidexInfrastructure.cs new file mode 100644 index 000000000..c0a3692f3 --- /dev/null +++ b/src/Squidex.Infrastructure/SquidexInfrastructure.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// SquidexInfrastructure.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Infrastructure +{ + public static class SquidexInfrastructure + { + } +} diff --git a/src/Squidex.Infrastructure/TypeNameRegistry.cs b/src/Squidex.Infrastructure/TypeNameRegistry.cs index f1aef2a1f..61135259c 100644 --- a/src/Squidex.Infrastructure/TypeNameRegistry.cs +++ b/src/Squidex.Infrastructure/TypeNameRegistry.cs @@ -17,20 +17,6 @@ namespace Squidex.Infrastructure private readonly Dictionary namesByType = new Dictionary(); private readonly Dictionary typesByName = new Dictionary(StringComparer.OrdinalIgnoreCase); - public TypeNameRegistry Map(Type type) - { - Guard.NotNull(type, nameof(type)); - - var typeNameAttribute = type.GetTypeInfo().GetCustomAttribute(); - - if (typeNameAttribute != null) - { - Map(type, typeNameAttribute.TypeName); - } - - return this; - } - public TypeNameRegistry MapObsolete(Type type, string name) { Guard.NotNull(type, nameof(type)); @@ -56,6 +42,20 @@ namespace Squidex.Infrastructure return this; } + public TypeNameRegistry Map(Type type) + { + Guard.NotNull(type, nameof(type)); + + var typeNameAttribute = type.GetTypeInfo().GetCustomAttribute(); + + if (!string.IsNullOrWhiteSpace(typeNameAttribute?.TypeName)) + { + Map(type, typeNameAttribute.TypeName); + } + + return this; + } + public TypeNameRegistry Map(Type type, string name) { Guard.NotNull(type, nameof(type)); @@ -95,15 +95,13 @@ namespace Squidex.Infrastructure return this; } - public TypeNameRegistry Map(Assembly assembly) + public TypeNameRegistry MapUnmapped(Assembly assembly) { foreach (var type in assembly.GetTypes()) { - var typeNameAttribute = type.GetTypeInfo().GetCustomAttribute(); - - if (!string.IsNullOrWhiteSpace(typeNameAttribute?.TypeName)) + if (!namesByType.ContainsKey(type)) { - Map(type, typeNameAttribute.TypeName); + Map(type); } } 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/Serializers.cs b/src/Squidex/Config/Domain/Serializers.cs index 400333d51..97c3d3139 100644 --- a/src/Squidex/Config/Domain/Serializers.cs +++ b/src/Squidex/Config/Domain/Serializers.cs @@ -6,18 +6,18 @@ // All rights reserved. // ========================================================================== -using System.Reflection; using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using NodaTime; using NodaTime.Serialization.JsonNet; +using Squidex.Domain.Apps.Core; 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.Domain.Apps.Events; using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.Json; using Squidex.Infrastructure.MongoDb; @@ -45,6 +45,7 @@ namespace Squidex.Config.Domain new NamedStringIdConverter(), new PropertiesBagConverter(), new RefTokenConverter(), + new RuleConverter(), new SchemaConverter(FieldRegistry), new StringEnumConverter()); @@ -62,8 +63,9 @@ namespace Squidex.Config.Domain static Serializers() { - TypeNameRegistry.Map(typeof(SquidexEvent).GetTypeInfo().Assembly); - TypeNameRegistry.Map(typeof(NoopEvent).GetTypeInfo().Assembly); + TypeNameRegistry.MapUnmapped(typeof(SquidexCoreModel).Assembly); + TypeNameRegistry.MapUnmapped(typeof(SquidexEvents).Assembly); + TypeNameRegistry.MapUnmapped(typeof(SquidexInfrastructure).Assembly); ConfigureJson(SerializerSettings, TypeNameHandling.Auto); 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 c93e5ea55..e6ce99535 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.Domain.Users; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Pipeline.CommandMiddlewares; @@ -80,7 +80,7 @@ namespace Squidex.Config.Domain .As() .SingleInstance(); - builder.RegisterType() + builder.RegisterType() .As() .SingleInstance(); @@ -96,7 +96,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..339fabf2c --- /dev/null +++ b/src/Squidex/Controllers/Api/Rules/Models/Converters/RuleConverter.cs @@ -0,0 +1,72 @@ +// ========================================================================== +// RuleConverter.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +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, Guid id) + { + var command = new UpdateRule { RuleId = id }; + + if (dto.Action != null) + { + command.Action = dto.Action.ToAction(); + } + + if (dto.Trigger != null) + { + command.Trigger = dto.Trigger.ToTrigger(); + } + + return command; + } + + public static CreateRule ToCommand(this CreateRuleDto dto) + { + var command = new CreateRule(); + + if (dto.Action != null) + { + command.Action = dto.Action.ToAction(); + } + + if (dto.Trigger != null) + { + command.Trigger = dto.Trigger.ToTrigger(); + } + + 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/CreateWebhookDto.cs b/src/Squidex/Controllers/Api/Rules/Models/CreateRuleDto.cs similarity index 60% rename from src/Squidex/Controllers/Api/Webhooks/Models/CreateWebhookDto.cs rename to src/Squidex/Controllers/Api/Rules/Models/CreateRuleDto.cs index d7dcd7f46..060ff6461 100644 --- a/src/Squidex/Controllers/Api/Webhooks/Models/CreateWebhookDto.cs +++ b/src/Squidex/Controllers/Api/Rules/Models/CreateRuleDto.cs @@ -1,29 +1,27 @@ // ========================================================================== -// CreateWebhookDto.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 CreateWebhookDto + 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..2b95ed5b6 --- /dev/null +++ b/src/Squidex/Controllers/Api/Rules/Models/RuleActionDto.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// RuleActionDto.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Runtime.Serialization; +using Newtonsoft.Json; +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..f65ea3f46 --- /dev/null +++ b/src/Squidex/Controllers/Api/Rules/Models/RuleTriggerDto.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// RuleTriggerDto.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Runtime.Serialization; +using Newtonsoft.Json; +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/UpdateWebhookDto.cs b/src/Squidex/Controllers/Api/Rules/Models/UpdateRuleDto.cs similarity index 50% rename from src/Squidex/Controllers/Api/Webhooks/Models/UpdateWebhookDto.cs rename to src/Squidex/Controllers/Api/Rules/Models/UpdateRuleDto.cs index d5b7da231..bad710cc8 100644 --- a/src/Squidex/Controllers/Api/Webhooks/Models/UpdateWebhookDto.cs +++ b/src/Squidex/Controllers/Api/Rules/Models/UpdateRuleDto.cs @@ -1,29 +1,23 @@ // ========================================================================== -// UpdateWebhookDto.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 UpdateWebhookDto + 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..100bb3633 --- /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(id); + + 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 NoContent(); + } + } +} 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..31e33fbad 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/FieldPropertiesDto.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/FieldPropertiesDto.cs @@ -9,7 +9,7 @@ using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; using Newtonsoft.Json; -using Squidex.Controllers.Api.Schemas.Models.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/JsonInheritanceConverter.cs similarity index 98% rename from src/Squidex/Controllers/Api/Schemas/Models/Converters/JsonInheritanceConverter.cs rename to src/Squidex/Controllers/JsonInheritanceConverter.cs index e55080547..5076a3791 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/Converters/JsonInheritanceConverter.cs +++ b/src/Squidex/Controllers/JsonInheritanceConverter.cs @@ -16,7 +16,7 @@ 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 { 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/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/content/pages/content/content-page.component.ts b/src/Squidex/app/features/content/pages/content/content-page.component.ts index aa85c4da8..a2b6fd49b 100644 --- a/src/Squidex/app/features/content/pages/content/content-page.component.ts +++ b/src/Squidex/app/features/content/pages/content/content-page.component.ts @@ -208,8 +208,12 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone private enableContentForm() { this.contentForm.markAsPristine(); - for (const field of this.schema.fields.filter(f => !f.isDisabled)) { - this.contentForm.controls[field.name].enable(); + if (this.schema.fields.length === 0) { + this.contentForm.enable(); + } else { + for (const field of this.schema.fields.filter(f => !f.isDisabled)) { + this.contentForm.controls[field.name].enable(); + } } } 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 94% 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..bdbf99255 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 @@ - +
@@ -39,7 +39,7 @@ Event - Url + Description Created @@ -57,7 +57,7 @@ {{event.eventName}} - {{event.requestUrl}} + {{event.description}} {{event.created | sqxFromNow}} @@ -71,7 +71,7 @@
-

Last Request

+

Last Invocation

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 87% 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 index e131e5532..bc650f4f1 100644 --- 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 @@ -5,6 +5,10 @@ h3 { margin-bottom: 1rem; } +.truncate { + @include truncate; +} + .event { &-stats { font-size: .8rem; @@ -31,7 +35,7 @@ h3 { &::before { @include caret-top($color-border); - @include absolute(-1.1rem, 1.9rem, auto, auto); + @include absolute(-1.1rem, 2.3rem, auto, auto); } h3 { 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..88bc0b3a7 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html @@ -0,0 +1,91 @@ + \ 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..f30dac5d9 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.ts @@ -0,0 +1,168 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; + +import { + AppComponentBase, + AppsStoreService, + AuthService, + CreateRuleDto, + DateTime, + DialogService, + fadeAnimation, + ruleActions, + ruleTriggers, + RuleDto, + RulesService, + SchemaDto, + UpdateRuleDto +} from 'shared'; + +export const MODE_WIZARD = 'Wizard'; +export const MODE_EDIT_TRIGGER = 'EditTrigger'; +export const MODE_EDIT_ACTION = 'EditAction'; + +@Component({ + selector: 'sqx-rule-wizard', + styleUrls: ['./rule-wizard.component.scss'], + templateUrl: './rule-wizard.component.html', + animations: [ + fadeAnimation + ] +}) +export class RuleWizardComponent extends AppComponentBase implements OnInit { + 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(); + + @Output() + public updated = new EventEmitter(); + + @Input() + public schemas: SchemaDto[]; + + @Input() + public rule: RuleDto; + + @Input() + public mode = MODE_WIZARD; + + constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService, + private readonly rulesService: RulesService + ) { + super(dialogs, apps, authService); + } + + public ngOnInit() { + if (this.mode === MODE_EDIT_ACTION) { + this.step = 4; + + this.action = Object.assign({}, this.rule.action); + this.actionType = this.rule.actionType; + + delete this.action.actionType; + } else if (this.mode === MODE_EDIT_TRIGGER) { + this.step = 2; + + this.trigger = Object.assign({}, this.rule.trigger); + this.triggerType = this.rule.triggerType; + + delete this.trigger.triggerType; + } + } + + public selectTriggerType(type: string) { + this.triggerType = type; + this.step++; + } + + public selectActionType(type: string) { + this.actionType = type; + this.step++; + } + + public selectTrigger(value: any) { + this.trigger = Object.assign({}, value, { triggerType: this.triggerType }); + + if (this.mode === MODE_WIZARD) { + this.step++; + } else { + this.updateTrigger(); + } + } + + public selectAction(value: any) { + this.action = Object.assign({}, value, { actionType: this.actionType }); + + if (this.mode === MODE_WIZARD) { + this.createRule(); + } else { + this.updateAction(); + } + } + + private createRule() { + 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); + }); + } + + private updateTrigger() { + const requestDto = new UpdateRuleDto(this.trigger, null); + + this.appNameOnce() + .switchMap(app => this.rulesService.putRule(app, this.rule.id, requestDto, this.rule.version)) + .subscribe(dto => { + const rule = this.rule.updateTrigger(this.trigger, this.authService.user.id, dto.version, DateTime.now()); + this.updated.emit(rule); + }, error => { + this.notifyError(error); + }); + } + + private updateAction() { + const requestDto = new UpdateRuleDto(null, this.action); + + this.appNameOnce() + .switchMap(app => this.rulesService.putRule(app, this.rule.id, requestDto, this.rule.version)) + .subscribe(dto => { + const rule = this.rule.updateAction(this.action, this.authService.user.id, dto.version, DateTime.now()); + + this.updated.emit(rule); + }, 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..448ecb97e --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/rules-page.component.html @@ -0,0 +1,108 @@ + + + +
+
+
+ + + + + + +
+ +

Rules

+
+ + + + +
+ +
+
+
+ No Rule created yet. +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+

If

+
+ + + + + + {{ruleTriggers[rule.triggerType]}} + + + +

then

+
+ + + + + + {{ruleActions[rule.actionType]}} + + + + + + +
+
+ +
+ + + +
+
+
+ + + + \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/rules-page.component.scss b/src/Squidex/app/features/rules/pages/rules/rules-page.component.scss new file mode 100644 index 000000000..c7674827d --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/rules-page.component.scss @@ -0,0 +1,26 @@ +@import '_vars'; +@import '_mixins'; + +sqx-toggle { + display: inline-block; +} + +.table-items { + tbody { + td { + padding-bottom: .5rem; + } + } +} + +.step-if { + padding-left: 1.25rem; + padding-right: 0; + text-align: left; +} + +.step-then { + padding-left: 0; + padding-right: 0; + text-align: center; +} \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/rules-page.component.ts b/src/Squidex/app/features/rules/pages/rules/rules-page.component.ts new file mode 100644 index 000000000..4062e78d9 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/rules-page.component.ts @@ -0,0 +1,138 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Component, OnInit } from '@angular/core'; + +import { + AppComponentBase, + AppsStoreService, + AuthService, + DateTime, + DialogService, + fadeAnimation, + ImmutableArray, + ModalView, + ruleActions, + ruleTriggers, + 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 ruleActions = ruleActions; + public ruleTriggers = ruleTriggers; + + public addRuleDialog = new ModalView(); + + public rules: ImmutableArray; + public schemas: SchemaDto[]; + + public wizardMode = 'Wizard'; + public wizardRule: RuleDto; + + 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); + }); + } + + public createNew() { + this.wizardMode = 'Wizard'; + this.wizardRule = null; + + this.addRuleDialog.show(); + } + + public editTrigger(rule: RuleDto) { + this.wizardMode = 'EditTrigger'; + this.wizardRule = rule; + + this.addRuleDialog.show(); + } + + public editAction(rule: RuleDto) { + this.wizardMode = 'EditAction'; + this.wizardRule = rule; + + this.addRuleDialog.show(); + } + + public onRuleUpdated(rule: RuleDto) { + this.rules = this.rules.replaceBy('id', rule); + + this.addRuleDialog.hide(); + } + + public onRuleCreated(rule: RuleDto) { + this.rules = this.rules.push(rule); + + this.addRuleDialog.hide(); + } + + public toggleRule(rule: RuleDto) { + if (rule.isEnabled) { + this.appNameOnce() + .switchMap(app => this.rulesService.disableRule(app, rule.id, rule.version)) + .subscribe(dto => { + this.rules = this.rules.replace(rule, rule.disable(this.authService.user.id, dto.version, DateTime.now())); + }, error => { + this.notifyError(error); + }); + } else { + this.appNameOnce() + .switchMap(app => this.rulesService.enableRule(app, rule.id, rule.version)) + .subscribe(dto => { + this.rules = this.rules.replace(rule, rule.enable(this.authService.user.id, dto.version, DateTime.now())); + }, error => { + this.notifyError(error); + }); + } + } + + public deleteRule(rule: RuleDto) { + this.appNameOnce() + .switchMap(app => this.rulesService.deleteRule(app, rule.id, rule.version)) + .subscribe(dto => { + this.rules = this.rules.remove(rule); + }, 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..dca702cbe --- /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 + + P +
+ {{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..dabe56214 --- /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; + sendPublish: 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, + sendPublish: triggerSchema.sendPublish + }); + } 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, + sendPublish: s.sendPublish + }; + }); + + 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, + 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 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.sendPublish = value; + return schemaForm; + } + + private updateSendAll(schemaForm: TriggerSchemaForm): TriggerSchemaForm { + schemaForm.sendAll = + schemaForm.sendCreate && + schemaForm.sendUpdate && + schemaForm.sendDelete && + schemaForm.sendPublish; + + 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.scss b/src/Squidex/app/features/webhooks/pages/webhooks-page.component.scss deleted file mode 100644 index f78f04a07..000000000 --- a/src/Squidex/app/features/webhooks/pages/webhooks-page.component.scss +++ /dev/null @@ -1,10 +0,0 @@ -@import '_vars'; -@import '_mixins'; - -.failed { - color: $color-theme-error; -} - -.success { - color: $color-theme-green; -} \ 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..f3a3728b9 --- /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).toEqual(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/angular/toggle.component.scss b/src/Squidex/app/framework/angular/toggle.component.scss index b0a2f4323..d3c6432b6 100644 --- a/src/Squidex/app/framework/angular/toggle.component.scss +++ b/src/Squidex/app/framework/angular/toggle.component.scss @@ -1,8 +1,8 @@ @import '_mixins'; @import '_vars'; -$toggle-width: 3.2rem; -$toggle-height: 2rem; +$toggle-width: 2.2rem; +$toggle-height: 1.4rem; $toggle-button-size: $toggle-height - .3rem; .toggle { diff --git a/src/Squidex/app/framework/angular/toggle.component.ts b/src/Squidex/app/framework/angular/toggle.component.ts index bac7739ba..5fa5aaa23 100644 --- a/src/Squidex/app/framework/angular/toggle.component.ts +++ b/src/Squidex/app/framework/angular/toggle.component.ts @@ -28,7 +28,7 @@ export class ToggleComponent implements ControlValueAccessor { public isDisabled = false; public writeValue(value: boolean | null | undefined) { - this.isChecked = Types.isBoolean(value) ? value || null : null; + this.isChecked = Types.isBoolean(value) ? value : null; } public setDisabledState(isDisabled: boolean): void { 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..daa592c37 --- /dev/null +++ b/src/Squidex/app/shared/services/rules.service.spec.ts @@ -0,0 +1,323 @@ +/* + * 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', () => { + const trigger = { param2: 2, triggerType: 'NewType' }; + + const rule_1 = new RuleDto('id1', creator, creator, creation, creation, version, true, {}, 'contentChanged', {}, 'webhook'); + const rule_2 = rule_1.updateTrigger(trigger, modifier, newVersion, modified); + + expect(rule_2.trigger).toEqual(trigger); + expect(rule_2.triggerType).toEqual(trigger.triggerType); + expect(rule_2.lastModified).toEqual(modified); + expect(rule_2.lastModifiedBy).toEqual(modifier); + expect(rule_2.version).toEqual(newVersion); + }); + + it('should update action', () => { + const action = { param2: 2, actionType: 'NewType' }; + + const rule_1 = new RuleDto('id1', creator, creator, creation, creation, version, true, {}, 'contentChanged', {}, 'webhook'); + const rule_2 = rule_1.updateAction(action, modifier, newVersion, modified); + + expect(rule_2.action).toEqual(action); + expect(rule_2.actionType).toEqual(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, + triggerType: 'ContentChanged' + }, + 'ContentChanged', + { + param3: 3, + param4: 4, + actionType: 'Webhook' + }, + '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', user, user, now, now, + version, + true, + { + param1: 1, + param2: 2, + triggerType: 'ContentChanged' + }, + 'ContentChanged', + { + param3: 3, + param4: 4, + actionType: 'Webhook' + }, + '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..2a5ae47a0 --- /dev/null +++ b/src/Squidex/app/shared/services/rules.service.ts @@ -0,0 +1,273 @@ +/* + * 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 Webhook' +}; + +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 updateTrigger(trigger: any, user: string, version: Version, now?: DateTime): RuleDto { + return new RuleDto( + this.id, + this.createdBy, user, + this.created, now || DateTime.now(), + version, + this.isEnabled, + trigger, + trigger.triggerType, + this.action, + this.action.actionType); + } + + public updateAction(action: any, user: string, version: Version, now?: DateTime): RuleDto { + return new RuleDto( + this.id, + this.createdBy, user, + this.created, now || DateTime.now(), + version, + this.isEnabled, + this.trigger, + this.trigger.triggerType, + action, + 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, + false, + 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 @@