From c2ec70e1113b4b692e02bf3b0f8770aee4cdd06c Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 21 Jan 2020 15:43:49 +0100 Subject: [PATCH] Refactoring/domain objects and Bulk Import (#471) --- .../ValidateContent/ValidationContext.cs | 50 +++--- .../ValidateContent/ValidationMode.cs | 15 ++ .../Validators/AssetsValidator.cs | 5 + .../Validators/ReferencesValidator.cs | 5 + .../Validators/UniqueValidator.cs | 5 + .../Assets/MongoAssetEntity.cs | 2 +- .../MongoAssetRepository_SnapshotStore.cs | 2 +- .../Contents/MongoContentCollection.cs | 26 +--- .../Contents/MongoContentEntity.cs | 2 +- .../Apps/{AppGrain.cs => AppDomainObject.cs} | 12 +- .../Apps/AppDomainObjectGrain.cs} | 19 +-- .../{AssetGrain.cs => AssetDomainObject.cs} | 22 +-- .../Assets/AssetDomainObjectGrain.cs | 41 +++++ ...derGrain.cs => AssetFolderDomainObject.cs} | 17 +-- .../Assets/AssetFolderDomainObjectGrain.cs | 40 +++++ .../Assets/BackupAssets.cs | 4 +- .../Contents/BackupContents.cs | 2 +- .../Contents/Commands/CreateContent.cs | 4 + .../Contents/Commands/CreateContents.cs | 31 ++++ ...ContentGrain.cs => ContentDomainObject.cs} | 43 +++--- .../Contents/ContentDomainObjectGrain.cs | 33 ++++ .../ContentImporterCommandMiddleware.cs | 69 +++++++++ .../Contents/ContentOperationContext.cs | 17 ++- .../Contents/ImportResult.cs | 15 ++ .../Contents/ImportResultItem.cs | 18 +++ .../Contents/SingletonCommandMiddleware.cs | 12 +- .../{RuleGrain.cs => RuleDomainObject.cs} | 12 +- .../Rules/RuleDomainObjectGrain.cs | 30 ++++ .../{SchemaGrain.cs => SchemaDomainObject.cs} | 6 +- .../Schemas/SchemaDomainObjectGrain.cs | 30 ++++ .../MongoDb/MongoExtensions.cs | 18 ++- .../Commands/DomainObject.cs | 97 ++++++++++++ ...ObjectGrainBase.cs => DomainObjectBase.cs} | 105 ++++++------- .../Commands/DomainObjectGrain.cs | 67 +++------ ...ectGrain.cs => LogSnapshotDomainObject.cs} | 34 +++-- .../Commands/Rebuilder.cs | 31 ++-- .../States/Persistence{TSnapshot,TKey}.cs | 2 +- .../src/Squidex.Web/ApiExceptionConverter.cs | 129 ++++++++++++++++ .../ApiExceptionFilterAttribute.cs | 109 +++----------- backend/src/Squidex.Web/ErrorDto.cs | 6 + .../Contents/ContentsController.cs | 38 ++++- .../Generator/SchemaOpenApiGenerator.cs | 2 +- .../Contents/Models/ImportContentsDto.cs | 44 ++++++ .../Contents/Models/ImportResultDto.cs | 32 ++++ .../src/Squidex/Config/Domain/AppsServices.cs | 3 + .../Squidex/Config/Domain/AssetServices.cs | 6 + .../Squidex/Config/Domain/CommandsServices.cs | 3 + .../Squidex/Config/Domain/ContentsServices.cs | 3 + .../src/Squidex/Config/Domain/RuleServices.cs | 3 + .../Squidex/Config/Domain/SchemasServices.cs | 3 + .../ValidateContent/AssetsFieldTests.cs | 12 ++ .../ValidateContent/ReferencesFieldTests.cs | 10 ++ .../Validators/UniqueValidatorTests.cs | 27 +++- ...pGrainTests.cs => AppDomainObjectTests.cs} | 12 +- .../Apps/Indexes/AppsIndexTests.cs | 12 +- .../InviteUserCommandMiddlewareTests.cs | 16 +- .../Assets/AssetCommandMiddlewareTests.cs | 13 +- .../Assets/AssetDomainObjectGrainTests.cs | 35 +++++ ...rainTests.cs => AssetDomainObjectTests.cs} | 21 +-- .../AssetFolderDomainObjectGrainTests.cs | 35 +++++ ...sts.cs => AssetFolderDomainObjectTests.cs} | 21 +-- .../Assets/BackupAssetsTests.cs | 4 +- .../Contents/BackupContentsTests.cs | 2 +- .../Contents/ContentDomainObjectGrainTests.cs | 35 +++++ ...inTests.cs => ContentDomainObjectTests.cs} | 112 +++++++------- .../ContentImporterCommandMiddlewareTests.cs | 142 ++++++++++++++++++ .../SingletonCommandMiddlewareTests.cs | 12 +- .../Rules/Indexes/RulesIndexTests.cs | 8 +- ...GrainTests.cs => RuleDomainObjectTests.cs} | 12 +- .../Schemas/Indexes/SchemasIndexTests.cs | 4 +- ...ainTests.cs => SchemaDomainObjectTests.cs} | 12 +- ...jectGrainTests.cs => DomainObjectTests.cs} | 67 +++++---- ...sts.cs => LogSnapshotDomainObjectTests.cs} | 67 +++++---- .../States/PersistenceEventSourcingTests.cs | 15 +- .../States/PersistenceSnapshotTests.cs | 11 ++ .../TestHelpers/MyDomainObject.cs | 4 +- .../ApiExceptionFilterAttributeTests.cs | 63 ++++++-- .../tools/Migrate_01/RebuilderExtensions.cs | 12 +- .../services/local-store.service.spec.ts | 2 + .../framework/services/local-store.service.ts | 14 +- .../internal/notifications-menu.component.ts | 32 ++-- 81 files changed, 1564 insertions(+), 604 deletions(-) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationMode.cs rename backend/src/Squidex.Domain.Apps.Entities/Apps/{AppGrain.cs => AppDomainObject.cs} (97%) rename backend/{tests/Squidex.Infrastructure.Tests/TestHelpers/MyGrain.cs => src/Squidex.Domain.Apps.Entities/Apps/AppDomainObjectGrain.cs} (51%) rename backend/src/Squidex.Domain.Apps.Entities/Assets/{AssetGrain.cs => AssetDomainObject.cs} (87%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObjectGrain.cs rename backend/src/Squidex.Domain.Apps.Entities/Assets/{AssetFolderGrain.cs => AssetFolderDomainObject.cs} (84%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/AssetFolderDomainObjectGrain.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContents.cs rename backend/src/Squidex.Domain.Apps.Entities/Contents/{ContentGrain.cs => ContentDomainObject.cs} (91%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObjectGrain.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/ContentImporterCommandMiddleware.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/ImportResult.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/ImportResultItem.cs rename backend/src/Squidex.Domain.Apps.Entities/Rules/{RuleGrain.cs => RuleDomainObject.cs} (90%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObjectGrain.cs rename backend/src/Squidex.Domain.Apps.Entities/Schemas/{SchemaGrain.cs => SchemaDomainObject.cs} (98%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObjectGrain.cs create mode 100644 backend/src/Squidex.Infrastructure/Commands/DomainObject.cs rename backend/src/Squidex.Infrastructure/Commands/{DomainObjectGrainBase.cs => DomainObjectBase.cs} (65%) rename backend/src/Squidex.Infrastructure/Commands/{LogSnapshotDomainObjectGrain.cs => LogSnapshotDomainObject.cs} (76%) create mode 100644 backend/src/Squidex.Web/ApiExceptionConverter.cs create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ImportContentsDto.cs create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ImportResultDto.cs rename backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/{AppGrainTests.cs => AppDomainObjectTests.cs} (98%) create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectGrainTests.cs rename backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/{AssetGrainTests.cs => AssetDomainObjectTests.cs} (93%) create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetFolderDomainObjectGrainTests.cs rename backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/{AssetFolderGrainTests.cs => AssetFolderDomainObjectTests.cs} (89%) create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectGrainTests.cs rename backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/{ContentGrainTests.cs => ContentDomainObjectTests.cs} (82%) create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentImporterCommandMiddlewareTests.cs rename backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/{RuleGrainTests.cs => RuleDomainObjectTests.cs} (95%) rename backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/{SchemaGrainTests.cs => SchemaDomainObjectTests.cs} (98%) rename backend/tests/Squidex.Infrastructure.Tests/Commands/{DomainObjectGrainTests.cs => DomainObjectTests.cs} (77%) rename backend/tests/Squidex.Infrastructure.Tests/Commands/{LogSnapshotDomainObjectGrainTests.cs => LogSnapshotDomainObjectTests.cs} (81%) diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs index 3bddf058d..8d2a2a6b0 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs @@ -46,13 +46,16 @@ namespace Squidex.Domain.Apps.Core.ValidateContent public bool IsOptional { get; } + public ValidationMode Mode { get; } + public ValidationContext( Guid contentId, Guid schemaId, CheckContents checkContent, CheckContentsByIds checkContentsByIds, - CheckAssets checkAsset) - : this(contentId, schemaId, checkContent, checkContentsByIds, checkAsset, ImmutableQueue.Empty, false) + CheckAssets checkAsset, + ValidationMode mode = ValidationMode.Default) + : this(contentId, schemaId, checkContent, checkContentsByIds, checkAsset, ImmutableQueue.Empty, false, mode) { } @@ -63,7 +66,8 @@ namespace Squidex.Domain.Apps.Core.ValidateContent CheckContentsByIds checkContentByIds, CheckAssets checkAsset, ImmutableQueue propertyPath, - bool isOptional) + bool isOptional, + ValidationMode mode = ValidationMode.Default) { Guard.NotNull(checkAsset); Guard.NotNull(checkContent); @@ -78,35 +82,47 @@ namespace Squidex.Domain.Apps.Core.ValidateContent this.schemaId = schemaId; + Mode = mode; + IsOptional = isOptional; } - public ValidationContext Optional(bool isOptional) + public ValidationContext Optimized(bool isOptimized = true) { - return isOptional == IsOptional ? this : OptionalCore(isOptional); + var mode = isOptimized ? ValidationMode.Optimized : ValidationMode.Default; + + if (Mode == mode) + { + return this; + } + + return Clone(propertyPath, IsOptional, mode); } - private ValidationContext OptionalCore(bool isOptional) + public ValidationContext Optional(bool isOptional) { - return new ValidationContext( - contentId, - schemaId, - checkContent, - checkContentByIds, - checkAsset, - propertyPath, - isOptional); + if (IsOptional == isOptional) + { + return this; + } + + return Clone(propertyPath, isOptional, Mode); } public ValidationContext Nested(string property) + { + return Clone(propertyPath.Enqueue(property), IsOptional, Mode); + } + + private ValidationContext Clone(ImmutableQueue path, bool isOptional, ValidationMode mode) { return new ValidationContext( - contentId, schemaId, + contentId, + schemaId, checkContent, checkContentByIds, checkAsset, - propertyPath.Enqueue(property), - IsOptional); + path, isOptional, mode); } public Task> GetContentIdsAsync(HashSet ids) diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationMode.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationMode.cs new file mode 100644 index 000000000..1230e45dd --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationMode.cs @@ -0,0 +1,15 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.ValidateContent +{ + public enum ValidationMode + { + Default, + Optimized + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs index bc0103253..46dacdba3 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs @@ -26,6 +26,11 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators public async Task ValidateAsync(object? value, ValidationContext context, AddError addError) { + if (context.Mode == ValidationMode.Optimized) + { + return; + } + if (value is ICollection assetIds && assetIds.Count > 0) { var assets = await context.GetAssetInfosAsync(assetIds); diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs index 09815efd4..55ab72669 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs @@ -23,6 +23,11 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators public async Task ValidateAsync(object? value, ValidationContext context, AddError addError) { + if (context.Mode == ValidationMode.Optimized) + { + return; + } + if (value is ICollection contentIds) { var foundIds = await context.GetContentIdsAsync(contentIds.ToHashSet()); diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs index fee6cf8e7..1d7f1d9ac 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs @@ -16,6 +16,11 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { public async Task ValidateAsync(object? value, ValidationContext context, AddError addError) { + if (context.Mode == ValidationMode.Optimized) + { + return; + } + var count = context.Path.Count(); if (value != null && (count == 0 || (count == 2 && context.Path.Last() == InvariantPartitioning.Key))) diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs index 913a39fdc..8301989ec 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs @@ -17,7 +17,7 @@ using Squidex.Infrastructure.MongoDb; namespace Squidex.Domain.Apps.Entities.MongoDb.Assets { - public sealed class MongoAssetEntity : IAssetEntity + public sealed class MongoAssetEntity : IAssetEntity, IVersionedEntity { [BsonId] [BsonElement("_id")] diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs index c50f90efd..ea27c74ab 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs @@ -47,7 +47,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets entity.Version = newVersion; entity.IndexedAppId = value.AppId.Id; - await Collection.ReplaceOneAsync(x => x.Id == key && x.Version == oldVersion, entity, Upsert); + await Collection.UpsertVersionedAsync(key, oldVersion, entity); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs index d9bbaa630..3d95fb581 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs @@ -24,7 +24,6 @@ using Squidex.Infrastructure.Json; using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { @@ -263,30 +262,9 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents return Collection.DeleteOneAsync(x => x.Id == id); } - public async Task UpsertAsync(MongoContentEntity content, long oldVersion) + public Task UpsertAsync(MongoContentEntity content, long oldVersion) { - try - { - await Collection.ReplaceOneAsync(x => x.Id == content.Id && x.Version == oldVersion, content, Upsert); - } - catch (MongoWriteException ex) - { - if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) - { - var existingVersion = - await Collection.Find(x => x.Id == content.Id).Only(x => x.Id, x => x.Version) - .FirstOrDefaultAsync(); - - if (existingVersion != null) - { - throw new InconsistentStateException(existingVersion["vs"].AsInt64, oldVersion, ex); - } - } - else - { - throw; - } - } + return Collection.UpsertVersionedAsync(content.Id, oldVersion, content); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs index 96e991253..7d32fd556 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs @@ -19,7 +19,7 @@ using Squidex.Infrastructure.MongoDb; namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { - public sealed class MongoContentEntity : IContentEntity + public sealed class MongoContentEntity : IContentEntity, IVersionedEntity { private NamedContentData? data; private NamedContentData dataDraft; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs similarity index 97% rename from backend/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs index 370bb034b..46fbc77b5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs @@ -19,21 +19,20 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.States; using Squidex.Shared.Users; namespace Squidex.Domain.Apps.Entities.Apps { - public sealed class AppGrain : DomainObjectGrain, IAppGrain + public class AppDomainObject : DomainObject { private readonly InitialPatterns initialPatterns; private readonly IAppPlansProvider appPlansProvider; private readonly IAppPlanBillingManager appPlansBillingManager; private readonly IUserResolver userResolver; - public AppGrain( + public AppDomainObject( InitialPatterns initialPatterns, IStore store, ISemanticLog log, @@ -53,7 +52,7 @@ namespace Squidex.Domain.Apps.Entities.Apps this.initialPatterns = initialPatterns; } - protected override Task ExecuteAsync(IAggregateCommand command) + public override Task ExecuteAsync(IAggregateCommand command) { VerifyNotArchived(); @@ -500,10 +499,5 @@ namespace Squidex.Domain.Apps.Entities.Apps { return new AppContributorAssigned { ContributorId = actor.Identifier, Role = Role.Owner }; } - - public Task> GetStateAsync() - { - return J.AsTask(Snapshot); - } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObjectGrain.cs similarity index 51% rename from backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObjectGrain.cs index 56cefc7d7..4a590f9d9 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObjectGrain.cs @@ -7,23 +7,24 @@ using System; using System.Threading.Tasks; -using FakeItEasy; +using Squidex.Domain.Apps.Entities.Apps.State; using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.States; +using Squidex.Infrastructure.Orleans; -namespace Squidex.Infrastructure.TestHelpers +namespace Squidex.Domain.Apps.Entities.Apps { - public class MyGrain : DomainObjectGrain + public sealed class AppDomainObjectGrain : DomainObjectGrain, IAppGrain { - public MyGrain(IStore store) - : base(store, A.Dummy()) + public AppDomainObjectGrain(IServiceProvider serviceProvider) + : base(serviceProvider) { } - protected override Task ExecuteAsync(IAggregateCommand command) + public async Task> GetStateAsync() { - return Task.FromResult(null); + await DomainObject.EnsureLoadedAsync(); + + return Snapshot; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs similarity index 87% rename from backend/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs index 2c27323a7..244b595eb 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs @@ -18,19 +18,17 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.Assets { - public sealed class AssetGrain : LogSnapshotDomainObjectGrain, IAssetGrain + public class AssetDomainObject : LogSnapshotDomainObject { - private static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(5); private readonly ITagService tagService; private readonly IAssetQueryService assetQuery; - public AssetGrain(IStore store, ITagService tagService, IAssetQueryService assetQuery, IActivationLimit limit, ISemanticLog log) + public AssetDomainObject(IStore store, ITagService tagService, IAssetQueryService assetQuery, ISemanticLog log) : base(store, log) { Guard.NotNull(tagService); @@ -39,18 +37,9 @@ namespace Squidex.Domain.Apps.Entities.Assets this.tagService = tagService; this.assetQuery = assetQuery; - - limit?.SetLimit(5000, Lifetime); - } - - protected override Task OnActivateAsync(Guid key) - { - TryDelayDeactivation(Lifetime); - - return base.OnActivateAsync(key); } - protected override Task ExecuteAsync(IAggregateCommand command) + public override Task ExecuteAsync(IAggregateCommand command) { VerifyNotDeleted(); @@ -186,10 +175,5 @@ namespace Squidex.Domain.Apps.Entities.Assets throw new DomainException("Asset has already been deleted"); } } - - public Task> GetStateAsync(long version = EtagVersion.Any) - { - return J.AsTask(GetSnapshot(version)); - } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObjectGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObjectGrain.cs new file mode 100644 index 000000000..d23103330 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObjectGrain.cs @@ -0,0 +1,41 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Assets.State; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Orleans; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public sealed class AssetDomainObjectGrain : DomainObjectGrain, IAssetGrain + { + private static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(5); + + public AssetDomainObjectGrain(IServiceProvider serviceProvider, IActivationLimit limit) + : base(serviceProvider) + { + limit?.SetLimit(5000, Lifetime); + } + + protected override Task OnActivateAsync(Guid key) + { + TryDelayDeactivation(Lifetime); + + return base.OnActivateAsync(key); + } + + public async Task> GetStateAsync(long version = EtagVersion.Any) + { + await DomainObject.EnsureLoadedAsync(); + + return DomainObject.GetSnapshot(version); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetFolderGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetFolderDomainObject.cs similarity index 84% rename from backend/src/Squidex.Domain.Apps.Entities/Assets/AssetFolderGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/AssetFolderDomainObject.cs index bed6848d5..529ab301f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetFolderGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetFolderDomainObject.cs @@ -16,35 +16,24 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.Assets { - public sealed class AssetFolderGrain : DomainObjectGrain, IAssetFolderGrain + public class AssetFolderDomainObject : DomainObject { - private static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(5); private readonly IAssetQueryService assetQuery; - public AssetFolderGrain(IStore store, IAssetQueryService assetQuery, IActivationLimit limit, ISemanticLog log) + public AssetFolderDomainObject(IStore store, IAssetQueryService assetQuery, ISemanticLog log) : base(store, log) { Guard.NotNull(assetQuery); this.assetQuery = assetQuery; - - limit?.SetLimit(5000, Lifetime); - } - - protected override Task OnActivateAsync(Guid key) - { - TryDelayDeactivation(Lifetime); - - return base.OnActivateAsync(key); } - protected override Task ExecuteAsync(IAggregateCommand command) + public override Task ExecuteAsync(IAggregateCommand command) { VerifyNotDeleted(); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetFolderDomainObjectGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetFolderDomainObjectGrain.cs new file mode 100644 index 000000000..5c045ed5b --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetFolderDomainObjectGrain.cs @@ -0,0 +1,40 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Assets.State; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Orleans; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public sealed class AssetFolderDomainObjectGrain : DomainObjectGrain, IAssetFolderGrain + { + private static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(5); + + public AssetFolderDomainObjectGrain(IServiceProvider serviceProvider, IActivationLimit limit) + : base(serviceProvider) + { + limit?.SetLimit(5000, Lifetime); + } + + protected override Task OnActivateAsync(Guid key) + { + TryDelayDeactivation(Lifetime); + + return base.OnActivateAsync(key); + } + + public async Task> GetStateAsync() + { + await DomainObject.EnsureLoadedAsync(); + + return Snapshot; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs index d56826b4c..4eed3f7a0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs @@ -83,7 +83,7 @@ namespace Squidex.Domain.Apps.Entities.Assets if (assetIds.Count > 0) { - await rebuilder.InsertManyAsync(async target => + await rebuilder.InsertManyAsync(async target => { foreach (var id in assetIds) { @@ -94,7 +94,7 @@ namespace Squidex.Domain.Apps.Entities.Assets if (assetFolderIds.Count > 0) { - await rebuilder.InsertManyAsync(async target => + await rebuilder.InsertManyAsync(async target => { foreach (var id in assetFolderIds) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs index ec0489173..47b5c3d45 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs @@ -53,7 +53,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { if (contentIdsBySchemaId.Count > 0) { - await rebuilder.InsertManyAsync(async target => + await rebuilder.InsertManyAsync(async target => { foreach (var contentId in contentIdsBySchemaId.Values.SelectMany(x => x)) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs index c382d28c3..077036d0c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs @@ -20,6 +20,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.Commands public bool DoNotValidate { get; set; } + public bool DoNotScript { get; set; } + + public bool OptimizeValidation { get; set; } + public CreateContent() { ContentId = Guid.NewGuid(); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContents.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContents.cs new file mode 100644 index 000000000..d3cfcef3f --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContents.cs @@ -0,0 +1,31 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Contents.Commands +{ + public sealed class CreateContents : SquidexCommand, ISchemaCommand, IAppCommand + { + public NamedId AppId { get; set; } + + public NamedId SchemaId { get; set; } + + public bool Publish { get; set; } + + public bool DoNotValidate { get; set; } + + public bool DoNotScript { get; set; } + + public bool OptimizeValidation { get; set; } + + public List Datas { get; set; } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs similarity index 91% rename from backend/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs index af04ee31b..89b52b3bb 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs @@ -20,30 +20,27 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.Contents { - public sealed class ContentGrain : LogSnapshotDomainObjectGrain, IContentGrain + public class ContentDomainObject : LogSnapshotDomainObject { - private static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(5); private readonly IAppProvider appProvider; private readonly IAssetRepository assetRepository; private readonly IContentRepository contentRepository; private readonly IScriptEngine scriptEngine; private readonly IContentWorkflow contentWorkflow; - public ContentGrain( + public ContentDomainObject( IStore store, ISemanticLog log, IAppProvider appProvider, IAssetRepository assetRepository, IScriptEngine scriptEngine, IContentWorkflow contentWorkflow, - IContentRepository contentRepository, - IActivationLimit limit) + IContentRepository contentRepository) : base(store, log) { Guard.NotNull(appProvider); @@ -57,11 +54,9 @@ namespace Squidex.Domain.Apps.Entities.Contents this.assetRepository = assetRepository; this.contentWorkflow = contentWorkflow; this.contentRepository = contentRepository; - - limit?.SetLimit(5000, Lifetime); } - protected override Task ExecuteAsync(IAggregateCommand command) + public override Task ExecuteAsync(IAggregateCommand command) { VerifyNotDeleted(); @@ -76,20 +71,23 @@ namespace Squidex.Domain.Apps.Entities.Contents await GuardContent.CanCreate(ctx.Schema, contentWorkflow, c); - c.Data = await ctx.ExecuteScriptAndTransformAsync(s => s.Create, - new ScriptContext - { - Operation = "Create", - Data = c.Data, - Status = status, - StatusOld = default - }); + if (!c.DoNotScript) + { + c.Data = await ctx.ExecuteScriptAndTransformAsync(s => s.Create, + new ScriptContext + { + Operation = "Create", + Data = c.Data, + Status = status, + StatusOld = default + }); + } await ctx.EnrichAsync(c.Data); if (!c.DoNotValidate) { - await ctx.ValidateAsync(c.Data); + await ctx.ValidateAsync(c.Data, c.OptimizeValidation); } if (c.Publish) @@ -231,11 +229,11 @@ namespace Squidex.Domain.Apps.Entities.Contents if (partial) { - await ctx.ValidatePartialAsync(command.Data); + await ctx.ValidatePartialAsync(command.Data, false); } else { - await ctx.ValidateAsync(command.Data); + await ctx.ValidateAsync(command.Data, false); } newData = await ctx.ExecuteScriptAndTransformAsync(s => s.Update, @@ -368,10 +366,5 @@ namespace Squidex.Domain.Apps.Entities.Contents return operationContext; } - - public Task> GetStateAsync(long version = EtagVersion.Any) - { - return J.AsTask(GetSnapshot(version)); - } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObjectGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObjectGrain.cs new file mode 100644 index 000000000..af3ee8651 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObjectGrain.cs @@ -0,0 +1,33 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Contents.State; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Orleans; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public sealed class ContentDomainObjectGrain : DomainObjectGrain, IContentGrain + { + private static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(5); + + public ContentDomainObjectGrain(IServiceProvider serviceProvider, IActivationLimit limit) + : base(serviceProvider) + { + limit?.SetLimit(5000, Lifetime); + } + + public async Task> GetStateAsync(long version = -2) + { + await DomainObject.EnsureLoadedAsync(); + + return DomainObject.GetSnapshot(version); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentImporterCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentImporterCommandMiddleware.cs new file mode 100644 index 000000000..ab452990a --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentImporterCommandMiddleware.cs @@ -0,0 +1,69 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public sealed class ContentImporterCommandMiddleware : ICommandMiddleware + { + private readonly IServiceProvider serviceProvider; + + public ContentImporterCommandMiddleware(IServiceProvider serviceProvider) + { + Guard.NotNull(serviceProvider); + + this.serviceProvider = serviceProvider; + } + + public async Task HandleAsync(CommandContext context, NextDelegate next) + { + if (context.Command is CreateContents createContents) + { + var result = new ImportResult(); + + if (createContents.Datas != null && createContents.Datas.Count > 0) + { + var command = SimpleMapper.Map(createContents, new CreateContent()); + + foreach (var data in createContents.Datas) + { + try + { + command.ContentId = Guid.NewGuid(); + command.Data = data; + + var content = serviceProvider.GetRequiredService(); + + content.Setup(command.ContentId); + + await content.ExecuteAsync(command); + + result.Add(new ImportResultItem { ContentId = command.ContentId }); + } + catch (Exception ex) + { + result.Add(new ImportResultItem { Exception = ex }); + } + } + } + + context.Complete(result); + } + else + { + await next(context); + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs index c1c27a52e..14d52aa4b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs @@ -83,16 +83,16 @@ namespace Squidex.Domain.Apps.Entities.Contents return TaskHelper.Done; } - public Task ValidateAsync(NamedContentData data) + public Task ValidateAsync(NamedContentData data, bool optimized) { - var ctx = CreateValidationContext(); + var ctx = CreateValidationContext(optimized); return data.ValidateAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), message); } - public Task ValidatePartialAsync(NamedContentData data) + public Task ValidatePartialAsync(NamedContentData data, bool optimized) { - var ctx = CreateValidationContext(); + var ctx = CreateValidationContext(optimized); return data.ValidatePartialAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), message); } @@ -122,12 +122,13 @@ namespace Squidex.Domain.Apps.Entities.Contents context.User = command.User; } - private ValidationContext CreateValidationContext() + private ValidationContext CreateValidationContext(bool optimized) { return new ValidationContext(command.ContentId, schemaId, - QueryContentsAsync, - QueryContentsAsync, - QueryAssetsAsync); + QueryContentsAsync, + QueryContentsAsync, + QueryAssetsAsync) + .Optimized(optimized); } private async Task> QueryAssetsAsync(IEnumerable assetIds) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ImportResult.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ImportResult.cs new file mode 100644 index 000000000..7128381a4 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ImportResult.cs @@ -0,0 +1,15 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public sealed class ImportResult : List + { + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ImportResultItem.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ImportResultItem.cs new file mode 100644 index 000000000..1e6d2254f --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ImportResultItem.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public sealed class ImportResultItem + { + public Guid? ContentId { get; set; } + + public Exception? Exception { get; set; } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/SingletonCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/SingletonCommandMiddleware.cs index 44ff091be..0d77147a9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/SingletonCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/SingletonCommandMiddleware.cs @@ -30,12 +30,18 @@ namespace Squidex.Domain.Apps.Entities.Contents var data = new NamedContentData(); var contentId = schemaId.Id; - var content = new CreateContent { Data = data, ContentId = contentId, SchemaId = schemaId, DoNotValidate = true }; + var content = new CreateContent + { + Data = data, + ContentId = contentId, + DoNotScript = true, + DoNotValidate = true, + Publish = true, + SchemaId = schemaId + }; SimpleMapper.Map(createSchema, content); - content.Publish = true; - await context.CommandBus.PublishAsync(content); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs similarity index 90% rename from backend/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs index 6c286ac0c..709d36abc 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs @@ -16,18 +16,17 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.Rules { - public sealed class RuleGrain : DomainObjectGrain, IRuleGrain + public class RuleDomainObject : DomainObject { private readonly IAppProvider appProvider; private readonly IRuleEnqueuer ruleEnqueuer; - public RuleGrain(IStore store, ISemanticLog log, IAppProvider appProvider, IRuleEnqueuer ruleEnqueuer) + public RuleDomainObject(IStore store, ISemanticLog log, IAppProvider appProvider, IRuleEnqueuer ruleEnqueuer) : base(store, log) { Guard.NotNull(appProvider); @@ -38,7 +37,7 @@ namespace Squidex.Domain.Apps.Entities.Rules this.ruleEnqueuer = ruleEnqueuer; } - protected override Task ExecuteAsync(IAggregateCommand command) + public override Task ExecuteAsync(IAggregateCommand command) { VerifyNotDeleted(); @@ -145,10 +144,5 @@ namespace Squidex.Domain.Apps.Entities.Rules throw new DomainException("Rule has already been deleted."); } } - - public Task> GetStateAsync() - { - return J.AsTask(Snapshot); - } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObjectGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObjectGrain.cs new file mode 100644 index 000000000..e542628a0 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObjectGrain.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Rules.State; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Orleans; + +namespace Squidex.Domain.Apps.Entities.Rules +{ + public sealed class RuleDomainObjectGrain : DomainObjectGrain, IRuleGrain + { + public RuleDomainObjectGrain(IServiceProvider serviceProvider) + : base(serviceProvider) + { + } + + public async Task> GetStateAsync() + { + await DomainObject.EnsureLoadedAsync(); + + return Snapshot; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs similarity index 98% rename from backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs index cc7793d98..24d9659f9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs @@ -24,14 +24,14 @@ using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.Schemas { - public sealed class SchemaGrain : DomainObjectGrain, ISchemaGrain + public class SchemaDomainObject : DomainObject { - public SchemaGrain(IStore store, ISemanticLog log) + public SchemaDomainObject(IStore store, ISemanticLog log) : base(store, log) { } - protected override Task ExecuteAsync(IAggregateCommand command) + public override Task ExecuteAsync(IAggregateCommand command) { VerifyNotDeleted(); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObjectGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObjectGrain.cs new file mode 100644 index 000000000..3f2762bb7 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObjectGrain.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Schemas.State; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Orleans; + +namespace Squidex.Domain.Apps.Entities.Schemas +{ + public sealed class SchemaDomainObjectGrain : DomainObjectGrain, ISchemaGrain + { + public SchemaDomainObjectGrain(IServiceProvider serviceProvider) + : base(serviceProvider) + { + } + + public async Task> GetStateAsync() + { + await DomainObject.EnsureLoadedAsync(); + + return Snapshot; + } + } +} diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs index 7f9f52924..30ed2326c 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs @@ -111,7 +111,14 @@ namespace Squidex.Infrastructure.MongoDb { var update = updater(Builders.Update.Set(x => x.Version, newVersion)); - await collection.UpdateOneAsync(x => x.Id.Equals(key) && x.Version == oldVersion, update, Upsert); + if (oldVersion > EtagVersion.Any) + { + await collection.UpdateOneAsync(x => x.Id.Equals(key) && x.Version == oldVersion, update, Upsert); + } + else + { + await collection.UpdateOneAsync(x => x.Id.Equals(key), update, Upsert); + } } catch (MongoWriteException ex) { @@ -137,7 +144,14 @@ namespace Squidex.Infrastructure.MongoDb { try { - await collection.ReplaceOneAsync(x => x.Id.Equals(key) && x.Version == oldVersion, doc, Upsert); + if (oldVersion > EtagVersion.Any) + { + await collection.ReplaceOneAsync(x => x.Id.Equals(key) && x.Version == oldVersion, doc, Upsert); + } + else + { + await collection.ReplaceOneAsync(x => x.Id.Equals(key), doc, Upsert); + } } catch (MongoWriteException ex) { diff --git a/backend/src/Squidex.Infrastructure/Commands/DomainObject.cs b/backend/src/Squidex.Infrastructure/Commands/DomainObject.cs new file mode 100644 index 000000000..4cc9876a5 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Commands/DomainObject.cs @@ -0,0 +1,97 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.States; + +namespace Squidex.Infrastructure.Commands +{ + public abstract class DomainObject : DomainObjectBase where T : class, IDomainState, new() + { + private readonly IStore store; + private T snapshot = new T { Version = EtagVersion.Empty }; + private IPersistence? persistence; + + public override T Snapshot + { + get { return snapshot; } + } + + protected DomainObject(IStore store, ISemanticLog log) + : base(log) + { + Guard.NotNull(store); + + this.store = store; + } + + protected override void OnSetup() + { + persistence = store.WithSnapshotsAndEventSourcing(GetType(), Id, new HandleSnapshot(ApplySnapshot), x => ApplyEvent(x, true)); + } + + protected sealed override bool ApplyEvent(Envelope @event, bool isLoading) + { + var newVersion = Version + 1; + + var newSnapshot = OnEvent(@event); + + if (!ReferenceEquals(Snapshot, newSnapshot) || isLoading) + { + snapshot = newSnapshot; + snapshot.Version = newVersion; + + return true; + } + + return false; + } + + protected sealed override void RestorePreviousSnapshot(T previousSnapshot, long previousVersion) + { + snapshot = previousSnapshot; + } + + private void ApplySnapshot(T state) + { + snapshot = state; + } + + protected sealed override async Task WriteAsync(Envelope[] newEvents, long previousVersion) + { + if (newEvents.Length > 0 && persistence != null) + { + await persistence.WriteEventsAsync(newEvents); + await persistence.WriteSnapshotAsync(Snapshot); + } + } + + protected async sealed override Task ReadAsync() + { + if (persistence != null) + { + await persistence.ReadAsync(); + } + } + + public async sealed override Task RebuildStateAsync() + { + if (persistence != null) + { + await persistence.WriteSnapshotAsync(Snapshot); + } + } + + protected T OnEvent(Envelope @event) + { + return Snapshot.Apply(@event); + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs b/backend/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs similarity index 65% rename from backend/src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs rename to backend/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs index a91018e50..b43b96312 100644 --- a/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs +++ b/backend/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs @@ -10,24 +10,17 @@ using System.Collections.Generic; using System.Threading.Tasks; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Tasks; namespace Squidex.Infrastructure.Commands { - public abstract class DomainObjectGrainBase : GrainOfGuid, IDomainObjectGrain where T : IDomainState, new() + public abstract class DomainObjectBase where T : IDomainState, new() { private readonly List> uncomittedEvents = new List>(); private readonly ISemanticLog log; + private bool isLoaded; private Guid id; - private enum Mode - { - Create, - Update, - Upsert - } - public Guid Id { get { return id; } @@ -40,34 +33,46 @@ namespace Squidex.Infrastructure.Commands public abstract T Snapshot { get; } - protected DomainObjectGrainBase(ISemanticLog log) + protected DomainObjectBase(ISemanticLog log) { Guard.NotNull(log); this.log = log; } - protected override async Task OnActivateAsync(Guid key) + public virtual void Setup(Guid id) + { + this.id = id; + + OnSetup(); + } + + public virtual async Task EnsureLoadedAsync() { - var logContext = (key: key.ToString(), name: GetType().Name); + if (isLoaded) + { + return; + } + + var logContext = (id: id.ToString(), name: GetType().Name); using (log.MeasureInformation(logContext, (ctx, w) => w .WriteProperty("action", "ActivateDomainObject") .WriteProperty("domainObjectType", ctx.name) - .WriteProperty("domainObjectKey", ctx.key))) + .WriteProperty("domainObjectKey", ctx.id))) { - id = key; - - await ReadAsync(GetType(), id); + await ReadAsync(); } + + isLoaded = true; } - public void RaiseEvent(IEvent @event) + protected void RaiseEvent(IEvent @event) { RaiseEvent(Envelope.Create(@event)); } - public virtual void RaiseEvent(Envelope @event) + protected virtual void RaiseEvent(Envelope @event) { Guard.NotNull(@event); @@ -91,82 +96,62 @@ namespace Squidex.Infrastructure.Commands protected Task CreateReturnAsync(TCommand command, Func> handler) where TCommand : class, IAggregateCommand { - return InvokeAsync(command, handler, Mode.Create); + return InvokeAsync(command, handler, false); } protected Task CreateReturn(TCommand command, Func handler) where TCommand : class, IAggregateCommand { - return InvokeAsync(command, handler?.ToAsync()!, Mode.Create); + return InvokeAsync(command, handler?.ToAsync()!, false); } protected Task CreateAsync(TCommand command, Func handler) where TCommand : class, IAggregateCommand { - return InvokeAsync(command, handler.ToDefault(), Mode.Create); + return InvokeAsync(command, handler.ToDefault(), false); } protected Task Create(TCommand command, Action handler) where TCommand : class, IAggregateCommand { - return InvokeAsync(command, handler?.ToDefault()?.ToAsync()!, Mode.Create); + return InvokeAsync(command, handler?.ToDefault()?.ToAsync()!, false); } protected Task UpdateReturnAsync(TCommand command, Func> handler) where TCommand : class, IAggregateCommand { - return InvokeAsync(command, handler, Mode.Update); + return InvokeAsync(command, handler, true); } protected Task UpdateReturn(TCommand command, Func handler) where TCommand : class, IAggregateCommand { - return InvokeAsync(command, handler?.ToAsync()!, Mode.Update); + return InvokeAsync(command, handler?.ToAsync()!, true); } protected Task UpdateAsync(TCommand command, Func handler) where TCommand : class, IAggregateCommand { - return InvokeAsync(command, handler?.ToDefault()!, Mode.Update); + return InvokeAsync(command, handler?.ToDefault()!, true); } protected Task Update(TCommand command, Action handler) where TCommand : class, IAggregateCommand { - return InvokeAsync(command, handler?.ToDefault()?.ToAsync()!, Mode.Update); - } - - protected Task UpsertReturnAsync(TCommand command, Func> handler) where TCommand : class, IAggregateCommand - { - return InvokeAsync(command, handler, Mode.Upsert); - } - - protected Task UpsertReturn(TCommand command, Func handler) where TCommand : class, IAggregateCommand - { - return InvokeAsync(command, handler?.ToAsync()!, Mode.Upsert); - } - - protected Task UpsertAsync(TCommand command, Func handler) where TCommand : class, IAggregateCommand - { - return InvokeAsync(command, handler?.ToDefault()!, Mode.Upsert); - } - - protected Task Upsert(TCommand command, Action handler) where TCommand : class, IAggregateCommand - { - return InvokeAsync(command, handler?.ToDefault()?.ToAsync()!, Mode.Upsert); + return InvokeAsync(command, handler?.ToDefault()?.ToAsync()!, true); } - private async Task InvokeAsync(TCommand command, Func> handler, Mode mode) where TCommand : class, IAggregateCommand + private async Task InvokeAsync(TCommand command, Func> handler, bool isUpdate) where TCommand : class, IAggregateCommand { Guard.NotNull(command); Guard.NotNull(handler); - if (command.ExpectedVersion > EtagVersion.Any && command.ExpectedVersion != Version) + if (isUpdate) { - throw new DomainObjectVersionException(id.ToString(), GetType(), Version, command.ExpectedVersion); + await EnsureLoadedAsync(); } - if (mode == Mode.Update && Version < 0) + if (command.ExpectedVersion > EtagVersion.Any && command.ExpectedVersion != Version) { - throw new DomainObjectNotFoundException(id.ToString(), GetType()); + throw new DomainObjectVersionException(id.ToString(), GetType(), Version, command.ExpectedVersion); } - if (mode == Mode.Create && Version >= 0) + if (isUpdate == true && Version < 0) { - throw new DomainException("Object has already been created."); + throw new DomainObjectNotFoundException(id.ToString(), GetType()); } var previousSnapshot = Snapshot; @@ -181,7 +166,7 @@ namespace Squidex.Infrastructure.Commands if (result == null) { - if (mode == Mode.Update || (mode == Mode.Upsert && Version == 0)) + if (isUpdate) { result = new EntitySavedResult(Version); } @@ -209,17 +194,19 @@ namespace Squidex.Infrastructure.Commands protected abstract bool ApplyEvent(Envelope @event, bool isLoading); - protected abstract Task ReadAsync(Type type, Guid id); + protected abstract Task ReadAsync(); protected abstract Task WriteAsync(Envelope[] newEvents, long previousVersion); - public async Task> ExecuteAsync(J command) + public virtual Task RebuildStateAsync() { - var result = await ExecuteAsync(command.Value); + return TaskHelper.Done; + } - return result; + protected virtual void OnSetup() + { } - protected abstract Task ExecuteAsync(IAggregateCommand command); + public abstract Task ExecuteAsync(IAggregateCommand command); } } \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs b/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs index 72f0e469d..7152aeb87 100644 --- a/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs +++ b/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs @@ -7,77 +7,44 @@ using System; using System.Threading.Tasks; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.States; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Infrastructure.Orleans; namespace Squidex.Infrastructure.Commands { - public abstract class DomainObjectGrain : DomainObjectGrainBase where T : class, IDomainState, new() + public abstract class DomainObjectGrain : GrainOfGuid where T : DomainObjectBase where TState : class, IDomainState, new() { - private readonly IStore store; - private T snapshot = new T { Version = EtagVersion.Empty }; - private IPersistence? persistence; + private readonly T domainObject; - public override T Snapshot + public TState Snapshot { - get { return snapshot; } + get { return domainObject.Snapshot; } } - protected DomainObjectGrain(IStore store, ISemanticLog log) - : base(log) + protected T DomainObject { - Guard.NotNull(store); - - this.store = store; + get { return domainObject; } } - protected sealed override bool ApplyEvent(Envelope @event, bool isLoading) + protected DomainObjectGrain(IServiceProvider serviceProvider) { - var newVersion = Version + 1; - - var newSnapshot = OnEvent(@event); - - if (!ReferenceEquals(Snapshot, newSnapshot) || isLoading) - { - snapshot = newSnapshot; - snapshot.Version = newVersion; - - return true; - } - - return false; - } + Guard.NotNull(serviceProvider); - protected sealed override void RestorePreviousSnapshot(T previousSnapshot, long previousVersion) - { - snapshot = previousSnapshot; + domainObject = serviceProvider.GetRequiredService(); } - protected sealed override Task ReadAsync(Type type, Guid id) + protected override Task OnActivateAsync(Guid key) { - persistence = store.WithSnapshotsAndEventSourcing(GetType(), id, new HandleSnapshot(ApplySnapshot), x => ApplyEvent(x, true)); - - return persistence.ReadAsync(); - } + domainObject.Setup(key); - private void ApplySnapshot(T state) - { - snapshot = state; + return base.OnActivateAsync(key); } - protected sealed override async Task WriteAsync(Envelope[] newEvents, long previousVersion) + public async Task> ExecuteAsync(J command) { - if (newEvents.Length > 0 && persistence != null) - { - await persistence.WriteEventsAsync(newEvents); - await persistence.WriteSnapshotAsync(Snapshot); - } - } + var result = await domainObject.ExecuteAsync(command.Value); - protected T OnEvent(Envelope @event) - { - return Snapshot.Apply(@event); + return result; } } } \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs b/backend/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObject.cs similarity index 76% rename from backend/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs rename to backend/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObject.cs index 66855d2c6..427a74739 100644 --- a/backend/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs +++ b/backend/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObject.cs @@ -15,7 +15,7 @@ using Squidex.Infrastructure.States; namespace Squidex.Infrastructure.Commands { - public abstract class LogSnapshotDomainObjectGrain : DomainObjectGrainBase where T : class, IDomainState, new() + public abstract class LogSnapshotDomainObject : DomainObjectBase where T : class, IDomainState, new() { private readonly IStore store; private readonly List snapshots = new List { new T { Version = EtagVersion.Empty } }; @@ -26,7 +26,7 @@ namespace Squidex.Infrastructure.Commands get { return snapshots.Last(); } } - protected LogSnapshotDomainObjectGrain(IStore store, ISemanticLog log) + protected LogSnapshotDomainObject(IStore store, ISemanticLog log) : base(log) { Guard.NotNull(log); @@ -34,6 +34,11 @@ namespace Squidex.Infrastructure.Commands this.store = store; } + protected override void OnSetup() + { + persistence = store.WithEventSourcing(GetType(), Id, x => ApplyEvent(x, true)); + } + public T GetSnapshot(long version) { if (version == EtagVersion.Any || version == EtagVersion.Auto) @@ -71,21 +76,32 @@ namespace Squidex.Infrastructure.Commands return false; } - protected sealed override Task ReadAsync(Type type, Guid id) + protected sealed override async Task WriteAsync(Envelope[] newEvents, long previousVersion) { - persistence = store.WithEventSourcing(type, id, x => ApplyEvent(x, true)); + if (newEvents.Length > 0 && persistence != null) + { + var persistedSnapshots = store.GetSnapshotStore(); - return persistence.ReadAsync(); + await persistence.WriteEventsAsync(newEvents); + await persistedSnapshots.WriteAsync(Id, Snapshot, previousVersion, Snapshot.Version); + } } - protected sealed override async Task WriteAsync(Envelope[] newEvents, long previousVersion) + protected async sealed override Task ReadAsync() { - if (newEvents.Length > 0 && persistence != null) + if (persistence != null) + { + await persistence.ReadAsync(); + } + } + + public async sealed override Task RebuildStateAsync() + { + if (persistence != null) { var persistedSnapshots = store.GetSnapshotStore(); - await persistence.WriteEventsAsync(newEvents); - await persistedSnapshots.WriteAsync(Id, Snapshot, previousVersion, previousVersion + newEvents.Length); + await persistedSnapshots.WriteAsync(Id, Snapshot, EtagVersion.Any, Snapshot.Version); } } diff --git a/backend/src/Squidex.Infrastructure/Commands/Rebuilder.cs b/backend/src/Squidex.Infrastructure/Commands/Rebuilder.cs index f45d94481..2c2bb583a 100644 --- a/backend/src/Squidex.Infrastructure/Commands/Rebuilder.cs +++ b/backend/src/Squidex.Infrastructure/Commands/Rebuilder.cs @@ -23,24 +23,27 @@ namespace Squidex.Infrastructure.Commands private readonly ILocalCache localCache; private readonly IStore store; private readonly IEventStore eventStore; + private readonly IServiceProvider serviceProvider; public Rebuilder( ILocalCache localCache, IStore store, - IEventStore eventStore) + IEventStore eventStore, + IServiceProvider serviceProvider) { Guard.NotNull(localCache); Guard.NotNull(store); Guard.NotNull(eventStore); this.eventStore = eventStore; + this.serviceProvider = serviceProvider; this.localCache = localCache; this.store = store; } - public Task RebuildAsync(string filter, CancellationToken ct) where TState : IDomainState, new() + public Task RebuildAsync(string filter, CancellationToken ct) where T : DomainObjectBase where TState : class, IDomainState, new() { - return RebuildAsync(async target => + return RebuildAsync(async target => { await eventStore.QueryAsync(async storedEvent => { @@ -51,16 +54,16 @@ namespace Squidex.Infrastructure.Commands }, ct); } - public virtual async Task RebuildAsync(IdSource source, CancellationToken ct = default) where TState : IDomainState, new() + public virtual async Task RebuildAsync(IdSource source, CancellationToken ct = default) where T : DomainObjectBase where TState : class, IDomainState, new() { Guard.NotNull(source); await store.GetSnapshotStore().ClearAsync(); - await InsertManyAsync(source, ct); + await InsertManyAsync(source, ct); } - public virtual async Task InsertManyAsync(IdSource source, CancellationToken ct = default) where TState : IDomainState, new() + public virtual async Task InsertManyAsync(IdSource source, CancellationToken ct = default) where T : DomainObjectBase where TState : class, IDomainState, new() { Guard.NotNull(source); @@ -68,20 +71,12 @@ namespace Squidex.Infrastructure.Commands { try { - var state = new TState - { - Version = EtagVersion.Empty - }; - - var persistence = store.WithSnapshotsAndEventSourcing(typeof(TGrain), id, (TState s) => state = s, e => - { - state = state.Apply(e); + var domainObject = (T)serviceProvider.GetService(typeof(T)); - state.Version++; - }); + domainObject.Setup(id); - await persistence.ReadAsync(); - await persistence.WriteSnapshotAsync(state); + await domainObject.EnsureLoadedAsync(); + await domainObject.RebuildStateAsync(); } catch (DomainObjectNotFoundException) { diff --git a/backend/src/Squidex.Infrastructure/States/Persistence{TSnapshot,TKey}.cs b/backend/src/Squidex.Infrastructure/States/Persistence{TSnapshot,TKey}.cs index 6f828189c..0465294ef 100644 --- a/backend/src/Squidex.Infrastructure/States/Persistence{TSnapshot,TKey}.cs +++ b/backend/src/Squidex.Infrastructure/States/Persistence{TSnapshot,TKey}.cs @@ -30,7 +30,7 @@ namespace Squidex.Infrastructure.States private readonly HandleEvent? applyEvent; private long versionSnapshot = EtagVersion.Empty; private long versionEvents = EtagVersion.Empty; - private long version; + private long version = EtagVersion.Empty; public long Version { diff --git a/backend/src/Squidex.Web/ApiExceptionConverter.cs b/backend/src/Squidex.Web/ApiExceptionConverter.cs new file mode 100644 index 000000000..e20c63f4e --- /dev/null +++ b/backend/src/Squidex.Web/ApiExceptionConverter.cs @@ -0,0 +1,129 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Security; +using System.Text; +using Microsoft.AspNetCore.Http; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Web +{ + public static class ApiExceptionConverter + { + private static readonly List> Handlers = new List>(); + private static readonly Dictionary Links = new Dictionary + { + [400] = "https://tools.ietf.org/html/rfc7231#section-6.5.1", + [401] = "https://tools.ietf.org/html/rfc7235#section-3.1", + [403] = "https://tools.ietf.org/html/rfc7231#section-6.5.3", + [404] = "https://tools.ietf.org/html/rfc7231#section-6.5.4", + [406] = "https://tools.ietf.org/html/rfc7231#section-6.5.6", + [409] = "https://tools.ietf.org/html/rfc7231#section-6.5.8", + [412] = "https://tools.ietf.org/html/rfc7231#section-6.5.10", + [415] = "https://tools.ietf.org/html/rfc7231#section-6.5.13", + [422] = "https://tools.ietf.org/html/rfc4918#section-11.2", + [500] = "https://tools.ietf.org/html/rfc7231#section-6.6.1", + }; + + private static void AddHandler(Func handler) where T : Exception + { + Handlers.Add(ex => ex is T typed ? handler(typed) : null); + } + + static ApiExceptionConverter() + { + AddHandler(OnValidationException); + AddHandler(OnDecoderException); + AddHandler(OnDomainObjectNotFoundException); + AddHandler(OnDomainObjectVersionException); + AddHandler(OnDomainForbiddenException); + AddHandler(OnDomainException); + AddHandler(OnSecurityException); + } + + public static ErrorDto ToErrorDto(this Exception exception, HttpContext? httpContext) + { + Guard.NotNull(exception); + + ErrorDto? result = null; + + foreach (var handler in Handlers) + { + result = handler(exception); + + if (result != null) + { + result.TraceId = Activity.Current?.Id ?? httpContext?.TraceIdentifier; + + if (result.StatusCode.HasValue) + { + result.Type = Links.GetOrDefault(result.StatusCode.Value); + } + + return result; + } + } + + return new ErrorDto { StatusCode = 500 }; + } + + private static ErrorDto OnDecoderException(DecoderFallbackException ex) + { + return new ErrorDto { StatusCode = 400, Message = ex.Message }; + } + + private static ErrorDto OnDomainObjectNotFoundException(DomainObjectNotFoundException ex) + { + return new ErrorDto { StatusCode = 404 }; + } + + private static ErrorDto OnDomainObjectVersionException(DomainObjectVersionException ex) + { + return new ErrorDto { StatusCode = 412, Message = ex.Message }; + } + + private static ErrorDto OnDomainException(DomainException ex) + { + return new ErrorDto { StatusCode = 400, Message = ex.Message }; + } + + private static ErrorDto OnDomainForbiddenException(DomainForbiddenException ex) + { + return new ErrorDto { StatusCode = 403, Message = ex.Message }; + } + + private static ErrorDto OnSecurityException(SecurityException ex) + { + return new ErrorDto { StatusCode = 403, Message = ex.Message }; + } + + private static ErrorDto OnValidationException(ValidationException ex) + { + return new ErrorDto { StatusCode = 400, Message = ex.Summary, Details = ToDetails(ex) }; + } + + private static string[] ToDetails(ValidationException ex) + { + return ex.Errors?.Select(e => + { + if (e.PropertyNames?.Any() == true) + { + return $"{string.Join(", ", e.PropertyNames)}: {e.Message}"; + } + else + { + return e.Message; + } + }).ToArray() ?? new string[0]; + } + } +} diff --git a/backend/src/Squidex.Web/ApiExceptionFilterAttribute.cs b/backend/src/Squidex.Web/ApiExceptionFilterAttribute.cs index 125e6169b..a24809644 100644 --- a/backend/src/Squidex.Web/ApiExceptionFilterAttribute.cs +++ b/backend/src/Squidex.Web/ApiExceptionFilterAttribute.cs @@ -5,113 +5,46 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security; -using System.Text; +using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; namespace Squidex.Web { - public sealed class ApiExceptionFilterAttribute : ActionFilterAttribute, IExceptionFilter + public sealed class ApiExceptionFilterAttribute : ActionFilterAttribute, IExceptionFilter, IAsyncActionFilter { - private static readonly List> Handlers = new List>(); - - private static void AddHandler(Func handler) where T : Exception - { - Handlers.Add(ex => ex is T typed ? handler(typed) : null); - } - - static ApiExceptionFilterAttribute() - { - AddHandler(OnValidationException); - AddHandler(OnDecoderException); - AddHandler(OnDomainObjectNotFoundException); - AddHandler(OnDomainObjectVersionException); - AddHandler(OnDomainForbiddenException); - AddHandler(OnDomainException); - AddHandler(OnSecurityException); - } - - private static IActionResult OnDecoderException(DecoderFallbackException ex) - { - return ErrorResult(400, new ErrorDto { Message = ex.Message }); - } - - private static IActionResult OnDomainObjectNotFoundException(DomainObjectNotFoundException ex) - { - return new NotFoundResult(); - } - - private static IActionResult OnDomainObjectVersionException(DomainObjectVersionException ex) - { - return ErrorResult(412, new ErrorDto { Message = ex.Message }); - } - - private static IActionResult OnDomainException(DomainException ex) + public override async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) { - return ErrorResult(400, new ErrorDto { Message = ex.Message }); - } + var resultContext = await next(); - private static IActionResult OnDomainForbiddenException(DomainForbiddenException ex) - { - return ErrorResult(403, new ErrorDto { Message = ex.Message }); - } - - private static IActionResult OnSecurityException(SecurityException ex) - { - return ErrorResult(403, new ErrorDto { Message = ex.Message }); - } - - private static IActionResult OnValidationException(ValidationException ex) - { - return ErrorResult(400, new ErrorDto { Message = ex.Summary, Details = ToDetails(ex) }); - } - - private static IActionResult ErrorResult(int statusCode, ErrorDto error) - { - error.StatusCode = statusCode; - - return new ObjectResult(error) { StatusCode = statusCode }; - } - - public void OnException(ExceptionContext context) - { - IActionResult? result = null; - - foreach (var handler in Handlers) + if (resultContext.Result is ObjectResult objectResult && objectResult.Value is ProblemDetails problem) { - result = handler(context.Exception); + var error = new ErrorDto { Message = problem.Title, Type = problem.Type, StatusCode = problem.Status }; - if (result != null) + if (problem.Extensions.TryGetValue("traceId", out var temp) && temp is string traceId) { - break; + error.TraceId = traceId; } - } - if (result != null) - { - context.Result = result; + objectResult.Value = error; } } - private static string[] ToDetails(ValidationException ex) + public void OnException(ExceptionContext context) { - return ex.Errors?.Select(e => + var error = context.Exception.ToErrorDto(context.HttpContext); + + if (error.StatusCode == 404) { - if (e.PropertyNames?.Any() == true) - { - return $"{string.Join(", ", e.PropertyNames)}: {e.Message}"; - } - else + context.Result = new NotFoundResult(); + } + else + { + context.Result = new ObjectResult(error) { - return e.Message; - } - }).ToArray() ?? new string[0]; + StatusCode = error.StatusCode + }; + } } } } diff --git a/backend/src/Squidex.Web/ErrorDto.cs b/backend/src/Squidex.Web/ErrorDto.cs index 139fc672b..fe3dfb892 100644 --- a/backend/src/Squidex.Web/ErrorDto.cs +++ b/backend/src/Squidex.Web/ErrorDto.cs @@ -15,6 +15,12 @@ namespace Squidex.Web [Display(Description = "Error message.")] public string Message { get; set; } + [Display(Description = "The optional trace id.")] + public string? TraceId { get; set; } + + [Display(Description = "Link to the error details.")] + public string? Type { get; set; } + [Display(Description = "Detailed error messages.")] public string[]? Details { get; set; } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs index 9f2bb9283..be11451fe 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; @@ -261,7 +262,7 @@ namespace Squidex.Areas.Api.Controllers.Contents /// The name of the app. /// The name of the schema. /// The full data for the content item. - /// Indicates whether the content should be published immediately. + /// True to automatically publish the content. /// /// 201 => Content created. /// 404 => Content, schema or app not found. @@ -286,6 +287,39 @@ namespace Squidex.Areas.Api.Controllers.Contents return CreatedAtAction(nameof(GetContent), new { app, name, id = command.ContentId }, response); } + /// + /// Import content items. + /// + /// The name of the app. + /// The name of the schema. + /// The import request. + /// + /// 201 => Contents created. + /// 404 => Content references, schema or app not found. + /// 400 => Content data is not valid. + /// + /// + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// + [HttpPost] + [Route("content/{app}/{name}/import")] + [ProducesResponseType(typeof(ImportResultDto[]), 200)] + [ApiPermission(Permissions.AppContentsCreate)] + [ApiCosts(5)] + public async Task PostContent(string app, string name, [FromBody] ImportContentsDto request) + { + await contentQuery.GetSchemaOrThrowAsync(Context, name); + + var command = request.ToCommand(); + + var context = await CommandBus.PublishAsync(command); + + var result = context.Result(); + var response = result.Select(x => ImportResultDto.FromImportResult(x, HttpContext)).ToArray(); + + return Ok(response); + } + /// /// Update a content item. /// @@ -296,7 +330,7 @@ namespace Squidex.Areas.Api.Controllers.Contents /// Indicates whether the update is a proposal. /// /// 200 => Content updated. - /// 404 => Content, schema or app not found. + /// 404 => Content references, schema or app not found. /// 400 => Content data is not valid. /// /// diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaOpenApiGenerator.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaOpenApiGenerator.cs index e32757c77..2e472237f 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaOpenApiGenerator.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaOpenApiGenerator.cs @@ -125,7 +125,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator operation.Summary = $"Create a {schemaName} content."; operation.AddBody("data", dataSchema, NSwagHelper.SchemaBodyDocs); - operation.AddQuery("publish", JsonObjectType.Boolean, "Set to true to autopublish content."); + operation.AddQuery("publish", JsonObjectType.Boolean, "True to automatically publish the content."); operation.AddResponse("201", $"{schemaName} content created.", contentSchema); operation.AddResponse("400", $"{schemaName} content not valid."); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ImportContentsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ImportContentsDto.cs new file mode 100644 index 000000000..2b5d15290 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ImportContentsDto.cs @@ -0,0 +1,44 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Areas.Api.Controllers.Contents.Models +{ + public sealed class ImportContentsDto + { + /// + /// The data to import. + /// + [Required] + public List Datas { get; set; } + + /// + /// True to automatically publish the content. + /// + public bool Publish { get; set; } + + /// + /// True to turn off scripting for faster inserts. Default: true. + /// + public bool DoNotScript { get; set; } = true; + + /// + /// True to turn off costly validation: Unique checks, asset checks and reference checks. Default: true. + /// + public bool OptimizeValidation { get; set; } = true; + + public CreateContents ToCommand() + { + return SimpleMapper.Map(this, new CreateContents()); + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ImportResultDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ImportResultDto.cs new file mode 100644 index 000000000..25a32acde --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ImportResultDto.cs @@ -0,0 +1,32 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Microsoft.AspNetCore.Http; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Contents.Models +{ + public sealed class ImportResultDto + { + /// + /// The error when the import failed. + /// + public ErrorDto? Error { get; set; } + + /// + /// The id of the content when the import succeeds. + /// + public Guid? ContentId { get; set; } + + public static ImportResultDto FromImportResult(ImportResultItem result, HttpContext httpContext) + { + return new ImportResultDto { ContentId = result.ContentId, Error = result.Exception?.ToErrorDto(httpContext) }; + } + } +} diff --git a/backend/src/Squidex/Config/Domain/AppsServices.cs b/backend/src/Squidex/Config/Domain/AppsServices.cs index af5571e76..975b32eeb 100644 --- a/backend/src/Squidex/Config/Domain/AppsServices.cs +++ b/backend/src/Squidex/Config/Domain/AppsServices.cs @@ -20,6 +20,9 @@ namespace Squidex.Config.Domain { public static void AddSquidexApps(this IServiceCollection services) { + services.AddTransientAs() + .AsSelf(); + services.AddSingletonAs() .AsSelf(); diff --git a/backend/src/Squidex/Config/Domain/AssetServices.cs b/backend/src/Squidex/Config/Domain/AssetServices.cs index 98938fe58..c70d7616a 100644 --- a/backend/src/Squidex/Config/Domain/AssetServices.cs +++ b/backend/src/Squidex/Config/Domain/AssetServices.cs @@ -28,6 +28,12 @@ namespace Squidex.Config.Domain services.Configure( config.GetSection("assets")); + services.AddTransientAs() + .AsSelf(); + + services.AddTransientAs() + .AsSelf(); + services.AddSingletonAs() .AsSelf(); diff --git a/backend/src/Squidex/Config/Domain/CommandsServices.cs b/backend/src/Squidex/Config/Domain/CommandsServices.cs index a3271e7c1..eb838beef 100644 --- a/backend/src/Squidex/Config/Domain/CommandsServices.cs +++ b/backend/src/Squidex/Config/Domain/CommandsServices.cs @@ -77,6 +77,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); diff --git a/backend/src/Squidex/Config/Domain/ContentsServices.cs b/backend/src/Squidex/Config/Domain/ContentsServices.cs index 74b311e9a..882090e91 100644 --- a/backend/src/Squidex/Config/Domain/ContentsServices.cs +++ b/backend/src/Squidex/Config/Domain/ContentsServices.cs @@ -30,6 +30,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .AsSelf(); + services.AddTransientAs() + .AsSelf(); + services.AddSingletonAs() .As(); diff --git a/backend/src/Squidex/Config/Domain/RuleServices.cs b/backend/src/Squidex/Config/Domain/RuleServices.cs index 1fb99ab33..2ea4f6806 100644 --- a/backend/src/Squidex/Config/Domain/RuleServices.cs +++ b/backend/src/Squidex/Config/Domain/RuleServices.cs @@ -28,6 +28,9 @@ namespace Squidex.Config.Domain services.Configure( config.GetSection("rules")); + services.AddTransientAs() + .AsSelf(); + services.AddSingletonAs() .As(); diff --git a/backend/src/Squidex/Config/Domain/SchemasServices.cs b/backend/src/Squidex/Config/Domain/SchemasServices.cs index 9326df263..0e0ffd9e2 100644 --- a/backend/src/Squidex/Config/Domain/SchemasServices.cs +++ b/backend/src/Squidex/Config/Domain/SchemasServices.cs @@ -15,6 +15,9 @@ namespace Squidex.Config.Domain { public static void AddSquidexSchemas(this IServiceCollection services) { + services.AddTransientAs() + .AsSelf(); + services.AddSingletonAs() .As(); } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs index 5e557e095..db14bc0d4 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs @@ -201,6 +201,18 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent new[] { $"[1]: Id '{assetId}' not found." }); } + [Fact] + public async Task Should_not_add_error_if_asset_are_not_valid_but_in_optimized_mode() + { + var assetId = Guid.NewGuid(); + + var sut = Field(new AssetsFieldProperties()); + + await sut.ValidateAsync(CreateValue(assetId), errors, ctx.Optimized()); + + Assert.Empty(errors); + } + [Fact] public async Task Should_add_error_if_document_is_too_small() { diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs index 9988da835..43a576ef5 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs @@ -148,6 +148,16 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent new[] { $"Contains invalid reference '{ref1}'." }); } + [Fact] + public async Task Should_not_add_error_if_reference_are_not_valid_but_in_optimized_mode() + { + var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId }); + + await sut.ValidateAsync(CreateValue(ref1), errors, ValidationTestExtensions.References().Optimized()); + + Assert.Empty(errors); + } + [Fact] public async Task Should_add_error_if_reference_schema_is_not_valid() { diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs index 7beb00d3a..eedf90485 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs @@ -28,7 +28,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators var filter = string.Empty; - await sut.ValidateAsync("hi", errors, Context(Guid.NewGuid(), f => filter = f)); + await sut.ValidateAsync("hi", errors, Context(Guid.NewGuid(), f => filter = f, ValidationMode.Default)); errors.Should().BeEquivalentTo( new[] { "property: Another content with the same value exists." }); @@ -43,7 +43,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators var filter = string.Empty; - await sut.ValidateAsync(12.5, errors, Context(Guid.NewGuid(), f => filter = f)); + await sut.ValidateAsync(12.5, errors, Context(Guid.NewGuid(), f => filter = f, ValidationMode.Default)); errors.Should().BeEquivalentTo( new[] { "property: Another content with the same value exists." }); @@ -51,6 +51,18 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators Assert.Equal("Data.property.iv == 12.5", filter); } + [Fact] + public async Task Should_not_add_error_if_string_value_not_found_but_in_optimized_mode() + { + var sut = new UniqueValidator(); + + var filter = string.Empty; + + await sut.ValidateAsync("hi", errors, Context(Guid.NewGuid(), f => filter = f, ValidationMode.Optimized)); + + Assert.Empty(errors); + } + [Fact] public async Task Should_not_add_error_if_string_value_found() { @@ -58,7 +70,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators var filter = string.Empty; - await sut.ValidateAsync("hi", errors, Context(contentId, f => filter = f)); + await sut.ValidateAsync("hi", errors, Context(contentId, f => filter = f, ValidationMode.Default)); Assert.Empty(errors); } @@ -70,12 +82,12 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators var filter = string.Empty; - await sut.ValidateAsync(12.5, errors, Context(contentId, f => filter = f)); + await sut.ValidateAsync(12.5, errors, Context(contentId, f => filter = f, ValidationMode.Default)); Assert.Empty(errors); } - private ValidationContext Context(Guid id, Action filter) + private ValidationContext Context(Guid id, Action filter, ValidationMode mode) { return new ValidationContext(contentId, schemaId, (schema, filterNode) => @@ -91,7 +103,10 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators ids => { return Task.FromResult>(new List()); - }).Nested("property").Nested("iv"); + }, + mode) + .Nested("property") + .Nested("iv"); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs similarity index 98% rename from backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs index e9b1f8b87..60196c735 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs @@ -27,7 +27,7 @@ using Xunit; namespace Squidex.Domain.Apps.Entities.Apps { - public class AppGrainTests : HandlerTestBase + public class AppDomainObjectTests : HandlerTestBase { private readonly IAppPlansProvider appPlansProvider = A.Fake(); private readonly IAppPlanBillingManager appPlansBillingManager = A.Fake(); @@ -39,7 +39,7 @@ namespace Squidex.Domain.Apps.Entities.Apps private readonly string roleName = "My Role"; private readonly string planIdPaid = "premium"; private readonly string planIdFree = "free"; - private readonly AppGrain sut; + private readonly AppDomainObject sut; private readonly Guid workflowId = Guid.NewGuid(); private readonly Guid patternId1 = Guid.NewGuid(); private readonly Guid patternId2 = Guid.NewGuid(); @@ -51,7 +51,7 @@ namespace Squidex.Domain.Apps.Entities.Apps get { return AppId; } } - public AppGrainTests() + public AppDomainObjectTests() { A.CallTo(() => user.Id) .Returns(contributorId); @@ -74,8 +74,8 @@ namespace Squidex.Domain.Apps.Entities.Apps { patternId2, new AppPattern("Numbers", "[0-9]*") } }; - sut = new AppGrain(initialPatterns, Store, A.Dummy(), appPlansProvider, appPlansBillingManager, userResolver); - sut.ActivateAsync(Id).Wait(); + sut = new AppDomainObject(initialPatterns, Store, A.Dummy(), appPlansProvider, appPlansBillingManager, userResolver); + sut.Setup(Id); } [Fact] @@ -734,7 +734,7 @@ namespace Squidex.Domain.Apps.Entities.Apps { var result = await sut.ExecuteAsync(CreateCommand(command)); - return result.Value; + return result; } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs index d6c57b7ab..571ca3db3 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs @@ -241,8 +241,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes [Fact] public async Task Should_add_app_to_index_on_contributor_assignment() { + var command = new AssignContributor { AppId = appId.Id, ContributorId = userId }; + var context = - new CommandContext(new AssignContributor { AppId = appId.Id, ContributorId = userId }, commandBus) + new CommandContext(command, commandBus) .Complete(); await sut.HandleAsync(context); @@ -254,8 +256,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes [Fact] public async Task Should_remove_from_user_index_on_remove_of_contributor() { + var command = new RemoveContributor { AppId = appId.Id, ContributorId = userId }; + var context = - new CommandContext(new RemoveContributor { AppId = appId.Id, ContributorId = userId }, commandBus) + new CommandContext(command, commandBus) .Complete(); await sut.HandleAsync(context); @@ -269,8 +273,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes { SetupApp(0, isArchived); + var command = new ArchiveApp { AppId = appId.Id }; + var context = - new CommandContext(new ArchiveApp { AppId = appId.Id }, commandBus) + new CommandContext(command, commandBus) .Complete(); await sut.HandleAsync(context); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InviteUserCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InviteUserCommandMiddlewareTests.cs index 447f0a013..83f24111e 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InviteUserCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InviteUserCommandMiddlewareTests.cs @@ -32,8 +32,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation [Fact] public async Task Should_invite_user_and_change_result() { + var command = new AssignContributor { ContributorId = "me@email.com", Invite = true }; + var context = - new CommandContext(new AssignContributor { ContributorId = "me@email.com", Invite = true }, commandBus) + new CommandContext(command, commandBus) .Complete(app); A.CallTo(() => userResolver.CreateUserIfNotExistsAsync("me@email.com", true)) @@ -50,8 +52,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation [Fact] public async Task Should_invite_user_and_not_change_result_if_not_added() { + var command = new AssignContributor { ContributorId = "me@email.com", Invite = true }; + var context = - new CommandContext(new AssignContributor { ContributorId = "me@email.com", Invite = true }, commandBus) + new CommandContext(command, commandBus) .Complete(app); A.CallTo(() => userResolver.CreateUserIfNotExistsAsync("me@email.com", true)) @@ -68,8 +72,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation [Fact] public async Task Should_not_call_user_resolver_if_not_email() { + var command = new AssignContributor { ContributorId = "123", Invite = true }; + var context = - new CommandContext(new AssignContributor { ContributorId = "123", Invite = true }, commandBus) + new CommandContext(command, commandBus) .Complete(app); await sut.HandleAsync(context); @@ -81,8 +87,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation [Fact] public async Task Should_not_call_user_resolver_if_not_inviting() { + var command = new AssignContributor { ContributorId = "123", Invite = false }; + var context = - new CommandContext(new AssignContributor { ContributorId = "123", Invite = false }, commandBus) + new CommandContext(command, commandBus) .Complete(app); await sut.HandleAsync(context); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs index 7c871b7b4..752838dab 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs @@ -20,7 +20,6 @@ using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Reflection; using Xunit; @@ -30,14 +29,15 @@ namespace Squidex.Domain.Apps.Entities.Assets { private readonly IAssetEnricher assetEnricher = A.Fake(); private readonly IAssetFileStore assetFileStore = A.Fake(); - private readonly IAssetQueryService assetQuery = A.Fake(); private readonly IAssetMetadataSource assetMetadataSource = A.Fake(); + private readonly IAssetQueryService assetQuery = A.Fake(); private readonly IContextProvider contextProvider = A.Fake(); private readonly IGrainFactory grainFactory = A.Fake(); + private readonly IServiceProvider serviceProvider = A.Fake(); private readonly ITagService tagService = A.Fake(); private readonly Guid assetId = Guid.NewGuid(); private readonly Stream stream = new MemoryStream(); - private readonly AssetGrain asset; + private readonly AssetDomainObjectGrain asset; private readonly AssetFile file; private readonly Context requestContext = Context.Anonymous(); private readonly AssetCommandMiddleware sut; @@ -55,7 +55,12 @@ namespace Squidex.Domain.Apps.Entities.Assets { file = new AssetFile("my-image.png", "image/png", 1024, () => stream); - asset = new AssetGrain(Store, tagService, assetQuery, A.Fake(), A.Dummy()); + var assetDomainObject = new AssetDomainObject(Store, tagService, assetQuery, A.Dummy()); + + A.CallTo(() => serviceProvider.GetService(typeof(AssetDomainObject))) + .Returns(assetDomainObject); + + asset = new AssetDomainObjectGrain(serviceProvider, null!); asset.ActivateAsync(Id).Wait(); A.CallTo(() => contextProvider.Context) diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectGrainTests.cs new file mode 100644 index 000000000..820a7a3be --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectGrainTests.cs @@ -0,0 +1,35 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using FakeItEasy; +using Squidex.Infrastructure.Orleans; +using Xunit; + +#pragma warning disable RECS0026 // Possible unassigned object created by 'new' + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public class AssetDomainObjectGrainTests + { + private readonly IActivationLimit limit = A.Fake(); + + [Fact] + public void Should_set_limit() + { + var serviceProvider = A.Fake(); + + A.CallTo(() => serviceProvider.GetService(typeof(AssetDomainObject))) + .Returns(A.Dummy()); + + new AssetDomainObjectGrain(serviceProvider, limit); + + A.CallTo(() => limit.SetLimit(5000, TimeSpan.FromMinutes(5))) + .MustHaveHappened(); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectTests.cs similarity index 93% rename from backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectTests.cs index 0db5c0e58..f89a1987d 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectTests.cs @@ -21,27 +21,25 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; using Xunit; namespace Squidex.Domain.Apps.Entities.Assets { - public class AssetGrainTests : HandlerTestBase + public class AssetDomainObjectTests : HandlerTestBase { private readonly ITagService tagService = A.Fake(); private readonly IAssetQueryService assetQuery = A.Fake(); - private readonly IActivationLimit limit = A.Fake(); private readonly Guid parentId = Guid.NewGuid(); private readonly Guid assetId = Guid.NewGuid(); private readonly AssetFile file = new AssetFile("my-image.png", "image/png", 1024, () => new MemoryStream()); - private readonly AssetGrain sut; + private readonly AssetDomainObject sut; protected override Guid Id { get { return assetId; } } - public AssetGrainTests() + public AssetDomainObjectTests() { A.CallTo(() => assetQuery.FindAssetFolderAsync(parentId)) .Returns(new List { A.Fake() }); @@ -49,15 +47,8 @@ namespace Squidex.Domain.Apps.Entities.Assets A.CallTo(() => tagService.NormalizeTagsAsync(AppId, TagGroups.Assets, A>.Ignored, A>.Ignored)) .ReturnsLazily(x => Task.FromResult(x.GetArgument>(2)?.ToDictionary(x => x)!)); - sut = new AssetGrain(Store, tagService, assetQuery, limit, A.Dummy()); - sut.ActivateAsync(Id).Wait(); - } - - [Fact] - public void Should_set_limit() - { - A.CallTo(() => limit.SetLimit(5000, TimeSpan.FromMinutes(5))) - .MustHaveHappened(); + sut = new AssetDomainObject(Store, tagService, assetQuery, A.Dummy()); + sut.Setup(Id); } [Fact] @@ -304,7 +295,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { var result = await sut.ExecuteAsync(CreateAssetCommand(command)); - return result.Value; + return result; } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetFolderDomainObjectGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetFolderDomainObjectGrainTests.cs new file mode 100644 index 000000000..50ba20ba7 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetFolderDomainObjectGrainTests.cs @@ -0,0 +1,35 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using FakeItEasy; +using Squidex.Infrastructure.Orleans; +using Xunit; + +#pragma warning disable RECS0026 // Possible unassigned object created by 'new' + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public class AssetFolderDomainObjectGrainTests + { + private readonly IActivationLimit limit = A.Fake(); + + [Fact] + public void Should_set_limit() + { + var serviceProvider = A.Fake(); + + A.CallTo(() => serviceProvider.GetService(typeof(AssetFolderDomainObject))) + .Returns(A.Dummy()); + + new AssetFolderDomainObjectGrain(serviceProvider, limit); + + A.CallTo(() => limit.SetLimit(5000, TimeSpan.FromMinutes(5))) + .MustHaveHappened(); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetFolderGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetFolderDomainObjectTests.cs similarity index 89% rename from backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetFolderGrainTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetFolderDomainObjectTests.cs index 280bde17c..e253ea3c7 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetFolderGrainTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetFolderDomainObjectTests.cs @@ -16,38 +16,29 @@ using Squidex.Domain.Apps.Events.Assets; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; using Xunit; namespace Squidex.Domain.Apps.Entities.Assets { - public class AssetFolderGrainTests : HandlerTestBase + public class AssetFolderDomainObjectTests : HandlerTestBase { private readonly IAssetQueryService assetQuery = A.Fake(); - private readonly IActivationLimit limit = A.Fake(); private readonly Guid parentId = Guid.NewGuid(); private readonly Guid assetFolderId = Guid.NewGuid(); - private readonly AssetFolderGrain sut; + private readonly AssetFolderDomainObject sut; protected override Guid Id { get { return assetFolderId; } } - public AssetFolderGrainTests() + public AssetFolderDomainObjectTests() { A.CallTo(() => assetQuery.FindAssetFolderAsync(parentId)) .Returns(new List { A.Fake() }); - sut = new AssetFolderGrain(Store, assetQuery, limit, A.Dummy()); - sut.ActivateAsync(Id).Wait(); - } - - [Fact] - public void Should_set_limit() - { - A.CallTo(() => limit.SetLimit(5000, TimeSpan.FromMinutes(5))) - .MustHaveHappened(); + sut = new AssetFolderDomainObject(Store, assetQuery, A.Dummy()); + sut.Setup(Id); } [Fact] @@ -184,7 +175,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { var result = await sut.ExecuteAsync(CreateAssetFolderCommand(command)); - return result.Value; + return result; } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/BackupAssetsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/BackupAssetsTests.cs index 498c3e5d3..dcf214f9c 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/BackupAssetsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/BackupAssetsTests.cs @@ -178,7 +178,7 @@ namespace Squidex.Domain.Apps.Entities.Assets return TaskHelper.Done; }); - A.CallTo(() => rebuilder.InsertManyAsync(A.Ignored, A.Ignored)) + A.CallTo(() => rebuilder.InsertManyAsync(A.Ignored, A.Ignored)) .Invokes((IdSource source, CancellationToken _) => source(add)); await sut.RestoreAsync(context); @@ -222,7 +222,7 @@ namespace Squidex.Domain.Apps.Entities.Assets return TaskHelper.Done; }); - A.CallTo(() => rebuilder.InsertManyAsync(A.Ignored, A.Ignored)) + A.CallTo(() => rebuilder.InsertManyAsync(A.Ignored, A.Ignored)) .Invokes((IdSource source, CancellationToken _) => source(add)); await sut.RestoreAsync(context); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BackupContentsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BackupContentsTests.cs index c4d93f304..335f6433c 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BackupContentsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BackupContentsTests.cs @@ -90,7 +90,7 @@ namespace Squidex.Domain.Apps.Entities.Contents return TaskHelper.Done; }); - A.CallTo(() => rebuilder.InsertManyAsync(A.Ignored, A.Ignored)) + A.CallTo(() => rebuilder.InsertManyAsync(A.Ignored, A.Ignored)) .Invokes((IdSource source, CancellationToken _) => source(add)); await sut.RestoreAsync(context); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectGrainTests.cs new file mode 100644 index 000000000..2636d1bc7 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectGrainTests.cs @@ -0,0 +1,35 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using FakeItEasy; +using Squidex.Infrastructure.Orleans; +using Xunit; + +#pragma warning disable RECS0026 // Possible unassigned object created by 'new' + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public class ContentDomainObjectGrainTests + { + private readonly IActivationLimit limit = A.Fake(); + + [Fact] + public void Should_set_limit() + { + var serviceProvider = A.Fake(); + + A.CallTo(() => serviceProvider.GetService(typeof(ContentDomainObject))) + .Returns(A.Dummy()); + + new ContentDomainObjectGrain(serviceProvider, limit); + + A.CallTo(() => limit.SetLimit(5000, TimeSpan.FromMinutes(5))) + .MustHaveHappened(); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs similarity index 82% rename from backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs index 77f68ed0b..94aed07c1 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs @@ -24,16 +24,14 @@ using Squidex.Domain.Apps.Events.Contents; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Validation; using Xunit; namespace Squidex.Domain.Apps.Entities.Contents { - public class ContentGrainTests : HandlerTestBase + public class ContentDomainObjectTests : HandlerTestBase { private readonly Guid contentId = Guid.NewGuid(); - private readonly IActivationLimit limit = A.Fake(); private readonly IAppEntity app; private readonly IAppProvider appProvider = A.Fake(); private readonly IContentRepository contentRepository = A.Dummy(); @@ -68,14 +66,14 @@ namespace Squidex.Domain.Apps.Entities.Contents new ContentFieldData() .AddValue("iv", 2)); private readonly NamedContentData patched; - private readonly ContentGrain sut; + private readonly ContentDomainObject sut; protected override Guid Id { get { return contentId; } } - public ContentGrainTests() + public ContentDomainObjectTests() { app = Mocks.App(AppNamedId, Language.DE); @@ -108,15 +106,8 @@ namespace Squidex.Domain.Apps.Entities.Contents patched = patch.MergeInto(data); - sut = new ContentGrain(Store, A.Dummy(), appProvider, A.Dummy(), scriptEngine, contentWorkflow, contentRepository, limit); - sut.ActivateAsync(Id).Wait(); - } - - [Fact] - public void Should_set_limit() - { - A.CallTo(() => limit.SetLimit(5000, TimeSpan.FromMinutes(5))) - .MustHaveHappened(); + sut = new ContentDomainObject(Store, A.Dummy(), appProvider, A.Dummy(), scriptEngine, contentWorkflow, contentRepository); + sut.Setup(Id); } [Fact] @@ -133,9 +124,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { var command = new CreateContent { Data = data }; - var result = await sut.ExecuteAsync(CreateContentCommand(command)); + var result = await PublishAsync(CreateContentCommand(command)); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Equal(Status.Draft, sut.Snapshot.Status); @@ -155,9 +146,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { var command = new CreateContent { Data = data, Publish = true }; - var result = await sut.ExecuteAsync(CreateContentCommand(command)); + var result = await PublishAsync(CreateContentCommand(command)); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Equal(Status.Published, sut.Snapshot.Status); @@ -178,7 +169,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { var command = new CreateContent { Data = invalidData }; - await Assert.ThrowsAsync(() => sut.ExecuteAsync(CreateContentCommand(command))); + await Assert.ThrowsAsync(() => PublishAsync(CreateContentCommand(command))); } [Fact] @@ -188,9 +179,9 @@ namespace Squidex.Domain.Apps.Entities.Contents await ExecuteCreateAsync(); - var result = await sut.ExecuteAsync(CreateContentCommand(command)); + var result = await PublishAsync(CreateContentCommand(command)); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); LastEvents .ShouldHaveSameEvents( @@ -209,9 +200,9 @@ namespace Squidex.Domain.Apps.Entities.Contents await ExecuteCreateAsync(); await ExecutePublishAsync(); - var result = await sut.ExecuteAsync(CreateContentCommand(command)); + var result = await PublishAsync(CreateContentCommand(command)); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.True(sut.Snapshot.IsPending); @@ -231,9 +222,9 @@ namespace Squidex.Domain.Apps.Entities.Contents await ExecuteCreateAsync(); - var result = await sut.ExecuteAsync(CreateContentCommand(command)); + var result = await PublishAsync(CreateContentCommand(command)); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Single(LastEvents); @@ -248,7 +239,7 @@ namespace Squidex.Domain.Apps.Entities.Contents await ExecuteCreateAsync(); - await Assert.ThrowsAsync(() => sut.ExecuteAsync(CreateContentCommand(command))); + await Assert.ThrowsAsync(() => PublishAsync(CreateContentCommand(command))); } [Fact] @@ -258,9 +249,9 @@ namespace Squidex.Domain.Apps.Entities.Contents await ExecuteCreateAsync(); - var result = await sut.ExecuteAsync(CreateContentCommand(command)); + var result = await PublishAsync(CreateContentCommand(command)); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); LastEvents .ShouldHaveSameEvents( @@ -279,9 +270,9 @@ namespace Squidex.Domain.Apps.Entities.Contents await ExecuteCreateAsync(); await ExecutePublishAsync(); - var result = await sut.ExecuteAsync(CreateContentCommand(command)); + var result = await PublishAsync(CreateContentCommand(command)); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.True(sut.Snapshot.IsPending); @@ -301,9 +292,9 @@ namespace Squidex.Domain.Apps.Entities.Contents await ExecuteCreateAsync(); - var result = await sut.ExecuteAsync(CreateContentCommand(command)); + var result = await PublishAsync(CreateContentCommand(command)); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Single(LastEvents); @@ -318,9 +309,9 @@ namespace Squidex.Domain.Apps.Entities.Contents await ExecuteCreateAsync(); - var result = await sut.ExecuteAsync(CreateContentCommand(command)); + var result = await PublishAsync(CreateContentCommand(command)); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Equal(Status.Published, sut.Snapshot.Status); @@ -340,9 +331,9 @@ namespace Squidex.Domain.Apps.Entities.Contents await ExecuteCreateAsync(); - var result = await sut.ExecuteAsync(CreateContentCommand(command)); + var result = await PublishAsync(CreateContentCommand(command)); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Equal(Status.Archived, sut.Snapshot.Status); @@ -363,9 +354,9 @@ namespace Squidex.Domain.Apps.Entities.Contents await ExecuteCreateAsync(); await ExecutePublishAsync(); - var result = await sut.ExecuteAsync(CreateContentCommand(command)); + var result = await PublishAsync(CreateContentCommand(command)); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Equal(Status.Draft, sut.Snapshot.Status); @@ -386,9 +377,9 @@ namespace Squidex.Domain.Apps.Entities.Contents await ExecuteCreateAsync(); await ExecuteArchiveAsync(); - var result = await sut.ExecuteAsync(CreateContentCommand(command)); + var result = await PublishAsync(CreateContentCommand(command)); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Equal(Status.Draft, sut.Snapshot.Status); @@ -410,9 +401,9 @@ namespace Squidex.Domain.Apps.Entities.Contents await ExecutePublishAsync(); await ExecuteProposeUpdateAsync(); - var result = await sut.ExecuteAsync(CreateContentCommand(command)); + var result = await PublishAsync(CreateContentCommand(command)); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.False(sut.Snapshot.IsPending); @@ -434,9 +425,9 @@ namespace Squidex.Domain.Apps.Entities.Contents await ExecuteCreateAsync(); - var result = await sut.ExecuteAsync(CreateContentCommand(command)); + var result = await PublishAsync(CreateContentCommand(command)); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Equal(Status.Draft, sut.Snapshot.Status); Assert.Equal(Status.Published, sut.Snapshot.ScheduleJob!.Status); @@ -462,9 +453,9 @@ namespace Squidex.Domain.Apps.Entities.Contents A.CallTo(() => contentWorkflow.CanMoveToAsync(A.Ignored, Status.Published, User)) .Returns(false); - var result = await sut.ExecuteAsync(CreateContentCommand(command)); + var result = await PublishAsync(CreateContentCommand(command)); - result.ShouldBeEquivalent(sut.Snapshot); + result.ShouldBeEquivalent2(sut.Snapshot); Assert.Null(sut.Snapshot.ScheduleJob); @@ -484,9 +475,9 @@ namespace Squidex.Domain.Apps.Entities.Contents await ExecuteCreateAsync(); - var result = await sut.ExecuteAsync(CreateContentCommand(command)); + var result = await PublishAsync(CreateContentCommand(command)); - result.ShouldBeEquivalent(new EntitySavedResult(1)); + result.ShouldBeEquivalent2(new EntitySavedResult(1)); Assert.True(sut.Snapshot.IsDeleted); @@ -508,9 +499,9 @@ namespace Squidex.Domain.Apps.Entities.Contents await ExecutePublishAsync(); await ExecuteProposeUpdateAsync(); - var result = await sut.ExecuteAsync(CreateContentCommand(command)); + var result = await PublishAsync(CreateContentCommand(command)); - result.ShouldBeEquivalent(new EntitySavedResult(3)); + result.ShouldBeEquivalent2(new EntitySavedResult(3)); Assert.False(sut.Snapshot.IsPending); @@ -522,37 +513,37 @@ namespace Squidex.Domain.Apps.Entities.Contents private Task ExecuteCreateAsync() { - return sut.ExecuteAsync(CreateContentCommand(new CreateContent { Data = data })); + return PublishAsync(CreateContentCommand(new CreateContent { Data = data })); } private Task ExecuteUpdateAsync() { - return sut.ExecuteAsync(CreateContentCommand(new UpdateContent { Data = otherData })); + return PublishAsync(CreateContentCommand(new UpdateContent { Data = otherData })); } private Task ExecuteProposeUpdateAsync() { - return sut.ExecuteAsync(CreateContentCommand(new UpdateContent { Data = otherData, AsDraft = true })); + return PublishAsync(CreateContentCommand(new UpdateContent { Data = otherData, AsDraft = true })); } private Task ExecuteChangeStatusAsync(Status status, Instant? dueTime = null) { - return sut.ExecuteAsync(CreateContentCommand(new ChangeContentStatus { Status = status, DueTime = dueTime })); + return PublishAsync(CreateContentCommand(new ChangeContentStatus { Status = status, DueTime = dueTime })); } private Task ExecuteDeleteAsync() { - return sut.ExecuteAsync(CreateContentCommand(new DeleteContent())); + return PublishAsync(CreateContentCommand(new DeleteContent())); } private Task ExecuteArchiveAsync() { - return sut.ExecuteAsync(CreateContentCommand(new ChangeContentStatus { Status = Status.Archived })); + return PublishAsync(CreateContentCommand(new ChangeContentStatus { Status = Status.Archived })); } private Task ExecutePublishAsync() { - return sut.ExecuteAsync(CreateContentCommand(new ChangeContentStatus { Status = Status.Published })); + return PublishAsync(CreateContentCommand(new ChangeContentStatus { Status = Status.Published })); } private ScriptContext ScriptContext(NamedContentData? newData, NamedContentData? oldData, Status newStatus) @@ -588,5 +579,12 @@ namespace Squidex.Domain.Apps.Entities.Contents return CreateCommand(command); } + + private async Task PublishAsync(ContentCommand command) + { + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + return result; + } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentImporterCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentImporterCommandMiddlewareTests.cs new file mode 100644 index 000000000..0deaf68fd --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentImporterCommandMiddlewareTests.cs @@ -0,0 +1,142 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Json.Objects; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public class ContentImporterCommandMiddlewareTests + { + private readonly IServiceProvider serviceProvider = A.Fake(); + private readonly ICommandBus commandBus = A.Dummy(); + private readonly ContentImporterCommandMiddleware sut; + + public ContentImporterCommandMiddlewareTests() + { + sut = new ContentImporterCommandMiddleware(serviceProvider); + } + + [Fact] + public async Task Should_do_nothing_if_datas_is_null() + { + var command = new CreateContents(); + + var context = new CommandContext(command, commandBus); + + await sut.HandleAsync(context); + + Assert.True(context.PlainResult is ImportResult); + + A.CallTo(() => serviceProvider.GetService(A.Ignored)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_do_nothing_if_datas_is_empty() + { + var command = new CreateContents { Datas = new List() }; + + var context = new CommandContext(command, commandBus); + + await sut.HandleAsync(context); + + Assert.True(context.PlainResult is ImportResult); + + A.CallTo(() => serviceProvider.GetService(A.Ignored)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_import_data() + { + var data1 = CreateData(1); + var data2 = CreateData(2); + + var domainObject = A.Fake(); + + A.CallTo(() => serviceProvider.GetService(typeof(ContentDomainObject))) + .Returns(domainObject); + + var command = new CreateContents + { + Datas = new List + { + data1, + data2 + } + }; + + var context = new CommandContext(command, commandBus); + + await sut.HandleAsync(context); + + var result = context.Result(); + + Assert.Equal(2, result.Count); + Assert.Equal(2, result.Count(x => x.ContentId.HasValue && x.Exception == null)); + + A.CallTo(() => domainObject.Setup(A.Ignored)) + .MustHaveHappenedTwiceExactly(); + + A.CallTo(() => domainObject.ExecuteAsync(A.Ignored)) + .MustHaveHappenedTwiceExactly(); + } + + [Fact] + public async Task Should_skip_exception() + { + var data1 = CreateData(1); + var data2 = CreateData(2); + + var domainObject = A.Fake(); + + var exception = new InvalidOperationException(); + + A.CallTo(() => serviceProvider.GetService(typeof(ContentDomainObject))) + .Returns(domainObject); + + A.CallTo(() => domainObject.ExecuteAsync(A.That.Matches(x => x.Data == data1))) + .Throws(exception); + + var command = new CreateContents + { + Datas = new List + { + data1, + data2 + } + }; + + var context = new CommandContext(command, commandBus); + + await sut.HandleAsync(context); + + var result = context.Result(); + + Assert.Equal(2, result.Count); + Assert.Equal(1, result.Count(x => x.ContentId.HasValue && x.Exception == null)); + Assert.Equal(1, result.Count(x => !x.ContentId.HasValue && x.Exception == exception)); + } + + private static NamedContentData CreateData(int value) + { + return new NamedContentData() + .AddField("value", + new ContentFieldData() + .AddJsonValue("iv", JsonValue.Create(value))); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/SingletonCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/SingletonCommandMiddlewareTests.cs index 45fc7d96a..5173e08f3 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/SingletonCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/SingletonCommandMiddlewareTests.cs @@ -22,8 +22,10 @@ namespace Squidex.Domain.Apps.Entities.Contents [Fact] public async Task Should_create_content_when_singleton_schema_is_created() { + var command = new CreateSchema { IsSingleton = true, Name = "my-schema" }; + var context = - new CommandContext(new CreateSchema { IsSingleton = true, Name = "my-schema" }, commandBus) + new CommandContext(command, commandBus) .Complete(); await sut.HandleAsync(context); @@ -35,8 +37,10 @@ namespace Squidex.Domain.Apps.Entities.Contents [Fact] public async Task Should_not_create_content_when_non_singleton_schema_is_created() { + var command = new CreateSchema { IsSingleton = false }; + var context = - new CommandContext(new CreateSchema { IsSingleton = false }, commandBus) + new CommandContext(command, commandBus) .Complete(); await sut.HandleAsync(context); @@ -48,8 +52,10 @@ namespace Squidex.Domain.Apps.Entities.Contents [Fact] public async Task Should_not_create_content_when_singleton_schema_not_created() { + var command = new CreateSchema { IsSingleton = true }; + var context = - new CommandContext(new CreateSchema { IsSingleton = true }, commandBus); + new CommandContext(command, commandBus); await sut.HandleAsync(context); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesIndexTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesIndexTests.cs index b1fb9cf12..d8407428b 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesIndexTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesIndexTests.cs @@ -78,8 +78,10 @@ namespace Squidex.Domain.Apps.Entities.Rules.Indexes { var ruleId = Guid.NewGuid(); + var command = new CreateRule { RuleId = ruleId, AppId = appId }; + var context = - new CommandContext(new CreateRule { RuleId = ruleId, AppId = appId }, commandBus) + new CommandContext(command, commandBus) .Complete(); await sut.HandleAsync(context); @@ -93,8 +95,10 @@ namespace Squidex.Domain.Apps.Entities.Rules.Indexes { var rule = SetupRule(0, false); + var command = new DeleteRule { RuleId = rule.Id }; + var context = - new CommandContext(new DeleteRule { RuleId = rule.Id }, commandBus) + new CommandContext(command, commandBus) .Complete(); await sut.HandleAsync(context); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs similarity index 95% rename from backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs index 01f3837ca..3e9e9c899 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs @@ -22,12 +22,12 @@ using Xunit; namespace Squidex.Domain.Apps.Entities.Rules { - public class RuleGrainTests : HandlerTestBase + public class RuleDomainObjectTests : HandlerTestBase { private readonly IAppProvider appProvider = A.Fake(); private readonly IRuleEnqueuer ruleEnqueuer = A.Fake(); private readonly Guid ruleId = Guid.NewGuid(); - private readonly RuleGrain sut; + private readonly RuleDomainObject sut; protected override Guid Id { @@ -39,10 +39,10 @@ namespace Squidex.Domain.Apps.Entities.Rules public int Value { get; set; } } - public RuleGrainTests() + public RuleDomainObjectTests() { - sut = new RuleGrain(Store, A.Dummy(), appProvider, ruleEnqueuer); - sut.ActivateAsync(Id).Wait(); + sut = new RuleDomainObject(Store, A.Dummy(), appProvider, ruleEnqueuer); + sut.Setup(Id); } [Fact] @@ -234,7 +234,7 @@ namespace Squidex.Domain.Apps.Entities.Rules { var result = await sut.ExecuteAsync(CreateRuleCommand(command)); - return result.Value; + return result; } } } \ No newline at end of file diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs index ca0989f5e..c4a821816 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs @@ -195,8 +195,10 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes { var schema = SetupSchema(0, isDeleted); + var command = new DeleteSchema { SchemaId = schema.Id }; + var context = - new CommandContext(new DeleteSchema { SchemaId = schema.Id }, commandBus) + new CommandContext(command, commandBus) .Complete(); await sut.HandleAsync(context); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaDomainObjectTests.cs similarity index 98% rename from backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaGrainTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaDomainObjectTests.cs index 8d32952a6..052f9f1ab 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaGrainTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaDomainObjectTests.cs @@ -23,24 +23,24 @@ using Xunit; namespace Squidex.Domain.Apps.Entities.Schemas { - public class SchemaGrainTests : HandlerTestBase + public class SchemaDomainObjectTests : HandlerTestBase { private readonly string fieldName = "age"; private readonly string arrayName = "array"; private readonly NamedId fieldId = NamedId.Of(1L, "age"); private readonly NamedId arrayId = NamedId.Of(1L, "array"); private readonly NamedId nestedId = NamedId.Of(2L, "age"); - private readonly SchemaGrain sut; + private readonly SchemaDomainObject sut; protected override Guid Id { get { return SchemaId; } } - public SchemaGrainTests() + public SchemaDomainObjectTests() { - sut = new SchemaGrain(Store, A.Dummy()); - sut.ActivateAsync(Id).Wait(); + sut = new SchemaDomainObject(Store, A.Dummy()); + sut.Setup(Id); } [Fact] @@ -762,7 +762,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas { var result = await sut.ExecuteAsync(CreateCommand(command)); - return result.Value; + return result; } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectTests.cs similarity index 77% rename from backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectTests.cs index 2424c8470..63e726cfe 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectTests.cs @@ -12,28 +12,27 @@ using System.Threading.Tasks; using FakeItEasy; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.States; using Squidex.Infrastructure.TestHelpers; using Xunit; namespace Squidex.Infrastructure.Commands { - public class DomainObjectGrainTests + public class DomainObjectTests { private readonly IStore store = A.Fake>(); private readonly IPersistence persistence = A.Fake>(); private readonly Guid id = Guid.NewGuid(); private readonly MyDomainObject sut; - public sealed class MyDomainObject : DomainObjectGrain + public sealed class MyDomainObject : DomainObject { public MyDomainObject(IStore store) : base(store, A.Dummy()) { } - protected override Task ExecuteAsync(IAggregateCommand command) + public override Task ExecuteAsync(IAggregateCommand command) { switch (command) { @@ -70,7 +69,7 @@ namespace Squidex.Infrastructure.Commands } } - public DomainObjectGrainTests() + public DomainObjectTests() { A.CallTo(() => store.WithSnapshotsAndEventSourcing(typeof(MyDomainObject), id, A>.Ignored, A.Ignored)) .Returns(persistence); @@ -89,14 +88,14 @@ namespace Squidex.Infrastructure.Commands { await SetupEmptyAsync(); - var result = await sut.ExecuteAsync(C(new CreateAuto { Value = 4 })); + var result = await sut.ExecuteAsync(new CreateAuto { Value = 4 }); A.CallTo(() => persistence.WriteSnapshotAsync(A.That.Matches(x => x.Value == 4))) .MustHaveHappened(); A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count() == 1))) .MustHaveHappened(); - Assert.True(result.Value is EntityCreatedResult); + Assert.True(result is EntityCreatedResult); Assert.Empty(sut.GetUncomittedEvents()); @@ -109,14 +108,14 @@ namespace Squidex.Infrastructure.Commands { await SetupCreatedAsync(); - var result = await sut.ExecuteAsync(C(new UpdateAuto { Value = 8 })); + var result = await sut.ExecuteAsync(new UpdateAuto { Value = 8 }); A.CallTo(() => persistence.WriteSnapshotAsync(A.That.Matches(x => x.Value == 8))) .MustHaveHappened(); A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count() == 1))) .MustHaveHappened(); - Assert.True(result.Value is EntitySavedResult); + Assert.True(result is EntitySavedResult); Assert.Empty(sut.GetUncomittedEvents()); @@ -124,6 +123,19 @@ namespace Squidex.Infrastructure.Commands Assert.Equal(1, sut.Snapshot.Version); } + [Fact] + public async Task Should_rebuild_state_async() + { + await SetupCreatedAsync(); + + await sut.RebuildStateAsync(); + + A.CallTo(() => persistence.WriteSnapshotAsync(A.That.Matches(x => x.Value == 4))) + .MustHaveHappened(); + A.CallTo(() => persistence.WriteEventsAsync(A>>.Ignored)) + .MustHaveHappenedOnceExactly(); + } + [Fact] public async Task Should_not_update_when_snapshot_is_not_changed() { @@ -131,9 +143,9 @@ namespace Squidex.Infrastructure.Commands var previousSnapshot = sut.Snapshot; - var result = await sut.ExecuteAsync(C(new UpdateAuto { Value = MyDomainState.Unchanged })); + var result = await sut.ExecuteAsync(new UpdateAuto { Value = MyDomainState.Unchanged }); - Assert.True(result.Value is EntitySavedResult); + Assert.True(result is EntitySavedResult); Assert.Empty(sut.GetUncomittedEvents()); @@ -144,11 +156,11 @@ namespace Squidex.Infrastructure.Commands } [Fact] - public async Task Should_throw_exception_when_already_created() + public async Task Should_not_throw_exception_when_already_created() { await SetupCreatedAsync(); - await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new CreateAuto()))); + await sut.ExecuteAsync(new CreateAuto()); } [Fact] @@ -156,7 +168,7 @@ namespace Squidex.Infrastructure.Commands { await SetupEmptyAsync(); - await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new UpdateAuto()))); + await Assert.ThrowsAsync(() => sut.ExecuteAsync(new UpdateAuto())); } [Fact] @@ -164,9 +176,9 @@ namespace Squidex.Infrastructure.Commands { await SetupEmptyAsync(); - var result = await sut.ExecuteAsync(C(new CreateCustom())); + var result = await sut.ExecuteAsync(new CreateCustom()); - Assert.Equal("CREATED", result.Value); + Assert.Equal("CREATED", result); } [Fact] @@ -174,9 +186,9 @@ namespace Squidex.Infrastructure.Commands { await SetupCreatedAsync(); - var result = await sut.ExecuteAsync(C(new UpdateCustom())); + var result = await sut.ExecuteAsync(new UpdateCustom()); - Assert.Equal("UPDATED", result.Value); + Assert.Equal("UPDATED", result); } [Fact] @@ -184,7 +196,7 @@ namespace Squidex.Infrastructure.Commands { await SetupCreatedAsync(); - await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new UpdateCustom { ExpectedVersion = 3 }))); + await Assert.ThrowsAsync(() => sut.ExecuteAsync(new UpdateCustom { ExpectedVersion = 3 })); } [Fact] @@ -195,7 +207,7 @@ namespace Squidex.Infrastructure.Commands A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) .Throws(new InvalidOperationException()); - await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new CreateAuto()))); + await Assert.ThrowsAsync(() => sut.ExecuteAsync(new CreateAuto())); Assert.Empty(sut.GetUncomittedEvents()); @@ -211,7 +223,7 @@ namespace Squidex.Infrastructure.Commands A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) .Throws(new InvalidOperationException()); - await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new UpdateAuto()))); + await Assert.ThrowsAsync(() => sut.ExecuteAsync(new UpdateAuto())); Assert.Empty(sut.GetUncomittedEvents()); @@ -221,19 +233,16 @@ namespace Squidex.Infrastructure.Commands private async Task SetupCreatedAsync() { - await sut.ActivateAsync(id); - - await sut.ExecuteAsync(C(new CreateAuto { Value = 4 })); - } + sut.Setup(id); - private static J C(IAggregateCommand command) - { - return command.AsJ(); + await sut.ExecuteAsync(new CreateAuto { Value = 4 }); } private async Task SetupEmptyAsync() { - await sut.ActivateAsync(id); + sut.Setup(id); + + await Task.Yield(); } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectGrainTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectTests.cs similarity index 81% rename from backend/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectGrainTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectTests.cs index fc2a9495d..e21aa16d4 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectGrainTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectTests.cs @@ -13,14 +13,13 @@ using FakeItEasy; using FluentAssertions; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.States; using Squidex.Infrastructure.TestHelpers; using Xunit; namespace Squidex.Infrastructure.Commands { - public class LogSnapshotDomainObjectGrainTests + public class LogSnapshotDomainObjectTests { private readonly IStore store = A.Fake>(); private readonly ISnapshotStore snapshotStore = A.Fake>(); @@ -28,14 +27,14 @@ namespace Squidex.Infrastructure.Commands private readonly Guid id = Guid.NewGuid(); private readonly MyLogDomainObject sut; - public sealed class MyLogDomainObject : LogSnapshotDomainObjectGrain + public sealed class MyLogDomainObject : LogSnapshotDomainObject { public MyLogDomainObject(IStore store) : base(store, A.Dummy()) { } - protected override Task ExecuteAsync(IAggregateCommand command) + public override Task ExecuteAsync(IAggregateCommand command) { switch (command) { @@ -72,7 +71,7 @@ namespace Squidex.Infrastructure.Commands } } - public LogSnapshotDomainObjectGrainTests() + public LogSnapshotDomainObjectTests() { A.CallTo(() => store.WithEventSourcing(typeof(MyLogDomainObject), id, A.Ignored)) .Returns(persistence); @@ -142,14 +141,14 @@ namespace Squidex.Infrastructure.Commands { await SetupEmptyAsync(); - var result = await sut.ExecuteAsync(C(new CreateAuto { Value = 4 })); + var result = await sut.ExecuteAsync(new CreateAuto { Value = 4 }); A.CallTo(() => snapshotStore.WriteAsync(id, A.That.Matches(x => x.Value == 4), -1, 0)) .MustHaveHappened(); A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count() == 1))) .MustHaveHappened(); - Assert.True(result.Value is EntityCreatedResult); + Assert.True(result is EntityCreatedResult); Assert.Empty(sut.GetUncomittedEvents()); @@ -162,14 +161,14 @@ namespace Squidex.Infrastructure.Commands { await SetupCreatedAsync(); - var result = await sut.ExecuteAsync(C(new UpdateAuto { Value = 8 })); + var result = await sut.ExecuteAsync(new UpdateAuto { Value = 8 }); A.CallTo(() => snapshotStore.WriteAsync(id, A.That.Matches(x => x.Value == 8), 0, 1)) .MustHaveHappened(); A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count() == 1))) .MustHaveHappened(); - Assert.True(result.Value is EntitySavedResult); + Assert.True(result is EntitySavedResult); Assert.Empty(sut.GetUncomittedEvents()); @@ -177,6 +176,19 @@ namespace Squidex.Infrastructure.Commands Assert.Equal(1, sut.Snapshot.Version); } + [Fact] + public async Task Should_rebuild_state_async() + { + await SetupCreatedAsync(); + + await sut.RebuildStateAsync(); + + A.CallTo(() => snapshotStore.WriteAsync(id, A.That.Matches(x => x.Value == 4), EtagVersion.Any, 0)) + .MustHaveHappened(); + A.CallTo(() => persistence.WriteEventsAsync(A>>.Ignored)) + .MustHaveHappenedOnceExactly(); + } + [Fact] public async Task Should_not_update_when_snapshot_is_not_changed() { @@ -184,9 +196,9 @@ namespace Squidex.Infrastructure.Commands var previousSnapshot = sut.Snapshot; - var result = await sut.ExecuteAsync(C(new UpdateAuto { Value = MyDomainState.Unchanged })); + var result = await sut.ExecuteAsync(new UpdateAuto { Value = MyDomainState.Unchanged }); - Assert.True(result.Value is EntitySavedResult); + Assert.True(result is EntitySavedResult); Assert.Empty(sut.GetUncomittedEvents()); @@ -197,11 +209,11 @@ namespace Squidex.Infrastructure.Commands } [Fact] - public async Task Should_throw_exception_when_already_created() + public async Task Should_not_throw_exception_when_already_created() { await SetupCreatedAsync(); - await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new CreateAuto()))); + await sut.ExecuteAsync(new CreateAuto()); } [Fact] @@ -209,7 +221,7 @@ namespace Squidex.Infrastructure.Commands { await SetupEmptyAsync(); - await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new UpdateAuto()))); + await Assert.ThrowsAsync(() => sut.ExecuteAsync(new UpdateAuto())); } [Fact] @@ -217,9 +229,9 @@ namespace Squidex.Infrastructure.Commands { await SetupEmptyAsync(); - var result = await sut.ExecuteAsync(C(new CreateCustom())); + var result = await sut.ExecuteAsync(new CreateCustom()); - Assert.Equal("CREATED", result.Value); + Assert.Equal("CREATED", result); } [Fact] @@ -227,9 +239,9 @@ namespace Squidex.Infrastructure.Commands { await SetupCreatedAsync(); - var result = await sut.ExecuteAsync(C(new UpdateCustom())); + var result = await sut.ExecuteAsync(new UpdateCustom()); - Assert.Equal("UPDATED", result.Value); + Assert.Equal("UPDATED", result); } [Fact] @@ -237,7 +249,7 @@ namespace Squidex.Infrastructure.Commands { await SetupCreatedAsync(); - await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new UpdateCustom { ExpectedVersion = 3 }))); + await Assert.ThrowsAsync(() => sut.ExecuteAsync(new UpdateCustom { ExpectedVersion = 3 })); } [Fact] @@ -248,7 +260,7 @@ namespace Squidex.Infrastructure.Commands A.CallTo(() => snapshotStore.WriteAsync(A.Ignored, A.Ignored, -1, 0)) .Throws(new InvalidOperationException()); - await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new CreateAuto()))); + await Assert.ThrowsAsync(() => sut.ExecuteAsync(new CreateAuto())); Assert.Empty(sut.GetUncomittedEvents()); @@ -264,7 +276,7 @@ namespace Squidex.Infrastructure.Commands A.CallTo(() => snapshotStore.WriteAsync(A.Ignored, A.Ignored, 0, 1)) .Throws(new InvalidOperationException()); - await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new UpdateAuto()))); + await Assert.ThrowsAsync(() => sut.ExecuteAsync(new UpdateAuto())); Assert.Empty(sut.GetUncomittedEvents()); @@ -274,26 +286,23 @@ namespace Squidex.Infrastructure.Commands private async Task SetupCreatedAsync() { - await sut.ActivateAsync(id); + sut.Setup(id); - await sut.ExecuteAsync(C(new CreateAuto { Value = 4 })); + await sut.ExecuteAsync(new CreateAuto { Value = 4 }); } private async Task SetupUpdatedAsync() { await SetupCreatedAsync(); - await sut.ExecuteAsync(C(new UpdateAuto { Value = 8 })); + await sut.ExecuteAsync(new UpdateAuto { Value = 8 }); } private async Task SetupEmptyAsync() { - await sut.ActivateAsync(id); - } + sut.Setup(id); - private static J C(IAggregateCommand command) - { - return command.AsJ(); + await Task.Yield(); } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs b/backend/tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs index 2cddffb8e..bdab8d8ca 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs @@ -176,7 +176,7 @@ namespace Squidex.Infrastructure.States } [Fact] - public async Task Should_write_to_store_with_previous_position() + public async Task Should_write_to_store_with_previous_version() { SetupEventStore(3); @@ -197,7 +197,18 @@ namespace Squidex.Infrastructure.States } [Fact] - public async Task Should_wrap_exception_when_writing_to_store_with_previous_position() + public async Task Should_write_events_to_store_with_empty_version() + { + var persistence = sut.WithEventSourcing(None.Type, key, null); + + await persistence.WriteEventAsync(Envelope.Create(new MyEvent())); + + A.CallTo(() => eventStore.AppendAsync(A.Ignored, key, EtagVersion.Empty, A>.That.Matches(x => x.Count == 1))) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_wrap_exception_when_writing_to_store_with_previous_version() { SetupEventStore(3); diff --git a/backend/tests/Squidex.Infrastructure.Tests/States/PersistenceSnapshotTests.cs b/backend/tests/Squidex.Infrastructure.Tests/States/PersistenceSnapshotTests.cs index 4dec98730..ac694d519 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/States/PersistenceSnapshotTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/States/PersistenceSnapshotTests.cs @@ -121,6 +121,17 @@ namespace Squidex.Infrastructure.States .MustHaveHappened(); } + [Fact] + public async Task Should_write_snapshot_to_store_with_empty_version() + { + var persistence = sut.WithSnapshots(None.Type, key, null); + + await persistence.WriteSnapshotAsync(100); + + A.CallTo(() => snapshotStore.WriteAsync(key, 100, EtagVersion.Empty, 0)) + .MustHaveHappened(); + } + [Fact] public async Task Should_not_wrap_exception_when_writing_to_store_with_previous_version() { diff --git a/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs index 9d96c2d89..d42aa25c2 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs @@ -14,14 +14,14 @@ using Squidex.Infrastructure.States; namespace Squidex.Infrastructure.TestHelpers { - public sealed class MyDomainObject : DomainObjectGrain + public sealed class MyDomainObject : DomainObject { public MyDomainObject(IStore store) : base(store, A.Dummy()) { } - protected override Task ExecuteAsync(IAggregateCommand command) + public override Task ExecuteAsync(IAggregateCommand command) { switch (command) { diff --git a/backend/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs b/backend/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs index 72c539c26..6737338c3 100644 --- a/backend/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs +++ b/backend/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Security; +using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; @@ -62,7 +63,7 @@ namespace Squidex.Web sut.OnException(context); - Validate(400, context); + Validate(400, context.Result, context.Exception); } [Fact] @@ -72,7 +73,7 @@ namespace Squidex.Web sut.OnException(context); - Validate(412, context); + Validate(412, context.Result, context.Exception); } [Fact] @@ -82,7 +83,7 @@ namespace Squidex.Web sut.OnException(context); - Validate(403, context); + Validate(403, context.Result, context.Exception); } [Fact] @@ -92,10 +93,44 @@ namespace Squidex.Web sut.OnException(context); - Validate(403, context); + Validate(403, context.Result, context.Exception); + } + + [Fact] + public async Task Should_unify_errror() + { + var context = R(new ProblemDetails { Status = 403, Type = "type" }); + + await sut.OnResultExecutionAsync(context, () => Task.FromResult(Result(context))); + + Validate(403, context.Result, null); + } + + private static ResultExecutedContext Result(ResultExecutingContext context) + { + var actionContext = ActionContext(); + + return new ResultExecutedContext(actionContext, new List(), context.Result, context.Controller); + } + + private static ResultExecutingContext R(ProblemDetails problem) + { + var actionContext = ActionContext(); + + return new ResultExecutingContext(actionContext, new List(), new ObjectResult(problem) { StatusCode = problem.Status }, null); } private static ExceptionContext E(Exception exception) + { + var actionContext = ActionContext(); + + return new ExceptionContext(actionContext, new List()) + { + Exception = exception + }; + } + + private static ActionContext ActionContext() { var httpContext = new DefaultHttpContext(); @@ -104,20 +139,24 @@ namespace Squidex.Web FilterDescriptors = new List() }); - return new ExceptionContext(actionContext, new List()) - { - Exception = exception - }; + return actionContext; } - private static void Validate(int statusCode, ExceptionContext context) + private static void Validate(int statusCode, IActionResult actionResult, Exception? exception) { - var result = (ObjectResult)context.Result!; + var result = (ObjectResult)actionResult; + + var error = (ErrorDto)result.Value; + + Assert.NotNull(error.Type); Assert.Equal(statusCode, result.StatusCode); - Assert.Equal(statusCode, (result.Value as ErrorDto)?.StatusCode); + Assert.Equal(statusCode, error.StatusCode); - Assert.Equal(context.Exception.Message, (result.Value as ErrorDto)!.Message); + if (exception != null) + { + Assert.Equal(exception.Message, error.Message); + } } } } diff --git a/backend/tools/Migrate_01/RebuilderExtensions.cs b/backend/tools/Migrate_01/RebuilderExtensions.cs index 68318e0c4..3aca8a079 100644 --- a/backend/tools/Migrate_01/RebuilderExtensions.cs +++ b/backend/tools/Migrate_01/RebuilderExtensions.cs @@ -25,32 +25,32 @@ namespace Migrate_01 { public static Task RebuildAppsAsync(this Rebuilder rebuilder, CancellationToken ct = default) { - return rebuilder.RebuildAsync("^app\\-", ct); + return rebuilder.RebuildAsync("^app\\-", ct); } public static Task RebuildSchemasAsync(this Rebuilder rebuilder, CancellationToken ct = default) { - return rebuilder.RebuildAsync("^schema\\-", ct); + return rebuilder.RebuildAsync("^schema\\-", ct); } public static Task RebuildRulesAsync(this Rebuilder rebuilder, CancellationToken ct = default) { - return rebuilder.RebuildAsync("^rule\\-", ct); + return rebuilder.RebuildAsync("^rule\\-", ct); } public static Task RebuildAssetsAsync(this Rebuilder rebuilder, CancellationToken ct = default) { - return rebuilder.RebuildAsync("^asset\\-", ct); + return rebuilder.RebuildAsync("^asset\\-", ct); } public static Task RebuildAssetFoldersAsync(this Rebuilder rebuilder, CancellationToken ct = default) { - return rebuilder.RebuildAsync("^assetfolder\\-", ct); + return rebuilder.RebuildAsync("^assetfolder\\-", ct); } public static Task RebuildContentAsync(this Rebuilder rebuilder, CancellationToken ct = default) { - return rebuilder.RebuildAsync("^content\\-", ct); + return rebuilder.RebuildAsync("^content\\-", ct); } } } \ No newline at end of file diff --git a/frontend/app/framework/services/local-store.service.spec.ts b/frontend/app/framework/services/local-store.service.spec.ts index 6c2b517bd..d721c8565 100644 --- a/frontend/app/framework/services/local-store.service.spec.ts +++ b/frontend/app/framework/services/local-store.service.spec.ts @@ -91,9 +91,11 @@ describe('LocalStore', () => { localStoreService.set('key1', 'abc'); localStoreService.setInt('key2', 2); + localStoreService.setInt('key3', 0); expect(localStoreService.getInt('key1', 13)).toBe(13); expect(localStoreService.getInt('key2', 13)).toBe(2); + expect(localStoreService.getInt('key3', 13)).toBe(0); expect(localStoreService.getInt('not_set', 13)).toBe(13); }); diff --git a/frontend/app/framework/services/local-store.service.ts b/frontend/app/framework/services/local-store.service.ts index 9bd1376e3..d3a60490a 100644 --- a/frontend/app/framework/services/local-store.service.ts +++ b/frontend/app/framework/services/local-store.service.ts @@ -7,6 +7,8 @@ import { Injectable } from '@angular/core'; +import { Types } from './../utils/types'; + export const LocalStoreServiceFactory = () => { return new LocalStoreService(); }; @@ -37,7 +39,17 @@ export class LocalStoreService { public getInt(key: string, fallback = 0): number { const value = this.get(key); - return value ? (parseInt(value, 10) || fallback) : fallback; + let result = fallback; + + if (Types.isString(value)) { + result = parseInt(value, 10); + } + + if (!Types.isNumber(result)) { + result = fallback; + } + + return result; } public set(key: string, value: string) { diff --git a/frontend/app/shell/pages/internal/notifications-menu.component.ts b/frontend/app/shell/pages/internal/notifications-menu.component.ts index 6744ff065..05e2a8f52 100644 --- a/frontend/app/shell/pages/internal/notifications-menu.component.ts +++ b/frontend/app/shell/pages/internal/notifications-menu.component.ts @@ -21,6 +21,8 @@ import { ResourceOwner } from '@app/shared'; +const CONFIG_KEY = 'notifications.version'; + @Component({ selector: 'sqx-notifications-menu', styleUrls: ['./notifications-menu.component.scss'], @@ -32,44 +34,42 @@ import { }) export class NotificationsMenuComponent extends ResourceOwner implements OnInit { private isOpen: boolean; - private configKey: string; public modalMenu = new ModalModel(); - public commentsUrl: string; public commentsState: CommentsState; - public userId: string; - public userToken: string; - public versionRead = -1; public versionReceived = -1; + public userToken: string; + public get unread() { return Math.max(0, this.versionReceived - this.versionRead); } - constructor(authService: AuthService, + constructor(authService: AuthService, commentsService: CommentsService, dialogs: DialogService, private readonly changeDetector: ChangeDetectorRef, - private readonly commentsService: CommentsService, - private readonly dialogs: DialogService, private readonly localStore: LocalStoreService ) { super(); this.userToken = authService.user!.token; - this.userId = authService.user!.id; - this.configKey = `users.${this.userId}.notifications`; - - this.versionRead = localStore.getInt(this.configKey, -1); + this.versionRead = localStore.getInt(CONFIG_KEY, -1); this.versionReceived = this.versionRead; + + const commentsUrl = `users/${authService.user!.id}/notifications`; + + this.commentsState = + new CommentsState( + commentsUrl, + commentsService, + dialogs, + this.versionRead); } public ngOnInit() { - this.commentsUrl = `users/${this.userId}/notifications`; - this.commentsState = new CommentsState(this.commentsUrl, this.commentsService, this.dialogs); - this.own( this.modalMenu.isOpen.pipe( tap(isOpen => { @@ -104,7 +104,7 @@ export class NotificationsMenuComponent extends ResourceOwner implements OnInit if (this.isOpen) { this.versionRead = this.versionReceived; - this.localStore.setInt(this.configKey, this.versionRead); + this.localStore.setInt(CONFIG_KEY, this.versionRead); } } } \ No newline at end of file