Browse Source

Structure improved.

pull/98/head
Sebastian Stehle 9 years ago
parent
commit
299727bec1
  1. 2
      src/Squidex.Domain.Apps.Events/Apps/AppContributorAssigned.cs
  2. 2
      src/Squidex.Domain.Apps.Events/Apps/AppContributorRemoved.cs
  3. 2
      src/Squidex.Domain.Apps.Events/Apps/AppCreated.cs
  4. 2
      src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs
  5. 2
      src/Squidex.Domain.Apps.Events/Assets/AssetRenamed.cs
  6. 2
      src/Squidex.Domain.Apps.Events/Assets/AssetUpdated.cs
  7. 2
      src/Squidex.Domain.Apps.Events/Contents/ContentCreated.cs
  8. 2
      src/Squidex.Domain.Apps.Events/Contents/ContentDeleted.cs
  9. 2
      src/Squidex.Domain.Apps.Events/Contents/ContentPublished.cs
  10. 2
      src/Squidex.Domain.Apps.Events/Contents/ContentUnpublished.cs
  11. 2
      src/Squidex.Domain.Apps.Events/Contents/ContentUpdated.cs
  12. 2
      src/Squidex.Domain.Apps.Events/Schemas/FieldAdded.cs
  13. 2
      src/Squidex.Domain.Apps.Events/Schemas/FieldDeleted.cs
  14. 2
      src/Squidex.Domain.Apps.Events/Schemas/FieldDisabled.cs
  15. 2
      src/Squidex.Domain.Apps.Events/Schemas/FieldEnabled.cs
  16. 2
      src/Squidex.Domain.Apps.Events/Schemas/FieldHidden.cs
  17. 2
      src/Squidex.Domain.Apps.Events/Schemas/FieldLocked.cs
  18. 2
      src/Squidex.Domain.Apps.Events/Schemas/FieldShown.cs
  19. 2
      src/Squidex.Domain.Apps.Events/Schemas/FieldUpdated.cs
  20. 2
      src/Squidex.Domain.Apps.Events/Schemas/SchemaCreated.cs
  21. 2
      src/Squidex.Domain.Apps.Events/Schemas/SchemaDeleted.cs
  22. 2
      src/Squidex.Domain.Apps.Events/Schemas/SchemaFieldsReordered.cs
  23. 2
      src/Squidex.Domain.Apps.Events/Schemas/SchemaPublished.cs
  24. 2
      src/Squidex.Domain.Apps.Events/Schemas/SchemaUnpublished.cs
  25. 2
      src/Squidex.Domain.Apps.Events/Schemas/SchemaUpdated.cs
  26. 23
      src/Squidex.Domain.Apps.Events/Schemas/WebhookAdded.cs
  27. 19
      src/Squidex.Domain.Apps.Events/Schemas/WebhookDeleted.cs
  28. 10
      src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs
  29. 57
      src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaWebhookEntity.cs
  30. 127
      src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaWebhookRepository.cs
  31. 68
      src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaWebhookRepository_EventHandling.cs
  32. 79
      src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoWebhookEventEntity.cs
  33. 116
      src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoWebhookEventRepository.cs
  34. 25
      src/Squidex.Domain.Apps.Read/Schemas/ISchemaWebhookEntity.cs
  35. 21
      src/Squidex.Domain.Apps.Read/Schemas/ISchemaWebhookUrlEntity.cs
  36. 27
      src/Squidex.Domain.Apps.Read/Schemas/IWebhookEventEntity.cs
  37. 23
      src/Squidex.Domain.Apps.Read/Schemas/Repositories/ISchemaWebhookRepository.cs
  38. 35
      src/Squidex.Domain.Apps.Read/Schemas/Repositories/IWebhookEventRepository.cs
  39. 8
      src/Squidex.Domain.Apps.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs
  40. 164
      src/Squidex.Domain.Apps.Read/Schemas/WebhookDequeuer.cs
  41. 126
      src/Squidex.Domain.Apps.Read/Schemas/WebhookEnqueuer.cs
  42. 32
      src/Squidex.Domain.Apps.Read/Schemas/WebhookJob.cs
  43. 18
      src/Squidex.Domain.Apps.Read/Schemas/WebhookJobResult.cs
  44. 18
      src/Squidex.Domain.Apps.Read/Schemas/WebhookResult.cs
  45. 89
      src/Squidex.Domain.Apps.Read/Schemas/WebhookSender.cs
  46. 2
      src/Squidex.Domain.Apps.Write/Apps/Commands/AssignContributor.cs
  47. 2
      src/Squidex.Domain.Apps.Write/Apps/Commands/RemoveContributor.cs
  48. 2
      src/Squidex.Domain.Apps.Write/Apps/Commands/RevokeClient.cs
  49. 2
      src/Squidex.Domain.Apps.Write/Apps/Commands/UpdateClient.cs
  50. 2
      src/Squidex.Domain.Apps.Write/Assets/Commands/UpdateAsset.cs
  51. 2
      src/Squidex.Domain.Apps.Write/Contents/Commands/CreateContent.cs
  52. 2
      src/Squidex.Domain.Apps.Write/Contents/Commands/DeleteContent.cs
  53. 2
      src/Squidex.Domain.Apps.Write/Contents/Commands/PatchContent.cs
  54. 2
      src/Squidex.Domain.Apps.Write/Contents/Commands/PublishContent.cs
  55. 2
      src/Squidex.Domain.Apps.Write/Contents/Commands/UnpublishContent.cs
  56. 2
      src/Squidex.Domain.Apps.Write/Contents/Commands/UpdateContent.cs
  57. 1
      src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs
  58. 2
      src/Squidex.Domain.Apps.Write/Schemas/Commands/AddField.cs
  59. 31
      src/Squidex.Domain.Apps.Write/Schemas/Commands/AddWebhook.cs
  60. 2
      src/Squidex.Domain.Apps.Write/Schemas/Commands/CreateSchema.cs
  61. 2
      src/Squidex.Domain.Apps.Write/Schemas/Commands/DeleteField.cs
  62. 2
      src/Squidex.Domain.Apps.Write/Schemas/Commands/DeleteSchema.cs
  63. 17
      src/Squidex.Domain.Apps.Write/Schemas/Commands/DeleteWebhook.cs
  64. 2
      src/Squidex.Domain.Apps.Write/Schemas/Commands/DisableField.cs
  65. 2
      src/Squidex.Domain.Apps.Write/Schemas/Commands/EnableField.cs
  66. 2
      src/Squidex.Domain.Apps.Write/Schemas/Commands/HideField.cs
  67. 2
      src/Squidex.Domain.Apps.Write/Schemas/Commands/LockField.cs
  68. 2
      src/Squidex.Domain.Apps.Write/Schemas/Commands/PublishSchema.cs
  69. 2
      src/Squidex.Domain.Apps.Write/Schemas/Commands/ReorderFields.cs
  70. 2
      src/Squidex.Domain.Apps.Write/Schemas/Commands/ShowField.cs
  71. 2
      src/Squidex.Domain.Apps.Write/Schemas/Commands/UnpublishSchema.cs
  72. 2
      src/Squidex.Domain.Apps.Write/Schemas/Commands/UpdateField.cs
  73. 2
      src/Squidex.Domain.Apps.Write/Schemas/Commands/UpdateSchema.cs
  74. 10
      src/Squidex.Domain.Apps.Write/Schemas/SchemaCommandMiddleware.cs
  75. 42
      src/Squidex.Domain.Apps.Write/Schemas/SchemaDomainObject.cs
  76. 6
      src/Squidex/Config/Domain/StoreMongoDbModule.cs
  77. 2
      src/Squidex/Controllers/Api/Apps/AppsController.cs
  78. 4
      src/Squidex/Controllers/Api/Schemas/SchemaFieldsController.cs
  79. 7
      src/Squidex/Controllers/Api/Schemas/SchemasController.cs
  80. 7
      src/Squidex/Controllers/Api/Webhooks/Models/CreateWebhookDto.cs
  81. 32
      src/Squidex/Controllers/Api/Webhooks/Models/WebhookCreatedDto.cs
  82. 35
      src/Squidex/Controllers/Api/Webhooks/Models/WebhookDto.cs
  83. 2
      src/Squidex/Controllers/Api/Webhooks/Models/WebhookEventDto.cs
  84. 80
      src/Squidex/Controllers/Api/Webhooks/WebhooksController.cs
  85. 130
      tests/Squidex.Domain.Apps.Read.Tests/Schemas/WebhookDequeuerTests.cs
  86. 118
      tests/Squidex.Domain.Apps.Read.Tests/Schemas/WebhookEnqueuerTests.cs
  87. 34
      tests/Squidex.Domain.Apps.Write.Tests/Schemas/SchemaCommandHandlerTests.cs
  88. 94
      tests/Squidex.Domain.Apps.Write.Tests/Schemas/SchemaDomainObjectTests.cs

2
src/Squidex.Domain.Apps.Events/Apps/AppContributorAssigned.cs

@ -12,7 +12,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Events.Apps
{
[TypeName("AppContributorAssignedEvent")]
public class AppContributorAssigned : AppEvent
public sealed class AppContributorAssigned : AppEvent
{
public string ContributorId { get; set; }

2
src/Squidex.Domain.Apps.Events/Apps/AppContributorRemoved.cs

@ -11,7 +11,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Events.Apps
{
[TypeName("AppContributorRemovedEvent")]
public class AppContributorRemoved : AppEvent
public sealed class AppContributorRemoved : AppEvent
{
public string ContributorId { get; set; }
}

2
src/Squidex.Domain.Apps.Events/Apps/AppCreated.cs

@ -11,7 +11,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Events.Apps
{
[TypeName("AppCreatedEvent")]
public class AppCreated : AppEvent
public sealed class AppCreated : AppEvent
{
public string Name { get; set; }
}

2
src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs

@ -11,7 +11,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Events.Assets
{
[TypeName("AssetCreatedEvent")]
public class AssetCreated : AssetEvent
public sealed class AssetCreated : AssetEvent
{
public string FileName { get; set; }

2
src/Squidex.Domain.Apps.Events/Assets/AssetRenamed.cs

@ -11,7 +11,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Events.Assets
{
[TypeName("AssetRenamedEvent")]
public class AssetRenamed : AssetEvent
public sealed class AssetRenamed : AssetEvent
{
public string FileName { get; set; }
}

2
src/Squidex.Domain.Apps.Events/Assets/AssetUpdated.cs

@ -11,7 +11,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Events.Assets
{
[TypeName("AssetUpdated")]
public class AssetUpdated : AssetEvent
public sealed class AssetUpdated : AssetEvent
{
public string MimeType { get; set; }

2
src/Squidex.Domain.Apps.Events/Contents/ContentCreated.cs

@ -12,7 +12,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Events.Contents
{
[TypeName("ContentCreatedEvent")]
public class ContentCreated : ContentEvent
public sealed class ContentCreated : ContentEvent
{
public NamedContentData Data { get; set; }
}

2
src/Squidex.Domain.Apps.Events/Contents/ContentDeleted.cs

@ -11,7 +11,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Events.Contents
{
[TypeName("ContentDeletedEvent")]
public class ContentDeleted : ContentEvent
public sealed class ContentDeleted : ContentEvent
{
}
}

2
src/Squidex.Domain.Apps.Events/Contents/ContentPublished.cs

@ -11,7 +11,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Events.Contents
{
[TypeName("ContentPublishedEvent")]
public class ContentPublished : ContentEvent
public sealed class ContentPublished : ContentEvent
{
}
}

2
src/Squidex.Domain.Apps.Events/Contents/ContentUnpublished.cs

@ -11,7 +11,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Events.Contents
{
[TypeName("ContentUnpublishedEvent")]
public class ContentUnpublished : ContentEvent
public sealed class ContentUnpublished : ContentEvent
{
}
}

2
src/Squidex.Domain.Apps.Events/Contents/ContentUpdated.cs

@ -12,7 +12,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Events.Contents
{
[TypeName("ContentUpdatedEvent")]
public class ContentUpdated : ContentEvent
public sealed class ContentUpdated : ContentEvent
{
public NamedContentData Data { get; set; }
}

2
src/Squidex.Domain.Apps.Events/Schemas/FieldAdded.cs

@ -12,7 +12,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Events.Schemas
{
[TypeName("FieldAddedEvent")]
public class FieldAdded : FieldEvent
public sealed class FieldAdded : FieldEvent
{
public string Name { get; set; }

2
src/Squidex.Domain.Apps.Events/Schemas/FieldDeleted.cs

@ -11,7 +11,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Events.Schemas
{
[TypeName("FieldDeletedEvent")]
public class FieldDeleted : FieldEvent
public sealed class FieldDeleted : FieldEvent
{
}
}

2
src/Squidex.Domain.Apps.Events/Schemas/FieldDisabled.cs

@ -11,7 +11,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Events.Schemas
{
[TypeName("FieldDisabledEvent")]
public class FieldDisabled : FieldEvent
public sealed class FieldDisabled : FieldEvent
{
}
}

2
src/Squidex.Domain.Apps.Events/Schemas/FieldEnabled.cs

@ -11,7 +11,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Events.Schemas
{
[TypeName("FieldEnabledEvent")]
public class FieldEnabled : FieldEvent
public sealed class FieldEnabled : FieldEvent
{
}
}

2
src/Squidex.Domain.Apps.Events/Schemas/FieldHidden.cs

@ -11,7 +11,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Events.Schemas
{
[TypeName("FieldHiddenEvent")]
public class FieldHidden : FieldEvent
public sealed class FieldHidden : FieldEvent
{
}
}

2
src/Squidex.Domain.Apps.Events/Schemas/FieldLocked.cs

@ -11,7 +11,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Events.Schemas
{
[TypeName("FieldLockedEvent")]
public class FieldLocked : FieldEvent
public sealed class FieldLocked : FieldEvent
{
}
}

2
src/Squidex.Domain.Apps.Events/Schemas/FieldShown.cs

@ -11,7 +11,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Events.Schemas
{
[TypeName("FieldShownEvent")]
public class FieldShown : FieldEvent
public sealed class FieldShown : FieldEvent
{
}
}

2
src/Squidex.Domain.Apps.Events/Schemas/FieldUpdated.cs

@ -12,7 +12,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Events.Schemas
{
[TypeName("FieldUpdatedEvent")]
public class FieldUpdated : FieldEvent
public sealed class FieldUpdated : FieldEvent
{
public FieldProperties Properties { get; set; }
}

2
src/Squidex.Domain.Apps.Events/Schemas/SchemaCreated.cs

@ -13,7 +13,7 @@ using SchemaFields = System.Collections.Generic.List<Squidex.Domain.Apps.Events.
namespace Squidex.Domain.Apps.Events.Schemas
{
[TypeName("SchemaCreatedEvent")]
public class SchemaCreated : SchemaEvent
public sealed class SchemaCreated : SchemaEvent
{
public string Name { get; set; }

2
src/Squidex.Domain.Apps.Events/Schemas/SchemaDeleted.cs

@ -11,7 +11,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Events.Schemas
{
[TypeName("SchemaDeletedEvent")]
public class SchemaDeleted : SchemaEvent
public sealed class SchemaDeleted : SchemaEvent
{
}
}

2
src/Squidex.Domain.Apps.Events/Schemas/SchemaFieldsReordered.cs

@ -12,7 +12,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Events.Schemas
{
[TypeName("SchemaFieldsReorderedEvent")]
public class SchemaFieldsReordered : SchemaEvent
public sealed class SchemaFieldsReordered : SchemaEvent
{
public List<long> FieldIds { get; set; }
}

2
src/Squidex.Domain.Apps.Events/Schemas/SchemaPublished.cs

@ -11,7 +11,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Events.Schemas
{
[TypeName("SchemaPublishedEvent")]
public class SchemaPublished : SchemaEvent
public sealed class SchemaPublished : SchemaEvent
{
}
}

2
src/Squidex.Domain.Apps.Events/Schemas/SchemaUnpublished.cs

@ -11,7 +11,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Events.Schemas
{
[TypeName("SchemaUnpublishedEvent")]
public class SchemaUnpublished : SchemaEvent
public sealed class SchemaUnpublished : SchemaEvent
{
}
}

2
src/Squidex.Domain.Apps.Events/Schemas/SchemaUpdated.cs

@ -12,7 +12,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Events.Schemas
{
[TypeName("SchemaUpdatedEvent")]
public class SchemaUpdated : SchemaEvent
public sealed class SchemaUpdated : SchemaEvent
{
public SchemaProperties Properties { get; set; }
}

23
src/Squidex.Domain.Apps.Events/Schemas/WebhookAdded.cs

@ -1,23 +0,0 @@
// ==========================================================================
// WebhookAdded.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Events.Schemas
{
[TypeName("WebhookAddedEvent")]
public sealed class WebhookAdded : SchemaEvent
{
public Guid Id { get; set; }
public Uri Url { get; set; }
public string SharedSecret { get; set; }
}
}

19
src/Squidex.Domain.Apps.Events/Schemas/WebhookDeleted.cs

@ -1,19 +0,0 @@
// ==========================================================================
// WebhookDeleted.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Events.Schemas
{
[TypeName("WebhookDeletedEvent")]
public sealed class WebhookDeleted : SchemaEvent
{
public Guid Id { get; set; }
}
}

10
src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs

@ -103,16 +103,6 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Schemas
return UpdateSchema(@event, headers, s => SchemaEventDispatcher.Dispatch(@event, s, registry));
}
protected Task On(WebhookAdded @event, EnvelopeHeaders headers)
{
return UpdateSchema(@event, headers, s => s);
}
protected Task On(WebhookDeleted @event, EnvelopeHeaders headers)
{
return UpdateSchema(@event, headers, s => s);
}
protected Task On(SchemaDeleted @event, EnvelopeHeaders headers)
{
return Collection.UpdateAsync(@event, headers, e => e.IsDeleted = true);

57
src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaWebhookEntity.cs

@ -1,57 +0,0 @@
// ==========================================================================
// MongoSchemaWebhookEntity.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using Squidex.Domain.Apps.Read.Schemas;
// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global
namespace Squidex.Domain.Apps.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 Guid AppId { get; set; }
[BsonRequired]
[BsonElement]
public Guid SchemaId { 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; }
}
}

127
src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaWebhookRepository.cs

@ -1,127 +0,0 @@
// ==========================================================================
// 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.Domain.Apps.Read.Schemas;
using Squidex.Domain.Apps.Read.Schemas.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Reflection;
// ReSharper disable SwitchStatementMissingSomeCases
namespace Squidex.Domain.Apps.Read.MongoDb.Schemas
{
public partial class MongoSchemaWebhookRepository : MongoRepositoryBase<MongoSchemaWebhookEntity>, ISchemaWebhookRepository, IEventConsumer
{
private static readonly List<ShortInfo> EmptyWebhooks = new List<ShortInfo>();
private Dictionary<Guid, Dictionary<Guid, List<ShortInfo>>> inMemoryWebhooks;
private readonly SemaphoreSlim lockObject = new SemaphoreSlim(1);
public sealed class ShortInfo : ISchemaWebhookUrlEntity
{
public Guid Id { get; set; }
public Uri Url { get; set; }
public string SharedSecret { get; set; }
}
public MongoSchemaWebhookRepository(IMongoDatabase database)
: base(database)
{
}
protected override string CollectionName()
{
return "Projections_SchemaWebhooks";
}
protected override Task SetupCollectionAsync(IMongoCollection<MongoSchemaWebhookEntity> collection)
{
return collection.Indexes.CreateOneAsync(Index.Ascending(x => x.SchemaId));
}
public async Task<IReadOnlyList<ISchemaWebhookEntity>> QueryByAppAsync(Guid appId)
{
return await Collection.Find(Filter.Eq(x => x.AppId, appId)).ToListAsync();
}
public async Task<IReadOnlyList<ISchemaWebhookUrlEntity>> QueryUrlsBySchemaAsync(Guid appId, Guid schemaId)
{
await EnsureWebooksLoadedAsync();
return inMemoryWebhooks.GetOrDefault(appId)?.GetOrDefault(schemaId)?.ToList() ?? 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)
{
var result = new Dictionary<Guid, Dictionary<Guid, List<ShortInfo>>>();
var webhooks = await Collection.Find(new BsonDocument()).ToListAsync();
foreach (var webhook in webhooks)
{
var list = result.GetOrAddNew(webhook.AppId).GetOrAddNew(webhook.SchemaId);
list.Add(SimpleMapper.Map(webhook, new ShortInfo()));
}
inMemoryWebhooks = result;
}
}
finally
{
lockObject.Release();
}
}
}
}
}

68
src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaWebhookRepository_EventHandling.cs

@ -1,68 +0,0 @@
// ==========================================================================
// MongoSchemaWebhookRepository_EventHandling.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Threading.Tasks;
using MongoDB.Driver;
using Squidex.Domain.Apps.Events.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Read.MongoDb.Schemas
{
public partial class MongoSchemaWebhookRepository
{
public string Name
{
get { return GetType().Name; }
}
public string EventsFilter
{
get { return "^schema-"; }
}
public Task On(Envelope<IEvent> @event)
{
return this.DispatchActionAsync(@event.Payload, @event.Headers);
}
protected async Task On(WebhookAdded @event, EnvelopeHeaders headers)
{
await EnsureWebooksLoadedAsync();
var theAppId = @event.AppId.Id;
var theSchemaId = @event.SchemaId.Id;
var webhook = SimpleMapper.Map(@event, new MongoSchemaWebhookEntity { AppId = theAppId, SchemaId = theSchemaId });
inMemoryWebhooks.GetOrAddNew(theAppId).GetOrAddNew(theSchemaId).Add(SimpleMapper.Map(@event, new ShortInfo()));
await Collection.InsertOneAsync(webhook);
}
protected async Task On(WebhookDeleted @event, EnvelopeHeaders headers)
{
await EnsureWebooksLoadedAsync();
inMemoryWebhooks.GetOrDefault(@event.AppId.Id)?.Remove(@event.SchemaId.Id);
await Collection.DeleteManyAsync(x => x.Id == @event.Id);
}
protected async Task On(SchemaDeleted @event, EnvelopeHeaders headers)
{
await EnsureWebooksLoadedAsync();
inMemoryWebhooks.GetOrDefault(@event.AppId.Id)?.Remove(@event.SchemaId.Id);
await Collection.DeleteManyAsync(x => x.SchemaId == @event.SchemaId.Id);
}
}
}

79
src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoWebhookEventEntity.cs

@ -1,79 +0,0 @@
// ==========================================================================
// MongoWebhookEventEntity.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using MongoDB.Bson.Serialization.Attributes;
using NodaTime;
using Squidex.Domain.Apps.Read.Schemas;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Read.MongoDb.Schemas
{
public sealed class MongoWebhookEventEntity : MongoEntity, IWebhookEventEntity
{
private WebhookJob job;
[BsonRequired]
[BsonElement]
public Guid AppId { get; set; }
[BsonRequired]
[BsonElement]
public Guid WebhookId { get; set; }
[BsonRequired]
[BsonElement]
public Uri RequestUrl { get; set; }
[BsonRequired]
[BsonElement]
public string RequestBody { get; set; }
[BsonRequired]
[BsonElement]
public string RequestSignature { get; set; }
[BsonRequired]
[BsonElement]
public string EventName { get; set; }
[BsonRequired]
[BsonElement]
public string LastDump { get; set; }
[BsonRequired]
[BsonElement]
public Instant Expires { get; set; }
[BsonRequired]
[BsonElement]
public Instant? NextAttempt { get; set; }
[BsonRequired]
[BsonElement]
public int NumCalls { get; set; }
[BsonRequired]
[BsonElement]
public bool IsSending { get; set; }
[BsonRequired]
[BsonElement]
public WebhookResult Result { get; set; }
[BsonRequired]
[BsonElement]
public WebhookJobResult JobResult { get; set; }
public WebhookJob Job
{
get { return job ?? (job = SimpleMapper.Map(this, new WebhookJob())); }
}
}
}

116
src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoWebhookEventRepository.cs

@ -1,116 +0,0 @@
// ==========================================================================
// MongoWebhookEventRepository.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 MongoDB.Driver;
using NodaTime;
using Squidex.Domain.Apps.Read.Schemas;
using Squidex.Domain.Apps.Read.Schemas.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Read.MongoDb.Schemas
{
public sealed class MongoWebhookEventRepository : MongoRepositoryBase<MongoWebhookEventEntity>, IWebhookEventRepository
{
private readonly IClock clock;
public MongoWebhookEventRepository(IMongoDatabase database, IClock clock)
: base(database)
{
Guard.NotNull(clock, nameof(clock));
this.clock = clock;
}
protected override string CollectionName()
{
return "WebhookEvents";
}
protected override async Task SetupCollectionAsync(IMongoCollection<MongoWebhookEventEntity> collection)
{
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.NextAttempt).Descending(x => x.IsSending));
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AppId).Descending(x => x.Created));
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Expires), new CreateIndexOptions { ExpireAfter = TimeSpan.Zero });
}
public Task QueryPendingAsync(Func<IWebhookEventEntity, Task> callback, CancellationToken cancellationToken = new 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)
{
var entities = await Collection.Find(x => x.AppId == appId).Skip(skip).Limit(take).SortByDescending(x => x.Created).ToListAsync();
return entities;
}
public async Task<IWebhookEventEntity> FindAsync(Guid id)
{
var entity = await Collection.Find(x => x.Id == id).FirstOrDefaultAsync();
return entity;
}
public async Task<int> CountByAppAsync(Guid appId)
{
return (int)await Collection.CountAsync(x => x.AppId == appId);
}
public Task EnqueueAsync(Guid id, Instant nextAttempt)
{
return Collection.UpdateOneAsync(x => x.Id == id, Update.Set(x => x.NextAttempt, nextAttempt));
}
public Task TraceSendingAsync(Guid jobId)
{
return Collection.UpdateOneAsync(x => x.Id == jobId, Update.Set(x => x.IsSending, true));
}
public Task EnqueueAsync(WebhookJob job, Instant nextAttempt)
{
var entity = SimpleMapper.Map(job, new MongoWebhookEventEntity { Created = clock.GetCurrentInstant(), NextAttempt = nextAttempt });
return Collection.InsertOneIfNotExistsAsync(entity);
}
public Task TraceSentAsync(Guid jobId, string dump, WebhookResult result, TimeSpan elapsed, Instant? nextAttempt)
{
WebhookJobResult jobResult;
if (result != WebhookResult.Success && nextAttempt == null)
{
jobResult = WebhookJobResult.Failed;
}
else if (result != WebhookResult.Success && nextAttempt.HasValue)
{
jobResult = WebhookJobResult.Retry;
}
else
{
jobResult = WebhookJobResult.Success;
}
return Collection.UpdateOneAsync(x => x.Id == jobId,
Update.Set(x => x.Result, result)
.Set(x => x.LastDump, dump)
.Set(x => x.JobResult, jobResult)
.Set(x => x.IsSending, false)
.Set(x => x.NextAttempt, nextAttempt)
.Inc(x => x.NumCalls, 1));
}
}
}

25
src/Squidex.Domain.Apps.Read/Schemas/ISchemaWebhookEntity.cs

@ -1,25 +0,0 @@
// ==========================================================================
// ISchemaWebhookEntity.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
namespace Squidex.Domain.Apps.Read.Schemas
{
public interface ISchemaWebhookEntity : ISchemaWebhookUrlEntity
{
Guid SchemaId { get; }
long TotalSucceeded { get; }
long TotalFailed { get; }
long TotalTimedout { get; }
long TotalRequestTime { get; }
}
}

21
src/Squidex.Domain.Apps.Read/Schemas/ISchemaWebhookUrlEntity.cs

@ -1,21 +0,0 @@
// ==========================================================================
// ISchemaWebhookUrlEntity.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
namespace Squidex.Domain.Apps.Read.Schemas
{
public interface ISchemaWebhookUrlEntity
{
Guid Id { get; }
Uri Url { get; }
string SharedSecret { get; }
}
}

27
src/Squidex.Domain.Apps.Read/Schemas/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.Schemas
{
public interface IWebhookEventEntity : IEntity
{
WebhookJob Job { get; }
Instant? NextAttempt { get; }
WebhookResult Result { get; }
WebhookJobResult JobResult { get; }
int NumCalls { get; }
string LastDump { get; }
}
}

23
src/Squidex.Domain.Apps.Read/Schemas/Repositories/ISchemaWebhookRepository.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.Schemas.Repositories
{
public interface ISchemaWebhookRepository
{
Task TraceSentAsync(Guid webhookId, WebhookResult result, TimeSpan elapsed);
Task<IReadOnlyList<ISchemaWebhookUrlEntity>> QueryUrlsBySchemaAsync(Guid appId, Guid schemaId);
Task<IReadOnlyList<ISchemaWebhookEntity>> QueryByAppAsync(Guid appId);
}
}

35
src/Squidex.Domain.Apps.Read/Schemas/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.Schemas.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);
}
}

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

@ -125,14 +125,6 @@ namespace Squidex.Domain.Apps.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;
}

164
src/Squidex.Domain.Apps.Read/Schemas/WebhookDequeuer.cs

@ -1,164 +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.Schemas.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Timers;
// ReSharper disable SwitchStatementMissingSomeCases
// ReSharper disable MethodSupportsCancellation
// ReSharper disable InvertIf
namespace Squidex.Domain.Apps.Read.Schemas
{
public sealed class WebhookDequeuer : DisposableObjectBase, IExternalSystem
{
private readonly ActionBlock<IWebhookEventEntity> requestBlock;
private readonly TransformBlock<IWebhookEventEntity, IWebhookEventEntity> blockBlock;
private readonly IWebhookEventRepository webhookEventRepository;
private readonly ISchemaWebhookRepository webhookRepository;
private readonly WebhookSender webhookSender;
private readonly CompletionTimer timer;
private readonly ISemanticLog log;
private readonly IClock clock;
public WebhookDequeuer(WebhookSender webhookSender,
IWebhookEventRepository webhookEventRepository,
ISchemaWebhookRepository 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;
}
}
}
}

126
src/Squidex.Domain.Apps.Read/Schemas/WebhookEnqueuer.cs

@ -1,126 +0,0 @@
// ==========================================================================
// WebhookEnqueuer.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NodaTime;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Domain.Apps.Read.Schemas.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Read.Schemas
{
public sealed class WebhookEnqueuer : IEventConsumer
{
private const string ContentPrefix = "Content";
private static readonly Duration ExpirationTime = Duration.FromDays(2);
private readonly IWebhookEventRepository webhookEventRepository;
private readonly ISchemaWebhookRepository 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,
ISchemaWebhookRepository 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.QueryUrlsBySchemaAsync(contentEvent.AppId.Id, contentEvent.SchemaId.Id);
if (webhooks.Count > 0)
{
var now = clock.GetCurrentInstant();
var payload = CreatePayload(@event, eventType);
var eventName = $"{contentEvent.SchemaId.Name.ToPascalCase()}{CreateContentEventName(eventType)}";
foreach (var webhook in webhooks)
{
await EnqueueJobAsync(payload, webhook, contentEvent, eventName, now);
}
}
}
}
private async Task EnqueueJobAsync(string payload, ISchemaWebhookUrlEntity 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 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) ? eventType.Substring(ContentPrefix.Length) : eventType;
}
}
}

32
src/Squidex.Domain.Apps.Read/Schemas/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.Schemas
{
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; }
}
}

18
src/Squidex.Domain.Apps.Read/Schemas/WebhookJobResult.cs

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

18
src/Squidex.Domain.Apps.Read/Schemas/WebhookResult.cs

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

89
src/Squidex.Domain.Apps.Read/Schemas/WebhookSender.cs

@ -1,89 +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;
// ReSharper disable SuggestVarOrType_SimpleTypes
namespace Squidex.Domain.Apps.Read.Schemas
{
public class WebhookSender
{
private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(2);
public virtual async Task<(string Dump, WebhookResult Result, TimeSpan Elapsed)> SendAsync(WebhookJob job)
{
HttpRequestMessage request = BuildRequest(job);
HttpResponseMessage response = null;
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;
}
finally
{
watch.Stop();
}
var responseString = string.Empty;
if (response != null)
{
responseString = await response.Content.ReadAsStringAsync();
}
var dump = DumpFormatter.BuildDump(request, response, job.RequestBody, responseString, watch.Elapsed);
var result = WebhookResult.Failed;
if (isTimeout)
{
result = WebhookResult.Timeout;
}
else if (response?.IsSuccessStatusCode == true)
{
result = WebhookResult.Success;
}
return (dump, result, watch.Elapsed);
}
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;
}
}
}

2
src/Squidex.Domain.Apps.Write/Apps/Commands/AssignContributor.cs

@ -12,7 +12,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Write.Apps.Commands
{
public class AssignContributor : AppAggregateCommand, IValidatable
public sealed class AssignContributor : AppAggregateCommand, IValidatable
{
public string ContributorId { get; set; }

2
src/Squidex.Domain.Apps.Write/Apps/Commands/RemoveContributor.cs

@ -11,7 +11,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Write.Apps.Commands
{
public class RemoveContributor : AppAggregateCommand, IValidatable
public sealed class RemoveContributor : AppAggregateCommand, IValidatable
{
public string ContributorId { get; set; }

2
src/Squidex.Domain.Apps.Write/Apps/Commands/RevokeClient.cs

@ -11,7 +11,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Write.Apps.Commands
{
public class RevokeClient : AppAggregateCommand, IValidatable
public sealed class RevokeClient : AppAggregateCommand, IValidatable
{
public string Id { get; set; }

2
src/Squidex.Domain.Apps.Write/Apps/Commands/UpdateClient.cs

@ -11,7 +11,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Write.Apps.Commands
{
public class UpdateClient : AppAggregateCommand, IValidatable
public sealed class UpdateClient : AppAggregateCommand, IValidatable
{
public string Id { get; set; }

2
src/Squidex.Domain.Apps.Write/Assets/Commands/UpdateAsset.cs

@ -10,7 +10,7 @@ using Squidex.Infrastructure.Assets;
namespace Squidex.Domain.Apps.Write.Assets.Commands
{
public class UpdateAsset : AssetAggregateCommand
public sealed class UpdateAsset : AssetAggregateCommand
{
public AssetFile File { get; set; }

2
src/Squidex.Domain.Apps.Write/Contents/Commands/CreateContent.cs

@ -10,7 +10,7 @@ using System;
namespace Squidex.Domain.Apps.Write.Contents.Commands
{
public class CreateContent : ContentDataCommand
public sealed class CreateContent : ContentDataCommand
{
public bool Publish { get; set; }

2
src/Squidex.Domain.Apps.Write/Contents/Commands/DeleteContent.cs

@ -8,7 +8,7 @@
namespace Squidex.Domain.Apps.Write.Contents.Commands
{
public class DeleteContent : ContentCommand
public sealed class DeleteContent : ContentCommand
{
}
}

2
src/Squidex.Domain.Apps.Write/Contents/Commands/PatchContent.cs

@ -8,7 +8,7 @@
namespace Squidex.Domain.Apps.Write.Contents.Commands
{
public class PatchContent : ContentDataCommand
public sealed class PatchContent : ContentDataCommand
{
}
}

2
src/Squidex.Domain.Apps.Write/Contents/Commands/PublishContent.cs

@ -8,7 +8,7 @@
namespace Squidex.Domain.Apps.Write.Contents.Commands
{
public class PublishContent : ContentCommand
public sealed class PublishContent : ContentCommand
{
}
}

2
src/Squidex.Domain.Apps.Write/Contents/Commands/UnpublishContent.cs

@ -8,7 +8,7 @@
namespace Squidex.Domain.Apps.Write.Contents.Commands
{
public class UnpublishContent : ContentCommand
public sealed class UnpublishContent : ContentCommand
{
}
}

2
src/Squidex.Domain.Apps.Write/Contents/Commands/UpdateContent.cs

@ -8,7 +8,7 @@
namespace Squidex.Domain.Apps.Write.Contents.Commands
{
public class UpdateContent : ContentDataCommand
public sealed class UpdateContent : ContentDataCommand
{
}
}

1
src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs

@ -42,7 +42,6 @@ namespace Squidex.Domain.Apps.Write.Contents
{
Guard.NotNull(handler, nameof(handler));
Guard.NotNull(schemas, nameof(schemas));
Guard.NotNull(handler, nameof(handler));
Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(assetRepository, nameof(assetRepository));
Guard.NotNull(contentRepository, nameof(contentRepository));

2
src/Squidex.Domain.Apps.Write/Schemas/Commands/AddField.cs

@ -13,7 +13,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Write.Schemas.Commands
{
public class AddField : FieldCommand, IValidatable
public sealed class AddField : FieldCommand, IValidatable
{
public string Name { get; set; }

31
src/Squidex.Domain.Apps.Write/Schemas/Commands/AddWebhook.cs

@ -1,31 +0,0 @@
// ==========================================================================
// AddWebhook.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Write.Schemas.Commands
{
public sealed class AddWebhook : SchemaAggregateCommand, IValidatable
{
public Guid Id { get; } = Guid.NewGuid();
public Uri Url { get; set; }
public string SharedSecret { get; } = RandomHash.New();
public void Validate(IList<ValidationError> errors)
{
if (Url == null || !Url.IsAbsoluteUri)
{
errors.Add(new ValidationError("Url must be specified and absolute", nameof(Url)));
}
}
}
}

2
src/Squidex.Domain.Apps.Write/Schemas/Commands/CreateSchema.cs

@ -15,7 +15,7 @@ using SchemaFields = System.Collections.Generic.List<Squidex.Domain.Apps.Write.S
namespace Squidex.Domain.Apps.Write.Schemas.Commands
{
public class CreateSchema : AppCommand, IValidatable, IAggregateCommand
public sealed class CreateSchema : AppCommand, IValidatable, IAggregateCommand
{
private SchemaProperties properties;
private SchemaFields fields;

2
src/Squidex.Domain.Apps.Write/Schemas/Commands/DeleteField.cs

@ -8,7 +8,7 @@
namespace Squidex.Domain.Apps.Write.Schemas.Commands
{
public class DeleteField : FieldCommand
public sealed class DeleteField : FieldCommand
{
}
}

2
src/Squidex.Domain.Apps.Write/Schemas/Commands/DeleteSchema.cs

@ -8,7 +8,7 @@
namespace Squidex.Domain.Apps.Write.Schemas.Commands
{
public class DeleteSchema : SchemaAggregateCommand
public sealed class DeleteSchema : SchemaAggregateCommand
{
}
}

17
src/Squidex.Domain.Apps.Write/Schemas/Commands/DeleteWebhook.cs

@ -1,17 +0,0 @@
// ==========================================================================
// DeleteWebhook.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
namespace Squidex.Domain.Apps.Write.Schemas.Commands
{
public class DeleteWebhook : SchemaAggregateCommand
{
public Guid Id { get; set; }
}
}

2
src/Squidex.Domain.Apps.Write/Schemas/Commands/DisableField.cs

@ -8,7 +8,7 @@
namespace Squidex.Domain.Apps.Write.Schemas.Commands
{
public class DisableField : FieldCommand
public sealed class DisableField : FieldCommand
{
}
}

2
src/Squidex.Domain.Apps.Write/Schemas/Commands/EnableField.cs

@ -8,7 +8,7 @@
namespace Squidex.Domain.Apps.Write.Schemas.Commands
{
public class EnableField : FieldCommand
public sealed class EnableField : FieldCommand
{
}
}

2
src/Squidex.Domain.Apps.Write/Schemas/Commands/HideField.cs

@ -8,7 +8,7 @@
namespace Squidex.Domain.Apps.Write.Schemas.Commands
{
public class HideField : FieldCommand
public sealed class HideField : FieldCommand
{
}
}

2
src/Squidex.Domain.Apps.Write/Schemas/Commands/LockField.cs

@ -8,7 +8,7 @@
namespace Squidex.Domain.Apps.Write.Schemas.Commands
{
public class LockField : FieldCommand
public sealed class LockField : FieldCommand
{
}
}

2
src/Squidex.Domain.Apps.Write/Schemas/Commands/PublishSchema.cs

@ -8,7 +8,7 @@
namespace Squidex.Domain.Apps.Write.Schemas.Commands
{
public class PublishSchema : SchemaAggregateCommand
public sealed class PublishSchema : SchemaAggregateCommand
{
}
}

2
src/Squidex.Domain.Apps.Write/Schemas/Commands/ReorderFields.cs

@ -11,7 +11,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Write.Schemas.Commands
{
public class ReorderFields : SchemaAggregateCommand, IValidatable
public sealed class ReorderFields : SchemaAggregateCommand, IValidatable
{
public List<long> FieldIds { get; set; }

2
src/Squidex.Domain.Apps.Write/Schemas/Commands/ShowField.cs

@ -8,7 +8,7 @@
namespace Squidex.Domain.Apps.Write.Schemas.Commands
{
public class ShowField : FieldCommand
public sealed class ShowField : FieldCommand
{
}
}

2
src/Squidex.Domain.Apps.Write/Schemas/Commands/UnpublishSchema.cs

@ -8,7 +8,7 @@
namespace Squidex.Domain.Apps.Write.Schemas.Commands
{
public class UnpublishSchema : SchemaAggregateCommand
public sealed class UnpublishSchema : SchemaAggregateCommand
{
}
}

2
src/Squidex.Domain.Apps.Write/Schemas/Commands/UpdateField.cs

@ -12,7 +12,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Write.Schemas.Commands
{
public class UpdateField : FieldCommand, IValidatable
public sealed class UpdateField : FieldCommand, IValidatable
{
public FieldProperties Properties { get; set; }

2
src/Squidex.Domain.Apps.Write/Schemas/Commands/UpdateSchema.cs

@ -12,7 +12,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Write.Schemas.Commands
{
public class UpdateSchema : SchemaAggregateCommand, IValidatable
public sealed class UpdateSchema : SchemaAggregateCommand, IValidatable
{
public SchemaProperties Properties { get; set; }

10
src/Squidex.Domain.Apps.Write/Schemas/SchemaCommandMiddleware.cs

@ -60,16 +60,6 @@ namespace Squidex.Domain.Apps.Write.Schemas
});
}
protected Task On(AddWebhook command, CommandContext context)
{
return handler.UpdateAsync<SchemaDomainObject>(context, s => s.AddWebhook(command));
}
protected Task On(DeleteWebhook command, CommandContext context)
{
return handler.UpdateAsync<SchemaDomainObject>(context, s => s.DeleteWebhook(command));
}
protected Task On(DeleteSchema command, CommandContext context)
{
return handler.UpdateAsync<SchemaDomainObject>(context, s => s.Delete(command));

42
src/Squidex.Domain.Apps.Write/Schemas/SchemaDomainObject.cs

@ -23,7 +23,6 @@ namespace Squidex.Domain.Apps.Write.Schemas
public class SchemaDomainObject : DomainObjectBase
{
private readonly FieldRegistry registry;
private readonly HashSet<Guid> webhookIds = new HashSet<Guid>();
private bool isDeleted;
private long totalFields;
private Schema schema;
@ -115,16 +114,6 @@ namespace Squidex.Domain.Apps.Write.Schemas
schema = SchemaEventDispatcher.Dispatch(@event, schema);
}
protected void On(WebhookAdded @event)
{
webhookIds.Add(@event.Id);
}
protected void On(WebhookDeleted @event)
{
webhookIds.Remove(@event.Id);
}
protected void On(SchemaDeleted @event)
{
isDeleted = true;
@ -155,29 +144,6 @@ namespace Squidex.Domain.Apps.Write.Schemas
return this;
}
public SchemaDomainObject DeleteWebhook(DeleteWebhook command)
{
Guard.NotNull(command, nameof(command));
VerifyCreatedAndNotDeleted();
VerifyWebhookExists(command.Id);
RaiseEvent(SimpleMapper.Map(command, new WebhookDeleted()));
return this;
}
public SchemaDomainObject AddWebhook(AddWebhook command)
{
Guard.Valid(command, nameof(command), () => "Cannot add webhook");
VerifyCreatedAndNotDeleted();
RaiseEvent(SimpleMapper.Map(command, new WebhookAdded()));
return this;
}
public SchemaDomainObject AddField(AddField command)
{
Guard.Valid(command, nameof(command), () => $"Cannot add field to schema {Id}");
@ -335,14 +301,6 @@ namespace Squidex.Domain.Apps.Write.Schemas
RaiseEvent(@event);
}
private void VerifyWebhookExists(Guid id)
{
if (!webhookIds.Contains(id))
{
throw new DomainObjectNotFoundException(id.ToString(), "Webhooks", typeof(Schema));
}
}
private void VerifyNotCreated()
{
if (schema != null)

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

@ -23,8 +23,10 @@ using Squidex.Domain.Apps.Read.MongoDb.Assets;
using Squidex.Domain.Apps.Read.MongoDb.Contents;
using Squidex.Domain.Apps.Read.MongoDb.History;
using Squidex.Domain.Apps.Read.MongoDb.Schemas;
using Squidex.Domain.Apps.Read.MongoDb.Webhooks;
using Squidex.Domain.Apps.Read.Schemas.Repositories;
using Squidex.Domain.Apps.Read.Schemas.Services.Implementations;
using Squidex.Domain.Apps.Read.Webhooks.Repositories;
using Squidex.Domain.Users;
using Squidex.Domain.Users.MongoDb;
using Squidex.Domain.Users.MongoDb.Infrastructure;
@ -170,9 +172,9 @@ namespace Squidex.Config.Domain
.AsSelf()
.SingleInstance();
builder.RegisterType<MongoSchemaWebhookRepository>()
builder.RegisterType<MongoWebhookRepository>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseRegistration))
.As<ISchemaWebhookRepository>()
.As<IWebhookRepository>()
.As<IEventConsumer>()
.As<IExternalSystem>()
.AsSelf()

2
src/Squidex/Controllers/Api/Apps/AppsController.cs

@ -96,7 +96,7 @@ namespace Squidex.Controllers.Api.Apps
var context = await CommandBus.PublishAsync(command);
var result = context.Result<EntityCreatedResult<Guid>>();
var response = new EntityCreatedDto { Id = result.ToString(), Version = result.Version };
var response = new EntityCreatedDto { Id = result.IdOrValue.ToString(), Version = result.Version };
return CreatedAtAction(nameof(GetApps), response);
}

4
src/Squidex/Controllers/Api/Schemas/SchemaFieldsController.cs

@ -59,8 +59,8 @@ namespace Squidex.Controllers.Api.Schemas
var context = await CommandBus.PublishAsync(command);
var result = context.Result<EntityCreatedResult<long>>().IdOrValue;
var response = new EntityCreatedDto { Id = result.ToString() };
var result = context.Result<EntityCreatedResult<long>>();
var response = new EntityCreatedDto { Id = result.IdOrValue.ToString(), Version = result.Version };
return StatusCode(201, response);
}

7
src/Squidex/Controllers/Api/Schemas/SchemasController.cs

@ -122,9 +122,12 @@ namespace Squidex.Controllers.Api.Schemas
{
var command = request.ToCommand();
await CommandBus.PublishAsync(command);
var context = await CommandBus.PublishAsync(command);
return CreatedAtAction(nameof(GetSchema), new { name = request.Name }, new EntityCreatedDto { Id = command.Name });
var result = context.Result<EntityCreatedResult<Guid>>();
var response = new EntityCreatedDto { Id = command.Name, Version = result.Version };
return CreatedAtAction(nameof(GetSchema), new { name = request.Name }, response);
}
/// <summary>

7
src/Squidex/Controllers/Api/Webhooks/Models/CreateWebhookDto.cs

@ -7,6 +7,7 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Squidex.Controllers.Api.Webhooks.Models
@ -18,5 +19,11 @@ namespace Squidex.Controllers.Api.Webhooks.Models
/// </summary>
[Required]
public Uri Url { get; set; }
/// <summary>
/// The schema settings.
/// </summary>
[Required]
public List<WebhookSchemaDto> Schemas { get; set; }
}
}

32
src/Squidex/Controllers/Api/Webhooks/Models/WebhookCreatedDto.cs

@ -1,32 +0,0 @@
// ==========================================================================
// WebhookCreatedDto.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.ComponentModel.DataAnnotations;
namespace Squidex.Controllers.Api.Webhooks.Models
{
public class WebhookCreatedDto
{
/// <summary>
/// The id of the webhook.
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// The shared secret that is used to calculate the signature.
/// </summary>
[Required]
public string SharedSecret { get; set; }
/// <summary>
/// The id of the schema.
/// </summary>
public string SchemaId { get; set; }
}
}

35
src/Squidex/Controllers/Api/Webhooks/Models/WebhookDto.cs

@ -8,6 +8,9 @@
using System;
using System.ComponentModel.DataAnnotations;
using NodaTime;
using Squidex.Infrastructure;
using System.Collections.Generic;
namespace Squidex.Controllers.Api.Webhooks.Models
{
@ -19,9 +22,31 @@ namespace Squidex.Controllers.Api.Webhooks.Models
public Guid Id { get; set; }
/// <summary>
/// The id of the schema.
/// The user that has created the webhook.
/// </summary>
public Guid SchemaId { get; set; }
[Required]
public RefToken CreatedBy { get; set; }
/// <summary>
/// The user that has updated the webhook.
/// </summary>
[Required]
public RefToken LastModifiedBy { get; set; }
/// <summary>
/// The date and time when the webhook has been created.
/// </summary>
public Instant Created { get; set; }
/// <summary>
/// The date and time when the webhook has been modified last.
/// </summary>
public Instant LastModified { get; set; }
/// <summary>
/// The version of the webhook.
/// </summary>
public int Version { get; set; }
/// <summary>
/// The number of succceeded calls.
@ -54,5 +79,11 @@ namespace Squidex.Controllers.Api.Webhooks.Models
/// </summary>
[Required]
public string SharedSecret { get; set; }
/// <summary>
/// The schema settings.
/// </summary>
[Required]
public List<WebhookSchemaDto> Schemas { get; set; }
}
}

2
src/Squidex/Controllers/Api/Webhooks/Models/WebhookEventDto.cs

@ -9,7 +9,7 @@
using System;
using System.ComponentModel.DataAnnotations;
using NodaTime;
using Squidex.Domain.Apps.Read.Schemas;
using Squidex.Domain.Apps.Read.Webhooks;
namespace Squidex.Controllers.Api.Webhooks.Models
{

80
src/Squidex/Controllers/Api/Webhooks/WebhooksController.cs

@ -10,12 +10,12 @@ using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using NodaTime;
using NSwag.Annotations;
using Squidex.Controllers.Api.Webhooks.Models;
using Squidex.Domain.Apps.Read.Schemas.Repositories;
using Squidex.Domain.Apps.Write.Schemas.Commands;
using Squidex.Domain.Apps.Core.Webhooks;
using Squidex.Domain.Apps.Read.Webhooks.Repositories;
using Squidex.Domain.Apps.Write.Webhooks.Commands;
using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline;
@ -31,11 +31,11 @@ namespace Squidex.Controllers.Api.Webhooks
[MustBeAppDeveloper]
public class WebhooksController : ControllerBase
{
private readonly ISchemaWebhookRepository webhooksRepository;
private readonly IWebhookRepository webhooksRepository;
private readonly IWebhookEventRepository webhookEventsRepository;
public WebhooksController(ICommandBus commandBus,
ISchemaWebhookRepository webhooksRepository,
IWebhookRepository webhooksRepository,
IWebhookEventRepository webhookEventsRepository)
: base(commandBus)
{
@ -59,14 +59,14 @@ namespace Squidex.Controllers.Api.Webhooks
{
var webhooks = await webhooksRepository.QueryByAppAsync(App.Id);
Response.Headers["ETag"] = new StringValues(App.Version.ToString());
var response = webhooks.Select(w =>
{
var count = w.TotalTimedout + w.TotalSucceeded + w.TotalFailed;
var average = count == 0 ? 0 : w.TotalRequestTime / count;
var totalCount = w.TotalTimedout + w.TotalSucceeded + w.TotalFailed;
var totalAverage = totalCount == 0 ? 0 : w.TotalRequestTime / totalCount;
var schemas = w.Schemas.Select(s => SimpleMapper.Map(s, new WebhookSchemaDto())).ToList();
return SimpleMapper.Map(w, new WebhookDto { AverageRequestTimeMs = average });
return SimpleMapper.Map(w, new WebhookDto { AverageRequestTimeMs = totalAverage, Schemas = schemas });
});
return Ok(response);
@ -76,50 +76,78 @@ namespace Squidex.Controllers.Api.Webhooks
/// Create a new webhook.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param>
/// <param name="request">The webhook object that needs to be added to the app.</param>
/// <returns>
/// 201 => Webhook created.
/// 400 => Webhook name or properties are not valid.
/// 409 => Webhook name already in use.
/// 404 => App or schema not found.
/// 400 => Webhook is not valid.
/// 404 => App not found.
/// </returns>
/// <remarks>
/// All events for the specified app will be sent to the url. The timeout is 2 seconds.
/// All events for the specified schemas will be sent to the url. The timeout is 2 seconds.
/// </remarks>
[HttpPost]
[Route("apps/{app}/schemas/{name}/webhooks/")]
[ProducesResponseType(typeof(WebhookCreatedDto), 201)]
[Route("apps/{app}/webhooks/")]
[ProducesResponseType(typeof(EntityCreatedDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ProducesResponseType(typeof(ErrorDto), 409)]
[ApiCosts(1)]
public async Task<IActionResult> PostWebhook(string app, string name, [FromBody] CreateWebhookDto request)
public async Task<IActionResult> PostWebhook(string app, [FromBody] CreateWebhookDto request)
{
var command = new AddWebhook { Url = request.Url };
var schemas = request.Schemas.Select(s => SimpleMapper.Map(s, new WebhookSchema())).ToList();
await CommandBus.PublishAsync(command);
var command = new CreateWebhook { Url = request.Url, Schemas = schemas };
var context = await CommandBus.PublishAsync(command);
var response = SimpleMapper.Map(command, new WebhookCreatedDto { SchemaId = command.SchemaId.Id.ToString() });
var result = context.Result<EntityCreatedResult<Guid>>();
var response = new EntityCreatedDto { Id = result.IdOrValue.ToString(), Version = result.Version };
return CreatedAtAction(nameof(GetWebhooks), new { app }, response);
}
/// <summary>
/// Update a webhook.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="id">The id of the webhook to update.</param>
/// <param name="request">The webhook object that needs to be added to the app.</param>
/// <returns>
/// 203 => Webhook updated.
/// 400 => Webhook is not valid.
/// 404 => App or webhook not found.
/// </returns>
/// <remarks>
/// All events for the specified schemas will be sent to the url. The timeout is 2 seconds.
/// </remarks>
[HttpPut]
[Route("apps/{app}/webhooks/{id}")]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiCosts(1)]
public async Task<IActionResult> PutWebhook(string app, Guid id, [FromBody] CreateWebhookDto request)
{
var schemas = request.Schemas.Select(s => SimpleMapper.Map(s, new WebhookSchema())).ToList();
var command = new UpdateWebhook { WebhookId = id, Url = request.Url, Schemas = schemas };
await CommandBus.PublishAsync(command);
return NoContent();
}
/// <summary>
/// Delete a webhook.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param>
/// <param name="id">The id of the webhook to delete.</param>
/// <returns>
/// 204 => Webhook has been deleted.
/// 404 => Webhook or shema or app not found.
/// </returns>
[HttpDelete]
[Route("apps/{app}/schemas/{name}/webhooks/{id}")]
[Route("apps/{app}//webhooks/{id}")]
[ApiCosts(1)]
public async Task<IActionResult> DeleteWebhook(string app, string name, Guid id)
public async Task<IActionResult> DeleteWebhook(string app, Guid id)
{
await CommandBus.PublishAsync(new DeleteWebhook { Id = id });
await CommandBus.PublishAsync(new DeleteWebhook { WebhookId = id });
return NoContent();
}

130
tests/Squidex.Domain.Apps.Read.Tests/Schemas/WebhookDequeuerTests.cs

@ -1,130 +0,0 @@
// ==========================================================================
// WebhookDequeuerTests.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Threading;
using System.Threading.Tasks;
using FakeItEasy;
using NodaTime;
using Squidex.Domain.Apps.Read.Schemas.Repositories;
using Squidex.Infrastructure.Log;
using Xunit;
// ReSharper disable MethodSupportsCancellation
// ReSharper disable ImplicitlyCapturedClosure
// ReSharper disable ConvertToConstant.Local
namespace Squidex.Domain.Apps.Read.Schemas
{
public class WebhookDequeuerTests
{
private readonly IClock clock = A.Fake<IClock>();
private readonly ISchemaWebhookRepository webhookRepository = A.Fake<ISchemaWebhookRepository>();
private readonly IWebhookEventRepository webhookEventRepository = A.Fake<IWebhookEventRepository>();
private readonly WebhookSender webhookSender = A.Fake<WebhookSender>();
private readonly Instant now = SystemClock.Instance.GetCurrentInstant();
public WebhookDequeuerTests()
{
A.CallTo(() => clock.GetCurrentInstant()).Returns(now);
}
[Fact]
public void Should_update_repositories_on_successful_requests()
{
var @event = CreateEvent(0);
var requestResult = WebhookResult.Success;
var requestTime = TimeSpan.FromMinutes(1);
var requestDump = "Dump";
SetupSender(@event, requestDump, requestResult, requestTime);
SetupPendingEvents(@event);
var sut = new WebhookDequeuer(
webhookSender,
webhookEventRepository,
webhookRepository,
clock, A.Fake<ISemanticLog>());
sut.Next();
sut.Dispose();
VerifyRepositories(@event, requestDump, requestResult, requestTime, null);
}
[Theory]
[InlineData(0, 5)]
[InlineData(1, 60)]
[InlineData(2, 300)]
[InlineData(3, 360)]
public void Should_set_next_attempt_based_on_num_calls(int calls, int minutes)
{
var @event = CreateEvent(calls);
var requestResult = WebhookResult.Failed;
var requestTime = TimeSpan.FromMinutes(1);
var requestDump = "Dump";
SetupSender(@event, requestDump, requestResult, requestTime);
SetupPendingEvents(@event);
var sut = new WebhookDequeuer(
webhookSender,
webhookEventRepository,
webhookRepository,
clock, A.Fake<ISemanticLog>());
sut.Next();
sut.Dispose();
VerifyRepositories(@event, requestDump, requestResult, requestTime, now.Plus(Duration.FromMinutes(minutes)));
}
private void SetupSender(IWebhookEventEntity @event, string requestDump, WebhookResult requestResult, TimeSpan requestTime)
{
A.CallTo(() => webhookSender.SendAsync(@event.Job))
.Returns(Task.FromResult((requestDump, requestResult, requestTime)));
}
private void SetupPendingEvents(IWebhookEventEntity @event)
{
A.CallTo(() => webhookEventRepository.QueryPendingAsync(A<Func<IWebhookEventEntity, Task>>.Ignored, A<CancellationToken>.Ignored))
.Invokes(async (Func<IWebhookEventEntity, Task> callback, CancellationToken ct) =>
{
await callback(@event);
});
}
private void VerifyRepositories(IWebhookEventEntity @event, string requestDump, WebhookResult requestResult, TimeSpan requestTime, Instant? nextAttempt)
{
A.CallTo(() => webhookEventRepository.TraceSendingAsync(@event.Id))
.MustHaveHappened();
A.CallTo(() => webhookEventRepository.TraceSendingAsync(@event.Id))
.MustHaveHappened();
A.CallTo(() => webhookEventRepository.TraceSentAsync(@event.Id, requestDump, requestResult, requestTime, nextAttempt))
.MustHaveHappened();
A.CallTo(() => webhookRepository.TraceSentAsync(@event.Job.WebhookId, requestResult, requestTime))
.MustHaveHappened();
}
private static IWebhookEventEntity CreateEvent(int numCalls)
{
var @event = A.Fake<IWebhookEventEntity>();
A.CallTo(() => @event.Id).Returns(Guid.NewGuid());
A.CallTo(() => @event.Job).Returns(new WebhookJob { WebhookId = Guid.NewGuid() });
A.CallTo(() => @event.NumCalls).Returns(numCalls);
return @event;
}
}
}

118
tests/Squidex.Domain.Apps.Read.Tests/Schemas/WebhookEnqueuerTests.cs

@ -1,118 +0,0 @@
// ==========================================================================
// WebhookEnqueuerTests.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using FakeItEasy;
using Newtonsoft.Json;
using NodaTime;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Domain.Apps.Read.Schemas.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
using Xunit;
// ReSharper disable MethodSupportsCancellation
// ReSharper disable ImplicitlyCapturedClosure
// ReSharper disable ConvertToConstant.Local
namespace Squidex.Domain.Apps.Read.Schemas
{
public class WebhookEnqueuerTests
{
private readonly IClock clock = A.Fake<IClock>();
private readonly ISchemaWebhookRepository webhookRepository = A.Fake<ISchemaWebhookRepository>();
private readonly IWebhookEventRepository webhookEventRepository = A.Fake<IWebhookEventRepository>();
private readonly TypeNameRegistry typeNameRegisty = new TypeNameRegistry();
private readonly Instant now = SystemClock.Instance.GetCurrentInstant();
private readonly WebhookEnqueuer sut;
public WebhookEnqueuerTests()
{
A.CallTo(() => clock.GetCurrentInstant()).Returns(now);
typeNameRegisty.Map(typeof(ContentCreated));
sut = new WebhookEnqueuer(
typeNameRegisty,
webhookEventRepository,
webhookRepository,
clock, new JsonSerializer());
}
[Fact]
public void Should_return_contents_filter_for_events_filter()
{
Assert.Equal("^content-", sut.EventsFilter);
}
[Fact]
public void Should_return_type_name_for_name()
{
Assert.Equal(typeof(WebhookEnqueuer).Name, sut.Name);
}
[Fact]
public Task Should_do_nothing_on_clear()
{
return sut.ClearAsync();
}
[Fact]
public async Task Should_update_repositories_on_successful_requests()
{
var appId = new NamedId<Guid>(Guid.NewGuid(), "my-app");
var schemaId = new NamedId<Guid>(Guid.NewGuid(), "my-schema");
var @event = Envelope.Create(new ContentCreated { AppId = appId, SchemaId = schemaId });
var webhook1 = CreateWebhook(1);
var webhook2 = CreateWebhook(2);
A.CallTo(() => webhookRepository.QueryUrlsBySchemaAsync(appId.Id, schemaId.Id))
.Returns(Task.FromResult<IReadOnlyList<ISchemaWebhookUrlEntity>>(new List<ISchemaWebhookUrlEntity> { webhook1, webhook2 }));
await sut.On(@event);
A.CallTo(() => webhookEventRepository.EnqueueAsync(
A<WebhookJob>.That.Matches(webhookJob =>
!string.IsNullOrWhiteSpace(webhookJob.RequestSignature)
&& !string.IsNullOrWhiteSpace(webhookJob.RequestBody)
&& webhookJob.Id != Guid.Empty
&& webhookJob.Expires == now.Plus(Duration.FromDays(2))
&& webhookJob.AppId == appId.Id
&& webhookJob.EventName == "MySchemaCreatedEvent"
&& webhookJob.RequestUrl == webhook1.Url
&& webhookJob.WebhookId == webhook1.Id), now)).MustHaveHappened();
A.CallTo(() => webhookEventRepository.EnqueueAsync(
A<WebhookJob>.That.Matches(webhookJob =>
!string.IsNullOrWhiteSpace(webhookJob.RequestSignature)
&& !string.IsNullOrWhiteSpace(webhookJob.RequestBody)
&& webhookJob.Id != Guid.Empty
&& webhookJob.Expires == now.Plus(Duration.FromDays(2))
&& webhookJob.AppId == appId.Id
&& webhookJob.EventName == "MySchemaCreatedEvent"
&& webhookJob.RequestUrl == webhook2.Url
&& webhookJob.WebhookId == webhook2.Id), now)).MustHaveHappened();
}
private static ISchemaWebhookUrlEntity CreateWebhook(int offset)
{
var webhook = A.Dummy<ISchemaWebhookUrlEntity>();
A.CallTo(() => webhook.Id).Returns(Guid.NewGuid());
A.CallTo(() => webhook.Url).Returns(new Uri($"http://domain{offset}.com"));
A.CallTo(() => webhook.SharedSecret).Returns($"secret{offset}");
return webhook;
}
}
}

34
tests/Squidex.Domain.Apps.Write.Tests/Schemas/SchemaCommandHandlerTests.cs

@ -251,40 +251,6 @@ namespace Squidex.Domain.Apps.Write.Schemas
});
}
[Fact]
public async Task AddWebhook_should_update_domain_object()
{
CreateSchema();
var context = CreateContextForCommand(new AddWebhook { Url = new Uri("http://cloud.squidex.io") });
await TestUpdate(schema, async _ =>
{
await sut.HandleAsync(context);
});
}
[Fact]
public async Task DeleteWebhook_should_update_domain_object()
{
var createCommand = new AddWebhook { Url = new Uri("http://cloud.squidex.io") };
CreateSchema();
CreateWebhook(createCommand);
var context = CreateContextForCommand(new DeleteWebhook { Id = createCommand.Id });
await TestUpdate(schema, async _ =>
{
await sut.HandleAsync(context);
});
}
private void CreateWebhook(AddWebhook command)
{
schema.AddWebhook(command);
}
private void CreateSchema()
{
schema.Create(CreateCommand(new CreateSchema { Name = SchemaName }));

94
tests/Squidex.Domain.Apps.Write.Tests/Schemas/SchemaDomainObjectTests.cs

@ -715,100 +715,6 @@ namespace Squidex.Domain.Apps.Write.Schemas
);
}
[Fact]
public void AddWebhook_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.AddWebhook(CreateCommand(new AddWebhook { Url = new Uri("https://cloud.squidex.io") }));
});
}
[Fact]
public void AddWebhook_should_throw_exception_if_command_is_not_valid()
{
Assert.Throws<ValidationException>(() =>
{
sut.AddWebhook(CreateCommand(new AddWebhook()));
});
}
[Fact]
public void AddWebhook_should_throw_exception_if_schema_is_deleted()
{
CreateSchema();
DeleteSchema();
Assert.Throws<DomainException>(() =>
{
sut.AddWebhook(CreateCommand(new AddWebhook { Url = new Uri("https://cloud.squidex.io") }));
});
}
[Fact]
public void AddWebhook_should_update_schema_and_create_events()
{
var command = new AddWebhook { Url = new Uri("https://cloud.squidex.io") };
CreateSchema();
sut.AddWebhook(CreateCommand(command));
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateEvent(new WebhookAdded { Id = command.Id, Url = command.Url, SharedSecret = command.SharedSecret })
);
}
[Fact]
public void DeleteWebhook_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.DeleteWebhook(CreateCommand(new DeleteWebhook()));
});
}
[Fact]
public void DeleteWebhook_should_throw_exception_if_webhook_not_found()
{
CreateSchema();
Assert.Throws<DomainObjectNotFoundException>(() =>
{
sut.DeleteWebhook(CreateCommand(new DeleteWebhook { Id = Guid.NewGuid() }));
});
}
[Fact]
public void DeleteWebhook_should_throw_exception_if_schema_is_deleted()
{
CreateSchema();
DeleteSchema();
Assert.Throws<DomainException>(() =>
{
sut.DeleteWebhook(CreateCommand(new DeleteWebhook { Id = Guid.NewGuid() }));
});
}
[Fact]
public void DeleteWebhook_should_update_schema_and_create_events()
{
var createCommand = new AddWebhook { Url = new Uri("https://cloud.squidex.io") };
CreateSchema();
sut.AddWebhook(CreateCommand(createCommand));
sut.DeleteWebhook(CreateCommand(new DeleteWebhook { Id = createCommand.Id }));
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateEvent(new WebhookAdded { Id = createCommand.Id, Url = createCommand.Url, SharedSecret = createCommand.SharedSecret }),
CreateEvent(new WebhookDeleted { Id = createCommand.Id })
);
}
private void CreateField()
{
sut.AddField(new AddField { Name = fieldName, Properties = new NumberFieldProperties() });

Loading…
Cancel
Save