From a7cd21473ad82259001d83261cf5bff1e2fbd96e Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sat, 9 Dec 2017 21:39:29 +0100 Subject: [PATCH] Squidex project updated --- .../Apps/MongoAppRepository.cs | 36 ++- .../Assets/MongoAssetRepository.cs | 9 +- .../Assets/MongoAssetStatsRepository.cs | 3 +- .../Contents/MongoContentEntity.cs | 21 +- .../Contents/MongoContentRepository.cs | 172 ++++++----- .../MongoContentRepository_EventHandling.cs | 58 ++++ .../Contents/Visitors/FindExtensions.cs | 1 + .../Rules/MongoRuleRepository.cs | 21 +- .../Schemas/MongoSchemaEntity.cs | 30 ++ .../Schemas/MongoSchemaRepository.cs | 101 +++++++ .../AppProvider.cs | 141 +++++++-- .../Apps/Repositories/IAppRepository.cs | 5 +- .../Contents/Commands/ChangeContentStatus.cs | 17 ++ .../Contents/Commands/ContentCommand.cs | 26 ++ .../Contents/Commands/ContentDataCommand.cs | 17 ++ .../Contents/Commands/CreateContent.cs | 15 + .../Commands/DeleteContent.cs} | 8 +- .../Contents/Commands/PatchContent.cs | 14 + .../Contents/Commands/UpdateContent.cs | 14 + .../Contents/ContentCommandMiddleware.cs | 158 ++++++++++ .../Contents/ContentDataChangedResult.cs | 24 ++ .../Contents/ContentDomainObject.cs | 101 +++++++ .../Contents/ContentOperationContext.cs | 139 +++++++++ .../Contents/ContentQueryService.cs | 11 +- .../Contents/GraphQL/CachingGraphQLService.cs | 2 +- .../Contents/Guards/GuardContent.cs | 74 +++++ .../Contents/IContentQueryService.cs | 2 +- .../Repositories/IContentRepository.cs | 2 + .../Contents/State/ContentState.cs | 49 +++- .../DomainObjectState.cs | 4 + .../IAppProvider.cs | 10 +- .../Rules/Guards/GuardRule.cs | 4 +- .../Rules/Guards/RuleTriggerValidator.cs | 4 +- .../Rules/Repositories/IRuleRepository.cs | 2 +- .../Rules/RuleEnqueuer.cs | 2 +- .../Schemas/Guards/GuardSchema.cs | 2 +- .../Schemas/Repositories/ISchemaRepository.cs | 4 +- .../MongoDb/MongoExtensions.cs | 23 ++ .../States/MongoSnapshotStore.cs | 4 +- .../UsageTracking/MongoUsageStore.cs | 2 - .../Commands/DomainObjectBase.cs | 9 +- .../Commands/IDomainObject.cs | 2 +- .../States/IPersistence.cs | 2 + .../States/Persistence.cs | 13 +- .../Areas/Api/Controllers/ApiController.cs | 7 +- .../Controllers/Apps/AppClientsController.cs | 2 +- .../Apps/AppContributorsController.cs | 4 +- .../Apps/AppLanguagesController.cs | 2 +- .../Api/Controllers/Apps/AppsController.cs | 6 +- .../Assets/AssetContentController.cs | 2 +- .../Controllers/Assets/AssetsController.cs | 8 +- .../Assets/Models/AssetCreatedDto.cs | 2 +- .../Assets/Models/AssetReplacedDto.cs | 4 +- .../Content/ContentSwaggerController.cs | 4 +- .../Controllers/Content/ContentsController.cs | 21 +- .../Generator/SchemasSwaggerGenerator.cs | 4 +- .../Controllers/Content/Models/ContentDto.cs | 2 +- .../Controllers/History/HistoryController.cs | 2 +- .../Controllers/Plans/AppPlansController.cs | 6 +- .../Rules/Models/Converters/RuleConverter.cs | 4 +- .../Controllers/Rules/Models/RuleEventDto.cs | 2 +- .../Api/Controllers/Rules/RulesController.cs | 8 +- .../Models/Converters/SchemaConverter.cs | 4 +- .../Schemas/SchemaFieldsController.cs | 2 +- .../Controllers/Schemas/SchemasController.cs | 12 +- .../Statistics/UsagesController.cs | 4 +- .../IdentityServer/Config/LazyClientStore.cs | 2 +- .../Middlewares/PortalRedirectMiddleware.cs | 2 +- src/Squidex/Config/Domain/ReadServices.cs | 36 +-- src/Squidex/Config/Domain/StoreServices.cs | 66 +++-- src/Squidex/Config/Domain/WriteServices.cs | 31 +- src/Squidex/Config/MyUsageOptions.cs | 2 +- src/Squidex/Pipeline/ApiCostsFilter.cs | 2 +- src/Squidex/Pipeline/AppApiFilter.cs | 4 +- .../Pipeline/AppPermissionAttribute.cs | 2 +- .../EnrichWithActorCommandMiddleware.cs | 2 +- .../EnrichWithAppIdCommandMiddleware.cs | 2 +- .../EnrichWithSchemaIdCommandMiddleware.cs | 10 +- src/Squidex/Pipeline/GraphQLUrlGenerator.cs | 10 +- src/Squidex/Pipeline/IAppFeature.cs | 2 +- src/Squidex/Squidex.csproj | 7 +- .../Contents/ContentCommandMiddlewareTests.cs | 244 ++++++++++++++++ .../Contents/ContentDomainObjectTests.cs | 269 ++++++++++++++++++ .../Contents/ContentEventTests.cs | 70 +++++ .../Contents/ContentQueryServiceTests.cs | 14 +- .../Contents/GraphQLTests.cs | 10 +- .../Contents/Guard/GuardContentTests.cs | 99 +++++++ .../Contents/ODataQueryTests.cs | 4 +- .../Rules/Guards/GuardRuleTests.cs | 2 +- .../Triggers/ContentChangedTriggerTests.cs | 14 +- .../Rules/RuleCommandMiddlewareTests.cs | 2 +- .../Rules/RuleEnqueuerTests.cs | 2 +- .../Squidex.Domain.Apps.Entities.Tests.csproj | 2 + .../Commands/AggregateHandlerTests.cs | 28 +- .../Commands/DomainObjectBaseTests.cs | 61 +--- .../Commands/TestHelpers/MyDomainObject.cs | 11 +- .../Grains/EventConsumerManagerTests.cs | 4 +- .../States/StateEventSourcingTests.cs | 42 +-- .../States/StateSnapshotTests.cs | 48 ++-- 99 files changed, 2121 insertions(+), 455 deletions(-) create mode 100644 src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_EventHandling.cs create mode 100644 src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs create mode 100644 src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentDataCommand.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs rename src/Squidex.Domain.Apps.Entities/{Assets/IAssetEventConsumer.cs => Contents/Commands/DeleteContent.cs} (65%) create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/Commands/PatchContent.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/Commands/UpdateContent.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/ContentDataChangedResult.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentEventTests.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs index 675ccddda..ee186a620 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -29,6 +30,33 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Apps return collection.Indexes.CreateOneAsync(Index.Ascending(x => x.UserIds)); } + public async Task FindAppIdByNameAsync(string name) + { + var appEntity = + await Collection.Find(x => x.State.Name == name).Only(x => x.Id) + .FirstOrDefaultAsync(); + + return appEntity != null ? Guid.Parse(appEntity.Id) : Guid.Empty; + } + + public async Task> QueryUserAppIdsAsync(string userId) + { + var appEntities = + await Collection.Find(x => x.UserIds.Contains(userId)).Only(x => x.Id) + .ToListAsync(); + + return appEntities.Select(x => Guid.Parse(x.Id)).ToList(); + } + + public async Task> QueryUserAppNamesAsync(string userId) + { + var appEntities = + await Collection.Find(x => x.UserIds.Contains(userId)).Project(Projection.Include(x => x.Id)) + .ToListAsync(); + + return appEntities.Select(x => x.Id).ToList(); + } + public async Task<(AppState Value, long Version)> ReadAsync(string key) { var existing = @@ -43,14 +71,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Apps return (null, -1); } - public async Task> QueryUserAppNamesAsync(string userId) - { - var appEntities = - await Collection.Find(x => x.UserIds.Contains(userId)).Project(Projection.Include(x => x.Id)).ToListAsync(); - - return appEntities.Select(x => x.Id).ToList(); - } - public async Task WriteAsync(string key, AppState value, long oldVersion, long newVersion) { try diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs index 32b27a99f..445ebe747 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs @@ -27,6 +27,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets { } + protected override string CollectionName() + { + return "Snapshots_Assets"; + } + protected override Task SetupCollectionAsync(IMongoCollection collection) { return collection.Indexes.CreateOneAsync( @@ -122,8 +127,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) { var existingVersion = - await Collection.Find(x => x.Id == key) - .Project(Projection.Exclude(x => x.Id)).FirstOrDefaultAsync(); + await Collection.Find(x => x.Id == key).Only(x => x.Id, x => x.Version) + .FirstOrDefaultAsync(); if (existingVersion != null) { diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsRepository.cs index a8fc50d26..ff01ab832 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsRepository.cs @@ -14,11 +14,12 @@ using MongoDB.Driver; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.MongoDb; namespace Squidex.Domain.Apps.Entities.MongoDb.Assets { - public partial class MongoAssetStatsRepository : MongoRepositoryBase, IAssetStatsRepository, IAssetEventConsumer + public partial class MongoAssetStatsRepository : MongoRepositoryBase, IAssetStatsRepository, IEventConsumer { public MongoAssetStatsRepository(IMongoDatabase database) : base(database) diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs index bb8ac758d..c1b815624 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs @@ -25,13 +25,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents [BsonId] [BsonElement] - [BsonRepresentation(BsonType.String)] - public Guid Id { get; set; } + public string DocumentId { get; set; } [BsonRequired] - [BsonElement("st")] - [BsonRepresentation(BsonType.String)] - public Status Status { get; set; } + [BsonElement] + public Guid Id { get; set; } [BsonRequired] [BsonElement("ct")] @@ -73,10 +71,19 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents [BsonElement("rd")] public List ReferencedIdsDeleted { get; set; } = new List(); + [BsonRequired] + [BsonElement("lt")] + public bool IsLatest { get; set; } + + [BsonRequired] + [BsonElement("st")] + [BsonRepresentation(BsonType.String)] + public Status Status { get; set; } + [BsonRequired] [BsonElement("do")] [BsonJson] - public IdContentData IdData { get; set; } + public IdContentData DataByIds { get; set; } NamedContentData IContentEntity.Data { @@ -85,7 +92,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents public void ParseData(Schema schema) { - data = IdData.ToData(schema, ReferencedIdsDeleted); + data = DataByIds.ToData(schema, ReferencedIdsDeleted); } } } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs index 29be5d217..8b927faf5 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs @@ -11,74 +11,129 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.OData.UriParser; -using MongoDB.Bson; using MongoDB.Driver; using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents.Repositories; +using Squidex.Domain.Apps.Entities.Contents.State; using Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.MongoDb; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { - public class MongoContentRepository : IContentRepository + public partial class MongoContentRepository : MongoRepositoryBase, + IEventConsumer, + IContentRepository, + ISnapshotStore { - private const string Prefix = "Projections_Content_"; - private readonly IMongoDatabase database; private readonly IAppProvider appProvider; - protected static FilterDefinitionBuilder Filter + public MongoContentRepository(IMongoDatabase database, IAppProvider appProvider) + : base(database) { - get - { - return Builders.Filter; - } + Guard.NotNull(appProvider, nameof(appProvider)); + + this.appProvider = appProvider; } - protected static UpdateDefinitionBuilder Update + protected override string CollectionName() { - get - { - return Builders.Update; - } + return "Snapshots_Assets"; } - protected static ProjectionDefinitionBuilder Projection + protected override async Task SetupCollectionAsync(IMongoCollection collection) { - get - { - return Builders.Projection; - } + await collection.Indexes.CreateOneAsync( + Index + .Ascending(x => x.Id) + .Descending(x => x.Version)); + + await collection.Indexes.CreateOneAsync( + Index + .Ascending(x => x.SchemaId) + .Descending(x => x.IsLatest) + .Descending(x => x.LastModified)); + + await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.ReferencedIds)); + await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Status)); + await collection.Indexes.CreateOneAsync(Index.Text(x => x.DataText)); } - protected static IndexKeysDefinitionBuilder Index + public async Task WriteAsync(string key, ContentState value, long oldVersion, long newVersion) { - get + var documentId = $"{key}_{oldVersion}"; + + var schema = await appProvider.GetSchemaAsync(value.AppId, value.SchemaId); + + if (schema == null) { - return Builders.IndexKeys; + throw new InvalidOperationException($"Cannot find schema {value.SchemaId}"); + } + + var idData = value.Data?.ToIdModel(schema.SchemaDef, true); + + var document = SimpleMapper.Map(value, new MongoContentEntity + { + DocumentId = documentId, + DataText = idData?.ToFullText(), + DataByIds = idData, + ReferencedIds = idData?.ToReferencedIds(schema.SchemaDef), + }); + + try + { + await Collection.InsertOneAsync(document); + } + catch (MongoWriteException ex) + { + if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) + { + var existingVersion = + await Collection.Find(x => x.Id == value.Id && x.IsLatest).Only(x => x.Id, x => x.Version) + .FirstOrDefaultAsync(); + + if (existingVersion != null) + { + throw new InconsistentStateException(existingVersion.Version, oldVersion, ex); + } + } + else + { + throw; + } } } - public MongoContentRepository(IMongoDatabase database, IAppProvider appProvider) + public async Task<(ContentState Value, long Version)> ReadAsync(string key) { - Guard.NotNull(database, nameof(database)); - Guard.NotNull(appProvider, nameof(appProvider)); + var id = Guid.Parse(key); - this.database = database; - this.appProvider = appProvider; + var existing = + await Collection.Find(x => x.Id == id && x.IsLatest) + .FirstOrDefaultAsync(); + + if (existing != null) + { + return (SimpleMapper.Map(existing, new ContentState()), existing.Version); + } + + return (null, -1); } public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, ODataUriParser odataQuery) { - var collection = GetCollection(app.Id); - IFindFluent cursor; try { cursor = - collection + Collection .Find(odataQuery, schema.Id, schema.SchemaDef, status) .Take(odataQuery) .Skip(odataQuery) @@ -103,14 +158,12 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents return contentEntities; } - public Task CountAsync(IAppEntity app, ISchemaEntity schema, Status[] status, ODataUriParser odataQuery) + public async Task CountAsync(IAppEntity app, ISchemaEntity schema, Status[] status, ODataUriParser odataQuery) { - var collection = GetCollection(app.Id); - IFindFluent cursor; try { - cursor = collection.Find(odataQuery, schema.Id, schema.SchemaDef, status); + cursor = Collection.Find(odataQuery, schema.Id, schema.SchemaDef, status); } catch (NotSupportedException) { @@ -121,15 +174,13 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents throw new ValidationException("This odata operation is not supported."); } - return cursor.CountAsync(); + return await cursor.CountAsync(); } public async Task CountAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet ids) { - var collection = GetCollection(app.Id); - var contentsCount = - await collection.Find(x => ids.Contains(x.Id)) + await Collection.Find(x => ids.Contains(x.Id) && x.IsLatest) .CountAsync(); return contentsCount; @@ -137,10 +188,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet ids) { - var collection = GetCollection(app.Id); - var contentEntities = - await collection.Find(x => ids.Contains(x.Id)) + await Collection.Find(x => ids.Contains(x.Id) && x.IsLatest) .ToListAsync(); foreach (var entity in contentEntities) @@ -153,21 +202,17 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents public async Task> QueryNotFoundAsync(Guid appId, Guid schemaId, IList contentIds) { - var collection = GetCollection(appId); - var contentEntities = - await collection.Find(x => contentIds.Contains(x.Id) && x.AppId == appId).Project(Projection.Include(x => x.Id)) + await Collection.Find(x => contentIds.Contains(x.Id) && x.AppId == appId).Only(x => x.Id) .ToListAsync(); - return contentIds.Except(contentEntities.Select(x => Guid.Parse(x["_id"].AsString))).ToList(); + return contentIds.Except(contentEntities.Select(x => x.Id)).ToList(); } - public async Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id) + public async Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id, long version) { - var collection = GetCollection(app.Id); - var contentEntity = - await collection.Find(x => x.Id == id) + await Collection.Find(x => x.Id == id && x.Version == version) .FirstOrDefaultAsync(); contentEntity?.ParseData(schema.SchemaDef); @@ -175,32 +220,15 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents return contentEntity; } - private async Task ForSchemaAsync(NamedId appId, Guid schemaId, Func, ISchemaEntity, Task> action) - { - var collection = GetCollection(appId.Id); - - var schema = await appProvider.GetSchemaAsync(appId.Name, schemaId, true); - - if (schema == null) - { - return; - } - - await action(collection, schema); - } - - private Task ForAppIdAsync(Guid appId, Func, Task> action) + public async Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id) { - var collection = GetCollection(appId); - - return action(collection); - } + var contentEntity = + await Collection.Find(x => x.Id == id && x.IsLatest) + .FirstOrDefaultAsync(); - private IMongoCollection GetCollection(Guid appId) - { - var name = $"{Prefix}{appId}"; + contentEntity?.ParseData(schema.SchemaDef); - return database.GetCollection(name); + return contentEntity; } } } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_EventHandling.cs new file mode 100644 index 000000000..74e8b36f7 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_EventHandling.cs @@ -0,0 +1,58 @@ +// ========================================================================== +// MongoContentRepository_EventHandling.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Domain.Apps.Events.Assets; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure.Dispatching; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents +{ + public partial class MongoContentRepository + { + public string Name + { + get { return GetType().Name; } + } + + public string EventsFilter + { + get { return "^(content-)|(asset-)"; } + } + + public override Task ClearAsync() + { + return TaskHelper.Done; + } + + public Task On(Envelope @event) + { + return this.DispatchActionAsync(@event.Payload, @event.Headers); + } + + protected Task On(AssetDeleted @event) + { + return Collection.UpdateManyAsync( + Filter.And( + Filter.AnyEq(x => x.ReferencedIds, @event.AssetId), + Filter.AnyNe(x => x.ReferencedIdsDeleted, @event.AssetId)), + Update.AddToSet(x => x.ReferencedIdsDeleted, @event.AssetId)); + } + + protected Task On(ContentDeleted @event) + { + return Collection.UpdateManyAsync( + Filter.And( + Filter.AnyEq(x => x.ReferencedIds, @event.ContentId), + Filter.AnyNe(x => x.ReferencedIdsDeleted, @event.ContentId)), + Update.AddToSet(x => x.ReferencedIdsDeleted, @event.ContentId)); + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs index 547107029..0c09e1398 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs @@ -69,6 +69,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors var filters = new List> { Filter.Eq(x => x.SchemaId, schemaId), + Filter.Eq(x => x.IsLatest, true), Filter.In(x => x.Status, status) }; diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs index c65f2820a..895bc54db 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs @@ -25,9 +25,15 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules { } - protected override Task SetupCollectionAsync(IMongoCollection collection) + protected override string CollectionName() { - return collection.Indexes.CreateOneAsync(Index.Ascending(x => x.State.AppId)); + return "States_Rules"; + } + + protected override async Task SetupCollectionAsync(IMongoCollection collection) + { + await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.State.AppId)); + await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.State.IsDeleted)); } public async Task<(RuleState Value, long Version)> ReadAsync(string key) @@ -44,12 +50,13 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules return (null, -1); } - public async Task> QueryRuleIdsAsync(Guid appId) + public async Task> QueryRuleIdsAsync(Guid appId) { var ruleEntities = - await Collection.Find(x => x.State.AppId == appId).Project(Projection.Include(x => x.Id)).ToListAsync(); + await Collection.Find(x => x.State.AppId == appId && !x.State.IsDeleted).Only(x => x.Id) + .ToListAsync(); - return ruleEntities.Select(x => x.Id).ToList(); + return ruleEntities.Select(x => Guid.Parse(x.Id)).ToList(); } public async Task WriteAsync(string key, RuleState value, long oldVersion, long newVersion) @@ -67,8 +74,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) { var existingVersion = - await Collection.Find(x => x.Id == key) - .Project(Projection.Exclude(x => x.Id)).FirstOrDefaultAsync(); + await Collection.Find(x => x.Id == key).Only(x => x.Id, x => x.Version) + .FirstOrDefaultAsync(); if (existingVersion != null) { diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs new file mode 100644 index 000000000..541575cef --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// MongoSchemaEntity.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using Squidex.Domain.Apps.Entities.Schemas.State; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas +{ + public sealed class MongoSchemaEntity + { + [BsonId] + [BsonElement] + [BsonRepresentation(BsonType.String)] + public string Id { get; set; } + + [BsonElement] + [BsonRequired] + public SchemaState State { get; set; } + + [BsonElement] + [BsonRequired] + public int Version { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs new file mode 100644 index 000000000..b1c14d9e9 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs @@ -0,0 +1,101 @@ +// ========================================================================== +// MongoSchemaRepository.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MongoDB.Driver; +using Squidex.Domain.Apps.Entities.Schemas.Repositories; +using Squidex.Domain.Apps.Entities.Schemas.State; +using Squidex.Infrastructure.MongoDb; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas +{ + public sealed class MongoSchemaRepository : MongoRepositoryBase, ISchemaRepository, ISnapshotStore + { + public MongoSchemaRepository(IMongoDatabase database) + : base(database) + { + } + + protected override string CollectionName() + { + return "Snapshots_Schemas"; + } + + protected override async Task SetupCollectionAsync(IMongoCollection collection) + { + await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.State.AppId)); + await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.State.Name)); + } + + public async Task<(SchemaState Value, long Version)> ReadAsync(string key) + { + var existing = + await Collection.Find(x => x.Id == key) + .FirstOrDefaultAsync(); + + if (existing != null) + { + return (existing.State, existing.Version); + } + + return (null, -1); + } + + public async Task FindSchemaIdAsync(Guid appId, string name) + { + var schemaEntity = + await Collection.Find(x => x.State.Name == name).Only(x => x.Id) + .FirstOrDefaultAsync(); + + return schemaEntity != null ? Guid.Parse(schemaEntity.Id) : Guid.Empty; + } + + public async Task> QuerySchemaIdsAsync(Guid appId) + { + var schemaEntities = + await Collection.Find(x => x.State.AppId == appId).Only(x => x.Id) + .ToListAsync(); + + return schemaEntities.Select(x => Guid.Parse(x.Id)).ToList(); + } + + public async Task WriteAsync(string key, SchemaState value, long oldVersion, long newVersion) + { + try + { + await Collection.UpdateOneAsync(x => x.Id == key && x.Version == oldVersion, + Update + .Set(x => x.State, value) + .Set(x => x.Version, newVersion), + Upsert); + } + catch (MongoWriteException ex) + { + if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) + { + var existingVersion = + await Collection.Find(x => x.Id == key).Only(x => x.Version) + .FirstOrDefaultAsync(); + + if (existingVersion != null) + { + throw new InconsistentStateException(existingVersion.Version, oldVersion, ex); + } + } + else + { + throw; + } + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/AppProvider.cs b/src/Squidex.Domain.Apps.Entities/AppProvider.cs index d862fe9a8..6d494f377 100644 --- a/src/Squidex.Domain.Apps.Entities/AppProvider.cs +++ b/src/Squidex.Domain.Apps.Entities/AppProvider.cs @@ -7,11 +7,16 @@ // ========================================================================== using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Apps.Repositories; using Squidex.Domain.Apps.Entities.Rules; +using Squidex.Domain.Apps.Entities.Rules.Repositories; using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Repositories; using Squidex.Infrastructure; using Squidex.Infrastructure.States; @@ -19,48 +24,148 @@ namespace Squidex.Domain.Apps.Entities { public sealed class AppProvider : IAppProvider { - private readonly IStateFactory factory; + private readonly ConcurrentDictionary appIds = new ConcurrentDictionary(); + private readonly ConcurrentDictionary, Guid> schemaIds = new ConcurrentDictionary, Guid>(); + private readonly IAppRepository appRepository; + private readonly IRuleRepository ruleRepository; + private readonly ISchemaRepository schemaRepository; + private readonly IStateFactory stateFactory; - public AppProvider(IStateFactory factory) + public AppProvider( + IAppRepository appRepository, + ISchemaRepository schemaRepository, + IStateFactory stateFactory, + IRuleRepository ruleRepository) { - Guard.NotNull(factory, nameof(factory)); + Guard.NotNull(appRepository, nameof(appRepository)); + Guard.NotNull(schemaRepository, nameof(schemaRepository)); + Guard.NotNull(stateFactory, nameof(stateFactory)); + Guard.NotNull(ruleRepository, nameof(ruleRepository)); - this.factory = factory; + this.appRepository = appRepository; + this.schemaRepository = schemaRepository; + this.stateFactory = stateFactory; + this.ruleRepository = ruleRepository; } - public Task GetAppAsync(string appName) + public async Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(Guid appId, Guid id) { - return null; + var app = await stateFactory.GetSingleAsync(appId.ToString()); + + if (app.Version < 0) + { + throw new DomainObjectNotFoundException(appId.ToString(), typeof(SchemaDomainObject)); + } + + var schema = await stateFactory.GetSingleAsync(id.ToString()); + + if (schema.Version < 0 || schema.State.IsDeleted) + { + throw new DomainObjectNotFoundException(id.ToString(), typeof(SchemaDomainObject)); + } + + return (app.State, schema.State); + } + + public async Task GetAppAsync(string appName) + { + var appId = await GetAppIdAsync(appName); + + var app = await stateFactory.GetSingleAsync(appName); + + if (app.Version < 0) + { + throw new DomainObjectNotFoundException(appName, typeof(SchemaDomainObject)); + } + + return app.State; + } + + public async Task GetSchemaAsync(Guid appId, string name, bool provideDeleted = false) + { + var schemaId = await GetSchemaIdAsync(appId, name); + + var schema = await stateFactory.GetSingleAsync(schemaId.ToString()); + + if (schema.Version < 0 || (schema.State.IsDeleted && !provideDeleted)) + { + throw new DomainObjectNotFoundException(schemaId.ToString(), typeof(SchemaDomainObject)); + } + + return schema.State; } - public Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(string appName, Guid id) + public async Task GetSchemaAsync(Guid appId, Guid id, bool provideDeleted = false) { - return null; + var schema = await stateFactory.GetSingleAsync(id.ToString()); + + if (schema.Version < 0 || (schema.State.IsDeleted && !provideDeleted)) + { + throw new DomainObjectNotFoundException(id.ToString(), typeof(SchemaDomainObject)); + } + + return schema.State; } - public Task> GetRulesAsync(string appName) + public async Task> GetSchemasAsync(Guid appId) { - return null; + var ids = await schemaRepository.QuerySchemaIdsAsync(appId); + + var schemas = + await Task.WhenAll( + ids.Select(id => stateFactory.GetSingleAsync(id.ToString()))); + + return schemas.Select(a => (ISchemaEntity)a.State).ToList(); } - public Task GetSchemaAsync(string appName, Guid id, bool provideDeleted = false) + public async Task> GetRulesAsync(Guid appId) { - return null; + var ids = await ruleRepository.QueryRuleIdsAsync(appId); + + var rules = + await Task.WhenAll( + ids.Select(id => stateFactory.GetSingleAsync(id.ToString()))); + + return rules.Select(a => (IRuleEntity)a.State).ToList(); } - public Task GetSchemaAsync(string appName, string name, bool provideDeleted = false) + public async Task> GetUserApps(string userId) { - return null; + var ids = await appRepository.QueryUserAppIdsAsync(userId); + + var apps = + await Task.WhenAll( + ids.Select(id => stateFactory.GetSingleAsync(id.ToString()))); + + return apps.Select(a => (IAppEntity)a.State).ToList(); } - public Task> GetSchemasAsync(string appName) + private async Task GetAppIdAsync(string name) { - return null; + var key = name; + + if (!appIds.TryGetValue(key, out var id)) + { + id = await appRepository.FindAppIdByNameAsync(name); + + appIds[key] = id; + } + + return id; } - public Task> GetUserApps(string userId) + private async Task GetSchemaIdAsync(Guid appId, string name) { - return null; + var key = Tuple.Create(appId, name); + + if (!schemaIds.TryGetValue(key, out var id)) + { + id = await schemaRepository.FindSchemaIdAsync(appId, name); + + schemaIds[key] = id; + } + + return id; } } } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs b/src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs index 2e567dc45..208ff3d5d 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -13,6 +14,8 @@ namespace Squidex.Domain.Apps.Entities.Apps.Repositories { public interface IAppRepository { - Task> QueryUserAppNamesAsync(string userId); + Task FindAppIdByNameAsync(string name); + + Task> QueryUserAppIdsAsync(string userId); } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs b/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs new file mode 100644 index 000000000..9628bbd3d --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// ChangeContentStatus.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================= + +using Squidex.Domain.Apps.Core.Contents; + +namespace Squidex.Domain.Apps.Entities.Contents.Commands +{ + public sealed class ChangeContentStatus : ContentCommand + { + public Status Status { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs b/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs new file mode 100644 index 000000000..e74e06d33 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// ContentCommand.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Security.Claims; +using Squidex.Infrastructure.Commands; + +namespace Squidex.Domain.Apps.Entities.Contents.Commands +{ + public abstract class ContentCommand : SchemaCommand, IAggregateCommand + { + public ClaimsPrincipal User { get; set; } + + public Guid ContentId { get; set; } + + Guid IAggregateCommand.AggregateId + { + get { return ContentId; } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentDataCommand.cs b/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentDataCommand.cs new file mode 100644 index 000000000..61cb005ed --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentDataCommand.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// ContentDataCommand.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Contents; + +namespace Squidex.Domain.Apps.Entities.Contents.Commands +{ + public abstract class ContentDataCommand : ContentCommand + { + public NamedContentData Data { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs b/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs new file mode 100644 index 000000000..87b43582b --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs @@ -0,0 +1,15 @@ +// ========================================================================== +// CreateContent.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Contents.Commands +{ + public sealed class CreateContent : ContentDataCommand + { + public bool Publish { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/IAssetEventConsumer.cs b/src/Squidex.Domain.Apps.Entities/Contents/Commands/DeleteContent.cs similarity index 65% rename from src/Squidex.Domain.Apps.Entities/Assets/IAssetEventConsumer.cs rename to src/Squidex.Domain.Apps.Entities/Contents/Commands/DeleteContent.cs index a188fead4..463f68010 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/IAssetEventConsumer.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Commands/DeleteContent.cs @@ -1,16 +1,14 @@ // ========================================================================== -// IAssetEventConsumer.cs +// DeleteContent.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Domain.Apps.Entities.Contents.Commands { - public interface IAssetEventConsumer : IEventConsumer + public sealed class DeleteContent : ContentCommand { } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/PatchContent.cs b/src/Squidex.Domain.Apps.Entities/Contents/Commands/PatchContent.cs new file mode 100644 index 000000000..5f250f517 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/Commands/PatchContent.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// PatchContent.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Contents.Commands +{ + public sealed class PatchContent : ContentDataCommand + { + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpdateContent.cs b/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpdateContent.cs new file mode 100644 index 000000000..3d6df181b --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpdateContent.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// UpdateContent.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Contents.Commands +{ + public sealed class UpdateContent : ContentDataCommand + { + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs new file mode 100644 index 000000000..95d376ec6 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs @@ -0,0 +1,158 @@ +// ========================================================================== +// ContentCommandMiddleware.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Entities.Assets.Repositories; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.Contents.Guards; +using Squidex.Domain.Apps.Entities.Contents.Repositories; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Dispatching; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public class ContentCommandMiddleware : ICommandMiddleware + { + private readonly IAggregateHandler handler; + private readonly IAppProvider appProvider; + private readonly IAssetRepository assetRepository; + private readonly IContentRepository contentRepository; + private readonly IScriptEngine scriptEngine; + + public ContentCommandMiddleware( + IAggregateHandler handler, + IAppProvider appProvider, + IAssetRepository assetRepository, + IScriptEngine scriptEngine, + IContentRepository contentRepository) + { + Guard.NotNull(handler, nameof(handler)); + Guard.NotNull(appProvider, nameof(appProvider)); + Guard.NotNull(scriptEngine, nameof(scriptEngine)); + Guard.NotNull(assetRepository, nameof(assetRepository)); + Guard.NotNull(contentRepository, nameof(contentRepository)); + + this.handler = handler; + this.appProvider = appProvider; + this.scriptEngine = scriptEngine; + this.assetRepository = assetRepository; + this.contentRepository = contentRepository; + } + + protected async Task On(CreateContent command, CommandContext context) + { + await handler.CreateAsync(context, async content => + { + GuardContent.CanCreate(command); + + var operationContext = await CreateContext(command, content, () => "Failed to create content."); + + if (command.Publish) + { + await operationContext.ExecuteScriptAsync(x => x.ScriptChange, "Published"); + } + + await operationContext.ExecuteScriptAndTransformAsync(x => x.ScriptCreate, "Create"); + await operationContext.EnrichAsync(); + await operationContext.ValidateAsync(false); + + content.Create(command); + + context.Complete(EntityCreatedResult.Create(command.Data, content.Version)); + }); + } + + protected async Task On(UpdateContent command, CommandContext context) + { + await handler.UpdateAsync(context, async content => + { + GuardContent.CanUpdate(command); + + var operationContext = await CreateContext(command, content, () => "Failed to update content."); + + await operationContext.ValidateAsync(true); + await operationContext.ExecuteScriptAndTransformAsync(x => x.ScriptUpdate, "Update"); + + content.Update(command); + + context.Complete(new ContentDataChangedResult(content.State.Data, content.Version)); + }); + } + + protected async Task On(PatchContent command, CommandContext context) + { + await handler.UpdateAsync(context, async content => + { + GuardContent.CanPatch(command); + + var operationContext = await CreateContext(command, content, () => "Failed to patch content."); + + await operationContext.ValidateAsync(true); + await operationContext.ExecuteScriptAndTransformAsync(x => x.ScriptUpdate, "Patch"); + + content.Patch(command); + + context.Complete(new ContentDataChangedResult(content.State.Data, content.Version)); + }); + } + + protected Task On(ChangeContentStatus command, CommandContext context) + { + return handler.UpdateAsync(context, async content => + { + GuardContent.CanChangeContentStatus(content.State.Status, command); + + var operationContext = await CreateContext(command, content, () => "Failed to patch content."); + + await operationContext.ExecuteScriptAsync(x => x.ScriptChange, command.Status); + + content.ChangeStatus(command); + }); + } + + protected Task On(DeleteContent command, CommandContext context) + { + return handler.UpdateAsync(context, async content => + { + GuardContent.CanDelete(command); + + var operationContext = await CreateContext(command, content, () => "Failed to delete content."); + + await operationContext.ExecuteScriptAsync(x => x.ScriptDelete, "Delete"); + + content.Delete(command); + }); + } + + public async Task HandleAsync(CommandContext context, Func next) + { + if (!await this.DispatchActionAsync(context.Command, context)) + { + await next(); + } + } + + private async Task CreateContext(ContentCommand command, ContentDomainObject content, Func message) + { + var operationContext = + await ContentOperationContext.CreateAsync( + contentRepository, + content, + command, + appProvider, + assetRepository, + scriptEngine, + message); + + return operationContext; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentDataChangedResult.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentDataChangedResult.cs new file mode 100644 index 000000000..1a8937e10 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentDataChangedResult.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// ContentChangedResult.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure.Commands; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public sealed class ContentDataChangedResult : EntitySavedResult + { + public NamedContentData Data { get; } + + public ContentDataChangedResult(NamedContentData data, long version) + : base(version) + { + Data = data; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs new file mode 100644 index 000000000..6284d11a7 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs @@ -0,0 +1,101 @@ +// ========================================================================== +// ContentDomainObject.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.Contents.State; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public class ContentDomainObject : DomainObjectBase + { + public ContentDomainObject Create(CreateContent command) + { + VerifyNotCreated(); + + RaiseEvent(SimpleMapper.Map(command, new ContentCreated())); + + if (command.Publish) + { + RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged { Status = Status.Published })); + } + + return this; + } + + public ContentDomainObject Delete(DeleteContent command) + { + VerifyCreatedAndNotDeleted(); + + RaiseEvent(SimpleMapper.Map(command, new ContentDeleted())); + + return this; + } + + public ContentDomainObject ChangeStatus(ChangeContentStatus command) + { + VerifyCreatedAndNotDeleted(); + + RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged())); + + return this; + } + + public ContentDomainObject Update(UpdateContent command) + { + VerifyCreatedAndNotDeleted(); + + if (!command.Data.Equals(State.Data)) + { + RaiseEvent(SimpleMapper.Map(command, new ContentUpdated())); + } + + return this; + } + + public ContentDomainObject Patch(PatchContent command) + { + VerifyCreatedAndNotDeleted(); + + var newData = State.Data.MergeInto(command.Data); + + if (!newData.Equals(State.Data)) + { + RaiseEvent(SimpleMapper.Map(command, new ContentUpdated { Data = newData })); + } + + return this; + } + + private void VerifyNotCreated() + { + if (State.Data != null) + { + throw new DomainException("Content has already been created."); + } + } + + private void VerifyCreatedAndNotDeleted() + { + if (State.IsDeleted || State.Data == null) + { + throw new DomainException("Content has already been deleted or not created yet."); + } + } + + protected override void OnRaised(Envelope @event) + { + UpdateState(State.Apply(@event)); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs new file mode 100644 index 000000000..6024c0b47 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs @@ -0,0 +1,139 @@ +// ========================================================================== +// ContentOperationContext.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.EnrichContent; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Core.ValidateContent; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Assets.Repositories; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.Contents.Repositories; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public sealed class ContentOperationContext + { + private ContentDomainObject content; + private ContentCommand command; + private IContentRepository contentRepository; + private IAssetRepository assetRepository; + private IScriptEngine scriptEngine; + private ISchemaEntity schemaEntity; + private IAppEntity appEntity; + private Func message; + + public static async Task CreateAsync( + IContentRepository contentRepository, + ContentDomainObject content, + ContentCommand command, + IAppProvider appProvider, + IAssetRepository assetRepository, + IScriptEngine scriptEngine, + Func message) + { + var (appEntity, schemaEntity) = await appProvider.GetAppWithSchemaAsync(command.AppId.Id, command.SchemaId.Id); + + var context = new ContentOperationContext + { + appEntity = appEntity, + assetRepository = assetRepository, + contentRepository = contentRepository, + content = content, + command = command, + message = message, + schemaEntity = schemaEntity, + scriptEngine = scriptEngine + }; + + return context; + } + + public Task EnrichAsync() + { + if (command is ContentDataCommand dataCommand) + { + dataCommand.Data.Enrich(schemaEntity.SchemaDef, appEntity.PartitionResolver()); + } + + return TaskHelper.Done; + } + + public async Task ValidateAsync(bool partial) + { + if (command is ContentDataCommand dataCommand) + { + var errors = new List(); + + var appId = command.AppId.Id; + + var ctx = + new ValidationContext( + (contentIds, schemaId) => + { + return QueryContentsAsync(appId, schemaId, contentIds); + }, + assetIds => + { + return QueryAssetsAsync(appId, assetIds); + }); + + if (partial) + { + await dataCommand.Data.ValidatePartialAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), errors); + } + else + { + await dataCommand.Data.ValidateAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), errors); + } + + if (errors.Count > 0) + { + throw new ValidationException(message(), errors.ToArray()); + } + } + } + + private async Task> QueryAssetsAsync(Guid appId, IEnumerable assetIds) + { + return await assetRepository.QueryAsync(appId, null, new HashSet(assetIds), null, int.MaxValue, 0); + } + + private async Task> QueryContentsAsync(Guid appId, Guid schemaId, IEnumerable contentIds) + { + return await contentRepository.QueryNotFoundAsync(appId, schemaId, contentIds.ToList()); + } + + public Task ExecuteScriptAndTransformAsync(Func script, object operation) + { + if (command is ContentDataCommand dataCommand) + { + var ctx = new ScriptContext { ContentId = content.State.Id, OldData = content.State.Data, User = command.User, Operation = operation.ToString(), Data = dataCommand.Data }; + + dataCommand.Data = scriptEngine.ExecuteAndTransform(ctx, script(schemaEntity)); + } + + return TaskHelper.Done; + } + + public Task ExecuteScriptAsync(Func script, object operation) + { + var ctx = new ScriptContext { ContentId = content.State.Id, OldData = content.State.Data, User = command.User, Operation = operation.ToString() }; + + scriptEngine.Execute(ctx, script(schemaEntity)); + + return TaskHelper.Done; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs index d04adaa1c..d9144cafb 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs @@ -50,7 +50,7 @@ namespace Squidex.Domain.Apps.Entities.Contents this.modelBuilder = modelBuilder; } - public async Task<(ISchemaEntity Schema, IContentEntity Content)> FindContentAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, Guid id) + public async Task<(ISchemaEntity Schema, IContentEntity Content)> FindContentAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, Guid id, long version = -1) { Guard.NotNull(app, nameof(app)); Guard.NotNull(user, nameof(user)); @@ -60,7 +60,10 @@ namespace Squidex.Domain.Apps.Entities.Contents var schema = await FindSchemaAsync(app, schemaIdOrName); - var content = await contentRepository.FindContentAsync(app, schema, id); + var content = + version > 0 ? + await contentRepository.FindContentAsync(app, schema, id, version) : + await contentRepository.FindContentAsync(app, schema, id); if (content == null || (content.Status != Status.Published && !isFrontendClient)) { @@ -155,12 +158,12 @@ namespace Squidex.Domain.Apps.Entities.Contents if (Guid.TryParse(schemaIdOrName, out var id)) { - schema = await appProvider.GetSchemaAsync(app.Name, id); + schema = await appProvider.GetSchemaAsync(app.Id, id); } if (schema == null) { - schema = await appProvider.GetSchemaAsync(app.Name, schemaIdOrName); + schema = await appProvider.GetSchemaAsync(app.Id, schemaIdOrName); } if (schema == null) diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs index 1a7f208e1..b1647d210 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs @@ -67,7 +67,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL if (modelContext == null) { - var allSchemas = await appProvider.GetSchemasAsync(app.Name); + var allSchemas = await appProvider.GetSchemasAsync(app.Id); modelContext = new GraphQLModel(app, allSchemas.Where(x => x.IsPublished), urlGenerator); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs b/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs new file mode 100644 index 000000000..f93c685bc --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs @@ -0,0 +1,74 @@ +// ========================================================================== +// GuardContent.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Contents.Guards +{ + public static class GuardContent + { + public static void CanCreate(CreateContent command) + { + Guard.NotNull(command, nameof(command)); + + Validate.It(() => "Cannot created content.", error => + { + if (command.Data == null) + { + error(new ValidationError("Data cannot be null.", nameof(command.Data))); + } + }); + } + + public static void CanUpdate(UpdateContent command) + { + Guard.NotNull(command, nameof(command)); + + Validate.It(() => "Cannot update content.", error => + { + if (command.Data == null) + { + error(new ValidationError("Data cannot be null.", nameof(command.Data))); + } + }); + } + + public static void CanPatch(PatchContent command) + { + Guard.NotNull(command, nameof(command)); + + Validate.It(() => "Cannot patch content.", error => + { + if (command.Data == null) + { + error(new ValidationError("Data cannot be null.", nameof(command.Data))); + } + }); + } + + public static void CanChangeContentStatus(Status status, ChangeContentStatus command) + { + Guard.NotNull(command, nameof(command)); + + Validate.It(() => "Cannot change status.", error => + { + if (!StatusFlow.Exists(command.Status) || !StatusFlow.CanChange(status, command.Status)) + { + error(new ValidationError($"Content cannot be changed from status {status} to {command.Status}.", nameof(command.Status))); + } + }); + } + + public static void CanDelete(DeleteContent command) + { + Guard.NotNull(command, nameof(command)); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs b/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs index 99517290d..a9a01d2df 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs @@ -21,7 +21,7 @@ namespace Squidex.Domain.Apps.Entities.Contents Task<(ISchemaEntity Schema, long Total, IReadOnlyList Items)> QueryWithCountAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, bool archived, string query); - Task<(ISchemaEntity Schema, IContentEntity Content)> FindContentAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, Guid id); + Task<(ISchemaEntity Schema, IContentEntity Content)> FindContentAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, Guid id, long version = -1); Task FindSchemaAsync(IAppEntity app, string schemaIdOrName); } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs b/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs index 79de0dbf0..857cc189a 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs @@ -29,5 +29,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Repositories Task CountAsync(IAppEntity app, ISchemaEntity schema, Status[] status, ODataUriParser odataQuery); Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id); + + Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id, long version); } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs b/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs index bee8e3061..356137ea9 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs @@ -6,17 +6,60 @@ // All rights reserved. // ========================================================================== +using System; using Newtonsoft.Json; using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure.Dispatching; +using Squidex.Infrastructure.EventSourcing; namespace Squidex.Domain.Apps.Entities.Contents.State { - public sealed class ContentState : DomainObjectState + public class ContentState : DomainObjectState, IContentEntity { [JsonProperty] - public IdContentData Data { get; set; } + public NamedContentData Data { get; set; } [JsonProperty] - public string Status { get; set; } + public Guid AppId { get; set; } + + [JsonProperty] + public Guid SchemaId { get; set; } + + [JsonProperty] + public Status Status { get; set; } + + [JsonProperty] + public bool IsDeleted { get; set; } + + protected void On(ContentCreated @event) + { + SchemaId = @event.SchemaId.Id; + + Data = @event.Data; + } + + protected void On(ContentUpdated @event) + { + Data = @event.Data; + } + + protected void On(ContentStatusChanged @event) + { + Status = @event.Status; + } + + protected void On(ContentDeleted @event) + { + IsDeleted = true; + } + + public ContentState Apply(Envelope @event) + { + var payload = (SquidexEvent)@event.Payload; + + return Clone().Update(payload, @event.Headers, r => r.DispatchAction(payload)); + } } } diff --git a/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs b/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs index 3775b6244..0a7dade8a 100644 --- a/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs +++ b/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs @@ -14,6 +14,10 @@ using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities { public abstract class DomainObjectState : Cloneable, + IEntity, + IEntityWithCreatedBy, + IEntityWithLastModifiedBy, + IEntityWithVersion, IUpdateableEntity, IUpdateableEntityWithCreatedBy, IUpdateableEntityWithLastModifiedBy, diff --git a/src/Squidex.Domain.Apps.Entities/IAppProvider.cs b/src/Squidex.Domain.Apps.Entities/IAppProvider.cs index 404a62e78..a8bef2d45 100644 --- a/src/Squidex.Domain.Apps.Entities/IAppProvider.cs +++ b/src/Squidex.Domain.Apps.Entities/IAppProvider.cs @@ -17,17 +17,17 @@ namespace Squidex.Domain.Apps.Entities { public interface IAppProvider { - Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(string appName, Guid id); + Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(Guid appId, Guid id); Task GetAppAsync(string appName); - Task GetSchemaAsync(string appName, Guid id, bool provideDeleted = false); + Task GetSchemaAsync(Guid appId, Guid id, bool provideDeleted = false); - Task GetSchemaAsync(string appName, string name, bool provideDeleted = false); + Task GetSchemaAsync(Guid appId, string name, bool provideDeleted = false); - Task> GetSchemasAsync(string appName); + Task> GetSchemasAsync(Guid appId); - Task> GetRulesAsync(string appName); + Task> GetRulesAsync(Guid appId); Task> GetUserApps(string userId); } diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs b/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs index 5a06c7b0c..ed9549606 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs @@ -27,7 +27,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards } else { - var errors = await RuleTriggerValidator.ValidateAsync(command.AppId.Name, command.Trigger, appProvider); + var errors = await RuleTriggerValidator.ValidateAsync(command.AppId.Id, command.Trigger, appProvider); errors.Foreach(error); } @@ -58,7 +58,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards if (command.Trigger != null) { - var errors = await RuleTriggerValidator.ValidateAsync(command.AppId.Name, command.Trigger, appProvider); + var errors = await RuleTriggerValidator.ValidateAsync(command.AppId.Id, command.Trigger, appProvider); errors.Foreach(error); } diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs b/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs index febb77364..faed510c2 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs @@ -26,12 +26,12 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards SchemaProvider = schemaProvider; } - public static Task> ValidateAsync(string appName, RuleTrigger action, IAppProvider appProvider) + public static Task> ValidateAsync(Guid appId, RuleTrigger action, IAppProvider appProvider) { Guard.NotNull(action, nameof(action)); Guard.NotNull(appProvider, nameof(appProvider)); - var visitor = new RuleTriggerValidator(x => appProvider.GetSchemaAsync(appName, x)); + var visitor = new RuleTriggerValidator(x => appProvider.GetSchemaAsync(appId, x)); return action.Accept(visitor); } diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleRepository.cs b/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleRepository.cs index 3a3f309ab..2dbbfe1e6 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleRepository.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleRepository.cs @@ -14,6 +14,6 @@ namespace Squidex.Domain.Apps.Entities.Rules.Repositories { public interface IRuleRepository { - Task> QueryRuleIdsAsync(Guid appId); + Task> QueryRuleIdsAsync(Guid appId); } } diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs index 31caed52d..d3fcbf959 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs @@ -56,7 +56,7 @@ namespace Squidex.Domain.Apps.Entities.Rules { if (@event.Payload is AppEvent appEvent) { - var rules = await appProvider.GetRulesAsync(appEvent.AppId.Name); + var rules = await appProvider.GetRulesAsync(appEvent.AppId.Id); foreach (var ruleEntity in rules) { diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs index 70670d74d..420520df2 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs @@ -28,7 +28,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards error(new ValidationError("Name must be a valid slug.", nameof(command.Name))); } - if (await appProvider.GetSchemaAsync(command.AppId.Name, command.Name) != null) + if (await appProvider.GetSchemaAsync(command.AppId.Id, command.Name) != null) { error(new ValidationError($"A schema with name '{command.Name}' already exists", nameof(command.Name))); } diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Repositories/ISchemaRepository.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Repositories/ISchemaRepository.cs index 04df68b68..642072660 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Repositories/ISchemaRepository.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Repositories/ISchemaRepository.cs @@ -14,8 +14,8 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Repositories { public interface ISchemaRepository { - Task FindSchemaNameAsync(Guid schemaId); + Task FindSchemaIdAsync(Guid appId, string name); - Task> QuerySchemaNamesAsync(Guid appId); + Task> QuerySchemaIdsAsync(Guid appId); } } diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs index 603f41d85..6228da562 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs @@ -6,6 +6,8 @@ // All rights reserved. // ========================================================================== +using System; +using System.Linq.Expressions; using System.Threading.Tasks; using MongoDB.Driver; @@ -31,5 +33,26 @@ namespace Squidex.Infrastructure.MongoDb return true; } + + public static IFindFluent Only(this IFindFluent find, + Expression> include) + { + return find.Project(Builders.Projection.Include(include)); + } + + public static IFindFluent Only(this IFindFluent find, + Expression> include1, + Expression> include2) + { + return find.Project(Builders.Projection.Include(include1).Include(include2)); + } + + public static IFindFluent Only(this IFindFluent find, + Expression> include1, + Expression> include2, + Expression> include3) + { + return find.Project(Builders.Projection.Include(include1).Include(include2).Include(include3)); + } } } diff --git a/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs b/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs index f29c431a0..393c41e6c 100644 --- a/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs +++ b/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs @@ -59,8 +59,8 @@ namespace Squidex.Infrastructure.States if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) { var existingVersion = - await Collection.Find(x => x.Id == key) - .Project>(Projection.Exclude(x => x.Id)).FirstOrDefaultAsync(); + await Collection.Find(x => x.Id == key).Only(x => x.Id, x => x.Version) + .FirstOrDefaultAsync(); if (existingVersion != null) { diff --git a/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageStore.cs b/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageStore.cs index 43acf6f5b..2764342be 100644 --- a/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageStore.cs +++ b/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageStore.cs @@ -17,8 +17,6 @@ namespace Squidex.Infrastructure.UsageTracking { public sealed class MongoUsageStore : MongoRepositoryBase, IUsageStore { - private static readonly UpdateOptions Upsert = new UpdateOptions { IsUpsert = true }; - public MongoUsageStore(IMongoDatabase database) : base(database) { diff --git a/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs b/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs index 62ba1fdcc..d3cee9702 100644 --- a/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs +++ b/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs @@ -18,18 +18,17 @@ namespace Squidex.Infrastructure.Commands public abstract class DomainObjectBase : IDomainObject where TState : new() { private readonly List> uncomittedEvents = new List>(); - private int version = -1; private TState state = new TState(); private IPersistence persistence; - public TState State + public long Version { - get { return state; } + get { return persistence.Version; } } - public int Version + public TState State { - get { return version; } + get { return state; } } public IReadOnlyList> GetUncomittedEvents() diff --git a/src/Squidex.Infrastructure/Commands/IDomainObject.cs b/src/Squidex.Infrastructure/Commands/IDomainObject.cs index 971382c81..f1df5c41e 100644 --- a/src/Squidex.Infrastructure/Commands/IDomainObject.cs +++ b/src/Squidex.Infrastructure/Commands/IDomainObject.cs @@ -14,7 +14,7 @@ namespace Squidex.Infrastructure.Commands { public interface IDomainObject : IStatefulObject { - int Version { get; } + long Version { get; } Task WriteAsync(ISemanticLog log); } diff --git a/src/Squidex.Infrastructure/States/IPersistence.cs b/src/Squidex.Infrastructure/States/IPersistence.cs index 4a36c48bf..51b17312c 100644 --- a/src/Squidex.Infrastructure/States/IPersistence.cs +++ b/src/Squidex.Infrastructure/States/IPersistence.cs @@ -13,6 +13,8 @@ namespace Squidex.Infrastructure.States { public interface IPersistence { + long Version { get; } + Task WriteEventsAsync(params Envelope[] @events); Task WriteSnapshotAsync(TState state); diff --git a/src/Squidex.Infrastructure/States/Persistence.cs b/src/Squidex.Infrastructure/States/Persistence.cs index d127964ea..5be140a1c 100644 --- a/src/Squidex.Infrastructure/States/Persistence.cs +++ b/src/Squidex.Infrastructure/States/Persistence.cs @@ -26,6 +26,11 @@ namespace Squidex.Infrastructure.States private long positionSnapshot = -1; private long positionEvent = -1; + public long Version + { + get { return Math.Max(positionEvent, positionSnapshot); } + } + public Persistence(string ownerKey, Action invalidate, IEventStore eventStore, @@ -85,17 +90,17 @@ namespace Squidex.Infrastructure.States } } - var maxVersion = Math.Max(positionEvent, positionSnapshot); + var newVersion = Version; - if (expectedVersion.HasValue && expectedVersion.Value != maxVersion) + if (expectedVersion.HasValue && expectedVersion.Value != newVersion) { - if (maxVersion == -1) + if (newVersion == -1) { throw new DomainObjectNotFoundException(ownerKey, typeof(TOwner)); } else { - throw new DomainObjectVersionException(ownerKey, typeof(TOwner), maxVersion, expectedVersion.Value); + throw new DomainObjectVersionException(ownerKey, typeof(TOwner), newVersion, expectedVersion.Value); } } } diff --git a/src/Squidex/Areas/Api/Controllers/ApiController.cs b/src/Squidex/Areas/Api/Controllers/ApiController.cs index fafbca6e0..66d163d43 100644 --- a/src/Squidex/Areas/Api/Controllers/ApiController.cs +++ b/src/Squidex/Areas/Api/Controllers/ApiController.cs @@ -9,7 +9,7 @@ using System; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; -using Squidex.Domain.Apps.Read.Apps; +using Squidex.Domain.Apps.Entities.Apps; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Pipeline; @@ -41,6 +41,11 @@ namespace Squidex.Areas.Api.Controllers get { return App.Name; } } + protected Guid AppId + { + get { return App.Id; } + } + protected ApiController(ICommandBus commandBus) { Guard.NotNull(commandBus, nameof(commandBus)); diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs index 0c2ab7276..6f0130bc5 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs @@ -12,7 +12,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Primitives; using NSwag.Annotations; using Squidex.Areas.Api.Controllers.Apps.Models; -using Squidex.Domain.Apps.Write.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Reflection; using Squidex.Pipeline; diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs index c6b6fc22c..3a2faddc5 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs @@ -12,8 +12,8 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Primitives; using NSwag.Annotations; using Squidex.Areas.Api.Controllers.Apps.Models; -using Squidex.Domain.Apps.Read.Apps.Services; -using Squidex.Domain.Apps.Write.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Services; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Reflection; using Squidex.Pipeline; diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs index 1c499ff2e..4d055de27 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs @@ -15,7 +15,7 @@ using Microsoft.Extensions.Primitives; using NSwag.Annotations; using Squidex.Areas.Api.Controllers.Apps.Models; using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Write.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Reflection; diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs index 71aa34709..78cd235c1 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs @@ -13,9 +13,9 @@ using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; using Squidex.Areas.Api.Controllers.Apps.Models; using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Read; -using Squidex.Domain.Apps.Read.Apps.Services; -using Squidex.Domain.Apps.Write.Apps.Commands; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Services; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Security; diff --git a/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs b/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs index 2d5e3b28e..330209f41 100644 --- a/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs @@ -11,7 +11,7 @@ using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; -using Squidex.Domain.Apps.Read.Assets.Repositories; +using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Commands; using Squidex.Pipeline; diff --git a/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs b/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs index 7f7313c40..d65cf73f7 100644 --- a/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs @@ -16,10 +16,10 @@ using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using NSwag.Annotations; using Squidex.Areas.Api.Controllers.Assets.Models; -using Squidex.Domain.Apps.Read.Apps.Services; -using Squidex.Domain.Apps.Read.Assets.Repositories; -using Squidex.Domain.Apps.Write.Assets; -using Squidex.Domain.Apps.Write.Assets.Commands; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Infrastructure; using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Commands; diff --git a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetCreatedDto.cs b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetCreatedDto.cs index b2db2f78a..6becf0403 100644 --- a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetCreatedDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetCreatedDto.cs @@ -8,7 +8,7 @@ using System; using System.ComponentModel.DataAnnotations; -using Squidex.Domain.Apps.Write.Assets.Commands; +using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Infrastructure.Commands; namespace Squidex.Areas.Api.Controllers.Assets.Models diff --git a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetReplacedDto.cs b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetReplacedDto.cs index a324b9ceb..c038d4f48 100644 --- a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetReplacedDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetReplacedDto.cs @@ -7,8 +7,8 @@ // ========================================================================== using System.ComponentModel.DataAnnotations; -using Squidex.Domain.Apps.Write.Assets; -using Squidex.Domain.Apps.Write.Assets.Commands; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Assets.Commands; namespace Squidex.Areas.Api.Controllers.Assets.Models { diff --git a/src/Squidex/Areas/Api/Controllers/Content/ContentSwaggerController.cs b/src/Squidex/Areas/Api/Controllers/Content/ContentSwaggerController.cs index 84a0afd57..a1f54b271 100644 --- a/src/Squidex/Areas/Api/Controllers/Content/ContentSwaggerController.cs +++ b/src/Squidex/Areas/Api/Controllers/Content/ContentSwaggerController.cs @@ -10,7 +10,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; using Squidex.Areas.Api.Controllers.Contents.Generator; -using Squidex.Domain.Apps.Read; +using Squidex.Domain.Apps.Entities; using Squidex.Infrastructure.Commands; using Squidex.Pipeline; @@ -47,7 +47,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [ApiCosts(0)] public async Task GetSwagger(string app) { - var schemas = await appProvider.GetSchemasAsync(AppName); + var schemas = await appProvider.GetSchemasAsync(AppId); var swaggerDocument = await schemasSwaggerGenerator.Generate(App, schemas); diff --git a/src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs b/src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs index 0f8612fe5..c01ee14ec 100644 --- a/src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs @@ -16,10 +16,9 @@ using NSwag.Annotations; using Squidex.Areas.Api.Controllers.Contents.Models; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.ConvertContent; -using Squidex.Domain.Apps.Read.Contents; -using Squidex.Domain.Apps.Read.Contents.GraphQL; -using Squidex.Domain.Apps.Write.Contents; -using Squidex.Domain.Apps.Write.Contents.Commands; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.Contents.GraphQL; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Reflection; using Squidex.Pipeline; @@ -33,17 +32,14 @@ namespace Squidex.Areas.Api.Controllers.Contents public sealed class ContentsController : ApiController { private readonly IContentQueryService contentQuery; - private readonly IContentVersionLoader contentVersionLoader; private readonly IGraphQLService graphQl; public ContentsController(ICommandBus commandBus, IContentQueryService contentQuery, - IContentVersionLoader contentVersionLoader, IGraphQLService graphQl) : base(commandBus) { this.contentQuery = contentQuery; - this.contentVersionLoader = contentVersionLoader; this.graphQl = graphQl; } @@ -142,9 +138,16 @@ namespace Squidex.Areas.Api.Controllers.Contents [ApiCosts(1)] public async Task GetContentVersion(string name, Guid id, int version) { - var contentData = await contentVersionLoader.LoadAsync(App.Id, id, version); + var content = await contentQuery.FindContentAsync(App, name, User, id, version); - var response = contentData; + var response = SimpleMapper.Map(content.Content, new ContentDto()); + + if (content.Content.Data != null) + { + var isFrontendClient = User.IsFrontendClient(); + + response.Data = content.Content.Data.ToApiModel(content.Schema.SchemaDef, App.LanguagesConfig, !isFrontendClient); + } Response.Headers["ETag"] = new StringValues(version.ToString()); diff --git a/src/Squidex/Areas/Api/Controllers/Content/Generator/SchemasSwaggerGenerator.cs b/src/Squidex/Areas/Api/Controllers/Content/Generator/SchemasSwaggerGenerator.cs index 82295c3a5..c895641f1 100644 --- a/src/Squidex/Areas/Api/Controllers/Content/Generator/SchemasSwaggerGenerator.cs +++ b/src/Squidex/Areas/Api/Controllers/Content/Generator/SchemasSwaggerGenerator.cs @@ -16,8 +16,8 @@ using NSwag; using NSwag.AspNetCore; using NSwag.SwaggerGeneration; using Squidex.Config; -using Squidex.Domain.Apps.Read.Apps; -using Squidex.Domain.Apps.Read.Schemas; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; using Squidex.Pipeline.Swagger; diff --git a/src/Squidex/Areas/Api/Controllers/Content/Models/ContentDto.cs b/src/Squidex/Areas/Api/Controllers/Content/Models/ContentDto.cs index 2a048594b..62471547c 100644 --- a/src/Squidex/Areas/Api/Controllers/Content/Models/ContentDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Content/Models/ContentDto.cs @@ -10,7 +10,7 @@ using System; using System.ComponentModel.DataAnnotations; using NodaTime; using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Write.Contents.Commands; +using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; diff --git a/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs b/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs index d0e224275..a4570cf22 100644 --- a/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs +++ b/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs @@ -11,7 +11,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; using Squidex.Areas.Api.Controllers.History.Models; -using Squidex.Domain.Apps.Read.History.Repositories; +using Squidex.Domain.Apps.Entities.History.Repositories; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Reflection; using Squidex.Pipeline; diff --git a/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs b/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs index 6481585a7..e7a42a3fc 100644 --- a/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs +++ b/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs @@ -12,8 +12,8 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Primitives; using NSwag.Annotations; using Squidex.Areas.Api.Controllers.Plans.Models; -using Squidex.Domain.Apps.Read.Apps.Services; -using Squidex.Domain.Apps.Write.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Services; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Reflection; using Squidex.Pipeline; @@ -62,7 +62,7 @@ namespace Squidex.Areas.Api.Controllers.Plans { CurrentPlanId = planId, Plans = appPlansProvider.GetAvailablePlans().Select(x => SimpleMapper.Map(x, new PlanDto())).ToList(), - PlanOwner = App.PlanOwner, + PlanOwner = App.Plan?.Owner.Identifier, HasPortal = appPlansBillingManager.HasPortal }; diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleConverter.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleConverter.cs index 3cf4b64ed..072ce5141 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleConverter.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleConverter.cs @@ -7,8 +7,8 @@ // ========================================================================== using System; -using Squidex.Domain.Apps.Read.Rules; -using Squidex.Domain.Apps.Write.Rules.Commands; +using Squidex.Domain.Apps.Entities.Rules; +using Squidex.Domain.Apps.Entities.Rules.Commands; using Squidex.Infrastructure.Reflection; namespace Squidex.Areas.Api.Controllers.Rules.Models.Converters diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs index 1a09e88be..7a973ee52 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs @@ -10,7 +10,7 @@ using System; using System.ComponentModel.DataAnnotations; using NodaTime; using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Read.Rules; +using Squidex.Domain.Apps.Entities.Rules; namespace Squidex.Areas.Api.Controllers.Rules.Models { diff --git a/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs b/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs index 5d0e34c75..481e4f309 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs @@ -14,9 +14,9 @@ using NodaTime; using NSwag.Annotations; using Squidex.Areas.Api.Controllers.Rules.Models; using Squidex.Areas.Api.Controllers.Rules.Models.Converters; -using Squidex.Domain.Apps.Read; -using Squidex.Domain.Apps.Read.Rules.Repositories; -using Squidex.Domain.Apps.Write.Rules.Commands; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Rules.Commands; +using Squidex.Domain.Apps.Entities.Rules.Repositories; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Reflection; using Squidex.Pipeline; @@ -59,7 +59,7 @@ namespace Squidex.Areas.Api.Controllers.Rules [ApiCosts(1)] public async Task GetRules(string app) { - var rules = await appProvider.GetRulesAsync(AppName); + var rules = await appProvider.GetRulesAsync(AppId); var response = rules.Select(r => r.ToModel()); diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/SchemaConverter.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/SchemaConverter.cs index 7233f65bc..dc7ba4c68 100644 --- a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/SchemaConverter.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/SchemaConverter.cs @@ -8,8 +8,8 @@ using System.Collections.Generic; using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Read.Schemas; -using Squidex.Domain.Apps.Write.Schemas.Commands; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Infrastructure.Reflection; namespace Squidex.Areas.Api.Controllers.Schemas.Models.Converters diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs b/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs index 7fbda49ad..40c40429e 100644 --- a/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs @@ -10,7 +10,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; using Squidex.Areas.Api.Controllers.Schemas.Models; -using Squidex.Domain.Apps.Write.Schemas.Commands; +using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Infrastructure.Commands; using Squidex.Pipeline; diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs b/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs index 2e5b8e00f..26ba58ac0 100644 --- a/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs @@ -15,9 +15,9 @@ using NSwag.Annotations; using Squidex.Areas.Api.Controllers.Schemas.Models; using Squidex.Areas.Api.Controllers.Schemas.Models.Converters; using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Read; -using Squidex.Domain.Apps.Read.Schemas; -using Squidex.Domain.Apps.Write.Schemas.Commands; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Reflection; using Squidex.Pipeline; @@ -56,7 +56,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas [ApiCosts(0)] public async Task GetSchemas(string app) { - var schemas = await appProvider.GetSchemasAsync(AppName); + var schemas = await appProvider.GetSchemasAsync(AppId); var response = schemas.Select(s => s.ToModel()).ToList(); @@ -83,11 +83,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas if (Guid.TryParse(name, out var id)) { - entity = await appProvider.GetSchemaAsync(AppName, id); + entity = await appProvider.GetSchemaAsync(AppId, id); } else { - entity = await appProvider.GetSchemaAsync(AppName, name); + entity = await appProvider.GetSchemaAsync(AppId, name); } if (entity == null || entity.IsDeleted) diff --git a/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs b/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs index d9895c4b1..d12f17656 100644 --- a/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs +++ b/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs @@ -12,8 +12,8 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; using Squidex.Areas.Api.Controllers.Statistics.Models; -using Squidex.Domain.Apps.Read.Apps.Services; -using Squidex.Domain.Apps.Read.Assets.Repositories; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.UsageTracking; using Squidex.Pipeline; diff --git a/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs b/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs index 4d86a172b..d384db228 100644 --- a/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs +++ b/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs @@ -15,7 +15,7 @@ using IdentityServer4.Stores; using Microsoft.Extensions.Options; using Squidex.Config; using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Read; +using Squidex.Domain.Apps.Entities; using Squidex.Infrastructure; namespace Squidex.Areas.IdentityServer.Config diff --git a/src/Squidex/Areas/Portal/Middlewares/PortalRedirectMiddleware.cs b/src/Squidex/Areas/Portal/Middlewares/PortalRedirectMiddleware.cs index 603f7bb94..a17190981 100644 --- a/src/Squidex/Areas/Portal/Middlewares/PortalRedirectMiddleware.cs +++ b/src/Squidex/Areas/Portal/Middlewares/PortalRedirectMiddleware.cs @@ -8,7 +8,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Squidex.Domain.Apps.Read.Apps.Services; +using Squidex.Domain.Apps.Entities.Apps.Services; using Squidex.Infrastructure.Security; namespace Squidex.Areas.Portal.Middlewares diff --git a/src/Squidex/Config/Domain/ReadServices.cs b/src/Squidex/Config/Domain/ReadServices.cs index 13f0ff490..d11421a78 100644 --- a/src/Squidex/Config/Domain/ReadServices.cs +++ b/src/Squidex/Config/Domain/ReadServices.cs @@ -13,23 +13,20 @@ using Microsoft.Extensions.Options; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules.Actions; using Squidex.Domain.Apps.Core.HandleRules.Triggers; -using Squidex.Domain.Apps.Read; -using Squidex.Domain.Apps.Read.Apps; -using Squidex.Domain.Apps.Read.Apps.Services; -using Squidex.Domain.Apps.Read.Apps.Services.Implementations; -using Squidex.Domain.Apps.Read.Assets; -using Squidex.Domain.Apps.Read.Contents; -using Squidex.Domain.Apps.Read.Contents.Edm; -using Squidex.Domain.Apps.Read.Contents.GraphQL; -using Squidex.Domain.Apps.Read.History; -using Squidex.Domain.Apps.Read.Rules; -using Squidex.Domain.Apps.Read.Schemas; -using Squidex.Domain.Apps.Read.State; -using Squidex.Domain.Apps.Read.State.Grains; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Domain.Apps.Entities.Apps.Services.Implementations; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Contents.Edm; +using Squidex.Domain.Apps.Entities.Contents.GraphQL; +using Squidex.Domain.Apps.Entities.History; +using Squidex.Domain.Apps.Entities.Rules; +using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Users; using Squidex.Infrastructure; using Squidex.Infrastructure.Assets; -using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing.Grains; using Squidex.Infrastructure.States; @@ -103,15 +100,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); - services.AddSingletonAs() - .As(); - services.AddSingletonAs() .As(); - services.AddSingletonAs(c => - new CompoundEventConsumer(c.GetServices().ToArray())); - services.AddSingletonAs(c => { var allEventConsumers = c.GetServices(); @@ -120,11 +111,6 @@ namespace Squidex.Config.Domain }); services.AddSingletonAs(); - - services.AddTransient(typeof(DomainObjectWrapper<>)); - services.AddTransient(); - services.AddTransient(); - services.AddSingleton(); } } diff --git a/src/Squidex/Config/Domain/StoreServices.cs b/src/Squidex/Config/Domain/StoreServices.cs index 5c76a96e1..a66dd3078 100644 --- a/src/Squidex/Config/Domain/StoreServices.cs +++ b/src/Squidex/Config/Domain/StoreServices.cs @@ -13,22 +13,30 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using MongoDB.Driver; using Newtonsoft.Json; -using Squidex.Domain.Apps.Read; -using Squidex.Domain.Apps.Read.Assets; -using Squidex.Domain.Apps.Read.Assets.Repositories; -using Squidex.Domain.Apps.Read.Contents.Repositories; -using Squidex.Domain.Apps.Read.History; -using Squidex.Domain.Apps.Read.History.Repositories; -using Squidex.Domain.Apps.Read.MongoDb.Assets; -using Squidex.Domain.Apps.Read.MongoDb.Contents; -using Squidex.Domain.Apps.Read.MongoDb.History; -using Squidex.Domain.Apps.Read.MongoDb.Rules; -using Squidex.Domain.Apps.Read.Rules.Repositories; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Apps.Repositories; +using Squidex.Domain.Apps.Entities.Apps.State; +using Squidex.Domain.Apps.Entities.Assets.Repositories; +using Squidex.Domain.Apps.Entities.Assets.State; +using Squidex.Domain.Apps.Entities.Contents.Repositories; +using Squidex.Domain.Apps.Entities.Contents.State; +using Squidex.Domain.Apps.Entities.History; +using Squidex.Domain.Apps.Entities.History.Repositories; +using Squidex.Domain.Apps.Entities.MongoDb.Apps; +using Squidex.Domain.Apps.Entities.MongoDb.Assets; +using Squidex.Domain.Apps.Entities.MongoDb.Contents; +using Squidex.Domain.Apps.Entities.MongoDb.History; +using Squidex.Domain.Apps.Entities.MongoDb.Rules; +using Squidex.Domain.Apps.Entities.MongoDb.Schemas; +using Squidex.Domain.Apps.Entities.Rules.Repositories; +using Squidex.Domain.Apps.Entities.Rules.State; +using Squidex.Domain.Apps.Entities.Schemas.Repositories; using Squidex.Domain.Users; using Squidex.Domain.Users.MongoDb; using Squidex.Domain.Users.MongoDb.Infrastructure; using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.EventSourcing.Grains; using Squidex.Infrastructure.States; using Squidex.Infrastructure.UsageTracking; using Squidex.Shared.Users; @@ -55,8 +63,8 @@ namespace Squidex.Config.Domain .As() .As(); - services.AddSingletonAs(c => new MongoSnapshotStore(mongoDatabase, c.GetRequiredService())) - .As() + services.AddSingletonAs(c => new MongoSnapshotStore(mongoDatabase, c.GetRequiredService())) + .As>() .As(); services.AddSingletonAs(c => new MongoUserStore(mongoDatabase)) @@ -78,12 +86,33 @@ namespace Squidex.Config.Domain .As() .As(); + services.AddSingletonAs(c => new MongoRuleEventRepository(mongoDatabase)) + .As() + .As(); + + services.AddSingletonAs(c => new MongoAppRepository(mongoDatabase)) + .As() + .As>() + .As(); + + services.AddSingletonAs(c => new MongoAssetRepository(mongoDatabase)) + .As() + .As>() + .As(); + services.AddSingletonAs(c => new MongoContentRepository(mongoContentDatabase, c.GetService())) .As() + .As>() .As(); - services.AddSingletonAs(c => new MongoRuleEventRepository(mongoDatabase)) - .As() + services.AddSingletonAs(c => new MongoRuleRepository(mongoContentDatabase)) + .As() + .As>() + .As(); + + services.AddSingletonAs(c => new MongoSchemaRepository(mongoDatabase)) + .As() + .As>() .As(); services.AddSingletonAs(c => new MongoHistoryEventRepository(mongoDatabase, c.GetServices())) @@ -91,14 +120,9 @@ namespace Squidex.Config.Domain .As() .As(); - services.AddSingletonAs(c => new MongoAssetRepository(mongoDatabase)) - .As() - .As() - .As(); - services.AddSingletonAs(c => new MongoAssetStatsRepository(mongoDatabase)) .As() - .As() + .As() .As(); } }); diff --git a/src/Squidex/Config/Domain/WriteServices.cs b/src/Squidex/Config/Domain/WriteServices.cs index b0b7e070d..56dba6359 100644 --- a/src/Squidex/Config/Domain/WriteServices.cs +++ b/src/Squidex/Config/Domain/WriteServices.cs @@ -7,13 +7,13 @@ // ========================================================================== using Microsoft.Extensions.DependencyInjection; -using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Write.Apps; -using Squidex.Domain.Apps.Write.Assets; -using Squidex.Domain.Apps.Write.Contents; -using Squidex.Domain.Apps.Write.Rules; -using Squidex.Domain.Apps.Write.Schemas; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Rules; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.State.SchemaDefs; using Squidex.Domain.Users; using Squidex.Infrastructure.Commands; using Squidex.Pipeline.CommandMiddlewares; @@ -30,9 +30,6 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); - services.AddSingletonAs() - .As(); - services.AddSingletonAs() .As(); @@ -63,17 +60,11 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); - services.AddSingletonAs>(c => (id => new AppDomainObject(id, -1))); - services.AddSingletonAs>(c => (id => new RuleDomainObject(id, -1))); - services.AddSingletonAs>(c => (id => new AssetDomainObject(id, -1))); - services.AddSingletonAs>(c => (id => new ContentDomainObject(id, -1))); - - services.AddSingletonAs>(c => - { - var fieldRegistry = c.GetRequiredService(); - - return id => new SchemaDomainObject(id, -1, fieldRegistry); - }); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); } } } diff --git a/src/Squidex/Config/MyUsageOptions.cs b/src/Squidex/Config/MyUsageOptions.cs index 370c4a90b..53c558bad 100644 --- a/src/Squidex/Config/MyUsageOptions.cs +++ b/src/Squidex/Config/MyUsageOptions.cs @@ -6,7 +6,7 @@ // All rights reserved. // ========================================================================== -using Squidex.Domain.Apps.Read.Apps.Services.Implementations; +using Squidex.Domain.Apps.Entities.Apps.Services.Implementations; namespace Squidex.Config { diff --git a/src/Squidex/Pipeline/ApiCostsFilter.cs b/src/Squidex/Pipeline/ApiCostsFilter.cs index 358c1a6f3..557fc9647 100644 --- a/src/Squidex/Pipeline/ApiCostsFilter.cs +++ b/src/Squidex/Pipeline/ApiCostsFilter.cs @@ -11,7 +11,7 @@ using System.Diagnostics; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; -using Squidex.Domain.Apps.Read.Apps.Services; +using Squidex.Domain.Apps.Entities.Apps.Services; using Squidex.Infrastructure.UsageTracking; namespace Squidex.Pipeline diff --git a/src/Squidex/Pipeline/AppApiFilter.cs b/src/Squidex/Pipeline/AppApiFilter.cs index a789893c7..88fa4555a 100644 --- a/src/Squidex/Pipeline/AppApiFilter.cs +++ b/src/Squidex/Pipeline/AppApiFilter.cs @@ -9,8 +9,8 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; -using Squidex.Domain.Apps.Read; -using Squidex.Domain.Apps.Read.Apps; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Apps; namespace Squidex.Pipeline { diff --git a/src/Squidex/Pipeline/AppPermissionAttribute.cs b/src/Squidex/Pipeline/AppPermissionAttribute.cs index 82517dd38..d6405f89a 100644 --- a/src/Squidex/Pipeline/AppPermissionAttribute.cs +++ b/src/Squidex/Pipeline/AppPermissionAttribute.cs @@ -12,7 +12,7 @@ using System.Security.Claims; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Read.Apps; +using Squidex.Domain.Apps.Entities.Apps; using Squidex.Infrastructure.Security; using Squidex.Shared.Identity; diff --git a/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs b/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs index 07a24847a..1cda63c8f 100644 --- a/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs +++ b/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs @@ -10,7 +10,7 @@ using System; using System.Security; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Squidex.Domain.Apps.Write; +using Squidex.Domain.Apps.Entities; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Security; diff --git a/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs b/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs index d5a277039..856af45fe 100644 --- a/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs +++ b/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs @@ -9,7 +9,7 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Squidex.Domain.Apps.Write; +using Squidex.Domain.Apps.Entities; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; diff --git a/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs b/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs index 596ea587f..b1b0df833 100644 --- a/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs +++ b/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs @@ -9,10 +9,8 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Infrastructure; -using Squidex.Domain.Apps.Read; -using Squidex.Domain.Apps.Read.Schemas; -using Squidex.Domain.Apps.Write; -using Squidex.Domain.Apps.Write.Schemas; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; @@ -44,11 +42,11 @@ namespace Squidex.Pipeline.CommandMiddlewares if (Guid.TryParse(schemaName, out var id)) { - schema = await appProvider.GetSchemaAsync(schemaCommand.AppId.Name, id); + schema = await appProvider.GetSchemaAsync(schemaCommand.AppId.Id, id); } else { - schema = await appProvider.GetSchemaAsync(schemaCommand.AppId.Name, schemaName); + schema = await appProvider.GetSchemaAsync(schemaCommand.AppId.Id, schemaName); } if (schema == null) diff --git a/src/Squidex/Pipeline/GraphQLUrlGenerator.cs b/src/Squidex/Pipeline/GraphQLUrlGenerator.cs index e6d06026e..5f6b783d3 100644 --- a/src/Squidex/Pipeline/GraphQLUrlGenerator.cs +++ b/src/Squidex/Pipeline/GraphQLUrlGenerator.cs @@ -8,11 +8,11 @@ using Microsoft.Extensions.Options; using Squidex.Config; -using Squidex.Domain.Apps.Read.Apps; -using Squidex.Domain.Apps.Read.Assets; -using Squidex.Domain.Apps.Read.Contents; -using Squidex.Domain.Apps.Read.Contents.GraphQL; -using Squidex.Domain.Apps.Read.Schemas; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Contents.GraphQL; +using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure.Assets; namespace Squidex.Pipeline diff --git a/src/Squidex/Pipeline/IAppFeature.cs b/src/Squidex/Pipeline/IAppFeature.cs index d85882707..fa2ced5e2 100644 --- a/src/Squidex/Pipeline/IAppFeature.cs +++ b/src/Squidex/Pipeline/IAppFeature.cs @@ -6,7 +6,7 @@ // All rights reserved. // ========================================================================== -using Squidex.Domain.Apps.Read.Apps; +using Squidex.Domain.Apps.Entities.Apps; namespace Squidex.Pipeline { diff --git a/src/Squidex/Squidex.csproj b/src/Squidex/Squidex.csproj index ebc86a383..bd330a626 100644 --- a/src/Squidex/Squidex.csproj +++ b/src/Squidex/Squidex.csproj @@ -31,9 +31,11 @@ + + - + @@ -41,9 +43,6 @@ - - - diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs new file mode 100644 index 000000000..b94d97994 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs @@ -0,0 +1,244 @@ +// ========================================================================== +// ContentCommandMiddlewareTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Assets.Repositories; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.Contents.Repositories; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public class ContentCommandMiddlewareTests : HandlerTestBase + { + private readonly ISchemaEntity schema = A.Fake(); + private readonly IScriptEngine scriptEngine = A.Fake(); + private readonly IAppProvider appProvider = A.Fake(); + private readonly IAppEntity app = A.Fake(); + private readonly ClaimsPrincipal user = new ClaimsPrincipal(); + private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.DE); + private readonly Guid contentId = Guid.NewGuid(); + private readonly ContentDomainObject content = new ContentDomainObject(); + private readonly ContentCommandMiddleware sut; + + private readonly NamedContentData invalidData = + new NamedContentData() + .AddField("my-field1", new ContentFieldData() + .AddValue(null)) + .AddField("my-field2", new ContentFieldData() + .AddValue(1)); + private readonly NamedContentData data = + new NamedContentData() + .AddField("my-field1", new ContentFieldData() + .AddValue(1)) + .AddField("my-field2", new ContentFieldData() + .AddValue(1)); + private readonly NamedContentData patch = + new NamedContentData() + .AddField("my-field1", new ContentFieldData() + .AddValue(1)); + + public ContentCommandMiddlewareTests() + { + var schemaDef = + new Schema("my-schema") + .AddField(new NumberField(1, "my-field1", Partitioning.Invariant, + new NumberFieldProperties { IsRequired = true })) + .AddField(new NumberField(2, "my-field2", Partitioning.Invariant, + new NumberFieldProperties { IsRequired = false })); + + sut = new ContentCommandMiddleware(Handler, appProvider, A.Dummy(), scriptEngine, A.Dummy()); + + A.CallTo(() => app.LanguagesConfig).Returns(languagesConfig); + + A.CallTo(() => appProvider.GetAppAsync(AppName)).Returns(app); + + A.CallTo(() => schema.SchemaDef).Returns(schemaDef); + A.CallTo(() => schema.ScriptCreate).Returns(""); + A.CallTo(() => schema.ScriptChange).Returns(""); + A.CallTo(() => schema.ScriptUpdate).Returns(""); + A.CallTo(() => schema.ScriptDelete).Returns(""); + + A.CallTo(() => appProvider.GetAppWithSchemaAsync(AppId, SchemaId)).Returns((app, schema)); + } + + [Fact] + public async Task Create_should_throw_exception_if_data_is_not_valid() + { + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) + .Returns(invalidData); + + var context = CreateContextForCommand(new CreateContent { ContentId = contentId, Data = invalidData, User = user }); + + await TestCreate(content, async _ => + { + await Assert.ThrowsAsync(() => sut.HandleAsync(context)); + }, false); + } + + [Fact] + public async Task Create_should_create_content() + { + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) + .Returns(data); + + var context = CreateContextForCommand(new CreateContent { ContentId = contentId, Data = data, User = user }); + + await TestCreate(content, async _ => + { + await sut.HandleAsync(context); + }); + + Assert.Equal(data, context.Result>().IdOrValue); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, "")).MustHaveHappened(); + A.CallTo(() => scriptEngine.Execute(A.Ignored, "")).MustNotHaveHappened(); + } + + [Fact] + public async Task Create_should_also_invoke_publish_script_when_publishing() + { + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) + .Returns(data); + + var context = CreateContextForCommand(new CreateContent { ContentId = contentId, Data = data, User = user, Publish = true }); + + await TestCreate(content, async _ => + { + await sut.HandleAsync(context); + }); + + Assert.Equal(data, context.Result>().IdOrValue); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, "")).MustHaveHappened(); + A.CallTo(() => scriptEngine.Execute(A.Ignored, "")).MustHaveHappened(); + } + + [Fact] + public async Task Update_should_throw_exception_if_data_is_not_valid() + { + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) + .Returns(invalidData); + + CreateContent(); + + var context = CreateContextForCommand(new UpdateContent { ContentId = contentId, Data = invalidData, User = user }); + + await TestUpdate(content, async _ => + { + await Assert.ThrowsAsync(() => sut.HandleAsync(context)); + }, false); + } + + [Fact] + public async Task Update_should_update_domain_object() + { + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) + .Returns(data); + + CreateContent(); + + var context = CreateContextForCommand(new UpdateContent { ContentId = contentId, Data = data, User = user }); + + await TestUpdate(content, async _ => + { + await sut.HandleAsync(context); + }); + + Assert.Equal(data, context.Result().Data); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, "")).MustHaveHappened(); + } + + [Fact] + public async Task Patch_should_throw_exception_if_data_is_not_valid() + { + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) + .Returns(invalidData); + + CreateContent(); + + var context = CreateContextForCommand(new PatchContent { ContentId = contentId, Data = invalidData, User = user }); + + await TestUpdate(content, async _ => + { + await Assert.ThrowsAsync(() => sut.HandleAsync(context)); + }, false); + } + + [Fact] + public async Task Patch_should_update_domain_object() + { + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) + .Returns(data); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)).Returns(patch); + + CreateContent(); + + var context = CreateContextForCommand(new PatchContent { ContentId = contentId, Data = patch, User = user }); + + await TestUpdate(content, async _ => + { + await sut.HandleAsync(context); + }); + + Assert.NotNull(context.Result().Data); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, "")).MustHaveHappened(); + } + + [Fact] + public async Task ChangeStatus_should_publish_domain_object() + { + CreateContent(); + + var context = CreateContextForCommand(new ChangeContentStatus { ContentId = contentId, User = user, Status = Status.Published }); + + await TestUpdate(content, async _ => + { + await sut.HandleAsync(context); + }); + + A.CallTo(() => scriptEngine.Execute(A.Ignored, "")).MustHaveHappened(); + } + + [Fact] + public async Task Delete_should_update_domain_object() + { + CreateContent(); + + var command = CreateContextForCommand(new DeleteContent { ContentId = contentId, User = user }); + + await TestUpdate(content, async _ => + { + await sut.HandleAsync(command); + }); + + A.CallTo(() => scriptEngine.Execute(A.Ignored, "")).MustHaveHappened(); + } + + private void CreateContent() + { + content.Create(new CreateContent { Data = data }); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs new file mode 100644 index 000000000..400964524 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs @@ -0,0 +1,269 @@ +// ========================================================================== +// ContentDomainObjectTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using FluentAssertions; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public class ContentDomainObjectTests : HandlerTestBase + { + private readonly NamedContentData data = + new NamedContentData() + .AddField("field1", + new ContentFieldData() + .AddValue("iv", 1)); + private readonly NamedContentData otherData = + new NamedContentData() + .AddField("field2", + new ContentFieldData() + .AddValue("iv", 2)); + private readonly Guid contentId = Guid.NewGuid(); + private readonly ContentDomainObject sut = new ContentDomainObject(); + + [Fact] + public void Create_should_throw_exception_if_created() + { + sut.Create(new CreateContent { Data = data }); + + Assert.Throws(() => + { + sut.Create(CreateContentCommand(new CreateContent { Data = data })); + }); + } + + [Fact] + public void Create_should_create_events() + { + sut.Create(CreateContentCommand(new CreateContent { Data = data })); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateContentEvent(new ContentCreated { Data = data }) + ); + } + + [Fact] + public void Create_should_also_publish_if_set_to_true() + { + sut.Create(CreateContentCommand(new CreateContent { Data = data, Publish = true })); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateContentEvent(new ContentCreated { Data = data }), + CreateContentEvent(new ContentStatusChanged { Status = Status.Published }) + ); + } + + [Fact] + public void Update_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.Update(CreateContentCommand(new UpdateContent { Data = data })); + }); + } + + [Fact] + public void Update_should_throw_exception_if_content_is_deleted() + { + CreateContent(); + DeleteContent(); + + Assert.Throws(() => + { + sut.Update(CreateContentCommand(new UpdateContent())); + }); + } + + [Fact] + public void Update_should_create_events() + { + CreateContent(); + + sut.Update(CreateContentCommand(new UpdateContent { Data = otherData })); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateContentEvent(new ContentUpdated { Data = otherData }) + ); + } + + [Fact] + public void Update_should_not_create_event_for_same_data() + { + CreateContent(); + UpdateContent(); + + sut.Update(CreateContentCommand(new UpdateContent { Data = data })); + + sut.GetUncomittedEvents().Should().BeEmpty(); + } + + [Fact] + public void Patch_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.Patch(CreateContentCommand(new PatchContent { Data = data })); + }); + } + + [Fact] + public void Patch_should_throw_exception_if_content_is_deleted() + { + CreateContent(); + DeleteContent(); + + Assert.Throws(() => + { + sut.Patch(CreateContentCommand(new PatchContent())); + }); + } + + [Fact] + public void Patch_should_create_events() + { + CreateContent(); + + sut.Patch(CreateContentCommand(new PatchContent { Data = otherData })); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateContentEvent(new ContentUpdated { Data = otherData }) + ); + } + + [Fact] + public void Patch_should_not_create_event_for_same_data() + { + CreateContent(); + UpdateContent(); + + sut.Patch(CreateContentCommand(new PatchContent { Data = data })); + + sut.GetUncomittedEvents().Should().BeEmpty(); + } + + [Fact] + public void ChangeStatus_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus())); + }); + } + + [Fact] + public void ChangeStatus_should_throw_exception_if_content_is_deleted() + { + CreateContent(); + DeleteContent(); + + Assert.Throws(() => + { + sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus())); + }); + } + + [Fact] + public void ChangeStatus_should_refresh_properties_and_create_events() + { + CreateContent(); + + sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus { Status = Status.Published })); + + Assert.Equal(Status.Published, sut.State.Status); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateContentEvent(new ContentStatusChanged { Status = Status.Published }) + ); + } + + [Fact] + public void Delete_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.Delete(CreateContentCommand(new DeleteContent())); + }); + } + + [Fact] + public void Delete_should_throw_exception_if_already_deleted() + { + CreateContent(); + DeleteContent(); + + Assert.Throws(() => + { + sut.Delete(CreateContentCommand(new DeleteContent())); + }); + } + + [Fact] + public void Delete_should_update_properties_and_create_events() + { + CreateContent(); + + sut.Delete(CreateContentCommand(new DeleteContent())); + + Assert.True(sut.State.IsDeleted); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateContentEvent(new ContentDeleted()) + ); + } + + private void CreateContent() + { + sut.Create(CreateContentCommand(new CreateContent { Data = data })); + sut.ClearUncommittedEvents(); + } + + private void UpdateContent() + { + sut.Update(CreateContentCommand(new UpdateContent { Data = data })); + sut.ClearUncommittedEvents(); + } + + private void ChangeStatus(Status status) + { + sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus { Status = status })); + sut.ClearUncommittedEvents(); + } + + private void DeleteContent() + { + sut.Delete(CreateContentCommand(new DeleteContent())); + sut.ClearUncommittedEvents(); + } + + protected T CreateContentEvent(T @event) where T : ContentEvent + { + @event.ContentId = contentId; + + return CreateEvent(@event); + } + + protected T CreateContentCommand(T command) where T : ContentCommand + { + command.ContentId = contentId; + + return CreateCommand(command); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentEventTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentEventTests.cs new file mode 100644 index 000000000..0dec9d79f --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentEventTests.cs @@ -0,0 +1,70 @@ +// ========================================================================== +// ContentEventTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Domain.Apps.Events.Contents.Old; +using Squidex.Infrastructure; +using Xunit; + +#pragma warning disable CS0612 // Type or member is obsolete + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public class ContentEventTests + { + private readonly RefToken actor = new RefToken("User", Guid.NewGuid().ToString()); + private readonly NamedId appId = new NamedId(Guid.NewGuid(), "my-app"); + private readonly NamedId schemaId = new NamedId(Guid.NewGuid(), "my-schema"); + private readonly Guid contentId = Guid.NewGuid(); + + [Fact] + public void Should_migrate_content_published_to_content_status_changed() + { + var source = CreateEvent(new ContentPublished()); + + source.Migrate().ShouldBeSameEvent(CreateEvent(new ContentStatusChanged { Status = Status.Published })); + } + + [Fact] + public void Should_migrate_content_unpublished_to_content_status_changed() + { + var source = CreateEvent(new ContentUnpublished()); + + source.Migrate().ShouldBeSameEvent(CreateEvent(new ContentStatusChanged { Status = Status.Draft })); + } + + [Fact] + public void Should_migrate_content_restored_to_content_status_changed() + { + var source = CreateEvent(new ContentRestored()); + + source.Migrate().ShouldBeSameEvent(CreateEvent(new ContentStatusChanged { Status = Status.Draft })); + } + + [Fact] + public void Should_migrate_content_archived_to_content_status_changed() + { + var source = CreateEvent(new ContentArchived()); + + source.Migrate().ShouldBeSameEvent(CreateEvent(new ContentStatusChanged { Status = Status.Archived })); + } + + private T CreateEvent(T contentEvent) where T : ContentEvent + { + contentEvent.Actor = actor; + contentEvent.AppId = appId; + contentEvent.SchemaId = schemaId; + contentEvent.ContentId = contentId; + + return contentEvent; + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs index 90fddc2c4..ecb503b83 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs @@ -60,7 +60,7 @@ namespace Squidex.Domain.Apps.Entities.Contents [Fact] public async Task Should_return_schema_from_id_if_string_is_guid() { - A.CallTo(() => appProvider.GetSchemaAsync(appName, schemaId, false)) + A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false)) .Returns(schema); var result = await sut.FindSchemaAsync(app, schemaId.ToString()); @@ -71,7 +71,7 @@ namespace Squidex.Domain.Apps.Entities.Contents [Fact] public async Task Should_return_schema_from_name_if_string_not_guid() { - A.CallTo(() => appProvider.GetSchemaAsync(appName, "my-schema", false)) + A.CallTo(() => appProvider.GetSchemaAsync(appId, "my-schema", false)) .Returns(schema); var result = await sut.FindSchemaAsync(app, "my-schema"); @@ -82,7 +82,7 @@ namespace Squidex.Domain.Apps.Entities.Contents [Fact] public async Task Should_throw_if_schema_not_found() { - A.CallTo(() => appProvider.GetSchemaAsync(appName, "my-schema", false)) + A.CallTo(() => appProvider.GetSchemaAsync(appId, "my-schema", false)) .Returns((ISchemaEntity)null); await Assert.ThrowsAsync(() => sut.FindSchemaAsync(app, "my-schema")); @@ -91,7 +91,7 @@ namespace Squidex.Domain.Apps.Entities.Contents [Fact] public async Task Should_return_content_from_repository_and_transform() { - A.CallTo(() => appProvider.GetSchemaAsync(appName, schemaId, false)) + A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false)) .Returns(schema); A.CallTo(() => contentRepository.FindContentAsync(app, schema, contentId)) .Returns(content); @@ -112,7 +112,7 @@ namespace Squidex.Domain.Apps.Entities.Contents [Fact] public async Task Should_throw_if_content_to_find_does_not_exist() { - A.CallTo(() => appProvider.GetSchemaAsync(appName, schemaId, false)) + A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false)) .Returns(schema); A.CallTo(() => contentRepository.FindContentAsync(app, schema, contentId)) .Returns((IContentEntity)null); @@ -190,7 +190,7 @@ namespace Squidex.Domain.Apps.Entities.Contents private void SetupFakeWithIdQuery(Status[] status, HashSet ids) { - A.CallTo(() => appProvider.GetSchemaAsync(appName, schemaId, false)) + A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false)) .Returns(schema); A.CallTo(() => contentRepository.QueryAsync(app, schema, A.That.IsSameSequenceAs(status), ids)) .Returns(new List { content }); @@ -200,7 +200,7 @@ namespace Squidex.Domain.Apps.Entities.Contents private void SetupFakeWithOdataQuery(Status[] status) { - A.CallTo(() => appProvider.GetSchemaAsync(appName, schemaId, false)) + A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false)) .Returns(schema); A.CallTo(() => contentRepository.QueryAsync(app, schema, A.That.IsSameSequenceAs(status), A.Ignored)) .Returns(new List { content }); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQLTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQLTests.cs index c4624d1ff..b43cfe6ab 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQLTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQLTests.cs @@ -85,7 +85,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var allSchemas = new List { schema }; - A.CallTo(() => appProvider.GetSchemasAsync(appName)).Returns(allSchemas); + A.CallTo(() => appProvider.GetSchemasAsync(appId)).Returns(allSchemas); sut = new CachingGraphQLService(cache, appProvider, assetRepository, contentQuery, new FakeUrlGenerator()); } @@ -391,7 +391,7 @@ namespace Squidex.Domain.Apps.Entities.Contents }} }}"; - A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId)) + A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId, -1)) .Returns((schema, content)); var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); @@ -483,7 +483,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var refContents = new List { contentRef }; - A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId)) + A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId, -1)) .Returns((schema, content)); A.CallTo(() => contentQuery.QueryWithCountAsync(app, schema.Id.ToString(), user, false, A>.That.Matches(x => x.Contains(contentRefId)))) @@ -543,7 +543,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var refAssets = new List { assetRef }; - A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId)) + A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId, -1)) .Returns((schema, content)); A.CallTo(() => assetRepository.QueryAsync(app.Id, null, A>.That.Matches(x => x.Contains(assetRefId)), null, int.MaxValue, 0)) @@ -602,7 +602,7 @@ namespace Squidex.Domain.Apps.Entities.Contents }} }}"; - A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId)) + A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId, -1)) .Returns((schema, content)); var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs new file mode 100644 index 000000000..49a9c465e --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs @@ -0,0 +1,99 @@ +// ========================================================================== +// GuardContentTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.Contents.Guards; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Contents.Guard +{ + public class GuardContentTests + { + [Fact] + public void CanCreate_should_throw_exception_if_data_is_null() + { + var command = new CreateContent(); + + Assert.Throws(() => GuardContent.CanCreate(command)); + } + + [Fact] + public void CanCreate_should_not_throw_exception_if_data_is_not_null() + { + var command = new CreateContent { Data = new NamedContentData() }; + + GuardContent.CanCreate(command); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_data_is_null() + { + var command = new UpdateContent(); + + Assert.Throws(() => GuardContent.CanUpdate(command)); + } + + [Fact] + public void CanUpdate_should_not_throw_exception_if_data_is_not_null() + { + var command = new UpdateContent { Data = new NamedContentData() }; + + GuardContent.CanUpdate(command); + } + + [Fact] + public void CanPatch_should_throw_exception_if_data_is_null() + { + var command = new PatchContent(); + + Assert.Throws(() => GuardContent.CanPatch(command)); + } + + [Fact] + public void CanPatch_should_not_throw_exception_if_data_is_not_null() + { + var command = new PatchContent { Data = new NamedContentData() }; + + GuardContent.CanPatch(command); + } + + [Fact] + public void CanChangeContentStatus_should_throw_exception_if_status_not_valid() + { + var command = new ChangeContentStatus { Status = (Status)10 }; + + Assert.Throws(() => GuardContent.CanChangeContentStatus(Status.Archived, command)); + } + + [Fact] + public void CanChangeContentStatus_should_throw_exception_if_status_flow_not_valid() + { + var command = new ChangeContentStatus { Status = Status.Published }; + + Assert.Throws(() => GuardContent.CanChangeContentStatus(Status.Archived, command)); + } + + [Fact] + public void CanChangeContentStatus_not_should_throw_exception_if_status_flow_valid() + { + var command = new ChangeContentStatus { Status = Status.Published }; + + GuardContent.CanChangeContentStatus(Status.Draft, command); + } + + [Fact] + public void CanPatch_should_not_throw_exception() + { + var command = new DeleteContent(); + + GuardContent.CanDelete(command); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ODataQueryTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ODataQueryTests.cs index bd00e634b..1f4d4d239 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ODataQueryTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ODataQueryTests.cs @@ -6,7 +6,6 @@ // All rights reserved. // ========================================================================== -/* using System; using System.Collections.Immutable; using FakeItEasy; @@ -390,5 +389,4 @@ namespace Squidex.Domain.Apps.Entities.Contents return query; } } -} -*/ \ No newline at end of file +} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs index 949759999..81d5f20fe 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/GuardRuleTests.cs @@ -31,7 +31,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards public GuardRuleTests() { - A.CallTo(() => appProvider.GetSchemaAsync(appId.Name, A.Ignored, false)) + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A.Ignored, false)) .Returns(A.Fake()); } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs index f86e99df0..f24ecd9cd 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs @@ -19,12 +19,12 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards.Triggers public class ContentChangedTriggerTests { private readonly IAppProvider appProvider = A.Fake(); - private readonly string appName = "my-app"; + private readonly Guid appId = Guid.NewGuid(); [Fact] public async Task Should_add_error_if_schemas_ids_are_not_valid() { - A.CallTo(() => appProvider.GetSchemaAsync(appName, A.Ignored, false)) + A.CallTo(() => appProvider.GetSchemaAsync(appId, A.Ignored, false)) .Returns(Task.FromResult(null)); var trigger = new ContentChangedTrigger @@ -34,7 +34,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards.Triggers ) }; - var errors = await RuleTriggerValidator.ValidateAsync(appName, trigger, appProvider); + var errors = await RuleTriggerValidator.ValidateAsync(appId, trigger, appProvider); Assert.NotEmpty(errors); } @@ -44,7 +44,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards.Triggers { var trigger = new ContentChangedTrigger(); - var errors = await RuleTriggerValidator.ValidateAsync(appName, trigger, appProvider); + var errors = await RuleTriggerValidator.ValidateAsync(appId, trigger, appProvider); Assert.Empty(errors); } @@ -57,7 +57,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards.Triggers Schemas = ImmutableList.Empty }; - var errors = await RuleTriggerValidator.ValidateAsync(appName, trigger, appProvider); + var errors = await RuleTriggerValidator.ValidateAsync(appId, trigger, appProvider); Assert.Empty(errors); } @@ -65,7 +65,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards.Triggers [Fact] public async Task Should_not_add_error_if_schemas_ids_are_valid() { - A.CallTo(() => appProvider.GetSchemaAsync(appName, A.Ignored, false)) + A.CallTo(() => appProvider.GetSchemaAsync(appId, A.Ignored, false)) .Returns(A.Fake()); var trigger = new ContentChangedTrigger @@ -75,7 +75,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards.Triggers ) }; - var errors = await RuleTriggerValidator.ValidateAsync(appName, trigger, appProvider); + var errors = await RuleTriggerValidator.ValidateAsync(appId, trigger, appProvider); Assert.Empty(errors); } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs index 3b04844ff..f721df368 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs @@ -31,7 +31,7 @@ namespace Squidex.Domain.Apps.Entities.Rules public RuleCommandMiddlewareTests() { - A.CallTo(() => appProvider.GetSchemaAsync(A.Ignored, A.Ignored, false)) + A.CallTo(() => appProvider.GetSchemaAsync(A.Ignored, A.Ignored, false)) .Returns(A.Fake()); sut = new RuleCommandMiddleware(Handler, appProvider); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs index 3cd294d17..fb6b8ce31 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs @@ -78,7 +78,7 @@ namespace Squidex.Domain.Apps.Entities.Rules A.CallTo(() => ruleEntity2.RuleDef).Returns(rule2); A.CallTo(() => ruleEntity3.RuleDef).Returns(rule3); - A.CallTo(() => appProvider.GetRulesAsync(appId.Name)) + A.CallTo(() => appProvider.GetRulesAsync(appId.Id)) .Returns(new List { ruleEntity1, ruleEntity2, ruleEntity3 }); A.CallTo(() => ruleService.CreateJob(rule1, @event)) diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj b/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj index eab4caa37..f89e8cc4f 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj @@ -12,7 +12,9 @@ + + diff --git a/tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs index ea6bf1899..e95fca91e 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using FakeItEasy; using Squidex.Infrastructure.Commands.TestHelpers; using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; using Squidex.Infrastructure.States; using Squidex.Infrastructure.Tasks; using Xunit; @@ -19,42 +20,29 @@ namespace Squidex.Infrastructure.Commands { public class AggregateHandlerTests { + private readonly ISemanticLog log = A.Fake(); private readonly IServiceProvider serviceProvider = A.Fake(); private readonly IStore store = A.Fake(); private readonly IStateFactory stateFactory = A.Fake(); private readonly IPersistence persistence = A.Fake>(); private readonly Envelope event1 = new Envelope(new MyEvent()); private readonly Envelope event2 = new Envelope(new MyEvent()); - private readonly DomainObjectFactoryFunction factory; private readonly CommandContext context; - private readonly AggregateHandler sut; - private readonly DomainObjectWrapper domainObjectWrapper = new DomainObjectWrapper(); private readonly Guid domainObjectId = Guid.NewGuid(); - private readonly MyDomainObject domainObject; + private readonly MyDomainObject domainObject = new MyDomainObject(); + private readonly AggregateHandler sut; public AggregateHandlerTests() { - factory = new DomainObjectFactoryFunction(id => domainObject); - - domainObject = - new MyDomainObject(domainObjectId, 1) - .RaiseNewEvent(event1) - .RaiseNewEvent(event2); - - context = new CommandContext(new MyCommand { AggregateId = domainObject.Id }); + context = new CommandContext(new MyCommand { AggregateId = domainObjectId }); A.CallTo(() => store.WithEventSourcing(domainObjectId.ToString(), A, Task>>.Ignored)) .Returns(persistence); - A.CallTo(() => serviceProvider.GetService(factory.GetType())) - .Returns(factory); - - A.CallTo(() => stateFactory.GetDetachedAsync>(domainObject.Id.ToString())) - .Returns(Task.FromResult(domainObjectWrapper)); - - sut = new AggregateHandler(stateFactory, serviceProvider); + A.CallTo(() => stateFactory.CreateAsync(domainObjectId.ToString())) + .Returns(Task.FromResult(domainObject)); - domainObjectWrapper.ActivateAsync(domainObjectId.ToString(), store).Wait(); + sut = new AggregateHandler(stateFactory, serviceProvider, log); } [Fact] diff --git a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs index 8db73aea9..903a2837a 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs @@ -22,7 +22,7 @@ namespace Squidex.Infrastructure.Commands var domainObjectId = Guid.NewGuid(); var domainObjectVersion = 123; - var sut = new MyDomainObject(domainObjectId, domainObjectVersion); + var sut = new MyDomainObject(); Assert.Equal(domainObjectId, sut.Id); Assert.Equal(domainObjectVersion, sut.Version); @@ -34,18 +34,16 @@ namespace Squidex.Infrastructure.Commands var event1 = new MyEvent(); var event2 = new MyEvent(); - var sut = new MyDomainObject(Guid.NewGuid(), 10); - - IAggregate aggregate = sut; + var sut = new MyDomainObject(); sut.RaiseNewEvent(event1); sut.RaiseNewEvent(event2); Assert.Equal(12, sut.Version); - Assert.Equal(new IEvent[] { event1, event2 }, aggregate.GetUncomittedEvents().Select(x => x.Payload).ToArray()); + Assert.Equal(new IEvent[] { event1, event2 }, sut.GetUncomittedEvents().Select(x => x.Payload).ToArray()); - aggregate.ClearUncommittedEvents(); + sut.ClearUncommittedEvents(); Assert.Equal(0, sut.GetUncomittedEvents().Count); } @@ -56,58 +54,13 @@ namespace Squidex.Infrastructure.Commands var event1 = new MyEvent(); var event2 = new MyEvent(); - var sut = new MyDomainObject(Guid.NewGuid(), 10); - - IAggregate aggregate = sut; + var sut = new MyDomainObject(); - aggregate.ApplyEvent(new Envelope(event1)); - aggregate.ApplyEvent(new Envelope(event2)); + sut.RaiseEvent(new Envelope(event1)); + sut.RaiseEvent(new Envelope(event2)); Assert.Equal(12, sut.Version); Assert.Equal(0, sut.GetUncomittedEvents().Count); } - - [Fact] - public void Should_make_correct_equal_comparisons() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var user1a = new MyDomainObject(id1, 1); - var user1b = new MyDomainObject(id1, 2); - var user2a = new MyDomainObject(id2, 2); - - Assert.True(user1a.Equals(user1b)); - Assert.False(user1a.Equals(user2a)); - } - - [Fact] - public void Should_make_correct_object_equal_comparisons() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var user1a = new MyDomainObject(id1, 1); - - object user1b = new MyDomainObject(id1, 2); - object user2a = new MyDomainObject(id2, 2); - - Assert.True(user1a.Equals(user1b)); - Assert.False(user1a.Equals(user2a)); - } - - [Fact] - public void Should_provide_correct_hash_codes() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var user1a = new MyDomainObject(id1, 1); - var user1b = new MyDomainObject(id1, 2); - var user2a = new MyDomainObject(id2, 2); - - Assert.Equal(user1a.GetHashCode(), user1b.GetHashCode()); - Assert.NotEqual(user1a.GetHashCode(), user2a.GetHashCode()); - } } } diff --git a/tests/Squidex.Infrastructure.Tests/Commands/TestHelpers/MyDomainObject.cs b/tests/Squidex.Infrastructure.Tests/Commands/TestHelpers/MyDomainObject.cs index 33d65efd8..c1eeb3c4b 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/TestHelpers/MyDomainObject.cs +++ b/tests/Squidex.Infrastructure.Tests/Commands/TestHelpers/MyDomainObject.cs @@ -11,13 +11,8 @@ using Squidex.Infrastructure.EventSourcing; namespace Squidex.Infrastructure.Commands.TestHelpers { - internal sealed class MyDomainObject : DomainObjectBase + internal sealed class MyDomainObject : DomainObjectBase { - public MyDomainObject(Guid id, int version) - : base(id, version) - { - } - public MyDomainObject RaiseNewEvent(IEvent @event) { RaiseEvent(@event); @@ -31,9 +26,5 @@ namespace Squidex.Infrastructure.Commands.TestHelpers return this; } - - protected override void DispatchEvent(Envelope @event) - { - } } } diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerManagerTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerManagerTests.cs index db9602b0c..e6d1edd31 100644 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerManagerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerManagerTests.cs @@ -33,8 +33,8 @@ namespace Squidex.Infrastructure.EventSourcing.Grains A.CallTo(() => consumer1.Name).Returns(consumerName1); A.CallTo(() => consumer2.Name).Returns(consumerName2); - A.CallTo(() => factory.GetDetachedAsync(consumerName1)).Returns(actor1); - A.CallTo(() => factory.GetDetachedAsync(consumerName2)).Returns(actor2); + A.CallTo(() => factory.CreateAsync(consumerName1)).Returns(actor1); + A.CallTo(() => factory.CreateAsync(consumerName2)).Returns(actor2); sut = new EventConsumerGrainManager(new IEventConsumer[] { consumer1, consumer2 }, pubSub, factory); } diff --git a/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs b/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs index 76198d0b1..b23a80a3b 100644 --- a/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs +++ b/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs @@ -72,7 +72,7 @@ namespace Squidex.Infrastructure.States private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); private readonly IPubSub pubSub = new InMemoryPubSub(true); private readonly IServiceProvider services = A.Fake(); - private readonly ISnapshotStore snapshotStore = A.Fake(); + private readonly ISnapshotStore snapshotStore = A.Fake>(); private readonly IStreamNameResolver streamNameResolver = A.Fake(); private readonly StateFactory sut; @@ -82,13 +82,15 @@ namespace Squidex.Infrastructure.States .Returns(statefulObject); A.CallTo(() => services.GetService(typeof(MyStatefulObjectWithSnapshot))) .Returns(statefulObjectWithSnapShot); + A.CallTo(() => services.GetService(typeof(ISnapshotStore))) + .Returns(snapshotStore); A.CallTo(() => streamNameResolver.GetStreamName(typeof(MyStatefulObject), key)) .Returns(key); A.CallTo(() => streamNameResolver.GetStreamName(typeof(MyStatefulObjectWithSnapshot), key)) .Returns(key); - sut = new StateFactory(pubSub, cache, eventStore, eventDataFormatter, services, snapshotStore, streamNameResolver); + sut = new StateFactory(pubSub, cache, eventStore, eventDataFormatter, services, streamNameResolver); sut.Connect(); } @@ -102,7 +104,7 @@ namespace Squidex.Infrastructure.States SetupEventStore(event1, event2); - var actualObject = await sut.GetSynchronizedAsync(key); + var actualObject = await sut.GetSingleAsync(key); Assert.Same(statefulObject, actualObject); Assert.NotNull(cache.Get(key)); @@ -115,12 +117,12 @@ namespace Squidex.Infrastructure.States { statefulObjectWithSnapShot.ExpectedVersion = null; - A.CallTo(() => snapshotStore.ReadAsync(key)) + A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((2, 2L)); SetupEventStore(3, 2); - await sut.GetSynchronizedAsync(key); + await sut.GetSingleAsync(key); A.CallTo(() => eventStore.GetEventsAsync(key, 3)) .MustHaveHappened(); @@ -131,12 +133,12 @@ namespace Squidex.Infrastructure.States { statefulObjectWithSnapShot.ExpectedVersion = null; - A.CallTo(() => snapshotStore.ReadAsync(key)) + A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((2, 2L)); SetupEventStore(3, 0, 3); - await Assert.ThrowsAsync(() => sut.GetSynchronizedAsync(key)); + await Assert.ThrowsAsync(() => sut.GetSingleAsync(key)); } [Fact] @@ -144,12 +146,12 @@ namespace Squidex.Infrastructure.States { statefulObjectWithSnapShot.ExpectedVersion = null; - A.CallTo(() => snapshotStore.ReadAsync(key)) + A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((2, 2L)); SetupEventStore(3, 4, 3); - await Assert.ThrowsAsync(() => sut.GetSynchronizedAsync(key)); + await Assert.ThrowsAsync(() => sut.GetSingleAsync(key)); } [Fact] @@ -159,7 +161,7 @@ namespace Squidex.Infrastructure.States SetupEventStore(0); - await Assert.ThrowsAsync(() => sut.GetSynchronizedAsync(key)); + await Assert.ThrowsAsync(() => sut.GetSingleAsync(key)); } [Fact] @@ -169,7 +171,7 @@ namespace Squidex.Infrastructure.States SetupEventStore(3); - await Assert.ThrowsAsync(() => sut.GetSynchronizedAsync(key)); + await Assert.ThrowsAsync(() => sut.GetSingleAsync(key)); } [Fact] @@ -179,7 +181,7 @@ namespace Squidex.Infrastructure.States SetupEventStore(0); - await sut.GetSynchronizedAsync(key); + await sut.GetSingleAsync(key); } [Fact] @@ -189,7 +191,7 @@ namespace Squidex.Infrastructure.States SetupEventStore(0); - var actualObject = await sut.GetSynchronizedAsync(key); + var actualObject = await sut.GetSingleAsync(key); Assert.Same(statefulObject, actualObject); Assert.NotNull(cache.Get(key)); @@ -202,12 +204,12 @@ namespace Squidex.Infrastructure.States SetupEventStore(0); - var actualObject1 = await sut.GetSynchronizedAsync(key); + var actualObject1 = await sut.GetSingleAsync(key); Assert.Same(statefulObject, actualObject1); Assert.NotNull(cache.Get(key)); - var actualObject2 = await sut.GetSynchronizedAsync(key); + var actualObject2 = await sut.GetSingleAsync(key); A.CallTo(() => services.GetService(typeof(MyStatefulObject))) .MustHaveHappened(Repeated.Exactly.Once); @@ -227,7 +229,7 @@ namespace Squidex.Infrastructure.States SetupEventStore(3); - var actualObject = await sut.GetSynchronizedAsync(key); + var actualObject = await sut.GetSingleAsync(key); Assert.Same(statefulObject, actualObject); @@ -250,7 +252,7 @@ namespace Squidex.Infrastructure.States SetupEventStore(3); - var actualObject = await sut.GetSynchronizedAsync(key); + var actualObject = await sut.GetSingleAsync(key); A.CallTo(() => eventStore.AppendEventsAsync(A.Ignored, key, 2, A>.That.Matches(x => x.Count == 2))) .Throws(new WrongEventVersionException(1, 1)); @@ -263,7 +265,7 @@ namespace Squidex.Infrastructure.States { statefulObject.ExpectedVersion = null; - var actualObject = await sut.GetSynchronizedAsync(key); + var actualObject = await sut.GetSingleAsync(key); await InvalidateCacheAsync(); @@ -275,14 +277,14 @@ namespace Squidex.Infrastructure.States { statefulObject.ExpectedVersion = null; - A.CallTo(() => snapshotStore.ReadAsync(key)) + A.CallTo(() => snapshotStore.ReadAsync(key)) .ReturnsLazily(() => Task.Delay(1).ContinueWith(x => (1, 1L))); var tasks = new List>(); for (var i = 0; i < 1000; i++) { - tasks.Add(Task.Run(() => sut.GetSynchronizedAsync(key))); + tasks.Add(Task.Run(() => sut.GetSingleAsync(key))); } var retrievedStates = await Task.WhenAll(tasks); diff --git a/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs b/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs index a57f05ab1..bb45d0cac 100644 --- a/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs +++ b/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs @@ -56,7 +56,7 @@ namespace Squidex.Infrastructure.States private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); private readonly IPubSub pubSub = new InMemoryPubSub(true); private readonly IServiceProvider services = A.Fake(); - private readonly ISnapshotStore snapshotStore = A.Fake(); + private readonly ISnapshotStore snapshotStore = A.Fake>(); private readonly IStreamNameResolver streamNameResolver = A.Fake(); private readonly StateFactory sut; @@ -64,8 +64,10 @@ namespace Squidex.Infrastructure.States { A.CallTo(() => services.GetService(typeof(MyStatefulObject))) .Returns(statefulObject); + A.CallTo(() => services.GetService(typeof(ISnapshotStore))) + .Returns(snapshotStore); - sut = new StateFactory(pubSub, cache, eventStore, eventDataFormatter, services, snapshotStore, streamNameResolver); + sut = new StateFactory(pubSub, cache, eventStore, eventDataFormatter, services, streamNameResolver); sut.Connect(); } @@ -79,10 +81,10 @@ namespace Squidex.Infrastructure.States { statefulObject.ExpectedVersion = 1; - A.CallTo(() => snapshotStore.ReadAsync(key)) + A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((123, 1)); - var actualObject = await sut.GetSynchronizedAsync(key); + var actualObject = await sut.GetSingleAsync(key); Assert.Same(statefulObject, actualObject); Assert.NotNull(cache.Get(key)); @@ -95,10 +97,10 @@ namespace Squidex.Infrastructure.States { statefulObject.ExpectedVersion = 0; - A.CallTo(() => snapshotStore.ReadAsync(key)) + A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((0, -1)); - await Assert.ThrowsAsync(() => sut.GetSynchronizedAsync(key)); + await Assert.ThrowsAsync(() => sut.GetSingleAsync(key)); } [Fact] @@ -106,10 +108,10 @@ namespace Squidex.Infrastructure.States { statefulObject.ExpectedVersion = 1; - A.CallTo(() => snapshotStore.ReadAsync(key)) + A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((2, 2)); - await Assert.ThrowsAsync(() => sut.GetSynchronizedAsync(key)); + await Assert.ThrowsAsync(() => sut.GetSingleAsync(key)); } [Fact] @@ -117,10 +119,10 @@ namespace Squidex.Infrastructure.States { statefulObject.ExpectedVersion = null; - A.CallTo(() => snapshotStore.ReadAsync(key)) + A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((0, -1)); - await sut.GetSynchronizedAsync(key); + await sut.GetSingleAsync(key); } [Fact] @@ -128,7 +130,7 @@ namespace Squidex.Infrastructure.States { statefulObject.ExpectedVersion = null; - var actualObject = await sut.GetSynchronizedAsync(key); + var actualObject = await sut.GetSingleAsync(key); Assert.Same(statefulObject, actualObject); Assert.NotNull(cache.Get(key)); @@ -139,12 +141,12 @@ namespace Squidex.Infrastructure.States { statefulObject.ExpectedVersion = null; - var actualObject1 = await sut.GetSynchronizedAsync(key); + var actualObject1 = await sut.GetSingleAsync(key); Assert.Same(statefulObject, actualObject1); Assert.NotNull(cache.Get(key)); - var actualObject2 = await sut.GetSynchronizedAsync(key); + var actualObject2 = await sut.GetSingleAsync(key); A.CallTo(() => services.GetService(typeof(MyStatefulObject))) .MustHaveHappened(Repeated.Exactly.Once); @@ -155,12 +157,12 @@ namespace Squidex.Infrastructure.States { statefulObject.ExpectedVersion = null; - var actualObject1 = await sut.GetDetachedAsync(key); + var actualObject1 = await sut.CreateAsync(key); Assert.Same(statefulObject, actualObject1); Assert.Null(cache.Get(key)); - var actualObject2 = await sut.GetDetachedAsync(key); + var actualObject2 = await sut.CreateAsync(key); A.CallTo(() => services.GetService(typeof(MyStatefulObject))) .MustHaveHappened(Repeated.Exactly.Twice); @@ -180,10 +182,10 @@ namespace Squidex.Infrastructure.States message = m; }); - A.CallTo(() => snapshotStore.ReadAsync(key)) + A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((123, version)); - var actualObject = await sut.GetSynchronizedAsync(key); + var actualObject = await sut.GetSingleAsync(key); Assert.Same(statefulObject, actualObject); Assert.Equal(123, statefulObject.State); @@ -206,13 +208,13 @@ namespace Squidex.Infrastructure.States var version = 1; - A.CallTo(() => snapshotStore.ReadAsync(key)) + A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((123, version)); A.CallTo(() => snapshotStore.WriteAsync(key, 123, version, 2)) .Throws(new InconsistentStateException(1, 1, new InvalidOperationException())); - var actualObject = await sut.GetSynchronizedAsync(key); + var actualObject = await sut.GetSingleAsync(key); await Assert.ThrowsAsync(() => statefulObject.WriteStateAsync()); } @@ -222,7 +224,7 @@ namespace Squidex.Infrastructure.States { statefulObject.ExpectedVersion = null; - var actualObject = await sut.GetSynchronizedAsync(key); + var actualObject = await sut.GetSingleAsync(key); await InvalidateCacheAsync(); @@ -234,14 +236,14 @@ namespace Squidex.Infrastructure.States { statefulObject.ExpectedVersion = null; - A.CallTo(() => snapshotStore.ReadAsync(key)) + A.CallTo(() => snapshotStore.ReadAsync(key)) .ReturnsLazily(() => Task.Delay(1).ContinueWith(x => (1, 1L))); var tasks = new List>(); for (var i = 0; i < 1000; i++) { - tasks.Add(Task.Run(() => sut.GetSynchronizedAsync(key))); + tasks.Add(Task.Run(() => sut.GetSingleAsync(key))); } var retrievedStates = await Task.WhenAll(tasks); @@ -251,7 +253,7 @@ namespace Squidex.Infrastructure.States Assert.Same(retrievedStates[0], retrievedState); } - A.CallTo(() => snapshotStore.ReadAsync(key)) + A.CallTo(() => snapshotStore.ReadAsync(key)) .MustHaveHappened(Repeated.Exactly.Once); }