diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs index 1d4cc508f..50d725d35 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs @@ -26,12 +26,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents public partial class MongoContentRepository : MongoRepositoryBase, IContentRepository { private readonly IAppProvider appProvider; - private readonly IMongoCollection archiveCollection; - - protected IMongoCollection ArchiveCollection - { - get { return archiveCollection; } - } public MongoContentRepository(IMongoDatabase database, IAppProvider appProvider) : base(database) @@ -39,8 +33,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents Guard.NotNull(appProvider, nameof(appProvider)); this.appProvider = appProvider; - - archiveCollection = database.GetCollection("States_Contents_Archive"); } protected override string CollectionName() @@ -52,15 +44,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { await collection.Indexes.TryDropOneAsync("si_1_st_1_dl_1_dt_text"); - await archiveCollection.Indexes.CreateOneAsync( - Index - .Ascending(x => x.ScheduledTo)); - - await archiveCollection.Indexes.CreateOneAsync( - Index - .Ascending(x => x.Id) - .Ascending(x => x.Version)); - await collection.Indexes.CreateOneAsync( Index .Text(x => x.DataText) @@ -150,17 +133,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents return ids.Except(contentEntities.Select(x => Guid.Parse(x["id"].AsString))).ToList(); } - public async Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id, long version) - { - var contentEntity = - await ArchiveCollection.Find(x => x.Id == id && x.Version >= version).SortBy(x => x.Version) - .FirstOrDefaultAsync(); - - contentEntity?.ParseData(schema.SchemaDef); - - return contentEntity; - } - public async Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id) { var contentEntity = @@ -180,12 +152,5 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents callback(c); }); } - - public override async Task ClearAsync() - { - await Database.DropCollectionAsync("States_Contents_Archive"); - - await base.ClearAsync(); - } } } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs index 54e236e4a..4bd67fd96 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs @@ -88,8 +88,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents } document.DocumentId = $"{key}_{newVersion}"; - - await ArchiveCollection.ReplaceOneAsync(x => x.DocumentId == document.DocumentId, document, Upsert); } private async Task GetSchemaAsync(Guid appId, Guid schemaId) diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs index ebd65e858..e93f09281 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs @@ -48,7 +48,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, long version = -1) + public async Task<(ISchemaEntity Schema, IContentEntity Content)> FindContentAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, Guid id) { Guard.NotNull(app, nameof(app)); Guard.NotNull(user, nameof(user)); @@ -58,10 +58,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var schema = await FindSchemaAsync(app, schemaIdOrName); - var content = - version > EtagVersion.Empty ? - await contentRepository.FindContentAsync(app, schema, id, version) : - await contentRepository.FindContentAsync(app, schema, id); + var content = await contentRepository.FindContentAsync(app, schema, id); if (content == null || (content.Status != Status.Published && !isFrontendClient)) { diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentVersionLoader.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentVersionLoader.cs new file mode 100644 index 000000000..04f02b1e0 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentVersionLoader.cs @@ -0,0 +1,95 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using NodaTime; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Contents.State; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.State; +using Squidex.Infrastructure; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public sealed class ContentVersionLoader : IContentVersionLoader + { + private readonly IStore store; + private readonly FieldRegistry registry; + + public ContentVersionLoader(IStore store, FieldRegistry registry) + { + Guard.NotNull(store, nameof(store)); + Guard.NotNull(registry, nameof(registry)); + + this.store = store; + + this.registry = registry; + } + + public async Task<(ISchemaEntity Schema, IContentEntity Content)> LoadAsync(Guid id, int version) + { + var content = new ContentState(); + + var persistence = store.WithEventSourcing(id, e => + { + if (content.Version < version) + { + content = content.Apply(e); + } + }); + + await persistence.ReadAsync(); + + if (content.Version != version) + { + throw new DomainObjectNotFoundException(id.ToString(), typeof(IContentEntity)); + } + + var (now, then) = await ReadSchema(content.SchemaId.Id, content.LastModified); + + foreach (var key in content.Data.Keys) + { + if (IsFieldRemovedOrChanged(then.SchemaDef, now.SchemaDef, key)) + { + content.Data.Remove(key); + } + } + + return (then, content); + } + + private static bool IsFieldRemovedOrChanged(Schema schemaThen, Schema schemaNow, string key) + { + return + !schemaThen.FieldsByName.TryGetValue(key, out var fieldThen) || + !schemaNow.FieldsByName.TryGetValue(key, out var fieldNow) || + fieldThen.GetType() != fieldNow.GetType(); + } + + private async Task<(ISchemaEntity, ISchemaEntity)> ReadSchema(Guid schemaId, Instant lastUpdate) + { + var state = new SchemaState(); + var stateAtVersion = (SchemaState)null; + + var persistence = store.WithEventSourcing(schemaId, e => + { + state = state.Apply(e, registry); + + if (state.LastModified < lastUpdate) + { + stateAtVersion = state; + } + }); + + await persistence.ReadAsync(); + + return (state, stateAtVersion); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs b/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs index f14a7d4cd..c2581135f 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, IResultList Contents)> QueryAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, bool archived, string query); - Task<(ISchemaEntity Schema, IContentEntity Content)> FindContentAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, Guid id, long version = EtagVersion.Any); + Task<(ISchemaEntity Schema, IContentEntity Content)> FindContentAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, Guid id); Task FindSchemaAsync(IAppEntity app, string schemaIdOrName); } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentVersionLoader.cs b/src/Squidex.Domain.Apps.Entities/Contents/IContentVersionLoader.cs new file mode 100644 index 000000000..54ae6a28b --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/IContentVersionLoader.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Schemas; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public interface IContentVersionLoader + { + Task<(ISchemaEntity Schema, IContentEntity Content)> LoadAsync(Guid id, int version); + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs b/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs index b9aba61be..0c2e687b7 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs @@ -27,8 +27,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Repositories Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id); - Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id, long version); - Task QueryScheduledWithoutDataAsync(Instant now, Func callback); } } diff --git a/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs b/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs index 210617ff6..13ce32acc 100644 --- a/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs +++ b/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs @@ -40,7 +40,7 @@ namespace Squidex.Domain.Apps.Entities public Instant LastModified { get; set; } [JsonProperty] - public long Version { get; set; } + public long Version { get; set; } = EtagVersion.Empty; public T Clone() { diff --git a/src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs b/src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs index 9ea5f1d54..673beb849 100644 --- a/src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs @@ -32,14 +32,17 @@ 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; } @@ -195,7 +198,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [ApiCosts(1)] public async Task GetContentVersion(string app, string name, Guid id, int version) { - var content = await contentQuery.FindContentAsync(App, name, User, id, version); + var content = await contentVersionLoader.LoadAsync(id, version); var response = SimpleMapper.Map(content.Content, new ContentDto()); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs index 669ea6d2f..ae5bc90fc 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs @@ -517,7 +517,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL }} }}"; - A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId, EtagVersion.Any)) + A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId)) .Returns((schema, content)); var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); @@ -610,7 +610,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var refContents = new List { contentRef }; - A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId, EtagVersion.Any)) + A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId)) .Returns((schema, content)); A.CallTo(() => contentQuery.QueryAsync(app, schema.Id.ToString(), user, false, A>.That.Matches(x => x.Contains(contentRefId)))) @@ -670,7 +670,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var refAssets = new List { assetRef }; - A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId, EtagVersion.Any)) + A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId)) .Returns((schema, content)); A.CallTo(() => assetRepository.QueryAsync(app.Id, A>.That.Matches(x => x.Contains(assetRefId)))) @@ -729,7 +729,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL }} }}"; - A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId, EtagVersion.Any)) + A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId)) .Returns((schema, content)); var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query });