diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs index 19cfff710..4bf245c0c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs @@ -16,7 +16,6 @@ using Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors; using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.MongoDb.Queries; -using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.Translations; using Squidex.Log; @@ -82,27 +81,40 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets } } - public async Task> QueryAsync(DomainId appId, DomainId? parentId, ClrQuery query) + public async Task> QueryAsync(DomainId appId, DomainId? parentId, Q q) { using (Profiler.TraceMethod("QueryAsyncByQuery")) { try { - query = query.AdjustToModel(); + if (q.Ids != null && q.Ids.Count > 0) + { + var assetEntities = + await Collection.Find(BuildFilter(appId, q.Ids.ToHashSet())).SortByDescending(x => x.LastModified) + .QueryLimit(q.Query) + .QuerySkip(q.Query) + .ToListAsync(); + + return ResultList.Create(assetEntities.Count, assetEntities.OfType()); + } + else + { + var query = q.Query.AdjustToModel(); - var filter = query.BuildFilter(appId, parentId); + var filter = query.BuildFilter(appId, parentId); - var assetCount = Collection.Find(filter).CountDocumentsAsync(); - var assetItems = - Collection.Find(filter) - .QueryLimit(query) - .QuerySkip(query) - .QuerySort(query) - .ToListAsync(); + var assetCount = Collection.Find(filter).CountDocumentsAsync(); + var assetItems = + Collection.Find(filter) + .QueryLimit(query) + .QuerySkip(query) + .QuerySort(query) + .ToListAsync(); - var (items, total) = await AsyncHelper.WhenAll(assetItems, assetCount); + var (items, total) = await AsyncHelper.WhenAll(assetItems, assetCount); - return ResultList.Create(total, items); + return ResultList.Create(total, items); + } } catch (MongoQueryException ex) when (ex.Message.Contains("17406")) { @@ -135,18 +147,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets } } - public async Task> QueryAsync(DomainId appId, HashSet ids) - { - using (Profiler.TraceMethod("QueryAsyncByIds")) - { - var assetEntities = - await Collection.Find(BuildFilter(appId, ids)).SortByDescending(x => x.LastModified) - .ToListAsync(); - - return ResultList.Create(assetEntities.Count, assetEntities.OfType()); - } - } - public async Task FindAssetAsync(DomainId appId, string hash, string fileName, long fileSize) { using (Profiler.TraceMethod()) diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Fields.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Fields.cs deleted file mode 100644 index 4bae382b1..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Fields.cs +++ /dev/null @@ -1,40 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using MongoDB.Bson.Serialization; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Contents -{ - internal static class Fields - { - private static readonly Lazy IdField = new Lazy(GetIdField); - private static readonly Lazy SchemaIdField = new Lazy(GetSchemaIdField); - private static readonly Lazy StatusField = new Lazy(GetStatusField); - - public static string Id => IdField.Value; - - public static string SchemaId => SchemaIdField.Value; - - public static string Status => StatusField.Value; - - private static string GetIdField() - { - return BsonClassMap.LookupClassMap(typeof(MongoContentEntity)).GetMemberMap(nameof(MongoContentEntity.Id)).ElementName; - } - - private static string GetSchemaIdField() - { - return BsonClassMap.LookupClassMap(typeof(MongoContentEntity)).GetMemberMap(nameof(MongoContentEntity.IndexedSchemaId)).ElementName; - } - - private static string GetStatusField() - { - return BsonClassMap.LookupClassMap(typeof(MongoContentEntity)).GetMemberMap(nameof(MongoContentEntity.Status)).ElementName; - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollectionAll.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs similarity index 55% rename from backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollectionAll.cs rename to backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs index 462633f0c..abb2186c1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollectionAll.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs @@ -7,7 +7,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; using MongoDB.Driver; @@ -25,24 +24,29 @@ using Squidex.Log; namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { - public sealed class MongoContentCollectionAll : MongoRepositoryBase + public sealed class MongoContentCollection : MongoRepositoryBase { - private readonly QueryContent queryContentAsync; - private readonly QueryContentsByIds queryContentsById; - private readonly QueryContentsByQuery queryContentsByQuery; - private readonly QueryIdsAsync queryIdsAsync; - private readonly QueryReferrersAsync queryReferrersAsync; - private readonly QueryScheduledContents queryScheduledItems; - - public MongoContentCollectionAll(IMongoDatabase database, IAppProvider appProvider, ITextIndex indexer, DataConverter converter) + private readonly QueryAsStream queryAsStream; + private readonly QueryById queryBdId; + private readonly QueryByIds queryByIds; + private readonly QueryByQuery queryByQuery; + private readonly QueryReferences queryReferences; + private readonly QueryReferrers queryReferrers; + private readonly QueryScheduled queryScheduled; + private readonly string name; + + public MongoContentCollection(string name, IMongoDatabase database, IAppProvider appProvider, ITextIndex indexer, DataConverter dataConverter) : base(database) { - queryContentAsync = new QueryContent(converter); - queryContentsById = new QueryContentsByIds(converter, appProvider); - queryContentsByQuery = new QueryContentsByQuery(converter, indexer, appProvider); - queryIdsAsync = new QueryIdsAsync(appProvider); - queryReferrersAsync = new QueryReferrersAsync(); - queryScheduledItems = new QueryScheduledContents(); + this.name = name; + + queryAsStream = new QueryAsStream(dataConverter, appProvider); + queryBdId = new QueryById(dataConverter); + queryByIds = new QueryByIds(dataConverter); + queryByQuery = new QueryByQuery(dataConverter, indexer, appProvider); + queryReferences = new QueryReferences(dataConverter, queryByIds); + queryReferrers = new QueryReferrers(dataConverter); + queryScheduled = new QueryScheduled(dataConverter); } public IMongoCollection GetInternalCollection() @@ -52,53 +56,63 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents protected override string CollectionName() { - return "States_Contents_All2"; + return name; } protected override async Task SetupCollectionAsync(IMongoCollection 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 queryReferrersAsync.PrepareAsync(collection, ct); - await queryScheduledItems.PrepareAsync(collection, ct); + await queryAsStream.PrepareAsync(collection, ct); + await queryBdId.PrepareAsync(collection, ct); + await queryByIds.PrepareAsync(collection, ct); + await queryByQuery.PrepareAsync(collection, ct); + await queryReferences.PrepareAsync(collection, ct); + await queryReferrers.PrepareAsync(collection, ct); + await queryScheduled.PrepareAsync(collection, ct); } public IAsyncEnumerable StreamAll(DomainId appId, HashSet? schemaIds) { - return queryContentsByQuery.StreamAll(appId, schemaIds); + return queryAsStream.StreamAll(appId, schemaIds); } - public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, ClrQuery query, DomainId? referenced) + public async Task> QueryAsync(IAppEntity app, List schemas, Q q) { - using (Profiler.TraceMethod("QueryAsyncByQuery")) + using (Profiler.TraceMethod()) { - return await queryContentsByQuery.DoAsync(app, schema, query, referenced, SearchScope.All); + if (q.Ids != null && q.Ids.Count > 0) + { + return await queryByIds.QueryAsync(app.Id, schemas, q); + } + + if (q.Referencing != default) + { + return await queryReferences.QueryAsync(app.Id, schemas, q); + } + + if (q.Reference != default) + { + return await queryByQuery.QueryAsync(app, schemas, q); + } + + throw new NotSupportedException(); } } - public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, HashSet ids) + public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Q q, SearchScope scope) { - Guard.NotNull(app, nameof(app)); - - using (Profiler.TraceMethod("QueryAsyncByIds")) + using (Profiler.TraceMethod()) { - var result = await queryContentsById.DoAsync(app.Id, schema, ids, false); - - return ResultList.Create(result.Count, result.Select(x => x.Content)); - } - } - - public async Task> QueryAsync(IAppEntity app, HashSet ids) - { - Guard.NotNull(app, nameof(app)); + if (q.Ids != null && q.Ids.Count > 0l) + { + return await queryByIds.QueryAsync(app.Id, new List { schema }, q); + } - using (Profiler.TraceMethod("QueryAsyncByIdsWithoutSchema")) - { - var result = await queryContentsById.DoAsync(app.Id, null, ids, false); + if (q.Referencing == default) + { + return await queryByQuery.QueryAsync(app, schema, q, scope); + } - return result; + throw new NotSupportedException(); } } @@ -106,7 +120,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { using (Profiler.TraceMethod()) { - return await queryContentAsync.DoAsync(schema, id); + return await queryBdId.QueryAsync(schema, id); } } @@ -114,7 +128,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { using (Profiler.TraceMethod()) { - await queryScheduledItems.DoAsync(now, callback); + await queryScheduled.QueryAsync(now, callback); } } @@ -122,7 +136,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { using (Profiler.TraceMethod()) { - return await queryIdsAsync.DoAsync(appId, ids); + return await queryByIds.QueryIdsAsync(appId, ids); } } @@ -130,7 +144,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { using (Profiler.TraceMethod()) { - return await queryIdsAsync.DoAsync(appId, schemaId, filterNode); + return await queryByQuery.QueryIdsAsync(appId, schemaId, filterNode); } } @@ -138,18 +152,18 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { using (Profiler.TraceMethod()) { - return await queryReferrersAsync.DoAsync(appId, contentId); + return await queryReferrers.CheckExistsAsync(appId, contentId); } } - public Task ResetScheduledAsync(DomainId documentId) + public Task FindAsync(DomainId documentId) { - return Collection.UpdateOneAsync(x => x.DocumentId == documentId, Update.Unset(x => x.ScheduleJob).Unset(x => x.ScheduledAt)); + return Collection.Find(x => x.DocumentId == documentId).FirstOrDefaultAsync(); } - public Task FindAsync(DomainId documentId) + public Task ResetScheduledAsync(DomainId documentId) { - return Collection.Find(x => x.DocumentId == documentId).FirstOrDefaultAsync(); + return Collection.UpdateOneAsync(x => x.DocumentId == documentId, Update.Unset(x => x.ScheduleJob).Unset(x => x.ScheduledAt)); } public Task UpsertVersionedAsync(DomainId documentId, long oldVersion, MongoContentEntity entity) diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollectionPublished.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollectionPublished.cs deleted file mode 100644 index 5c8332459..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollectionPublished.cs +++ /dev/null @@ -1,138 +0,0 @@ -// ========================================================================== -// 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.Core.Contents; -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.MongoDb; -using Squidex.Infrastructure.Queries; -using Squidex.Log; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Contents -{ - public sealed class MongoContentCollectionPublished : MongoRepositoryBase - { - private readonly QueryContent queryContentAsync; - private readonly QueryContentsByIds queryContentsById; - private readonly QueryContentsByQuery queryContentsByQuery; - private readonly QueryIdsAsync queryIdsAsync; - private readonly QueryReferrersAsync queryReferrersAsync; - - public MongoContentCollectionPublished(IMongoDatabase database, IAppProvider appProvider, ITextIndex indexer, DataConverter converter) - : base(database) - { - queryContentAsync = new QueryContent(converter); - queryContentsById = new QueryContentsByIds(converter, appProvider); - queryContentsByQuery = new QueryContentsByQuery(converter, indexer, appProvider); - queryReferrersAsync = new QueryReferrersAsync(); - queryIdsAsync = new QueryIdsAsync(appProvider); - } - - public IMongoCollection GetInternalCollection() - { - return Collection; - } - - protected override MongoCollectionSettings CollectionSettings() - { - return new MongoCollectionSettings - { - ReadPreference = ReadPreference.SecondaryPreferred.With(TimeSpan.FromMinutes(2)) - }; - } - - protected override string CollectionName() - { - return "States_Contents_Published2"; - } - - protected override async Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) - { - await queryContentAsync.PrepareAsync(collection, ct); - await queryContentsById.PrepareAsync(collection, ct); - await queryContentsByQuery.PrepareAsync(collection, ct); - await queryReferrersAsync.PrepareAsync(collection, ct); - await queryIdsAsync.PrepareAsync(collection, ct); - } - - public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, ClrQuery query, DomainId? referenced) - { - using (Profiler.TraceMethod("QueryAsyncByQuery")) - { - return await queryContentsByQuery.DoAsync(app, schema, query, referenced, SearchScope.Published); - } - } - - public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, HashSet ids) - { - Guard.NotNull(app, nameof(app)); - - using (Profiler.TraceMethod("QueryAsyncByIds")) - { - var result = await queryContentsById.DoAsync(app.Id, schema, ids, true); - - return ResultList.Create(result.Count, result.Select(x => x.Content)); - } - } - - public async Task> QueryAsync(IAppEntity app, HashSet ids) - { - Guard.NotNull(app, nameof(app)); - - using (Profiler.TraceMethod("QueryAsyncByIdsWithoutSchema")) - { - var result = await queryContentsById.DoAsync(app.Id, null, ids, true); - - return result; - } - } - - public async Task FindContentAsync(ISchemaEntity schema, DomainId id) - { - using (Profiler.TraceMethod()) - { - return await queryContentAsync.DoAsync(schema, id); - } - } - - public async Task> QueryIdsAsync(DomainId appId, HashSet ids) - { - using (Profiler.TraceMethod()) - { - return await queryIdsAsync.DoAsync(appId, ids); - } - } - - public async Task HasReferrersAsync(DomainId appId, DomainId contentId) - { - using (Profiler.TraceMethod()) - { - return await queryReferrersAsync.DoAsync(appId, contentId); - } - } - - public Task UpsertVersionedAsync(DomainId documentId, long oldVersion, MongoContentEntity entity) - { - return Collection.UpsertVersionedAsync(documentId, oldVersion, entity.Version, entity); - } - - public Task RemoveAsync(DomainId documentId) - { - return Collection.DeleteOneAsync(x => x.DocumentId == documentId); - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs index aafb095e4..47eb09222 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs @@ -28,8 +28,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { private readonly IAppProvider appProvider; private readonly DataConverter converter; - private readonly MongoContentCollectionAll collectionAll; - private readonly MongoContentCollectionPublished collectionPublished; + private readonly MongoContentCollection collectionAll; + private readonly MongoContentCollection collectionPublished; static MongoContentRepository() { @@ -45,8 +45,13 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents converter = new DataConverter(serializer); - collectionAll = new MongoContentCollectionAll(database, appProvider, indexer, converter); - collectionPublished = new MongoContentCollectionPublished(database, appProvider, indexer, converter); + collectionAll = + new MongoContentCollection( + "States_Contents_All2", database, appProvider, indexer, converter); + + collectionPublished = + new MongoContentCollection( + "States_Contents_Published2", database, appProvider, indexer, converter); } public async Task InitializeAsync(CancellationToken ct = default) @@ -60,39 +65,27 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents return collectionAll.StreamAll(appId, schemaIds); } - public Task> QueryAsync(IAppEntity app, ISchemaEntity schema, ClrQuery query, DomainId? referenced, SearchScope scope) - { - if (scope == SearchScope.All) - { - return collectionAll.QueryAsync(app, schema, query, referenced); - } - else - { - return collectionPublished.QueryAsync(app, schema, query, referenced); - } - } - - public Task> QueryAsync(IAppEntity app, ISchemaEntity schema, HashSet ids, SearchScope scope) + public Task> QueryAsync(IAppEntity app, List schemas, Q q, SearchScope scope) { if (scope == SearchScope.All) { - return collectionAll.QueryAsync(app, schema, ids); + return collectionAll.QueryAsync(app, schemas, q); } else { - return collectionPublished.QueryAsync(app, schema, ids); + return collectionPublished.QueryAsync(app, schemas, q); } } - public Task> QueryAsync(IAppEntity app, HashSet ids, SearchScope scope) + public Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Q q, SearchScope scope) { if (scope == SearchScope.All) { - return collectionAll.QueryAsync(app, ids); + return collectionAll.QueryAsync(app, schema, q, scope); } else { - return collectionPublished.QueryAsync(app, ids); + return collectionPublished.QueryAsync(app, schema, q, scope); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Adapt.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Adapt.cs index 719233d18..076e27400 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Adapt.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Adapt.cs @@ -35,13 +35,13 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations return x.Name; } - public static Func Path(Schema schema) + public static Func Path(Schema? schema) { return propertyNames => { var result = new List(propertyNames); - if (result.Count > 1) + if (result.Count > 1 && schema != null) { var rootEdmName = result[1].UnescapeEdmField(); var rootField = schema.FieldsByName[rootEdmName]; @@ -78,7 +78,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations }; } - public static ClrQuery AdjustToModel(this ClrQuery query, Schema schema) + public static ClrQuery AdjustToModel(this ClrQuery query, Schema? schema) { var pathConverter = Path(schema); diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Extensions.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Extensions.cs new file mode 100644 index 000000000..91b8aaf9d --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Extensions.cs @@ -0,0 +1,50 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; +using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Driver; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations +{ + public static class Extensions + { + public sealed class StatusModel + { + [BsonId] + [BsonElement("_id")] + public DomainId DocumentId { get; set; } + + [BsonRequired] + [BsonElement("id")] + public DomainId Id { get; set; } + + [BsonRequired] + [BsonElement("_si")] + public DomainId IndexedSchemaId { get; set; } + + [BsonRequired] + [BsonElement("ss")] + public Status Status { get; set; } + } + + public static Task> FindStatusAsync(this IMongoCollection collection, FilterDefinition filter) + { + var projections = Builders.Projection; + + return collection.Find(filter) + .Project(projections + .Include(x => x.Id) + .Include(x => x.IndexedSchemaId) + .Include(x => x.Status)) + .ToListAsync(); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/OperationBase.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/OperationBase.cs index edac8aac1..16b037b57 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/OperationBase.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/OperationBase.cs @@ -21,6 +21,13 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations public IMongoCollection Collection { get; private set; } + public DataConverter DataConverter { get; } + + protected OperationBase(DataConverter dataConverter) + { + DataConverter = dataConverter; + } + public Task PrepareAsync(IMongoCollection collection, CancellationToken ct = default) { Collection = collection; diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryAsStream.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryAsStream.cs new file mode 100644 index 000000000..0d4d6d0f0 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryAsStream.cs @@ -0,0 +1,64 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Driver; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations +{ + public sealed class QueryAsStream : OperationBase + { + private readonly IAppProvider appProvider; + + public QueryAsStream(DataConverter converter, IAppProvider appProvider) + : base(converter) + { + this.appProvider = appProvider; + } + + protected override async Task PrepareAsync(CancellationToken ct = default) + { + var indexBySchema = + new CreateIndexModel(Index + .Ascending(x => x.IndexedAppId) + .Ascending(x => x.IsDeleted) + .Ascending(x => x.IndexedSchemaId)); + + await Collection.Indexes.CreateOneAsync(indexBySchema, cancellationToken: ct); + } + + public async IAsyncEnumerable StreamAll(DomainId appId, HashSet? schemaIds) + { + var find = + schemaIds != null ? + Collection.Find(x => x.IndexedAppId == appId && !x.IsDeleted && schemaIds.Contains(x.IndexedSchemaId)) : + Collection.Find(x => x.IndexedAppId == appId && !x.IsDeleted); + + using (var cursor = await find.ToCursorAsync()) + { + while (await cursor.MoveNextAsync()) + { + foreach (var entity in cursor.Current) + { + var schema = await appProvider.GetSchemaAsync(appId, entity.SchemaId.Id, false); + + if (schema != null) + { + entity.ParseData(schema.SchemaDef, DataConverter); + + yield return entity; + } + } + } + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryContent.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryById.cs similarity index 76% rename from backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryContent.cs rename to backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryById.cs index 2190f29c5..ca46d44e5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryContent.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryById.cs @@ -13,16 +13,14 @@ using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations { - internal sealed class QueryContent : OperationBase + internal sealed class QueryById : OperationBase { - private readonly DataConverter converter; - - public QueryContent(DataConverter converter) + public QueryById(DataConverter dataConverter) + : base(dataConverter) { - this.converter = converter; } - public async Task DoAsync(ISchemaEntity schema, DomainId id) + public async Task QueryAsync(ISchemaEntity schema, DomainId id) { Guard.NotNull(schema, nameof(schema)); @@ -39,7 +37,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations return null; } - contentEntity?.ParseData(schema.SchemaDef, converter); + contentEntity?.ParseData(schema.SchemaDef, DataConverter); } return contentEntity; diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByIds.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByIds.cs new file mode 100644 index 000000000..a1475e935 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByIds.cs @@ -0,0 +1,112 @@ +// ========================================================================== +// 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 MongoDB.Driver; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.MongoDb.Queries; +using Squidex.Infrastructure.Queries; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations +{ + internal sealed class QueryByIds : OperationBase + { + public QueryByIds(DataConverter dataConverter) + : base(dataConverter) + { + } + + public async Task> QueryIdsAsync(DomainId appId, HashSet ids) + { + if (ids == null || ids.Count == 0) + { + return new List<(DomainId SchemaId, DomainId Id, Status Status)>(); + } + + var filter = CreateFilter(appId, null, ids); + + var contentItems = await Collection.FindStatusAsync(filter); + + return contentItems.Select(x => (x.IndexedSchemaId, x.Id, x.Status)).ToList(); + } + + public async Task> QueryAsync(DomainId appId, List schemas, Q q) + { + Guard.NotNull(q, nameof(q)); + + if (q.Ids == null || q.Ids.Count == 0) + { + return ResultList.CreateFrom(0); + } + + var filter = CreateFilter(appId, schemas.Select(x => x.Id), q.Ids.ToHashSet()); + + var items = await FindContentsAsync(q.Query, filter); + + if (items.Count > 0) + { + var contentSchemas = schemas.ToDictionary(x => x.Id); + + foreach (var content in items) + { + var schema = contentSchemas[content.SchemaId.Id]; + + content.ParseData(schema.SchemaDef, DataConverter); + } + } + + return ResultList.Create(items.Count, items); + } + + private async Task> FindContentsAsync(ClrQuery query, FilterDefinition filter) + { + var result = + Collection.Find(filter) + .QueryLimit(query) + .QuerySkip(query) + .ToListAsync(); + + return await result; + } + + private static FilterDefinition CreateFilter(DomainId appId, IEnumerable? schemaIds, HashSet ids) + { + var filters = new List>(); + + var documentIds = ids.Select(x => DomainId.Combine(appId, x)).ToList(); + + if (documentIds.Count > 1) + { + filters.Add( + Filter.Or( + Filter.In(x => x.DocumentId, documentIds))); + } + else + { + var first = documentIds.First(); + + filters.Add( + Filter.Or( + Filter.Eq(x => x.DocumentId, first))); + } + + if (schemaIds != null) + { + filters.Add(Filter.In(x => x.IndexedSchemaId, schemaIds)); + } + + filters.Add(Filter.Ne(x => x.IsDeleted, true)); + + return Filter.And(filters); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryContentsByQuery.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByQuery.cs similarity index 60% rename from backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryContentsByQuery.cs rename to backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByQuery.cs index 796c7f15a..feb45e63b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryContentsByQuery.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByQuery.cs @@ -5,12 +5,14 @@ // 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.Bson.Serialization.Attributes; using MongoDB.Driver; +using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents.Text; @@ -23,9 +25,8 @@ using Squidex.Infrastructure.Translations; namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations { - internal sealed class QueryContentsByQuery : OperationBase + internal sealed class QueryByQuery : OperationBase { - private readonly DataConverter converter; private readonly ITextIndex indexer; private readonly IAppProvider appProvider; @@ -39,10 +40,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations public MongoContentEntity[] Joined { get; set; } } - public QueryContentsByQuery(DataConverter converter, ITextIndex indexer, IAppProvider appProvider) + public QueryByQuery(DataConverter dataConverter, ITextIndex indexer, IAppProvider appProvider) + : base(dataConverter) { - this.converter = converter; this.indexer = indexer; + this.appProvider = appProvider; } @@ -67,41 +69,89 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations await Collection.Indexes.CreateOneAsync(indexBySchema, cancellationToken: ct); } - public async IAsyncEnumerable StreamAll(DomainId appId, HashSet? schemaIds) + public async Task> QueryIdsAsync(DomainId appId, DomainId schemaId, FilterNode filterNode) { - var find = - schemaIds != null ? - Collection.Find(x => x.IndexedAppId == appId && schemaIds.Contains(x.IndexedSchemaId) && !x.IsDeleted) : - Collection.Find(x => x.IndexedAppId == appId && !x.IsDeleted); + Guard.NotNull(filterNode, nameof(filterNode)); - using (var cursor = await find.ToCursorAsync()) + try { - while (await cursor.MoveNextAsync()) + var schema = await appProvider.GetSchemaAsync(appId, schemaId, false); + + if (schema == null) { - foreach (var entity in cursor.Current) - { - var schema = await appProvider.GetSchemaAsync(appId, entity.SchemaId.Id, false); + return new List<(DomainId SchemaId, DomainId Id, Status Status)>(); + } + + var filter = BuildFilter(appId, schemaId, filterNode.AdjustToModel(schema.SchemaDef)); - if (schema != null) - { - entity.ParseData(schema.SchemaDef, converter); + var contentItems = await Collection.FindStatusAsync(filter); - yield return entity; - } + return contentItems.Select(x => (x.IndexedSchemaId, x.Id, x.Status)).ToList(); + } + catch (MongoCommandException ex) when (ex.Code == 96) + { + throw new DomainException(T.Get("common.resultTooLarge")); + } + catch (MongoQueryException ex) when (ex.Message.Contains("17406")) + { + throw new DomainException(T.Get("common.resultTooLarge")); + } + } + + public async Task> QueryAsync(IAppEntity app, List schemas, Q q) + { + Guard.NotNull(app, nameof(app)); + Guard.NotNull(q, nameof(q)); + + try + { + var query = q.Query.AdjustToModel(null); + + List? fullTextIds = null; + + if (!string.IsNullOrWhiteSpace(query.FullText)) + { + throw new NotSupportedException(); + } + + var filter = CreateFilter(app.Id, schemas.Select(x => x.Id), fullTextIds, query, q.Reference); + + var contentCount = Collection.Find(filter).CountDocumentsAsync(); + var contentItems = FindContentsAsync(query, filter); + + var (items, total) = await AsyncHelper.WhenAll(contentItems, contentCount); + + if (items.Count > 0) + { + var contentSchemas = schemas.ToDictionary(x => x.Id); + + foreach (var entity in items) + { + entity.ParseData(contentSchemas[entity.IndexedSchemaId].SchemaDef, DataConverter); } } + + return ResultList.Create(total, items); + } + catch (MongoCommandException ex) when (ex.Code == 96) + { + throw new DomainException(T.Get("common.resultTooLarge")); + } + catch (MongoQueryException ex) when (ex.Message.Contains("17406")) + { + throw new DomainException(T.Get("common.resultTooLarge")); } } - public async Task> DoAsync(IAppEntity app, ISchemaEntity schema, ClrQuery query, DomainId? referenced, SearchScope scope) + public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Q q, SearchScope scope) { Guard.NotNull(app, nameof(app)); Guard.NotNull(schema, nameof(schema)); - Guard.NotNull(query, nameof(query)); + Guard.NotNull(q, nameof(q)); try { - query = query.AdjustToModel(schema.SchemaDef); + var query = q.Query.AdjustToModel(schema.SchemaDef); List? fullTextIds = null; @@ -117,7 +167,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations } } - var filter = CreateFilter(schema.AppId.Id, schema.Id, fullTextIds, query, referenced); + var filter = CreateFilter(schema.AppId.Id, Enumerable.Repeat(schema.Id, 1), fullTextIds, query, q.Reference); var contentCount = Collection.Find(filter).CountDocumentsAsync(); var contentItems = FindContentsAsync(query, filter); @@ -126,7 +176,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations foreach (var entity in items) { - entity.ParseData(schema.SchemaDef, converter); + entity.ParseData(schema.SchemaDef, DataConverter); } return ResultList.Create(total, items); @@ -180,7 +230,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations return query.Sort?.All(x => x.Path.ToString() == "mt" && x.Order == SortOrder.Descending) == true; } - private static FilterDefinition CreateFilter(DomainId appId, DomainId schemaId, ICollection? ids, ClrQuery? query, DomainId? referenced) + private static FilterDefinition BuildFilter(DomainId appId, DomainId schemaId, FilterNode? filterNode) { var filters = new List> { @@ -189,6 +239,23 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations Filter.Ne(x => x.IsDeleted, true) }; + if (filterNode != null) + { + filters.Add(filterNode.BuildFilter()); + } + + return Filter.And(filters); + } + + private static FilterDefinition CreateFilter(DomainId appId, IEnumerable schemaIds, ICollection? ids, ClrQuery? query, DomainId referenced) + { + var filters = new List> + { + Filter.Eq(x => x.IndexedAppId, appId), + Filter.In(x => x.IndexedSchemaId, schemaIds), + Filter.Ne(x => x.IsDeleted, true) + }; + if (ids != null && ids.Count > 0) { var documentIds = ids.Select(x => DomainId.Combine(appId, x)).ToList(); @@ -204,9 +271,9 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations filters.Add(query.Filter.BuildFilter()); } - if (referenced != null) + if (referenced != default) { - filters.Add(Filter.AnyEq(x => x.ReferencedIds, referenced.Value)); + filters.Add(Filter.AnyEq(x => x.ReferencedIds, referenced)); } return Filter.And(filters); diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryContentsByIds.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryContentsByIds.cs deleted file mode 100644 index 32af83166..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryContentsByIds.cs +++ /dev/null @@ -1,111 +0,0 @@ -// ========================================================================== -// 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 MongoDB.Driver; -using Squidex.Domain.Apps.Entities.Contents; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations -{ - internal sealed class QueryContentsByIds : OperationBase - { - private readonly DataConverter converter; - private readonly IAppProvider appProvider; - - public QueryContentsByIds(DataConverter converter, IAppProvider appProvider) - { - this.converter = converter; - - this.appProvider = appProvider; - } - - public async Task> DoAsync(DomainId appId, ISchemaEntity? schema, HashSet ids, bool canCache) - { - Guard.NotNull(ids, nameof(ids)); - - var find = Collection.Find(CreateFilter(appId, ids)); - - var contentItems = await find.ToListAsync(); - var contentSchemas = await GetSchemasAsync(appId, schema, contentItems, canCache); - - var result = new List<(IContentEntity Content, ISchemaEntity Schema)>(); - - foreach (var contentEntity in contentItems) - { - if (contentSchemas.TryGetValue(contentEntity.IndexedSchemaId, out var contentSchema)) - { - contentEntity.ParseData(contentSchema.SchemaDef, converter); - - result.Add((contentEntity, contentSchema)); - } - } - - return result; - } - - private async Task> GetSchemasAsync(DomainId appId, ISchemaEntity? schema, List contentItems, bool canCache) - { - var schemas = new Dictionary(); - - if (schema != null) - { - schemas[schema.Id] = schema; - } - - var schemaIds = contentItems.Select(x => x.IndexedSchemaId).Distinct(); - - foreach (var schemaId in schemaIds) - { - if (!schemas.ContainsKey(schemaId)) - { - var found = await appProvider.GetSchemaAsync(appId, schemaId, false, canCache); - - if (found != null) - { - schemas[schemaId] = found; - } - } - } - - return schemas; - } - - private static FilterDefinition CreateFilter(DomainId appId, ICollection ids) - { - var filters = new List> - { - Filter.Ne(x => x.IsDeleted, true) - }; - - if (ids != null && ids.Count > 0) - { - var documentIds = ids.Select(x => DomainId.Combine(appId, x)).ToList(); - - if (ids.Count > 1) - { - filters.Add( - Filter.Or( - Filter.In(x => x.DocumentId, documentIds))); - } - else - { - var first = documentIds.First(); - - filters.Add( - Filter.Or( - Filter.Eq(x => x.DocumentId, first))); - } - } - - return Filter.And(filters); - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryIdsAsync.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryIdsAsync.cs deleted file mode 100644 index c6fb8ea85..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryIdsAsync.cs +++ /dev/null @@ -1,98 +0,0 @@ -// ========================================================================== -// 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; -using System.Threading.Tasks; -using MongoDB.Driver; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Infrastructure; -using Squidex.Infrastructure.MongoDb; -using Squidex.Infrastructure.MongoDb.Queries; -using Squidex.Infrastructure.Queries; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations -{ - internal sealed class QueryIdsAsync : OperationBase - { - private static readonly List<(DomainId SchemaId, DomainId Id, Status Status)> EmptyIds = new List<(DomainId SchemaId, DomainId Id, Status Status)>(); - private readonly IAppProvider appProvider; - - public QueryIdsAsync(IAppProvider appProvider) - { - this.appProvider = appProvider; - } - - protected override Task PrepareAsync(CancellationToken ct = default) - { - var index = - new CreateIndexModel(Index - .Ascending(x => x.IndexedAppId) - .Ascending(x => x.IndexedSchemaId) - .Ascending(x => x.IsDeleted)); - - return Collection.Indexes.CreateOneAsync(index, cancellationToken: ct); - } - - public async Task> DoAsync(DomainId appId, HashSet ids) - { - var documentIds = ids.Select(x => DomainId.Combine(appId, x)); - - var filter = - Filter.And( - Filter.In(x => x.DocumentId, documentIds), - Filter.Ne(x => x.IsDeleted, true)); - - return await SearchAsync(filter); - } - - public async Task> DoAsync(DomainId appId, DomainId schemaId, FilterNode filterNode) - { - var schema = await appProvider.GetSchemaAsync(appId, schemaId, false); - - if (schema == null) - { - return EmptyIds; - } - - var filter = BuildFilter(filterNode.AdjustToModel(schema.SchemaDef), appId, schemaId); - - return await SearchAsync(filter); - } - - private async Task> SearchAsync(FilterDefinition filter) - { - var contentEntities = - await Collection.Find(filter).Only(x => x.Id, x => x.IndexedSchemaId, x => x.Status) - .ToListAsync(); - - return contentEntities.Select(x => ( - DomainId.Create(x[Fields.SchemaId].AsString), - DomainId.Create(x[Fields.Id].AsString), - new Status(x[Fields.Status].AsString) - )).ToList(); - } - - private static FilterDefinition BuildFilter(FilterNode? filterNode, DomainId appId, DomainId schemaId) - { - var filters = new List> - { - Filter.Eq(x => x.IndexedAppId, appId), - Filter.Eq(x => x.IndexedSchemaId, schemaId), - Filter.Ne(x => x.IsDeleted, true) - }; - - if (filterNode != null) - { - filters.Add(filterNode.BuildFilter()); - } - - return Filter.And(filters); - } - } -} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferences.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferences.cs new file mode 100644 index 000000000..6e4909df5 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferences.cs @@ -0,0 +1,66 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; +using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Driver; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations +{ + internal class QueryReferences : OperationBase + { + private static readonly IResultList EmptyIds = ResultList.CreateFrom(0); + private readonly QueryByIds queryByIds; + + public sealed class ReferencedIdsOnly + { + [BsonId] + [BsonElement("_id")] + public DomainId DocumentId { get; set; } + + [BsonRequired] + [BsonElement("rf")] + public HashSet? ReferencedIds { get; set; } + } + + public QueryReferences(DataConverter dataConverter, QueryByIds queryByIds) + : base(dataConverter) + { + this.queryByIds = queryByIds; + } + + public async Task> QueryAsync(DomainId appId, List schemas, Q q) + { + var documentId = DomainId.Combine(appId, q.Referencing); + + var find = + Collection + .Find(x => x.DocumentId == documentId) + .Project(Projection.Include(x => x.ReferencedIds)); + + var contentEntity = await find.FirstOrDefaultAsync(); + + if (contentEntity == null) + { + throw new DomainObjectNotFoundException(q.Referencing.ToString()); + } + + if (contentEntity.ReferencedIds == null || contentEntity.ReferencedIds.Count == 0) + { + return EmptyIds; + } + + q = q.WithReferencing(default).WithIds(contentEntity.ReferencedIds!); + + return await queryByIds.QueryAsync(appId, schemas, q); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferrersAsync.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferrers.cs similarity index 85% rename from backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferrersAsync.cs rename to backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferrers.cs index 163b49a85..f8c0bd15a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferrersAsync.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferrers.cs @@ -13,8 +13,13 @@ using Squidex.Infrastructure.MongoDb; namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations { - internal sealed class QueryReferrersAsync : OperationBase + internal sealed class QueryReferrers : OperationBase { + public QueryReferrers(DataConverter dataConverter) + : base(dataConverter) + { + } + protected override Task PrepareAsync(CancellationToken ct = default) { var index = @@ -26,7 +31,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations return Collection.Indexes.CreateOneAsync(index, cancellationToken: ct); } - public async Task DoAsync(DomainId appId, DomainId contentId) + public async Task CheckExistsAsync(DomainId appId, DomainId contentId) { var filter = Filter.And( diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryScheduledContents.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryScheduled.cs similarity index 84% rename from backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryScheduledContents.cs rename to backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryScheduled.cs index d24133c90..35655a4d3 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryScheduledContents.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryScheduled.cs @@ -16,8 +16,13 @@ using Squidex.Infrastructure.MongoDb; namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations { - internal sealed class QueryScheduledContents : OperationBase + internal sealed class QueryScheduled : OperationBase { + public QueryScheduled(DataConverter dataConverter) + : base(dataConverter) + { + } + protected override Task PrepareAsync(CancellationToken ct = default) { var index = @@ -28,7 +33,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations return Collection.Indexes.CreateOneAsync(index, cancellationToken: ct); } - public Task DoAsync(Instant now, Func callback) + public Task QueryAsync(Instant now, Func callback) { Guard.NotNull(callback, nameof(callback)); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs index 8a0351b1b..2d4945aa9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs @@ -13,7 +13,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { public interface IAssetQueryService { - Task> QueryAsync(Context context, DomainId? parentId, Q query); + Task> QueryAsync(Context context, DomainId? parentId, Q q); Task> QueryAssetFoldersAsync(Context context, DomainId parentId); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs index 030ebc5ac..047b49bfb 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs @@ -46,60 +46,49 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries this.options = options.Value; } - public virtual async ValueTask ParseQueryAsync(Context context, Q q) + public virtual async ValueTask ParseQueryAsync(Context context, Q q) { Guard.NotNull(context, nameof(context)); Guard.NotNull(q, nameof(q)); using (Profiler.TraceMethod()) { - ClrQuery result; + var query = q.Query; - if (q.Query != null) + if (!string.IsNullOrWhiteSpace(q?.JsonQueryString)) { - result = q.Query; + query = ParseJson(q.JsonQueryString); } - else + else if (!string.IsNullOrWhiteSpace(q?.ODataQuery)) { - if (!string.IsNullOrWhiteSpace(q?.JsonQuery)) - { - result = ParseJson(q.JsonQuery); - } - else if (!string.IsNullOrWhiteSpace(q?.ODataQuery)) - { - result = ParseOData(q.ODataQuery); - } - else - { - result = new ClrQuery(); - } + query = ParseOData(q.ODataQuery); } - if (result.Filter != null) + if (query.Filter != null) { - result.Filter = await FilterTagTransformer.TransformAsync(result.Filter, context.App.Id, tagService); + query.Filter = await FilterTagTransformer.TransformAsync(query.Filter, context.App.Id, tagService); } - if (result.Sort.Count == 0) + if (query.Sort.Count == 0) { - result.Sort.Add(new SortNode(new List { "lastModified" }, SortOrder.Descending)); + query.Sort.Add(new SortNode(new List { "lastModified" }, SortOrder.Descending)); } - if (!result.Sort.Any(x => string.Equals(x.Path.ToString(), "id", StringComparison.OrdinalIgnoreCase))) + if (!query.Sort.Any(x => string.Equals(x.Path.ToString(), "id", StringComparison.OrdinalIgnoreCase))) { - result.Sort.Add(new SortNode(new List { "id" }, SortOrder.Ascending)); + query.Sort.Add(new SortNode(new List { "id" }, SortOrder.Ascending)); } - if (result.Take == long.MaxValue) + if (query.Take == long.MaxValue) { - result.Take = options.DefaultPageSize; + query.Take = options.DefaultPageSize; } - else if (result.Take > options.MaxResults) + else if (query.Take > options.MaxResults) { - result.Take = options.MaxResults; + query.Take = options.MaxResults; } - return result; + return q!.WithQuery(query); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs index 7329ab973..3919afd6b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs @@ -94,44 +94,23 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries return assetFolders; } - public async Task> QueryAsync(Context context, DomainId? parentId, Q query) + public async Task> QueryAsync(Context context, DomainId? parentId, Q q) { Guard.NotNull(context, nameof(context)); - Guard.NotNull(query, nameof(query)); + Guard.NotNull(q, nameof(q)); - IResultList assets; + q = await queryParser.ParseQueryAsync(context, q); - if (query.Ids != null && query.Ids.Count > 0) - { - assets = await QueryByIdsAsync(context, query); - } - else + var assets = await assetRepository.QueryAsync(context.App.Id, parentId, q); + + if (q.Ids != null && q.Ids.Count > 0) { - assets = await QueryByQueryAsync(context, parentId, query); + assets = assets.SortSet(x => x.Id, q.Ids); } var enriched = await assetEnricher.EnrichAsync(assets, context); return ResultList.Create(assets.Total, enriched); } - - private async Task> QueryByQueryAsync(Context context, DomainId? parentId, Q query) - { - var parsedQuery = await queryParser.ParseQueryAsync(context, query); - - return await assetRepository.QueryAsync(context.App.Id, parentId, parsedQuery); - } - - private async Task> QueryByIdsAsync(Context context, Q query) - { - var assets = await assetRepository.QueryAsync(context.App.Id, new HashSet(query.Ids)); - - return Sort(assets, query.Ids); - } - - private static IResultList Sort(IResultList assets, IReadOnlyList ids) - { - return assets.SortSet(x => x.Id, ids); - } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs index abe59cf72..d50fe4bc7 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs @@ -8,7 +8,6 @@ using System.Collections.Generic; using System.Threading.Tasks; using Squidex.Infrastructure; -using Squidex.Infrastructure.Queries; namespace Squidex.Domain.Apps.Entities.Assets.Repositories { @@ -16,9 +15,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Repositories { IAsyncEnumerable StreamAll(DomainId appId); - Task> QueryAsync(DomainId appId, DomainId? parentId, ClrQuery query); - - Task> QueryAsync(DomainId appId, HashSet ids); + Task> QueryAsync(DomainId appId, DomainId? parentId, Q q); Task> QueryIdsAsync(DomainId appId, HashSet ids); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentsSearchSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentsSearchSource.cs index c51e8e045..b99c52718 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentsSearchSource.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentsSearchSource.cs @@ -65,7 +65,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var appId = context.App.NamedId(); - var contents = await contentQuery.QueryAsync(context, ids); + var contents = await contentQuery.QueryAsync(context, Q.Empty.WithIds(ids)); foreach (var content in contents) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs index 67c23f6c8..ce9f61cd4 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs @@ -14,7 +14,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { public interface IContentQueryService { - Task> QueryAsync(Context context, IReadOnlyList ids); + Task> QueryAsync(Context context, Q q); Task> QueryAsync(Context context, string schemaIdOrName, Q query); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs index ebaca8ef8..ac5c8ea13 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs @@ -35,6 +35,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries public class ContentQueryParser { private static readonly TimeSpan CacheTime = TimeSpan.FromMinutes(60); + private readonly EdmModel genericEdmModel = BuildEdmModel("Generic", "Content", new EdmModel(), null); + private readonly JsonSchema genericJsonSchema = BuildJsonSchema("Content", null); private readonly IMemoryCache cache; private readonly IJsonSerializer jsonSerializer; private readonly ContentOptions options; @@ -50,78 +52,68 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries this.options = options.Value; } - public virtual ValueTask ParseQueryAsync(Context context, ISchemaEntity schema, Q q) + public virtual ValueTask ParseAsync(Context context, Q q, ISchemaEntity? schema = null) { Guard.NotNull(context, nameof(context)); - Guard.NotNull(schema, nameof(schema)); Guard.NotNull(q, nameof(q)); using (Profiler.TraceMethod()) { - ClrQuery result; + var query = q.Query; - if (q.Query != null) + if (!string.IsNullOrWhiteSpace(q.JsonQueryString)) { - result = q.Query; + query = ParseJson(context, schema, q.JsonQueryString); } - else + else if (q?.JsonQuery != null) { - if (!string.IsNullOrWhiteSpace(q?.JsonQuery)) - { - result = ParseJson(context, schema, q.JsonQuery); - } - else if (q?.ParsedJsonQuery != null) - { - result = ParseJson(context, schema, q.ParsedJsonQuery); - } - else if (!string.IsNullOrWhiteSpace(q?.ODataQuery)) - { - result = ParseOData(context, schema, q.ODataQuery); - } - else - { - result = new ClrQuery(); - } + query = ParseJson(context, schema, q.JsonQuery); + } + else if (!string.IsNullOrWhiteSpace(q?.ODataQuery)) + { + query = ParseOData(context, schema, q.ODataQuery); } - if (result.Sort.Count == 0) + if (query.Sort.Count == 0) { - result.Sort.Add(new SortNode(new List { "lastModified" }, SortOrder.Descending)); + query.Sort.Add(new SortNode(new List { "lastModified" }, SortOrder.Descending)); } - if (!result.Sort.Any(x => string.Equals(x.Path.ToString(), "id", StringComparison.OrdinalIgnoreCase))) + if (!query.Sort.Any(x => string.Equals(x.Path.ToString(), "id", StringComparison.OrdinalIgnoreCase))) { - result.Sort.Add(new SortNode(new List { "id" }, SortOrder.Ascending)); + query.Sort.Add(new SortNode(new List { "id" }, SortOrder.Ascending)); } - if (result.Take == long.MaxValue) + if (query.Take == long.MaxValue) { - result.Take = options.DefaultPageSize; + query.Take = options.DefaultPageSize; } - else if (result.Take > options.MaxResults) + else if (query.Take > options.MaxResults) { - result.Take = options.MaxResults; + query.Take = options.MaxResults; } - return new ValueTask(result); + q = q!.WithQuery(query); + + return new ValueTask(q); } } - private ClrQuery ParseJson(Context context, ISchemaEntity schema, Query query) + private ClrQuery ParseJson(Context context, ISchemaEntity? schema, Query query) { var jsonSchema = BuildJsonSchema(context, schema); return jsonSchema.Convert(query); } - private ClrQuery ParseJson(Context context, ISchemaEntity schema, string json) + private ClrQuery ParseJson(Context context, ISchemaEntity? schema, string json) { var jsonSchema = BuildJsonSchema(context, schema); return jsonSchema.Parse(json, jsonSerializer); } - private ClrQuery ParseOData(Context context, ISchemaEntity schema, string odata) + private ClrQuery ParseOData(Context context, ISchemaEntity? schema, string odata) { try { @@ -139,8 +131,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries } } - private JsonSchema BuildJsonSchema(Context context, ISchemaEntity schema) + private JsonSchema BuildJsonSchema(Context context, ISchemaEntity? schema) { + if (schema == null) + { + return genericJsonSchema; + } + var cacheKey = BuildJsonCacheKey(context.App, schema, context.IsFrontendClient); var result = cache.GetOrCreate(cacheKey, entry => @@ -153,8 +150,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries return result; } - private IEdmModel BuildEdmModel(Context context, ISchemaEntity schema) + private IEdmModel BuildEdmModel(Context context, ISchemaEntity? schema) { + if (schema == null) + { + return genericEdmModel; + } + var cacheKey = BuildEmdCacheKey(context.App, schema, context.IsFrontendClient); var result = cache.GetOrCreate(cacheKey, entry => @@ -171,7 +173,34 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { var dataSchema = schema.BuildJsonSchema(app.PartitionResolver(), (n, s) => s, withHiddenFields); - return ContentSchemaBuilder.CreateContentSchema(schema, dataSchema); + return BuildJsonSchema(schema.DisplayName(), dataSchema); + } + + private static JsonSchema BuildJsonSchema(string name, JsonSchema? dataSchema) + { + var schema = new JsonSchema + { + Properties = + { + ["id"] = SchemaBuilder.StringProperty($"The id of the {name} content.", true), + ["version"] = SchemaBuilder.NumberProperty($"The version of the {name}.", true), + ["created"] = SchemaBuilder.DateTimeProperty($"The date and time when the {name} content has been created.", true), + ["createdBy"] = SchemaBuilder.StringProperty($"The user that has created the {name} content.", true), + ["lastModified"] = SchemaBuilder.DateTimeProperty($"The date and time when the {name} content has been modified last.", true), + ["lastModifiedBy"] = SchemaBuilder.StringProperty($"The user that has updated the {name} content last.", true), + ["newStatus"] = SchemaBuilder.StringProperty($"The new status of the content.", false), + ["status"] = SchemaBuilder.StringProperty($"The status of the content.", true) + }, + Type = JsonObjectType.Object + }; + + if (dataSchema != null) + { + schema.Properties["data"] = SchemaBuilder.ObjectProperty(dataSchema, $"The data of the {name}.", true); + schema.Properties["dataDraft"] = SchemaBuilder.ObjectProperty(dataSchema, $"The draft data of the {name}."); + } + + return schema; } private static EdmModel BuildEdmModel(Schema schema, IAppEntity app, bool withHiddenFields) @@ -207,7 +236,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries var schemaType = schema.BuildEdmType(withHiddenFields, app.PartitionResolver(), typeFactory); - var entityType = new EdmEntityType(app.Name.ToPascalCase(), schema.Name); + return BuildEdmModel(app.Name.ToPascalCase(), schema.Name, model, schemaType); + } + + private static EdmModel BuildEdmModel(string modelName, string name, EdmModel model, EdmComplexType? schemaType) + { + var entityType = new EdmEntityType(modelName, name); + entityType.AddStructuralProperty(nameof(IContentEntity.Id).ToCamelCase(), EdmPrimitiveTypeKind.String); entityType.AddStructuralProperty(nameof(IContentEntity.Created).ToCamelCase(), EdmPrimitiveTypeKind.DateTimeOffset); entityType.AddStructuralProperty(nameof(IContentEntity.CreatedBy).ToCamelCase(), EdmPrimitiveTypeKind.String); @@ -216,14 +251,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries entityType.AddStructuralProperty(nameof(IContentEntity.NewStatus).ToCamelCase(), EdmPrimitiveTypeKind.String); entityType.AddStructuralProperty(nameof(IContentEntity.Status).ToCamelCase(), EdmPrimitiveTypeKind.String); entityType.AddStructuralProperty(nameof(IContentEntity.Version).ToCamelCase(), EdmPrimitiveTypeKind.Int32); - entityType.AddStructuralProperty(nameof(IContentEntity.Data).ToCamelCase(), new EdmComplexTypeReference(schemaType, false)); + + if (schemaType != null) + { + entityType.AddStructuralProperty("data", new EdmComplexTypeReference(schemaType, false)); + entityType.AddStructuralProperty("dataDraft", new EdmComplexTypeReference(schemaType, false)); + + model.AddElement(schemaType); + } var container = new EdmEntityContainer("Squidex", "Container"); container.AddEntitySet("ContentSet", entityType); model.AddElement(container); - model.AddElement(schemaType); model.AddElement(entityType); return model; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs index 7c414ce32..e5aa2a57c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs @@ -12,13 +12,10 @@ using System.Threading.Tasks; using Squidex.Domain.Apps.Entities.Contents.Repositories; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; -using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Translations; using Squidex.Log; using Squidex.Shared; -#pragma warning disable RECS0147 - namespace Squidex.Domain.Apps.Entities.Contents.Queries { public sealed class ContentQueryService : IContentQueryService @@ -62,8 +59,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName); - CheckPermission(context, schema); - using (Profiler.TraceMethod()) { IContentEntity? content; @@ -86,58 +81,60 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries } } - public async Task> QueryAsync(Context context, string schemaIdOrName, Q query) + public async Task> QueryAsync(Context context, string schemaIdOrName, Q q) { Guard.NotNull(context, nameof(context)); - if (query == null) + if (q == null) { return EmptyContents; } var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName); - CheckPermission(context, schema); - using (Profiler.TraceMethod()) { - IResultList contents; + q = await queryParser.ParseAsync(context, q, schema); - if (query.Ids != null && query.Ids.Count > 0) - { - contents = await QueryByIdsAsync(context, schema, query); - } - else + var contents = await contentRepository.QueryAsync(context.App, schema, q, context.Scope()); + + if (q.Ids != null && q.Ids.Count > 0) { - contents = await QueryByQueryAsync(context, schema, query); + contents = contents.SortSet(x => x.Id, q.Ids); } return await TransformAsync(context, contents); } } - public async Task> QueryAsync(Context context, IReadOnlyList ids) + public async Task> QueryAsync(Context context, Q q) { Guard.NotNull(context, nameof(context)); - if (ids == null || ids.Count == 0) + if (q == null) + { + return EmptyContents; + } + + var schemas = await GetSchemasAsync(context); + + if (schemas.Count == 0) { return EmptyContents; } using (Profiler.TraceMethod()) { - var contents = await QueryCoreAsync(context, ids); + q = await queryParser.ParseAsync(context, q); - var filtered = - contents - .GroupBy(x => x.Schema.Id) - .Select(g => FilterContents(g, context)) - .SelectMany(c => c); + var contents = await contentRepository.QueryAsync(context.App, schemas, q, context.Scope()); - var results = await TransformCoreAsync(context, filtered); + if (q.Ids != null && q.Ids.Count > 0) + { + contents = contents.SortSet(x => x.Id, q.Ids); + } - return ResultList.Create(results.Count, results.SortList(x => x.Id, ids)); + return await TransformAsync(context, contents); } } @@ -186,70 +183,28 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries throw new DomainObjectNotFoundException(schemaIdOrName); } - return schema; - } - - private static void CheckPermission(Context context, params ISchemaEntity[] schemas) - { - foreach (var schema in schemas) + if (!HasPermission(context, schema)) { - if (!HasPermission(context, schema)) - { - throw new DomainForbiddenException(T.Get("schemas.noPermission")); - } + throw new DomainForbiddenException(T.Get("schemas.noPermission")); } + + return schema; } - private static IEnumerable FilterContents(IGrouping group, Context context) + private async Task> GetSchemasAsync(Context context) { - var schema = group.First().Schema; + var schemas = await appProvider.GetSchemasAsync(context.App.Id); - if (HasPermission(context, schema)) - { - return group.Select(x => x.Content); - } - else - { - return Enumerable.Empty(); - } + return schemas.Where(x => HasPermission(context, x)).ToList(); } private static bool HasPermission(Context context, ISchemaEntity schema) { - var permission = Permissions.ForApp(Permissions.AppContentsRead, schema.AppId.Name, schema.SchemaDef.Name); + var permission = Permissions.ForApp(Permissions.AppContentsRead, context.App.Name, schema.SchemaDef.Name); return context.Permissions.Allows(permission); } - private async Task> QueryByQueryAsync(Context context, ISchemaEntity schema, Q query) - { - var parsedQuery = await queryParser.ParseQueryAsync(context, schema, query); - - return await QueryCoreAsync(context, schema, parsedQuery, query.Reference); - } - - private async Task> QueryByIdsAsync(Context context, ISchemaEntity schema, Q query) - { - var contents = await QueryCoreAsync(context, schema, query.Ids.ToHashSet()); - - return contents.SortSet(x => x.Id, query.Ids); - } - - private Task> QueryCoreAsync(Context context, IReadOnlyList ids) - { - return contentRepository.QueryAsync(context.App, new HashSet(ids), context.Scope()); - } - - private Task> QueryCoreAsync(Context context, ISchemaEntity schema, ClrQuery query, DomainId? referenced) - { - return contentRepository.QueryAsync(context.App, schema, query, referenced, context.Scope()); - } - - private Task> QueryCoreAsync(Context context, ISchemaEntity schema, HashSet ids) - { - return contentRepository.QueryAsync(context.App, schema, ids, context.Scope()); - } - private Task FindCoreAsync(Context context, DomainId id, ISchemaEntity schema) { return contentRepository.FindContentAsync(context.App, schema, id, context.Scope()); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs index 38dfd97e3..35c392765 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs @@ -180,7 +180,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries await maxRequests.WaitAsync(); try { - contents = await contentQuery.QueryAsync(context, notLoadedContents); + contents = await contentQuery.QueryAsync(context, Q.Empty.WithIds(notLoadedContents)); } finally { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveReferences.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveReferences.cs index 9be5b8737..4fa18aa2d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveReferences.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveReferences.cs @@ -153,7 +153,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps return EmptyContents; } - var references = await ContentQuery.QueryAsync(context.Clone().WithoutContentEnrichment(true), ids.ToList()); + var references = await ContentQuery.QueryAsync(context.Clone().WithoutContentEnrichment(true), Q.Empty.WithIds(ids)); return references.ToLookup(x => x.Id); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesFluidExtension.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesFluidExtension.cs index 1c40eb586..cfda02e8a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesFluidExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesFluidExtension.cs @@ -62,7 +62,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var domainId = DomainId.Create(id); var domainIds = new List { domainId }; - var references = await contentQueryService.QueryAsync(appContext, domainIds); + var references = await contentQueryService.QueryAsync(appContext, Q.Empty.WithIds(domainIds)); var reference = references.FirstOrDefault(); if (reference != null) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs index 15fdaed5d..8074db839 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs @@ -21,11 +21,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Repositories { IAsyncEnumerable StreamAll(DomainId appId, HashSet? schemaIds); - Task> QueryAsync(IAppEntity app, HashSet ids, SearchScope scope); + Task> QueryAsync(IAppEntity app, List schemas, Q q, SearchScope scope); - Task> QueryAsync(IAppEntity app, ISchemaEntity schema, HashSet ids, SearchScope scope); - - Task> QueryAsync(IAppEntity app, ISchemaEntity schema, ClrQuery query, DomainId? referenced, SearchScope scope); + Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Q q, SearchScope scope); Task> QueryIdsAsync(DomainId appId, DomainId schemaId, FilterNode filterNode); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Validation/DependencyValidatorsFactory.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Validation/DependencyValidatorsFactory.cs index 30cab7545..d5fe735f8 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Validation/DependencyValidatorsFactory.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Validation/DependencyValidatorsFactory.cs @@ -42,7 +42,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Validation { var checkAssets = new CheckAssets(async ids => { - return await assetRepository.QueryAsync(context.AppId.Id, new HashSet(ids)); + return await assetRepository.QueryAsync(context.AppId.Id, null, Q.Empty.WithIds(ids)); }); yield return new AssetsValidator(isRequired, assetsField.Properties, checkAssets); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Q.cs b/backend/src/Squidex.Domain.Apps.Entities/Q.cs index ac7738894..e4e7e0dcb 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Q.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Q.cs @@ -13,75 +13,88 @@ using Squidex.Infrastructure.Queries; namespace Squidex.Domain.Apps.Entities { - public sealed class Q : Cloneable + public record Q { - public static readonly Q Empty = new Q(); + public static Q Empty => new Q(); - public IReadOnlyList Ids { get; private set; } + public IReadOnlyList? Ids { get; init; } - public DomainId? Reference { get; private set; } + public DomainId Referencing { get; init; } - public string? ODataQuery { get; private set; } + public DomainId Reference { get; init; } - public string? JsonQuery { get; private set; } + public string? ODataQuery { get; init; } - public Query? ParsedJsonQuery { get; private set; } + public string? JsonQueryString { get; init; } - public ClrQuery? Query { get; private set; } + public Query? JsonQuery { get; init; } - public Q WithQuery(ClrQuery? query) + public ClrQuery Query { get; init; } = new ClrQuery(); + + private Q() { - return Clone(clone => clone.Query = query); } - public Q WithODataQuery(string? odataQuery) + public Q WithQuery(ClrQuery query) { - return Clone(clone => clone.ODataQuery = odataQuery); + Guard.NotNull(query, nameof(query)); + + return this with { Query = query }; } - public Q WithJsonQuery(string? jsonQuery) + public Q WithODataQuery(string? query) { - return Clone(clone => clone.JsonQuery = jsonQuery); + return this with { ODataQuery = query }; } - public Q WithJsonQuery(Query? jsonQuery) + public Q WithJsonQuery(string? query) { - return Clone(clone => clone.ParsedJsonQuery = jsonQuery); + return this with { JsonQueryString = query }; } - public Q WithIds(params DomainId[] ids) + public Q WithJsonQuery(Query? query) + { + return this with { JsonQuery = query }; + } + + public Q WithReferencing(DomainId id) + { + return this with { Referencing = id }; + } + + public Q WithReference(DomainId id) { - return Clone(clone => clone.Ids = ids.ToList()); + return this with { Reference = id }; } - public Q WithReference(DomainId? reference) + public Q WithIds(params DomainId[] ids) { - return Clone(clone => clone.Reference = reference); + return this with { Ids = ids?.ToList() }; } public Q WithIds(IEnumerable ids) { - return Clone(clone => clone.Ids = ids.ToList()); + return this with { Ids = ids?.ToList() }; } public Q WithIds(string? ids) { - if (!string.IsNullOrEmpty(ids)) + if (string.IsNullOrWhiteSpace(ids)) { - return Clone(clone => - { - var idsList = new List(); + return this with { Ids = null }; + } - foreach (var id in ids.Split(',')) - { - idsList.Add(DomainId.Create(id)); - } + var idsList = new List(); - clone.Ids = idsList; - }); + if (!string.IsNullOrEmpty(ids)) + { + foreach (var id in ids.Split(',')) + { + idsList.Add(DomainId.Create(id)); + } } - return this; + return this with { Ids = idsList }; } } } diff --git a/backend/src/Squidex.Infrastructure/Queries/Json/QueryParser.cs b/backend/src/Squidex.Infrastructure/Queries/Json/QueryParser.cs index df4366e64..59665aa9e 100644 --- a/backend/src/Squidex.Infrastructure/Queries/Json/QueryParser.cs +++ b/backend/src/Squidex.Infrastructure/Queries/Json/QueryParser.cs @@ -84,7 +84,7 @@ namespace Squidex.Infrastructure.Queries.Json } } - private static Query ParseFromJson(string json, IJsonSerializer jsonSerializer) + public static Query ParseFromJson(string json, IJsonSerializer jsonSerializer) { try { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs index cb10eb13e..623807984 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs @@ -154,7 +154,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [ApiCosts(1)] public async Task GetAllContents(string app, [FromQuery] string ids) { - var contents = await contentQuery.QueryAsync(Context, Q.Empty.WithIds(ids).Ids); + var contents = await contentQuery.QueryAsync(Context, Q.Empty.WithIds(ids)); var response = Deferred.AsyncResponse(() => { @@ -183,7 +183,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [ApiCosts(1)] public async Task GetAllContentsPost(string app, [FromBody] ContentsIdsQueryDto query) { - var contents = await contentQuery.QueryAsync(Context, query.Ids); + var contents = await contentQuery.QueryAsync(Context, Q.Empty.WithIds(query.Ids)); var response = Deferred.AsyncResponse(() => { @@ -273,7 +273,7 @@ namespace Squidex.Areas.Api.Controllers.Contents /// [HttpGet] [Route("content/{app}/{name}/{id}/")] - [ProducesResponseType(typeof(ContentsDto), 200)] + [ProducesResponseType(typeof(ContentDto), 200)] [ApiPermissionOrAnonymous] [ApiCosts(1)] public async Task GetContent(string app, string name, DomainId id) @@ -285,6 +285,68 @@ namespace Squidex.Areas.Api.Controllers.Contents return Ok(response); } + /// + /// Get all references of a content. + /// + /// The name of the app. + /// The name of the schema. + /// The id of the content to fetch. + /// The optional json query. + /// + /// 200 => Contents returned. + /// 404 => Content, schema or app not found. + /// + /// + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// + [HttpGet] + [Route("content/{app}/{name}/{id}/references")] + [ProducesResponseType(typeof(ContentsDto), 200)] + [ApiPermissionOrAnonymous] + [ApiCosts(1)] + public async Task GetReferences(string app, string name, DomainId id, [FromQuery] string? q = null) + { + var contents = await contentQuery.QueryAsync(Context, CreateQuery(null, q).WithReferencing(id)); + + var response = Deferred.AsyncResponse(() => + { + return ContentsDto.FromContentsAsync(contents, Resources, null, contentWorkflow); + }); + + return Ok(response); + } + + /// + /// Get a referencing contents of a content item. + /// + /// The name of the app. + /// The name of the schema. + /// The id of the content to fetch. + /// The optional json query. + /// + /// 200 => Content returned. + /// 404 => Content, schema or app not found. + /// + /// + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// + [HttpGet] + [Route("content/{app}/{name}/{id}/referencing")] + [ProducesResponseType(typeof(ContentsDto), 200)] + [ApiPermissionOrAnonymous] + [ApiCosts(1)] + public async Task GetReferencing(string app, string name, DomainId id, [FromQuery] string? q = null) + { + var contents = await contentQuery.QueryAsync(Context, CreateQuery(null, q).WithReference(id)); + + var response = Deferred.AsyncResponse(() => + { + return ContentsDto.FromContentsAsync(contents, Resources, null, contentWorkflow); + }); + + return Ok(response); + } + /// /// Get a content by version. /// diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs index b42655896..91ffad592 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs @@ -61,7 +61,7 @@ namespace Squidex.Domain.Apps.Core.Operations.GenerateJsonSchema var jsonSchema = schema.BuildJsonSchema(languagesConfig.ToResolver(), (n, s) => new JsonSchema { Reference = s }); - Assert.NotNull(ContentSchemaBuilder.CreateContentSchema(schema, jsonSchema)); + Assert.NotNull(jsonSchema); } private static HashSet AllPropertyNames(JsonSchema schema) diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryTests.cs index 55950b630..aeec3db70 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryTests.cs @@ -30,7 +30,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb [Fact] public async Task Should_find_asset_by_slug() { - var asset = await _.AssetRepository.FindAssetBySlugAsync(_.RandomAppId(), _.RandomValue()); + var random = _.RandomValue(); + + var asset = await _.AssetRepository.FindAssetBySlugAsync(_.RandomAppId(), random); Assert.NotNull(asset); } @@ -38,7 +40,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb [Fact] public async Task Should_query_asset_by_hash() { - var assets = await _.AssetRepository.FindAssetAsync(_.RandomAppId(), _.RandomValue(), _.RandomValue(), 123); + var random = _.RandomValue(); + + var assets = await _.AssetRepository.FindAssetAsync(_.RandomAppId(), random, random, 1024); Assert.NotNull(assets); } @@ -68,9 +72,11 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb [MemberData(nameof(ParentIds))] public async Task Should_query_assets_by_tags(DomainId? parentId) { + var random = _.RandomValue(); + var query = new ClrQuery { - Filter = F.Eq("Tags", _.RandomValue()) + Filter = F.Eq("Tags", random) }; var assets = await QueryAsync(parentId, query); @@ -82,9 +88,11 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb [MemberData(nameof(ParentIds))] public async Task Should_query_assets_by_tags_and_name(DomainId? parentId) { + var random = _.RandomValue(); + var query = new ClrQuery { - Filter = F.And(F.Eq("Tags", _.RandomValue()), F.Contains("FileName", _.RandomValue())) + Filter = F.And(F.Eq("Tags", random), F.Contains("FileName", random)) }; var assets = await QueryAsync(parentId, query); @@ -96,9 +104,11 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb [MemberData(nameof(ParentIds))] public async Task Should_query_assets_by_fileName(DomainId? parentId) { + var random = _.RandomValue(); + var query = new ClrQuery { - Filter = F.Contains("FileName", _.RandomValue()) + Filter = F.Contains("FileName", random) }; var assets = await QueryAsync(parentId, query); @@ -110,9 +120,11 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb [MemberData(nameof(ParentIds))] public async Task Should_query_assets_by_fileName_and_tags(DomainId? parentId) { + var random = _.RandomValue(); + var query = new ClrQuery { - Filter = F.And(F.Contains("FileName", _.RandomValue()), F.Eq("Tags", _.RandomValue())) + Filter = F.And(F.Contains("FileName", random), F.Eq("Tags", random)) }; var assets = await QueryAsync(parentId, query); @@ -137,7 +149,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb new SortNode("LastModified", SortOrder.Descending) }; - var assets = await _.AssetRepository.QueryAsync(_.RandomAppId(), parentId, query); + var assets = await _.AssetRepository.QueryAsync(_.RandomAppId(), parentId, Q.Empty.WithQuery(query)); return assets; } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryParserTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryParserTests.cs index 1f4953d76..1348fe148 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryParserTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryParserTests.cs @@ -34,16 +34,6 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries sut = new AssetQueryParser(JsonHelper.DefaultSerializer, tagService, options); } - [Fact] - public async Task Should_use_existing_query() - { - var clrQuery = new ClrQuery(); - - var parsed = await sut.ParseQueryAsync(requestContext, Q.Empty.WithQuery(clrQuery)); - - Assert.Same(parsed, clrQuery); - } - [Fact] public async Task Should_throw_if_odata_query_is_invalid() { @@ -65,9 +55,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries { var query = Q.Empty.WithODataQuery("$top=100&$orderby=fileName asc&$search=Hello World"); - var parsed = await sut.ParseQueryAsync(requestContext, query); + var q = await sut.ParseQueryAsync(requestContext, query); - Assert.Equal("FullText: 'Hello World'; Take: 100; Sort: fileName Ascending, id Ascending", parsed.ToString()); + Assert.Equal("FullText: 'Hello World'; Take: 100; Sort: fileName Ascending, id Ascending", q.Query.ToString()); } [Fact] @@ -75,9 +65,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries { var query = Q.Empty.WithODataQuery("$top=200&$filter=fileName eq 'ABC'"); - var parsed = await sut.ParseQueryAsync(requestContext, query); + var q = await sut.ParseQueryAsync(requestContext, query); - Assert.Equal("Filter: fileName == 'ABC'; Take: 200; Sort: lastModified Descending, id Ascending", parsed.ToString()); + Assert.Equal("Filter: fileName == 'ABC'; Take: 200; Sort: lastModified Descending, id Ascending", q.Query.ToString()); } [Fact] @@ -85,9 +75,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries { var query = Q.Empty.WithJsonQuery(Json("{ 'filter': { 'path': 'fileName', 'op': 'eq', 'value': 'ABC' } }")); - var parsed = await sut.ParseQueryAsync(requestContext, query); + var q = await sut.ParseQueryAsync(requestContext, query); - Assert.Equal("Filter: fileName == 'ABC'; Take: 30; Sort: lastModified Descending, id Ascending", parsed.ToString()); + Assert.Equal("Filter: fileName == 'ABC'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); } [Fact] @@ -95,9 +85,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries { var query = Q.Empty.WithJsonQuery(Json("{ 'fullText': 'Hello' }")); - var parsed = await sut.ParseQueryAsync(requestContext, query); + var q = await sut.ParseQueryAsync(requestContext, query); - Assert.Equal("FullText: 'Hello'; Take: 30; Sort: lastModified Descending, id Ascending", parsed.ToString()); + Assert.Equal("FullText: 'Hello'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); } [Fact] @@ -105,9 +95,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries { var query = Q.Empty; - var parsed = await sut.ParseQueryAsync(requestContext, query); + var q = await sut.ParseQueryAsync(requestContext, query); - Assert.Equal("Take: 30; Sort: lastModified Descending, id Ascending", parsed.ToString()); + Assert.Equal("Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); } [Fact] @@ -115,9 +105,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries { var query = Q.Empty.WithODataQuery("$top=300&$skip=20"); - var parsed = await sut.ParseQueryAsync(requestContext, query); + var q = await sut.ParseQueryAsync(requestContext, query); - Assert.Equal("Skip: 20; Take: 200; Sort: lastModified Descending, id Ascending", parsed.ToString()); + Assert.Equal("Skip: 20; Take: 200; Sort: lastModified Descending, id Ascending", q.Query.ToString()); } [Fact] @@ -125,9 +115,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries { var query = Q.Empty.WithODataQuery("$top=300&$skip=20&$orderby=id desc"); - var parsed = await sut.ParseQueryAsync(requestContext, query); + var q = await sut.ParseQueryAsync(requestContext, query); - Assert.Equal("Skip: 20; Take: 200; Sort: id Descending", parsed.ToString()); + Assert.Equal("Skip: 20; Take: 200; Sort: id Descending", q.Query.ToString()); } [Fact] @@ -138,9 +128,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries var query = Q.Empty.WithODataQuery("$filter=tags eq 'name1'"); - var parsed = await sut.ParseQueryAsync(requestContext, query); + var q = await sut.ParseQueryAsync(requestContext, query); - Assert.Equal("Filter: tags == 'id1'; Take: 30; Sort: lastModified Descending, id Ascending", parsed.ToString()); + Assert.Equal("Filter: tags == 'id1'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); } private static string Json(string text) diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs index 10f570a79..6fc0bd182 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs @@ -12,7 +12,6 @@ using FakeItEasy; using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; -using Squidex.Infrastructure.Queries; using Xunit; namespace Squidex.Domain.Apps.Entities.Assets.Queries @@ -32,7 +31,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries requestContext = new Context(Mocks.FrontendUser(), Mocks.App(appId)); A.CallTo(() => queryParser.ParseQueryAsync(requestContext, A._)) - .Returns(new ClrQuery()); + .ReturnsLazily(c => new ValueTask(c.GetArgument(1)!)); sut = new AssetQueryService(assetEnricher, assetRepository, assetFolderRepository, queryParser); } @@ -73,30 +72,6 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries Assert.Same(enriched, result); } - [Fact] - public async Task Should_load_assets_from_ids_and_resolve_tags() - { - var found1 = new AssetEntity { Id = DomainId.NewGuid() }; - var found2 = new AssetEntity { Id = DomainId.NewGuid() }; - - var enriched1 = new AssetEntity(); - var enriched2 = new AssetEntity(); - - var ids = HashSet.Of(found1.Id, found2.Id); - - A.CallTo(() => assetRepository.QueryAsync(appId.Id, A>.That.Is(ids))) - .Returns(ResultList.CreateFrom(8, found1, found2)); - - A.CallTo(() => assetEnricher.EnrichAsync(A>.That.IsSameSequenceAs(found1, found2), requestContext)) - .Returns(new List { enriched1, enriched2 }); - - var result = await sut.QueryAsync(requestContext, null, Q.Empty.WithIds(ids)); - - Assert.Equal(8, result.Total); - - Assert.Equal(new[] { enriched1, enriched2 }, result.ToArray()); - } - [Fact] public async Task Should_load_assets_with_query_and_resolve_tags() { @@ -108,13 +83,15 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries var parentId = DomainId.NewGuid(); - A.CallTo(() => assetRepository.QueryAsync(appId.Id, parentId, A._)) + var q = Q.Empty.WithODataQuery("fileName eq 'Name'"); + + A.CallTo(() => assetRepository.QueryAsync(appId.Id, parentId, q)) .Returns(ResultList.CreateFrom(8, found1, found2)); A.CallTo(() => assetEnricher.EnrichAsync(A>.That.IsSameSequenceAs(found1, found2), requestContext)) .Returns(new List { enriched1, enriched2 }); - var result = await sut.QueryAsync(requestContext, parentId, Q.Empty); + var result = await sut.QueryAsync(requestContext, parentId, q); Assert.Equal(8, result.Total); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BulkUpdateCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BulkUpdateCommandMiddlewareTests.cs index f774c9419..f93a2da71 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BulkUpdateCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BulkUpdateCommandMiddlewareTests.cs @@ -163,7 +163,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { var (id, data, query) = CreateTestData(true); - A.CallTo(() => contentQuery.QueryAsync(requestContext, A._, A.That.Matches(x => x.ParsedJsonQuery == query))) + A.CallTo(() => contentQuery.QueryAsync(requestContext, A._, A.That.Matches(x => x.JsonQuery == query))) .Returns(ResultList.CreateFrom(1, CreateContent(id))); var command = new BulkUpdateContents @@ -199,7 +199,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { var (id, data, query) = CreateTestData(true); - A.CallTo(() => contentQuery.QueryAsync(requestContext, A._, A.That.Matches(x => x.ParsedJsonQuery == query))) + A.CallTo(() => contentQuery.QueryAsync(requestContext, A._, A.That.Matches(x => x.JsonQuery == query))) .Returns(ResultList.CreateFrom(2, CreateContent(id), CreateContent(id))); var command = new BulkUpdateContents diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentsSearchSourceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentsSearchSourceTests.cs index 0c1316405..74f5a8dce 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentsSearchSourceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentsSearchSourceTests.cs @@ -176,7 +176,7 @@ namespace Squidex.Domain.Apps.Entities.Contents Assert.Empty(result); - A.CallTo(() => contentQuery.QueryAsync(ctx, A>._)) + A.CallTo(() => contentQuery.QueryAsync(ctx, A._)) .MustNotHaveHappened(); } @@ -193,7 +193,7 @@ namespace Squidex.Domain.Apps.Entities.Contents A.CallTo(() => contentIndex.SearchAsync("query~", ctx.App, A.That.IsEqualTo(searchFilter), ctx.Scope())) .Returns(ids); - A.CallTo(() => contentQuery.QueryAsync(ctx, ids)) + A.CallTo(() => contentQuery.QueryAsync(ctx, A.That.HasIds(ids))) .Returns(ResultList.CreateFrom(1, content)); A.CallTo(() => urlGenerator.ContentUI(appId, schemaId1, content.Id)) diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs index 7b935b738..36065195d 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs @@ -117,7 +117,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", assetId.ToString()); - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, MatchIdQuery(assetId))) + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A.That.HasIds(assetId))) .Returns(ResultList.CreateFrom(1)); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); @@ -146,7 +146,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", assetId.ToString()).Replace("", TestAsset.AllFields); - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, MatchIdQuery(assetId))) + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A.That.HasIds(assetId))) .Returns(ResultList.CreateFrom(1, asset)); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); @@ -344,7 +344,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", contentId.ToString()); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A.That.HasIds(contentId))) .Returns(ResultList.CreateFrom(1)); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); @@ -373,7 +373,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", TestContent.AllFields).Replace("", contentId.ToString()); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A.That.HasIds(contentId))) .Returns(ResultList.CreateFrom(1, content)); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); @@ -417,10 +417,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", contentId.ToString()); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A>._)) + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A._)) .Returns(ResultList.CreateFrom(0, contentRef)); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A.That.HasIds(contentId))) .Returns(ResultList.CreateFrom(1, content)); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); @@ -483,10 +483,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", contentRefId.ToString()); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A>._)) - .Returns(ResultList.CreateFrom(0, contentRef)); + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A.That.HasIds(contentRefId))) + .Returns(ResultList.CreateFrom(1, contentRef)); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.Id.ToString(), A.That.HasOData("?$top=30&$skip=5", contentRefId))) + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), content.SchemaId.Id.ToString(), A.That.HasOData("?$top=30&$skip=5", contentRefId))) .Returns(ResultList.CreateFrom(1, content)); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); @@ -546,10 +546,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", contentRefId.ToString()); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A>._)) - .Returns(ResultList.CreateFrom(0, contentRef)); + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A.That.HasIds(contentRefId))) + .Returns(ResultList.CreateFrom(1, contentRef)); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.Id.ToString(), A.That.HasOData("?$top=30&$skip=5", contentRefId))) + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), content.SchemaId.Id.ToString(), A.That.HasOData("?$top=30&$skip=5", contentRefId))) .Returns(ResultList.CreateFrom(1, content)); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); @@ -619,10 +619,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", contentId.ToString()); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A>._)) + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A._)) .Returns(ResultList.CreateFrom(0, contentRef)); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A.That.HasIds(contentId))) .Returns(ResultList.CreateFrom(1, content)); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); @@ -685,7 +685,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", contentId.ToString()); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A.That.HasIds(contentId))) .Returns(ResultList.CreateFrom(1, content)); A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A._)) @@ -741,10 +741,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", assetId2.ToString()); - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, MatchIdQuery(assetId1))) + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A.That.HasIds(assetId1))) .Returns(ResultList.CreateFrom(0, asset1)); - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, MatchIdQuery(assetId2))) + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A.That.HasIds(assetId2))) .Returns(ResultList.CreateFrom(0, asset2)); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query1 }, new GraphQLQuery { Query = query2 }); @@ -800,7 +800,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", contentId.ToString()); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A.That.HasIds(contentId))) .Returns(ResultList.CreateFrom(1, content)); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); @@ -810,16 +810,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL Assert.Contains("\"data\":null", json); } - private static IReadOnlyList MatchId(DomainId contentId) - { - return A>.That.Matches(x => x.Count == 1 && x[0] == contentId); - } - - private static Q MatchIdQuery(DomainId contentId) - { - return A.That.Matches(x => x.Ids.Count == 1 && x.Ids[0] == contentId); - } - private Context MatchsAssetContext() { return A.That.Matches(x => x.App == app && x.User == requestContext.User); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryTests.cs index 7f632d3e2..835a33f84 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryTests.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using NodaTime; +using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; using Squidex.Infrastructure.Queries; using Xunit; @@ -43,7 +44,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb { var ids = Enumerable.Repeat(0, 50).Select(_ => DomainId.NewGuid()).ToHashSet(); - var contents = await _.ContentRepository.QueryAsync(_.RandomApp(), ids, SearchScope.All); + var schemas = new List + { + _.RandomSchema() + }; + + var contents = await _.ContentRepository.QueryAsync(_.RandomApp(), schemas, Q.Empty.WithIds(ids), SearchScope.All); Assert.NotNull(contents); } @@ -53,7 +59,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb { var ids = Enumerable.Repeat(0, 50).Select(_ => DomainId.NewGuid()).ToHashSet(); - var contents = await _.ContentRepository.QueryAsync(_.RandomApp(), _.RandomSchema(), ids, SearchScope.All); + var contents = await _.ContentRepository.QueryAsync(_.RandomApp(), _.RandomSchema(), Q.Empty.WithIds(ids), SearchScope.All); Assert.NotNull(contents); } @@ -80,7 +86,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb Filter = F.Eq("data.value.iv", 12) }; - var contents = await _.ContentRepository.QueryAsync(_.RandomApp(), _.RandomSchema(), query, null, SearchScope.Published); + var contents = await _.ContentRepository.QueryAsync(_.RandomApp(), _.RandomSchema(), Q.Empty.WithQuery(query), SearchScope.Published); Assert.NotEmpty(contents); } @@ -108,9 +114,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb { var query = new ClrQuery(); - var contents = await QueryAsync(query, id: DomainId.NewGuid()); + var contents = await QueryAsync(query, reference: DomainId.NewGuid()); - Assert.NotEmpty(contents); + Assert.Empty(contents); } [Fact] @@ -163,12 +169,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb Filter = F.Eq("data.value.iv", 12) }; - var contents = await QueryAsync(query, 1000, 0, id: DomainId.NewGuid()); + var contents = await QueryAsync(query, 1000, 0, reference: DomainId.NewGuid()); Assert.Empty(contents); } - private async Task> QueryAsync(ClrQuery clrQuery, int take = 1000, int skip = 100, DomainId? id = null) + private async Task> QueryAsync(ClrQuery clrQuery, int take = 1000, int skip = 100, DomainId reference = default) { if (clrQuery.Take == long.MaxValue) { @@ -188,7 +194,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb }; } - var contents = await _.ContentRepository.QueryAsync(_.RandomApp(), _.RandomSchema(), clrQuery, id, SearchScope.All); + var q = + Q.Empty + .WithQuery(clrQuery) + .WithReference(reference); + + var contents = await _.ContentRepository.QueryAsync(_.RandomApp(), _.RandomSchema(), q, SearchScope.All); return contents; } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryParserTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryParserTests.cs index 7671ef337..67549c7b6 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryParserTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryParserTests.cs @@ -46,29 +46,39 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries } [Fact] - public async Task Should_use_existing_query() + public async Task Should_throw_if_odata_query_is_invalid() { - var clrQuery = new ClrQuery(); + var query = Q.Empty.WithODataQuery("$filter=invalid"); + + await Assert.ThrowsAsync(() => sut.ParseAsync(requestContext, query, schema).AsTask()); + } - var parsed = await sut.ParseQueryAsync(requestContext, schema, Q.Empty.WithQuery(clrQuery)); + [Fact] + public async Task Should_throw_if_json_query_is_invalid() + { + var query = Q.Empty.WithJsonQuery("invalid"); - Assert.Same(parsed, clrQuery); + await Assert.ThrowsAsync(() => sut.ParseAsync(requestContext, query, schema).AsTask()); } [Fact] - public async Task Should_throw_if_odata_query_is_invalid() + public async Task Should_parse_odata_query_without_schema() { - var query = Q.Empty.WithODataQuery("$filter=invalid"); + var query = Q.Empty.WithODataQuery("$filter=status eq 'Draft'"); + + var q = await sut.ParseAsync(requestContext, query); - await Assert.ThrowsAsync(() => sut.ParseQueryAsync(requestContext, schema, query).AsTask()); + Assert.Equal("Filter: status == 'Draft'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); } [Fact] - public async Task Should_throw_if_json_query_is_invalid() + public async Task Should_parse_json_query_without_schema() { - var query = Q.Empty.WithJsonQuery("invalid"); + var query = Q.Empty.WithJsonQuery("{ 'filter': { 'path': 'status', 'op': 'eq', 'value': 'Draft' } }"); + + var q = await sut.ParseAsync(requestContext, query); - await Assert.ThrowsAsync(() => sut.ParseQueryAsync(requestContext, schema, query).AsTask()); + Assert.Equal("Filter: status == 'Draft'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); } [Fact] @@ -76,9 +86,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { var query = Q.Empty.WithODataQuery("$top=100&$orderby=data/firstName/iv asc&$search=Hello World"); - var parsed = await sut.ParseQueryAsync(requestContext, schema, query); + var q = await sut.ParseAsync(requestContext, query, schema); - Assert.Equal("FullText: 'Hello World'; Take: 100; Sort: data.firstName.iv Ascending, id Ascending", parsed.ToString()); + Assert.Equal("FullText: 'Hello World'; Take: 100; Sort: data.firstName.iv Ascending, id Ascending", q.Query.ToString()); } [Fact] @@ -86,9 +96,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { var query = Q.Empty.WithODataQuery("$top=200&$filter=data/firstName/iv eq 'ABC'"); - var parsed = await sut.ParseQueryAsync(requestContext, schema, query); + var q = await sut.ParseAsync(requestContext, query, schema); - Assert.Equal("Filter: data.firstName.iv == 'ABC'; Take: 200; Sort: lastModified Descending, id Ascending", parsed.ToString()); + Assert.Equal("Filter: data.firstName.iv == 'ABC'; Take: 200; Sort: lastModified Descending, id Ascending", q.Query.ToString()); } [Fact] @@ -96,9 +106,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { var query = Q.Empty.WithJsonQuery(Json("{ 'filter': { 'path': 'data.firstName.iv', 'op': 'eq', 'value': 'ABC' } }")); - var parsed = await sut.ParseQueryAsync(requestContext, schema, query); + var q = await sut.ParseAsync(requestContext, query, schema); - Assert.Equal("Filter: data.firstName.iv == 'ABC'; Take: 30; Sort: lastModified Descending, id Ascending", parsed.ToString()); + Assert.Equal("Filter: data.firstName.iv == 'ABC'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); } [Fact] @@ -110,9 +120,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries Filter = new CompareFilter("data.firstName.iv", CompareOperator.Equals, JsonValue.Create("ABC")) }); - var parsed = await sut.ParseQueryAsync(requestContext, schema, query); + var q = await sut.ParseAsync(requestContext, query, schema); - Assert.Equal("Filter: data.firstName.iv == 'ABC'; Take: 30; Sort: lastModified Descending, id Ascending", parsed.ToString()); + Assert.Equal("Filter: data.firstName.iv == 'ABC'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); } [Fact] @@ -120,9 +130,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { var query = Q.Empty.WithJsonQuery(Json("{ 'fullText': 'Hello' }")); - var parsed = await sut.ParseQueryAsync(requestContext, schema, query); + var q = await sut.ParseAsync(requestContext, query, schema); - Assert.Equal("FullText: 'Hello'; Take: 30; Sort: lastModified Descending, id Ascending", parsed.ToString()); + Assert.Equal("FullText: 'Hello'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); } [Fact] @@ -134,9 +144,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries FullText = "Hello" }); - var parsed = await sut.ParseQueryAsync(requestContext, schema, query); + var q = await sut.ParseAsync(requestContext, query, schema); - Assert.Equal("FullText: 'Hello'; Take: 30; Sort: lastModified Descending, id Ascending", parsed.ToString()); + Assert.Equal("FullText: 'Hello'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); } [Fact] @@ -144,9 +154,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { var query = Q.Empty; - var parsed = await sut.ParseQueryAsync(requestContext, schema, query); + var q = await sut.ParseAsync(requestContext, query, schema); - Assert.Equal("Take: 30; Sort: lastModified Descending, id Ascending", parsed.ToString()); + Assert.Equal("Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); } [Fact] @@ -154,9 +164,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { var query = Q.Empty.WithODataQuery("$top=300&$skip=20"); - var parsed = await sut.ParseQueryAsync(requestContext, schema, query); + var q = await sut.ParseAsync(requestContext, query, schema); - Assert.Equal("Skip: 20; Take: 200; Sort: lastModified Descending, id Ascending", parsed.ToString()); + Assert.Equal("Skip: 20; Take: 200; Sort: lastModified Descending, id Ascending", q.Query.ToString()); } [Fact] @@ -164,9 +174,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { var query = Q.Empty.WithODataQuery("$top=300&$skip=20&$orderby=id desc"); - var parsed = await sut.ParseQueryAsync(requestContext, schema, query); + var q = await sut.ParseAsync(requestContext, query, schema); - Assert.Equal("Skip: 20; Take: 200; Sort: id Descending", parsed.ToString()); + Assert.Equal("Skip: 20; Take: 200; Sort: id Descending", q.Query.ToString()); } private static string Json(string text) diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs index 0de10819b..db7d5742d 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs @@ -16,7 +16,6 @@ using Squidex.Domain.Apps.Entities.Contents.Repositories; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; -using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Security; using Squidex.Shared; @@ -53,8 +52,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name, A._)) .Returns(schema); - A.CallTo(() => queryParser.ParseQueryAsync(A._, schema, A._)) - .Returns(new ClrQuery()); + A.CallTo(() => appProvider.GetSchemasAsync(appId.Id)) + .Returns(new List { schema }); + + A.CallTo(() => queryParser.ParseAsync(A._, A._, A._)) + .ReturnsLazily(c => new ValueTask(c.GetArgument(1)!)); sut = new ContentQueryService( appProvider, @@ -180,18 +182,18 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries [InlineData(0, 0, SearchScope.Published)] public async Task QueryAsync_should_return_contents(int isFrontend, int unpublished, SearchScope scope) { - var reference = DomainId.NewGuid(); - var ctx = CreateContext(isFrontend: isFrontend == 1, allowSchema: true) .WithUnpublished(unpublished == 1); var content = CreateContent(contentId); - A.CallTo(() => contentRepository.QueryAsync(ctx.App, schema, A._, reference, scope)) + var q = Q.Empty.WithReference(DomainId.NewGuid()); + + A.CallTo(() => contentRepository.QueryAsync(ctx.App, schema, q, scope)) .Returns(ResultList.CreateFrom(5, content)); - var result = await sut.QueryAsync(ctx, schemaId.Name, Q.Empty.WithReference(reference)); + var result = await sut.QueryAsync(ctx, schemaId.Name, q); Assert.Equal(contentData, result[0].Data); Assert.Equal(contentId, result[0].Id); @@ -200,16 +202,18 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries } [Fact] - public async Task QueryByIds_should_not_return_contents_if_user_has_no_permission() + public async Task QueryAll_should_not_return_contents_if_user_has_no_permission() { var ctx = CreateContext(isFrontend: false, allowSchema: false); var ids = Enumerable.Range(0, 5).Select(x => DomainId.NewGuid()).ToList(); - A.CallTo(() => contentRepository.QueryAsync(ctx.App, A>._, SearchScope.All)) - .Returns(ids.Select(x => (CreateContent(x), schema)).ToList()); + var q = Q.Empty.WithIds(ids); + + A.CallTo(() => contentRepository.QueryAsync(ctx.App, A>.That.Matches(x => x.Count == 0), q, SearchScope.All)) + .Returns(ResultList.Create(0, ids.Select(CreateContent))); - var result = await sut.QueryAsync(ctx, ids); + var result = await sut.QueryAsync(ctx, q); Assert.Empty(result); } @@ -219,7 +223,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries [InlineData(1, 1, SearchScope.All)] [InlineData(0, 1, SearchScope.All)] [InlineData(0, 0, SearchScope.Published)] - public async Task QueryByIds_should_return_contents(int isFrontend, int unpublished, SearchScope scope) + public async Task QueryAll_should_return_contents(int isFrontend, int unpublished, SearchScope scope) { var ctx = CreateContext(isFrontend: isFrontend == 1, allowSchema: true) @@ -227,25 +231,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries var ids = Enumerable.Range(0, 5).Select(x => DomainId.NewGuid()).ToList(); - A.CallTo(() => contentRepository.QueryAsync(ctx.App, A>._, scope)) - .Returns(ids.Select(x => (CreateContent(x), schema)).ToList()); - - var result = await sut.QueryAsync(ctx, ids); - - Assert.Equal(ids, result.Select(x => x.Id).ToList()); - } - - [Fact] - public async Task QueryByIds_should_not_call_repository_if_no_id_defined() - { - var ctx = CreateContext(isFrontend: false, allowSchema: true); + var q = Q.Empty.WithIds(ids); - var result = await sut.QueryAsync(ctx, new List()); + A.CallTo(() => contentRepository.QueryAsync(ctx.App, A>.That.Matches(x => x.Count == 1), q, scope)) + .Returns(ResultList.Create(5, ids.Select(CreateContent))); - Assert.Empty(result); + var result = await sut.QueryAsync(ctx, q); - A.CallTo(() => contentRepository.QueryAsync(ctx.App, A>._, A._)) - .MustNotHaveHappened(); + Assert.Equal(ids, result.Select(x => x.Id).ToList()); } private void SetupEnricher() diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveAssetsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveAssetsTests.cs index 3824943de..a3a087f29 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveAssetsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveAssetsTests.cs @@ -75,52 +75,52 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries [Fact] public async Task Should_add_assets_id_and_versions_as_dependency() { - var document1 = CreateAsset(DomainId.NewGuid(), 3, AssetType.Unknown, "Document1.docx"); - var document2 = CreateAsset(DomainId.NewGuid(), 4, AssetType.Unknown, "Document2.docx"); + var doc1 = CreateAsset(DomainId.NewGuid(), 3, AssetType.Unknown, "Document1.docx"); + var doc2 = CreateAsset(DomainId.NewGuid(), 4, AssetType.Unknown, "Document2.docx"); var contents = new[] { CreateContent( - new[] { document1.Id }, - new[] { document1.Id }), + new[] { doc1.Id }, + new[] { doc1.Id }), CreateContent( - new[] { document2.Id }, - new[] { document2.Id }) + new[] { doc2.Id }, + new[] { doc2.Id }) }; - A.CallTo(() => assetQuery.QueryAsync(A.That.Matches(x => !x.ShouldEnrichAsset()), null, A.That.Matches(x => x.Ids.Count == 2))) - .Returns(ResultList.CreateFrom(4, document1, document2)); + A.CallTo(() => assetQuery.QueryAsync(A.That.Matches(x => !x.ShouldEnrichAsset()), null, A.That.HasIds(doc1.Id, doc2.Id))) + .Returns(ResultList.CreateFrom(4, doc1, doc2)); await sut.EnrichAsync(requestContext, contents, schemaProvider); - A.CallTo(() => requestCache.AddDependency(document1.UniqueId, document1.Version)) + A.CallTo(() => requestCache.AddDependency(doc1.UniqueId, doc1.Version)) .MustHaveHappened(); - A.CallTo(() => requestCache.AddDependency(document2.UniqueId, document2.Version)) + A.CallTo(() => requestCache.AddDependency(doc2.UniqueId, doc2.Version)) .MustHaveHappened(); } [Fact] public async Task Should_enrich_with_asset_urls() { - var image1 = CreateAsset(DomainId.NewGuid(), 1, AssetType.Image, "Image1.png"); - var image2 = CreateAsset(DomainId.NewGuid(), 2, AssetType.Image, "Image2.png"); + var img1 = CreateAsset(DomainId.NewGuid(), 1, AssetType.Image, "Image1.png"); + var img2 = CreateAsset(DomainId.NewGuid(), 2, AssetType.Image, "Image2.png"); - var document1 = CreateAsset(DomainId.NewGuid(), 3, AssetType.Unknown, "Document1.png"); - var document2 = CreateAsset(DomainId.NewGuid(), 4, AssetType.Unknown, "Document2.png"); + var doc1 = CreateAsset(DomainId.NewGuid(), 3, AssetType.Unknown, "Document1.png"); + var doc2 = CreateAsset(DomainId.NewGuid(), 4, AssetType.Unknown, "Document2.png"); var contents = new[] { CreateContent( - new[] { image1.Id }, - new[] { image2.Id, image1.Id }), + new[] { img1.Id }, + new[] { img2.Id, img1.Id }), CreateContent( - new[] { document1.Id }, - new[] { document2.Id, document1.Id }) + new[] { doc1.Id }, + new[] { doc2.Id, doc1.Id }) }; - A.CallTo(() => assetQuery.QueryAsync(A.That.Matches(x => !x.ShouldEnrichAsset()), null, A.That.Matches(x => x.Ids.Count == 4))) - .Returns(ResultList.CreateFrom(4, image1, image2, document1, document2)); + A.CallTo(() => assetQuery.QueryAsync(A.That.Matches(x => !x.ShouldEnrichAsset()), null, A.That.HasIds(doc1.Id, doc2.Id, img1.Id, img2.Id))) + .Returns(ResultList.CreateFrom(4, img1, img2, doc1, doc2)); await sut.EnrichAsync(requestContext, contents, schemaProvider); @@ -128,20 +128,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries new NamedContentData() .AddField("asset1", new ContentFieldData() - .AddValue("iv", JsonValue.Array($"url/to/{image1.Id}", image1.FileName))) + .AddValue("iv", JsonValue.Array($"url/to/{img1.Id}", img1.FileName))) .AddField("asset2", new ContentFieldData() - .AddValue("en", JsonValue.Array($"url/to/{image2.Id}", image2.FileName))), + .AddValue("en", JsonValue.Array($"url/to/{img2.Id}", img2.FileName))), contents[0].ReferenceData); Assert.Equal( new NamedContentData() .AddField("asset1", new ContentFieldData() - .AddValue("iv", JsonValue.Array(document1.FileName))) + .AddValue("iv", JsonValue.Array(doc1.FileName))) .AddField("asset2", new ContentFieldData() - .AddValue("en", JsonValue.Array(document2.FileName))), + .AddValue("en", JsonValue.Array(doc2.FileName))), contents[1].ReferenceData); } @@ -212,7 +212,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries Assert.NotNull(contents[0].ReferenceData); - A.CallTo(() => assetQuery.QueryAsync(A.That.Matches(x => !x.ShouldEnrichAsset()), null, A.That.Matches(x => x.Ids.Count == 1))) + A.CallTo(() => assetQuery.QueryAsync(A.That.Matches(x => !x.ShouldEnrichAsset()), null, A.That.HasIds(id1))) .MustHaveHappened(); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveReferencesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveReferencesTests.cs index f87ec446a..2c7d11cea 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveReferencesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveReferencesTests.cs @@ -102,7 +102,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries CreateContent(new[] { ref1_2.Id }, new[] { ref2_2.Id }) }; - A.CallTo(() => contentQuery.QueryAsync(A._, A>.That.Matches(x => x.Count == 4))) + A.CallTo(() => contentQuery.QueryAsync(A._, A.That.HasIds(ref1_1.Id, ref1_2.Id, ref2_1.Id, ref2_2.Id))) .Returns(ResultList.CreateFrom(4, ref1_1, ref1_2, ref2_1, ref2_2)); await sut.EnrichAsync(requestContext, contents, schemaProvider); @@ -140,7 +140,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries CreateContent(new[] { ref1_2.Id }, new[] { ref2_2.Id }) }; - A.CallTo(() => contentQuery.QueryAsync(A.That.Matches(x => !x.ShouldEnrichContent()), A>.That.Matches(x => x.Count == 4))) + A.CallTo(() => contentQuery.QueryAsync(A.That.Matches(x => !x.ShouldEnrichContent()), A.That.HasIds(ref1_1.Id, ref1_2.Id, ref2_1.Id, ref2_2.Id))) .Returns(ResultList.CreateFrom(4, ref1_1, ref1_2, ref2_1, ref2_2)); await sut.EnrichAsync(requestContext, contents, schemaProvider); @@ -192,7 +192,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries CreateContent(new[] { ref1_2.Id }, new[] { ref2_1.Id, ref2_2.Id }) }; - A.CallTo(() => contentQuery.QueryAsync(A.That.Matches(x => !x.ShouldEnrichContent()), A>.That.Matches(x => x.Count == 4))) + A.CallTo(() => contentQuery.QueryAsync(A.That.Matches(x => !x.ShouldEnrichContent()), A.That.HasIds(ref1_1.Id, ref1_2.Id, ref2_1.Id, ref2_2.Id))) .Returns(ResultList.CreateFrom(4, ref1_1, ref1_2, ref2_1, ref2_2)); await sut.EnrichAsync(requestContext, contents, schemaProvider); @@ -244,7 +244,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries Assert.Null(contents[0].ReferenceData); - A.CallTo(() => contentQuery.QueryAsync(A._, A>._)) + A.CallTo(() => contentQuery.QueryAsync(A._, A._)) .MustNotHaveHappened(); } @@ -262,7 +262,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries Assert.Null(contents[0].ReferenceData); - A.CallTo(() => contentQuery.QueryAsync(A._, A>._)) + A.CallTo(() => contentQuery.QueryAsync(A._, A._)) .MustNotHaveHappened(); } @@ -278,7 +278,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries Assert.NotNull(contents[0].ReferenceData); - A.CallTo(() => contentQuery.QueryAsync(A._, A>._)) + A.CallTo(() => contentQuery.QueryAsync(A._, A._)) .MustNotHaveHappened(); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferenceFluidExtensionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferenceFluidExtensionTests.cs index a2c3eb5d0..9cb9102c7 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferenceFluidExtensionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferenceFluidExtensionTests.cs @@ -56,10 +56,10 @@ namespace Squidex.Domain.Apps.Entities.Contents AppId = appId }; - A.CallTo(() => contentQuery.QueryAsync(A._, A>.That.Contains(referenceId1))) + A.CallTo(() => contentQuery.QueryAsync(A._, A.That.HasIds(referenceId1))) .Returns(ResultList.CreateFrom(1, reference1)); - A.CallTo(() => contentQuery.QueryAsync(A._, A>.That.Contains(referenceId2))) + A.CallTo(() => contentQuery.QueryAsync(A._, A.That.HasIds(referenceId2))) .Returns(ResultList.CreateFrom(1, reference2)); var vars = new TemplateVars diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs index ef6df0429..1fc794f75 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs @@ -22,14 +22,24 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers public static Q HasOData(this INegatableArgumentConstraintManager that, string odata) { - return that.HasOData(odata, null); + return that.HasOData(odata, default); } - public static Q HasOData(this INegatableArgumentConstraintManager that, string odata, DomainId? reference = null) + public static Q HasOData(this INegatableArgumentConstraintManager that, string odata, DomainId reference = default) { return that.Matches(x => x.ODataQuery == odata && x.Reference == reference); } + public static Q HasIds(this INegatableArgumentConstraintManager that, params DomainId[] ids) + { + return that.Matches(x => x.Ids != null && x.Ids.SetEquals(ids)); + } + + public static Q HasIds(this INegatableArgumentConstraintManager that, IEnumerable ids) + { + return that.Matches(x => x.Ids != null && x.Ids.SetEquals(ids.ToHashSet())); + } + public static ClrQuery Is(this INegatableArgumentConstraintManager that, string query) { return that.Matches(x => x.ToString() == query);