Browse Source

Merge pull request #157 from Squidex/feature-rules

Feature rules
pull/164/head
Sebastian Stehle 8 years ago
committed by GitHub
parent
commit
4d868bcb13
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs
  2. 15
      src/Squidex.Domain.Apps.Core.Model/Rules/Actions/WebhookAction.cs
  3. 17
      src/Squidex.Domain.Apps.Core.Model/Rules/IRuleActionVisitor.cs
  4. 17
      src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs
  5. 46
      src/Squidex.Domain.Apps.Core.Model/Rules/Json/JsonRule.cs
  6. 26
      src/Squidex.Domain.Apps.Core.Model/Rules/Json/RuleConverter.cs
  7. 78
      src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs
  8. 15
      src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs
  9. 20
      src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs
  10. 17
      src/Squidex.Domain.Apps.Core.Model/Rules/RuleJobData.cs
  11. 15
      src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs
  12. 24
      src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTrigger.cs
  13. 6
      src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchema.cs
  14. 1
      src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs
  15. 6
      src/Squidex.Domain.Apps.Core.Model/SquidexCoreModel.cs
  16. 115
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs
  17. 25
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleActionHandler.cs
  18. 22
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs
  19. 33
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs
  20. 6
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleResult.cs
  21. 158
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs
  22. 30
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs
  23. 49
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs
  24. 1
      src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
  25. 13
      src/Squidex.Domain.Apps.Events/Rules/RuleCreated.cs
  26. 8
      src/Squidex.Domain.Apps.Events/Rules/RuleDeleted.cs
  27. 8
      src/Squidex.Domain.Apps.Events/Rules/RuleDisabled.cs
  28. 17
      src/Squidex.Domain.Apps.Events/Rules/RuleEnabled.cs
  29. 8
      src/Squidex.Domain.Apps.Events/Rules/RuleEvent.cs
  30. 21
      src/Squidex.Domain.Apps.Events/Rules/RuleUpdated.cs
  31. 43
      src/Squidex.Domain.Apps.Events/Rules/Utils/RuleEventDispatcher.cs
  32. 6
      src/Squidex.Domain.Apps.Events/SquidexEvents.cs
  33. 1
      src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs
  34. 1
      src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository_EventHandling.cs
  35. 1
      src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs
  36. 2
      src/Squidex.Domain.Apps.Read.MongoDb/EntityMapper.cs
  37. 1
      src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventRepository.cs
  38. 2
      src/Squidex.Domain.Apps.Read.MongoDb/MongoCollectionExtensions.cs
  39. 41
      src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEntity.cs
  40. 47
      src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventEntity.cs
  41. 69
      src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventRepository.cs
  42. 90
      src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository.cs
  43. 97
      src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository_EventHandling.cs
  44. 1
      src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs
  45. 74
      src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookEntity.cs
  46. 119
      src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookRepository.cs
  47. 99
      src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookRepository_EventHandling.cs
  48. 1
      src/Squidex.Domain.Apps.Read/Apps/Services/Implementations/CachingAppProvider.cs
  49. 2
      src/Squidex.Domain.Apps.Read/CachingProviderBase.cs
  50. 1
      src/Squidex.Domain.Apps.Read/Contents/Edm/EdmModelBuilder.cs
  51. 1
      src/Squidex.Domain.Apps.Read/Contents/GraphQL/CachingGraphQLService.cs
  52. 17
      src/Squidex.Domain.Apps.Read/Rules/IRuleEntity.cs
  53. 14
      src/Squidex.Domain.Apps.Read/Rules/IRuleEventEntity.cs
  54. 37
      src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleEventRepository.cs
  55. 12
      src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleRepository.cs
  56. 85
      src/Squidex.Domain.Apps.Read/Rules/RuleDequeuer.cs
  57. 72
      src/Squidex.Domain.Apps.Read/Rules/RuleEnqueuer.cs
  58. 6
      src/Squidex.Domain.Apps.Read/Rules/RuleJobResult.cs
  59. 1
      src/Squidex.Domain.Apps.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs
  60. 31
      src/Squidex.Domain.Apps.Read/Webhooks/IWebhookEntity.cs
  61. 35
      src/Squidex.Domain.Apps.Read/Webhooks/Repositories/IWebhookEventRepository.cs
  62. 23
      src/Squidex.Domain.Apps.Read/Webhooks/Repositories/IWebhookRepository.cs
  63. 140
      src/Squidex.Domain.Apps.Read/Webhooks/WebhookEnqueuer.cs
  64. 98
      src/Squidex.Domain.Apps.Read/Webhooks/WebhookSender.cs
  65. 20
      src/Squidex.Domain.Apps.Write/Rules/Commands/CreateRule.cs
  66. 14
      src/Squidex.Domain.Apps.Write/Rules/Commands/DeleteRule.cs
  67. 14
      src/Squidex.Domain.Apps.Write/Rules/Commands/DisableRule.cs
  68. 14
      src/Squidex.Domain.Apps.Write/Rules/Commands/EnableRule.cs
  69. 10
      src/Squidex.Domain.Apps.Write/Rules/Commands/RuleAggregateCommand.cs
  70. 19
      src/Squidex.Domain.Apps.Write/Rules/Commands/RuleEditCommand.cs
  71. 14
      src/Squidex.Domain.Apps.Write/Rules/Commands/UpdateRule.cs
  72. 107
      src/Squidex.Domain.Apps.Write/Rules/Guards/GuardRule.cs
  73. 40
      src/Squidex.Domain.Apps.Write/Rules/Guards/RuleActionValidator.cs
  74. 54
      src/Squidex.Domain.Apps.Write/Rules/Guards/RuleTriggerValidator.cs
  75. 92
      src/Squidex.Domain.Apps.Write/Rules/RuleCommandMiddleware.cs
  76. 118
      src/Squidex.Domain.Apps.Write/Rules/RuleDomainObject.cs
  77. 21
      src/Squidex.Domain.Apps.Write/Webhooks/Commands/WebhookEditCommand.cs
  78. 61
      src/Squidex.Domain.Apps.Write/Webhooks/Guards/GuardWebhook.cs
  79. 72
      src/Squidex.Domain.Apps.Write/Webhooks/WebhookCommandMiddleware.cs
  80. 82
      src/Squidex.Domain.Apps.Write/Webhooks/WebhookDomainObject.cs
  81. 4
      src/Squidex.Infrastructure/Dispatching/ActionContextDispatcher.cs
  82. 4
      src/Squidex.Infrastructure/Dispatching/ActionDispatcher.cs
  83. 4
      src/Squidex.Infrastructure/Dispatching/FuncContextDispatcher.cs
  84. 4
      src/Squidex.Infrastructure/Dispatching/FuncDispatcher.cs
  85. 14
      src/Squidex.Infrastructure/SquidexInfrastructure.cs
  86. 36
      src/Squidex.Infrastructure/TypeNameRegistry.cs
  87. 21
      src/Squidex/Config/Domain/ReadModule.cs
  88. 10
      src/Squidex/Config/Domain/Serializers.cs
  89. 12
      src/Squidex/Config/Domain/StoreMongoDbModule.cs
  90. 6
      src/Squidex/Config/Domain/WriteModule.cs
  91. 25
      src/Squidex/Controllers/Api/Rules/Models/Actions/WebhookActionDto.cs
  92. 34
      src/Squidex/Controllers/Api/Rules/Models/Converters/RuleActionDtoFactory.cs
  93. 72
      src/Squidex/Controllers/Api/Rules/Models/Converters/RuleConverter.cs
  94. 38
      src/Squidex/Controllers/Api/Rules/Models/Converters/RuleTriggerDtoFactory.cs
  95. 16
      src/Squidex/Controllers/Api/Rules/Models/CreateRuleDto.cs
  96. 22
      src/Squidex/Controllers/Api/Rules/Models/RuleActionDto.cs
  97. 67
      src/Squidex/Controllers/Api/Rules/Models/RuleDto.cs
  98. 17
      src/Squidex/Controllers/Api/Rules/Models/RuleEventDto.cs
  99. 12
      src/Squidex/Controllers/Api/Rules/Models/RuleEventsDto.cs
  100. 22
      src/Squidex/Controllers/Api/Rules/Models/RuleTriggerDto.cs

2
src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs

@ -99,7 +99,7 @@ namespace Squidex.Domain.Apps.Core.Apps
var newMaster =
state.Master.Language != language ?
state.Master :
state.Languages.Values.FirstOrDefault();
newLanguages.Values.FirstOrDefault();
state = new State(newLanguages, newMaster);
}

15
src/Squidex.Domain.Apps.Write/Webhooks/Commands/CreateWebhook.cs → src/Squidex.Domain.Apps.Core.Model/Rules/Actions/WebhookAction.cs

@ -1,5 +1,5 @@
// ==========================================================================
// CreateWebhook.cs
// WebhookAction.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
@ -9,15 +9,18 @@
using System;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Write.Webhooks.Commands
namespace Squidex.Domain.Apps.Core.Rules.Actions
{
public sealed class CreateWebhook : WebhookEditCommand
[TypeName(nameof(WebhookAction))]
public sealed class WebhookAction : RuleAction
{
public string SharedSecret { get; } = RandomHash.New();
public Uri Url { get; set; }
public CreateWebhook()
public string SharedSecret { get; set; }
public override T Accept<T>(IRuleActionVisitor<T> visitor)
{
WebhookId = Guid.NewGuid();
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);
}
}

46
src/Squidex.Domain.Apps.Core.Model/Rules/Json/JsonRule.cs

@ -0,0 +1,46 @@
// ==========================================================================
// JsonRule.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Newtonsoft.Json;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Core.Rules.Json
{
public sealed class JsonRule
{
[JsonProperty]
public RuleTrigger Trigger { get; set; }
[JsonProperty]
public RuleAction Action { get; set; }
[JsonProperty]
public bool IsEnabled { get; set; }
public JsonRule()
{
}
public JsonRule(Rule rule)
{
SimpleMapper.Map(rule, this);
}
public Rule ToRule()
{
var rule = new Rule(Trigger, Action);
if (!IsEnabled)
{
rule.Disable();
}
return rule;
}
}
}

26
src/Squidex.Domain.Apps.Core.Model/Rules/Json/RuleConverter.cs

@ -0,0 +1,26 @@
// ==========================================================================
// RuleConverter.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Newtonsoft.Json;
using Squidex.Infrastructure.Json;
namespace Squidex.Domain.Apps.Core.Rules.Json
{
public sealed class RuleConverter : JsonClassConverter<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));
}
}
}

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() != action.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);
}
}

20
src/Squidex.Domain.Apps.Read/Webhooks/WebhookJob.cs → src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs

@ -1,5 +1,5 @@
// ==========================================================================
// WebhookJob.cs
// RuleJob.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
@ -9,24 +9,24 @@
using System;
using NodaTime;
namespace Squidex.Domain.Apps.Read.Webhooks
namespace Squidex.Domain.Apps.Core.Rules
{
public sealed class WebhookJob
public sealed class RuleJob
{
public Guid Id { get; set; }
public Guid RuleId { get; set; }
public Guid AppId { get; set; }
public Guid WebhookId { get; set; }
public Uri RequestUrl { get; set; }
public string EventName { get; set; }
public string RequestBody { get; set; }
public string ActionName { get; set; }
public string RequestSignature { get; set; }
public string Description { get; set; }
public string EventName { get; set; }
public Instant Created { get; set; }
public Instant Expires { get; set; }
public RuleJobData ActionData { get; set; }
}
}

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);
}
}
}

6
src/Squidex.Domain.Apps.Core.Model/Webhooks/WebhookSchema.cs → src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchema.cs

@ -1,5 +1,5 @@
// ==========================================================================
// WebhookSchema.cs
// ContentChangedTriggerSchema.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
@ -8,9 +8,9 @@
using System;
namespace Squidex.Domain.Apps.Core.Webhooks
namespace Squidex.Domain.Apps.Core.Rules.Triggers
{
public sealed class WebhookSchema
public sealed class ContentChangedTriggerSchema
{
public Guid SchemaId { get; set; }

1
src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs

@ -84,7 +84,6 @@ namespace Squidex.Domain.Apps.Core.Schemas
new TagsField(id, name, partitioning, (TagsFieldProperties)properties));
typeNameRegistry.MapObsolete(typeof(ReferencesFieldProperties), "DateTime");
typeNameRegistry.MapObsolete(typeof(DateTimeFieldProperties), "References");
}

6
src/Squidex.Domain.Apps.Write/Webhooks/Commands/UpdateWebhook.cs → src/Squidex.Domain.Apps.Core.Model/SquidexCoreModel.cs

@ -1,14 +1,14 @@
// ==========================================================================
// UpdateWebhook.cs
// SquidexCoreModel.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Write.Webhooks.Commands
namespace Squidex.Domain.Apps.Core
{
public sealed class UpdateWebhook : WebhookEditCommand
public static class SquidexCoreModel
{
}
}

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);
}
}

6
src/Squidex.Domain.Apps.Read/Webhooks/WebhookResult.cs → src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleResult.cs

@ -1,14 +1,14 @@
// ==========================================================================
// WebhookResult.cs
// RuleResult.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Read.Webhooks
namespace Squidex.Domain.Apps.Core.HandleRules
{
public enum WebhookResult
public enum RuleResult
{
Pending,
Success,

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

@ -0,0 +1,158 @@
// ==========================================================================
// RuleService.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NodaTime;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Domain.Apps.Core.HandleRules
{
public class RuleService
{
private const string ContentPrefix = "Content";
private static readonly Duration ExpirationTime = Duration.FromDays(2);
private readonly Dictionary<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;
}
}
}

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 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
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>

13
src/Squidex.Domain.Apps.Events/Webhooks/WebhookCreated.cs → src/Squidex.Domain.Apps.Events/Rules/RuleCreated.cs

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

8
src/Squidex.Domain.Apps.Events/Webhooks/WebhookDeleted.cs → src/Squidex.Domain.Apps.Events/Rules/RuleDeleted.cs

@ -1,5 +1,5 @@
// ==========================================================================
// WebhookDeleted.cs
// RuleDeleted.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
@ -8,10 +8,10 @@
using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Domain.Apps.Events.Webhooks
namespace Squidex.Domain.Apps.Events.Rules
{
[EventType(nameof(WebhookDeleted), 2)]
public sealed class WebhookDeleted : WebhookEvent
[EventType(nameof(RuleDeleted))]
public sealed class RuleDeleted : RuleEvent
{
}
}

8
src/Squidex.Domain.Apps.Events/Webhooks/WebhookUpdated.cs → src/Squidex.Domain.Apps.Events/Rules/RuleDisabled.cs

@ -1,5 +1,5 @@
// ==========================================================================
// WebhookUpdated.cs
// RuleDisabled.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
@ -8,10 +8,10 @@
using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Domain.Apps.Events.Webhooks
namespace Squidex.Domain.Apps.Events.Rules
{
[EventType(nameof(WebhookUpdated))]
public sealed class WebhookUpdated : WebhookEditEvent
[EventType(nameof(RuleDisabled))]
public sealed class RuleDisabled : RuleEvent
{
}
}

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
{
}
}

8
src/Squidex.Domain.Apps.Events/Webhooks/WebhookEvent.cs → src/Squidex.Domain.Apps.Events/Rules/RuleEvent.cs

@ -1,5 +1,5 @@
// ==========================================================================
// WebhookEvent.cs
// RuleEvent.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
@ -8,10 +8,10 @@
using System;
namespace Squidex.Domain.Apps.Events.Webhooks
namespace Squidex.Domain.Apps.Events.Rules
{
public abstract class WebhookEvent : AppEvent
public abstract class RuleEvent : AppEvent
{
public Guid WebhookId { get; set; }
public Guid RuleId { get; set; }
}
}

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; }
}
}

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

@ -0,0 +1,43 @@
// ==========================================================================
// RuleEventDispatcher.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Domain.Apps.Core.Rules;
namespace Squidex.Domain.Apps.Events.Rules.Utils
{
public static class RuleEventDispatcher
{
public static Rule Create(RuleCreated @event)
{
return new Rule(@event.Trigger, @event.Action);
}
public static void Apply(this Rule rule, RuleUpdated @event)
{
if (@event.Trigger != null)
{
rule.Update(@event.Trigger);
}
if (@event.Action != null)
{
rule.Update(@event.Action);
}
}
public static void Apply(this Rule rule, RuleEnabled @event)
{
rule.Enable();
}
public static void Apply(this Rule rule, RuleDisabled @event)
{
rule.Disable();
}
}
}

6
src/Squidex.Domain.Apps.Write/Webhooks/Commands/DeleteWebhook.cs → src/Squidex.Domain.Apps.Events/SquidexEvents.cs

@ -1,14 +1,14 @@
// ==========================================================================
// DeleteWebhook.cs
// SquidexEvents.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Write.Webhooks.Commands
namespace Squidex.Domain.Apps.Events
{
public sealed class DeleteWebhook : WebhookAggregateCommand
public static class SquidexEvents
{
}
}

1
src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs

@ -13,7 +13,6 @@ using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Apps;
using Squidex.Domain.Apps.Events.Apps.Utils;
using Squidex.Domain.Apps.Read.MongoDb.Utils;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Dispatching;

1
src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository_EventHandling.cs

@ -9,7 +9,6 @@
using System.Threading.Tasks;
using MongoDB.Driver;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Domain.Apps.Read.MongoDb.Utils;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.Reflection;

1
src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs

@ -13,7 +13,6 @@ using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Events.Apps;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Domain.Apps.Read.MongoDb.Utils;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.Reflection;

2
src/Squidex.Domain.Apps.Read.MongoDb/Utils/EntityMapper.cs → src/Squidex.Domain.Apps.Read.MongoDb/EntityMapper.cs

@ -10,7 +10,7 @@ using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.MongoDb;
namespace Squidex.Domain.Apps.Read.MongoDb.Utils
namespace Squidex.Domain.Apps.Read.MongoDb
{
public static class EntityMapper
{

1
src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventRepository.cs

@ -14,7 +14,6 @@ using MongoDB.Driver;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Read.History;
using Squidex.Domain.Apps.Read.History.Repositories;
using Squidex.Domain.Apps.Read.MongoDb.Utils;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.MongoDb;

2
src/Squidex.Domain.Apps.Read.MongoDb/Utils/MongoCollectionExtensions.cs → src/Squidex.Domain.Apps.Read.MongoDb/MongoCollectionExtensions.cs

@ -14,7 +14,7 @@ using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.MongoDb;
namespace Squidex.Domain.Apps.Read.MongoDb.Utils
namespace Squidex.Domain.Apps.Read.MongoDb
{
public static class MongoCollectionExtensions
{

41
src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEntity.cs

@ -0,0 +1,41 @@
// ==========================================================================
// MongoRuleEntity.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using MongoDB.Bson.Serialization.Attributes;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Read.Rules;
using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb;
namespace Squidex.Domain.Apps.Read.MongoDb.Rules
{
public class MongoRuleEntity : MongoEntity, IRuleEntity
{
[BsonRequired]
[BsonElement]
public Guid AppId { get; set; }
[BsonRequired]
[BsonElement]
public RefToken CreatedBy { get; set; }
[BsonRequired]
[BsonElement]
public RefToken LastModifiedBy { get; set; }
[BsonRequired]
[BsonElement]
public long Version { get; set; }
[BsonRequired]
[BsonElement]
[BsonJson]
public Rule Rule { get; set; }
}
}

47
src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookEventEntity.cs → src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventEntity.cs

@ -1,5 +1,5 @@
// ==========================================================================
// MongoWebhookEventEntity.cs
// MongoRuleEventEntity.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
@ -9,43 +9,26 @@
using System;
using MongoDB.Bson.Serialization.Attributes;
using NodaTime;
using Squidex.Domain.Apps.Read.Webhooks;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Read.Rules;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Read.MongoDb.Webhooks
namespace Squidex.Domain.Apps.Read.MongoDb.Rules
{
public sealed class MongoWebhookEventEntity : MongoEntity, IWebhookEventEntity
public sealed class MongoRuleEventEntity : MongoEntity, IRuleEventEntity
{
private WebhookJob job;
[BsonRequired]
[BsonElement]
public Guid AppId { get; set; }
[BsonRequired]
[BsonElement]
public long Version { get; set; }
[BsonRequired]
[BsonElement]
public Uri RequestUrl { get; set; }
[BsonRequired]
[BsonElement]
public string RequestBody { get; set; }
[BsonRequired]
[BsonElement]
public string RequestSignature { get; set; }
[BsonRequired]
[BsonElement]
public string EventName { get; set; }
public string LastDump { get; set; }
[BsonRequired]
[BsonElement]
public string LastDump { get; set; }
public int NumCalls { get; set; }
[BsonRequired]
[BsonElement]
@ -57,23 +40,19 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Webhooks
[BsonRequired]
[BsonElement]
public int NumCalls { get; set; }
public bool IsSending { get; set; }
[BsonRequired]
[BsonElement]
public bool IsSending { get; set; }
public RuleResult Result { get; set; }
[BsonRequired]
[BsonElement]
public WebhookResult Result { get; set; }
public RuleJobResult JobResult { get; set; }
[BsonRequired]
[BsonElement]
public WebhookJobResult JobResult { get; set; }
public WebhookJob Job
{
get { return job ?? (job = SimpleMapper.Map(this, new WebhookJob())); }
}
[BsonJson]
public RuleJob Job { get; set; }
}
}

69
src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookEventRepository.cs → src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventRepository.cs

@ -1,5 +1,5 @@
// ==========================================================================
// MongoWebhookEventRepository.cs
// MongoRuleEventRepository.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
@ -12,32 +12,28 @@ using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver;
using NodaTime;
using Squidex.Domain.Apps.Read.Webhooks;
using Squidex.Domain.Apps.Read.Webhooks.Repositories;
using Squidex.Infrastructure;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Read.Rules;
using Squidex.Domain.Apps.Read.Rules.Repositories;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Read.MongoDb.Webhooks
namespace Squidex.Domain.Apps.Read.MongoDb.Rules
{
public sealed class MongoWebhookEventRepository : MongoRepositoryBase<MongoWebhookEventEntity>, IWebhookEventRepository
public sealed class MongoRuleEventRepository : MongoRepositoryBase<MongoRuleEventEntity>, IRuleEventRepository
{
private readonly IClock clock;
public MongoWebhookEventRepository(IMongoDatabase database, IClock clock)
public MongoRuleEventRepository(IMongoDatabase database)
: base(database)
{
Guard.NotNull(clock, nameof(clock));
this.clock = clock;
}
protected override string CollectionName()
{
return "WebhookEvents";
return "RuleEvents";
}
protected override Task SetupCollectionAsync(IMongoCollection<MongoWebhookEventEntity> collection)
protected override Task SetupCollectionAsync(IMongoCollection<MongoRuleEventEntity> collection)
{
return Task.WhenAll(
collection.Indexes.CreateOneAsync(Index.Ascending(x => x.NextAttempt).Descending(x => x.IsSending)),
@ -45,29 +41,27 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Webhooks
collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Expires), new CreateIndexOptions { ExpireAfter = TimeSpan.Zero }));
}
public Task QueryPendingAsync(Func<IWebhookEventEntity, Task> callback, CancellationToken cancellationToken = default(CancellationToken))
public Task QueryPendingAsync(Instant now, Func<IRuleEventEntity, Task> callback, CancellationToken cancellationToken = default(CancellationToken))
{
var now = clock.GetCurrentInstant();
return Collection.Find(x => x.NextAttempt < now && !x.IsSending).ForEachAsync(callback, cancellationToken);
}
public async Task<IReadOnlyList<IWebhookEventEntity>> QueryByAppAsync(Guid appId, int skip = 0, int take = 20)
public async Task<IReadOnlyList<IRuleEventEntity>> QueryByAppAsync(Guid appId, int skip = 0, int take = 20)
{
var webhookEventEntities =
var ruleEventEntities =
await Collection.Find(x => x.AppId == appId).Skip(skip).Limit(take).SortByDescending(x => x.Created)
.ToListAsync();
return webhookEventEntities;
return ruleEventEntities;
}
public async Task<IWebhookEventEntity> FindAsync(Guid id)
public async Task<IRuleEventEntity> FindAsync(Guid id)
{
var webhookEventEntity =
var ruleEvent =
await Collection.Find(x => x.Id == id)
.FirstOrDefaultAsync();
return webhookEventEntity;
return ruleEvent;
}
public async Task<int> CountByAppAsync(Guid appId)
@ -80,35 +74,20 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Webhooks
return Collection.UpdateOneAsync(x => x.Id == id, Update.Set(x => x.NextAttempt, nextAttempt));
}
public Task TraceSendingAsync(Guid jobId)
public Task EnqueueAsync(RuleJob job, Instant nextAttempt)
{
return Collection.UpdateOneAsync(x => x.Id == jobId, Update.Set(x => x.IsSending, true));
}
public Task EnqueueAsync(WebhookJob job, Instant nextAttempt)
{
var entity = SimpleMapper.Map(job, new MongoWebhookEventEntity { Created = clock.GetCurrentInstant(), NextAttempt = nextAttempt });
var entity = SimpleMapper.Map(job, new MongoRuleEventEntity { Job = job, Created = nextAttempt, NextAttempt = nextAttempt });
return Collection.InsertOneIfNotExistsAsync(entity);
}
public Task TraceSentAsync(Guid jobId, string dump, WebhookResult result, TimeSpan elapsed, Instant? nextAttempt)
public Task MarkSendingAsync(Guid jobId)
{
WebhookJobResult jobResult;
if (result != WebhookResult.Success && nextAttempt == null)
{
jobResult = WebhookJobResult.Failed;
}
else if (result != WebhookResult.Success && nextAttempt.HasValue)
{
jobResult = WebhookJobResult.Retry;
}
else
{
jobResult = WebhookJobResult.Success;
}
return Collection.UpdateOneAsync(x => x.Id == jobId, Update.Set(x => x.IsSending, true));
}
public Task MarkSentAsync(Guid jobId, string dump, RuleResult result, RuleJobResult jobResult, TimeSpan elapsed, Instant? nextAttempt)
{
return Collection.UpdateOneAsync(x => x.Id == jobId,
Update.Set(x => x.Result, result)
.Set(x => x.LastDump, dump)

90
src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository.cs

@ -0,0 +1,90 @@
// ==========================================================================
// MongoRuleRepository.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Driver;
using Squidex.Domain.Apps.Read.Rules;
using Squidex.Domain.Apps.Read.Rules.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.MongoDb;
namespace Squidex.Domain.Apps.Read.MongoDb.Rules
{
public partial class MongoRuleRepository : MongoRepositoryBase<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();
}
}
}
}
}

97
src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository_EventHandling.cs

@ -0,0 +1,97 @@
// ==========================================================================
// MongoRuleRepository_EventHandling.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Threading.Tasks;
using MongoDB.Driver;
using Squidex.Domain.Apps.Events.Rules;
using Squidex.Domain.Apps.Events.Rules.Utils;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Dispatching;
namespace Squidex.Domain.Apps.Read.MongoDb.Rules
{
public partial class MongoRuleRepository
{
public string Name
{
get { return GetType().Name; }
}
public string EventsFilter
{
get { return "^rule-"; }
}
public Task On(Envelope<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
src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs

@ -12,7 +12,6 @@ using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Schemas;
using Squidex.Domain.Apps.Events.Schemas.Old;
using Squidex.Domain.Apps.Events.Schemas.Utils;
using Squidex.Domain.Apps.Read.MongoDb.Utils;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.Reflection;

74
src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookEntity.cs

@ -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; }
}
}
}

119
src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookRepository.cs

@ -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();
}
}
}
}
}

99
src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookRepository_EventHandling.cs

@ -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);
}
}
}

1
src/Squidex.Domain.Apps.Read/Apps/Services/Implementations/CachingAppProvider.cs

@ -11,7 +11,6 @@ using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Read.Apps.Repositories;
using Squidex.Domain.Apps.Read.Utils;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.CQRS.Events;

2
src/Squidex.Domain.Apps.Read/Utils/CachingProviderBase.cs → src/Squidex.Domain.Apps.Read/CachingProviderBase.cs

@ -9,7 +9,7 @@
using Microsoft.Extensions.Caching.Memory;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Read.Utils
namespace Squidex.Domain.Apps.Read
{
public abstract class CachingProviderBase
{

1
src/Squidex.Domain.Apps.Read/Contents/Edm/EdmModelBuilder.cs

@ -14,7 +14,6 @@ using Squidex.Domain.Apps.Core.GenerateEdmSchema;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Read.Apps;
using Squidex.Domain.Apps.Read.Schemas;
using Squidex.Domain.Apps.Read.Utils;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Read.Contents.Edm

1
src/Squidex.Domain.Apps.Read/Contents/GraphQL/CachingGraphQLService.cs

@ -15,7 +15,6 @@ using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Read.Apps;
using Squidex.Domain.Apps.Read.Assets.Repositories;
using Squidex.Domain.Apps.Read.Schemas.Repositories;
using Squidex.Domain.Apps.Read.Utils;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Tasks;

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; }
}
}

14
src/Squidex.Domain.Apps.Read/Webhooks/IWebhookEventEntity.cs → src/Squidex.Domain.Apps.Read/Rules/IRuleEventEntity.cs

@ -1,5 +1,5 @@
// ==========================================================================
// IWebhookEventEntity.cs
// IRuleEventEntity.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
@ -7,18 +7,20 @@
// ==========================================================================
using NodaTime;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules;
namespace Squidex.Domain.Apps.Read.Webhooks
namespace Squidex.Domain.Apps.Read.Rules
{
public interface IWebhookEventEntity : IEntity
public interface IRuleEventEntity : IEntity
{
WebhookJob Job { get; }
RuleJob Job { get; }
Instant? NextAttempt { get; }
WebhookResult Result { get; }
RuleJobResult JobResult { get; }
WebhookJobResult JobResult { get; }
RuleResult Result { get; }
int NumCalls { get; }

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 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);
}
}

12
src/Squidex.Domain.Apps.Events/Webhooks/WebhookEditEvent.cs → src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleRepository.cs

@ -1,5 +1,5 @@
// ==========================================================================
// WebhookEditEvent.cs
// IRuleRepository.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
@ -8,14 +8,14 @@
using System;
using System.Collections.Generic;
using Squidex.Domain.Apps.Core.Webhooks;
using System.Threading.Tasks;
namespace Squidex.Domain.Apps.Events.Webhooks
namespace Squidex.Domain.Apps.Read.Rules.Repositories
{
public abstract class WebhookEditEvent : WebhookEvent
public interface IRuleRepository
{
public Uri Url { get; set; }
Task<IReadOnlyList<IRuleEntity>> QueryByAppAsync(Guid appId);
public List<WebhookSchema> Schemas { get; set; }
Task<IReadOnlyList<IRuleEntity>> QueryCachedByAppAsync(Guid appId);
}
}

85
src/Squidex.Domain.Apps.Read/Webhooks/WebhookDequeuer.cs → src/Squidex.Domain.Apps.Read/Rules/RuleDequeuer.cs

@ -1,5 +1,5 @@
// ==========================================================================
// WebhookDequeuer.cs
// RuleDequeuer.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
@ -11,50 +11,44 @@ using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using NodaTime;
using Squidex.Domain.Apps.Read.Webhooks.Repositories;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Read.Rules.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Timers;
namespace Squidex.Domain.Apps.Read.Webhooks
namespace Squidex.Domain.Apps.Read.Rules
{
public sealed class WebhookDequeuer : DisposableObjectBase, IExternalSystem
public sealed class RuleDequeuer : DisposableObjectBase, IExternalSystem
{
private readonly ActionBlock<IWebhookEventEntity> requestBlock;
private readonly TransformBlock<IWebhookEventEntity, IWebhookEventEntity> blockBlock;
private readonly IWebhookEventRepository webhookEventRepository;
private readonly IWebhookRepository webhookRepository;
private readonly WebhookSender webhookSender;
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;
private readonly IClock clock;
private readonly ISemanticLog log;
public WebhookDequeuer(WebhookSender webhookSender,
IWebhookEventRepository webhookEventRepository,
IWebhookRepository webhookRepository,
IClock clock,
ISemanticLog log)
public RuleDequeuer(RuleService ruleService, IRuleEventRepository ruleEventRepository, ISemanticLog log, IClock clock)
{
Guard.NotNull(webhookEventRepository, nameof(webhookEventRepository));
Guard.NotNull(webhookRepository, nameof(webhookRepository));
Guard.NotNull(webhookSender, nameof(webhookSender));
Guard.NotNull(ruleEventRepository, nameof(ruleEventRepository));
Guard.NotNull(ruleService, nameof(ruleService));
Guard.NotNull(clock, nameof(clock));
Guard.NotNull(log, nameof(log));
this.webhookEventRepository = webhookEventRepository;
this.webhookRepository = webhookRepository;
this.webhookSender = webhookSender;
this.ruleEventRepository = ruleEventRepository;
this.ruleService = ruleService;
this.clock = clock;
this.log = log;
requestBlock =
new ActionBlock<IWebhookEventEntity>(MakeRequestAsync,
new ActionBlock<IRuleEventEntity>(MakeRequestAsync,
new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 32, BoundedCapacity = 32 });
blockBlock =
new TransformBlock<IWebhookEventEntity, IWebhookEventEntity>(x => BlockAsync(x),
new TransformBlock<IRuleEventEntity, IRuleEventEntity>(x => BlockAsync(x),
new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 1, BoundedCapacity = 1 });
blockBlock.LinkTo(requestBlock, new DataflowLinkOptions { PropagateCompletion = true });
@ -86,7 +80,9 @@ namespace Squidex.Domain.Apps.Read.Webhooks
{
try
{
await webhookEventRepository.QueryPendingAsync(blockBlock.SendAsync, cancellationToken);
var now = clock.GetCurrentInstant();
await ruleEventRepository.QueryPendingAsync(now, blockBlock.SendAsync, cancellationToken);
}
catch (Exception ex)
{
@ -96,11 +92,11 @@ namespace Squidex.Domain.Apps.Read.Webhooks
}
}
private async Task<IWebhookEventEntity> BlockAsync(IWebhookEventEntity @event)
private async Task<IRuleEventEntity> BlockAsync(IRuleEventEntity @event)
{
try
{
await webhookEventRepository.TraceSendingAsync(@event.Id);
await ruleEventRepository.MarkSendingAsync(@event.Id);
return @event;
}
@ -114,38 +110,51 @@ namespace Squidex.Domain.Apps.Read.Webhooks
}
}
private async Task MakeRequestAsync(IWebhookEventEntity @event)
private async Task MakeRequestAsync(IRuleEventEntity @event)
{
try
{
var response = await webhookSender.SendAsync(@event.Job);
var job = @event.Job;
var response = await ruleService.InvokeAsync(job.ActionName, job.ActionData);
Instant? nextCall = null;
if (response.Result != WebhookResult.Success)
if (response.Result != RuleResult.Success)
{
var now = clock.GetCurrentInstant();
switch (@event.NumCalls)
{
case 0:
nextCall = now.Plus(Duration.FromMinutes(5));
nextCall = job.Created.Plus(Duration.FromMinutes(5));
break;
case 1:
nextCall = now.Plus(Duration.FromHours(1));
nextCall = job.Created.Plus(Duration.FromHours(1));
break;
case 2:
nextCall = now.Plus(Duration.FromHours(5));
nextCall = job.Created.Plus(Duration.FromHours(6));
break;
case 3:
nextCall = now.Plus(Duration.FromHours(6));
nextCall = job.Created.Plus(Duration.FromHours(12));
break;
}
}
await Task.WhenAll(
webhookRepository.TraceSentAsync(@event.Job.WebhookId, response.Result, response.Elapsed),
webhookEventRepository.TraceSentAsync(@event.Id, response.Dump, response.Result, response.Elapsed, nextCall));
RuleJobResult jobResult;
if (response.Result != RuleResult.Success && !nextCall.HasValue)
{
jobResult = RuleJobResult.Failed;
}
else if (response.Result != RuleResult.Success && nextCall.HasValue)
{
jobResult = RuleJobResult.Retry;
}
else
{
jobResult = RuleJobResult.Success;
}
await ruleEventRepository.MarkSentAsync(@event.Id, response.Dump, response.Result, jobResult, response.Elapsed, nextCall);
}
catch (Exception ex)
{

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

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

6
src/Squidex.Domain.Apps.Read/Webhooks/WebhookJobResult.cs → src/Squidex.Domain.Apps.Read/Rules/RuleJobResult.cs

@ -1,14 +1,14 @@
// ==========================================================================
// WebhookJobResult.cs
// RuleJobResult.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Read.Webhooks
namespace Squidex.Domain.Apps.Read.Rules
{
public enum WebhookJobResult
public enum RuleJobResult
{
Pending,
Success,

1
src/Squidex.Domain.Apps.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs

@ -11,7 +11,6 @@ using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Read.Schemas.Repositories;
using Squidex.Domain.Apps.Read.Utils;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.CQRS.Events;

31
src/Squidex.Domain.Apps.Read/Webhooks/IWebhookEntity.cs

@ -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; }
}
}

35
src/Squidex.Domain.Apps.Read/Webhooks/Repositories/IWebhookEventRepository.cs

@ -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);
}
}

23
src/Squidex.Domain.Apps.Read/Webhooks/Repositories/IWebhookRepository.cs

@ -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);
}
}

140
src/Squidex.Domain.Apps.Read/Webhooks/WebhookEnqueuer.cs

@ -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;
}
}
}

98
src/Squidex.Domain.Apps.Read/Webhooks/WebhookSender.cs

@ -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;
}
}
}

20
src/Squidex.Domain.Apps.Write/Rules/Commands/CreateRule.cs

@ -0,0 +1,20 @@
// ==========================================================================
// CreateRule.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
namespace Squidex.Domain.Apps.Write.Rules.Commands
{
public sealed class CreateRule : RuleEditCommand
{
public CreateRule()
{
RuleId = Guid.NewGuid();
}
}
}

14
src/Squidex.Domain.Apps.Write/Rules/Commands/DeleteRule.cs

@ -0,0 +1,14 @@
// ==========================================================================
// DeleteRule.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Write.Rules.Commands
{
public sealed class DeleteRule : RuleAggregateCommand
{
}
}

14
src/Squidex.Domain.Apps.Write/Rules/Commands/DisableRule.cs

@ -0,0 +1,14 @@
// ==========================================================================
// DisableRule.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Write.Rules.Commands
{
public sealed class DisableRule : RuleAggregateCommand
{
}
}

14
src/Squidex.Domain.Apps.Write/Rules/Commands/EnableRule.cs

@ -0,0 +1,14 @@
// ==========================================================================
// EnableRule.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Write.Rules.Commands
{
public sealed class EnableRule : RuleAggregateCommand
{
}
}

10
src/Squidex.Domain.Apps.Write/Webhooks/Commands/WebhookAggregateCommand.cs → src/Squidex.Domain.Apps.Write/Rules/Commands/RuleAggregateCommand.cs

@ -1,5 +1,5 @@
// ==========================================================================
// WebhookAggregateCommand.cs
// RuleAggregateCommand.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
@ -9,15 +9,15 @@
using System;
using Squidex.Infrastructure.CQRS.Commands;
namespace Squidex.Domain.Apps.Write.Webhooks.Commands
namespace Squidex.Domain.Apps.Write.Rules.Commands
{
public abstract class WebhookAggregateCommand : AppCommand, IAggregateCommand
public abstract class RuleAggregateCommand : AppCommand, IAggregateCommand
{
public Guid WebhookId { get; set; }
public Guid RuleId { get; set; }
Guid IAggregateCommand.AggregateId
{
get { return WebhookId; }
get { return RuleId; }
}
}
}

19
src/Squidex.Domain.Apps.Write/Rules/Commands/RuleEditCommand.cs

@ -0,0 +1,19 @@
// ==========================================================================
// RuleEditCommand.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Domain.Apps.Core.Rules;
namespace Squidex.Domain.Apps.Write.Rules.Commands
{
public abstract class RuleEditCommand : RuleAggregateCommand
{
public RuleTrigger Trigger { get; set; }
public RuleAction Action { get; set; }
}
}

14
src/Squidex.Domain.Apps.Write/Rules/Commands/UpdateRule.cs

@ -0,0 +1,14 @@
// ==========================================================================
// UpdateRule.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Write.Rules.Commands
{
public sealed class UpdateRule : RuleEditCommand
{
}
}

107
src/Squidex.Domain.Apps.Write/Rules/Guards/GuardRule.cs

@ -0,0 +1,107 @@
// ==========================================================================
// GuardRule.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Read.Schemas.Services;
using Squidex.Domain.Apps.Write.Rules.Commands;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Write.Rules.Guards
{
public static class GuardRule
{
public static Task CanCreate(CreateRule command, ISchemaProvider schemas)
{
Guard.NotNull(command, nameof(command));
return Validate.It(() => "Cannot create rule.", async error =>
{
if (command.Trigger == null)
{
error(new ValidationError("Trigger must be defined.", nameof(command.Trigger)));
}
else
{
var errors = await RuleTriggerValidator.ValidateAsync(command.Trigger, schemas);
errors.Foreach(error);
}
if (command.Action == null)
{
error(new ValidationError("Trigger must be defined.", nameof(command.Action)));
}
else
{
var errors = await RuleActionValidator.ValidateAsync(command.Action);
errors.Foreach(error);
}
});
}
public static Task CanUpdate(UpdateRule command, ISchemaProvider schemas)
{
Guard.NotNull(command, nameof(command));
return Validate.It(() => "Cannot update rule.", async error =>
{
if (command.Trigger == null && command.Action == null)
{
error(new ValidationError("Either trigger or action must be defined.", nameof(command.Trigger), nameof(command.Action)));
}
if (command.Trigger != null)
{
var errors = await RuleTriggerValidator.ValidateAsync(command.Trigger, schemas);
errors.Foreach(error);
}
if (command.Action != null)
{
var errors = await RuleActionValidator.ValidateAsync(command.Action);
errors.Foreach(error);
}
});
}
public static void CanEnable(EnableRule command, Rule rule)
{
Guard.NotNull(command, nameof(command));
Validate.It(() => "Cannot enable rule.", error =>
{
if (rule.IsEnabled)
{
error(new ValidationError("Rule is already enabled."));
}
});
}
public static void CanDisable(DisableRule command, Rule rule)
{
Guard.NotNull(command, nameof(command));
Validate.It(() => "Cannot disable rule.", error =>
{
if (!rule.IsEnabled)
{
error(new ValidationError("Rule is already disabled."));
}
});
}
public static void CanDelete(DeleteRule command)
{
Guard.NotNull(command, nameof(command));
}
}
}

40
src/Squidex.Domain.Apps.Write/Rules/Guards/RuleActionValidator.cs

@ -0,0 +1,40 @@
// ==========================================================================
// RuleActionValidator.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.Actions;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Write.Rules.Guards
{
public sealed class RuleActionValidator : IRuleActionVisitor<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);
}
}
}

54
src/Squidex.Domain.Apps.Write/Rules/Guards/RuleTriggerValidator.cs

@ -0,0 +1,54 @@
// ==========================================================================
// RuleTriggerValidator.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Read.Schemas.Services;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Write.Rules.Guards
{
public sealed class RuleTriggerValidator : IRuleTriggerVisitor<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>();
}
}
}

92
src/Squidex.Domain.Apps.Write/Rules/RuleCommandMiddleware.cs

@ -0,0 +1,92 @@
// ==========================================================================
// RuleCommandMiddleware.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Read.Schemas.Services;
using Squidex.Domain.Apps.Write.Rules.Commands;
using Squidex.Domain.Apps.Write.Rules.Guards;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Dispatching;
namespace Squidex.Domain.Apps.Write.Rules
{
public class RuleCommandMiddleware : ICommandMiddleware
{
private readonly IAggregateHandler handler;
private readonly ISchemaProvider schemas;
public RuleCommandMiddleware(IAggregateHandler handler, ISchemaProvider schemas)
{
Guard.NotNull(handler, nameof(handler));
Guard.NotNull(schemas, nameof(schemas));
this.handler = handler;
this.schemas = schemas;
}
protected Task On(CreateRule command, CommandContext context)
{
return handler.CreateAsync<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();
}
}
}
}

118
src/Squidex.Domain.Apps.Write/Rules/RuleDomainObject.cs

@ -0,0 +1,118 @@
// ==========================================================================
// RuleDomainObject.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Events.Rules;
using Squidex.Domain.Apps.Events.Rules.Utils;
using Squidex.Domain.Apps.Write.Rules.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Write.Rules
{
public class RuleDomainObject : DomainObjectBase
{
private Rule rule;
private bool isDeleted;
public Rule Rule
{
get { return rule; }
}
public RuleDomainObject(Guid id, int version)
: base(id, version)
{
}
protected void On(RuleCreated @event)
{
rule = RuleEventDispatcher.Create(@event);
}
protected void On(RuleUpdated @event)
{
rule.Apply(@event);
}
protected void On(RuleEnabled @event)
{
rule.Apply(@event);
}
protected void On(RuleDisabled @event)
{
rule.Apply(@event);
}
protected void On(RuleDeleted @event)
{
isDeleted = true;
}
public void Create(CreateRule command)
{
VerifyNotCreated();
RaiseEvent(SimpleMapper.Map(command, new RuleCreated()));
}
public void Update(UpdateRule command)
{
VerifyCreatedAndNotDeleted();
RaiseEvent(SimpleMapper.Map(command, new RuleUpdated()));
}
public void Enable(EnableRule command)
{
VerifyCreatedAndNotDeleted();
RaiseEvent(SimpleMapper.Map(command, new RuleEnabled()));
}
public void Disable(DisableRule command)
{
VerifyCreatedAndNotDeleted();
RaiseEvent(SimpleMapper.Map(command, new RuleDisabled()));
}
public void Delete(DeleteRule command)
{
VerifyCreatedAndNotDeleted();
RaiseEvent(SimpleMapper.Map(command, new RuleDeleted()));
}
private void VerifyNotCreated()
{
if (rule != null)
{
throw new DomainException("Webhook has already been created.");
}
}
private void VerifyCreatedAndNotDeleted()
{
if (isDeleted || rule == null)
{
throw new DomainException("Webhook has already been deleted or not created yet.");
}
}
protected override void DispatchEvent(Envelope<IEvent> @event)
{
this.DispatchAction(@event.Payload);
}
}
}

21
src/Squidex.Domain.Apps.Write/Webhooks/Commands/WebhookEditCommand.cs

@ -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>();
}
}

61
src/Squidex.Domain.Apps.Write/Webhooks/Guards/GuardWebhook.cs

@ -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);
}
}
}
}
}

72
src/Squidex.Domain.Apps.Write/Webhooks/WebhookCommandMiddleware.cs

@ -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();
}
}
}
}

82
src/Squidex.Domain.Apps.Write/Webhooks/WebhookDomainObject.cs

@ -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);
}
}
}

4
src/Squidex.Infrastructure/Dispatching/ActionContextDispatcher.cs

@ -10,9 +10,11 @@ using System;
using System.Linq;
using System.Reflection;
#pragma warning disable IDE0033 // Use explicitly provided tuple name
namespace Squidex.Infrastructure.Dispatching
{
public sealed class ActionContextDispatcher<TTarget, TIn, TContext>
public static class ActionContextDispatcher<TTarget, TIn, TContext>
{
public delegate void ActionContextDelegate<in T>(TTarget target, T input, TContext context) where T : TIn;

4
src/Squidex.Infrastructure/Dispatching/ActionDispatcher.cs

@ -10,9 +10,11 @@ using System;
using System.Linq;
using System.Reflection;
#pragma warning disable IDE0033 // Use explicitly provided tuple name
namespace Squidex.Infrastructure.Dispatching
{
public sealed class ActionDispatcher<TTarget, TIn>
public static class ActionDispatcher<TTarget, TIn>
{
public delegate void ActionDelegate<in T>(TTarget target, T input) where T : TIn;

4
src/Squidex.Infrastructure/Dispatching/FuncContextDispatcher.cs

@ -9,9 +9,11 @@
using System.Linq;
using System.Reflection;
#pragma warning disable IDE0033 // Use explicitly provided tuple name
namespace Squidex.Infrastructure.Dispatching
{
public sealed class FuncContextDispatcher<TTarget, TIn, TContext, TOut>
public static class FuncContextDispatcher<TTarget, TIn, TContext, TOut>
{
public delegate TOut FuncContextDelegate<in T>(TTarget target, T input, TContext context) where T : TIn;

4
src/Squidex.Infrastructure/Dispatching/FuncDispatcher.cs

@ -9,9 +9,11 @@
using System.Linq;
using System.Reflection;
#pragma warning disable IDE0033 // Use explicitly provided tuple name
namespace Squidex.Infrastructure.Dispatching
{
public sealed class FuncDispatcher<TTarget, TIn, TOut>
public static class FuncDispatcher<TTarget, TIn, TOut>
{
public delegate TOut FuncDelegate<in T>(TTarget target, T input) where T : TIn;

14
src/Squidex.Infrastructure/SquidexInfrastructure.cs

@ -0,0 +1,14 @@
// ==========================================================================
// SquidexInfrastructure.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Infrastructure
{
public static class SquidexInfrastructure
{
}
}

36
src/Squidex.Infrastructure/TypeNameRegistry.cs

@ -17,20 +17,6 @@ namespace Squidex.Infrastructure
private readonly Dictionary<Type, string> namesByType = new Dictionary<Type, string>();
private readonly Dictionary<string, Type> typesByName = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);
public TypeNameRegistry Map(Type type)
{
Guard.NotNull(type, nameof(type));
var typeNameAttribute = type.GetTypeInfo().GetCustomAttribute<TypeNameAttribute>();
if (typeNameAttribute != null)
{
Map(type, typeNameAttribute.TypeName);
}
return this;
}
public TypeNameRegistry MapObsolete(Type type, string name)
{
Guard.NotNull(type, nameof(type));
@ -56,6 +42,20 @@ namespace Squidex.Infrastructure
return this;
}
public TypeNameRegistry Map(Type type)
{
Guard.NotNull(type, nameof(type));
var typeNameAttribute = type.GetTypeInfo().GetCustomAttribute<TypeNameAttribute>();
if (!string.IsNullOrWhiteSpace(typeNameAttribute?.TypeName))
{
Map(type, typeNameAttribute.TypeName);
}
return this;
}
public TypeNameRegistry Map(Type type, string name)
{
Guard.NotNull(type, nameof(type));
@ -95,15 +95,13 @@ namespace Squidex.Infrastructure
return this;
}
public TypeNameRegistry Map(Assembly assembly)
public TypeNameRegistry MapUnmapped(Assembly assembly)
{
foreach (var type in assembly.GetTypes())
{
var typeNameAttribute = type.GetTypeInfo().GetCustomAttribute<TypeNameAttribute>();
if (!string.IsNullOrWhiteSpace(typeNameAttribute?.TypeName))
if (!namesByType.ContainsKey(type))
{
Map(type, typeNameAttribute.TypeName);
Map(type);
}
}

21
src/Squidex/Config/Domain/ReadModule.cs

@ -11,6 +11,9 @@ using System.Linq;
using Autofac;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.ActionHandlers;
using Squidex.Domain.Apps.Core.HandleRules.Triggers;
using Squidex.Domain.Apps.Read.Apps;
using Squidex.Domain.Apps.Read.Apps.Services;
using Squidex.Domain.Apps.Read.Apps.Services.Implementations;
@ -18,10 +21,10 @@ using Squidex.Domain.Apps.Read.Contents;
using Squidex.Domain.Apps.Read.Contents.Edm;
using Squidex.Domain.Apps.Read.Contents.GraphQL;
using Squidex.Domain.Apps.Read.History;
using Squidex.Domain.Apps.Read.Rules;
using Squidex.Domain.Apps.Read.Schemas;
using Squidex.Domain.Apps.Read.Schemas.Services;
using Squidex.Domain.Apps.Read.Schemas.Services.Implementations;
using Squidex.Domain.Apps.Read.Webhooks;
using Squidex.Domain.Users;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
@ -104,17 +107,27 @@ namespace Squidex.Config.Domain
.AsSelf()
.InstancePerDependency();
builder.RegisterType<WebhookDequeuer>()
builder.RegisterType<RuleDequeuer>()
.As<IExternalSystem>()
.AsSelf()
.InstancePerDependency();
builder.RegisterType<WebhookEnqueuer>()
builder.RegisterType<RuleEnqueuer>()
.As<IEventConsumer>()
.AsSelf()
.InstancePerDependency();
builder.RegisterType<WebhookSender>()
builder.RegisterType<ContentChangedTriggerHandler>()
.As<IRuleTriggerHandler>()
.AsSelf()
.SingleInstance();
builder.RegisterType<WebhookActionHandler>()
.As<IRuleActionHandler>()
.AsSelf()
.SingleInstance();
builder.RegisterType<RuleService>()
.AsSelf()
.SingleInstance();

10
src/Squidex/Config/Domain/Serializers.cs

@ -6,18 +6,18 @@
// All rights reserved.
// ==========================================================================
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using NodaTime;
using NodaTime.Serialization.JsonNet;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Apps.Json;
using Squidex.Domain.Apps.Core.Rules.Json;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.Schemas.Json;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.MongoDb;
@ -45,6 +45,7 @@ namespace Squidex.Config.Domain
new NamedStringIdConverter(),
new PropertiesBagConverter(),
new RefTokenConverter(),
new RuleConverter(),
new SchemaConverter(FieldRegistry),
new StringEnumConverter());
@ -62,8 +63,9 @@ namespace Squidex.Config.Domain
static Serializers()
{
TypeNameRegistry.Map(typeof(SquidexEvent).GetTypeInfo().Assembly);
TypeNameRegistry.Map(typeof(NoopEvent).GetTypeInfo().Assembly);
TypeNameRegistry.MapUnmapped(typeof(SquidexCoreModel).Assembly);
TypeNameRegistry.MapUnmapped(typeof(SquidexEvents).Assembly);
TypeNameRegistry.MapUnmapped(typeof(SquidexInfrastructure).Assembly);
ConfigureJson(SerializerSettings, TypeNameHandling.Auto);

12
src/Squidex/Config/Domain/StoreMongoDbModule.cs

@ -22,11 +22,11 @@ using Squidex.Domain.Apps.Read.MongoDb.Apps;
using Squidex.Domain.Apps.Read.MongoDb.Assets;
using Squidex.Domain.Apps.Read.MongoDb.Contents;
using Squidex.Domain.Apps.Read.MongoDb.History;
using Squidex.Domain.Apps.Read.MongoDb.Rules;
using Squidex.Domain.Apps.Read.MongoDb.Schemas;
using Squidex.Domain.Apps.Read.MongoDb.Webhooks;
using Squidex.Domain.Apps.Read.Rules.Repositories;
using Squidex.Domain.Apps.Read.Schemas.Repositories;
using Squidex.Domain.Apps.Read.Schemas.Services.Implementations;
using Squidex.Domain.Apps.Read.Webhooks.Repositories;
using Squidex.Domain.Users;
using Squidex.Domain.Users.MongoDb;
using Squidex.Domain.Users.MongoDb.Infrastructure;
@ -136,9 +136,9 @@ namespace Squidex.Config.Domain
.AsSelf()
.SingleInstance();
builder.RegisterType<MongoWebhookEventRepository>()
builder.RegisterType<MongoRuleEventRepository>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseRegistration))
.As<IWebhookEventRepository>()
.As<IRuleEventRepository>()
.As<IExternalSystem>()
.AsSelf()
.SingleInstance();
@ -171,9 +171,9 @@ namespace Squidex.Config.Domain
.AsSelf()
.SingleInstance();
builder.RegisterType<MongoWebhookRepository>()
builder.RegisterType<MongoRuleRepository>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseRegistration))
.As<IWebhookRepository>()
.As<IRuleRepository>()
.As<IEventConsumer>()
.As<IExternalSystem>()
.AsSelf()

6
src/Squidex/Config/Domain/WriteModule.cs

@ -13,8 +13,8 @@ using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Write.Apps;
using Squidex.Domain.Apps.Write.Assets;
using Squidex.Domain.Apps.Write.Contents;
using Squidex.Domain.Apps.Write.Rules;
using Squidex.Domain.Apps.Write.Schemas;
using Squidex.Domain.Apps.Write.Webhooks;
using Squidex.Domain.Users;
using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Pipeline.CommandMiddlewares;
@ -80,7 +80,7 @@ namespace Squidex.Config.Domain
.As<ICommandMiddleware>()
.SingleInstance();
builder.RegisterType<WebhookCommandMiddleware>()
builder.RegisterType<RuleCommandMiddleware>()
.As<ICommandMiddleware>()
.SingleInstance();
@ -96,7 +96,7 @@ namespace Squidex.Config.Domain
.AsSelf()
.SingleInstance();
builder.Register<DomainObjectFactoryFunction<WebhookDomainObject>>(c => (id => new WebhookDomainObject(id, -1)))
builder.Register<DomainObjectFactoryFunction<RuleDomainObject>>(c => (id => new RuleDomainObject(id, -1)))
.AsSelf()
.SingleInstance();

25
src/Squidex/Controllers/Api/Webhooks/Models/WebhookCreatedDto.cs → src/Squidex/Controllers/Api/Rules/Models/Actions/WebhookActionDto.cs

@ -1,5 +1,5 @@
// ==========================================================================
// WebhookCreatedDto.cs
// WebhookActionDto.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
@ -8,25 +8,30 @@
using System;
using System.ComponentModel.DataAnnotations;
using NJsonSchema.Annotations;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.Actions;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Controllers.Api.Webhooks.Models
namespace Squidex.Controllers.Api.Rules.Models.Actions
{
public sealed class WebhookCreatedDto
[JsonSchema("Webhook")]
public sealed class WebhookActionDto : RuleActionDto
{
/// <summary>
/// The id of the webhook.
/// The url of the rule.
/// </summary>
public Guid Id { get; set; }
[Required]
public Uri Url { get; set; }
/// <summary>
/// The shared secret that is used to calculate the signature.
/// </summary>
[Required]
public string SharedSecret { get; set; }
/// <summary>
/// The version of the schema.
/// </summary>
public long Version { get; set; }
public override RuleAction ToAction()
{
return SimpleMapper.Map(this, new WebhookAction());
}
}
}

34
src/Squidex/Controllers/Api/Rules/Models/Converters/RuleActionDtoFactory.cs

@ -0,0 +1,34 @@
// ==========================================================================
// RuleActionDtoFactory.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Controllers.Api.Rules.Models.Actions;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.Actions;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Controllers.Api.Rules.Models.Converters
{
public sealed class RuleActionDtoFactory : IRuleActionVisitor<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());
}
}
}

72
src/Squidex/Controllers/Api/Rules/Models/Converters/RuleConverter.cs

@ -0,0 +1,72 @@
// ==========================================================================
// RuleConverter.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Squidex.Domain.Apps.Read.Rules;
using Squidex.Domain.Apps.Write.Rules.Commands;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Controllers.Api.Rules.Models.Converters
{
public static class RuleConverter
{
public static RuleDto ToModel(this IRuleEntity entity)
{
var dto = new RuleDto();
SimpleMapper.Map(entity, dto);
SimpleMapper.Map(entity.Rule, dto);
if (entity.Rule.Trigger != null)
{
dto.Trigger = RuleTriggerDtoFactory.Create(entity.Rule.Trigger);
}
if (entity.Rule.Action != null)
{
dto.Action = RuleActionDtoFactory.Create(entity.Rule.Action);
}
return dto;
}
public static UpdateRule ToCommand(this UpdateRuleDto dto, Guid id)
{
var command = new UpdateRule { RuleId = id };
if (dto.Action != null)
{
command.Action = dto.Action.ToAction();
}
if (dto.Trigger != null)
{
command.Trigger = dto.Trigger.ToTrigger();
}
return command;
}
public static CreateRule ToCommand(this CreateRuleDto dto)
{
var command = new CreateRule();
if (dto.Action != null)
{
command.Action = dto.Action.ToAction();
}
if (dto.Trigger != null)
{
command.Trigger = dto.Trigger.ToTrigger();
}
return command;
}
}
}

38
src/Squidex/Controllers/Api/Rules/Models/Converters/RuleTriggerDtoFactory.cs

@ -0,0 +1,38 @@
// ==========================================================================
// RuleTriggerDtoFactory.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Linq;
using Squidex.Controllers.Api.Rules.Models.Triggers;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Controllers.Api.Rules.Models.Converters
{
public sealed class RuleTriggerDtoFactory : IRuleTriggerVisitor<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()
};
}
}
}

16
src/Squidex/Controllers/Api/Webhooks/Models/CreateWebhookDto.cs → src/Squidex/Controllers/Api/Rules/Models/CreateRuleDto.cs

@ -1,29 +1,27 @@
// ==========================================================================
// CreateWebhookDto.cs
// CreateRuleDto.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Squidex.Controllers.Api.Webhooks.Models
namespace Squidex.Controllers.Api.Rules.Models
{
public sealed class CreateWebhookDto
public sealed class CreateRuleDto
{
/// <summary>
/// The url of the webhook.
/// The trigger properties.
/// </summary>
[Required]
public Uri Url { get; set; }
public RuleTriggerDto Trigger { get; set; }
/// <summary>
/// The schema settings.
/// The action properties.
/// </summary>
[Required]
public List<WebhookSchemaDto> Schemas { get; set; }
public RuleActionDto Action { get; set; }
}
}

22
src/Squidex/Controllers/Api/Rules/Models/RuleActionDto.cs

@ -0,0 +1,22 @@
// ==========================================================================
// RuleActionDto.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Runtime.Serialization;
using Newtonsoft.Json;
using Squidex.Controllers.Api.Rules.Models.Actions;
using Squidex.Domain.Apps.Core.Rules;
namespace Squidex.Controllers.Api.Rules.Models
{
[JsonConverter(typeof(JsonInheritanceConverter), "actionType")]
[KnownType(typeof(WebhookActionDto))]
public abstract class RuleActionDto
{
public abstract RuleAction ToAction();
}
}

67
src/Squidex/Controllers/Api/Rules/Models/RuleDto.cs

@ -0,0 +1,67 @@
// ==========================================================================
// RuleDto.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.ComponentModel.DataAnnotations;
using NodaTime;
using Squidex.Infrastructure;
namespace Squidex.Controllers.Api.Rules.Models
{
public sealed class RuleDto
{
/// <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; }
}
}

17
src/Squidex/Controllers/Api/Webhooks/Models/WebhookEventDto.cs → src/Squidex/Controllers/Api/Rules/Models/RuleEventDto.cs

@ -1,5 +1,5 @@
// ==========================================================================
// WebhookEventDto.cs
// RuleEventDto.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
@ -9,11 +9,12 @@
using System;
using System.ComponentModel.DataAnnotations;
using NodaTime;
using Squidex.Domain.Apps.Read.Webhooks;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Read.Rules;
namespace Squidex.Controllers.Api.Webhooks.Models
namespace Squidex.Controllers.Api.Rules.Models
{
public sealed class WebhookEventDto
public sealed class RuleEventDto
{
/// <summary>
/// The id of the event.
@ -26,10 +27,10 @@ namespace Squidex.Controllers.Api.Webhooks.Models
public Instant Created { get; set; }
/// <summary>
/// The request url.
/// The description
/// </summary>
[Required]
public Uri RequestUrl { get; set; }
public string Description { get; set; }
/// <summary>
/// The name of the event.
@ -55,11 +56,11 @@ namespace Squidex.Controllers.Api.Webhooks.Models
/// <summary>
/// The result of the event.
/// </summary>
public WebhookResult Result { get; set; }
public RuleResult Result { get; set; }
/// <summary>
/// The result of the job.
/// </summary>
public WebhookJobResult JobResult { get; set; }
public RuleJobResult JobResult { get; set; }
}
}

12
src/Squidex/Controllers/Api/Webhooks/Models/WebhookEventsDto.cs → src/Squidex/Controllers/Api/Rules/Models/RuleEventsDto.cs

@ -1,23 +1,23 @@
// ==========================================================================
// WebhookEventsDto.cs
// RuleEventsDto.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Controllers.Api.Webhooks.Models
namespace Squidex.Controllers.Api.Rules.Models
{
public sealed class WebhookEventsDto
public sealed class RuleEventsDto
{
/// <summary>
/// The total number of webhook events.
/// The total number of rule events.
/// </summary>
public long Total { get; set; }
/// <summary>
/// The webhook events.
/// The rule events.
/// </summary>
public WebhookEventDto[] Items { get; set; }
public RuleEventDto[] Items { get; set; }
}
}

22
src/Squidex/Controllers/Api/Rules/Models/RuleTriggerDto.cs

@ -0,0 +1,22 @@
// ==========================================================================
// RuleTriggerDto.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Runtime.Serialization;
using Newtonsoft.Json;
using Squidex.Controllers.Api.Rules.Models.Triggers;
using Squidex.Domain.Apps.Core.Rules;
namespace Squidex.Controllers.Api.Rules.Models
{
[JsonConverter(typeof(JsonInheritanceConverter), "triggerType")]
[KnownType(typeof(ContentChangedTriggerDto))]
public abstract class RuleTriggerDto
{
public abstract RuleTrigger ToTrigger();
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save