Browse Source

Refactoring/domain objects and Bulk Import (#471)

pull/473/head
Sebastian Stehle 6 years ago
committed by GitHub
parent
commit
c2ec70e111
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 50
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs
  2. 15
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationMode.cs
  3. 5
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs
  4. 5
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs
  5. 5
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs
  6. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs
  7. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs
  8. 26
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs
  9. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs
  10. 12
      backend/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs
  11. 19
      backend/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObjectGrain.cs
  12. 22
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs
  13. 41
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObjectGrain.cs
  14. 17
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetFolderDomainObject.cs
  15. 40
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetFolderDomainObjectGrain.cs
  16. 4
      backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs
  17. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs
  18. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs
  19. 31
      backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContents.cs
  20. 43
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs
  21. 33
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObjectGrain.cs
  22. 69
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentImporterCommandMiddleware.cs
  23. 17
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs
  24. 15
      backend/src/Squidex.Domain.Apps.Entities/Contents/ImportResult.cs
  25. 18
      backend/src/Squidex.Domain.Apps.Entities/Contents/ImportResultItem.cs
  26. 12
      backend/src/Squidex.Domain.Apps.Entities/Contents/SingletonCommandMiddleware.cs
  27. 12
      backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs
  28. 30
      backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObjectGrain.cs
  29. 6
      backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs
  30. 30
      backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObjectGrain.cs
  31. 18
      backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs
  32. 97
      backend/src/Squidex.Infrastructure/Commands/DomainObject.cs
  33. 105
      backend/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs
  34. 67
      backend/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs
  35. 34
      backend/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObject.cs
  36. 31
      backend/src/Squidex.Infrastructure/Commands/Rebuilder.cs
  37. 2
      backend/src/Squidex.Infrastructure/States/Persistence{TSnapshot,TKey}.cs
  38. 129
      backend/src/Squidex.Web/ApiExceptionConverter.cs
  39. 109
      backend/src/Squidex.Web/ApiExceptionFilterAttribute.cs
  40. 6
      backend/src/Squidex.Web/ErrorDto.cs
  41. 38
      backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  42. 2
      backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaOpenApiGenerator.cs
  43. 44
      backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ImportContentsDto.cs
  44. 32
      backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ImportResultDto.cs
  45. 3
      backend/src/Squidex/Config/Domain/AppsServices.cs
  46. 6
      backend/src/Squidex/Config/Domain/AssetServices.cs
  47. 3
      backend/src/Squidex/Config/Domain/CommandsServices.cs
  48. 3
      backend/src/Squidex/Config/Domain/ContentsServices.cs
  49. 3
      backend/src/Squidex/Config/Domain/RuleServices.cs
  50. 3
      backend/src/Squidex/Config/Domain/SchemasServices.cs
  51. 12
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs
  52. 10
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs
  53. 27
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs
  54. 12
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs
  55. 12
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs
  56. 16
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InviteUserCommandMiddlewareTests.cs
  57. 13
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs
  58. 35
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectGrainTests.cs
  59. 21
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectTests.cs
  60. 35
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetFolderDomainObjectGrainTests.cs
  61. 21
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetFolderDomainObjectTests.cs
  62. 4
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/BackupAssetsTests.cs
  63. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BackupContentsTests.cs
  64. 35
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectGrainTests.cs
  65. 112
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs
  66. 142
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentImporterCommandMiddlewareTests.cs
  67. 12
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/SingletonCommandMiddlewareTests.cs
  68. 8
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesIndexTests.cs
  69. 12
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs
  70. 4
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs
  71. 12
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaDomainObjectTests.cs
  72. 67
      backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectTests.cs
  73. 67
      backend/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectTests.cs
  74. 15
      backend/tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs
  75. 11
      backend/tests/Squidex.Infrastructure.Tests/States/PersistenceSnapshotTests.cs
  76. 4
      backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs
  77. 63
      backend/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs
  78. 12
      backend/tools/Migrate_01/RebuilderExtensions.cs
  79. 2
      frontend/app/framework/services/local-store.service.spec.ts
  80. 14
      frontend/app/framework/services/local-store.service.ts
  81. 32
      frontend/app/shell/pages/internal/notifications-menu.component.ts

50
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<string>.Empty, false)
CheckAssets checkAsset,
ValidationMode mode = ValidationMode.Default)
: this(contentId, schemaId, checkContent, checkContentsByIds, checkAsset, ImmutableQueue<string>.Empty, false, mode)
{
}
@ -63,7 +66,8 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
CheckContentsByIds checkContentByIds,
CheckAssets checkAsset,
ImmutableQueue<string> 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<string> path, bool isOptional, ValidationMode mode)
{
return new ValidationContext(
contentId, schemaId,
contentId,
schemaId,
checkContent,
checkContentByIds,
checkAsset,
propertyPath.Enqueue(property),
IsOptional);
path, isOptional, mode);
}
public Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> GetContentIdsAsync(HashSet<Guid> ids)

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

5
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<Guid> assetIds && assetIds.Count > 0)
{
var assets = await context.GetAssetInfosAsync(assetIds);

5
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<Guid> contentIds)
{
var foundIds = await context.GetContentIdsAsync(contentIds.ToHashSet());

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

2
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<Guid>
{
[BsonId]
[BsonElement("_id")]

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

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

2
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<Guid>
{
private NamedContentData? data;
private NamedContentData dataDraft;

12
backend/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs → 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<AppState>, IAppGrain
public class AppDomainObject : DomainObject<AppState>
{
private readonly InitialPatterns initialPatterns;
private readonly IAppPlansProvider appPlansProvider;
private readonly IAppPlanBillingManager appPlansBillingManager;
private readonly IUserResolver userResolver;
public AppGrain(
public AppDomainObject(
InitialPatterns initialPatterns,
IStore<Guid> store,
ISemanticLog log,
@ -53,7 +52,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
this.initialPatterns = initialPatterns;
}
protected override Task<object?> ExecuteAsync(IAggregateCommand command)
public override Task<object?> 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<J<IAppEntity>> GetStateAsync()
{
return J.AsTask<IAppEntity>(Snapshot);
}
}
}

19
backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyGrain.cs → 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<MyDomainState>
public sealed class AppDomainObjectGrain : DomainObjectGrain<AppDomainObject, AppState>, IAppGrain
{
public MyGrain(IStore<Guid> store)
: base(store, A.Dummy<ISemanticLog>())
public AppDomainObjectGrain(IServiceProvider serviceProvider)
: base(serviceProvider)
{
}
protected override Task<object?> ExecuteAsync(IAggregateCommand command)
public async Task<J<IAppEntity>> GetStateAsync()
{
return Task.FromResult<object?>(null);
await DomainObject.EnsureLoadedAsync();
return Snapshot;
}
}
}

22
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs → 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<AssetState>, IAssetGrain
public class AssetDomainObject : LogSnapshotDomainObject<AssetState>
{
private static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(5);
private readonly ITagService tagService;
private readonly IAssetQueryService assetQuery;
public AssetGrain(IStore<Guid> store, ITagService tagService, IAssetQueryService assetQuery, IActivationLimit limit, ISemanticLog log)
public AssetDomainObject(IStore<Guid> 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<object?> ExecuteAsync(IAggregateCommand command)
public override Task<object?> ExecuteAsync(IAggregateCommand command)
{
VerifyNotDeleted();
@ -186,10 +175,5 @@ namespace Squidex.Domain.Apps.Entities.Assets
throw new DomainException("Asset has already been deleted");
}
}
public Task<J<IAssetEntity>> GetStateAsync(long version = EtagVersion.Any)
{
return J.AsTask<IAssetEntity>(GetSnapshot(version));
}
}
}

41
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<AssetDomainObject, AssetState>, 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<J<IAssetEntity>> GetStateAsync(long version = EtagVersion.Any)
{
await DomainObject.EnsureLoadedAsync();
return DomainObject.GetSnapshot(version);
}
}
}

17
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetFolderGrain.cs → 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<AssetFolderState>, IAssetFolderGrain
public class AssetFolderDomainObject : DomainObject<AssetFolderState>
{
private static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(5);
private readonly IAssetQueryService assetQuery;
public AssetFolderGrain(IStore<Guid> store, IAssetQueryService assetQuery, IActivationLimit limit, ISemanticLog log)
public AssetFolderDomainObject(IStore<Guid> 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<object?> ExecuteAsync(IAggregateCommand command)
public override Task<object?> ExecuteAsync(IAggregateCommand command)
{
VerifyNotDeleted();

40
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<AssetFolderDomainObject, AssetFolderState>, 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<J<IAssetFolderEntity>> GetStateAsync()
{
await DomainObject.EnsureLoadedAsync();
return Snapshot;
}
}
}

4
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<AssetState, AssetGrain>(async target =>
await rebuilder.InsertManyAsync<AssetDomainObject, AssetState>(async target =>
{
foreach (var id in assetIds)
{
@ -94,7 +94,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
if (assetFolderIds.Count > 0)
{
await rebuilder.InsertManyAsync<AssetFolderState, AssetFolderGrain>(async target =>
await rebuilder.InsertManyAsync<AssetFolderDomainObject, AssetFolderState>(async target =>
{
foreach (var id in assetFolderIds)
{

2
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<ContentState, ContentGrain>(async target =>
await rebuilder.InsertManyAsync<ContentDomainObject, ContentState>(async target =>
{
foreach (var contentId in contentIdsBySchemaId.Values.SelectMany(x => x))
{

4
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();

31
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<Guid> AppId { get; set; }
public NamedId<Guid> 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<NamedContentData> Datas { get; set; }
}
}

43
backend/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs → 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<ContentState>, IContentGrain
public class ContentDomainObject : LogSnapshotDomainObject<ContentState>
{
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<Guid> 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<object?> ExecuteAsync(IAggregateCommand command)
public override Task<object?> 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<J<IContentEntity>> GetStateAsync(long version = EtagVersion.Any)
{
return J.AsTask<IContentEntity>(GetSnapshot(version));
}
}
}

33
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<ContentDomainObject, ContentState>, IContentGrain
{
private static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(5);
public ContentDomainObjectGrain(IServiceProvider serviceProvider, IActivationLimit limit)
: base(serviceProvider)
{
limit?.SetLimit(5000, Lifetime);
}
public async Task<J<IContentEntity>> GetStateAsync(long version = -2)
{
await DomainObject.EnsureLoadedAsync();
return DomainObject.GetSnapshot(version);
}
}
}

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

17
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<IReadOnlyList<IAssetInfo>> QueryAssetsAsync(IEnumerable<Guid> assetIds)

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

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

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

12
backend/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs → 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<RuleState>, IRuleGrain
public class RuleDomainObject : DomainObject<RuleState>
{
private readonly IAppProvider appProvider;
private readonly IRuleEnqueuer ruleEnqueuer;
public RuleGrain(IStore<Guid> store, ISemanticLog log, IAppProvider appProvider, IRuleEnqueuer ruleEnqueuer)
public RuleDomainObject(IStore<Guid> 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<object?> ExecuteAsync(IAggregateCommand command)
public override Task<object?> ExecuteAsync(IAggregateCommand command)
{
VerifyNotDeleted();
@ -145,10 +144,5 @@ namespace Squidex.Domain.Apps.Entities.Rules
throw new DomainException("Rule has already been deleted.");
}
}
public Task<J<IRuleEntity>> GetStateAsync()
{
return J.AsTask<IRuleEntity>(Snapshot);
}
}
}

30
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<RuleDomainObject, RuleState>, IRuleGrain
{
public RuleDomainObjectGrain(IServiceProvider serviceProvider)
: base(serviceProvider)
{
}
public async Task<J<IRuleEntity>> GetStateAsync()
{
await DomainObject.EnsureLoadedAsync();
return Snapshot;
}
}
}

6
backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs → 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<SchemaState>, ISchemaGrain
public class SchemaDomainObject : DomainObject<SchemaState>
{
public SchemaGrain(IStore<Guid> store, ISemanticLog log)
public SchemaDomainObject(IStore<Guid> store, ISemanticLog log)
: base(store, log)
{
}
protected override Task<object?> ExecuteAsync(IAggregateCommand command)
public override Task<object?> ExecuteAsync(IAggregateCommand command)
{
VerifyNotDeleted();

30
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<SchemaDomainObject, SchemaState>, ISchemaGrain
{
public SchemaDomainObjectGrain(IServiceProvider serviceProvider)
: base(serviceProvider)
{
}
public async Task<J<ISchemaEntity>> GetStateAsync()
{
await DomainObject.EnsureLoadedAsync();
return Snapshot;
}
}
}

18
backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs

@ -111,7 +111,14 @@ namespace Squidex.Infrastructure.MongoDb
{
var update = updater(Builders<T>.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)
{

97
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<T> : DomainObjectBase<T> where T : class, IDomainState<T>, new()
{
private readonly IStore<Guid> store;
private T snapshot = new T { Version = EtagVersion.Empty };
private IPersistence<T>? persistence;
public override T Snapshot
{
get { return snapshot; }
}
protected DomainObject(IStore<Guid> store, ISemanticLog log)
: base(log)
{
Guard.NotNull(store);
this.store = store;
}
protected override void OnSetup()
{
persistence = store.WithSnapshotsAndEventSourcing(GetType(), Id, new HandleSnapshot<T>(ApplySnapshot), x => ApplyEvent(x, true));
}
protected sealed override bool ApplyEvent(Envelope<IEvent> @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<IEvent>[] 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<IEvent> @event)
{
return Snapshot.Apply(@event);
}
}
}

105
backend/src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs → 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<T> : GrainOfGuid, IDomainObjectGrain where T : IDomainState<T>, new()
public abstract class DomainObjectBase<T> where T : IDomainState<T>, new()
{
private readonly List<Envelope<IEvent>> uncomittedEvents = new List<Envelope<IEvent>>();
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<IEvent> @event)
protected virtual void RaiseEvent(Envelope<IEvent> @event)
{
Guard.NotNull(@event);
@ -91,82 +96,62 @@ namespace Squidex.Infrastructure.Commands
protected Task<object?> CreateReturnAsync<TCommand>(TCommand command, Func<TCommand, Task<object?>> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler, Mode.Create);
return InvokeAsync(command, handler, false);
}
protected Task<object?> CreateReturn<TCommand>(TCommand command, Func<TCommand, object?> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler?.ToAsync()!, Mode.Create);
return InvokeAsync(command, handler?.ToAsync()!, false);
}
protected Task<object?> CreateAsync<TCommand>(TCommand command, Func<TCommand, Task> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler.ToDefault<TCommand, object?>(), Mode.Create);
return InvokeAsync(command, handler.ToDefault<TCommand, object?>(), false);
}
protected Task<object?> Create<TCommand>(TCommand command, Action<TCommand> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler?.ToDefault<TCommand, object>()?.ToAsync()!, Mode.Create);
return InvokeAsync(command, handler?.ToDefault<TCommand, object>()?.ToAsync()!, false);
}
protected Task<object?> UpdateReturnAsync<TCommand>(TCommand command, Func<TCommand, Task<object?>> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler, Mode.Update);
return InvokeAsync(command, handler, true);
}
protected Task<object?> UpdateReturn<TCommand>(TCommand command, Func<TCommand, object?> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler?.ToAsync()!, Mode.Update);
return InvokeAsync(command, handler?.ToAsync()!, true);
}
protected Task<object?> UpdateAsync<TCommand>(TCommand command, Func<TCommand, Task> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler?.ToDefault<TCommand, object>()!, Mode.Update);
return InvokeAsync(command, handler?.ToDefault<TCommand, object>()!, true);
}
protected Task<object?> Update<TCommand>(TCommand command, Action<TCommand> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler?.ToDefault<TCommand, object>()?.ToAsync()!, Mode.Update);
}
protected Task<object?> UpsertReturnAsync<TCommand>(TCommand command, Func<TCommand, Task<object?>> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler, Mode.Upsert);
}
protected Task<object?> UpsertReturn<TCommand>(TCommand command, Func<TCommand, object?> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler?.ToAsync()!, Mode.Upsert);
}
protected Task<object?> UpsertAsync<TCommand>(TCommand command, Func<TCommand, Task> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler?.ToDefault<TCommand, object>()!, Mode.Upsert);
}
protected Task<object?> Upsert<TCommand>(TCommand command, Action<TCommand> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler?.ToDefault<TCommand, object>()?.ToAsync()!, Mode.Upsert);
return InvokeAsync(command, handler?.ToDefault<TCommand, object>()?.ToAsync()!, true);
}
private async Task<object?> InvokeAsync<TCommand>(TCommand command, Func<TCommand, Task<object?>> handler, Mode mode) where TCommand : class, IAggregateCommand
private async Task<object?> InvokeAsync<TCommand>(TCommand command, Func<TCommand, Task<object?>> 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<IEvent> @event, bool isLoading);
protected abstract Task ReadAsync(Type type, Guid id);
protected abstract Task ReadAsync();
protected abstract Task WriteAsync(Envelope<IEvent>[] newEvents, long previousVersion);
public async Task<J<object?>> ExecuteAsync(J<IAggregateCommand> command)
public virtual Task RebuildStateAsync()
{
var result = await ExecuteAsync(command.Value);
return TaskHelper.Done;
}
return result;
protected virtual void OnSetup()
{
}
protected abstract Task<object?> ExecuteAsync(IAggregateCommand command);
public abstract Task<object?> ExecuteAsync(IAggregateCommand command);
}
}

67
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<T> : DomainObjectGrainBase<T> where T : class, IDomainState<T>, new()
public abstract class DomainObjectGrain<T, TState> : GrainOfGuid where T : DomainObjectBase<TState> where TState : class, IDomainState<TState>, new()
{
private readonly IStore<Guid> store;
private T snapshot = new T { Version = EtagVersion.Empty };
private IPersistence<T>? persistence;
private readonly T domainObject;
public override T Snapshot
public TState Snapshot
{
get { return snapshot; }
get { return domainObject.Snapshot; }
}
protected DomainObjectGrain(IStore<Guid> store, ISemanticLog log)
: base(log)
protected T DomainObject
{
Guard.NotNull(store);
this.store = store;
get { return domainObject; }
}
protected sealed override bool ApplyEvent(Envelope<IEvent> @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<T>();
}
protected sealed override Task ReadAsync(Type type, Guid id)
protected override Task OnActivateAsync(Guid key)
{
persistence = store.WithSnapshotsAndEventSourcing(GetType(), id, new HandleSnapshot<T>(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<IEvent>[] newEvents, long previousVersion)
public async Task<J<object?>> ExecuteAsync(J<IAggregateCommand> 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<IEvent> @event)
{
return Snapshot.Apply(@event);
return result;
}
}
}

34
backend/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs → backend/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObject.cs

@ -15,7 +15,7 @@ using Squidex.Infrastructure.States;
namespace Squidex.Infrastructure.Commands
{
public abstract class LogSnapshotDomainObjectGrain<T> : DomainObjectGrainBase<T> where T : class, IDomainState<T>, new()
public abstract class LogSnapshotDomainObject<T> : DomainObjectBase<T> where T : class, IDomainState<T>, new()
{
private readonly IStore<Guid> store;
private readonly List<T> snapshots = new List<T> { new T { Version = EtagVersion.Empty } };
@ -26,7 +26,7 @@ namespace Squidex.Infrastructure.Commands
get { return snapshots.Last(); }
}
protected LogSnapshotDomainObjectGrain(IStore<Guid> store, ISemanticLog log)
protected LogSnapshotDomainObject(IStore<Guid> 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<IEvent>[] newEvents, long previousVersion)
{
persistence = store.WithEventSourcing(type, id, x => ApplyEvent(x, true));
if (newEvents.Length > 0 && persistence != null)
{
var persistedSnapshots = store.GetSnapshotStore<T>();
return persistence.ReadAsync();
await persistence.WriteEventsAsync(newEvents);
await persistedSnapshots.WriteAsync(Id, Snapshot, previousVersion, Snapshot.Version);
}
}
protected sealed override async Task WriteAsync(Envelope<IEvent>[] 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<T>();
await persistence.WriteEventsAsync(newEvents);
await persistedSnapshots.WriteAsync(Id, Snapshot, previousVersion, previousVersion + newEvents.Length);
await persistedSnapshots.WriteAsync(Id, Snapshot, EtagVersion.Any, Snapshot.Version);
}
}

31
backend/src/Squidex.Infrastructure/Commands/Rebuilder.cs

@ -23,24 +23,27 @@ namespace Squidex.Infrastructure.Commands
private readonly ILocalCache localCache;
private readonly IStore<Guid> store;
private readonly IEventStore eventStore;
private readonly IServiceProvider serviceProvider;
public Rebuilder(
ILocalCache localCache,
IStore<Guid> 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<TState, TGrain>(string filter, CancellationToken ct) where TState : IDomainState<TState>, new()
public Task RebuildAsync<T, TState>(string filter, CancellationToken ct) where T : DomainObjectBase<TState> where TState : class, IDomainState<TState>, new()
{
return RebuildAsync<TState, TGrain>(async target =>
return RebuildAsync<T, TState>(async target =>
{
await eventStore.QueryAsync(async storedEvent =>
{
@ -51,16 +54,16 @@ namespace Squidex.Infrastructure.Commands
}, ct);
}
public virtual async Task RebuildAsync<TState, TGrain>(IdSource source, CancellationToken ct = default) where TState : IDomainState<TState>, new()
public virtual async Task RebuildAsync<T, TState>(IdSource source, CancellationToken ct = default) where T : DomainObjectBase<TState> where TState : class, IDomainState<TState>, new()
{
Guard.NotNull(source);
await store.GetSnapshotStore<TState>().ClearAsync();
await InsertManyAsync<TState, TGrain>(source, ct);
await InsertManyAsync<T, TState>(source, ct);
}
public virtual async Task InsertManyAsync<TState, TGrain>(IdSource source, CancellationToken ct = default) where TState : IDomainState<TState>, new()
public virtual async Task InsertManyAsync<T, TState>(IdSource source, CancellationToken ct = default) where T : DomainObjectBase<TState> where TState : class, IDomainState<TState>, 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)
{

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

129
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<Func<Exception, ErrorDto?>> Handlers = new List<Func<Exception, ErrorDto?>>();
private static readonly Dictionary<int, string> Links = new Dictionary<int, string>
{
[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<T>(Func<T, ErrorDto> handler) where T : Exception
{
Handlers.Add(ex => ex is T typed ? handler(typed) : null);
}
static ApiExceptionConverter()
{
AddHandler<ValidationException>(OnValidationException);
AddHandler<DecoderFallbackException>(OnDecoderException);
AddHandler<DomainObjectNotFoundException>(OnDomainObjectNotFoundException);
AddHandler<DomainObjectVersionException>(OnDomainObjectVersionException);
AddHandler<DomainForbiddenException>(OnDomainForbiddenException);
AddHandler<DomainException>(OnDomainException);
AddHandler<SecurityException>(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];
}
}
}

109
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<Func<Exception, IActionResult?>> Handlers = new List<Func<Exception, IActionResult?>>();
private static void AddHandler<T>(Func<T, IActionResult> handler) where T : Exception
{
Handlers.Add(ex => ex is T typed ? handler(typed) : null);
}
static ApiExceptionFilterAttribute()
{
AddHandler<ValidationException>(OnValidationException);
AddHandler<DecoderFallbackException>(OnDecoderException);
AddHandler<DomainObjectNotFoundException>(OnDomainObjectNotFoundException);
AddHandler<DomainObjectVersionException>(OnDomainObjectVersionException);
AddHandler<DomainForbiddenException>(OnDomainForbiddenException);
AddHandler<DomainException>(OnDomainException);
AddHandler<SecurityException>(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
};
}
}
}
}

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

38
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
/// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param>
/// <param name="request">The full data for the content item.</param>
/// <param name="publish">Indicates whether the content should be published immediately.</param>
/// <param name="publish">True to automatically publish the content.</param>
/// <returns>
/// 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);
}
/// <summary>
/// Import content items.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param>
/// <param name="request">The import request.</param>
/// <returns>
/// 201 => Contents created.
/// 404 => Content references, schema or app not found.
/// 400 => Content data is not valid.
/// </returns>
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>
[HttpPost]
[Route("content/{app}/{name}/import")]
[ProducesResponseType(typeof(ImportResultDto[]), 200)]
[ApiPermission(Permissions.AppContentsCreate)]
[ApiCosts(5)]
public async Task<IActionResult> 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<ImportResult>();
var response = result.Select(x => ImportResultDto.FromImportResult(x, HttpContext)).ToArray();
return Ok(response);
}
/// <summary>
/// Update a content item.
/// </summary>
@ -296,7 +330,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// <param name="asDraft">Indicates whether the update is a proposal.</param>
/// <returns>
/// 200 => Content updated.
/// 404 => Content, schema or app not found.
/// 404 => Content references, schema or app not found.
/// 400 => Content data is not valid.
/// </returns>
/// <remarks>

2
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.");

44
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
{
/// <summary>
/// The data to import.
/// </summary>
[Required]
public List<NamedContentData> Datas { get; set; }
/// <summary>
/// True to automatically publish the content.
/// </summary>
public bool Publish { get; set; }
/// <summary>
/// True to turn off scripting for faster inserts. Default: true.
/// </summary>
public bool DoNotScript { get; set; } = true;
/// <summary>
/// True to turn off costly validation: Unique checks, asset checks and reference checks. Default: true.
/// </summary>
public bool OptimizeValidation { get; set; } = true;
public CreateContents ToCommand()
{
return SimpleMapper.Map(this, new CreateContents());
}
}
}

32
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
{
/// <summary>
/// The error when the import failed.
/// </summary>
public ErrorDto? Error { get; set; }
/// <summary>
/// The id of the content when the import succeeds.
/// </summary>
public Guid? ContentId { get; set; }
public static ImportResultDto FromImportResult(ImportResultItem result, HttpContext httpContext)
{
return new ImportResultDto { ContentId = result.ContentId, Error = result.Exception?.ToErrorDto(httpContext) };
}
}
}

3
backend/src/Squidex/Config/Domain/AppsServices.cs

@ -20,6 +20,9 @@ namespace Squidex.Config.Domain
{
public static void AddSquidexApps(this IServiceCollection services)
{
services.AddTransientAs<AppDomainObject>()
.AsSelf();
services.AddSingletonAs<RolePermissionsProvider>()
.AsSelf();

6
backend/src/Squidex/Config/Domain/AssetServices.cs

@ -28,6 +28,12 @@ namespace Squidex.Config.Domain
services.Configure<AssetOptions>(
config.GetSection("assets"));
services.AddTransientAs<AssetDomainObject>()
.AsSelf();
services.AddTransientAs<AssetFolderDomainObject>()
.AsSelf();
services.AddSingletonAs<AssetQueryParser>()
.AsSelf();

3
backend/src/Squidex/Config/Domain/CommandsServices.cs

@ -77,6 +77,9 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<AssetCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddSingletonAs<ContentImporterCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddSingletonAs<ContentCommandMiddleware>()
.As<ICommandMiddleware>();

3
backend/src/Squidex/Config/Domain/ContentsServices.cs

@ -30,6 +30,9 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<ContentQueryParser>()
.AsSelf();
services.AddTransientAs<ContentDomainObject>()
.AsSelf();
services.AddSingletonAs<ContentHistoryEventsCreator>()
.As<IHistoryEventsCreator>();

3
backend/src/Squidex/Config/Domain/RuleServices.cs

@ -28,6 +28,9 @@ namespace Squidex.Config.Domain
services.Configure<RuleOptions>(
config.GetSection("rules"));
services.AddTransientAs<RuleDomainObject>()
.AsSelf();
services.AddSingletonAs<EventEnricher>()
.As<IEventEnricher>();

3
backend/src/Squidex/Config/Domain/SchemasServices.cs

@ -15,6 +15,9 @@ namespace Squidex.Config.Domain
{
public static void AddSquidexSchemas(this IServiceCollection services)
{
services.AddTransientAs<SchemaDomainObject>()
.AsSelf();
services.AddSingletonAs<SchemaHistoryEventsCreator>()
.As<IHistoryEventsCreator>();
}

12
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()
{

10
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()
{

27
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<string> filter)
private ValidationContext Context(Guid id, Action<string> 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<IReadOnlyList<IAssetInfo>>(new List<IAssetInfo>());
}).Nested("property").Nested("iv");
},
mode)
.Nested("property")
.Nested("iv");
}
}
}

12
backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs → 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<AppState>
public class AppDomainObjectTests : HandlerTestBase<AppState>
{
private readonly IAppPlansProvider appPlansProvider = A.Fake<IAppPlansProvider>();
private readonly IAppPlanBillingManager appPlansBillingManager = A.Fake<IAppPlanBillingManager>();
@ -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<ISemanticLog>(), appPlansProvider, appPlansBillingManager, userResolver);
sut.ActivateAsync(Id).Wait();
sut = new AppDomainObject(initialPatterns, Store, A.Dummy<ISemanticLog>(), 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;
}
}
}

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

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

13
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<IAssetEnricher>();
private readonly IAssetFileStore assetFileStore = A.Fake<IAssetFileStore>();
private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
private readonly IAssetMetadataSource assetMetadataSource = A.Fake<IAssetMetadataSource>();
private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
private readonly IContextProvider contextProvider = A.Fake<IContextProvider>();
private readonly IGrainFactory grainFactory = A.Fake<IGrainFactory>();
private readonly IServiceProvider serviceProvider = A.Fake<IServiceProvider>();
private readonly ITagService tagService = A.Fake<ITagService>();
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<IActivationLimit>(), A.Dummy<ISemanticLog>());
var assetDomainObject = new AssetDomainObject(Store, tagService, assetQuery, A.Dummy<ISemanticLog>());
A.CallTo(() => serviceProvider.GetService(typeof(AssetDomainObject)))
.Returns(assetDomainObject);
asset = new AssetDomainObjectGrain(serviceProvider, null!);
asset.ActivateAsync(Id).Wait();
A.CallTo(() => contextProvider.Context)

35
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<IActivationLimit>();
[Fact]
public void Should_set_limit()
{
var serviceProvider = A.Fake<IServiceProvider>();
A.CallTo(() => serviceProvider.GetService(typeof(AssetDomainObject)))
.Returns(A.Dummy<AssetDomainObject>());
new AssetDomainObjectGrain(serviceProvider, limit);
A.CallTo(() => limit.SetLimit(5000, TimeSpan.FromMinutes(5)))
.MustHaveHappened();
}
}
}

21
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs → 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<AssetState>
public class AssetDomainObjectTests : HandlerTestBase<AssetState>
{
private readonly ITagService tagService = A.Fake<ITagService>();
private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
private readonly IActivationLimit limit = A.Fake<IActivationLimit>();
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<IAssetFolderEntity> { A.Fake<IAssetFolderEntity>() });
@ -49,15 +47,8 @@ namespace Squidex.Domain.Apps.Entities.Assets
A.CallTo(() => tagService.NormalizeTagsAsync(AppId, TagGroups.Assets, A<HashSet<string>>.Ignored, A<HashSet<string>>.Ignored))
.ReturnsLazily(x => Task.FromResult(x.GetArgument<HashSet<string>>(2)?.ToDictionary(x => x)!));
sut = new AssetGrain(Store, tagService, assetQuery, limit, A.Dummy<ISemanticLog>());
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<ISemanticLog>());
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;
}
}
}

35
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<IActivationLimit>();
[Fact]
public void Should_set_limit()
{
var serviceProvider = A.Fake<IServiceProvider>();
A.CallTo(() => serviceProvider.GetService(typeof(AssetFolderDomainObject)))
.Returns(A.Dummy<AssetFolderDomainObject>());
new AssetFolderDomainObjectGrain(serviceProvider, limit);
A.CallTo(() => limit.SetLimit(5000, TimeSpan.FromMinutes(5)))
.MustHaveHappened();
}
}
}

21
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetFolderGrainTests.cs → 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<AssetFolderState>
public class AssetFolderDomainObjectTests : HandlerTestBase<AssetFolderState>
{
private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
private readonly IActivationLimit limit = A.Fake<IActivationLimit>();
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<IAssetFolderEntity> { A.Fake<IAssetFolderEntity>() });
sut = new AssetFolderGrain(Store, assetQuery, limit, A.Dummy<ISemanticLog>());
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<ISemanticLog>());
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;
}
}
}

4
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<AssetState, AssetGrain>(A<IdSource>.Ignored, A<CancellationToken>.Ignored))
A.CallTo(() => rebuilder.InsertManyAsync<AssetDomainObject, AssetState>(A<IdSource>.Ignored, A<CancellationToken>.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<AssetFolderState, AssetFolderGrain>(A<IdSource>.Ignored, A<CancellationToken>.Ignored))
A.CallTo(() => rebuilder.InsertManyAsync<AssetFolderDomainObject, AssetFolderState>(A<IdSource>.Ignored, A<CancellationToken>.Ignored))
.Invokes((IdSource source, CancellationToken _) => source(add));
await sut.RestoreAsync(context);

2
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<ContentState, ContentGrain>(A<IdSource>.Ignored, A<CancellationToken>.Ignored))
A.CallTo(() => rebuilder.InsertManyAsync<ContentDomainObject, ContentState>(A<IdSource>.Ignored, A<CancellationToken>.Ignored))
.Invokes((IdSource source, CancellationToken _) => source(add));
await sut.RestoreAsync(context);

35
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<IActivationLimit>();
[Fact]
public void Should_set_limit()
{
var serviceProvider = A.Fake<IServiceProvider>();
A.CallTo(() => serviceProvider.GetService(typeof(ContentDomainObject)))
.Returns(A.Dummy<ContentDomainObject>());
new ContentDomainObjectGrain(serviceProvider, limit);
A.CallTo(() => limit.SetLimit(5000, TimeSpan.FromMinutes(5)))
.MustHaveHappened();
}
}
}

112
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs → 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<ContentState>
public class ContentDomainObjectTests : HandlerTestBase<ContentState>
{
private readonly Guid contentId = Guid.NewGuid();
private readonly IActivationLimit limit = A.Fake<IActivationLimit>();
private readonly IAppEntity app;
private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly IContentRepository contentRepository = A.Dummy<IContentRepository>();
@ -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<ISemanticLog>(), appProvider, A.Dummy<IAssetRepository>(), 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<ISemanticLog>(), appProvider, A.Dummy<IAssetRepository>(), 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<ValidationException>(() => sut.ExecuteAsync(CreateContentCommand(command)));
await Assert.ThrowsAsync<ValidationException>(() => 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<ValidationException>(() => sut.ExecuteAsync(CreateContentCommand(command)));
await Assert.ThrowsAsync<ValidationException>(() => 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<IContentEntity>.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<object?> PublishAsync(ContentCommand command)
{
var result = await sut.ExecuteAsync(CreateContentCommand(command));
return result;
}
}
}

142
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<IServiceProvider>();
private readonly ICommandBus commandBus = A.Dummy<ICommandBus>();
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<Type>.Ignored))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_do_nothing_if_datas_is_empty()
{
var command = new CreateContents { Datas = new List<NamedContentData>() };
var context = new CommandContext(command, commandBus);
await sut.HandleAsync(context);
Assert.True(context.PlainResult is ImportResult);
A.CallTo(() => serviceProvider.GetService(A<Type>.Ignored))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_import_data()
{
var data1 = CreateData(1);
var data2 = CreateData(2);
var domainObject = A.Fake<ContentDomainObject>();
A.CallTo(() => serviceProvider.GetService(typeof(ContentDomainObject)))
.Returns(domainObject);
var command = new CreateContents
{
Datas = new List<NamedContentData>
{
data1,
data2
}
};
var context = new CommandContext(command, commandBus);
await sut.HandleAsync(context);
var result = context.Result<ImportResult>();
Assert.Equal(2, result.Count);
Assert.Equal(2, result.Count(x => x.ContentId.HasValue && x.Exception == null));
A.CallTo(() => domainObject.Setup(A<Guid>.Ignored))
.MustHaveHappenedTwiceExactly();
A.CallTo(() => domainObject.ExecuteAsync(A<CreateContent>.Ignored))
.MustHaveHappenedTwiceExactly();
}
[Fact]
public async Task Should_skip_exception()
{
var data1 = CreateData(1);
var data2 = CreateData(2);
var domainObject = A.Fake<ContentDomainObject>();
var exception = new InvalidOperationException();
A.CallTo(() => serviceProvider.GetService(typeof(ContentDomainObject)))
.Returns(domainObject);
A.CallTo(() => domainObject.ExecuteAsync(A<CreateContent>.That.Matches(x => x.Data == data1)))
.Throws(exception);
var command = new CreateContents
{
Datas = new List<NamedContentData>
{
data1,
data2
}
};
var context = new CommandContext(command, commandBus);
await sut.HandleAsync(context);
var result = context.Result<ImportResult>();
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)));
}
}
}

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

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

12
backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs → 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<RuleState>
public class RuleDomainObjectTests : HandlerTestBase<RuleState>
{
private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly IRuleEnqueuer ruleEnqueuer = A.Fake<IRuleEnqueuer>();
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<ISemanticLog>(), appProvider, ruleEnqueuer);
sut.ActivateAsync(Id).Wait();
sut = new RuleDomainObject(Store, A.Dummy<ISemanticLog>(), 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;
}
}
}

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

12
backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaGrainTests.cs → 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<SchemaState>
public class SchemaDomainObjectTests : HandlerTestBase<SchemaState>
{
private readonly string fieldName = "age";
private readonly string arrayName = "array";
private readonly NamedId<long> fieldId = NamedId.Of(1L, "age");
private readonly NamedId<long> arrayId = NamedId.Of(1L, "array");
private readonly NamedId<long> 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<ISemanticLog>());
sut.ActivateAsync(Id).Wait();
sut = new SchemaDomainObject(Store, A.Dummy<ISemanticLog>());
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;
}
}
}

67
backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs → 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<Guid> store = A.Fake<IStore<Guid>>();
private readonly IPersistence<MyDomainState> persistence = A.Fake<IPersistence<MyDomainState>>();
private readonly Guid id = Guid.NewGuid();
private readonly MyDomainObject sut;
public sealed class MyDomainObject : DomainObjectGrain<MyDomainState>
public sealed class MyDomainObject : DomainObject<MyDomainState>
{
public MyDomainObject(IStore<Guid> store)
: base(store, A.Dummy<ISemanticLog>())
{
}
protected override Task<object?> ExecuteAsync(IAggregateCommand command)
public override Task<object?> 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<HandleSnapshot<MyDomainState>>.Ignored, A<HandleEvent>.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<MyDomainState>.That.Matches(x => x.Value == 4)))
.MustHaveHappened();
A.CallTo(() => persistence.WriteEventsAsync(A<IEnumerable<Envelope<IEvent>>>.That.Matches(x => x.Count() == 1)))
.MustHaveHappened();
Assert.True(result.Value is EntityCreatedResult<Guid>);
Assert.True(result is EntityCreatedResult<Guid>);
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<MyDomainState>.That.Matches(x => x.Value == 8)))
.MustHaveHappened();
A.CallTo(() => persistence.WriteEventsAsync(A<IEnumerable<Envelope<IEvent>>>.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<MyDomainState>.That.Matches(x => x.Value == 4)))
.MustHaveHappened();
A.CallTo(() => persistence.WriteEventsAsync(A<IEnumerable<Envelope<IEvent>>>.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<DomainException>(() => sut.ExecuteAsync(C(new CreateAuto())));
await sut.ExecuteAsync(new CreateAuto());
}
[Fact]
@ -156,7 +168,7 @@ namespace Squidex.Infrastructure.Commands
{
await SetupEmptyAsync();
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.ExecuteAsync(C(new UpdateAuto())));
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => 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<DomainObjectVersionException>(() => sut.ExecuteAsync(C(new UpdateCustom { ExpectedVersion = 3 })));
await Assert.ThrowsAsync<DomainObjectVersionException>(() => sut.ExecuteAsync(new UpdateCustom { ExpectedVersion = 3 }));
}
[Fact]
@ -195,7 +207,7 @@ namespace Squidex.Infrastructure.Commands
A.CallTo(() => persistence.WriteSnapshotAsync(A<MyDomainState>.Ignored))
.Throws(new InvalidOperationException());
await Assert.ThrowsAsync<InvalidOperationException>(() => sut.ExecuteAsync(C(new CreateAuto())));
await Assert.ThrowsAsync<InvalidOperationException>(() => sut.ExecuteAsync(new CreateAuto()));
Assert.Empty(sut.GetUncomittedEvents());
@ -211,7 +223,7 @@ namespace Squidex.Infrastructure.Commands
A.CallTo(() => persistence.WriteSnapshotAsync(A<MyDomainState>.Ignored))
.Throws(new InvalidOperationException());
await Assert.ThrowsAsync<InvalidOperationException>(() => sut.ExecuteAsync(C(new UpdateAuto())));
await Assert.ThrowsAsync<InvalidOperationException>(() => 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<IAggregateCommand> 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();
}
}
}

67
backend/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectGrainTests.cs → 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<Guid> store = A.Fake<IStore<Guid>>();
private readonly ISnapshotStore<MyDomainState, Guid> snapshotStore = A.Fake<ISnapshotStore<MyDomainState, Guid>>();
@ -28,14 +27,14 @@ namespace Squidex.Infrastructure.Commands
private readonly Guid id = Guid.NewGuid();
private readonly MyLogDomainObject sut;
public sealed class MyLogDomainObject : LogSnapshotDomainObjectGrain<MyDomainState>
public sealed class MyLogDomainObject : LogSnapshotDomainObject<MyDomainState>
{
public MyLogDomainObject(IStore<Guid> store)
: base(store, A.Dummy<ISemanticLog>())
{
}
protected override Task<object?> ExecuteAsync(IAggregateCommand command)
public override Task<object?> 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<HandleEvent>.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<MyDomainState>.That.Matches(x => x.Value == 4), -1, 0))
.MustHaveHappened();
A.CallTo(() => persistence.WriteEventsAsync(A<IEnumerable<Envelope<IEvent>>>.That.Matches(x => x.Count() == 1)))
.MustHaveHappened();
Assert.True(result.Value is EntityCreatedResult<Guid>);
Assert.True(result is EntityCreatedResult<Guid>);
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<MyDomainState>.That.Matches(x => x.Value == 8), 0, 1))
.MustHaveHappened();
A.CallTo(() => persistence.WriteEventsAsync(A<IEnumerable<Envelope<IEvent>>>.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<MyDomainState>.That.Matches(x => x.Value == 4), EtagVersion.Any, 0))
.MustHaveHappened();
A.CallTo(() => persistence.WriteEventsAsync(A<IEnumerable<Envelope<IEvent>>>.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<DomainException>(() => sut.ExecuteAsync(C(new CreateAuto())));
await sut.ExecuteAsync(new CreateAuto());
}
[Fact]
@ -209,7 +221,7 @@ namespace Squidex.Infrastructure.Commands
{
await SetupEmptyAsync();
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.ExecuteAsync(C(new UpdateAuto())));
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => 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<DomainObjectVersionException>(() => sut.ExecuteAsync(C(new UpdateCustom { ExpectedVersion = 3 })));
await Assert.ThrowsAsync<DomainObjectVersionException>(() => sut.ExecuteAsync(new UpdateCustom { ExpectedVersion = 3 }));
}
[Fact]
@ -248,7 +260,7 @@ namespace Squidex.Infrastructure.Commands
A.CallTo(() => snapshotStore.WriteAsync(A<Guid>.Ignored, A<MyDomainState>.Ignored, -1, 0))
.Throws(new InvalidOperationException());
await Assert.ThrowsAsync<InvalidOperationException>(() => sut.ExecuteAsync(C(new CreateAuto())));
await Assert.ThrowsAsync<InvalidOperationException>(() => sut.ExecuteAsync(new CreateAuto()));
Assert.Empty(sut.GetUncomittedEvents());
@ -264,7 +276,7 @@ namespace Squidex.Infrastructure.Commands
A.CallTo(() => snapshotStore.WriteAsync(A<Guid>.Ignored, A<MyDomainState>.Ignored, 0, 1))
.Throws(new InvalidOperationException());
await Assert.ThrowsAsync<InvalidOperationException>(() => sut.ExecuteAsync(C(new UpdateAuto())));
await Assert.ThrowsAsync<InvalidOperationException>(() => 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<IAggregateCommand> C(IAggregateCommand command)
{
return command.AsJ();
await Task.Yield();
}
}
}

15
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<Guid>.Ignored, key, EtagVersion.Empty, A<ICollection<EventData>>.That.Matches(x => x.Count == 1)))
.MustHaveHappened();
}
[Fact]
public async Task Should_wrap_exception_when_writing_to_store_with_previous_version()
{
SetupEventStore(3);

11
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<int>(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()
{

4
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<MyDomainState>
public sealed class MyDomainObject : DomainObject<MyDomainState>
{
public MyDomainObject(IStore<Guid> store)
: base(store, A.Dummy<ISemanticLog>())
{
}
protected override Task<object?> ExecuteAsync(IAggregateCommand command)
public override Task<object?> ExecuteAsync(IAggregateCommand command)
{
switch (command)
{

63
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<IFilterMetadata>(), context.Result, context.Controller);
}
private static ResultExecutingContext R(ProblemDetails problem)
{
var actionContext = ActionContext();
return new ResultExecutingContext(actionContext, new List<IFilterMetadata>(), new ObjectResult(problem) { StatusCode = problem.Status }, null);
}
private static ExceptionContext E(Exception exception)
{
var actionContext = ActionContext();
return new ExceptionContext(actionContext, new List<IFilterMetadata>())
{
Exception = exception
};
}
private static ActionContext ActionContext()
{
var httpContext = new DefaultHttpContext();
@ -104,20 +139,24 @@ namespace Squidex.Web
FilterDescriptors = new List<FilterDescriptor>()
});
return new ExceptionContext(actionContext, new List<IFilterMetadata>())
{
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);
}
}
}
}

12
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<AppState, AppGrain>("^app\\-", ct);
return rebuilder.RebuildAsync<AppDomainObject, AppState>("^app\\-", ct);
}
public static Task RebuildSchemasAsync(this Rebuilder rebuilder, CancellationToken ct = default)
{
return rebuilder.RebuildAsync<SchemaState, SchemaGrain>("^schema\\-", ct);
return rebuilder.RebuildAsync<SchemaDomainObject, SchemaState>("^schema\\-", ct);
}
public static Task RebuildRulesAsync(this Rebuilder rebuilder, CancellationToken ct = default)
{
return rebuilder.RebuildAsync<RuleState, RuleGrain>("^rule\\-", ct);
return rebuilder.RebuildAsync<RuleDomainObject, RuleState>("^rule\\-", ct);
}
public static Task RebuildAssetsAsync(this Rebuilder rebuilder, CancellationToken ct = default)
{
return rebuilder.RebuildAsync<AssetState, AssetGrain>("^asset\\-", ct);
return rebuilder.RebuildAsync<AssetDomainObject, AssetState>("^asset\\-", ct);
}
public static Task RebuildAssetFoldersAsync(this Rebuilder rebuilder, CancellationToken ct = default)
{
return rebuilder.RebuildAsync<AssetFolderState, AssetFolderGrain>("^assetfolder\\-", ct);
return rebuilder.RebuildAsync<AssetFolderDomainObject, AssetFolderState>("^assetfolder\\-", ct);
}
public static Task RebuildContentAsync(this Rebuilder rebuilder, CancellationToken ct = default)
{
return rebuilder.RebuildAsync<ContentState, ContentGrain>("^content\\-", ct);
return rebuilder.RebuildAsync<ContentDomainObject, ContentState>("^content\\-", ct);
}
}
}

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

14
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) {

32
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);
}
}
}
Loading…
Cancel
Save