Browse Source

Write logic updated and tested.

pull/157/head
Sebastian Stehle 8 years ago
parent
commit
9e1540e909
  1. 1
      src/Squidex.Domain.Apps.Core.Model/Rules/Json/RuleConverter.cs
  2. 25
      src/Squidex.Domain.Apps.Core.Model/Webhooks/WebhookSchema.cs
  3. 37
      src/Squidex.Domain.Apps.Events/Rules/Utils/RuleEventDispatcher.cs
  4. 18
      src/Squidex.Domain.Apps.Events/Webhooks/WebhookCreated.cs
  5. 17
      src/Squidex.Domain.Apps.Events/Webhooks/WebhookDeleted.cs
  6. 21
      src/Squidex.Domain.Apps.Events/Webhooks/WebhookEditEvent.cs
  7. 17
      src/Squidex.Domain.Apps.Events/Webhooks/WebhookUpdated.cs
  8. 1
      src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs
  9. 1
      src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository_EventHandling.cs
  10. 1
      src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs
  11. 2
      src/Squidex.Domain.Apps.Read.MongoDb/EntityMapper.cs
  12. 1
      src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventRepository.cs
  13. 2
      src/Squidex.Domain.Apps.Read.MongoDb/MongoCollectionExtensions.cs
  14. 41
      src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEntity.cs
  15. 45
      src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventEntity.cs
  16. 50
      src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventRepository.cs
  17. 90
      src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository.cs
  18. 97
      src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository_EventHandling.cs
  19. 1
      src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs
  20. 74
      src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookEntity.cs
  21. 119
      src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookRepository.cs
  22. 99
      src/Squidex.Domain.Apps.Read.MongoDb/Webhooks/MongoWebhookRepository_EventHandling.cs
  23. 1
      src/Squidex.Domain.Apps.Read/Apps/Services/Implementations/CachingAppProvider.cs
  24. 2
      src/Squidex.Domain.Apps.Read/CachingProviderBase.cs
  25. 1
      src/Squidex.Domain.Apps.Read/Contents/Edm/EdmModelBuilder.cs
  26. 1
      src/Squidex.Domain.Apps.Read/Contents/GraphQL/CachingGraphQLService.cs
  27. 1
      src/Squidex.Domain.Apps.Read/Rules/IRuleEventEntity.cs
  28. 2
      src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleEventRepository.cs
  29. 4
      src/Squidex.Domain.Apps.Read/Rules/RuleDequeuer.cs
  30. 1
      src/Squidex.Domain.Apps.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs
  31. 31
      src/Squidex.Domain.Apps.Read/Webhooks/IWebhookEntity.cs
  32. 27
      src/Squidex.Domain.Apps.Read/Webhooks/IWebhookEventEntity.cs
  33. 35
      src/Squidex.Domain.Apps.Read/Webhooks/Repositories/IWebhookEventRepository.cs
  34. 23
      src/Squidex.Domain.Apps.Read/Webhooks/Repositories/IWebhookRepository.cs
  35. 160
      src/Squidex.Domain.Apps.Read/Webhooks/WebhookDequeuer.cs
  36. 140
      src/Squidex.Domain.Apps.Read/Webhooks/WebhookEnqueuer.cs
  37. 32
      src/Squidex.Domain.Apps.Read/Webhooks/WebhookJob.cs
  38. 98
      src/Squidex.Domain.Apps.Read/Webhooks/WebhookSender.cs
  39. 6
      src/Squidex.Domain.Apps.Write/Rules/Commands/CreateRule.cs
  40. 6
      src/Squidex.Domain.Apps.Write/Rules/Commands/DeleteRule.cs
  41. 10
      src/Squidex.Domain.Apps.Write/Rules/Commands/DisableRule.cs
  42. 10
      src/Squidex.Domain.Apps.Write/Rules/Commands/EnableRule.cs
  43. 10
      src/Squidex.Domain.Apps.Write/Rules/Commands/RuleAggregateCommand.cs
  44. 12
      src/Squidex.Domain.Apps.Write/Rules/Commands/RuleEditCommand.cs
  45. 14
      src/Squidex.Domain.Apps.Write/Rules/Commands/UpdateRule.cs
  46. 107
      src/Squidex.Domain.Apps.Write/Rules/Guards/GuardRule.cs
  47. 40
      src/Squidex.Domain.Apps.Write/Rules/Guards/RuleActionValidator.cs
  48. 54
      src/Squidex.Domain.Apps.Write/Rules/Guards/RuleTriggerValidator.cs
  49. 92
      src/Squidex.Domain.Apps.Write/Rules/RuleCommandMiddleware.cs
  50. 118
      src/Squidex.Domain.Apps.Write/Rules/RuleDomainObject.cs
  51. 23
      src/Squidex.Domain.Apps.Write/Webhooks/Commands/CreateWebhook.cs
  52. 21
      src/Squidex.Domain.Apps.Write/Webhooks/Commands/WebhookEditCommand.cs
  53. 61
      src/Squidex.Domain.Apps.Write/Webhooks/Guards/GuardWebhook.cs
  54. 72
      src/Squidex.Domain.Apps.Write/Webhooks/WebhookCommandMiddleware.cs
  55. 82
      src/Squidex.Domain.Apps.Write/Webhooks/WebhookDomainObject.cs
  56. 2
      src/Squidex/Squidex.csproj
  57. 48
      tests/Squidex.Domain.Apps.Write.Tests/Rules/Guards/Actions/WebhookActionTests.cs
  58. 158
      tests/Squidex.Domain.Apps.Write.Tests/Rules/Guards/GuardRuleTests.cs
  59. 85
      tests/Squidex.Domain.Apps.Write.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs
  60. 117
      tests/Squidex.Domain.Apps.Write.Tests/Rules/RuleCommandMiddlewareTests.cs
  61. 240
      tests/Squidex.Domain.Apps.Write.Tests/Rules/RuleDomainObjectTests.cs
  62. 138
      tests/Squidex.Domain.Apps.Write.Tests/Webhooks/Guards/GuardWebhookTests.cs
  63. 115
      tests/Squidex.Domain.Apps.Write.Tests/Webhooks/WebhookCommandMiddlewareTests.cs
  64. 159
      tests/Squidex.Domain.Apps.Write.Tests/Webhooks/WebhookDomainObjectTests.cs

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

@ -6,7 +6,6 @@
// All rights reserved.
// ==========================================================================
using System;
using Newtonsoft.Json;
using Squidex.Infrastructure.Json;

25
src/Squidex.Domain.Apps.Core.Model/Webhooks/WebhookSchema.cs

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

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

@ -6,12 +6,7 @@
// All rights reserved.
// ==========================================================================
using System.Linq;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.Actions;
using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Events.Webhooks;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Events.Rules.Utils
{
@ -22,21 +17,6 @@ namespace Squidex.Domain.Apps.Events.Rules.Utils
return new Rule(@event.Trigger, @event.Action);
}
public static Rule Create(WebhookCreated @event)
{
return new Rule(CreateTrigger(@event), CreateAction(@event));
}
public static void Apply(this Rule rule, WebhookUpdated @event)
{
rule.Update(CreateTrigger(@event));
if (rule.Action is WebhookAction webhookAction)
{
webhookAction.Url = @event.Url;
}
}
public static void Apply(this Rule rule, RuleUpdated @event)
{
if (@event.Trigger != null)
@ -59,22 +39,5 @@ namespace Squidex.Domain.Apps.Events.Rules.Utils
{
rule.Disable();
}
private static WebhookAction CreateAction(WebhookCreated @event)
{
var action = new WebhookAction { Url = @event.Url, SharedSecret = @event.SharedSecret };
return action;
}
private static ContentChangedTrigger CreateTrigger(WebhookEditEvent @event)
{
var trigger = new ContentChangedTrigger
{
Schemas = @event.Schemas.Select(x => SimpleMapper.Map(x, new ContentChangedTriggerSchema())).ToList()
};
return trigger;
}
}
}

18
src/Squidex.Domain.Apps.Events/Webhooks/WebhookCreated.cs

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

17
src/Squidex.Domain.Apps.Events/Webhooks/WebhookDeleted.cs

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

21
src/Squidex.Domain.Apps.Events/Webhooks/WebhookEditEvent.cs

@ -1,21 +0,0 @@
// ==========================================================================
// WebhookEditEvent.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using Squidex.Domain.Apps.Core.Webhooks;
namespace Squidex.Domain.Apps.Events.Webhooks
{
public abstract class WebhookEditEvent : WebhookEvent
{
public Uri Url { get; set; }
public List<WebhookSchema> Schemas { get; set; }
}
}

17
src/Squidex.Domain.Apps.Events/Webhooks/WebhookUpdated.cs

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

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

45
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,30 @@
using System;
using MongoDB.Bson.Serialization.Attributes;
using NodaTime;
using Squidex.Domain.Apps.Read.Webhooks;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Read.Rules;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Read.MongoDb.Webhooks
namespace Squidex.Domain.Apps.Read.MongoDb.Rules
{
public sealed class MongoWebhookEventEntity : MongoEntity, IWebhookEventEntity
public sealed class MongoRuleEventEntity : MongoEntity, IRuleEventEntity
{
private WebhookJob job;
[BsonRequired]
[BsonElement]
public Guid AppId { get; set; }
[BsonRequired]
[BsonElement]
public long Version { get; set; }
[BsonRequired]
[BsonElement]
public Uri RequestUrl { get; set; }
[BsonRequired]
[BsonElement]
public string RequestBody { get; set; }
[BsonRequired]
[BsonElement]
public string RequestSignature { get; set; }
public string EventName { get; set; }
[BsonRequired]
[BsonElement]
public string EventName { get; set; }
public string LastDump { get; set; }
[BsonRequired]
[BsonElement]
public string LastDump { get; set; }
public int NumCalls { get; set; }
[BsonRequired]
[BsonElement]
@ -57,23 +44,19 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Webhooks
[BsonRequired]
[BsonElement]
public int NumCalls { get; set; }
public bool IsSending { get; set; }
[BsonRequired]
[BsonElement]
public bool IsSending { get; set; }
public RuleResult Result { get; set; }
[BsonRequired]
[BsonElement]
public WebhookResult Result { get; set; }
public RuleJobResult JobResult { get; set; }
[BsonRequired]
[BsonElement]
public WebhookJobResult JobResult { get; set; }
public WebhookJob Job
{
get { return job ?? (job = SimpleMapper.Map(this, new WebhookJob())); }
}
[BsonJson]
public RuleJob Job { get; set; }
}
}

50
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,19 +12,21 @@ using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver;
using NodaTime;
using Squidex.Domain.Apps.Read.Webhooks;
using Squidex.Domain.Apps.Read.Webhooks.Repositories;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Read.Rules;
using Squidex.Domain.Apps.Read.Rules.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Read.MongoDb.Webhooks
namespace Squidex.Domain.Apps.Read.MongoDb.Rules
{
public sealed class MongoWebhookEventRepository : MongoRepositoryBase<MongoWebhookEventEntity>, IWebhookEventRepository
public sealed class MongoRuleEventRepository : MongoRepositoryBase<MongoRuleEventEntity>, IRuleEventRepository
{
private readonly IClock clock;
public MongoWebhookEventRepository(IMongoDatabase database, IClock clock)
public MongoRuleEventRepository(IMongoDatabase database, IClock clock)
: base(database)
{
Guard.NotNull(clock, nameof(clock));
@ -34,10 +36,10 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Webhooks
protected override string CollectionName()
{
return "WebhookEvents";
return "RuleEvents";
}
protected override Task SetupCollectionAsync(IMongoCollection<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,14 +47,14 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Webhooks
collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Expires), new CreateIndexOptions { ExpireAfter = TimeSpan.Zero }));
}
public Task QueryPendingAsync(Func<IWebhookEventEntity, Task> callback, CancellationToken cancellationToken = default(CancellationToken))
public Task QueryPendingAsync(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 =
await Collection.Find(x => x.AppId == appId).Skip(skip).Limit(take).SortByDescending(x => x.Created)
@ -61,7 +63,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Webhooks
return webhookEventEntities;
}
public async Task<IWebhookEventEntity> FindAsync(Guid id)
public async Task<IRuleEventEntity> FindAsync(Guid id)
{
var webhookEventEntity =
await Collection.Find(x => x.Id == id)
@ -80,33 +82,33 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Webhooks
return Collection.UpdateOneAsync(x => x.Id == id, Update.Set(x => x.NextAttempt, nextAttempt));
}
public Task TraceSendingAsync(Guid jobId)
public Task EnqueueAsync(RuleJob job, Instant nextAttempt)
{
return Collection.UpdateOneAsync(x => x.Id == jobId, Update.Set(x => x.IsSending, true));
var entity = SimpleMapper.Map(job, new MongoRuleEventEntity { Created = clock.GetCurrentInstant(), NextAttempt = nextAttempt });
return Collection.InsertOneIfNotExistsAsync(entity);
}
public Task EnqueueAsync(WebhookJob job, Instant nextAttempt)
public Task MarkSendingAsync(Guid jobId)
{
var entity = SimpleMapper.Map(job, new MongoWebhookEventEntity { Created = clock.GetCurrentInstant(), NextAttempt = nextAttempt });
return Collection.InsertOneIfNotExistsAsync(entity);
return Collection.UpdateOneAsync(x => x.Id == jobId, Update.Set(x => x.IsSending, true));
}
public Task TraceSentAsync(Guid jobId, string dump, WebhookResult result, TimeSpan elapsed, Instant? nextAttempt)
public Task MarkSentAsync(Guid jobId, string dump, RuleResult result, TimeSpan elapsed, Instant? nextAttempt)
{
WebhookJobResult jobResult;
RuleJobResult jobResult;
if (result != WebhookResult.Success && nextAttempt == null)
if (result != RuleResult.Success && nextAttempt == null)
{
jobResult = WebhookJobResult.Failed;
jobResult = RuleJobResult.Failed;
}
else if (result != WebhookResult.Success && nextAttempt.HasValue)
else if (result != RuleResult.Success && nextAttempt.HasValue)
{
jobResult = WebhookJobResult.Retry;
jobResult = RuleJobResult.Retry;
}
else
{
jobResult = WebhookJobResult.Success;
jobResult = RuleJobResult.Success;
}
return Collection.UpdateOneAsync(x => x.Id == jobId,

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>> inMemoryWebhooks;
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 inMemoryWebhooks.GetOrDefault(appId) ?? EmptyRules;
}
private async Task EnsureRulesLoadedAsync()
{
if (inMemoryWebhooks == null)
{
try
{
await lockObject.WaitAsync();
if (inMemoryWebhooks == null)
{
inMemoryWebhooks = new Dictionary<Guid, List<IRuleEntity>>();
var webhooks =
await Collection.Find(new BsonDocument())
.ToListAsync();
foreach (var webhook in webhooks)
{
inMemoryWebhooks.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 "^rules-"; }
}
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);
inMemoryWebhooks.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id);
inMemoryWebhooks.GetOrAddNew(w.AppId).Add(w);
});
}
protected async Task On(RuleUpdated @event, EnvelopeHeaders headers)
{
await EnsureRulesLoadedAsync();
await Collection.UpdateAsync(@event, headers, w =>
{
w.Rule.Apply(@event);
inMemoryWebhooks.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id);
inMemoryWebhooks.GetOrAddNew(w.AppId).Add(w);
});
}
protected async Task On(RuleEnabled @event, EnvelopeHeaders headers)
{
await EnsureRulesLoadedAsync();
await Collection.UpdateAsync(@event, headers, w =>
{
w.Rule.Apply(@event);
inMemoryWebhooks.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id);
inMemoryWebhooks.GetOrAddNew(w.AppId).Add(w);
});
}
protected async Task On(RuleDisabled @event, EnvelopeHeaders headers)
{
await EnsureRulesLoadedAsync();
await Collection.UpdateAsync(@event, headers, w =>
{
w.Rule.Apply(@event);
inMemoryWebhooks.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id);
inMemoryWebhooks.GetOrAddNew(w.AppId).Add(w);
});
}
protected async Task On(RuleDeleted @event, EnvelopeHeaders headers)
{
await EnsureRulesLoadedAsync();
inMemoryWebhooks.GetOrAddNew(@event.AppId.Id).RemoveAll(x => x.Id == @event.RuleId);
await Collection.DeleteManyAsync(x => x.Id == @event.RuleId);
}
}
}

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;

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

@ -8,6 +8,7 @@
using NodaTime;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules;
namespace Squidex.Domain.Apps.Read.Rules
{

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

@ -24,7 +24,7 @@ namespace Squidex.Domain.Apps.Read.Rules.Repositories
Task MarkSendingAsync(Guid jobId);
Task TraceSentAsync(Guid jobId, string dump, RuleResult result, TimeSpan elapsed, Instant? nextCall);
Task MarkSentAsync(Guid jobId, string dump, RuleResult result, TimeSpan elapsed, Instant? nextCall);
Task QueryPendingAsync(Func<IRuleEventEntity, Task> callback, CancellationToken cancellationToken = default(CancellationToken));

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

@ -110,7 +110,7 @@ namespace Squidex.Domain.Apps.Read.Rules
{
var job = @event.Job;
var response = await ruleService.InvokeAsync(job.ActionName, job.Details);
var response = await ruleService.InvokeAsync(job.ActionName, job.ActionData);
Instant? nextCall = null;
@ -133,7 +133,7 @@ namespace Squidex.Domain.Apps.Read.Rules
}
}
await ruleEventRepository.TraceSentAsync(@event.Id, response.Dump, response.Result, response.Elapsed, nextCall);
await ruleEventRepository.MarkSentAsync(@event.Id, response.Dump, response.Result, response.Elapsed, nextCall);
}
catch (Exception ex)
{

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

27
src/Squidex.Domain.Apps.Read/Webhooks/IWebhookEventEntity.cs

@ -1,27 +0,0 @@
// ==========================================================================
// IWebhookEventEntity.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using NodaTime;
namespace Squidex.Domain.Apps.Read.Webhooks
{
public interface IWebhookEventEntity : IEntity
{
WebhookJob Job { get; }
Instant? NextAttempt { get; }
WebhookResult Result { get; }
WebhookJobResult JobResult { get; }
int NumCalls { get; }
string LastDump { get; }
}
}

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

160
src/Squidex.Domain.Apps.Read/Webhooks/WebhookDequeuer.cs

@ -1,160 +0,0 @@
// ==========================================================================
// WebhookDequeuer.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using NodaTime;
using Squidex.Domain.Apps.Read.Webhooks.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Timers;
namespace Squidex.Domain.Apps.Read.Webhooks
{
public sealed class WebhookDequeuer : DisposableObjectBase, IExternalSystem
{
private readonly ActionBlock<IWebhookEventEntity> requestBlock;
private readonly TransformBlock<IWebhookEventEntity, IWebhookEventEntity> blockBlock;
private readonly IWebhookEventRepository webhookEventRepository;
private readonly IWebhookRepository webhookRepository;
private readonly WebhookSender webhookSender;
private readonly CompletionTimer timer;
private readonly ISemanticLog log;
private readonly IClock clock;
public WebhookDequeuer(WebhookSender webhookSender,
IWebhookEventRepository webhookEventRepository,
IWebhookRepository webhookRepository,
IClock clock,
ISemanticLog log)
{
Guard.NotNull(webhookEventRepository, nameof(webhookEventRepository));
Guard.NotNull(webhookRepository, nameof(webhookRepository));
Guard.NotNull(webhookSender, nameof(webhookSender));
Guard.NotNull(clock, nameof(clock));
Guard.NotNull(log, nameof(log));
this.webhookEventRepository = webhookEventRepository;
this.webhookRepository = webhookRepository;
this.webhookSender = webhookSender;
this.clock = clock;
this.log = log;
requestBlock =
new ActionBlock<IWebhookEventEntity>(MakeRequestAsync,
new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 32, BoundedCapacity = 32 });
blockBlock =
new TransformBlock<IWebhookEventEntity, IWebhookEventEntity>(x => BlockAsync(x),
new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 1, BoundedCapacity = 1 });
blockBlock.LinkTo(requestBlock, new DataflowLinkOptions { PropagateCompletion = true });
timer = new CompletionTimer(5000, QueryAsync);
}
protected override void DisposeObject(bool disposing)
{
if (disposing)
{
timer.StopAsync().Wait();
blockBlock.Complete();
requestBlock.Completion.Wait();
}
}
public void Connect()
{
}
public void Next()
{
timer.SkipCurrentDelay();
}
private async Task QueryAsync(CancellationToken cancellationToken)
{
try
{
await webhookEventRepository.QueryPendingAsync(blockBlock.SendAsync, cancellationToken);
}
catch (Exception ex)
{
log.LogError(ex, w => w
.WriteProperty("action", "QueueWebhookEvents")
.WriteProperty("status", "Failed"));
}
}
private async Task<IWebhookEventEntity> BlockAsync(IWebhookEventEntity @event)
{
try
{
await webhookEventRepository.TraceSendingAsync(@event.Id);
return @event;
}
catch (Exception ex)
{
log.LogError(ex, w => w
.WriteProperty("action", "BlockWebhookEvent")
.WriteProperty("status", "Failed"));
throw;
}
}
private async Task MakeRequestAsync(IWebhookEventEntity @event)
{
try
{
var response = await webhookSender.SendAsync(@event.Job);
Instant? nextCall = null;
if (response.Result != WebhookResult.Success)
{
var now = clock.GetCurrentInstant();
switch (@event.NumCalls)
{
case 0:
nextCall = now.Plus(Duration.FromMinutes(5));
break;
case 1:
nextCall = now.Plus(Duration.FromHours(1));
break;
case 2:
nextCall = now.Plus(Duration.FromHours(5));
break;
case 3:
nextCall = now.Plus(Duration.FromHours(6));
break;
}
}
await Task.WhenAll(
webhookRepository.TraceSentAsync(@event.Job.WebhookId, response.Result, response.Elapsed),
webhookEventRepository.TraceSentAsync(@event.Id, response.Dump, response.Result, response.Elapsed, nextCall));
}
catch (Exception ex)
{
log.LogError(ex, w => w
.WriteProperty("action", "SendWebhookEvent")
.WriteProperty("status", "Failed"));
throw;
}
}
}
}

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

32
src/Squidex.Domain.Apps.Read/Webhooks/WebhookJob.cs

@ -1,32 +0,0 @@
// ==========================================================================
// WebhookJob.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using NodaTime;
namespace Squidex.Domain.Apps.Read.Webhooks
{
public sealed class WebhookJob
{
public Guid Id { get; set; }
public Guid AppId { get; set; }
public Guid WebhookId { get; set; }
public Uri RequestUrl { get; set; }
public string RequestBody { get; set; }
public string RequestSignature { get; set; }
public string EventName { get; set; }
public Instant Expires { get; set; }
}
}

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

6
src/Squidex.Domain.Apps.Write/Webhooks/Commands/UpdateWebhook.cs → src/Squidex.Domain.Apps.Write/Rules/Commands/CreateRule.cs

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

6
src/Squidex.Domain.Apps.Write/Webhooks/Commands/DeleteWebhook.cs → src/Squidex.Domain.Apps.Write/Rules/Commands/DeleteRule.cs

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

10
src/Squidex.Domain.Apps.Read/Webhooks/WebhookResult.cs → src/Squidex.Domain.Apps.Write/Rules/Commands/DisableRule.cs

@ -1,18 +1,14 @@
// ==========================================================================
// WebhookResult.cs
// DisableRule.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Read.Webhooks
namespace Squidex.Domain.Apps.Write.Rules.Commands
{
public enum WebhookResult
public sealed class DisableRule : RuleAggregateCommand
{
Pending,
Success,
Failed,
Timeout
}
}

10
src/Squidex.Domain.Apps.Read/Webhooks/WebhookJobResult.cs → src/Squidex.Domain.Apps.Write/Rules/Commands/EnableRule.cs

@ -1,18 +1,14 @@
// ==========================================================================
// WebhookJobResult.cs
// EnableRule.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Read.Webhooks
namespace Squidex.Domain.Apps.Write.Rules.Commands
{
public enum WebhookJobResult
public sealed class EnableRule : RuleAggregateCommand
{
Pending,
Success,
Retry,
Failed
}
}

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

12
src/Squidex.Domain.Apps.Events/Webhooks/WebhookEvent.cs → src/Squidex.Domain.Apps.Write/Rules/Commands/RuleEditCommand.cs

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

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

23
src/Squidex.Domain.Apps.Write/Webhooks/Commands/CreateWebhook.cs

@ -1,23 +0,0 @@
// ==========================================================================
// CreateWebhook.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Write.Webhooks.Commands
{
public sealed class CreateWebhook : WebhookEditCommand
{
public string SharedSecret { get; } = RandomHash.New();
public CreateWebhook()
{
WebhookId = Guid.NewGuid();
}
}
}

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

2
src/Squidex/Squidex.csproj

@ -66,7 +66,7 @@
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="2.0.0" />
<PackageReference Include="Microsoft.Data.Edm" Version="5.8.3" />
<PackageReference Include="Microsoft.Data.Edm" Version="5.8.2" />
<PackageReference Include="Microsoft.OData.Core" Version="7.3.1" />
<PackageReference Include="MongoDB.Driver" Version="2.4.4" />
<PackageReference Include="NJsonSchema" Version="9.8.3" />

48
tests/Squidex.Domain.Apps.Write.Tests/Rules/Guards/Actions/WebhookActionTests.cs

@ -0,0 +1,48 @@
// ==========================================================================
// WebhookActionTests.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Rules.Actions;
using Xunit;
namespace Squidex.Domain.Apps.Write.Rules.Guards.Actions
{
public sealed class WebhookActionTests
{
[Fact]
public async Task Should_add_error_if_url_is_null()
{
var action = new WebhookAction { Url = null };
var errors = await RuleActionValidator.ValidateAsync(action);
Assert.NotEmpty(errors);
}
[Fact]
public async Task Should_add_error_if_url_is_relative()
{
var action = new WebhookAction { Url = new Uri("/invalid", UriKind.Relative) };
var errors = await RuleActionValidator.ValidateAsync(action);
Assert.NotEmpty(errors);
}
[Fact]
public async Task Should_not_add_error_if_url_is_absolute()
{
var action = new WebhookAction { Url = new Uri("https://squidex.io", UriKind.Absolute) };
var errors = await RuleActionValidator.ValidateAsync(action);
Assert.Empty(errors);
}
}
}

158
tests/Squidex.Domain.Apps.Write.Tests/Rules/Guards/GuardRuleTests.cs

@ -0,0 +1,158 @@
// ==========================================================================
// GuardRuleTests.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.Actions;
using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Read.Schemas;
using Squidex.Domain.Apps.Read.Schemas.Services;
using Squidex.Domain.Apps.Write.Rules.Commands;
using Squidex.Infrastructure;
using Xunit;
namespace Squidex.Domain.Apps.Write.Rules.Guards
{
public class GuardRuleTests
{
private readonly Uri validUrl = new Uri("https://squidex.io");
private readonly Rule rule = new Rule(new ContentChangedTrigger(), new WebhookAction());
private readonly ISchemaProvider schemas = A.Fake<ISchemaProvider>();
public GuardRuleTests()
{
A.CallTo(() => schemas.FindSchemaByIdAsync(A<Guid>.Ignored, false))
.Returns(A.Fake<ISchemaEntity>());
}
[Fact]
public async Task CanCreate_should_throw_exception_if_trigger_null()
{
var command = new CreateRule
{
Trigger = null,
Action = new WebhookAction
{
Url = validUrl
}
};
await Assert.ThrowsAsync<ValidationException>(() => GuardRule.CanCreate(command, schemas));
}
[Fact]
public async Task CanCreate_should_throw_exception_if_action_null()
{
var command = new CreateRule
{
Trigger = new ContentChangedTrigger
{
Schemas = new List<ContentChangedTriggerSchema>()
},
Action = null
};
await Assert.ThrowsAsync<ValidationException>(() => GuardRule.CanCreate(command, schemas));
}
[Fact]
public async Task CanCreate_should_not_throw_exception_if_trigger_and_action_valid()
{
var command = new CreateRule
{
Trigger = new ContentChangedTrigger
{
Schemas = new List<ContentChangedTriggerSchema>()
},
Action = new WebhookAction
{
Url = validUrl
}
};
await GuardRule.CanCreate(command, schemas);
}
[Fact]
public async Task CanUpdate_should_throw_exception_if_action_and_trigger_are_null()
{
var command = new UpdateRule();
await Assert.ThrowsAsync<ValidationException>(() => GuardRule.CanUpdate(command, schemas));
}
[Fact]
public async Task CanUpdate_should_not_throw_exception_if_trigger_and_action_valid()
{
var command = new UpdateRule
{
Trigger = new ContentChangedTrigger
{
Schemas = new List<ContentChangedTriggerSchema>()
},
Action = new WebhookAction
{
Url = validUrl
}
};
await GuardRule.CanUpdate(command, schemas);
}
[Fact]
public void CanEnable_should_throw_exception_if_rule_enabled()
{
var command = new EnableRule();
rule.Enable();
Assert.Throws<ValidationException>(() => GuardRule.CanEnable(command, rule));
}
[Fact]
public void CanEnable_should_not_throw_exception_if_rule_disabled()
{
var command = new EnableRule();
rule.Disable();
GuardRule.CanEnable(command, rule);
}
[Fact]
public void CanDisable_should_throw_exception_if_rule_disabled()
{
var command = new DisableRule();
rule.Disable();
Assert.Throws<ValidationException>(() => GuardRule.CanDisable(command, rule));
}
[Fact]
public void CanDisable_should_not_throw_exception_if_rule_enabled()
{
var command = new DisableRule();
rule.Enable();
GuardRule.CanDisable(command, rule);
}
[Fact]
public void CanDelete_should_not_throw_exception()
{
var command = new DeleteRule();
GuardRule.CanDelete(command);
}
}
}

85
tests/Squidex.Domain.Apps.Write.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs

@ -0,0 +1,85 @@
// ==========================================================================
// ContentChangedTriggerTests.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Read.Schemas;
using Squidex.Domain.Apps.Read.Schemas.Services;
using Xunit;
namespace Squidex.Domain.Apps.Write.Rules.Guards.Triggers
{
public class ContentChangedTriggerTests
{
private readonly ISchemaProvider schemas = A.Fake<ISchemaProvider>();
[Fact]
public async Task Should_add_error_if_schemas_ids_are_not_valid()
{
A.CallTo(() => schemas.FindSchemaByIdAsync(A<Guid>.Ignored, false))
.Returns(Task.FromResult<ISchemaEntity>(null));
var trigger = new ContentChangedTrigger
{
Schemas = new List<ContentChangedTriggerSchema>
{
new ContentChangedTriggerSchema()
}
};
var errors = await RuleTriggerValidator.ValidateAsync(trigger, schemas);
Assert.NotEmpty(errors);
}
[Fact]
public async Task Should_not_add_error_if_schemas_is_null()
{
var trigger = new ContentChangedTrigger();
var errors = await RuleTriggerValidator.ValidateAsync(trigger, schemas);
Assert.Empty(errors);
}
[Fact]
public async Task Should_not_add_error_if_schemas_is_empty()
{
var trigger = new ContentChangedTrigger
{
Schemas = new List<ContentChangedTriggerSchema>()
};
var errors = await RuleTriggerValidator.ValidateAsync(trigger, schemas);
Assert.Empty(errors);
}
[Fact]
public async Task Should_not_add_error_if_schemas_ids_are_valid()
{
A.CallTo(() => schemas.FindSchemaByIdAsync(A<Guid>.Ignored, false))
.Returns(A.Fake<ISchemaEntity>());
var trigger = new ContentChangedTrigger
{
Schemas = new List<ContentChangedTriggerSchema>
{
new ContentChangedTriggerSchema()
}
};
var errors = await RuleTriggerValidator.ValidateAsync(trigger, schemas);
Assert.Empty(errors);
}
}
}

117
tests/Squidex.Domain.Apps.Write.Tests/Rules/RuleCommandMiddlewareTests.cs

@ -0,0 +1,117 @@
// ==========================================================================
// RuleCommandMiddlewareTests.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.Actions;
using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Read.Schemas;
using Squidex.Domain.Apps.Read.Schemas.Services;
using Squidex.Domain.Apps.Write.Rules.Commands;
using Squidex.Domain.Apps.Write.TestHelpers;
using Squidex.Infrastructure.CQRS.Commands;
using Xunit;
namespace Squidex.Domain.Apps.Write.Rules
{
public class RuleCommandMiddlewareTests : HandlerTestBase<RuleDomainObject>
{
private readonly ISchemaProvider schemas = A.Fake<ISchemaProvider>();
private readonly RuleCommandMiddleware sut;
private readonly RuleDomainObject rule;
private readonly RuleTrigger ruleTrigger = new ContentChangedTrigger();
private readonly RuleAction ruleAction = new WebhookAction { Url = new Uri("https://squidex.io") };
private readonly Guid ruleId = Guid.NewGuid();
public RuleCommandMiddlewareTests()
{
A.CallTo(() => schemas.FindSchemaByIdAsync(A<Guid>.Ignored, false))
.Returns(A.Fake<ISchemaEntity>());
rule = new RuleDomainObject(ruleId, -1);
sut = new RuleCommandMiddleware(Handler, schemas);
}
[Fact]
public async Task Create_should_create_domain_object()
{
var context = CreateContextForCommand(new CreateRule { Trigger = ruleTrigger, Action = ruleAction });
await TestCreate(rule, async _ =>
{
await sut.HandleAsync(context);
});
}
[Fact]
public async Task Update_should_update_domain_object()
{
var context = CreateContextForCommand(new UpdateRule { Trigger = ruleTrigger, Action = ruleAction });
CreateRule();
await TestUpdate(rule, async _ =>
{
await sut.HandleAsync(context);
});
}
[Fact]
public async Task Enable_should_update_domain_object()
{
CreateRule();
DisableRule();
var command = CreateContextForCommand(new EnableRule { RuleId = ruleId });
await TestUpdate(rule, async _ =>
{
await sut.HandleAsync(command);
});
}
[Fact]
public async Task Disable_should_update_domain_object()
{
CreateRule();
var command = CreateContextForCommand(new DisableRule { RuleId = ruleId });
await TestUpdate(rule, async _ =>
{
await sut.HandleAsync(command);
});
}
[Fact]
public async Task Delete_should_update_domain_object()
{
CreateRule();
var command = CreateContextForCommand(new DeleteRule { RuleId = ruleId });
await TestUpdate(rule, async _ =>
{
await sut.HandleAsync(command);
});
}
private void DisableRule()
{
rule.Disable(new DisableRule());
}
private void CreateRule()
{
rule.Create(new CreateRule { Trigger = ruleTrigger, Action = ruleAction });
}
}
}

240
tests/Squidex.Domain.Apps.Write.Tests/Rules/RuleDomainObjectTests.cs

@ -0,0 +1,240 @@
// ==========================================================================
// RuleDomainObjectTests.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.Actions;
using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Events.Rules;
using Squidex.Domain.Apps.Write.Rules.Commands;
using Squidex.Domain.Apps.Write.TestHelpers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS;
using Xunit;
namespace Squidex.Domain.Apps.Write.Rules
{
public class RuleDomainObjectTests : HandlerTestBase<RuleDomainObject>
{
private readonly RuleTrigger ruleTrigger = new ContentChangedTrigger();
private readonly RuleAction ruleAction = new WebhookAction { Url = new Uri("https://squidex.io") };
private readonly RuleDomainObject sut;
public Guid RuleId { get; } = Guid.NewGuid();
public RuleDomainObjectTests()
{
sut = new RuleDomainObject(RuleId, 0);
}
[Fact]
public void Create_should_throw_exception_if_created()
{
sut.Create(new CreateRule { Trigger = ruleTrigger, Action = ruleAction });
Assert.Throws<DomainException>(() =>
{
sut.Create(CreateRuleCommand(new CreateRule { Trigger = ruleTrigger, Action = ruleAction }));
});
}
[Fact]
public void Create_should_create_events()
{
var command = new CreateRule { Trigger = ruleTrigger, Action = ruleAction };
sut.Create(CreateRuleCommand(command));
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateRuleEvent(new RuleCreated { Trigger = ruleTrigger, Action = ruleAction })
);
}
[Fact]
public void Update_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.Update(CreateRuleCommand(new UpdateRule { Trigger = ruleTrigger, Action = ruleAction }));
});
}
[Fact]
public void Update_should_throw_exception_if_rule_is_deleted()
{
CreateRule();
DeleteRule();
Assert.Throws<DomainException>(() =>
{
sut.Update(CreateRuleCommand(new UpdateRule { Trigger = ruleTrigger, Action = ruleAction }));
});
}
[Fact]
public void Update_should_create_events()
{
var newTrigger = new ContentChangedTrigger
{
Schemas = new List<ContentChangedTriggerSchema>()
};
var newAction = new WebhookAction
{
Url = new Uri("https://squidex.io/v2")
};
CreateRule();
var command = new UpdateRule { Trigger = newTrigger, Action = newAction };
sut.Update(CreateRuleCommand(command));
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateRuleEvent(new RuleUpdated { Trigger = newTrigger, Action = newAction })
);
}
[Fact]
public void Enable_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.Enable(CreateRuleCommand(new EnableRule()));
});
}
[Fact]
public void Enable_should_throw_exception_if_rule_is_deleted()
{
CreateRule();
DeleteRule();
Assert.Throws<DomainException>(() =>
{
sut.Enable(CreateRuleCommand(new EnableRule()));
});
}
[Fact]
public void Enable_should_create_events()
{
CreateRule();
var command = new EnableRule();
sut.Enable(CreateRuleCommand(command));
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateRuleEvent(new RuleEnabled())
);
}
[Fact]
public void Disable_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.Disable(CreateRuleCommand(new DisableRule()));
});
}
[Fact]
public void Disable_should_throw_exception_if_rule_is_deleted()
{
CreateRule();
DeleteRule();
Assert.Throws<DomainException>(() =>
{
sut.Disable(CreateRuleCommand(new DisableRule()));
});
}
[Fact]
public void Disable_should_create_events()
{
CreateRule();
var command = new DisableRule();
sut.Disable(CreateRuleCommand(command));
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateRuleEvent(new RuleDisabled())
);
}
[Fact]
public void Delete_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.Delete(CreateRuleCommand(new DeleteRule()));
});
}
[Fact]
public void Delete_should_throw_exception_if_already_deleted()
{
CreateRule();
DeleteRule();
Assert.Throws<DomainException>(() =>
{
sut.Delete(CreateRuleCommand(new DeleteRule()));
});
}
[Fact]
public void Delete_should_update_create_events()
{
CreateRule();
sut.Delete(CreateRuleCommand(new DeleteRule()));
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateRuleEvent(new RuleDeleted())
);
}
private void CreateRule()
{
sut.Create(CreateRuleCommand(new CreateRule { Trigger = ruleTrigger, Action = ruleAction }));
((IAggregate)sut).ClearUncommittedEvents();
}
private void DeleteRule()
{
sut.Delete(CreateRuleCommand(new DeleteRule()));
((IAggregate)sut).ClearUncommittedEvents();
}
protected T CreateRuleEvent<T>(T @event) where T : RuleEvent
{
@event.RuleId = RuleId;
return CreateEvent(@event);
}
protected T CreateRuleCommand<T>(T command) where T : RuleAggregateCommand
{
command.RuleId = RuleId;
return CreateCommand(command);
}
}
}

138
tests/Squidex.Domain.Apps.Write.Tests/Webhooks/Guards/GuardWebhookTests.cs

@ -1,138 +0,0 @@
// ==========================================================================
// GuardWebhookTests.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Domain.Apps.Core.Webhooks;
using Squidex.Domain.Apps.Read.Schemas;
using Squidex.Domain.Apps.Read.Schemas.Services;
using Squidex.Domain.Apps.Write.Webhooks.Commands;
using Squidex.Infrastructure;
using Xunit;
namespace Squidex.Domain.Apps.Write.Webhooks.Guards
{
public class GuardWebhookTests
{
private readonly ISchemaProvider schemas = A.Fake<ISchemaProvider>();
public GuardWebhookTests()
{
A.CallTo(() => schemas.FindSchemaByIdAsync(A<Guid>.Ignored, false))
.Returns(A.Fake<ISchemaEntity>());
}
[Fact]
public async Task CanCreate_should_throw_exception_if_url_defined()
{
var command = new CreateWebhook();
await Assert.ThrowsAsync<ValidationException>(() => GuardWebhook.CanCreate(command, schemas));
}
[Fact]
public async Task CanCreate_should_throw_exception_if_url_not_valid()
{
var command = new CreateWebhook { Url = new Uri("/invalid", UriKind.Relative) };
await Assert.ThrowsAsync<ValidationException>(() => GuardWebhook.CanCreate(command, schemas));
}
[Fact]
public async Task CanCreate_should_throw_exception_if_schema_id_not_found()
{
A.CallTo(() => schemas.FindSchemaByIdAsync(A<Guid>.Ignored, false))
.Returns(Task.FromResult<ISchemaEntity>(null));
var command = new CreateWebhook
{
Schemas = new List<WebhookSchema>
{
new WebhookSchema()
},
Url = new Uri("/invalid", UriKind.Relative)
};
await Assert.ThrowsAsync<ValidationException>(() => GuardWebhook.CanCreate(command, schemas));
}
[Fact]
public async Task CanCreate_should_not_throw_exception_if_schema_id_found()
{
var command = new CreateWebhook
{
Schemas = new List<WebhookSchema>
{
new WebhookSchema()
},
Url = new Uri("/invalid", UriKind.Relative)
};
await Assert.ThrowsAsync<ValidationException>(() => GuardWebhook.CanCreate(command, schemas));
}
[Fact]
public async Task CanUpdate_should_throw_exception_if_url_not_defined()
{
var command = new UpdateWebhook();
await Assert.ThrowsAsync<ValidationException>(() => GuardWebhook.CanUpdate(command, schemas));
}
[Fact]
public async Task CanUpdate_should_throw_exception_if_url_not_valid()
{
var command = new UpdateWebhook { Url = new Uri("/invalid", UriKind.Relative) };
await Assert.ThrowsAsync<ValidationException>(() => GuardWebhook.CanUpdate(command, schemas));
}
[Fact]
public async Task CanUpdate_should_throw_exception_if_schema_id_not_found()
{
A.CallTo(() => schemas.FindSchemaByIdAsync(A<Guid>.Ignored, false))
.Returns(Task.FromResult<ISchemaEntity>(null));
var command = new UpdateWebhook
{
Schemas = new List<WebhookSchema>
{
new WebhookSchema()
},
Url = new Uri("/invalid", UriKind.Relative)
};
await Assert.ThrowsAsync<ValidationException>(() => GuardWebhook.CanUpdate(command, schemas));
}
[Fact]
public async Task CanUpdate_should_not_throw_exception_if_schema_id_found()
{
var command = new UpdateWebhook
{
Schemas = new List<WebhookSchema>
{
new WebhookSchema()
},
Url = new Uri("/invalid", UriKind.Relative)
};
await Assert.ThrowsAsync<ValidationException>(() => GuardWebhook.CanUpdate(command, schemas));
}
[Fact]
public void CanDelete_should_not_throw_exception()
{
var command = new DeleteWebhook();
GuardWebhook.CanDelete(command);
}
}
}

115
tests/Squidex.Domain.Apps.Write.Tests/Webhooks/WebhookCommandMiddlewareTests.cs

@ -1,115 +0,0 @@
// ==========================================================================
// WebhookCommandMiddlewareTests.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Domain.Apps.Core.Webhooks;
using Squidex.Domain.Apps.Read.Schemas;
using Squidex.Domain.Apps.Read.Schemas.Services;
using Squidex.Domain.Apps.Write.TestHelpers;
using Squidex.Domain.Apps.Write.Webhooks.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Commands;
using Xunit;
namespace Squidex.Domain.Apps.Write.Webhooks
{
public class WebhookCommandMiddlewareTests : HandlerTestBase<WebhookDomainObject>
{
private readonly ISchemaProvider schemas = A.Fake<ISchemaProvider>();
private readonly WebhookCommandMiddleware sut;
private readonly WebhookDomainObject webhook;
private readonly Uri url = new Uri("http://squidex.io");
private readonly Guid schemaId = Guid.NewGuid();
private readonly Guid webhookId = Guid.NewGuid();
private readonly List<WebhookSchema> webhookSchemas;
public WebhookCommandMiddlewareTests()
{
A.CallTo(() => schemas.FindSchemaByIdAsync(schemaId, false))
.Returns(A.Fake<ISchemaEntity>());
webhook = new WebhookDomainObject(webhookId, -1);
webhookSchemas = new List<WebhookSchema>
{
new WebhookSchema { SchemaId = schemaId }
};
sut = new WebhookCommandMiddleware(Handler, schemas);
}
[Fact]
public async Task Create_should_create_domain_object()
{
var context = CreateContextForCommand(new CreateWebhook { Schemas = webhookSchemas, Url = url, WebhookId = webhookId });
await TestCreate(webhook, async _ =>
{
await sut.HandleAsync(context);
});
A.CallTo(() => schemas.FindSchemaByIdAsync(schemaId, false)).MustHaveHappened();
}
[Fact]
public async Task Update_should_update_domain_object()
{
var context = CreateContextForCommand(new UpdateWebhook { Schemas = webhookSchemas, Url = url, WebhookId = webhookId });
A.CallTo(() => schemas.FindSchemaByIdAsync(schemaId, false)).Returns(A.Fake<ISchemaEntity>());
CreateWebhook();
await TestUpdate(webhook, async _ =>
{
await sut.HandleAsync(context);
});
A.CallTo(() => schemas.FindSchemaByIdAsync(schemaId, false)).MustHaveHappened();
}
[Fact]
public async Task Update_should_throw_exception_when_schema_is_not_found()
{
var context = CreateContextForCommand(new UpdateWebhook { Schemas = webhookSchemas, Url = url, WebhookId = webhookId });
A.CallTo(() => schemas.FindSchemaByIdAsync(schemaId, false)).Returns((ISchemaEntity)null);
CreateWebhook();
await Assert.ThrowsAsync<ValidationException>(async () =>
{
await TestCreate(webhook, async _ =>
{
await sut.HandleAsync(context);
});
});
}
[Fact]
public async Task Delete_should_update_domain_object()
{
CreateWebhook();
var command = CreateContextForCommand(new DeleteWebhook { WebhookId = webhookId });
await TestUpdate(webhook, async _ =>
{
await sut.HandleAsync(command);
});
}
private void CreateWebhook()
{
webhook.Create(new CreateWebhook { Url = url });
}
}
}

159
tests/Squidex.Domain.Apps.Write.Tests/Webhooks/WebhookDomainObjectTests.cs

@ -1,159 +0,0 @@
// ==========================================================================
// WebhookDomainObjectTests.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Squidex.Domain.Apps.Events.Webhooks;
using Squidex.Domain.Apps.Write.TestHelpers;
using Squidex.Domain.Apps.Write.Webhooks.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS;
using Xunit;
namespace Squidex.Domain.Apps.Write.Webhooks
{
public class WebhookDomainObjectTests : HandlerTestBase<WebhookDomainObject>
{
private readonly Uri url = new Uri("http://squidex.io");
private readonly WebhookDomainObject sut;
public Guid WebhookId { get; } = Guid.NewGuid();
public WebhookDomainObjectTests()
{
sut = new WebhookDomainObject(WebhookId, 0);
}
[Fact]
public void Create_should_throw_exception_if_created()
{
sut.Create(new CreateWebhook { Url = url });
Assert.Throws<DomainException>(() =>
{
sut.Create(CreateWebhookCommand(new CreateWebhook { Url = url }));
});
}
[Fact]
public void Create_should_create_events()
{
var command = new CreateWebhook { Url = url };
sut.Create(CreateWebhookCommand(command));
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateWebhookEvent(new WebhookCreated
{
Url = url,
Schemas = command.Schemas,
SharedSecret = command.SharedSecret,
WebhookId = command.WebhookId
})
);
}
[Fact]
public void Update_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.Update(CreateWebhookCommand(new UpdateWebhook { Url = url }));
});
}
[Fact]
public void Update_should_throw_exception_if_webhook_is_deleted()
{
CreateWebhook();
DeleteWebhook();
Assert.Throws<DomainException>(() =>
{
sut.Update(CreateWebhookCommand(new UpdateWebhook { Url = url }));
});
}
[Fact]
public void Update_should_create_events()
{
CreateWebhook();
var command = new UpdateWebhook { Url = url };
sut.Update(CreateWebhookCommand(command));
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateWebhookEvent(new WebhookUpdated { Url = url, Schemas = command.Schemas })
);
}
[Fact]
public void Delete_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.Delete(CreateWebhookCommand(new DeleteWebhook()));
});
}
[Fact]
public void Delete_should_throw_exception_if_already_deleted()
{
CreateWebhook();
DeleteWebhook();
Assert.Throws<DomainException>(() =>
{
sut.Delete(CreateWebhookCommand(new DeleteWebhook()));
});
}
[Fact]
public void Delete_should_update_properties_create_events()
{
CreateWebhook();
sut.Delete(CreateWebhookCommand(new DeleteWebhook()));
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateWebhookEvent(new WebhookDeleted())
);
}
private void CreateWebhook()
{
sut.Create(CreateWebhookCommand(new CreateWebhook { Url = url }));
((IAggregate)sut).ClearUncommittedEvents();
}
private void DeleteWebhook()
{
sut.Delete(CreateWebhookCommand(new DeleteWebhook()));
((IAggregate)sut).ClearUncommittedEvents();
}
protected T CreateWebhookEvent<T>(T @event) where T : WebhookEvent
{
@event.WebhookId = WebhookId;
return CreateEvent(@event);
}
protected T CreateWebhookCommand<T>(T command) where T : WebhookAggregateCommand
{
command.WebhookId = WebhookId;
return CreateCommand(command);
}
}
}
Loading…
Cancel
Save