mirror of https://github.com/Squidex/squidex.git
committed by
GitHub
197 changed files with 6479 additions and 3362 deletions
@ -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> |
||||
|
{ |
||||
|
T Visit(WebhookAction action); |
||||
|
} |
||||
|
} |
||||
@ -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> |
||||
|
{ |
||||
|
T Visit(ContentChangedTrigger trigger); |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<Rule> |
||||
|
{ |
||||
|
protected override Rule ReadValue(JsonReader reader, JsonSerializer serializer) |
||||
|
{ |
||||
|
return serializer.Deserialize<JsonRule>(reader).ToRule(); |
||||
|
} |
||||
|
|
||||
|
protected override void WriteValue(JsonWriter writer, Rule value, JsonSerializer serializer) |
||||
|
{ |
||||
|
serializer.Serialize(writer, new JsonRule(value)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<T>(IRuleActionVisitor<T> visitor); |
||||
|
} |
||||
|
} |
||||
@ -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<string, JToken> |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -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<T>(IRuleTriggerVisitor<T> visitor); |
||||
|
} |
||||
|
} |
||||
@ -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<ContentChangedTriggerSchema> Schemas { get; set; } |
||||
|
|
||||
|
public override T Accept<T>(IRuleTriggerVisitor<T> visitor) |
||||
|
{ |
||||
|
return visitor.Visit(this); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,14 +1,14 @@ |
|||||
// ==========================================================================
|
// ==========================================================================
|
||||
// UpdateWebhook.cs
|
// SquidexCoreModel.cs
|
||||
// Squidex Headless CMS
|
// Squidex Headless CMS
|
||||
// ==========================================================================
|
// ==========================================================================
|
||||
// Copyright (c) Squidex Group
|
// Copyright (c) Squidex Group
|
||||
// All rights reserved.
|
// All rights reserved.
|
||||
// ==========================================================================
|
// ==========================================================================
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Write.Webhooks.Commands |
namespace Squidex.Domain.Apps.Core |
||||
{ |
{ |
||||
public sealed class UpdateWebhook : WebhookEditCommand |
public static class SquidexCoreModel |
||||
{ |
{ |
||||
} |
} |
||||
} |
} |
||||
@ -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<WebhookAction> |
||||
|
{ |
||||
|
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<AppEvent> @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<AppEvent> @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<string, JToken> 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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<AppEvent> @event, string eventName, RuleAction action); |
||||
|
|
||||
|
Task<(string Dump, Exception Exception)> ExecuteJobAsync(RuleJobData data); |
||||
|
} |
||||
|
} |
||||
@ -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<AppEvent> @event, RuleTrigger trigger); |
||||
|
} |
||||
|
} |
||||
@ -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<T> : IRuleActionHandler where T : RuleAction |
||||
|
{ |
||||
|
Type IRuleActionHandler.ActionType |
||||
|
{ |
||||
|
get { return typeof(T); } |
||||
|
} |
||||
|
|
||||
|
(string Description, RuleJobData Data) IRuleActionHandler.CreateJob(Envelope<AppEvent> @event, string eventName, RuleAction action) |
||||
|
{ |
||||
|
return CreateJob(@event, eventName, (T)action); |
||||
|
} |
||||
|
|
||||
|
protected abstract (string Description, RuleJobData Data) CreateJob(Envelope<AppEvent> @event, string eventName, T action); |
||||
|
|
||||
|
public abstract Task<(string Dump, Exception Exception)> ExecuteJobAsync(RuleJobData job); |
||||
|
} |
||||
|
} |
||||
@ -1,14 +1,14 @@ |
|||||
// ==========================================================================
|
// ==========================================================================
|
||||
// WebhookResult.cs
|
// RuleResult.cs
|
||||
// Squidex Headless CMS
|
// Squidex Headless CMS
|
||||
// ==========================================================================
|
// ==========================================================================
|
||||
// Copyright (c) Squidex Group
|
// Copyright (c) Squidex Group
|
||||
// All rights reserved.
|
// All rights reserved.
|
||||
// ==========================================================================
|
// ==========================================================================
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Read.Webhooks |
namespace Squidex.Domain.Apps.Core.HandleRules |
||||
{ |
{ |
||||
public enum WebhookResult |
public enum RuleResult |
||||
{ |
{ |
||||
Pending, |
Pending, |
||||
Success, |
Success, |
||||
@ -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<Type, IRuleActionHandler> ruleActionHandlers; |
||||
|
private readonly Dictionary<Type, IRuleTriggerHandler> ruleTriggerHandlers; |
||||
|
private readonly TypeNameRegistry typeNameRegistry; |
||||
|
private readonly IClock clock; |
||||
|
|
||||
|
public RuleService( |
||||
|
IEnumerable<IRuleTriggerHandler> ruleTriggerHandlers, |
||||
|
IEnumerable<IRuleActionHandler> 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<IEvent> @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<AppEvent>(); |
||||
|
|
||||
|
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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<T> : IRuleTriggerHandler where T : RuleTrigger |
||||
|
{ |
||||
|
public Type TriggerType |
||||
|
{ |
||||
|
get { return typeof(T); } |
||||
|
} |
||||
|
|
||||
|
bool IRuleTriggerHandler.Triggers(Envelope<AppEvent> @event, RuleTrigger trigger) |
||||
|
{ |
||||
|
return Triggers(@event, (T)trigger); |
||||
|
} |
||||
|
|
||||
|
protected abstract bool Triggers(Envelope<AppEvent> @event, T trigger); |
||||
|
} |
||||
|
} |
||||
@ -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<ContentChangedTrigger> |
||||
|
{ |
||||
|
protected override bool Triggers(Envelope<AppEvent> @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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,18 +1,21 @@ |
|||||
// ==========================================================================
|
// ==========================================================================
|
||||
// WebhookCreated.cs
|
// RuleCreated.cs
|
||||
// Squidex Headless CMS
|
// Squidex Headless CMS
|
||||
// ==========================================================================
|
// ==========================================================================
|
||||
// Copyright (c) Squidex Group
|
// Copyright (c) Squidex Group
|
||||
// All rights reserved.
|
// All rights reserved.
|
||||
// ==========================================================================
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Domain.Apps.Core.Rules; |
||||
using Squidex.Infrastructure.CQRS.Events; |
using Squidex.Infrastructure.CQRS.Events; |
||||
|
|
||||
namespace Squidex.Domain.Apps.Events.Webhooks |
namespace Squidex.Domain.Apps.Events.Rules |
||||
{ |
{ |
||||
[EventType(nameof(WebhookCreated))] |
[EventType(nameof(RuleCreated))] |
||||
public sealed class WebhookCreated : WebhookEditEvent |
public sealed class RuleCreated : RuleEvent |
||||
{ |
{ |
||||
public string SharedSecret { get; set; } |
public RuleTrigger Trigger { get; set; } |
||||
|
|
||||
|
public RuleAction Action { get; set; } |
||||
} |
} |
||||
} |
} |
||||
@ -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 |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -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; } |
||||
|
} |
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,14 +1,14 @@ |
|||||
// ==========================================================================
|
// ==========================================================================
|
||||
// DeleteWebhook.cs
|
// SquidexEvents.cs
|
||||
// Squidex Headless CMS
|
// Squidex Headless CMS
|
||||
// ==========================================================================
|
// ==========================================================================
|
||||
// Copyright (c) Squidex Group
|
// Copyright (c) Squidex Group
|
||||
// All rights reserved.
|
// All rights reserved.
|
||||
// ==========================================================================
|
// ==========================================================================
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Write.Webhooks.Commands |
namespace Squidex.Domain.Apps.Events |
||||
{ |
{ |
||||
public sealed class DeleteWebhook : WebhookAggregateCommand |
public static class SquidexEvents |
||||
{ |
{ |
||||
} |
} |
||||
} |
} |
||||
@ -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; } |
||||
|
} |
||||
|
} |
||||
@ -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<MongoRuleEntity>, IRuleRepository, IEventConsumer |
||||
|
{ |
||||
|
private static readonly List<IRuleEntity> EmptyRules = new List<IRuleEntity>(); |
||||
|
private readonly SemaphoreSlim lockObject = new SemaphoreSlim(1); |
||||
|
private Dictionary<Guid, List<IRuleEntity>> inMemoryRules; |
||||
|
|
||||
|
public MongoRuleRepository(IMongoDatabase database) |
||||
|
: base(database) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
protected override string CollectionName() |
||||
|
{ |
||||
|
return "Projections_Rules"; |
||||
|
} |
||||
|
|
||||
|
protected override Task SetupCollectionAsync(IMongoCollection<MongoRuleEntity> collection) |
||||
|
{ |
||||
|
return Task.WhenAll(collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AppId))); |
||||
|
} |
||||
|
|
||||
|
public async Task<IReadOnlyList<IRuleEntity>> QueryByAppAsync(Guid appId) |
||||
|
{ |
||||
|
var entities = |
||||
|
await Collection.Find(x => x.AppId == appId) |
||||
|
.ToListAsync(); |
||||
|
|
||||
|
return entities.OfType<IRuleEntity>().ToList(); |
||||
|
} |
||||
|
|
||||
|
public async Task<IReadOnlyList<IRuleEntity>> 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<Guid, List<IRuleEntity>>(); |
||||
|
|
||||
|
var webhooks = |
||||
|
await Collection.Find(new BsonDocument()) |
||||
|
.ToListAsync(); |
||||
|
|
||||
|
foreach (var webhook in webhooks) |
||||
|
{ |
||||
|
inMemoryRules.GetOrAddNew(webhook.AppId).Add(webhook); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
lockObject.Release(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<IEvent> @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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<WebhookSchema> Schemas { get; set; } |
|
||||
|
|
||||
[BsonRequired] |
|
||||
[BsonElement] |
|
||||
public List<Guid> SchemaIds { get; set; } |
|
||||
|
|
||||
IEnumerable<WebhookSchema> IWebhookEntity.Schemas |
|
||||
{ |
|
||||
get { return Schemas; } |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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<MongoWebhookEntity>, IWebhookRepository, IEventConsumer |
|
||||
{ |
|
||||
private static readonly List<IWebhookEntity> EmptyWebhooks = new List<IWebhookEntity>(); |
|
||||
private readonly SemaphoreSlim lockObject = new SemaphoreSlim(1); |
|
||||
private Dictionary<Guid, List<IWebhookEntity>> inMemoryWebhooks; |
|
||||
|
|
||||
public MongoWebhookRepository(IMongoDatabase database) |
|
||||
: base(database) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
protected override string CollectionName() |
|
||||
{ |
|
||||
return "Projections_SchemaWebhooks"; |
|
||||
} |
|
||||
|
|
||||
protected override Task SetupCollectionAsync(IMongoCollection<MongoWebhookEntity> collection) |
|
||||
{ |
|
||||
return Task.WhenAll( |
|
||||
collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AppId)), |
|
||||
collection.Indexes.CreateOneAsync(Index.Ascending(x => x.SchemaIds))); |
|
||||
} |
|
||||
|
|
||||
public async Task<IReadOnlyList<IWebhookEntity>> QueryByAppAsync(Guid appId) |
|
||||
{ |
|
||||
var entities = |
|
||||
await Collection.Find(x => x.AppId == appId) |
|
||||
.ToListAsync(); |
|
||||
|
|
||||
return entities.OfType<IWebhookEntity>().ToList(); |
|
||||
} |
|
||||
|
|
||||
public async Task<IReadOnlyList<IWebhookEntity>> 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<Guid, List<IWebhookEntity>>(); |
|
||||
|
|
||||
var webhooks = |
|
||||
await Collection.Find(new BsonDocument()) |
|
||||
.ToListAsync(); |
|
||||
|
|
||||
foreach (var webhook in webhooks) |
|
||||
{ |
|
||||
inMemoryWebhooks.GetOrAddNew(webhook.AppId).Add(webhook); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
finally |
|
||||
{ |
|
||||
lockObject.Release(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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<IEvent> @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); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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; } |
||||
|
} |
||||
|
} |
||||
@ -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<IRuleEventEntity, Task> callback, CancellationToken cancellationToken = default(CancellationToken)); |
||||
|
|
||||
|
Task<int> CountByAppAsync(Guid appId); |
||||
|
|
||||
|
Task<IReadOnlyList<IRuleEventEntity>> QueryByAppAsync(Guid appId, int skip = 0, int take = 20); |
||||
|
|
||||
|
Task<IRuleEventEntity> FindAsync(Guid id); |
||||
|
} |
||||
|
} |
||||
@ -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<IEvent> @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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,14 +1,14 @@ |
|||||
// ==========================================================================
|
// ==========================================================================
|
||||
// WebhookJobResult.cs
|
// RuleJobResult.cs
|
||||
// Squidex Headless CMS
|
// Squidex Headless CMS
|
||||
// ==========================================================================
|
// ==========================================================================
|
||||
// Copyright (c) Squidex Group
|
// Copyright (c) Squidex Group
|
||||
// All rights reserved.
|
// All rights reserved.
|
||||
// ==========================================================================
|
// ==========================================================================
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Read.Webhooks |
namespace Squidex.Domain.Apps.Read.Rules |
||||
{ |
{ |
||||
public enum WebhookJobResult |
public enum RuleJobResult |
||||
{ |
{ |
||||
Pending, |
Pending, |
||||
Success, |
Success, |
||||
@ -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<WebhookSchema> Schemas { get; } |
|
||||
} |
|
||||
} |
|
||||
@ -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<IWebhookEventEntity, Task> callback, CancellationToken cancellationToken = default(CancellationToken)); |
|
||||
|
|
||||
Task<int> CountByAppAsync(Guid appId); |
|
||||
|
|
||||
Task<IReadOnlyList<IWebhookEventEntity>> QueryByAppAsync(Guid appId, int skip = 0, int take = 20); |
|
||||
|
|
||||
Task<IWebhookEventEntity> FindAsync(Guid id); |
|
||||
} |
|
||||
} |
|
||||
@ -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<IReadOnlyList<IWebhookEntity>> QueryByAppAsync(Guid appId); |
|
||||
|
|
||||
Task<IReadOnlyList<IWebhookEntity>> QueryCachedByAppAsync(Guid appId); |
|
||||
} |
|
||||
} |
|
||||
@ -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<IEvent> @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<IEvent> @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; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -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; } |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -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)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<Task<IEnumerable<ValidationError>>> |
||||
|
{ |
||||
|
public static Task<IEnumerable<ValidationError>> ValidateAsync(RuleAction action) |
||||
|
{ |
||||
|
Guard.NotNull(action, nameof(action)); |
||||
|
|
||||
|
var visitor = new RuleActionValidator(); |
||||
|
|
||||
|
return action.Accept(visitor); |
||||
|
} |
||||
|
|
||||
|
public Task<IEnumerable<ValidationError>> Visit(WebhookAction action) |
||||
|
{ |
||||
|
var errors = new List<ValidationError>(); |
||||
|
|
||||
|
if (action.Url == null || !action.Url.IsAbsoluteUri) |
||||
|
{ |
||||
|
errors.Add(new ValidationError("Url must be specified and absolute.", nameof(action.Url))); |
||||
|
} |
||||
|
|
||||
|
return Task.FromResult<IEnumerable<ValidationError>>(errors); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<Task<IEnumerable<ValidationError>>> |
||||
|
{ |
||||
|
public ISchemaProvider Schemas { get; } |
||||
|
|
||||
|
public RuleTriggerValidator(ISchemaProvider schemas) |
||||
|
{ |
||||
|
Schemas = schemas; |
||||
|
} |
||||
|
|
||||
|
public static Task<IEnumerable<ValidationError>> 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<IEnumerable<ValidationError>> 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<ValidationError>(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<RuleDomainObject>(context, async w => |
||||
|
{ |
||||
|
await GuardRule.CanCreate(command, schemas); |
||||
|
|
||||
|
w.Create(command); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
protected Task On(UpdateRule command, CommandContext context) |
||||
|
{ |
||||
|
return handler.UpdateAsync<RuleDomainObject>(context, async c => |
||||
|
{ |
||||
|
await GuardRule.CanUpdate(command, schemas); |
||||
|
|
||||
|
c.Update(command); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
protected Task On(EnableRule command, CommandContext context) |
||||
|
{ |
||||
|
return handler.UpdateAsync<RuleDomainObject>(context, r => |
||||
|
{ |
||||
|
GuardRule.CanEnable(command, r.Rule); |
||||
|
|
||||
|
r.Enable(command); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
protected Task On(DisableRule command, CommandContext context) |
||||
|
{ |
||||
|
return handler.UpdateAsync<RuleDomainObject>(context, r => |
||||
|
{ |
||||
|
GuardRule.CanDisable(command, r.Rule); |
||||
|
|
||||
|
r.Disable(command); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
protected Task On(DeleteRule command, CommandContext context) |
||||
|
{ |
||||
|
return handler.UpdateAsync<RuleDomainObject>(context, c => |
||||
|
{ |
||||
|
GuardRule.CanDelete(command); |
||||
|
|
||||
|
c.Delete(command); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public async Task HandleAsync(CommandContext context, Func<Task> next) |
||||
|
{ |
||||
|
if (!await this.DispatchActionAsync(context.Command, context)) |
||||
|
{ |
||||
|
await next(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<IEvent> @event) |
||||
|
{ |
||||
|
this.DispatchAction(@event.Payload); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<WebhookSchema> Schemas { get; set; } = new List<WebhookSchema>(); |
|
||||
} |
|
||||
} |
|
||||
@ -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<ValidationError> 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); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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<WebhookDomainObject>(context, async w => |
|
||||
{ |
|
||||
await GuardWebhook.CanCreate(command, schemas); |
|
||||
|
|
||||
w.Create(command); |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
protected async Task On(UpdateWebhook command, CommandContext context) |
|
||||
{ |
|
||||
await handler.UpdateAsync<WebhookDomainObject>(context, async c => |
|
||||
{ |
|
||||
await GuardWebhook.CanUpdate(command, schemas); |
|
||||
|
|
||||
c.Update(command); |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
protected Task On(DeleteWebhook command, CommandContext context) |
|
||||
{ |
|
||||
return handler.UpdateAsync<WebhookDomainObject>(context, c => |
|
||||
{ |
|
||||
GuardWebhook.CanDelete(command); |
|
||||
|
|
||||
c.Delete(command); |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
public async Task HandleAsync(CommandContext context, Func<Task> next) |
|
||||
{ |
|
||||
if (!await this.DispatchActionAsync(context.Command, context)) |
|
||||
{ |
|
||||
await next(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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<IEvent> @event) |
|
||||
{ |
|
||||
this.DispatchAction(@event.Payload); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,14 @@ |
|||||
|
// ==========================================================================
|
||||
|
// SquidexInfrastructure.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Infrastructure |
||||
|
{ |
||||
|
public static class SquidexInfrastructure |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -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<RuleActionDto> |
||||
|
{ |
||||
|
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()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<RuleTriggerDto> |
||||
|
{ |
||||
|
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() |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,29 +1,27 @@ |
|||||
// ==========================================================================
|
// ==========================================================================
|
||||
// CreateWebhookDto.cs
|
// CreateRuleDto.cs
|
||||
// Squidex Headless CMS
|
// Squidex Headless CMS
|
||||
// ==========================================================================
|
// ==========================================================================
|
||||
// Copyright (c) Squidex Group
|
// Copyright (c) Squidex Group
|
||||
// All rights reserved.
|
// All rights reserved.
|
||||
// ==========================================================================
|
// ==========================================================================
|
||||
|
|
||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.ComponentModel.DataAnnotations; |
using System.ComponentModel.DataAnnotations; |
||||
|
|
||||
namespace Squidex.Controllers.Api.Webhooks.Models |
namespace Squidex.Controllers.Api.Rules.Models |
||||
{ |
{ |
||||
public sealed class CreateWebhookDto |
public sealed class CreateRuleDto |
||||
{ |
{ |
||||
/// <summary>
|
/// <summary>
|
||||
/// The url of the webhook.
|
/// The trigger properties.
|
||||
/// </summary>
|
/// </summary>
|
||||
[Required] |
[Required] |
||||
public Uri Url { get; set; } |
public RuleTriggerDto Trigger { get; set; } |
||||
|
|
||||
/// <summary>
|
/// <summary>
|
||||
/// The schema settings.
|
/// The action properties.
|
||||
/// </summary>
|
/// </summary>
|
||||
[Required] |
[Required] |
||||
public List<WebhookSchemaDto> Schemas { get; set; } |
public RuleActionDto Action { get; set; } |
||||
} |
} |
||||
} |
} |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The id of the rule.
|
||||
|
/// </summary>
|
||||
|
public Guid Id { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The user that has created the rule.
|
||||
|
/// </summary>
|
||||
|
[Required] |
||||
|
public RefToken CreatedBy { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The user that has updated the rule.
|
||||
|
/// </summary>
|
||||
|
[Required] |
||||
|
public RefToken LastModifiedBy { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The date and time when the rule has been created.
|
||||
|
/// </summary>
|
||||
|
public Instant Created { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The date and time when the rule has been modified last.
|
||||
|
/// </summary>
|
||||
|
public Instant LastModified { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The version of the rule.
|
||||
|
/// </summary>
|
||||
|
public int Version { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The trigger properties.
|
||||
|
/// </summary>
|
||||
|
[Required] |
||||
|
public RuleTriggerDto Trigger { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The action properties.
|
||||
|
/// </summary>
|
||||
|
[Required] |
||||
|
public RuleActionDto Action { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Determines if the rule is enabled.
|
||||
|
/// </summary>
|
||||
|
public bool IsEnabled { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -1,23 +1,23 @@ |
|||||
// ==========================================================================
|
// ==========================================================================
|
||||
// WebhookEventsDto.cs
|
// RuleEventsDto.cs
|
||||
// Squidex Headless CMS
|
// Squidex Headless CMS
|
||||
// ==========================================================================
|
// ==========================================================================
|
||||
// Copyright (c) Squidex Group
|
// Copyright (c) Squidex Group
|
||||
// All rights reserved.
|
// All rights reserved.
|
||||
// ==========================================================================
|
// ==========================================================================
|
||||
|
|
||||
namespace Squidex.Controllers.Api.Webhooks.Models |
namespace Squidex.Controllers.Api.Rules.Models |
||||
{ |
{ |
||||
public sealed class WebhookEventsDto |
public sealed class RuleEventsDto |
||||
{ |
{ |
||||
/// <summary>
|
/// <summary>
|
||||
/// The total number of webhook events.
|
/// The total number of rule events.
|
||||
/// </summary>
|
/// </summary>
|
||||
public long Total { get; set; } |
public long Total { get; set; } |
||||
|
|
||||
/// <summary>
|
/// <summary>
|
||||
/// The webhook events.
|
/// The rule events.
|
||||
/// </summary>
|
/// </summary>
|
||||
public WebhookEventDto[] Items { get; set; } |
public RuleEventDto[] Items { get; set; } |
||||
} |
} |
||||
} |
} |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue