From e79d73afb0461816e180044d5a31fcbf4d2cd209 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 11 Jun 2017 20:37:51 +0200 Subject: [PATCH] Webhook Read Model and Invoker --- .../CQRS/Events/CompoundEventConsumer.cs | 19 +-- .../Apps/MongoAppEntity.cs | 12 +- ...lientEntity.cs => MongoAppEntityClient.cs} | 4 +- ...Entity.cs => MongoAppEntityContributor.cs} | 4 +- ...pLanguage.cs => MongoAppEntityLanguage.cs} | 4 +- .../Apps/MongoAppRepository_EventHandling.cs | 2 +- .../Schemas/MongoSchemaEntity.cs | 10 ++ .../Schemas/MongoSchemaEntityWebhook.cs | 29 +++++ .../MongoSchemaRepository_EventHandling.cs | 10 ++ .../Schemas/MongoSchemaWebhookEntity.cs | 35 +++++ .../Schemas/MongoSchemaWebhookRepository.cs | 121 ++++++++++++++++++ .../Implementations/CachingAppProvider.cs | 2 +- src/Squidex.Read/Schemas/ISchemaEntity.cs | 3 + .../Schemas/ISchemaWebhookEntity.cs | 21 +++ .../Repositories/ISchemaWebhookRepository.cs | 19 +++ .../Implementations/CachingSchemaProvider.cs | 10 +- src/Squidex.Read/Schemas/WebhookInvoker.cs | 119 +++++++++++++++++ src/Squidex/Config/Domain/ReadModule.cs | 4 + .../Config/Domain/StoreMongoDbModule.cs | 16 +++ src/Squidex/Config/Domain/WriteModule.cs | 4 - .../CQRS/Events/CompoundEventConsumerTests.cs | 28 ++-- .../Apps/AppCommandHandlerTests.cs | 1 - 22 files changed, 435 insertions(+), 42 deletions(-) rename src/Squidex.Read.MongoDb/Apps/{MongoAppClientEntity.cs => MongoAppEntityClient.cs} (90%) rename src/Squidex.Read.MongoDb/Apps/{MongoAppContributorEntity.cs => MongoAppEntityContributor.cs} (87%) rename src/Squidex.Read.MongoDb/Apps/{MongoAppLanguage.cs => MongoAppEntityLanguage.cs} (90%) create mode 100644 src/Squidex.Read.MongoDb/Schemas/MongoSchemaEntityWebhook.cs create mode 100644 src/Squidex.Read.MongoDb/Schemas/MongoSchemaWebhookEntity.cs create mode 100644 src/Squidex.Read.MongoDb/Schemas/MongoSchemaWebhookRepository.cs create mode 100644 src/Squidex.Read/Schemas/ISchemaWebhookEntity.cs create mode 100644 src/Squidex.Read/Schemas/Repositories/ISchemaWebhookRepository.cs create mode 100644 src/Squidex.Read/Schemas/WebhookInvoker.cs diff --git a/src/Squidex.Infrastructure/CQRS/Events/CompoundEventConsumer.cs b/src/Squidex.Infrastructure/CQRS/Events/CompoundEventConsumer.cs index 7a792650e..93be936c1 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/CompoundEventConsumer.cs +++ b/src/Squidex.Infrastructure/CQRS/Events/CompoundEventConsumer.cs @@ -17,29 +17,24 @@ namespace Squidex.Infrastructure.CQRS.Events public string Name { get; } - public string EventsFilter - { - get { return inners.FirstOrDefault()?.EventsFilter; } - } + public string EventsFilter { get; } public CompoundEventConsumer(IEventConsumer first, params IEventConsumer[] inners) + : this(first?.Name, first, inners) { - Guard.NotNull(first, nameof(first)); - Guard.NotNull(inners, nameof(inners)); - - this.inners = new[] { first }.Union(inners).ToArray(); - - Name = first.Name; } - public CompoundEventConsumer(string name, params IEventConsumer[] inners) + public CompoundEventConsumer(string name, IEventConsumer first, params IEventConsumer[] inners) { + Guard.NotNull(first, nameof(first)); Guard.NotNull(inners, nameof(inners)); Guard.NotNullOrEmpty(name, nameof(name)); - this.inners = inners; + this.inners = new[] { first }.Union(inners).ToArray(); Name = name; + + EventsFilter = string.Join("|", this.inners.Where(x => !string.IsNullOrWhiteSpace(x.EventsFilter)).Select(x => $"({x.EventsFilter})")); } public Task ClearAsync() diff --git a/src/Squidex.Read.MongoDb/Apps/MongoAppEntity.cs b/src/Squidex.Read.MongoDb/Apps/MongoAppEntity.cs index 62dd7c6fc..a7af7ac42 100644 --- a/src/Squidex.Read.MongoDb/Apps/MongoAppEntity.cs +++ b/src/Squidex.Read.MongoDb/Apps/MongoAppEntity.cs @@ -46,15 +46,15 @@ namespace Squidex.Read.MongoDb.Apps [BsonRequired] [BsonElement] - public List Languages { get; set; } = new List(); + public List Languages { get; set; } = new List(); [BsonRequired] [BsonElement] - public Dictionary Clients { get; set; } = new Dictionary(); + public Dictionary Clients { get; set; } = new Dictionary(); [BsonRequired] [BsonElement] - public Dictionary Contributors { get; set; } = new Dictionary(); + public Dictionary Contributors { get; set; } = new Dictionary(); public PartitionResolver PartitionResolver { @@ -101,12 +101,12 @@ namespace Squidex.Read.MongoDb.Apps return languagesConfig; } - private static MongoAppLanguage FromLanguageConfig(LanguageConfig l) + private static MongoAppEntityLanguage FromLanguageConfig(LanguageConfig l) { - return new MongoAppLanguage { Iso2Code = l.Language, IsOptional = l.IsOptional, Fallback = l.Fallback.Select(x => x.Iso2Code).ToList() }; + return new MongoAppEntityLanguage { Iso2Code = l.Language, IsOptional = l.IsOptional, Fallback = l.Fallback.Select(x => x.Iso2Code).ToList() }; } - private static LanguageConfig ToLanguageConfig(MongoAppLanguage l) + private static LanguageConfig ToLanguageConfig(MongoAppEntityLanguage l) { return new LanguageConfig(l.Iso2Code, l.IsOptional, l.Fallback?.Select(f => f)); } diff --git a/src/Squidex.Read.MongoDb/Apps/MongoAppClientEntity.cs b/src/Squidex.Read.MongoDb/Apps/MongoAppEntityClient.cs similarity index 90% rename from src/Squidex.Read.MongoDb/Apps/MongoAppClientEntity.cs rename to src/Squidex.Read.MongoDb/Apps/MongoAppEntityClient.cs index be38a14f5..465b76d23 100644 --- a/src/Squidex.Read.MongoDb/Apps/MongoAppClientEntity.cs +++ b/src/Squidex.Read.MongoDb/Apps/MongoAppEntityClient.cs @@ -1,5 +1,5 @@ // ========================================================================== -// MongoAppClientEntity.cs +// MongoAppEntityClient.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -11,7 +11,7 @@ using Squidex.Read.Apps; namespace Squidex.Read.MongoDb.Apps { - public sealed class MongoAppClientEntity : IAppClientEntity + public sealed class MongoAppEntityClient : IAppClientEntity { [BsonRequired] [BsonElement] diff --git a/src/Squidex.Read.MongoDb/Apps/MongoAppContributorEntity.cs b/src/Squidex.Read.MongoDb/Apps/MongoAppEntityContributor.cs similarity index 87% rename from src/Squidex.Read.MongoDb/Apps/MongoAppContributorEntity.cs rename to src/Squidex.Read.MongoDb/Apps/MongoAppEntityContributor.cs index a0c09db0c..1de3d39a8 100644 --- a/src/Squidex.Read.MongoDb/Apps/MongoAppContributorEntity.cs +++ b/src/Squidex.Read.MongoDb/Apps/MongoAppEntityContributor.cs @@ -1,5 +1,5 @@ // ========================================================================== -// MongoAppContributorEntity.cs +// MongoAppEntityContributor.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -12,7 +12,7 @@ using Squidex.Read.Apps; namespace Squidex.Read.MongoDb.Apps { - public sealed class MongoAppContributorEntity : IAppContributorEntity + public sealed class MongoAppEntityContributor : IAppContributorEntity { [BsonRequired] [BsonElement] diff --git a/src/Squidex.Read.MongoDb/Apps/MongoAppLanguage.cs b/src/Squidex.Read.MongoDb/Apps/MongoAppEntityLanguage.cs similarity index 90% rename from src/Squidex.Read.MongoDb/Apps/MongoAppLanguage.cs rename to src/Squidex.Read.MongoDb/Apps/MongoAppEntityLanguage.cs index a4ecd3a23..86620b854 100644 --- a/src/Squidex.Read.MongoDb/Apps/MongoAppLanguage.cs +++ b/src/Squidex.Read.MongoDb/Apps/MongoAppEntityLanguage.cs @@ -1,5 +1,5 @@ // ========================================================================== -// MongoAppLanguage.cs +// MongoAppEntityLanguage.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -11,7 +11,7 @@ using MongoDB.Bson.Serialization.Attributes; namespace Squidex.Read.MongoDb.Apps { - public sealed class MongoAppLanguage + public sealed class MongoAppEntityLanguage { [BsonRequired] [BsonElement] diff --git a/src/Squidex.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs b/src/Squidex.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs index 88ab3ef63..d58345b93 100644 --- a/src/Squidex.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs +++ b/src/Squidex.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs @@ -53,7 +53,7 @@ namespace Squidex.Read.MongoDb.Apps { return Collection.UpdateAsync(@event, headers, a => { - a.Clients[@event.Id] = SimpleMapper.Map(@event, new MongoAppClientEntity()); + a.Clients[@event.Id] = SimpleMapper.Map(@event, new MongoAppEntityClient()); }); } diff --git a/src/Squidex.Read.MongoDb/Schemas/MongoSchemaEntity.cs b/src/Squidex.Read.MongoDb/Schemas/MongoSchemaEntity.cs index 232f98c74..f3b46b61d 100644 --- a/src/Squidex.Read.MongoDb/Schemas/MongoSchemaEntity.cs +++ b/src/Squidex.Read.MongoDb/Schemas/MongoSchemaEntity.cs @@ -7,6 +7,7 @@ // ========================================================================== using System; +using System.Collections.Generic; using MongoDB.Bson.Serialization.Attributes; using Newtonsoft.Json.Linq; using Squidex.Core.Schemas; @@ -53,6 +54,15 @@ namespace Squidex.Read.MongoDb.Schemas [BsonElement] public bool IsDeleted { get; set; } + [BsonRequired] + [BsonElement] + public List Webhooks { get; set; } = new List(); + + IEnumerable ISchemaEntity.Webhooks + { + get { return Webhooks; } + } + Schema ISchemaEntity.Schema { get { return schema.Value; } diff --git a/src/Squidex.Read.MongoDb/Schemas/MongoSchemaEntityWebhook.cs b/src/Squidex.Read.MongoDb/Schemas/MongoSchemaEntityWebhook.cs new file mode 100644 index 000000000..af16fd927 --- /dev/null +++ b/src/Squidex.Read.MongoDb/Schemas/MongoSchemaEntityWebhook.cs @@ -0,0 +1,29 @@ +// ========================================================================== +// MongoSchemaEntityWebhook.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using MongoDB.Bson.Serialization.Attributes; +using Squidex.Read.Schemas; + +namespace Squidex.Read.MongoDb.Schemas +{ + public sealed class MongoSchemaEntityWebhook : ISchemaWebhookEntity + { + [BsonRequired] + [BsonElement] + public Guid Id { get; set; } + + [BsonRequired] + [BsonElement] + public Uri Url { get; set; } + + [BsonRequired] + [BsonElement] + public string SecurityToken { get; set; } + } +} diff --git a/src/Squidex.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs b/src/Squidex.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs index 6136473bc..77d80cdd7 100644 --- a/src/Squidex.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs +++ b/src/Squidex.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs @@ -98,6 +98,16 @@ namespace Squidex.Read.MongoDb.Schemas return UpdateSchema(@event, headers, s => SchemaEventDispatcher.Dispatch(@event, s, registry)); } + protected Task On(WebhookAdded @event, EnvelopeHeaders headers) + { + return Collection.UpdateAsync(@event, headers, e => e.Webhooks.Add(SimpleMapper.Map(@event, new MongoSchemaEntityWebhook()))); + } + + protected Task On(WebhookDeleted @event, EnvelopeHeaders headers) + { + return Collection.UpdateAsync(@event, headers, e => e.Webhooks.RemoveAll(w => w.Id == @event.Id)); + } + protected Task On(SchemaDeleted @event, EnvelopeHeaders headers) { return Collection.UpdateAsync(@event, headers, e => e.IsDeleted = true); diff --git a/src/Squidex.Read.MongoDb/Schemas/MongoSchemaWebhookEntity.cs b/src/Squidex.Read.MongoDb/Schemas/MongoSchemaWebhookEntity.cs new file mode 100644 index 000000000..6df8336a7 --- /dev/null +++ b/src/Squidex.Read.MongoDb/Schemas/MongoSchemaWebhookEntity.cs @@ -0,0 +1,35 @@ +// ========================================================================== +// MongoSchemaWebhookEntity.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using Squidex.Read.Schemas; + +namespace Squidex.Read.MongoDb.Schemas +{ + public class MongoSchemaWebhookEntity : ISchemaWebhookEntity + { + [BsonId] + [BsonElement] + [BsonRepresentation(BsonType.String)] + public Guid Id { get; set; } + + [BsonRequired] + [BsonElement] + public Uri Url { get; set; } + + [BsonRequired] + [BsonElement] + public string SecurityToken { get; set; } + + [BsonRequired] + [BsonElement] + public Guid SchemaId { get; set; } + } +} diff --git a/src/Squidex.Read.MongoDb/Schemas/MongoSchemaWebhookRepository.cs b/src/Squidex.Read.MongoDb/Schemas/MongoSchemaWebhookRepository.cs new file mode 100644 index 000000000..1c6ae7a32 --- /dev/null +++ b/src/Squidex.Read.MongoDb/Schemas/MongoSchemaWebhookRepository.cs @@ -0,0 +1,121 @@ +// ========================================================================== +// MongoSchemaWebhookRepository.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.Events.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Events; +using Squidex.Infrastructure.Dispatching; +using Squidex.Infrastructure.MongoDb; +using Squidex.Infrastructure.Reflection; +using Squidex.Read.Schemas; +using Squidex.Read.Schemas.Repositories; + +namespace Squidex.Read.MongoDb.Schemas +{ + public class MongoSchemaWebhookRepository : MongoRepositoryBase, ISchemaWebhookRepository, IEventConsumer + { + private static readonly List EmptyWebhooks = new List(); + private Dictionary> inMemoryWebhooks; + private readonly SemaphoreSlim lockObject = new SemaphoreSlim(1); + + public string Name + { + get { return GetType().Name; } + } + + public string EventsFilter + { + get { return "^schema-"; } + } + + public MongoSchemaWebhookRepository(IMongoDatabase database) + : base(database) + { + } + + protected override string CollectionName() + { + return "Projections_SchemaWebhooks"; + } + + protected override Task SetupCollectionAsync(IMongoCollection collection) + { + return collection.Indexes.CreateOneAsync(IndexKeys.Ascending(x => x.SchemaId)); + } + + public Task On(Envelope @event) + { + return this.DispatchActionAsync(@event.Payload, @event.Headers); + } + + protected async Task On(WebhookAdded @event, EnvelopeHeaders headers) + { + await EnsureWebooksLoadedAsync(); + + var webhook = SimpleMapper.Map(@event, new MongoSchemaWebhookEntity { SchemaId = @event.SchemaId.Id }); + + inMemoryWebhooks.GetOrAddNew(webhook.SchemaId).Add(webhook); + + await Collection.InsertOneAsync(webhook); + } + + protected async Task On(WebhookDeleted @event, EnvelopeHeaders headers) + { + await EnsureWebooksLoadedAsync(); + + inMemoryWebhooks.GetOrDefault(@event.SchemaId.Id)?.RemoveAll(w => w.Id == @event.Id); + + await Collection.DeleteManyAsync(x => x.Id == @event.Id); + } + + protected async Task On(SchemaDeleted @event, EnvelopeHeaders headers) + { + await EnsureWebooksLoadedAsync(); + + inMemoryWebhooks.Remove(@event.SchemaId.Id); + + await Collection.DeleteManyAsync(x => x.SchemaId == @event.SchemaId.Id); + } + + public async Task> QueryBySchemaAsync(Guid schemaId) + { + await EnsureWebooksLoadedAsync(); + + return inMemoryWebhooks.GetOrDefault(schemaId)?.OfType()?.ToList() ?? EmptyWebhooks; + } + + private async Task EnsureWebooksLoadedAsync() + { + if (inMemoryWebhooks == null) + { + try + { + await lockObject.WaitAsync(); + + if (inMemoryWebhooks == null) + { + var webhooks = await Collection.Find(new BsonDocument()).ToListAsync(); + + inMemoryWebhooks = webhooks.GroupBy(x => x.SchemaId).ToDictionary(x => x.Key, x => x.ToList()); + } + } + finally + { + lockObject.Release(); + } + } + } + } +} diff --git a/src/Squidex.Read/Apps/Services/Implementations/CachingAppProvider.cs b/src/Squidex.Read/Apps/Services/Implementations/CachingAppProvider.cs index c8bf9db1e..92cf3cb5d 100644 --- a/src/Squidex.Read/Apps/Services/Implementations/CachingAppProvider.cs +++ b/src/Squidex.Read/Apps/Services/Implementations/CachingAppProvider.cs @@ -34,7 +34,7 @@ namespace Squidex.Read.Apps.Services.Implementations public string EventsFilter { - get { return "*"; } + get { return string.Empty; } } public CachingAppProvider(IMemoryCache cache, IAppRepository repository) diff --git a/src/Squidex.Read/Schemas/ISchemaEntity.cs b/src/Squidex.Read/Schemas/ISchemaEntity.cs index 8a32e0e7a..a0bca2b25 100644 --- a/src/Squidex.Read/Schemas/ISchemaEntity.cs +++ b/src/Squidex.Read/Schemas/ISchemaEntity.cs @@ -7,6 +7,7 @@ // ========================================================================== using Squidex.Core.Schemas; +using System.Collections.Generic; namespace Squidex.Read.Schemas { @@ -19,5 +20,7 @@ namespace Squidex.Read.Schemas bool IsDeleted { get; } Schema Schema { get; } + + IEnumerable Webhooks { get; } } } diff --git a/src/Squidex.Read/Schemas/ISchemaWebhookEntity.cs b/src/Squidex.Read/Schemas/ISchemaWebhookEntity.cs new file mode 100644 index 000000000..8397aa623 --- /dev/null +++ b/src/Squidex.Read/Schemas/ISchemaWebhookEntity.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// ISchemaWebhookEntity.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; + +namespace Squidex.Read.Schemas +{ + public interface ISchemaWebhookEntity + { + Guid Id { get; } + + Uri Url { get; } + + string SecurityToken { get; } + } +} diff --git a/src/Squidex.Read/Schemas/Repositories/ISchemaWebhookRepository.cs b/src/Squidex.Read/Schemas/Repositories/ISchemaWebhookRepository.cs new file mode 100644 index 000000000..b58676312 --- /dev/null +++ b/src/Squidex.Read/Schemas/Repositories/ISchemaWebhookRepository.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// ISchemaWebhookRepository.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Squidex.Read.Schemas.Repositories +{ + public interface ISchemaWebhookRepository + { + Task> QueryBySchemaAsync(Guid schemaId); + } +} diff --git a/src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs b/src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs index ff14fb5d6..b164036d5 100644 --- a/src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs +++ b/src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs @@ -34,7 +34,7 @@ namespace Squidex.Read.Schemas.Services.Implementations public string EventsFilter { - get { return "*"; } + get { return string.Empty; } } public CachingSchemaProvider(IMemoryCache cache, ISchemaRepository repository) @@ -125,6 +125,14 @@ namespace Squidex.Read.Schemas.Services.Implementations { Remove(schemaUpdatedEvent.AppId, schemaUpdatedEvent.SchemaId); } + else if (@event.Payload is WebhookAdded webhookAddedEvent) + { + Remove(webhookAddedEvent.AppId, webhookAddedEvent.SchemaId); + } + else if (@event.Payload is WebhookDeleted webhookDeletedEvent) + { + Remove(webhookDeletedEvent.AppId, webhookDeletedEvent.SchemaId); + } return TaskHelper.Done; } diff --git a/src/Squidex.Read/Schemas/WebhookInvoker.cs b/src/Squidex.Read/Schemas/WebhookInvoker.cs new file mode 100644 index 000000000..b51083453 --- /dev/null +++ b/src/Squidex.Read/Schemas/WebhookInvoker.cs @@ -0,0 +1,119 @@ +// ========================================================================== +// WebhookInvoker.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Squidex.Events.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Events; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Tasks; +using Squidex.Read.Schemas.Repositories; + +namespace Squidex.Read.Schemas +{ + public sealed class WebhookInvoker : IEventConsumer + { + private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(2); + + private readonly ISchemaWebhookRepository webhookRepository; + private readonly ISemanticLog log; + private readonly JsonSerializer webhookSerializer; + + public string Name + { + get { return GetType().Name; } + } + + public string EventsFilter + { + get { return "^content-"; } + } + + public WebhookInvoker(ISchemaWebhookRepository webhookRepository, JsonSerializer webhookSerializer, ISemanticLog log) + { + Guard.NotNull(webhookRepository, nameof(webhookRepository)); + Guard.NotNull(webhookSerializer, nameof(webhookSerializer)); + Guard.NotNull(log, nameof(log)); + + this.webhookRepository = webhookRepository; + this.webhookSerializer = webhookSerializer; + + this.log = log; + } + + public Task ClearAsync() + { + return TaskHelper.Done; + } + + public async Task On(Envelope @event) + { + if (@event.Payload is ContentEvent contentEvent) + { + var hooks = await webhookRepository.QueryBySchemaAsync(contentEvent.SchemaId.Id); + + if (hooks.Count > 0) + { + var payload = CreatePayload(@event); + + foreach (var hook in hooks) + { + DispatchEventAsync(payload, hook).Forget(); + } + } + } + } + + private JObject CreatePayload(Envelope @event) + { + return new JObject( + new JProperty("type", @event.Payload.GetType().Name), + new JProperty("meta", JObject.FromObject(@event.Headers, webhookSerializer)), + new JProperty("data", JObject.FromObject(@event.Headers, webhookSerializer))); + } + + private async Task DispatchEventAsync(JObject payload, ISchemaWebhookEntity webhook) + { + try + { + using (log.MeasureInformation(w => w + .WriteProperty("Action", "SendToHook") + .WriteProperty("Status", "Invoked"))) + { + using (var client = new HttpClient()) + { + client.Timeout = Timeout; + + var message = new HttpRequestMessage(HttpMethod.Post, webhook.Url) + { + Content = new StringContent(payload.ToString(), Encoding.UTF8, "application/json") + }; + + message.Headers.TryAddWithoutValidation("X-SecurityToken", webhook.SecurityToken); + message.Headers.Add("User-Agent", "Squidex"); + + var response = await client.SendAsync(message); + + response.EnsureSuccessStatusCode(); + } + } + } + catch (Exception ex) + { + log.LogError(ex, w => w + .WriteProperty("Action", "SendToHook") + .WriteProperty("Status", "Failed")); + } + } + } +} diff --git a/src/Squidex/Config/Domain/ReadModule.cs b/src/Squidex/Config/Domain/ReadModule.cs index 2ee32fb5d..ebe234103 100644 --- a/src/Squidex/Config/Domain/ReadModule.cs +++ b/src/Squidex/Config/Domain/ReadModule.cs @@ -73,6 +73,10 @@ namespace Squidex.Config.Domain .As() .SingleInstance(); + builder.RegisterType() + .AsSelf() + .SingleInstance(); + builder.RegisterType() .AsSelf() .SingleInstance(); diff --git a/src/Squidex/Config/Domain/StoreMongoDbModule.cs b/src/Squidex/Config/Domain/StoreMongoDbModule.cs index 742ce4936..78c8788ba 100644 --- a/src/Squidex/Config/Domain/StoreMongoDbModule.cs +++ b/src/Squidex/Config/Domain/StoreMongoDbModule.cs @@ -30,6 +30,7 @@ using Squidex.Read.MongoDb.History; using Squidex.Read.MongoDb.Infrastructure; using Squidex.Read.MongoDb.Schemas; using Squidex.Read.MongoDb.Users; +using Squidex.Read.Schemas; using Squidex.Read.Schemas.Repositories; using Squidex.Read.Schemas.Services.Implementations; using Squidex.Read.Users; @@ -160,6 +161,13 @@ namespace Squidex.Config.Domain .AsSelf() .SingleInstance(); + builder.RegisterType() + .WithParameter(ResolvedParameter.ForNamed(MongoDatabaseRegistration)) + .As() + .As() + .AsSelf() + .SingleInstance(); + builder.Register(c => new CompoundEventConsumer( c.Resolve(), @@ -175,6 +183,14 @@ namespace Squidex.Config.Domain .As() .AsSelf() .SingleInstance(); + + builder.Register(c => + new CompoundEventConsumer( + c.Resolve(), + c.Resolve())) + .As() + .AsSelf() + .SingleInstance(); } } } diff --git a/src/Squidex/Config/Domain/WriteModule.cs b/src/Squidex/Config/Domain/WriteModule.cs index eaff8f4fd..8332232d5 100644 --- a/src/Squidex/Config/Domain/WriteModule.cs +++ b/src/Squidex/Config/Domain/WriteModule.cs @@ -51,10 +51,6 @@ namespace Squidex.Config.Domain .As() .SingleInstance(); - builder.RegisterType() - .AsSelf() - .SingleInstance(); - builder.RegisterType() .AsSelf() .SingleInstance(); diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Events/CompoundEventConsumerTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Events/CompoundEventConsumerTests.cs index 14e70e829..b940cd9ce 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Events/CompoundEventConsumerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Events/CompoundEventConsumerTests.cs @@ -25,7 +25,7 @@ namespace Squidex.Infrastructure.CQRS.Events [Fact] public void Should_return_given_name() { - var sut = new CompoundEventConsumer("consumer-name"); + var sut = new CompoundEventConsumer("consumer-name", consumer1.Object); Assert.Equal("consumer-name", sut.Name); } @@ -33,25 +33,33 @@ namespace Squidex.Infrastructure.CQRS.Events [Fact] public void Should_return_first_inner_name() { - const string name = "my-inner-consumer"; - - consumer1.Setup(x => x.Name).Returns(name); + consumer1.Setup(x => x.Name).Returns("my-inner-consumer"); var sut = new CompoundEventConsumer(consumer1.Object, consumer2.Object); - Assert.Equal(name, sut.Name); + Assert.Equal("my-inner-consumer", sut.Name); } [Fact] - public void Should_return_first_inner_filter() + public void Should_return_compound_filter() { - const string filter = "my-inner-filter"; + consumer1.Setup(x => x.EventsFilter).Returns("filter1"); + consumer2.Setup(x => x.EventsFilter).Returns("filter2"); - consumer1.Setup(x => x.EventsFilter).Returns(filter); + var sut = new CompoundEventConsumer("my", consumer1.Object, consumer2.Object); - var sut = new CompoundEventConsumer(consumer1.Object, consumer2.Object); + Assert.Equal("(filter1)|(filter2)", sut.EventsFilter); + } + + [Fact] + public void Should_ignore_empty_filters() + { + consumer1.Setup(x => x.EventsFilter).Returns("filter1"); + consumer2.Setup(x => x.EventsFilter).Returns(""); + + var sut = new CompoundEventConsumer("my", consumer1.Object, consumer2.Object); - Assert.Equal(filter, sut.EventsFilter); + Assert.Equal("(filter1)", sut.EventsFilter); } [Fact] diff --git a/tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs b/tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs index 352b9e461..3dc761828 100644 --- a/tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs +++ b/tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs @@ -8,7 +8,6 @@ using System; using System.Threading.Tasks; -using FluentAssertions; using Moq; using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Commands;