Browse Source

Started with rules.

pull/157/head
Sebastian Stehle 8 years ago
parent
commit
bfc4c9c5cc
  1. 26
      src/Squidex.Domain.Apps.Core.Model/Rules/Actions/WebhookAction.cs
  2. 17
      src/Squidex.Domain.Apps.Core.Model/Rules/IRuleActionVisitor.cs
  3. 17
      src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs
  4. 78
      src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs
  5. 15
      src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs
  6. 32
      src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs
  7. 17
      src/Squidex.Domain.Apps.Core.Model/Rules/RuleJobData.cs
  8. 15
      src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs
  9. 24
      src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTrigger.cs
  10. 25
      src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchema.cs
  11. 115
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs
  12. 25
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleActionHandler.cs
  13. 22
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs
  14. 33
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs
  15. 18
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleResult.cs
  16. 159
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs
  17. 30
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs
  18. 49
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs
  19. 1
      src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
  20. 21
      src/Squidex.Domain.Apps.Events/Rules/RuleCreated.cs
  21. 17
      src/Squidex.Domain.Apps.Events/Rules/RuleDeleted.cs
  22. 17
      src/Squidex.Domain.Apps.Events/Rules/RuleDisabled.cs
  23. 17
      src/Squidex.Domain.Apps.Events/Rules/RuleEnabled.cs
  24. 17
      src/Squidex.Domain.Apps.Events/Rules/RuleEvent.cs
  25. 21
      src/Squidex.Domain.Apps.Events/Rules/RuleUpdated.cs
  26. 80
      src/Squidex.Domain.Apps.Events/Rules/Utils/RuleEventDispatcher.cs
  27. 17
      src/Squidex.Domain.Apps.Read/Rules/IRuleEntity.cs
  28. 28
      src/Squidex.Domain.Apps.Read/Rules/IRuleEventEntity.cs
  29. 37
      src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleEventRepository.cs
  30. 21
      src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleRepository.cs
  31. 148
      src/Squidex.Domain.Apps.Read/Rules/RuleDequeuer.cs
  32. 74
      src/Squidex.Domain.Apps.Read/Rules/RuleEnqueuer.cs
  33. 18
      src/Squidex.Domain.Apps.Read/Rules/RuleJobResult.cs

26
src/Squidex.Domain.Apps.Core.Model/Rules/Actions/WebhookAction.cs

@ -0,0 +1,26 @@
// ==========================================================================
// WebhookAction.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Rules.Actions
{
[TypeName(nameof(WebhookAction))]
public sealed class WebhookAction : RuleAction
{
public Uri Url { get; set; }
public string SharedSecret { get; set; }
public override T Accept<T>(IRuleActionVisitor<T> visitor)
{
return visitor.Visit(this);
}
}
}

17
src/Squidex.Domain.Apps.Core.Model/Rules/IRuleActionVisitor.cs

@ -0,0 +1,17 @@
// ==========================================================================
// IActionVisitor.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Domain.Apps.Core.Rules.Actions;
namespace Squidex.Domain.Apps.Core.Rules
{
public interface IRuleActionVisitor<T>
{
T Visit(WebhookAction action);
}
}

17
src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs

@ -0,0 +1,17 @@
// ==========================================================================
// IRuleTriggerVisitor.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Domain.Apps.Core.Rules.Triggers;
namespace Squidex.Domain.Apps.Core.Rules
{
public interface IRuleTriggerVisitor<T>
{
T Visit(ContentChangedTrigger trigger);
}
}

78
src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs

@ -0,0 +1,78 @@
// ==========================================================================
// Rule.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Rules
{
public sealed class Rule
{
private RuleTrigger trigger;
private RuleAction action;
private bool isEnabled = true;
public RuleTrigger Trigger
{
get { return trigger; }
}
public RuleAction Action
{
get { return action; }
}
public bool IsEnabled
{
get { return isEnabled; }
}
public Rule(RuleTrigger trigger, RuleAction action)
{
Guard.NotNull(trigger, nameof(trigger));
Guard.NotNull(action, nameof(action));
this.trigger = trigger;
this.action = action;
}
public void Enable()
{
this.isEnabled = true;
}
public void Disable()
{
this.isEnabled = false;
}
public void Update(RuleTrigger newTrigger)
{
Guard.NotNull(newTrigger, nameof(newTrigger));
if (newTrigger.GetType() != trigger.GetType())
{
throw new ArgumentException("New trigger has another type.", nameof(newTrigger));
}
trigger = newTrigger;
}
public void Update(RuleAction newAction)
{
Guard.NotNull(newAction, nameof(newAction));
if (newAction.GetType() != trigger.GetType())
{
throw new ArgumentException("New action has another type.", nameof(newAction));
}
action = newAction;
}
}
}

15
src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs

@ -0,0 +1,15 @@
// ==========================================================================
// RuleAction.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Rules
{
public abstract class RuleAction
{
public abstract T Accept<T>(IRuleActionVisitor<T> visitor);
}
}

32
src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs

@ -0,0 +1,32 @@
// ==========================================================================
// RuleJob.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using NodaTime;
namespace Squidex.Domain.Apps.Core.Rules
{
public sealed class RuleJob
{
public Guid Id { get; set; }
public Guid AppId { get; set; }
public string EventName { get; set; }
public string ActionName { get; set; }
public string Description { get; set; }
public Instant Created { get; set; }
public Instant Expires { get; set; }
public RuleJobData ActionData { get; set; }
}
}

17
src/Squidex.Domain.Apps.Core.Model/Rules/RuleJobData.cs

@ -0,0 +1,17 @@
// ==========================================================================
// RuleJobData.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
namespace Squidex.Domain.Apps.Core.Rules
{
public sealed class RuleJobData : Dictionary<string, JToken>
{
}
}

15
src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs

@ -0,0 +1,15 @@
// ==========================================================================
// RuleTrigger.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Rules
{
public abstract class RuleTrigger
{
public abstract T Accept<T>(IRuleTriggerVisitor<T> visitor);
}
}

24
src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTrigger.cs

@ -0,0 +1,24 @@
// ==========================================================================
// ContentChangedTrigger.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Rules.Triggers
{
[TypeName(nameof(ContentChangedTrigger))]
public sealed class ContentChangedTrigger : RuleTrigger
{
public List<ContentChangedTriggerSchema> Schemas { get; set; }
public override T Accept<T>(IRuleTriggerVisitor<T> visitor)
{
return visitor.Visit(this);
}
}
}

25
src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchema.cs

@ -0,0 +1,25 @@
// ==========================================================================
// ContentChangedTriggerSchema.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
namespace Squidex.Domain.Apps.Core.Rules.Triggers
{
public sealed class ContentChangedTriggerSchema
{
public Guid SchemaId { get; set; }
public bool SendCreate { get; set; }
public bool SendUpdate { get; set; }
public bool SendDelete { get; set; }
public bool SendPublish { get; set; }
}
}

115
src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs

@ -0,0 +1,115 @@
// ==========================================================================
// WebhookActionHandler.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.Actions;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Http;
namespace Squidex.Domain.Apps.Core.HandleRules.ActionHandlers
{
public sealed class WebhookActionHandler : RuleActionHandler<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;
}
}
}

25
src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleActionHandler.cs

@ -0,0 +1,25 @@
// ==========================================================================
// IRuleActionHandler.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Domain.Apps.Core.HandleRules
{
public interface IRuleActionHandler
{
Type ActionType { get; }
(string Description, RuleJobData Data) CreateJob(Envelope<AppEvent> @event, string eventName, RuleAction action);
Task<(string Dump, Exception Exception)> ExecuteJobAsync(RuleJobData data);
}
}

22
src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs

@ -0,0 +1,22 @@
// ==========================================================================
// IRuleTriggerHandler.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Domain.Apps.Core.HandleRules
{
public interface IRuleTriggerHandler
{
Type TriggerType { get; }
bool Triggers(Envelope<AppEvent> @event, RuleTrigger trigger);
}
}

33
src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs

@ -0,0 +1,33 @@
// ==========================================================================
// RuleActionHandler.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Domain.Apps.Core.HandleRules
{
public abstract class RuleActionHandler<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);
}
}

18
src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleResult.cs

@ -0,0 +1,18 @@
// ==========================================================================
// RuleResult.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.HandleRules
{
public enum RuleResult
{
Pending,
Success,
Failed,
Timeout
}
}

159
src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs

@ -0,0 +1,159 @@
// ==========================================================================
// RuleService.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using NodaTime;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Domain.Apps.Core.HandleRules
{
public sealed class RuleService
{
private const string ContentPrefix = "Content";
private static readonly Duration ExpirationTime = Duration.FromDays(2);
private readonly Dictionary<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 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
{
Id = Guid.NewGuid(),
ActionName = actionName,
ActionData = actionData.Data,
AppId = appEvent.AppId.Id,
Created = now,
EventName = eventName,
Expires = now.Plus(ExpirationTime),
Description = actionData.Description
};
return job;
}
public async Task<(string Dump, RuleResult Result, TimeSpan Elapsed)> InvokeAsync(string actionName, Dictionary<string, JToken> job)
{
try
{
var actionType = typeNameRegistry.GetType(actionName);
var actionWatch = Stopwatch.StartNew();
var result = await ruleActionHandlers[actionType].ExecuteJobAsync(job);
actionWatch.Stop();
var dumpBuilder = new StringBuilder(result.Dump);
dumpBuilder.AppendLine();
dumpBuilder.AppendFormat("Elapesed {0}.", actionWatch.Elapsed);
dumpBuilder.AppendLine();
if (result.Exception is TimeoutException || result.Exception is OperationCanceledException)
{
dumpBuilder.AppendLine();
dumpBuilder.AppendLine("Action timed out.");
return (dumpBuilder.ToString(), RuleResult.Timeout, actionWatch.Elapsed);
}
else if (result.Exception != null)
{
return (dumpBuilder.ToString(), RuleResult.Failed, actionWatch.Elapsed);
}
else
{
return (dumpBuilder.ToString(), RuleResult.Success, actionWatch.Elapsed);
}
}
catch (Exception ex)
{
return (ex.ToString(), RuleResult.Failed, TimeSpan.Zero);
}
}
private string CreateEventName(AppEvent appEvent)
{
var eventName = typeNameRegistry.GetName(appEvent.GetType());
if (appEvent is SchemaEvent schemaEvent)
{
if (eventName.StartsWith(ContentPrefix, StringComparison.Ordinal))
{
eventName = eventName.Substring(ContentPrefix.Length);
}
return $"{schemaEvent.SchemaId.Name.ToPascalCase()}{eventName}";
}
return eventName;
}
}
}

30
src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs

@ -0,0 +1,30 @@
// ==========================================================================
// RuleTriggerHandler.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Domain.Apps.Core.HandleRules
{
public abstract class RuleTriggerHandler<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);
}
}

49
src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs

@ -0,0 +1,49 @@
// ==========================================================================
// ContentChangedTriggerHandler.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Domain.Apps.Core.HandleRules.Triggers
{
public sealed class ContentChangedTriggerHandler : RuleTriggerHandler<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 webhookSchema, SchemaEvent @event)
{
return @event.SchemaId.Id == webhookSchema.SchemaId;
}
private static bool MatchsType(ContentChangedTriggerSchema webhookSchema, SchemaEvent @event)
{
return
(webhookSchema.SendCreate && @event is ContentCreated) ||
(webhookSchema.SendUpdate && @event is ContentUpdated) ||
(webhookSchema.SendDelete && @event is ContentDeleted) ||
(webhookSchema.SendPublish && @event is ContentStatusChanged statusChanged && statusChanged.Status == Status.Published);
}
}
}

1
src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj

@ -9,6 +9,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Squidex.Domain.Apps.Core.Model\Squidex.Domain.Apps.Core.Model.csproj" />
<ProjectReference Include="..\Squidex.Domain.Apps.Events\Squidex.Domain.Apps.Events.csproj" />
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>

21
src/Squidex.Domain.Apps.Events/Rules/RuleCreated.cs

@ -0,0 +1,21 @@
// ==========================================================================
// RuleCreated.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Domain.Apps.Events.Rules
{
[EventType(nameof(RuleCreated))]
public sealed class RuleCreated : RuleEvent
{
public RuleTrigger Trigger { get; set; }
public RuleAction Action { get; set; }
}
}

17
src/Squidex.Domain.Apps.Events/Rules/RuleDeleted.cs

@ -0,0 +1,17 @@
// ==========================================================================
// RuleDeleted.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Domain.Apps.Events.Rules
{
[EventType(nameof(RuleDeleted))]
public sealed class RuleDeleted : RuleEvent
{
}
}

17
src/Squidex.Domain.Apps.Events/Rules/RuleDisabled.cs

@ -0,0 +1,17 @@
// ==========================================================================
// RuleDisabled.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Domain.Apps.Events.Rules
{
[EventType(nameof(RuleDisabled))]
public sealed class RuleDisabled : RuleEvent
{
}
}

17
src/Squidex.Domain.Apps.Events/Rules/RuleEnabled.cs

@ -0,0 +1,17 @@
// ==========================================================================
// RuleEnabled.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Domain.Apps.Events.Rules
{
[EventType(nameof(RuleEnabled))]
public sealed class RuleEnabled : RuleEvent
{
}
}

17
src/Squidex.Domain.Apps.Events/Rules/RuleEvent.cs

@ -0,0 +1,17 @@
// ==========================================================================
// RuleEvent.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
namespace Squidex.Domain.Apps.Events.Rules
{
public abstract class RuleEvent : AppEvent
{
public Guid RuleId { get; set; }
}
}

21
src/Squidex.Domain.Apps.Events/Rules/RuleUpdated.cs

@ -0,0 +1,21 @@
// ==========================================================================
// RuleUpdated.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Domain.Apps.Events.Rules
{
[EventType(nameof(RuleUpdated))]
public sealed class RuleUpdated : RuleEvent
{
public RuleTrigger Trigger { get; set; }
public RuleAction Action { get; set; }
}
}

80
src/Squidex.Domain.Apps.Events/Rules/Utils/RuleEventDispatcher.cs

@ -0,0 +1,80 @@
// ==========================================================================
// RuleEventDispatcher.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Linq;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.Actions;
using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Events.Webhooks;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Events.Rules.Utils
{
public static class RuleEventDispatcher
{
public static Rule Create(RuleCreated @event)
{
return new Rule(@event.Trigger, @event.Action);
}
public static Rule Create(WebhookCreated @event)
{
return new Rule(CreateTrigger(@event), CreateAction(@event));
}
public static void Apply(this Rule rule, WebhookUpdated @event)
{
rule.Update(CreateTrigger(@event));
if (rule.Action is WebhookAction webhookAction)
{
webhookAction.Url = @event.Url;
}
}
public static void Apply(this Rule rule, RuleUpdated @event)
{
if (@event.Trigger != null)
{
rule.Update(@event.Trigger);
}
if (@event.Action != null)
{
rule.Update(@event.Action);
}
}
public static void Apply(this Rule rule, RuleEnabled @event)
{
rule.Enable();
}
public static void Apply(this Rule rule, RuleDisabled @event)
{
rule.Disable();
}
private static WebhookAction CreateAction(WebhookCreated @event)
{
var action = new WebhookAction { Url = @event.Url, SharedSecret = @event.SharedSecret };
return action;
}
private static ContentChangedTrigger CreateTrigger(WebhookEditEvent @event)
{
var trigger = new ContentChangedTrigger
{
Schemas = @event.Schemas.Select(x => SimpleMapper.Map(x, new ContentChangedTriggerSchema())).ToList()
};
return trigger;
}
}
}

17
src/Squidex.Domain.Apps.Read/Rules/IRuleEntity.cs

@ -0,0 +1,17 @@
// ==========================================================================
// IRuleEntity.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Domain.Apps.Core.Rules;
namespace Squidex.Domain.Apps.Read.Rules
{
public interface IRuleEntity : IAppRefEntity, IEntityWithCreatedBy, IEntityWithLastModifiedBy, IEntityWithVersion
{
Rule Rule { get; }
}
}

28
src/Squidex.Domain.Apps.Read/Rules/IRuleEventEntity.cs

@ -0,0 +1,28 @@
// ==========================================================================
// IRuleEventEntity.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using NodaTime;
using Squidex.Domain.Apps.Core.HandleRules;
namespace Squidex.Domain.Apps.Read.Rules
{
public interface IRuleEventEntity : IEntity
{
RuleJob Job { get; }
Instant? NextAttempt { get; }
RuleJobResult JobResult { get; }
RuleResult Result { get; }
int NumCalls { get; }
string LastDump { get; }
}
}

37
src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleEventRepository.cs

@ -0,0 +1,37 @@
// ==========================================================================
// IRuleEventRepository.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using NodaTime;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules;
namespace Squidex.Domain.Apps.Read.Rules.Repositories
{
public interface IRuleEventRepository
{
Task EnqueueAsync(RuleJob job, Instant nextAttempt);
Task EnqueueAsync(Guid id, Instant nextAttempt);
Task MarkSendingAsync(Guid jobId);
Task TraceSentAsync(Guid jobId, string dump, RuleResult result, TimeSpan elapsed, Instant? nextCall);
Task QueryPendingAsync(Func<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);
}
}

21
src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleRepository.cs

@ -0,0 +1,21 @@
// ==========================================================================
// IRuleRepository.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Squidex.Domain.Apps.Read.Rules.Repositories
{
public interface IRuleRepository
{
Task<IReadOnlyList<IRuleEntity>> QueryByAppAsync(Guid appId);
Task<IReadOnlyList<IRuleEntity>> QueryCachedByAppAsync(Guid appId);
}
}

148
src/Squidex.Domain.Apps.Read/Rules/RuleDequeuer.cs

@ -0,0 +1,148 @@
// ==========================================================================
// RuleDequeuer.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using NodaTime;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Read.Rules.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Timers;
namespace Squidex.Domain.Apps.Read.Rules
{
public sealed class RuleDequeuer : DisposableObjectBase, IExternalSystem
{
private readonly ActionBlock<IRuleEventEntity> requestBlock;
private readonly TransformBlock<IRuleEventEntity, IRuleEventEntity> blockBlock;
private readonly IRuleEventRepository ruleEventRepository;
private readonly RuleService ruleService;
private readonly CompletionTimer timer;
private readonly ISemanticLog log;
public RuleDequeuer(RuleService ruleService, IRuleEventRepository ruleEventRepository, ISemanticLog log)
{
Guard.NotNull(ruleEventRepository, nameof(ruleEventRepository));
Guard.NotNull(ruleService, nameof(ruleService));
Guard.NotNull(log, nameof(log));
this.ruleEventRepository = ruleEventRepository;
this.ruleService = ruleService;
this.log = log;
requestBlock =
new ActionBlock<IRuleEventEntity>(MakeRequestAsync,
new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 32, BoundedCapacity = 32 });
blockBlock =
new TransformBlock<IRuleEventEntity, IRuleEventEntity>(x => BlockAsync(x),
new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 1, BoundedCapacity = 1 });
blockBlock.LinkTo(requestBlock, new DataflowLinkOptions { PropagateCompletion = true });
timer = new CompletionTimer(5000, QueryAsync);
}
protected override void DisposeObject(bool disposing)
{
if (disposing)
{
timer.StopAsync().Wait();
blockBlock.Complete();
requestBlock.Completion.Wait();
}
}
public void Connect()
{
}
public void Next()
{
timer.SkipCurrentDelay();
}
private async Task QueryAsync(CancellationToken cancellationToken)
{
try
{
await ruleEventRepository.QueryPendingAsync(blockBlock.SendAsync, cancellationToken);
}
catch (Exception ex)
{
log.LogError(ex, w => w
.WriteProperty("action", "QueueWebhookEvents")
.WriteProperty("status", "Failed"));
}
}
private async Task<IRuleEventEntity> BlockAsync(IRuleEventEntity @event)
{
try
{
await ruleEventRepository.MarkSendingAsync(@event.Id);
return @event;
}
catch (Exception ex)
{
log.LogError(ex, w => w
.WriteProperty("action", "BlockWebhookEvent")
.WriteProperty("status", "Failed"));
throw;
}
}
private async Task MakeRequestAsync(IRuleEventEntity @event)
{
try
{
var job = @event.Job;
var response = await ruleService.InvokeAsync(job.ActionName, job.Details);
Instant? nextCall = null;
if (response.Result != RuleResult.Success)
{
switch (@event.NumCalls)
{
case 0:
nextCall = job.Created.Plus(Duration.FromMinutes(5));
break;
case 1:
nextCall = job.Created.Plus(Duration.FromHours(1));
break;
case 2:
nextCall = job.Created.Plus(Duration.FromHours(6));
break;
case 3:
nextCall = job.Created.Plus(Duration.FromHours(12));
break;
}
}
await ruleEventRepository.TraceSentAsync(@event.Id, response.Dump, response.Result, response.Elapsed, nextCall);
}
catch (Exception ex)
{
log.LogError(ex, w => w
.WriteProperty("action", "SendWebhookEvent")
.WriteProperty("status", "Failed"));
throw;
}
}
}
}

74
src/Squidex.Domain.Apps.Read/Rules/RuleEnqueuer.cs

@ -0,0 +1,74 @@
// ==========================================================================
// RuleEnqueuer.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Read.Rules.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Read.Rules
{
public sealed class RuleEnqueuer : IEventConsumer
{
private readonly IRuleEventRepository ruleEventRepository;
private readonly IRuleRepository ruleRepository;
private readonly RuleService ruleService;
public string Name
{
get { return GetType().Name; }
}
public string EventsFilter
{
get { return "^content-"; }
}
public RuleEnqueuer(
IRuleEventRepository ruleEventRepository,
IRuleRepository ruleRepository,
RuleService ruleService)
{
Guard.NotNull(ruleEventRepository, nameof(ruleEventRepository));
Guard.NotNull(ruleRepository, nameof(ruleRepository));
Guard.NotNull(ruleService, nameof(ruleService));
this.ruleEventRepository = ruleEventRepository;
this.ruleRepository = ruleRepository;
this.ruleService = ruleService;
}
public Task ClearAsync()
{
return TaskHelper.Done;
}
public async Task On(Envelope<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)
{
continue;
}
await ruleEventRepository.EnqueueAsync(job, job.Created);
}
}
}
}
}

18
src/Squidex.Domain.Apps.Read/Rules/RuleJobResult.cs

@ -0,0 +1,18 @@
// ==========================================================================
// RuleJobResult.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Read.Rules
{
public enum RuleJobResult
{
Pending,
Success,
Retry,
Failed
}
}
Loading…
Cancel
Save