mirror of https://github.com/Squidex/squidex.git
Browse Source
* Temporary * More progress. * Updates to text idnexer. * Some progress. * Test * Upgrade to NG9 * Build fixed. * Tsconfig udpated * Progress * Small build optimization. * Improvements * Fixes * Elastic search indexer. * Text indexer improvement. * Text indexer improvements. * Impróvements for singleton content items. * Found * Started with tests. * More tests. * More tests. * Better text index. * Handle asset search. * Remove unused status filter. * Tests fixed. * More tests. * Small task fix. * Loading animations. * Extracted scripting. * Fix formatting. * Performance improvement and migration fix.pull/492/head
committed by
GitHub
355 changed files with 7878 additions and 3774 deletions
@ -0,0 +1,135 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Domain.Apps.Core.Schemas; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Json.Objects; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.ConvertContent |
||||
|
{ |
||||
|
public sealed class StringFormatter : IFieldVisitor<string> |
||||
|
{ |
||||
|
private readonly IJsonValue value; |
||||
|
|
||||
|
private StringFormatter(IJsonValue value) |
||||
|
{ |
||||
|
this.value = value; |
||||
|
} |
||||
|
|
||||
|
public static string Format(IJsonValue? value, IField field) |
||||
|
{ |
||||
|
Guard.NotNull(field); |
||||
|
|
||||
|
if (value == null || value is JsonNull) |
||||
|
{ |
||||
|
return string.Empty; |
||||
|
} |
||||
|
|
||||
|
return field.Accept(new StringFormatter(value)); |
||||
|
} |
||||
|
|
||||
|
public string Visit(IArrayField field) |
||||
|
{ |
||||
|
return FormatArray("Item", "Items"); |
||||
|
} |
||||
|
|
||||
|
public string Visit(IField<AssetsFieldProperties> field) |
||||
|
{ |
||||
|
return FormatArray("Asset", "Assets"); |
||||
|
} |
||||
|
|
||||
|
public string Visit(IField<BooleanFieldProperties> field) |
||||
|
{ |
||||
|
if (value is JsonBoolean boolean && boolean.Value) |
||||
|
{ |
||||
|
return "Yes"; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
return "No"; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public string Visit(IField<DateTimeFieldProperties> field) |
||||
|
{ |
||||
|
return value.ToString(); |
||||
|
} |
||||
|
|
||||
|
public string Visit(IField<GeolocationFieldProperties> field) |
||||
|
{ |
||||
|
if (value is JsonObject obj && obj.TryGetValue("latitude", out var lat) && obj.TryGetValue("longitude", out var lon)) |
||||
|
{ |
||||
|
return $"{lat}, {lon}"; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
return string.Empty; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public string Visit(IField<JsonFieldProperties> field) |
||||
|
{ |
||||
|
return "<Json />"; |
||||
|
} |
||||
|
|
||||
|
public string Visit(IField<NumberFieldProperties> field) |
||||
|
{ |
||||
|
return value.ToString(); |
||||
|
} |
||||
|
|
||||
|
public string Visit(IField<ReferencesFieldProperties> field) |
||||
|
{ |
||||
|
return FormatArray("Reference", "References"); |
||||
|
} |
||||
|
|
||||
|
public string Visit(IField<StringFieldProperties> field) |
||||
|
{ |
||||
|
if (field.Properties.Editor == StringFieldEditor.StockPhoto) |
||||
|
{ |
||||
|
return "[Photo]"; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
return value.ToString(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public string Visit(IField<TagsFieldProperties> field) |
||||
|
{ |
||||
|
if (value is JsonArray array) |
||||
|
{ |
||||
|
return string.Join(", ", array); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
return string.Empty; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public string Visit(IField<UIFieldProperties> field) |
||||
|
{ |
||||
|
return string.Empty; |
||||
|
} |
||||
|
|
||||
|
private string FormatArray(string singularName, string pluralName) |
||||
|
{ |
||||
|
if (value is JsonArray array) |
||||
|
{ |
||||
|
if (array.Count > 1) |
||||
|
{ |
||||
|
return $"{array.Count} {pluralName}"; |
||||
|
} |
||||
|
else if (array.Count == 1) |
||||
|
{ |
||||
|
return $"1 {singularName}"; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return $"0 {pluralName}"; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,51 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core |
||||
|
{ |
||||
|
public interface IUrlGenerator |
||||
|
{ |
||||
|
string AppSettingsUI(NamedId<Guid> appId); |
||||
|
|
||||
|
string AssetsUI(NamedId<Guid> appId); |
||||
|
|
||||
|
string AssetsUI(NamedId<Guid> appId, string? query = null); |
||||
|
|
||||
|
string BackupsUI(NamedId<Guid> appId); |
||||
|
|
||||
|
string ClientsUI(NamedId<Guid> appId); |
||||
|
|
||||
|
string ContentsUI(NamedId<Guid> appId); |
||||
|
|
||||
|
string ContentsUI(NamedId<Guid> appId, NamedId<Guid> schemaId); |
||||
|
|
||||
|
string ContentUI(NamedId<Guid> appId, NamedId<Guid> schemaId, Guid contentId); |
||||
|
|
||||
|
string ContributorsUI(NamedId<Guid> appId); |
||||
|
|
||||
|
string DashboardUI(NamedId<Guid> appId); |
||||
|
|
||||
|
string LanguagesUI(NamedId<Guid> appId); |
||||
|
|
||||
|
string PatternsUI(NamedId<Guid> appId); |
||||
|
|
||||
|
string PlansUI(NamedId<Guid> appId); |
||||
|
|
||||
|
string RolesUI(NamedId<Guid> appId); |
||||
|
|
||||
|
string RulesUI(NamedId<Guid> appId); |
||||
|
|
||||
|
string SchemasUI(NamedId<Guid> appId); |
||||
|
|
||||
|
string SchemaUI(NamedId<Guid> appId, NamedId<Guid> schemaId); |
||||
|
|
||||
|
string WorkflowsUI(NamedId<Guid> appId); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,144 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using MongoDB.Driver; |
||||
|
using NodaTime; |
||||
|
using Squidex.Domain.Apps.Entities.Apps; |
||||
|
using Squidex.Domain.Apps.Entities.Contents; |
||||
|
using Squidex.Domain.Apps.Entities.Contents.Text; |
||||
|
using Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations; |
||||
|
using Squidex.Domain.Apps.Entities.Schemas; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Json; |
||||
|
using Squidex.Infrastructure.Log; |
||||
|
using Squidex.Infrastructure.MongoDb; |
||||
|
using Squidex.Infrastructure.Queries; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents |
||||
|
{ |
||||
|
public sealed class MongoContentCollectionAll : MongoRepositoryBase<MongoContentEntity> |
||||
|
{ |
||||
|
private readonly QueryContent queryContentAsync; |
||||
|
private readonly QueryContentsByIds queryContentsById; |
||||
|
private readonly QueryContentsByQuery queryContentsByQuery; |
||||
|
private readonly QueryIdsAsync queryIdsAsync; |
||||
|
private readonly QueryScheduledContents queryScheduledItems; |
||||
|
|
||||
|
public MongoContentCollectionAll(IMongoDatabase database, IAppProvider appProvider, IContentTextIndex indexer, IJsonSerializer serializer) |
||||
|
: base(database) |
||||
|
{ |
||||
|
queryContentAsync = new QueryContent(serializer); |
||||
|
queryContentsById = new QueryContentsByIds(serializer, appProvider); |
||||
|
queryContentsByQuery = new QueryContentsByQuery(serializer, indexer); |
||||
|
queryIdsAsync = new QueryIdsAsync(appProvider); |
||||
|
queryScheduledItems = new QueryScheduledContents(); |
||||
|
} |
||||
|
|
||||
|
public IMongoCollection<MongoContentEntity> GetInternalCollection() |
||||
|
{ |
||||
|
return Collection; |
||||
|
} |
||||
|
|
||||
|
protected override string CollectionName() |
||||
|
{ |
||||
|
return "State_Contents_All"; |
||||
|
} |
||||
|
|
||||
|
protected override async Task SetupCollectionAsync(IMongoCollection<MongoContentEntity> collection, CancellationToken ct = default) |
||||
|
{ |
||||
|
await queryContentAsync.PrepareAsync(collection, ct); |
||||
|
await queryContentsById.PrepareAsync(collection, ct); |
||||
|
await queryContentsByQuery.PrepareAsync(collection, ct); |
||||
|
await queryIdsAsync.PrepareAsync(collection, ct); |
||||
|
await queryScheduledItems.PrepareAsync(collection, ct); |
||||
|
} |
||||
|
|
||||
|
public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, ClrQuery query) |
||||
|
{ |
||||
|
using (Profiler.TraceMethod<MongoContentRepository>("QueryAsyncByQuery")) |
||||
|
{ |
||||
|
return await queryContentsByQuery.DoAsync(app, schema, query, SearchScope.All); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, HashSet<Guid> ids) |
||||
|
{ |
||||
|
Guard.NotNull(app); |
||||
|
|
||||
|
using (Profiler.TraceMethod<MongoContentRepository>("QueryAsyncByIds")) |
||||
|
{ |
||||
|
var result = await queryContentsById.DoAsync(app.Id, schema, ids); |
||||
|
|
||||
|
return ResultList.Create(result.Count, result.Select(x => x.Content)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task<List<(IContentEntity Content, ISchemaEntity Schema)>> QueryAsync(IAppEntity app, HashSet<Guid> ids) |
||||
|
{ |
||||
|
Guard.NotNull(app); |
||||
|
|
||||
|
using (Profiler.TraceMethod<MongoContentRepository>("QueryAsyncByIdsWithoutSchema")) |
||||
|
{ |
||||
|
var result = await queryContentsById.DoAsync(app.Id, null, ids); |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task<IContentEntity?> FindContentAsync(ISchemaEntity schema, Guid id) |
||||
|
{ |
||||
|
using (Profiler.TraceMethod<MongoContentRepository>()) |
||||
|
{ |
||||
|
return await queryContentAsync.DoAsync(schema, id); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task QueryScheduledWithoutDataAsync(Instant now, Func<IContentEntity, Task> callback) |
||||
|
{ |
||||
|
using (Profiler.TraceMethod<MongoContentRepository>()) |
||||
|
{ |
||||
|
await queryScheduledItems.DoAsync(now, callback); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> QueryIdsAsync(Guid appId, HashSet<Guid> ids) |
||||
|
{ |
||||
|
using (Profiler.TraceMethod<MongoContentRepository>()) |
||||
|
{ |
||||
|
return await queryIdsAsync.DoAsync(appId, ids); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> QueryIdsAsync(Guid appId, Guid schemaId, FilterNode<ClrValue> filterNode) |
||||
|
{ |
||||
|
using (Profiler.TraceMethod<MongoContentRepository>()) |
||||
|
{ |
||||
|
return await queryIdsAsync.DoAsync(appId, schemaId, filterNode); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public Task<MongoContentEntity> FindAsync(Guid id) |
||||
|
{ |
||||
|
return Collection.Find(x => x.Id == id).FirstOrDefaultAsync(); |
||||
|
} |
||||
|
|
||||
|
public Task UpsertVersionedAsync(Guid id, long oldVersion, MongoContentEntity entity) |
||||
|
{ |
||||
|
return Collection.UpsertVersionedAsync(id, oldVersion, entity); |
||||
|
} |
||||
|
|
||||
|
public Task RemoveAsync(Guid id) |
||||
|
{ |
||||
|
return Collection.DeleteOneAsync(x => x.Id == id); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,119 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using MongoDB.Driver; |
||||
|
using Squidex.Domain.Apps.Entities.Apps; |
||||
|
using Squidex.Domain.Apps.Entities.Contents; |
||||
|
using Squidex.Domain.Apps.Entities.Contents.Text; |
||||
|
using Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations; |
||||
|
using Squidex.Domain.Apps.Entities.Schemas; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Json; |
||||
|
using Squidex.Infrastructure.Log; |
||||
|
using Squidex.Infrastructure.MongoDb; |
||||
|
using Squidex.Infrastructure.Queries; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents |
||||
|
{ |
||||
|
public sealed class MongoContentCollectionPublished : MongoRepositoryBase<MongoContentEntity> |
||||
|
{ |
||||
|
private readonly QueryContent queryContentAsync; |
||||
|
private readonly QueryContentsByIds queryContentsById; |
||||
|
private readonly QueryContentsByQuery queryContentsByQuery; |
||||
|
private readonly QueryIdsAsync queryIdsAsync; |
||||
|
|
||||
|
public MongoContentCollectionPublished(IMongoDatabase database, IAppProvider appProvider, IContentTextIndex indexer, IJsonSerializer serializer) |
||||
|
: base(database) |
||||
|
{ |
||||
|
queryContentAsync = new QueryContent(serializer); |
||||
|
queryContentsById = new QueryContentsByIds(serializer, appProvider); |
||||
|
queryContentsByQuery = new QueryContentsByQuery(serializer, indexer); |
||||
|
queryIdsAsync = new QueryIdsAsync(appProvider); |
||||
|
} |
||||
|
|
||||
|
public IMongoCollection<MongoContentEntity> GetInternalCollection() |
||||
|
{ |
||||
|
return Collection; |
||||
|
} |
||||
|
|
||||
|
protected override string CollectionName() |
||||
|
{ |
||||
|
return "State_Contents_Published"; |
||||
|
} |
||||
|
|
||||
|
protected override async Task SetupCollectionAsync(IMongoCollection<MongoContentEntity> collection, CancellationToken ct = default) |
||||
|
{ |
||||
|
await queryContentAsync.PrepareAsync(collection, ct); |
||||
|
await queryContentsById.PrepareAsync(collection, ct); |
||||
|
await queryContentsByQuery.PrepareAsync(collection, ct); |
||||
|
await queryIdsAsync.PrepareAsync(collection, ct); |
||||
|
} |
||||
|
|
||||
|
public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, ClrQuery query) |
||||
|
{ |
||||
|
using (Profiler.TraceMethod<MongoContentRepository>("QueryAsyncByQuery")) |
||||
|
{ |
||||
|
return await queryContentsByQuery.DoAsync(app, schema, query, SearchScope.Published); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, HashSet<Guid> ids) |
||||
|
{ |
||||
|
Guard.NotNull(app); |
||||
|
|
||||
|
using (Profiler.TraceMethod<MongoContentRepository>("QueryAsyncByIds")) |
||||
|
{ |
||||
|
var result = await queryContentsById.DoAsync(app.Id, schema, ids); |
||||
|
|
||||
|
return ResultList.Create(result.Count, result.Select(x => x.Content)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task<List<(IContentEntity Content, ISchemaEntity Schema)>> QueryAsync(IAppEntity app, HashSet<Guid> ids) |
||||
|
{ |
||||
|
Guard.NotNull(app); |
||||
|
|
||||
|
using (Profiler.TraceMethod<MongoContentRepository>("QueryAsyncByIdsWithoutSchema")) |
||||
|
{ |
||||
|
var result = await queryContentsById.DoAsync(app.Id, null, ids); |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task<IContentEntity?> FindContentAsync(ISchemaEntity schema, Guid id) |
||||
|
{ |
||||
|
using (Profiler.TraceMethod<MongoContentRepository>()) |
||||
|
{ |
||||
|
return await queryContentAsync.DoAsync(schema, id); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> QueryIdsAsync(Guid appId, HashSet<Guid> ids) |
||||
|
{ |
||||
|
using (Profiler.TraceMethod<MongoContentRepository>()) |
||||
|
{ |
||||
|
return await queryIdsAsync.DoAsync(appId, ids); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public Task UpsertVersionedAsync(Guid id, long oldVersion, MongoContentEntity entity) |
||||
|
{ |
||||
|
return Collection.UpsertVersionedAsync(id, oldVersion, entity); |
||||
|
} |
||||
|
|
||||
|
public Task RemoveAsync(Guid id) |
||||
|
{ |
||||
|
return Collection.DeleteOneAsync(x => x.Id == id); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,61 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using MongoDB.Bson.Serialization; |
||||
|
using MongoDB.Driver; |
||||
|
using Squidex.Domain.Apps.Entities.Contents.Text.State; |
||||
|
using Squidex.Infrastructure.MongoDb; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.MongoDb.FullText |
||||
|
{ |
||||
|
public sealed class MongoTextIndexerState : MongoRepositoryBase<TextContentState>, ITextIndexerState |
||||
|
{ |
||||
|
static MongoTextIndexerState() |
||||
|
{ |
||||
|
BsonClassMap.RegisterClassMap<TextContentState>(cm => |
||||
|
{ |
||||
|
cm.MapIdField(x => x.ContentId); |
||||
|
|
||||
|
cm.MapProperty(x => x.DocIdCurrent) |
||||
|
.SetElementName("c"); |
||||
|
|
||||
|
cm.MapProperty(x => x.DocIdNew) |
||||
|
.SetElementName("n").SetIgnoreIfNull(true); |
||||
|
|
||||
|
cm.MapProperty(x => x.DocIdForPublished) |
||||
|
.SetElementName("p").SetIgnoreIfNull(true); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public MongoTextIndexerState(IMongoDatabase database, bool setup = false) |
||||
|
: base(database, setup) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
protected override string CollectionName() |
||||
|
{ |
||||
|
return "TextIndexerState"; |
||||
|
} |
||||
|
|
||||
|
public Task<TextContentState?> GetAsync(Guid contentId) |
||||
|
{ |
||||
|
return Collection.Find(x => x.ContentId == contentId).FirstOrDefaultAsync()!; |
||||
|
} |
||||
|
|
||||
|
public Task RemoveAsync(Guid contentId) |
||||
|
{ |
||||
|
return Collection.DeleteOneAsync(x => x.ContentId == contentId); |
||||
|
} |
||||
|
|
||||
|
public Task SetAsync(TextContentState state) |
||||
|
{ |
||||
|
return Collection.ReplaceOneAsync(x => x.ContentId == state.ContentId, state, UpsertReplace); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,92 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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.Core; |
||||
|
using Squidex.Domain.Apps.Entities.Search; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Shared; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Apps |
||||
|
{ |
||||
|
public sealed class AppSettingsSearchSource : ISearchSource |
||||
|
{ |
||||
|
private const int MaxItems = 3; |
||||
|
private readonly IUrlGenerator urlGenerator; |
||||
|
|
||||
|
public AppSettingsSearchSource(IUrlGenerator urlGenerator) |
||||
|
{ |
||||
|
Guard.NotNull(urlGenerator); |
||||
|
|
||||
|
this.urlGenerator = urlGenerator; |
||||
|
} |
||||
|
|
||||
|
public Task<SearchResults> SearchAsync(string query, Context context) |
||||
|
{ |
||||
|
var result = new SearchResults(); |
||||
|
|
||||
|
var appId = context.App.NamedId(); |
||||
|
|
||||
|
void Search(string term, string permissionId, Func<NamedId<Guid>, string> generate, SearchResultType type) |
||||
|
{ |
||||
|
if (result.Count < MaxItems && term.Contains(query, StringComparison.OrdinalIgnoreCase)) |
||||
|
{ |
||||
|
var permission = Permissions.ForApp(permissionId, appId.Name); |
||||
|
|
||||
|
if (context.Permissions.Allows(permission)) |
||||
|
{ |
||||
|
var url = generate(appId); |
||||
|
|
||||
|
result.Add(term, type, url); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Search("Assets", Permissions.AppAssetsRead, |
||||
|
urlGenerator.AssetsUI, SearchResultType.Asset); |
||||
|
|
||||
|
Search("Backups", Permissions.AppBackupsRead, |
||||
|
urlGenerator.BackupsUI, SearchResultType.Setting); |
||||
|
|
||||
|
Search("Clients", Permissions.AppClientsRead, |
||||
|
urlGenerator.ClientsUI, SearchResultType.Setting); |
||||
|
|
||||
|
Search("Contents", Permissions.AppCommon, |
||||
|
urlGenerator.ContentsUI, SearchResultType.Content); |
||||
|
|
||||
|
Search("Contributors", Permissions.AppContributorsRead, |
||||
|
urlGenerator.ContributorsUI, SearchResultType.Setting); |
||||
|
|
||||
|
Search("Dashboard", Permissions.AppCommon, |
||||
|
urlGenerator.DashboardUI, SearchResultType.Dashboard); |
||||
|
|
||||
|
Search("Languages", Permissions.AppCommon, |
||||
|
urlGenerator.LanguagesUI, SearchResultType.Setting); |
||||
|
|
||||
|
Search("Patterns", Permissions.AppCommon, |
||||
|
urlGenerator.PatternsUI, SearchResultType.Setting); |
||||
|
|
||||
|
Search("Roles", Permissions.AppRolesRead, |
||||
|
urlGenerator.RolesUI, SearchResultType.Setting); |
||||
|
|
||||
|
Search("Rules", Permissions.AppRulesRead, |
||||
|
urlGenerator.RulesUI, SearchResultType.Rule); |
||||
|
|
||||
|
Search("Schemas", Permissions.AppCommon, |
||||
|
urlGenerator.SchemasUI, SearchResultType.Schema); |
||||
|
|
||||
|
Search("Subscription", Permissions.AppPlansRead, |
||||
|
urlGenerator.PlansUI, SearchResultType.Setting); |
||||
|
|
||||
|
Search("Workflows", Permissions.AppWorkflowsRead, |
||||
|
urlGenerator.WorkflowsUI, SearchResultType.Setting); |
||||
|
|
||||
|
return Task.FromResult(result); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,60 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Domain.Apps.Core; |
||||
|
using Squidex.Domain.Apps.Entities.Search; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Queries; |
||||
|
using Squidex.Shared; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Assets |
||||
|
{ |
||||
|
public sealed class AssetsSearchSource : ISearchSource |
||||
|
{ |
||||
|
private readonly IAssetQueryService assetQuery; |
||||
|
private readonly IUrlGenerator urlGenerator; |
||||
|
|
||||
|
public AssetsSearchSource(IAssetQueryService assetQuery, IUrlGenerator urlGenerator) |
||||
|
{ |
||||
|
Guard.NotNull(assetQuery); |
||||
|
Guard.NotNull(urlGenerator); |
||||
|
|
||||
|
this.assetQuery = assetQuery; |
||||
|
|
||||
|
this.urlGenerator = urlGenerator; |
||||
|
} |
||||
|
|
||||
|
public async Task<SearchResults> SearchAsync(string query, Context context) |
||||
|
{ |
||||
|
var result = new SearchResults(); |
||||
|
|
||||
|
var permission = Permissions.ForApp(Permissions.AppAssetsRead, context.App.Name); |
||||
|
|
||||
|
if (context.Permissions.Allows(permission)) |
||||
|
{ |
||||
|
var filter = ClrFilter.Contains("fileName", query); |
||||
|
|
||||
|
var clrQuery = new ClrQuery { Filter = filter, Take = 5 }; |
||||
|
|
||||
|
var assets = await assetQuery.QueryAsync(context, null, Q.Empty.WithQuery(clrQuery)); |
||||
|
|
||||
|
if (assets.Count > 0) |
||||
|
{ |
||||
|
var url = urlGenerator.AssetsUI(context.App.NamedId(), query); |
||||
|
|
||||
|
foreach (var asset in assets) |
||||
|
{ |
||||
|
result.Add(asset.FileName, SearchResultType.Asset, url); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,13 +1,13 @@ |
|||||
// ==========================================================================
|
// ==========================================================================
|
||||
// Squidex Headless CMS
|
// Squidex Headless CMS
|
||||
// ==========================================================================
|
// ==========================================================================
|
||||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
// All rights reserved. Licensed under the MIT license.
|
// All rights reserved. Licensed under the MIT license.
|
||||
// ==========================================================================
|
// ==========================================================================
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Entities.Contents.Commands |
namespace Squidex.Domain.Apps.Entities.Contents.Commands |
||||
{ |
{ |
||||
public sealed class DiscardChanges : ContentCommand |
public sealed class CreateContentDraft : ContentCommand |
||||
{ |
{ |
||||
} |
} |
||||
} |
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.Commands |
||||
|
{ |
||||
|
public sealed class DeleteContentDraft : ContentCommand |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,163 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Text; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Domain.Apps.Core; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Domain.Apps.Core.ConvertContent; |
||||
|
using Squidex.Domain.Apps.Core.Schemas; |
||||
|
using Squidex.Domain.Apps.Entities.Contents.Text; |
||||
|
using Squidex.Domain.Apps.Entities.Search; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Json.Objects; |
||||
|
using Squidex.Shared; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents |
||||
|
{ |
||||
|
public sealed class ContentsSearchSource : ISearchSource |
||||
|
{ |
||||
|
private readonly IAppProvider appProvider; |
||||
|
private readonly IContentQueryService contentQuery; |
||||
|
private readonly IContentTextIndex contentTextIndexer; |
||||
|
private readonly IUrlGenerator urlGenerator; |
||||
|
|
||||
|
public ContentsSearchSource( |
||||
|
IAppProvider appProvider, |
||||
|
IContentQueryService contentQuery, |
||||
|
IContentTextIndex contentTextIndexer, |
||||
|
IUrlGenerator urlGenerator) |
||||
|
{ |
||||
|
Guard.NotNull(appProvider); |
||||
|
Guard.NotNull(contentQuery); |
||||
|
Guard.NotNull(contentTextIndexer); |
||||
|
Guard.NotNull(urlGenerator); |
||||
|
|
||||
|
this.appProvider = appProvider; |
||||
|
this.contentQuery = contentQuery; |
||||
|
this.contentTextIndexer = contentTextIndexer; |
||||
|
this.urlGenerator = urlGenerator; |
||||
|
} |
||||
|
|
||||
|
public async Task<SearchResults> SearchAsync(string query, Context context) |
||||
|
{ |
||||
|
var result = new SearchResults(); |
||||
|
|
||||
|
var searchFilter = await CreateSearchFilterAsync(context); |
||||
|
|
||||
|
if (searchFilter == null) |
||||
|
{ |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
var ids = await contentTextIndexer.SearchAsync($"{query}~", context.App, searchFilter, context.Scope()); |
||||
|
|
||||
|
if (ids == null || ids.Count == 0) |
||||
|
{ |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
var appId = context.App.NamedId(); |
||||
|
|
||||
|
var contents = await contentQuery.QueryAsync(context, ids); |
||||
|
|
||||
|
foreach (var content in contents) |
||||
|
{ |
||||
|
var url = urlGenerator.ContentUI(appId, content.SchemaId, content.Id); |
||||
|
|
||||
|
var name = FormatName(content, context.App.LanguagesConfig.Master); |
||||
|
|
||||
|
result.Add(name, SearchResultType.Content, url, content.SchemaDisplayName); |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
private async Task<SearchFilter?> CreateSearchFilterAsync(Context context) |
||||
|
{ |
||||
|
var allowedSchemas = new List<Guid>(); |
||||
|
|
||||
|
var schemas = await appProvider.GetSchemasAsync(context.App.Id); |
||||
|
|
||||
|
foreach (var schema in schemas) |
||||
|
{ |
||||
|
if (HasPermission(context, schema.SchemaDef.Name)) |
||||
|
{ |
||||
|
allowedSchemas.Add(schema.Id); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (allowedSchemas.Count == 0) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
return SearchFilter.MustHaveSchemas(allowedSchemas.ToArray()); |
||||
|
} |
||||
|
|
||||
|
private static bool HasPermission(Context context, string schemaName) |
||||
|
{ |
||||
|
var permission = Permissions.ForApp(Permissions.AppContentsRead, context.App.Name, schemaName); |
||||
|
|
||||
|
return context.Permissions.Allows(permission); |
||||
|
} |
||||
|
|
||||
|
private string FormatName(IEnrichedContentEntity content, string masterLanguage) |
||||
|
{ |
||||
|
var sb = new StringBuilder(); |
||||
|
|
||||
|
IJsonValue? GetValue(NamedContentData? data, RootField field) |
||||
|
{ |
||||
|
if (data != null && data.TryGetValue(field.Name, out var fieldValue) && fieldValue != null) |
||||
|
{ |
||||
|
var isInvariant = field.Partitioning.Equals(Partitioning.Invariant); |
||||
|
|
||||
|
if (isInvariant && fieldValue.TryGetValue("iv", out var value)) |
||||
|
{ |
||||
|
return value; |
||||
|
} |
||||
|
|
||||
|
if (!isInvariant && fieldValue.TryGetValue(masterLanguage, out value)) |
||||
|
{ |
||||
|
return value; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
if (content.ReferenceFields != null) |
||||
|
{ |
||||
|
foreach (var field in content.ReferenceFields) |
||||
|
{ |
||||
|
var value = GetValue(content.ReferenceData, field) ?? GetValue(content.Data, field); |
||||
|
|
||||
|
var formatted = StringFormatter.Format(value, field); |
||||
|
|
||||
|
if (!string.IsNullOrWhiteSpace(formatted)) |
||||
|
{ |
||||
|
if (sb.Length > 0) |
||||
|
{ |
||||
|
sb.Append(", "); |
||||
|
} |
||||
|
|
||||
|
sb.Append(formatted); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (sb.Length == 0) |
||||
|
{ |
||||
|
return "Content"; |
||||
|
} |
||||
|
|
||||
|
return sb.ToString(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,60 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Domain.Apps.Core.Scripting; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps |
||||
|
{ |
||||
|
public sealed class ScriptContent : IContentEnricherStep |
||||
|
{ |
||||
|
private readonly IScriptEngine scriptEngine; |
||||
|
|
||||
|
public ScriptContent(IScriptEngine scriptEngine) |
||||
|
{ |
||||
|
Guard.NotNull(scriptEngine, nameof(scriptEngine)); |
||||
|
|
||||
|
this.scriptEngine = scriptEngine; |
||||
|
} |
||||
|
|
||||
|
public async Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas) |
||||
|
{ |
||||
|
if (ShouldEnrich(context)) |
||||
|
{ |
||||
|
foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) |
||||
|
{ |
||||
|
var schema = await schemas(group.Key); |
||||
|
|
||||
|
var script = schema.SchemaDef.Scripts.Query; |
||||
|
|
||||
|
if (!string.IsNullOrWhiteSpace(script)) |
||||
|
{ |
||||
|
var results = new List<IEnrichedContentEntity>(); |
||||
|
|
||||
|
var scriptContext = new ScriptContext { User = context.User }; |
||||
|
|
||||
|
foreach (var content in group) |
||||
|
{ |
||||
|
scriptContext.Data = content.Data; |
||||
|
scriptContext.ContentId = content.Id; |
||||
|
|
||||
|
content.Data = scriptEngine.Transform(scriptContext, script); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static bool ShouldEnrich(Context context) |
||||
|
{ |
||||
|
return !context.IsFrontendClient; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,15 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents |
||||
|
{ |
||||
|
public enum SearchScope |
||||
|
{ |
||||
|
All, |
||||
|
Published |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,38 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.State |
||||
|
{ |
||||
|
public sealed class ContentVersion |
||||
|
{ |
||||
|
public Status Status { get; } |
||||
|
|
||||
|
public NamedContentData Data { get; } |
||||
|
|
||||
|
public ContentVersion(Status status, NamedContentData data) |
||||
|
{ |
||||
|
Guard.NotNull(data); |
||||
|
|
||||
|
Status = status; |
||||
|
|
||||
|
Data = data; |
||||
|
} |
||||
|
|
||||
|
public ContentVersion WithStatus(Status status) |
||||
|
{ |
||||
|
return new ContentVersion(status, Data); |
||||
|
} |
||||
|
|
||||
|
public ContentVersion WithData(NamedContentData data) |
||||
|
{ |
||||
|
return new ContentVersion(Status, data); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,198 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Diagnostics.CodeAnalysis; |
||||
|
using System.Threading.Tasks; |
||||
|
using Elasticsearch.Net; |
||||
|
using Squidex.Domain.Apps.Entities.Apps; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.Text.Elastic |
||||
|
{ |
||||
|
[ExcludeFromCodeCoverage] |
||||
|
public sealed class ElasticSearchTextIndex : IContentTextIndex |
||||
|
{ |
||||
|
private const string IndexName = "contents"; |
||||
|
private readonly ElasticLowLevelClient client; |
||||
|
|
||||
|
public ElasticSearchTextIndex() |
||||
|
{ |
||||
|
var config = new ConnectionConfiguration(new Uri("http://localhost:9200")); |
||||
|
|
||||
|
client = new ElasticLowLevelClient(config); |
||||
|
} |
||||
|
|
||||
|
public async Task ExecuteAsync(NamedId<Guid> appId, NamedId<Guid> schemaId, params IndexCommand[] commands) |
||||
|
{ |
||||
|
foreach (var command in commands) |
||||
|
{ |
||||
|
switch (command) |
||||
|
{ |
||||
|
case UpsertIndexEntry upsert: |
||||
|
await UpsertAsync(appId, schemaId, upsert); |
||||
|
break; |
||||
|
case UpdateIndexEntry update: |
||||
|
await UpdateAsync(update); |
||||
|
break; |
||||
|
case DeleteIndexEntry delete: |
||||
|
await DeleteAsync(delete); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async Task UpsertAsync(NamedId<Guid> appId, NamedId<Guid> schemaId, UpsertIndexEntry upsert) |
||||
|
{ |
||||
|
upsert.Texts["en"] = "Foo"; |
||||
|
|
||||
|
var data = new |
||||
|
{ |
||||
|
appId = appId.Id, |
||||
|
appName = appId.Name, |
||||
|
contentId = upsert.ContentId, |
||||
|
schemaId = schemaId.Id, |
||||
|
schemaName = schemaId.Name, |
||||
|
serveAll = upsert.ServeAll, |
||||
|
servePublished = upsert.ServePublished, |
||||
|
texts = upsert.Texts, |
||||
|
}; |
||||
|
|
||||
|
var result = await client.IndexAsync<StringResponse>(IndexName, upsert.DocId, CreatePost(data)); |
||||
|
|
||||
|
if (!result.Success) |
||||
|
{ |
||||
|
throw new InvalidOperationException($"Failed with ${result.Body}", result.OriginalException); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async Task UpdateAsync(UpdateIndexEntry update) |
||||
|
{ |
||||
|
var data = new |
||||
|
{ |
||||
|
doc = new |
||||
|
{ |
||||
|
update.ServeAll, |
||||
|
update.ServePublished |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
var result = await client.UpdateAsync<StringResponse>(IndexName, update.DocId, CreatePost(data)); |
||||
|
|
||||
|
if (!result.Success) |
||||
|
{ |
||||
|
throw new InvalidOperationException($"Failed with ${result.Body}", result.OriginalException); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private Task DeleteAsync(DeleteIndexEntry delete) |
||||
|
{ |
||||
|
return client.DeleteAsync<StringResponse>(IndexName, delete.DocId); |
||||
|
} |
||||
|
|
||||
|
private static PostData CreatePost<T>(T data) |
||||
|
{ |
||||
|
return new SerializableData<T>(data); |
||||
|
} |
||||
|
|
||||
|
public async Task<List<Guid>?> SearchAsync(string? queryText, IAppEntity app, SearchFilter? filter, SearchScope scope) |
||||
|
{ |
||||
|
var serveField = GetServeField(scope); |
||||
|
|
||||
|
var query = new |
||||
|
{ |
||||
|
query = new |
||||
|
{ |
||||
|
@bool = new |
||||
|
{ |
||||
|
must = new List<object> |
||||
|
{ |
||||
|
new |
||||
|
{ |
||||
|
term = new Dictionary<string, object> |
||||
|
{ |
||||
|
["appId.keyword"] = app.Id |
||||
|
} |
||||
|
}, |
||||
|
new |
||||
|
{ |
||||
|
term = new Dictionary<string, string> |
||||
|
{ |
||||
|
[serveField] = "true" |
||||
|
} |
||||
|
}, |
||||
|
new |
||||
|
{ |
||||
|
multi_match = new |
||||
|
{ |
||||
|
fields = new[] |
||||
|
{ |
||||
|
"texts.*" |
||||
|
}, |
||||
|
query = queryText |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
should = new List<object>() |
||||
|
} |
||||
|
}, |
||||
|
_source = new[] |
||||
|
{ |
||||
|
"contentId" |
||||
|
}, |
||||
|
size = 2000 |
||||
|
}; |
||||
|
|
||||
|
if (filter?.SchemaIds.Count > 0) |
||||
|
{ |
||||
|
var bySchema = new |
||||
|
{ |
||||
|
term = new Dictionary<string, object> |
||||
|
{ |
||||
|
["schemaId.keyword"] = filter.SchemaIds |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
if (filter.Must) |
||||
|
{ |
||||
|
query.query.@bool.must.Add(bySchema); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
query.query.@bool.should.Add(bySchema); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var result = await client.SearchAsync<DynamicResponse>(IndexName, CreatePost(query)); |
||||
|
|
||||
|
if (!result.Success) |
||||
|
{ |
||||
|
throw result.OriginalException; |
||||
|
} |
||||
|
|
||||
|
var ids = new List<Guid>(); |
||||
|
|
||||
|
foreach (var item in result.Body.hits.hits) |
||||
|
{ |
||||
|
if (item != null) |
||||
|
{ |
||||
|
ids.Add(Guid.Parse(item["_source"]["contentId"])); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return ids; |
||||
|
} |
||||
|
|
||||
|
private static string GetServeField(SearchScope scope) |
||||
|
{ |
||||
|
return scope == SearchScope.Published ? |
||||
|
"servePublished" : |
||||
|
"serveAll"; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,117 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// 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 Orleans; |
|
||||
using Squidex.Domain.Apps.Core.Contents; |
|
||||
using Squidex.Domain.Apps.Entities.Apps; |
|
||||
using Squidex.Domain.Apps.Events.Contents; |
|
||||
using Squidex.Infrastructure; |
|
||||
using Squidex.Infrastructure.EventSourcing; |
|
||||
using Squidex.Infrastructure.Log; |
|
||||
using Squidex.Infrastructure.Tasks; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Entities.Contents.Text |
|
||||
{ |
|
||||
public sealed class GrainTextIndexer : ITextIndexer, IEventConsumer |
|
||||
{ |
|
||||
private readonly IGrainFactory grainFactory; |
|
||||
|
|
||||
public string Name |
|
||||
{ |
|
||||
get { return "TextIndexer2"; } |
|
||||
} |
|
||||
|
|
||||
public string EventsFilter |
|
||||
{ |
|
||||
get { return "^content-"; } |
|
||||
} |
|
||||
|
|
||||
public GrainTextIndexer(IGrainFactory grainFactory) |
|
||||
{ |
|
||||
Guard.NotNull(grainFactory); |
|
||||
|
|
||||
this.grainFactory = grainFactory; |
|
||||
} |
|
||||
|
|
||||
public bool Handles(StoredEvent @event) |
|
||||
{ |
|
||||
return true; |
|
||||
} |
|
||||
|
|
||||
public Task ClearAsync() |
|
||||
{ |
|
||||
return TaskHelper.Done; |
|
||||
} |
|
||||
|
|
||||
public async Task On(Envelope<IEvent> @event) |
|
||||
{ |
|
||||
if (@event.Payload is ContentEvent contentEvent) |
|
||||
{ |
|
||||
var index = grainFactory.GetGrain<ITextIndexerGrain>(contentEvent.SchemaId.Id); |
|
||||
|
|
||||
var id = contentEvent.ContentId; |
|
||||
|
|
||||
switch (@event.Payload) |
|
||||
{ |
|
||||
case ContentDeleted _: |
|
||||
await index.DeleteAsync(id); |
|
||||
break; |
|
||||
case ContentCreated contentCreated: |
|
||||
await index.IndexAsync(Data(id, contentCreated.Data, true)); |
|
||||
break; |
|
||||
case ContentUpdateProposed contentUpdateProposed: |
|
||||
await index.IndexAsync(Data(id, contentUpdateProposed.Data, true)); |
|
||||
break; |
|
||||
case ContentUpdated contentUpdated: |
|
||||
await index.IndexAsync(Data(id, contentUpdated.Data, false)); |
|
||||
break; |
|
||||
case ContentChangesDiscarded _: |
|
||||
await index.CopyAsync(id, false); |
|
||||
break; |
|
||||
case ContentChangesPublished _: |
|
||||
await index.CopyAsync(id, true); |
|
||||
break; |
|
||||
case ContentStatusChanged contentStatusChanged when contentStatusChanged.Status == Status.Published: |
|
||||
await index.CopyAsync(id, true); |
|
||||
break; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private static Update Data(Guid contentId, NamedContentData data, bool onlyDraft) |
|
||||
{ |
|
||||
return new Update { Id = contentId, Text = data.ToTexts(), OnlyDraft = onlyDraft }; |
|
||||
} |
|
||||
|
|
||||
public async Task<List<Guid>?> SearchAsync(string? queryText, IAppEntity app, Guid schemaId, Scope scope = Scope.Published) |
|
||||
{ |
|
||||
if (string.IsNullOrWhiteSpace(queryText)) |
|
||||
{ |
|
||||
return null; |
|
||||
} |
|
||||
|
|
||||
var index = grainFactory.GetGrain<ITextIndexerGrain>(schemaId); |
|
||||
|
|
||||
using (Profiler.TraceMethod<GrainTextIndexer>()) |
|
||||
{ |
|
||||
var context = CreateContext(app, scope); |
|
||||
|
|
||||
return await index.SearchAsync(queryText, context); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private static SearchContext CreateContext(IAppEntity app, Scope scope) |
|
||||
{ |
|
||||
var languages = new HashSet<string>(app.LanguagesConfig.AllKeys); |
|
||||
|
|
||||
return new SearchContext { Languages = languages, Scope = scope }; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,14 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.Text |
||||
|
{ |
||||
|
public abstract class IndexCommand |
||||
|
{ |
||||
|
public string DocId { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -1,114 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Linq; |
|
||||
using Lucene.Net.Documents; |
|
||||
using Lucene.Net.Index; |
|
||||
using Lucene.Net.Search; |
|
||||
using Lucene.Net.Util; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Entities.Contents.Text |
|
||||
{ |
|
||||
internal sealed class IndexState |
|
||||
{ |
|
||||
private const string MetaFor = "_fd"; |
|
||||
private readonly Dictionary<(Guid, Scope), (bool, bool)> lastChanges = new Dictionary<(Guid, Scope), (bool, bool)>(); |
|
||||
private readonly BytesRef bytesRef = new BytesRef(2); |
|
||||
private readonly IIndex index; |
|
||||
|
|
||||
public IndexState(IIndex index) |
|
||||
{ |
|
||||
this.index = index; |
|
||||
} |
|
||||
|
|
||||
public void Index(Guid id, Scope scope, Document document, bool forDraft, bool forPublished) |
|
||||
{ |
|
||||
var value = GetValue(forDraft, forPublished); |
|
||||
|
|
||||
document.SetBinaryDocValue(MetaFor, value); |
|
||||
|
|
||||
lastChanges[(id, scope)] = (forDraft, forPublished); |
|
||||
} |
|
||||
|
|
||||
public void Index(Guid id, Scope scope, Term term, bool forDraft, bool forPublished) |
|
||||
{ |
|
||||
var value = GetValue(forDraft, forPublished); |
|
||||
|
|
||||
index.Writer.UpdateBinaryDocValue(term, MetaFor, value); |
|
||||
|
|
||||
lastChanges[(id, scope)] = (forDraft, forPublished); |
|
||||
} |
|
||||
|
|
||||
public bool HasBeenAdded(Guid id, Scope scope, Term term, out int docId) |
|
||||
{ |
|
||||
docId = -1; |
|
||||
|
|
||||
if (lastChanges.ContainsKey((id, scope))) |
|
||||
{ |
|
||||
return true; |
|
||||
} |
|
||||
|
|
||||
if (index.Searcher == null) |
|
||||
{ |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
var docs = index.Searcher.Search(new TermQuery(term), 1); |
|
||||
|
|
||||
var found = docs.ScoreDocs.FirstOrDefault(); |
|
||||
|
|
||||
if (found != null) |
|
||||
{ |
|
||||
docId = found.Doc; |
|
||||
|
|
||||
return true; |
|
||||
} |
|
||||
|
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
public void Get(Guid id, Scope scope, int docId, out bool forDraft, out bool forPublished) |
|
||||
{ |
|
||||
if (lastChanges.TryGetValue((id, scope), out var forValue)) |
|
||||
{ |
|
||||
(forDraft, forPublished) = forValue; |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
Get(docId, out forDraft, out forPublished); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public void Get(int docId, out bool forDraft, out bool forPublished) |
|
||||
{ |
|
||||
var forValue = GetForValues(docId); |
|
||||
|
|
||||
(forDraft, forPublished) = ToFlags(forValue); |
|
||||
} |
|
||||
|
|
||||
private BytesRef GetForValues(int docId) |
|
||||
{ |
|
||||
return index.Reader.GetBinaryValue(MetaFor, docId, bytesRef); |
|
||||
} |
|
||||
|
|
||||
private static BytesRef GetValue(bool forDraft, bool forPublished) |
|
||||
{ |
|
||||
return new BytesRef(new[] |
|
||||
{ |
|
||||
(byte)(forDraft ? 1 : 0), |
|
||||
(byte)(forPublished ? 1 : 0) |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
private static (bool, bool) ToFlags(BytesRef bytes) |
|
||||
{ |
|
||||
return (bytes.Bytes[0] == 1, bytes.Bytes[1] == 1); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,53 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using Lucene.Net.Index; |
||||
|
using Lucene.Net.Util; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.Text.Lucene |
||||
|
{ |
||||
|
public static class LuceneExtensions |
||||
|
{ |
||||
|
public static BytesRef GetBinaryValue(this IndexReader? reader, string field, int docId, BytesRef? result = null) |
||||
|
{ |
||||
|
if (result != null) |
||||
|
{ |
||||
|
Array.Clear(result.Bytes, 0, result.Bytes.Length); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
result = new BytesRef(); |
||||
|
} |
||||
|
|
||||
|
if (reader == null || docId < 0) |
||||
|
{ |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
var leaves = reader.Leaves; |
||||
|
|
||||
|
if (leaves.Count == 1) |
||||
|
{ |
||||
|
var docValues = leaves[0].AtomicReader.GetBinaryDocValues(field); |
||||
|
|
||||
|
docValues.Get(docId, result); |
||||
|
} |
||||
|
else if (leaves.Count > 1) |
||||
|
{ |
||||
|
var subIndex = ReaderUtil.SubIndex(docId, leaves); |
||||
|
|
||||
|
var subLeave = leaves[subIndex]; |
||||
|
var subValues = subLeave.AtomicReader.GetBinaryDocValues(field); |
||||
|
|
||||
|
subValues.Get(docId - subLeave.DocBase, result); |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,61 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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 Orleans; |
||||
|
using Orleans.Concurrency; |
||||
|
using Squidex.Domain.Apps.Entities.Apps; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Log; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.Text.Lucene |
||||
|
{ |
||||
|
public sealed class LuceneTextIndex : IContentTextIndex |
||||
|
{ |
||||
|
private readonly IGrainFactory grainFactory; |
||||
|
|
||||
|
public LuceneTextIndex(IGrainFactory grainFactory) |
||||
|
{ |
||||
|
Guard.NotNull(grainFactory); |
||||
|
|
||||
|
this.grainFactory = grainFactory; |
||||
|
} |
||||
|
|
||||
|
public async Task<List<Guid>?> SearchAsync(string? queryText, IAppEntity app, SearchFilter? filter, SearchScope scope) |
||||
|
{ |
||||
|
if (string.IsNullOrWhiteSpace(queryText)) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
var index = grainFactory.GetGrain<ILuceneTextIndexGrain>(app.Id); |
||||
|
|
||||
|
using (Profiler.TraceMethod<LuceneTextIndex>()) |
||||
|
{ |
||||
|
var context = CreateContext(app, scope); |
||||
|
|
||||
|
return await index.SearchAsync(queryText, filter, context); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static SearchContext CreateContext(IAppEntity app, SearchScope scope) |
||||
|
{ |
||||
|
var languages = new HashSet<string>(app.LanguagesConfig.AllKeys); |
||||
|
|
||||
|
return new SearchContext { Languages = languages, Scope = scope }; |
||||
|
} |
||||
|
|
||||
|
public Task ExecuteAsync(NamedId<Guid> appId, NamedId<Guid> schemaId, params IndexCommand[] commands) |
||||
|
{ |
||||
|
var index = grainFactory.GetGrain<ILuceneTextIndexGrain>(appId.Id); |
||||
|
|
||||
|
return index.IndexAsync(schemaId, commands.AsImmutable()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,253 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Lucene.Net.Documents; |
||||
|
using Lucene.Net.Index; |
||||
|
using Lucene.Net.QueryParsers.Classic; |
||||
|
using Lucene.Net.Search; |
||||
|
using Lucene.Net.Util; |
||||
|
using Orleans.Concurrency; |
||||
|
using Squidex.Domain.Apps.Core; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Orleans; |
||||
|
using Squidex.Infrastructure.Validation; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.Text.Lucene |
||||
|
{ |
||||
|
public sealed class LuceneTextIndexGrain : GrainOfGuid, ILuceneTextIndexGrain |
||||
|
{ |
||||
|
private const LuceneVersion Version = LuceneVersion.LUCENE_48; |
||||
|
private const int MaxResults = 2000; |
||||
|
private const int MaxUpdates = 400; |
||||
|
private const string MetaId = "_id"; |
||||
|
private const string MetaFor = "_fd"; |
||||
|
private const string MetaContentId = "_cid"; |
||||
|
private const string MetaSchemaId = "_si"; |
||||
|
private const string MetaSchemaName = "_sn"; |
||||
|
private static readonly TimeSpan CommitDelay = TimeSpan.FromSeconds(10); |
||||
|
private static readonly string[] Invariant = { InvariantPartitioning.Key }; |
||||
|
private readonly IndexManager indexManager; |
||||
|
private IDisposable? timer; |
||||
|
private IIndex index; |
||||
|
private QueryParser? queryParser; |
||||
|
private HashSet<string>? currentLanguages; |
||||
|
private int updates; |
||||
|
|
||||
|
public LuceneTextIndexGrain(IndexManager indexManager) |
||||
|
{ |
||||
|
Guard.NotNull(indexManager); |
||||
|
|
||||
|
this.indexManager = indexManager; |
||||
|
} |
||||
|
|
||||
|
public override async Task OnDeactivateAsync() |
||||
|
{ |
||||
|
if (index != null) |
||||
|
{ |
||||
|
await CommitAsync(); |
||||
|
|
||||
|
await indexManager.ReleaseAsync(index); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected override async Task OnActivateAsync(Guid key) |
||||
|
{ |
||||
|
index = await indexManager.AcquireAsync(key); |
||||
|
} |
||||
|
|
||||
|
public Task<List<Guid>> SearchAsync(string queryText, SearchFilter? filter, SearchContext context) |
||||
|
{ |
||||
|
var result = new List<Guid>(); |
||||
|
|
||||
|
if (!string.IsNullOrWhiteSpace(queryText)) |
||||
|
{ |
||||
|
index.EnsureReader(); |
||||
|
|
||||
|
if (index.Searcher != null) |
||||
|
{ |
||||
|
var query = BuildQuery(queryText, filter, context); |
||||
|
|
||||
|
var hits = index.Searcher.Search(query, MaxResults).ScoreDocs; |
||||
|
|
||||
|
if (hits.Length > 0) |
||||
|
{ |
||||
|
var buffer = new BytesRef(2); |
||||
|
|
||||
|
var found = new HashSet<Guid>(); |
||||
|
|
||||
|
foreach (var hit in hits) |
||||
|
{ |
||||
|
var forValue = index.Reader.GetBinaryValue(MetaFor, hit.Doc, buffer); |
||||
|
|
||||
|
if (context.Scope == SearchScope.All && forValue.Bytes[0] != 1) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
if (context.Scope == SearchScope.Published && forValue.Bytes[1] != 1) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
var document = index.Searcher.Doc(hit.Doc); |
||||
|
|
||||
|
if (document != null) |
||||
|
{ |
||||
|
var idString = document.Get(MetaContentId); |
||||
|
|
||||
|
if (Guid.TryParse(idString, out var id)) |
||||
|
{ |
||||
|
if (found.Add(id)) |
||||
|
{ |
||||
|
result.Add(id); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return Task.FromResult(result); |
||||
|
} |
||||
|
|
||||
|
private Query BuildQuery(string query, SearchFilter? filter, SearchContext context) |
||||
|
{ |
||||
|
if (queryParser == null || currentLanguages == null || !currentLanguages.SetEquals(context.Languages)) |
||||
|
{ |
||||
|
var fields = context.Languages.Union(Invariant).ToArray(); |
||||
|
|
||||
|
queryParser = new MultiFieldQueryParser(Version, fields, index.Analyzer); |
||||
|
|
||||
|
currentLanguages = context.Languages; |
||||
|
} |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
var byQuery = queryParser.Parse(query); |
||||
|
|
||||
|
if (filter?.SchemaIds.Count > 0) |
||||
|
{ |
||||
|
var bySchemas = new BooleanQuery |
||||
|
{ |
||||
|
Boost = 2f |
||||
|
}; |
||||
|
|
||||
|
foreach (var schemaId in filter.SchemaIds) |
||||
|
{ |
||||
|
var term = new Term(MetaSchemaId, schemaId.ToString()); |
||||
|
|
||||
|
bySchemas.Add(new TermQuery(term), Occur.SHOULD); |
||||
|
} |
||||
|
|
||||
|
var occur = filter.Must ? Occur.MUST : Occur.SHOULD; |
||||
|
|
||||
|
return new BooleanQuery |
||||
|
{ |
||||
|
{ byQuery, Occur.MUST }, |
||||
|
{ bySchemas, occur } |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
return byQuery; |
||||
|
} |
||||
|
catch (ParseException ex) |
||||
|
{ |
||||
|
throw new ValidationException(ex.Message); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async Task<bool> TryCommitAsync() |
||||
|
{ |
||||
|
timer?.Dispose(); |
||||
|
|
||||
|
updates++; |
||||
|
|
||||
|
if (updates >= MaxUpdates) |
||||
|
{ |
||||
|
await CommitAsync(); |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
index.MarkStale(); |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
timer = RegisterTimer(_ => CommitAsync(), null, CommitDelay, CommitDelay); |
||||
|
} |
||||
|
catch (InvalidOperationException) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
public async Task CommitAsync() |
||||
|
{ |
||||
|
if (updates > 0) |
||||
|
{ |
||||
|
await indexManager.CommitAsync(index); |
||||
|
|
||||
|
updates = 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public Task IndexAsync(NamedId<Guid> schemaId, Immutable<IndexCommand[]> updates) |
||||
|
{ |
||||
|
foreach (var command in updates.Value) |
||||
|
{ |
||||
|
switch (command) |
||||
|
{ |
||||
|
case DeleteIndexEntry delete: |
||||
|
index.Writer.DeleteDocuments(new Term(MetaId, delete.DocId)); |
||||
|
break; |
||||
|
case UpdateIndexEntry update: |
||||
|
index.Writer.UpdateBinaryDocValue(new Term(MetaId, update.DocId), MetaFor, GetValue(update.ServeAll, update.ServePublished)); |
||||
|
break; |
||||
|
case UpsertIndexEntry upsert: |
||||
|
{ |
||||
|
var document = new Document(); |
||||
|
|
||||
|
document.AddStringField(MetaId, upsert.DocId, Field.Store.YES); |
||||
|
document.AddStringField(MetaContentId, upsert.ContentId.ToString(), Field.Store.YES); |
||||
|
document.AddStringField(MetaSchemaId, schemaId.Id.ToString(), Field.Store.YES); |
||||
|
document.AddStringField(MetaSchemaName, schemaId.Name, Field.Store.YES); |
||||
|
document.AddBinaryDocValuesField(MetaFor, GetValue(upsert.ServeAll, upsert.ServePublished)); |
||||
|
|
||||
|
foreach (var (key, value) in upsert.Texts) |
||||
|
{ |
||||
|
document.AddTextField(key, value, Field.Store.NO); |
||||
|
} |
||||
|
|
||||
|
index.Writer.UpdateDocument(new Term(MetaId, upsert.DocId), document); |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return TryCommitAsync(); |
||||
|
} |
||||
|
|
||||
|
private static BytesRef GetValue(bool forDraft, bool forPublished) |
||||
|
{ |
||||
|
return new BytesRef(new[] |
||||
|
{ |
||||
|
(byte)(forDraft ? 1 : 0), |
||||
|
(byte)(forPublished ? 1 : 0) |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,46 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.Text |
||||
|
{ |
||||
|
[Equals(DoNotAddEqualityOperators = true)] |
||||
|
public sealed class SearchFilter |
||||
|
{ |
||||
|
public IReadOnlyList<Guid> SchemaIds { get; } |
||||
|
|
||||
|
public bool Must { get; } |
||||
|
|
||||
|
public SearchFilter(IReadOnlyList<Guid> schemaIds, bool must) |
||||
|
{ |
||||
|
Guard.NotNull(schemaIds); |
||||
|
|
||||
|
SchemaIds = schemaIds; |
||||
|
|
||||
|
Must = must; |
||||
|
} |
||||
|
|
||||
|
public static SearchFilter MustHaveSchemas(List<Guid> schemaIds) |
||||
|
{ |
||||
|
return new SearchFilter(schemaIds, true); |
||||
|
} |
||||
|
|
||||
|
public static SearchFilter MustHaveSchemas(params Guid[] schemaIds) |
||||
|
{ |
||||
|
return new SearchFilter(schemaIds?.ToList()!, true); |
||||
|
} |
||||
|
|
||||
|
public static SearchFilter ShouldHaveSchemas(params Guid[] schemaIds) |
||||
|
{ |
||||
|
return new SearchFilter(schemaIds?.ToList()!, false); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,64 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Caching; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.Text.State |
||||
|
{ |
||||
|
public sealed class CachingTextIndexerState : ITextIndexerState |
||||
|
{ |
||||
|
private readonly ITextIndexerState inner; |
||||
|
private LRUCache<Guid, Tuple<TextContentState?>> cache = new LRUCache<Guid, Tuple<TextContentState?>>(1000); |
||||
|
|
||||
|
public CachingTextIndexerState(ITextIndexerState inner) |
||||
|
{ |
||||
|
Guard.NotNull(inner); |
||||
|
|
||||
|
this.inner = inner; |
||||
|
} |
||||
|
|
||||
|
public async Task ClearAsync() |
||||
|
{ |
||||
|
await inner.ClearAsync(); |
||||
|
|
||||
|
cache = new LRUCache<Guid, Tuple<TextContentState?>>(1000); |
||||
|
} |
||||
|
|
||||
|
public async Task<TextContentState?> GetAsync(Guid contentId) |
||||
|
{ |
||||
|
if (cache.TryGetValue(contentId, out var value)) |
||||
|
{ |
||||
|
return value.Item1; |
||||
|
} |
||||
|
|
||||
|
var result = await inner.GetAsync(contentId); |
||||
|
|
||||
|
cache.Set(contentId, Tuple.Create(result)); |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
public Task SetAsync(TextContentState state) |
||||
|
{ |
||||
|
Guard.NotNull(state); |
||||
|
|
||||
|
cache.Set(state.ContentId, Tuple.Create<TextContentState?>(state)); |
||||
|
|
||||
|
return inner.SetAsync(state); |
||||
|
} |
||||
|
|
||||
|
public Task RemoveAsync(Guid contentId) |
||||
|
{ |
||||
|
cache.Set(contentId, Tuple.Create<TextContentState?>(null)); |
||||
|
|
||||
|
return inner.RemoveAsync(contentId); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.Text.State |
||||
|
{ |
||||
|
public interface ITextIndexerState |
||||
|
{ |
||||
|
Task<TextContentState?> GetAsync(Guid contentId); |
||||
|
|
||||
|
Task SetAsync(TextContentState state); |
||||
|
|
||||
|
Task RemoveAsync(Guid contentId); |
||||
|
|
||||
|
Task ClearAsync(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,50 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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 Squidex.Infrastructure.Tasks; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.Text.State |
||||
|
{ |
||||
|
public sealed class InMemoryTextIndexerState : ITextIndexerState |
||||
|
{ |
||||
|
private readonly Dictionary<Guid, TextContentState> states = new Dictionary<Guid, TextContentState>(); |
||||
|
|
||||
|
public Task ClearAsync() |
||||
|
{ |
||||
|
states.Clear(); |
||||
|
|
||||
|
return TaskHelper.Done; |
||||
|
} |
||||
|
|
||||
|
public Task<TextContentState?> GetAsync(Guid contentId) |
||||
|
{ |
||||
|
if (states.TryGetValue(contentId, out var result)) |
||||
|
{ |
||||
|
return Task.FromResult<TextContentState?>(result); |
||||
|
} |
||||
|
|
||||
|
return Task.FromResult<TextContentState?>(null); |
||||
|
} |
||||
|
|
||||
|
public Task RemoveAsync(Guid contentId) |
||||
|
{ |
||||
|
states.Remove(contentId); |
||||
|
|
||||
|
return TaskHelper.Done; |
||||
|
} |
||||
|
|
||||
|
public Task SetAsync(TextContentState state) |
||||
|
{ |
||||
|
states[state.ContentId] = state; |
||||
|
|
||||
|
return TaskHelper.Done; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,46 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.Text.State |
||||
|
{ |
||||
|
public sealed class TextContentState |
||||
|
{ |
||||
|
public Guid ContentId { get; set; } |
||||
|
|
||||
|
public string DocIdCurrent { get; set; } |
||||
|
|
||||
|
public string? DocIdNew { get; set; } |
||||
|
|
||||
|
public string? DocIdForPublished { get; set; } |
||||
|
|
||||
|
public void GenerateDocIdNew() |
||||
|
{ |
||||
|
if (DocIdCurrent?.EndsWith("_2") != false) |
||||
|
{ |
||||
|
DocIdNew = $"{ContentId}_1"; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
DocIdNew = $"{ContentId}_2"; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void GenerateDocIdCurrent() |
||||
|
{ |
||||
|
if (DocIdNew?.EndsWith("_2") != false) |
||||
|
{ |
||||
|
DocIdCurrent = $"{ContentId}_1"; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
DocIdCurrent = $"{ContentId}_2"; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,172 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using Lucene.Net.Documents; |
|
||||
using Lucene.Net.Index; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Entities.Contents.Text |
|
||||
{ |
|
||||
internal sealed class TextIndexContent |
|
||||
{ |
|
||||
private const string MetaId = "_id"; |
|
||||
private const string MetaKey = "_key"; |
|
||||
private readonly IIndex index; |
|
||||
private readonly IndexState indexState; |
|
||||
private readonly Guid id; |
|
||||
|
|
||||
public TextIndexContent(IIndex index, IndexState indexState, Guid id) |
|
||||
{ |
|
||||
this.index = index; |
|
||||
this.indexState = indexState; |
|
||||
|
|
||||
this.id = id; |
|
||||
} |
|
||||
|
|
||||
public void Delete() |
|
||||
{ |
|
||||
index.Writer.DeleteDocuments(new Term(MetaId, id.ToString())); |
|
||||
} |
|
||||
|
|
||||
public static bool TryGetId(int docId, Scope scope, IIndex index, IndexState indexState, out Guid result) |
|
||||
{ |
|
||||
result = Guid.Empty; |
|
||||
|
|
||||
indexState.Get(docId, out var draft, out var published); |
|
||||
|
|
||||
if (scope == Scope.Draft && !draft) |
|
||||
{ |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
if (scope == Scope.Published && !published) |
|
||||
{ |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
var document = index.Searcher?.Doc(docId); |
|
||||
|
|
||||
if (document != null) |
|
||||
{ |
|
||||
var idString = document.Get(MetaId); |
|
||||
|
|
||||
if (!Guid.TryParse(idString, out result)) |
|
||||
{ |
|
||||
return false; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return true; |
|
||||
} |
|
||||
|
|
||||
public void Index(Dictionary<string, string> text, bool onlyDraft) |
|
||||
{ |
|
||||
var converted = CreateDocument(text); |
|
||||
|
|
||||
Upsert(converted, Scope.Draft, |
|
||||
forDraft: true, |
|
||||
forPublished: false); |
|
||||
|
|
||||
var isPublishDocumentAdded = IsAdded(Scope.Published, out var docId); |
|
||||
var isPublishForPublished = IsForPublished(Scope.Published, docId); |
|
||||
|
|
||||
if (!onlyDraft && isPublishDocumentAdded && isPublishForPublished) |
|
||||
{ |
|
||||
Upsert(converted, Scope.Published, |
|
||||
forDraft: false, |
|
||||
forPublished: true); |
|
||||
} |
|
||||
else if (!onlyDraft || !isPublishDocumentAdded) |
|
||||
{ |
|
||||
Upsert(converted, Scope.Published, |
|
||||
forDraft: false, |
|
||||
forPublished: false); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
UpdateFor(Scope.Published, |
|
||||
forDraft: false, |
|
||||
forPublished: isPublishForPublished); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public void Copy(bool fromDraft) |
|
||||
{ |
|
||||
if (fromDraft) |
|
||||
{ |
|
||||
UpdateFor(Scope.Draft, |
|
||||
forDraft: true, |
|
||||
forPublished: false); |
|
||||
|
|
||||
UpdateFor(Scope.Published, |
|
||||
forDraft: false, |
|
||||
forPublished: true); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
UpdateFor(Scope.Draft, |
|
||||
forDraft: false, |
|
||||
forPublished: false); |
|
||||
|
|
||||
UpdateFor(Scope.Published, |
|
||||
forDraft: true, |
|
||||
forPublished: true); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private static Document CreateDocument(Dictionary<string, string> text) |
|
||||
{ |
|
||||
var document = new Document(); |
|
||||
|
|
||||
foreach (var (key, value) in text) |
|
||||
{ |
|
||||
document.AddTextField(key, value, Field.Store.NO); |
|
||||
} |
|
||||
|
|
||||
return document; |
|
||||
} |
|
||||
|
|
||||
private void UpdateFor(Scope scope, bool forDraft, bool forPublished) |
|
||||
{ |
|
||||
var term = new Term(MetaKey, BuildKey(scope)); |
|
||||
|
|
||||
indexState.Index(id, scope, term, forDraft, forPublished); |
|
||||
} |
|
||||
|
|
||||
private void Upsert(Document document, Scope draft, bool forDraft, bool forPublished) |
|
||||
{ |
|
||||
var contentKey = BuildKey(draft); |
|
||||
|
|
||||
document.SetField(MetaId, id.ToString()); |
|
||||
document.SetField(MetaKey, contentKey); |
|
||||
|
|
||||
indexState.Index(id, draft, document, forDraft, forPublished); |
|
||||
|
|
||||
index.Writer.UpdateDocument(new Term(MetaKey, contentKey), document); |
|
||||
} |
|
||||
|
|
||||
private bool IsAdded(Scope scope, out int docId) |
|
||||
{ |
|
||||
var term = new Term(MetaKey, BuildKey(scope)); |
|
||||
|
|
||||
return indexState.HasBeenAdded(id, scope, term, out docId); |
|
||||
} |
|
||||
|
|
||||
private bool IsForPublished(Scope scope, int docId) |
|
||||
{ |
|
||||
indexState.Get(id, scope, docId, out _, out var forPublished); |
|
||||
|
|
||||
return forPublished; |
|
||||
} |
|
||||
|
|
||||
private string BuildKey(Scope scope) |
|
||||
{ |
|
||||
return $"{id}_{(scope == Scope.Draft ? 1 : 0)}"; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,181 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Linq; |
|
||||
using System.Threading.Tasks; |
|
||||
using Lucene.Net.QueryParsers.Classic; |
|
||||
using Lucene.Net.Search; |
|
||||
using Lucene.Net.Util; |
|
||||
using Squidex.Domain.Apps.Core; |
|
||||
using Squidex.Infrastructure; |
|
||||
using Squidex.Infrastructure.Orleans; |
|
||||
using Squidex.Infrastructure.Validation; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Entities.Contents.Text |
|
||||
{ |
|
||||
public sealed class TextIndexerGrain : GrainOfGuid, ITextIndexerGrain |
|
||||
{ |
|
||||
private const LuceneVersion Version = LuceneVersion.LUCENE_48; |
|
||||
private const int MaxResults = 2000; |
|
||||
private const int MaxUpdates = 400; |
|
||||
private static readonly TimeSpan CommitDelay = TimeSpan.FromSeconds(10); |
|
||||
private static readonly string[] Invariant = { InvariantPartitioning.Key }; |
|
||||
private readonly IndexManager indexManager; |
|
||||
private IDisposable? timer; |
|
||||
private IIndex index; |
|
||||
private IndexState indexState; |
|
||||
private QueryParser? queryParser; |
|
||||
private HashSet<string>? currentLanguages; |
|
||||
private int updates; |
|
||||
|
|
||||
public TextIndexerGrain(IndexManager indexManager) |
|
||||
{ |
|
||||
Guard.NotNull(indexManager); |
|
||||
|
|
||||
this.indexManager = indexManager; |
|
||||
} |
|
||||
|
|
||||
public override async Task OnDeactivateAsync() |
|
||||
{ |
|
||||
if (index != null) |
|
||||
{ |
|
||||
await indexManager.ReleaseAsync(index); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
protected override async Task OnActivateAsync(Guid key) |
|
||||
{ |
|
||||
index = await indexManager.AcquireAsync(key); |
|
||||
|
|
||||
indexState = new IndexState(index); |
|
||||
} |
|
||||
|
|
||||
public Task<bool> IndexAsync(Update update) |
|
||||
{ |
|
||||
var content = new TextIndexContent(index, indexState, update.Id); |
|
||||
|
|
||||
content.Index(update.Text, update.OnlyDraft); |
|
||||
|
|
||||
return TryCommitAsync(); |
|
||||
} |
|
||||
|
|
||||
public Task<bool> CopyAsync(Guid id, bool fromDraft) |
|
||||
{ |
|
||||
var content = new TextIndexContent(index, indexState, id); |
|
||||
|
|
||||
content.Copy(fromDraft); |
|
||||
|
|
||||
return TryCommitAsync(); |
|
||||
} |
|
||||
|
|
||||
public Task<bool> DeleteAsync(Guid id) |
|
||||
{ |
|
||||
var content = new TextIndexContent(index, indexState, id); |
|
||||
|
|
||||
content.Delete(); |
|
||||
|
|
||||
return TryCommitAsync(); |
|
||||
} |
|
||||
|
|
||||
public Task<List<Guid>> SearchAsync(string queryText, SearchContext context) |
|
||||
{ |
|
||||
var result = new List<Guid>(); |
|
||||
|
|
||||
if (!string.IsNullOrWhiteSpace(queryText)) |
|
||||
{ |
|
||||
index.EnsureReader(); |
|
||||
|
|
||||
if (index.Searcher != null) |
|
||||
{ |
|
||||
var query = BuildQuery(queryText, context); |
|
||||
|
|
||||
var hits = index.Searcher.Search(query, MaxResults).ScoreDocs; |
|
||||
|
|
||||
if (hits.Length > 0) |
|
||||
{ |
|
||||
var found = new HashSet<Guid>(); |
|
||||
|
|
||||
foreach (var hit in hits) |
|
||||
{ |
|
||||
if (TextIndexContent.TryGetId(hit.Doc, context.Scope, index, indexState, out var id)) |
|
||||
{ |
|
||||
if (found.Add(id)) |
|
||||
{ |
|
||||
result.Add(id); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return Task.FromResult(result); |
|
||||
} |
|
||||
|
|
||||
private Query BuildQuery(string query, SearchContext context) |
|
||||
{ |
|
||||
if (queryParser == null || currentLanguages == null || !currentLanguages.SetEquals(context.Languages)) |
|
||||
{ |
|
||||
var fields = context.Languages.Union(Invariant).ToArray(); |
|
||||
|
|
||||
queryParser = new MultiFieldQueryParser(Version, fields, index.Analyzer); |
|
||||
|
|
||||
currentLanguages = context.Languages; |
|
||||
} |
|
||||
|
|
||||
try |
|
||||
{ |
|
||||
return queryParser.Parse(query); |
|
||||
} |
|
||||
catch (ParseException ex) |
|
||||
{ |
|
||||
throw new ValidationException(ex.Message); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private async Task<bool> TryCommitAsync() |
|
||||
{ |
|
||||
timer?.Dispose(); |
|
||||
|
|
||||
updates++; |
|
||||
|
|
||||
if (updates >= MaxUpdates) |
|
||||
{ |
|
||||
await CommitAsync(); |
|
||||
|
|
||||
return true; |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
index.MarkStale(); |
|
||||
|
|
||||
try |
|
||||
{ |
|
||||
timer = RegisterTimer(_ => CommitAsync(), null, CommitDelay, CommitDelay); |
|
||||
} |
|
||||
catch (InvalidOperationException) |
|
||||
{ |
|
||||
return false; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
public async Task CommitAsync() |
|
||||
{ |
|
||||
if (updates > 0) |
|
||||
{ |
|
||||
await indexManager.CommitAsync(index); |
|
||||
|
|
||||
updates = 0; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,277 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Domain.Apps.Entities.Contents.Text.State; |
||||
|
using Squidex.Domain.Apps.Events.Contents; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.Text |
||||
|
{ |
||||
|
public sealed class TextIndexingProcess : IEventConsumer |
||||
|
{ |
||||
|
private const string NotFound = "<404>"; |
||||
|
private readonly IContentTextIndex textIndexer; |
||||
|
private readonly ITextIndexerState textIndexerState; |
||||
|
|
||||
|
public string Name |
||||
|
{ |
||||
|
get { return "TextIndexer2"; } |
||||
|
} |
||||
|
|
||||
|
public string EventsFilter |
||||
|
{ |
||||
|
get { return "^content-"; } |
||||
|
} |
||||
|
|
||||
|
public IContentTextIndex TextIndexer |
||||
|
{ |
||||
|
get { return textIndexer; } |
||||
|
} |
||||
|
|
||||
|
public TextIndexingProcess(IContentTextIndex textIndexer, ITextIndexerState textIndexerState) |
||||
|
{ |
||||
|
Guard.NotNull(textIndexer); |
||||
|
Guard.NotNull(textIndexerState); |
||||
|
|
||||
|
this.textIndexer = textIndexer; |
||||
|
this.textIndexerState = textIndexerState; |
||||
|
} |
||||
|
|
||||
|
public bool Handles(StoredEvent @event) |
||||
|
{ |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
public Task ClearAsync() |
||||
|
{ |
||||
|
return textIndexerState.ClearAsync(); |
||||
|
} |
||||
|
|
||||
|
public async Task On(Envelope<IEvent> @event) |
||||
|
{ |
||||
|
switch (@event.Payload) |
||||
|
{ |
||||
|
case ContentCreated created: |
||||
|
await CreateAsync(created); |
||||
|
break; |
||||
|
case ContentUpdated updated: |
||||
|
await UpdateAsync(updated); |
||||
|
break; |
||||
|
case ContentStatusChanged statusChanged when statusChanged.Status == Status.Published: |
||||
|
await PublishAsync(statusChanged); |
||||
|
break; |
||||
|
case ContentStatusChanged statusChanged: |
||||
|
await UnpublishAsync(statusChanged); |
||||
|
break; |
||||
|
case ContentDraftCreated draftCreated: |
||||
|
await CreateDraftAsync(draftCreated); |
||||
|
break; |
||||
|
case ContentDraftDeleted draftDelted: |
||||
|
await DeleteDraftAsync(draftDelted); |
||||
|
break; |
||||
|
case ContentDeleted deleted: |
||||
|
await DeleteAsync(deleted); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async Task CreateAsync(ContentCreated @event) |
||||
|
{ |
||||
|
var state = new TextContentState |
||||
|
{ |
||||
|
ContentId = @event.ContentId |
||||
|
}; |
||||
|
|
||||
|
state.GenerateDocIdCurrent(); |
||||
|
|
||||
|
await IndexAsync(@event, |
||||
|
new UpsertIndexEntry |
||||
|
{ |
||||
|
ContentId = @event.ContentId, |
||||
|
DocId = state.DocIdCurrent, |
||||
|
ServeAll = true, |
||||
|
ServePublished = false, |
||||
|
Texts = @event.Data.ToTexts(), |
||||
|
}); |
||||
|
|
||||
|
await textIndexerState.SetAsync(state); |
||||
|
} |
||||
|
|
||||
|
private async Task CreateDraftAsync(ContentDraftCreated @event) |
||||
|
{ |
||||
|
var state = await textIndexerState.GetAsync(@event.ContentId); |
||||
|
|
||||
|
if (state != null) |
||||
|
{ |
||||
|
state.GenerateDocIdNew(); |
||||
|
|
||||
|
await textIndexerState.SetAsync(state); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async Task UpdateAsync(ContentUpdated @event) |
||||
|
{ |
||||
|
var state = await textIndexerState.GetAsync(@event.ContentId); |
||||
|
|
||||
|
if (state != null) |
||||
|
{ |
||||
|
if (state.DocIdNew != null) |
||||
|
{ |
||||
|
await IndexAsync(@event, |
||||
|
new UpsertIndexEntry |
||||
|
{ |
||||
|
ContentId = @event.ContentId, |
||||
|
DocId = state.DocIdNew, |
||||
|
ServeAll = true, |
||||
|
ServePublished = false, |
||||
|
Texts = @event.Data.ToTexts() |
||||
|
}, |
||||
|
new UpdateIndexEntry |
||||
|
{ |
||||
|
DocId = state.DocIdCurrent, |
||||
|
ServeAll = false, |
||||
|
ServePublished = true |
||||
|
}); |
||||
|
|
||||
|
state.DocIdForPublished = state.DocIdCurrent; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
var isPublished = state.DocIdCurrent == state.DocIdForPublished; |
||||
|
|
||||
|
await IndexAsync(@event, |
||||
|
new UpsertIndexEntry |
||||
|
{ |
||||
|
ContentId = @event.ContentId, |
||||
|
DocId = state.DocIdCurrent, |
||||
|
ServeAll = true, |
||||
|
ServePublished = isPublished, |
||||
|
Texts = @event.Data.ToTexts() |
||||
|
}); |
||||
|
|
||||
|
state.DocIdForPublished = state.DocIdNew; |
||||
|
} |
||||
|
|
||||
|
await textIndexerState.SetAsync(state); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async Task UnpublishAsync(ContentStatusChanged @event) |
||||
|
{ |
||||
|
var state = await textIndexerState.GetAsync(@event.ContentId); |
||||
|
|
||||
|
if (state != null && state.DocIdForPublished != null) |
||||
|
{ |
||||
|
await IndexAsync(@event, |
||||
|
new UpdateIndexEntry |
||||
|
{ |
||||
|
DocId = state.DocIdForPublished, |
||||
|
ServeAll = true, |
||||
|
ServePublished = false |
||||
|
}); |
||||
|
|
||||
|
state.DocIdForPublished = null; |
||||
|
|
||||
|
await textIndexerState.SetAsync(state); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async Task PublishAsync(ContentStatusChanged @event) |
||||
|
{ |
||||
|
var state = await textIndexerState.GetAsync(@event.ContentId); |
||||
|
|
||||
|
if (state != null) |
||||
|
{ |
||||
|
if (state.DocIdNew != null) |
||||
|
{ |
||||
|
await IndexAsync(@event, |
||||
|
new UpdateIndexEntry |
||||
|
{ |
||||
|
DocId = state.DocIdNew, |
||||
|
ServeAll = true, |
||||
|
ServePublished = true |
||||
|
}, |
||||
|
new DeleteIndexEntry |
||||
|
{ |
||||
|
DocId = state.DocIdCurrent |
||||
|
}); |
||||
|
|
||||
|
state.DocIdForPublished = state.DocIdNew; |
||||
|
state.DocIdCurrent = state.DocIdNew; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
await IndexAsync(@event, |
||||
|
new UpdateIndexEntry |
||||
|
{ |
||||
|
DocId = state.DocIdCurrent, |
||||
|
ServeAll = true, |
||||
|
ServePublished = true |
||||
|
}); |
||||
|
|
||||
|
state.DocIdForPublished = state.DocIdCurrent; |
||||
|
} |
||||
|
|
||||
|
state.DocIdNew = null; |
||||
|
|
||||
|
await textIndexerState.SetAsync(state); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async Task DeleteDraftAsync(ContentDraftDeleted @event) |
||||
|
{ |
||||
|
var state = await textIndexerState.GetAsync(@event.ContentId); |
||||
|
|
||||
|
if (state != null && state.DocIdNew != null) |
||||
|
{ |
||||
|
await IndexAsync(@event, |
||||
|
new UpdateIndexEntry |
||||
|
{ |
||||
|
DocId = state.DocIdCurrent, |
||||
|
ServeAll = true, |
||||
|
ServePublished = true |
||||
|
}, |
||||
|
new DeleteIndexEntry |
||||
|
{ |
||||
|
DocId = state.DocIdNew, |
||||
|
}); |
||||
|
|
||||
|
state.DocIdNew = null; |
||||
|
|
||||
|
await textIndexerState.SetAsync(state); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async Task DeleteAsync(ContentDeleted @event) |
||||
|
{ |
||||
|
var state = await textIndexerState.GetAsync(@event.ContentId); |
||||
|
|
||||
|
if (state != null) |
||||
|
{ |
||||
|
await IndexAsync(@event, |
||||
|
new DeleteIndexEntry |
||||
|
{ |
||||
|
DocId = state.DocIdCurrent |
||||
|
}, |
||||
|
new DeleteIndexEntry |
||||
|
{ |
||||
|
DocId = state.DocIdNew ?? NotFound, |
||||
|
}); |
||||
|
|
||||
|
await textIndexerState.RemoveAsync(state.ContentId); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private Task IndexAsync(ContentEvent @event, params IndexCommand[] commands) |
||||
|
{ |
||||
|
return textIndexer.ExecuteAsync(@event.AppId, @event.SchemaId, commands); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.Text |
||||
|
{ |
||||
|
public sealed class UpdateIndexEntry : IndexCommand |
||||
|
{ |
||||
|
public bool ServeAll { get; set; } |
||||
|
|
||||
|
public bool ServePublished { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
||||
|
<Equals /> |
||||
|
</Weavers> |
||||
@ -0,0 +1,26 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
||||
|
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
||||
|
<xs:element name="Weavers"> |
||||
|
<xs:complexType> |
||||
|
<xs:all> |
||||
|
<xs:element name="Equals" minOccurs="0" maxOccurs="1" type="xs:anyType" /> |
||||
|
</xs:all> |
||||
|
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:schema> |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue