diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs index 405148b16..cd9ee470e 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs @@ -57,7 +57,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { var useDraft = RequiresPublished(status); - var fullTextIds = await indexer.SearchAsync(query.FullText, app, schema, useDraft); + var fullTextIds = await indexer.SearchAsync(query.FullText, app, schema.Id, useDraft); if (fullTextIds?.Count == 0) { 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 a1a9620dd..31b46bc64 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs @@ -59,7 +59,14 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents await contents.UpsertAsync(content, oldVersion); - await indexer.IndexAsync(value.SchemaId.Id, value.Id, value.Data, value.DataDraft); + if (value.IsDeleted) + { + await indexer.DeleteAsync(value.SchemaId.Id, value.Id); + } + else + { + await indexer.IndexAsync(value.SchemaId.Id, value.Id, value.Data, value.DataDraft); + } } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/GrainTextIndexer.cs b/src/Squidex.Domain.Apps.Entities/Contents/Text/GrainTextIndexer.cs index d54cba803..4dc53ce67 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Text/GrainTextIndexer.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Text/GrainTextIndexer.cs @@ -21,46 +21,74 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text public sealed class GrainTextIndexer : ITextIndexer { private readonly IGrainFactory grainFactory; + private readonly ISemanticLog log; - public GrainTextIndexer(IGrainFactory grainFactory) + public GrainTextIndexer(IGrainFactory grainFactory, ISemanticLog log) { Guard.NotNull(grainFactory, nameof(grainFactory)); + Guard.NotNull(log, nameof(log)); this.grainFactory = grainFactory; + + this.log = log; } - public Task DeleteAsync(Guid schemaId, Guid id) + public async Task DeleteAsync(Guid schemaId, Guid id) { var index = grainFactory.GetGrain(schemaId); - return index.DeleteAsync(id); + using (Profiler.TraceMethod()) + { + try + { + await index.DeleteAsync(id); + } + catch (Exception ex) + { + log.LogError(ex, w => w + .WriteProperty("action", "DeleteTextEntry") + .WriteProperty("status", "Failed")); + } + } } public async Task IndexAsync(Guid schemaId, Guid id, NamedContentData data, NamedContentData dataDraft) { var index = grainFactory.GetGrain(schemaId); - if (data != null) + using (Profiler.TraceMethod()) { - await index.IndexAsync(id, new IndexData { Data = data }); - } + try + { + if (data != null) + { + await index.IndexAsync(id, new IndexData { Data = data }); + } - if (dataDraft != null) - { - await index.IndexAsync(id, new IndexData { Data = dataDraft, IsDraft = true }); + if (dataDraft != null) + { + await index.IndexAsync(id, new IndexData { Data = dataDraft, IsDraft = true }); + } + } + catch (Exception ex) + { + log.LogError(ex, w => w + .WriteProperty("action", "UpdateTextEntry") + .WriteProperty("status", "Failed")); + } } } - public async Task> SearchAsync(string queryText, IAppEntity app, ISchemaEntity schema, bool useDraft = false) + public async Task> SearchAsync(string queryText, IAppEntity app, Guid schemaId, bool useDraft = false) { if (string.IsNullOrWhiteSpace(queryText)) { return null; } - var index = grainFactory.GetGrain(schema.Id); + var index = grainFactory.GetGrain(schemaId); - using (Profiler.TraceMethod("SearchAsync")) + using (Profiler.TraceMethod()) { var context = CreateContext(app, useDraft); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexer.cs b/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexer.cs index 51dcf82ed..b350b8ee9 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexer.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexer.cs @@ -10,7 +10,6 @@ using System.Collections.Generic; using System.Threading.Tasks; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Schemas; namespace Squidex.Domain.Apps.Entities.Contents.Text { @@ -20,6 +19,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text Task IndexAsync(Guid schemaId, Guid id, NamedContentData data, NamedContentData dataDraft); - Task> SearchAsync(string queryText, IAppEntity app, ISchemaEntity schema, bool useDraft = false); + Task> SearchAsync(string queryText, IAppEntity app, Guid schemaId, bool useDraft = false); } } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/GrainTextIndexerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/GrainTextIndexerTests.cs new file mode 100644 index 000000000..35ebe6607 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/GrainTextIndexerTests.cs @@ -0,0 +1,144 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Orleans; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Orleans; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Contents.Text +{ + public class GrainTextIndexerTests + { + private readonly IGrainFactory grainFactory = A.Fake(); + private readonly ITextIndexerGrain grain = A.Fake(); + private readonly Guid schemaId = Guid.NewGuid(); + private readonly Guid contentId = Guid.NewGuid(); + private readonly GrainTextIndexer sut; + + public GrainTextIndexerTests() + { + A.CallTo(() => grainFactory.GetGrain(schemaId, null)) + .Returns(grain); + + sut = new GrainTextIndexer(grainFactory, A.Fake()); + } + + [Fact] + public async Task Should_call_grain_when_deleting_entry() + { + await sut.DeleteAsync(schemaId, contentId); + + A.CallTo(() => grain.DeleteAsync(contentId)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_catch_exception_when_deleting_failed() + { + A.CallTo(() => grain.DeleteAsync(contentId)) + .Throws(new InvalidOperationException()); + + await sut.DeleteAsync(schemaId, contentId); + } + + [Fact] + public async Task Should_call_grain_when_indexing_data() + { + var data = new NamedContentData(); + var dataDraft = new NamedContentData(); + + await sut.IndexAsync(schemaId, contentId, data, dataDraft); + + A.CallTo(() => grain.IndexAsync(contentId, A>.That.Matches(x => x.Value.Data == data && !x.Value.IsDraft))) + .MustHaveHappened(); + + A.CallTo(() => grain.IndexAsync(contentId, A>.That.Matches(x => x.Value.Data == dataDraft && x.Value.IsDraft))) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_not_call_grain_when_data_is_null() + { + var dataDraft = new NamedContentData(); + + await sut.IndexAsync(schemaId, contentId, null, dataDraft); + + A.CallTo(() => grain.IndexAsync(contentId, A>.That.Matches(x => !x.Value.IsDraft))) + .MustNotHaveHappened(); + + A.CallTo(() => grain.IndexAsync(contentId, A>.That.Matches(x => x.Value.Data == dataDraft && x.Value.IsDraft))) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_not_call_grain_when_data_draft_is_null() + { + var data = new NamedContentData(); + + await sut.IndexAsync(schemaId, contentId, data, null); + + A.CallTo(() => grain.IndexAsync(contentId, A>.That.Matches(x => x.Value.Data == data && !x.Value.IsDraft))) + .MustHaveHappened(); + + A.CallTo(() => grain.IndexAsync(contentId, A>.That.Matches(x => x.Value.IsDraft))) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_catch_exception_when_indexing_failed() + { + var data = new NamedContentData(); + + A.CallTo(() => grain.IndexAsync(contentId, A>.Ignored)) + .Throws(new InvalidOperationException()); + + await sut.IndexAsync(schemaId, contentId, data, null); + } + + [Fact] + public async Task Should_call_grain_when_searching() + { + var foundIds = new List { Guid.NewGuid() }; + + A.CallTo(() => grain.SearchAsync("Search", A.Ignored)) + .Returns(foundIds); + + var ids = await sut.SearchAsync("Search", GetApp(), schemaId, true); + + Assert.Equal(foundIds, ids); + } + + [Fact] + public async Task Should_not_call_grain_when_input_is_empty() + { + var ids = await sut.SearchAsync(string.Empty, GetApp(), schemaId, false); + + Assert.Null(ids); + + A.CallTo(() => grain.SearchAsync(A.Ignored, A.Ignored)) + .MustNotHaveHappened(); + } + + private static IAppEntity GetApp() + { + var app = A.Fake(); + + A.CallTo(() => app.LanguagesConfig).Returns(LanguagesConfig.Build(Language.EN, Language.DE)); + + return app; + } + } +}