diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs index b5f371186..4c3dd0c16 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs @@ -9,8 +9,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Microsoft.OData; -using Microsoft.OData.UriParser; using MongoDB.Driver; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets.Edm; @@ -23,12 +21,9 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets { public sealed partial class MongoAssetRepository : MongoRepositoryBase, IAssetRepository { - private readonly EdmModelBuilder modelBuilder; - - public MongoAssetRepository(IMongoDatabase database, EdmModelBuilder modelBuilder) + public MongoAssetRepository(IMongoDatabase database) : base(database) { - this.modelBuilder = modelBuilder; } protected override string CollectionName() @@ -48,32 +43,55 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets public async Task> QueryAsync(Guid appId, string query = null) { - var parsedQuery = ParseQuery(query); + try + { + var odataQuery = EdmAssetQuery.ParseQuery(query); - var assetEntities = - await Collection - .Find(parsedQuery, appId) - .Skip(parsedQuery) - .Take(parsedQuery) - .SortByDescending(x => x.LastModified) - .ToListAsync(); + var filter = FindExtensions.BuildQuery(odataQuery, appId); - var assetCount = await Collection.Find(parsedQuery, appId).CountAsync(); + var contentCount = Collection.Find(filter).CountAsync(); + var contentItems = + Collection.Find(filter) + .AssetTake(odataQuery) + .AssetSkip(odataQuery) + .AssetSort(odataQuery) + .ToListAsync(); - return ResultList.Create(assetEntities.OfType().ToList(), assetCount); + await Task.WhenAll(contentItems, contentCount); + + return ResultList.Create(contentItems.Result, contentCount.Result); + } + catch (NotSupportedException) + { + throw new ValidationException("This odata operation is not supported."); + } + catch (NotImplementedException) + { + throw new ValidationException("This odata operation is not supported."); + } + catch (MongoQueryException ex) + { + if (ex.Message.Contains("17406")) + { + throw new DomainException("Result set is too large to be retrieved. Use $top parameter to reduce the number of items."); + } + else + { + throw; + } + } } public async Task> QueryAsync(Guid appId, HashSet ids) { - var find = Collection - .Find(Filter.In(x => x.Id, ids)) - .SortByDescending(x => x.LastModified); + var find = Collection.Find(Filter.In(x => x.Id, ids)).SortByDescending(x => x.LastModified); - var assetEntities = find.ToListAsync(); + var assetItems = find.ToListAsync(); var assetCount = find.CountAsync(); - await Task.WhenAll(assetEntities, assetCount); - return ResultList.Create(assetEntities.Result.OfType().ToList(), assetCount.Result); + await Task.WhenAll(assetItems, assetCount); + + return ResultList.Create(assetItems.Result.OfType().ToList(), assetCount.Result); } public async Task FindAssetAsync(Guid id) @@ -84,19 +102,5 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets return assetEntity; } - - private ODataUriParser ParseQuery(string query) - { - try - { - var model = modelBuilder.EdmModel; - - return model.ParseQuery(query); - } - catch (ODataException ex) - { - throw new ValidationException($"Failed to parse query: {ex.Message}", ex); - } - } } } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs index 89bdc2c09..3e1cf0c22 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs @@ -34,6 +34,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets public async Task WriteAsync(Guid key, AssetState value, long oldVersion, long newVersion) { var entity = SimpleMapper.Map(value, new MongoAssetEntity()); + entity.Version = newVersion; await Collection.ReplaceOneAsync(x => x.Id == key && x.Version == oldVersion, entity, Upsert); diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/AssetModel.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/AssetModel.cs new file mode 100644 index 000000000..97e565942 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/AssetModel.cs @@ -0,0 +1,47 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.OData.Edm; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors +{ + public static class AssetModel + { + public static readonly IEdmModel Edm; + + static AssetModel() + { + var entityType = new EdmEntityType("Squidex", "Asset"); + + entityType.AddStructuralProperty(nameof(MongoAssetEntity.Id), EdmPrimitiveTypeKind.Guid); + entityType.AddStructuralProperty(nameof(MongoAssetEntity.AppId), EdmPrimitiveTypeKind.Guid); + entityType.AddStructuralProperty(nameof(MongoAssetEntity.Created), EdmPrimitiveTypeKind.DateTimeOffset); + entityType.AddStructuralProperty(nameof(MongoAssetEntity.CreatedBy), EdmPrimitiveTypeKind.String); + entityType.AddStructuralProperty(nameof(MongoAssetEntity.LastModified), EdmPrimitiveTypeKind.DateTimeOffset); + entityType.AddStructuralProperty(nameof(MongoAssetEntity.LastModifiedBy), EdmPrimitiveTypeKind.String); + entityType.AddStructuralProperty(nameof(MongoAssetEntity.Version), EdmPrimitiveTypeKind.Int64); + entityType.AddStructuralProperty(nameof(MongoAssetEntity.FileName), EdmPrimitiveTypeKind.String); + entityType.AddStructuralProperty(nameof(MongoAssetEntity.FileSize), EdmPrimitiveTypeKind.Int64); + entityType.AddStructuralProperty(nameof(MongoAssetEntity.FileVersion), EdmPrimitiveTypeKind.Int64); + entityType.AddStructuralProperty(nameof(MongoAssetEntity.IsImage), EdmPrimitiveTypeKind.Boolean); + entityType.AddStructuralProperty(nameof(MongoAssetEntity.MimeType), EdmPrimitiveTypeKind.String); + entityType.AddStructuralProperty(nameof(MongoAssetEntity.PixelHeight), EdmPrimitiveTypeKind.Int32); + entityType.AddStructuralProperty(nameof(MongoAssetEntity.PixelWidth), EdmPrimitiveTypeKind.Int32); + + var container = new EdmEntityContainer("Squidex", "Container"); + + container.AddEntitySet("AssetSet", entityType); + + var model = new EdmModel(); + + model.AddElement(container); + model.AddElement(entityType); + + Edm = model; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FilterBuilder.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FilterBuilder.cs deleted file mode 100644 index 00e192f2e..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FilterBuilder.cs +++ /dev/null @@ -1,57 +0,0 @@ -// ========================================================================== -// FilterBuilder.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== -using System.Collections.Generic; -using Microsoft.OData; -using Microsoft.OData.UriParser; -using MongoDB.Driver; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors -{ - public static class FilterBuilder - { - private static readonly FilterDefinitionBuilder Filter = Builders.Filter; - - public static List> Build(ODataUriParser query) - { - List> filters = new List>(); - - SearchClause search; - try - { - search = query.ParseSearch(); - } - catch (ODataException ex) - { - throw new ValidationException("Query $search clause not valid.", new ValidationError(ex.Message)); - } - - if (search != null) - { - filters.Add(Filter.Text(SearchTermVisitor.Visit(search.Expression).ToString())); - } - - FilterClause filter; - try - { - filter = query.ParseFilter(); - } - catch (ODataException ex) - { - throw new ValidationException("Query $filter clause not valid.", new ValidationError(ex.Message)); - } - - if (filter != null) - { - filters.Add(FilterVisitor.Visit(filter.Expression)); - } - - return filters; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FilterVisitor.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FilterVisitor.cs deleted file mode 100644 index 2ef2e94eb..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FilterVisitor.cs +++ /dev/null @@ -1,155 +0,0 @@ -// ========================================================================== -// FilterVisitor.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== -using System; -using System.Linq; -using Microsoft.OData.UriParser; -using MongoDB.Bson; -using MongoDB.Driver; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors -{ - public class FilterVisitor : QueryNodeVisitor> - { - private static readonly FilterDefinitionBuilder Filter = Builders.Filter; - - public static FilterDefinition Visit(QueryNode node) - { - var visitor = new FilterVisitor(); - - return node.Accept(visitor); - } - - public override FilterDefinition Visit(ConvertNode nodeIn) - { - return nodeIn.Source.Accept(this); - } - - public override FilterDefinition Visit(UnaryOperatorNode nodeIn) - { - if (nodeIn.OperatorKind == UnaryOperatorKind.Not) - { - return Filter.Not(nodeIn.Operand.Accept(this)); - } - - throw new NotSupportedException(); - } - - public override FilterDefinition Visit(SingleValueFunctionCallNode nodeIn) - { - var fieldNode = nodeIn.Parameters.ElementAt(0); - var valueNode = nodeIn.Parameters.ElementAt(1); - - if (string.Equals(nodeIn.Name, "endswith", StringComparison.OrdinalIgnoreCase)) - { - var value = BuildRegex(valueNode, v => v + "$"); - - return Filter.Regex(BuildFieldDefinition(fieldNode), value); - } - - if (string.Equals(nodeIn.Name, "startswith", StringComparison.OrdinalIgnoreCase)) - { - var value = BuildRegex(valueNode, v => "^" + v); - - return Filter.Regex(BuildFieldDefinition(fieldNode), value); - } - - if (string.Equals(nodeIn.Name, "contains", StringComparison.OrdinalIgnoreCase)) - { - var value = BuildRegex(valueNode, v => v); - - return Filter.Regex(BuildFieldDefinition(fieldNode), value); - } - - throw new NotSupportedException(); - } - - public override FilterDefinition Visit(BinaryOperatorNode nodeIn) - { - if (nodeIn.OperatorKind == BinaryOperatorKind.And) - { - return Filter.And(nodeIn.Left.Accept(this), nodeIn.Right.Accept(this)); - } - - if (nodeIn.OperatorKind == BinaryOperatorKind.Or) - { - return Filter.Or(nodeIn.Left.Accept(this), nodeIn.Right.Accept(this)); - } - - if (nodeIn.Left is SingleValueFunctionCallNode functionNode) - { - var regexFilter = Visit(functionNode); - - var value = BuildValue(nodeIn.Right); - - if (value is bool booleanRight) - { - if ((nodeIn.OperatorKind == BinaryOperatorKind.Equal && !booleanRight) || - (nodeIn.OperatorKind == BinaryOperatorKind.NotEqual && booleanRight)) - { - regexFilter = Filter.Not(regexFilter); - } - - return regexFilter; - } - } - else - { - if (nodeIn.OperatorKind == BinaryOperatorKind.NotEqual) - { - var field = BuildFieldDefinition(nodeIn.Left); - - return Filter.Or( - Filter.Not(Filter.Exists(field)), - Filter.Ne(field, BuildValue(nodeIn.Right))); - } - - if (nodeIn.OperatorKind == BinaryOperatorKind.Equal) - { - return Filter.Eq(BuildFieldDefinition(nodeIn.Left), BuildValue(nodeIn.Right)); - } - - if (nodeIn.OperatorKind == BinaryOperatorKind.LessThan) - { - return Filter.Lt(BuildFieldDefinition(nodeIn.Left), BuildValue(nodeIn.Right)); - } - - if (nodeIn.OperatorKind == BinaryOperatorKind.LessThanOrEqual) - { - return Filter.Lte(BuildFieldDefinition(nodeIn.Left), BuildValue(nodeIn.Right)); - } - - if (nodeIn.OperatorKind == BinaryOperatorKind.GreaterThan) - { - return Filter.Gt(BuildFieldDefinition(nodeIn.Left), BuildValue(nodeIn.Right)); - } - - if (nodeIn.OperatorKind == BinaryOperatorKind.GreaterThanOrEqual) - { - return Filter.Gte(BuildFieldDefinition(nodeIn.Left), BuildValue(nodeIn.Right)); - } - } - - throw new NotSupportedException(); - } - - private static BsonRegularExpression BuildRegex(QueryNode node, Func formatter) - { - return new BsonRegularExpression(formatter(BuildValue(node).ToString()), "i"); - } - - private FieldDefinition BuildFieldDefinition(QueryNode nodeIn) - { - return PropertyVisitor.Visit(nodeIn); - } - - private static object BuildValue(QueryNode nodeIn) - { - return ConstantVisitor.Visit(nodeIn); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs index 711a7f58f..f2aec0c02 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using Microsoft.OData.UriParser; using MongoDB.Bson; using MongoDB.Driver; +using Squidex.Infrastructure.MongoDb.OData; namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors { @@ -17,48 +18,21 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors { private static readonly FilterDefinitionBuilder Filter = Builders.Filter; - public static IFindFluent Sort(this IFindFluent cursor, ODataUriParser query) + public static IFindFluent AssetSort(this IFindFluent cursor, ODataUriParser query) { - return cursor.Sort(SortBuilder.BuildSort(query)); - } - - public static IFindFluent Take(this IFindFluent cursor, ODataUriParser query) - { - var top = query.ParseTop(); - - if (top.HasValue) - { - cursor = cursor.Limit(Math.Min((int)top.Value, 200)); - } - else - { - cursor = cursor.Limit(20); - } + var sort = query.BuildSort(); - return cursor; + return sort != null ? cursor.Sort(sort) : cursor.SortByDescending(x => x.LastModified); } - public static IFindFluent Skip(this IFindFluent cursor, ODataUriParser query) + public static IFindFluent AssetTake(this IFindFluent cursor, ODataUriParser query) { - var skip = query.ParseSkip(); - - if (skip.HasValue) - { - cursor = cursor.Skip((int)skip.Value); - } - else - { - cursor = cursor.Skip(null); - } - - return cursor; + return cursor.Take(query, 200, 20); } - public static IFindFluent Find(this IMongoCollection cursor, ODataUriParser query, Guid appId) + public static IFindFluent AssetSkip(this IFindFluent cursor, ODataUriParser query) { - var filter = BuildQuery(query, appId); - - return cursor.Find(filter); + return cursor.Skip(query); } public static FilterDefinition BuildQuery(ODataUriParser query, Guid appId) @@ -69,7 +43,19 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors Filter.Eq(x => x.IsDeleted, false) }; - filters.AddRange(FilterBuilder.Build(query)); + var filter = query.BuildFilter(null, false); + + if (filter.Filter != null) + { + if (filter.Last) + { + filters.Add(filter.Filter); + } + else + { + filters.Insert(0, filter.Filter); + } + } if (filters.Count > 1) { diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/SearchTermVisitor.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/SearchTermVisitor.cs deleted file mode 100644 index 23366fde2..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/SearchTermVisitor.cs +++ /dev/null @@ -1,41 +0,0 @@ -// ========================================================================== -// SearchTermVisitor.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== -using System; -using Microsoft.OData.UriParser; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors -{ - public class SearchTermVisitor : QueryNodeVisitor - { - private static readonly SearchTermVisitor Instance = new SearchTermVisitor(); - - private SearchTermVisitor() - { - } - - public static object Visit(QueryNode node) - { - return node.Accept(Instance); - } - - public override string Visit(BinaryOperatorNode nodeIn) - { - if (nodeIn.OperatorKind == BinaryOperatorKind.And) - { - return nodeIn.Left.Accept(this) + " " + nodeIn.Right.Accept(this); - } - - throw new NotSupportedException(); - } - - public override string Visit(SearchTermNode nodeIn) - { - return nodeIn.Text; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs index e44eb84ba..a8ad1abcc 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs @@ -75,24 +75,19 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, ODataUriParser odataQuery) { - FilterDefinition filter; try { - filter = FindExtensions.BuildQuery(odataQuery, schema.Id, schema.SchemaDef, status); - } - catch (NotSupportedException) - { - throw new ValidationException("This odata operation is not supported."); - } - catch (NotImplementedException) - { - throw new ValidationException("This odata operation is not supported."); - } + var propertyCalculator = FindExtensions.CreatePropertyCalculator(schema.SchemaDef); + + var filter = FindExtensions.BuildQuery(odataQuery, schema.Id, status, propertyCalculator); - try - { - var contentItems = Collection.Find(filter).Take(odataQuery).Skip(odataQuery).Sort(odataQuery, schema.SchemaDef).ToListAsync(); var contentCount = Collection.Find(filter).CountAsync(); + var contentItems = + Collection.Find(filter) + .ContentTake(odataQuery) + .ContentSkip(odataQuery) + .ContentSort(odataQuery, propertyCalculator) + .ToListAsync(); await Task.WhenAll(contentItems, contentCount); @@ -103,6 +98,14 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents return ResultList.Create(contentItems.Result, contentCount.Result); } + catch (NotSupportedException) + { + throw new ValidationException("This odata operation is not supported."); + } + catch (NotImplementedException) + { + throw new ValidationException("This odata operation is not supported."); + } catch (MongoQueryException ex) { if (ex.Message.Contains("17406")) diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/ConstantVisitor.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/ConstantVisitor.cs deleted file mode 100644 index 04f31ee48..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/ConstantVisitor.cs +++ /dev/null @@ -1,60 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Microsoft.OData.Edm; -using Microsoft.OData.UriParser; -using NodaTime; -using NodaTime.Text; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors -{ - public sealed class ConstantVisitor : QueryNodeVisitor - { - private static readonly ConstantVisitor Instance = new ConstantVisitor(); - - private ConstantVisitor() - { - } - - public static object Visit(QueryNode node) - { - return node.Accept(Instance); - } - - public override object Visit(ConvertNode nodeIn) - { - var booleanType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Boolean); - - if (nodeIn.TypeReference.Definition == booleanType) - { - return bool.Parse(Visit(nodeIn.Source).ToString()); - } - - var dateTimeType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.DateTimeOffset); - - if (nodeIn.TypeReference.Definition == dateTimeType) - { - var value = Visit(nodeIn.Source); - - if (value is DateTimeOffset dateTimeOffset) - { - return Instant.FromDateTimeOffset(dateTimeOffset); - } - - return InstantPattern.General.Parse(Visit(nodeIn.Source).ToString()).Value; - } - - return base.Visit(nodeIn); - } - - public override object Visit(ConstantNode nodeIn) - { - return nodeIn.Value; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs index 9851560bc..72c9bb682 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs @@ -7,10 +7,13 @@ using System; using System.Collections.Generic; +using System.Linq; using Microsoft.OData.UriParser; using MongoDB.Driver; using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.GenerateEdmSchema; using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure.MongoDb.OData; namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors { @@ -18,51 +21,46 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors { private static readonly FilterDefinitionBuilder Filter = Builders.Filter; - public static IFindFluent Sort(this IFindFluent cursor, ODataUriParser query, Schema schema) + public static PropertyCalculator CreatePropertyCalculator(Schema schema) { - return cursor.Sort(SortBuilder.BuildSort(query, schema)); - } + return propertyNames => + { + if (propertyNames.Length == 3) + { + var edmName = propertyNames[1].UnescapeEdmField(); - public static IFindFluent Take(this IFindFluent cursor, ODataUriParser query) - { - var top = query.ParseTop(); + if (!schema.FieldsByName.TryGetValue(edmName, out var field)) + { + throw new NotSupportedException(); + } - if (top.HasValue) - { - cursor = cursor.Limit(Math.Min((int)top.Value, 200)); - } - else - { - cursor = cursor.Limit(20); - } + propertyNames[1] = field.Id.ToString(); + } - return cursor; + var propertyName = $"do.{string.Join(".", propertyNames.Skip(1))}"; + + return propertyName; + }; } - public static IFindFluent Skip(this IFindFluent cursor, ODataUriParser query) + public static IFindFluent ContentSort(this IFindFluent cursor, ODataUriParser query, PropertyCalculator propertyCalculator) { - var skip = query.ParseSkip(); - - if (skip.HasValue) - { - cursor = cursor.Skip((int)skip.Value); - } - else - { - cursor = cursor.Skip(null); - } + var sort = query.BuildSort(propertyCalculator); - return cursor; + return sort != null ? cursor.Sort(sort) : cursor.SortByDescending(x => x.LastModified); } - public static IFindFluent Find(this IMongoCollection cursor, ODataUriParser query, Guid schemaId, Schema schema, Status[] status) + public static IFindFluent ContentTake(this IFindFluent cursor, ODataUriParser query) { - var filter = BuildQuery(query, schemaId, schema, status); + return cursor.Take(query, 200, 20); + } - return cursor.Find(filter); + public static IFindFluent ContentSkip(this IFindFluent cursor, ODataUriParser query) + { + return cursor.Skip(query); } - public static FilterDefinition BuildQuery(ODataUriParser query, Guid schemaId, Schema schema, Status[] status) + public static FilterDefinition BuildQuery(ODataUriParser query, Guid schemaId, Status[] status, PropertyCalculator propertyCalculator) { var filters = new List> { @@ -71,7 +69,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors Filter.Eq(x => x.IsDeleted, false) }; - var filter = FilterBuilder.Build(query, schema); + var filter = query.BuildFilter(propertyCalculator); if (filter.Filter != null) { diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/PropertyVisitor.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/PropertyVisitor.cs deleted file mode 100644 index e72959bc1..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/PropertyVisitor.cs +++ /dev/null @@ -1,72 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Immutable; -using System.Linq; -using Microsoft.OData.UriParser; -using MongoDB.Driver; -using Squidex.Domain.Apps.Core.GenerateEdmSchema; -using Squidex.Domain.Apps.Core.Schemas; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors -{ - public sealed class PropertyVisitor : QueryNodeVisitor> - { - private static readonly PropertyVisitor Instance = new PropertyVisitor(); - - public static StringFieldDefinition Visit(QueryNode node, Schema schema) - { - var propertyNames = node.Accept(Instance).ToArray(); - - if (propertyNames.Length == 3) - { - var edmName = propertyNames[1].UnescapeEdmField(); - - if (!schema.FieldsByName.TryGetValue(edmName, out var field)) - { - throw new NotSupportedException(); - } - - propertyNames[1] = field.Id.ToString(); - } - - var propertyName = $"do.{string.Join(".", propertyNames.Skip(1))}"; - - return new StringFieldDefinition(propertyName); - } - - public override ImmutableList Visit(ConvertNode nodeIn) - { - return nodeIn.Source.Accept(this); - } - - public override ImmutableList Visit(SingleComplexNode nodeIn) - { - if (nodeIn.Source is SingleComplexNode) - { - return nodeIn.Source.Accept(this).Add(nodeIn.Property.Name); - } - else - { - return ImmutableList.Create(nodeIn.Property.Name); - } - } - - public override ImmutableList Visit(SingleValuePropertyAccessNode nodeIn) - { - if (nodeIn.Source is SingleComplexNode) - { - return nodeIn.Source.Accept(this).Add(nodeIn.Property.Name); - } - else - { - return ImmutableList.Create(nodeIn.Property.Name); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/SortBuilder.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/SortBuilder.cs deleted file mode 100644 index c0d63c8e4..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/SortBuilder.cs +++ /dev/null @@ -1,61 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using Microsoft.OData.UriParser; -using MongoDB.Driver; -using Squidex.Domain.Apps.Core.Schemas; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors -{ - public static class SortBuilder - { - private static readonly SortDefinitionBuilder Sort = Builders.Sort; - - public static SortDefinition BuildSort(ODataUriParser query, Schema schema) - { - var orderBy = query.ParseOrderBy(); - - if (orderBy != null) - { - var sorts = new List>(); - - while (orderBy != null) - { - sorts.Add(OrderBy(orderBy, schema)); - - orderBy = orderBy.ThenBy; - } - - if (sorts.Count > 1) - { - return Sort.Combine(sorts); - } - else - { - return sorts[0]; - } - } - else - { - return Sort.Descending(x => x.LastModified); - } - } - - public static SortDefinition OrderBy(OrderByClause clause, Schema schema) - { - if (clause.Direction == OrderByDirection.Ascending) - { - return Sort.Ascending(PropertyVisitor.Visit(clause.Expression, schema)); - } - else - { - return Sort.Descending(PropertyVisitor.Visit(clause.Expression, schema)); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Edm/EdmAssetModel.cs b/src/Squidex.Domain.Apps.Entities/Assets/Edm/EdmAssetModel.cs new file mode 100644 index 000000000..33f991d94 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Assets/Edm/EdmAssetModel.cs @@ -0,0 +1,47 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.OData.Edm; + +namespace Squidex.Domain.Apps.Entities.Assets.Edm +{ + public static class EdmAssetModel + { + public static readonly IEdmModel Edm; + + static EdmAssetModel() + { + var entityType = new EdmEntityType("Squidex", "Asset"); + + entityType.AddStructuralProperty(nameof(IAssetEntity.Id), EdmPrimitiveTypeKind.Guid); + entityType.AddStructuralProperty(nameof(IAssetEntity.AppId), EdmPrimitiveTypeKind.Guid); + entityType.AddStructuralProperty(nameof(IAssetEntity.Created), EdmPrimitiveTypeKind.DateTimeOffset); + entityType.AddStructuralProperty(nameof(IAssetEntity.CreatedBy), EdmPrimitiveTypeKind.String); + entityType.AddStructuralProperty(nameof(IAssetEntity.LastModified), EdmPrimitiveTypeKind.DateTimeOffset); + entityType.AddStructuralProperty(nameof(IAssetEntity.LastModifiedBy), EdmPrimitiveTypeKind.String); + entityType.AddStructuralProperty(nameof(IAssetEntity.Version), EdmPrimitiveTypeKind.Int64); + entityType.AddStructuralProperty(nameof(IAssetEntity.FileName), EdmPrimitiveTypeKind.String); + entityType.AddStructuralProperty(nameof(IAssetEntity.FileSize), EdmPrimitiveTypeKind.Int64); + entityType.AddStructuralProperty(nameof(IAssetEntity.FileVersion), EdmPrimitiveTypeKind.Int64); + entityType.AddStructuralProperty(nameof(IAssetEntity.IsImage), EdmPrimitiveTypeKind.Boolean); + entityType.AddStructuralProperty(nameof(IAssetEntity.MimeType), EdmPrimitiveTypeKind.String); + entityType.AddStructuralProperty(nameof(IAssetEntity.PixelHeight), EdmPrimitiveTypeKind.Int32); + entityType.AddStructuralProperty(nameof(IAssetEntity.PixelWidth), EdmPrimitiveTypeKind.Int32); + + var container = new EdmEntityContainer("Squidex", "Container"); + + container.AddEntitySet("AssetSet", entityType); + + var model = new EdmModel(); + + model.AddElement(container); + model.AddElement(entityType); + + Edm = model; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Edm/EdmAssetQuery.cs b/src/Squidex.Domain.Apps.Entities/Assets/Edm/EdmAssetQuery.cs new file mode 100644 index 000000000..db991ec92 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Assets/Edm/EdmAssetQuery.cs @@ -0,0 +1,49 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using Microsoft.OData; +using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Assets.Edm +{ + public static class EdmAssetQuery + { + public static ODataUriParser ParseQuery(string query) + { + try + { + var model = EdmAssetModel.Edm; + + if (!model.EntityContainer.EntitySets().Any()) + { + return null; + } + + query = query ?? string.Empty; + + var path = model.EntityContainer.EntitySets().First().Path.Path.Split('.').Last(); + + if (query.StartsWith("?", StringComparison.Ordinal)) + { + query = query.Substring(1); + } + + var parser = new ODataUriParser(model, new Uri($"{path}?{query}", UriKind.Relative)); + + return parser; + } + catch (ODataException ex) + { + throw new ValidationException($"Failed to parse query: {ex.Message}", ex); + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Edm/EdmModelBuilder.cs b/src/Squidex.Domain.Apps.Entities/Assets/Edm/EdmModelBuilder.cs deleted file mode 100644 index 61516c68e..000000000 --- a/src/Squidex.Domain.Apps.Entities/Assets/Edm/EdmModelBuilder.cs +++ /dev/null @@ -1,55 +0,0 @@ -// ========================================================================== -// EdmModelBuilder.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== -using Microsoft.OData.Edm; -using Squidex.Domain.Apps.Entities.Assets.State; - -namespace Squidex.Domain.Apps.Entities.Assets.Edm -{ - public class EdmModelBuilder - { - private readonly IEdmModel edmModel; - public EdmModelBuilder() - { - edmModel = BuildEdmModel(); - } - - public virtual IEdmModel EdmModel - { - get { return edmModel; } - } - - private IEdmModel BuildEdmModel() - { - var model = new EdmModel(); - var container = new EdmEntityContainer("Squidex", "Container"); - var entityType = new EdmEntityType("Squidex", "Asset"); - - entityType.AddStructuralProperty(nameof(AssetState.Id), EdmPrimitiveTypeKind.Guid); - entityType.AddStructuralProperty(nameof(AssetState.AppId), EdmPrimitiveTypeKind.Guid); - entityType.AddStructuralProperty(nameof(AssetState.Created), EdmPrimitiveTypeKind.DateTimeOffset); - entityType.AddStructuralProperty(nameof(AssetState.CreatedBy), EdmPrimitiveTypeKind.String); - entityType.AddStructuralProperty(nameof(AssetState.LastModified), EdmPrimitiveTypeKind.DateTimeOffset); - entityType.AddStructuralProperty(nameof(AssetState.LastModifiedBy), EdmPrimitiveTypeKind.String); - entityType.AddStructuralProperty(nameof(AssetState.Version), EdmPrimitiveTypeKind.Int64); - entityType.AddStructuralProperty(nameof(AssetState.FileName), EdmPrimitiveTypeKind.String); - entityType.AddStructuralProperty(nameof(AssetState.FileSize), EdmPrimitiveTypeKind.Int64); - entityType.AddStructuralProperty(nameof(AssetState.FileVersion), EdmPrimitiveTypeKind.Int64); - entityType.AddStructuralProperty(nameof(AssetState.IsImage), EdmPrimitiveTypeKind.Boolean); - entityType.AddStructuralProperty(nameof(AssetState.MimeType), EdmPrimitiveTypeKind.String); - entityType.AddStructuralProperty(nameof(AssetState.PixelHeight), EdmPrimitiveTypeKind.Int32); - entityType.AddStructuralProperty(nameof(AssetState.PixelWidth), EdmPrimitiveTypeKind.Int32); - - model.AddElement(container); - model.AddElement(entityType); - - container.AddEntitySet("AssetSet", entityType); - - return model; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Edm/EdmModelExtensions.cs b/src/Squidex.Domain.Apps.Entities/Assets/Edm/EdmModelExtensions.cs deleted file mode 100644 index 6bb691335..000000000 --- a/src/Squidex.Domain.Apps.Entities/Assets/Edm/EdmModelExtensions.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ========================================================================== -// EdmModelExtensions.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== -using System; -using System.Linq; -using Microsoft.OData.Edm; -using Microsoft.OData.UriParser; - -namespace Squidex.Domain.Apps.Entities.Assets.Edm -{ - public static class EdmModelExtensions - { - public static ODataUriParser ParseQuery(this IEdmModel model, string query) - { - if (!model.EntityContainer.EntitySets().Any()) - { - return null; - } - - query = query ?? string.Empty; - - var path = model.EntityContainer.EntitySets().First().Path.Path.Split('.').Last(); - - if (query.StartsWith("?", StringComparison.Ordinal)) - { - query = query.Substring(1); - } - - var parser = new ODataUriParser(model, new Uri($"{path}?{query}", UriKind.Relative)); - - return parser; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/ConstantVisitor.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/ConstantVisitor.cs similarity index 71% rename from src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/ConstantVisitor.cs rename to src/Squidex.Infrastructure.MongoDb/MongoDb/OData/ConstantVisitor.cs index 3984c456e..482d6a7c1 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/ConstantVisitor.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/ConstantVisitor.cs @@ -11,10 +11,14 @@ using Microsoft.OData.UriParser; using NodaTime; using NodaTime.Text; -namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors +namespace Squidex.Infrastructure.MongoDb.OData { public sealed class ConstantVisitor : QueryNodeVisitor { + private static readonly IEdmPrimitiveType BooleanType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Boolean); + private static readonly IEdmPrimitiveType DateTimeType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.DateTimeOffset); + private static readonly IEdmPrimitiveType GuidType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Guid); + private static readonly ConstantVisitor Instance = new ConstantVisitor(); private ConstantVisitor() @@ -28,16 +32,17 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors public override object Visit(ConvertNode nodeIn) { - var booleanType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Boolean); - - if (nodeIn.TypeReference.Definition == booleanType) + if (nodeIn.TypeReference.Definition == BooleanType) { return bool.Parse(Visit(nodeIn.Source).ToString()); } - var dateTimeType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.DateTimeOffset); + if (nodeIn.TypeReference.Definition == GuidType) + { + return Guid.Parse(Visit(nodeIn.Source).ToString()); + } - if (nodeIn.TypeReference.Definition == dateTimeType) + if (nodeIn.TypeReference.Definition == DateTimeType) { var value = Visit(nodeIn.Source); @@ -49,13 +54,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors return InstantPattern.General.Parse(Visit(nodeIn.Source).ToString()).Value; } - var guidType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Guid); - - if (nodeIn.TypeReference.Definition == guidType) - { - return Guid.Parse(Visit(nodeIn.Source).ToString()); - } - return base.Visit(nodeIn); } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterBuilder.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/FilterBuilder.cs similarity index 68% rename from src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterBuilder.cs rename to src/Squidex.Infrastructure.MongoDb/MongoDb/OData/FilterBuilder.cs index b39155a00..ffc82c1dc 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterBuilder.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/FilterBuilder.cs @@ -8,16 +8,12 @@ using Microsoft.OData; using Microsoft.OData.UriParser; using MongoDB.Driver; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors +namespace Squidex.Infrastructure.MongoDb.OData { public static class FilterBuilder { - private static readonly FilterDefinitionBuilder Filter = Builders.Filter; - - public static (FilterDefinition Filter, bool Last) Build(ODataUriParser query, Schema schema) + public static (FilterDefinition Filter, bool Last) BuildFilter(this ODataUriParser query, PropertyCalculator propertyCalculator = null, bool supportsSearch = true) { SearchClause search; try @@ -31,7 +27,12 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors if (search != null) { - return (Filter.Text(SearchTermVisitor.Visit(search.Expression).ToString()), false); + if (!supportsSearch) + { + throw new ValidationException("Query $search clause not supported."); + } + + return (Builders.Filter.Text(SearchTermVisitor.Visit(search.Expression).ToString()), false); } FilterClause filter; @@ -46,7 +47,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors if (filter != null) { - return (FilterVisitor.Visit(filter.Expression, schema), true); + return (FilterVisitor.Visit(filter.Expression, propertyCalculator), true); } return (null, false); diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterVisitor.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/FilterVisitor.cs similarity index 80% rename from src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterVisitor.cs rename to src/Squidex.Infrastructure.MongoDb/MongoDb/OData/FilterVisitor.cs index 1463f0343..cfe498c4e 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterVisitor.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/FilterVisitor.cs @@ -10,33 +10,32 @@ using System.Linq; using Microsoft.OData.UriParser; using MongoDB.Bson; using MongoDB.Driver; -using Squidex.Domain.Apps.Core.Schemas; -namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors +namespace Squidex.Infrastructure.MongoDb.OData { - public class FilterVisitor : QueryNodeVisitor> + public sealed class FilterVisitor : QueryNodeVisitor> { - private static readonly FilterDefinitionBuilder Filter = Builders.Filter; - private readonly Schema schema; + private static readonly FilterDefinitionBuilder Filter = Builders.Filter; + private readonly PropertyCalculator propertyCalculator; - private FilterVisitor(Schema schema) + private FilterVisitor(PropertyCalculator propertyCalculator) { - this.schema = schema; + this.propertyCalculator = propertyCalculator; } - public static FilterDefinition Visit(QueryNode node, Schema schema) + public static FilterDefinition Visit(QueryNode node, PropertyCalculator propertyCalculator) { - var visitor = new FilterVisitor(schema); + var visitor = new FilterVisitor(propertyCalculator); return node.Accept(visitor); } - public override FilterDefinition Visit(ConvertNode nodeIn) + public override FilterDefinition Visit(ConvertNode nodeIn) { return nodeIn.Source.Accept(this); } - public override FilterDefinition Visit(UnaryOperatorNode nodeIn) + public override FilterDefinition Visit(UnaryOperatorNode nodeIn) { if (nodeIn.OperatorKind == UnaryOperatorKind.Not) { @@ -46,7 +45,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors throw new NotSupportedException(); } - public override FilterDefinition Visit(SingleValueFunctionCallNode nodeIn) + public override FilterDefinition Visit(SingleValueFunctionCallNode nodeIn) { var fieldNode = nodeIn.Parameters.ElementAt(0); var valueNode = nodeIn.Parameters.ElementAt(1); @@ -75,7 +74,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors throw new NotSupportedException(); } - public override FilterDefinition Visit(BinaryOperatorNode nodeIn) + public override FilterDefinition Visit(BinaryOperatorNode nodeIn) { if (nodeIn.OperatorKind == BinaryOperatorKind.And) { @@ -149,9 +148,9 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors return new BsonRegularExpression(formatter(BuildValue(node).ToString()), "i"); } - private FieldDefinition BuildFieldDefinition(QueryNode nodeIn) + private FieldDefinition BuildFieldDefinition(QueryNode nodeIn) { - return PropertyVisitor.Visit(nodeIn, schema); + return nodeIn.BuildFieldDefinition(propertyCalculator); } private static object BuildValue(QueryNode nodeIn) diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/LimitExtensions.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/LimitExtensions.cs new file mode 100644 index 000000000..8296a4476 --- /dev/null +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/LimitExtensions.cs @@ -0,0 +1,48 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Microsoft.OData.UriParser; +using MongoDB.Driver; + +namespace Squidex.Infrastructure.MongoDb.OData +{ + public static class LimitExtensions + { + public static IFindFluent Take(this IFindFluent cursor, ODataUriParser query, int maxValue = 200, int defaultValue = 20) + { + var top = query.ParseTop(); + + if (top.HasValue) + { + cursor = cursor.Limit(Math.Min((int)top.Value, maxValue)); + } + else + { + cursor = cursor.Limit(defaultValue); + } + + return cursor; + } + + public static IFindFluent Skip(this IFindFluent cursor, ODataUriParser query) + { + var skip = query.ParseSkip(); + + if (skip.HasValue) + { + cursor = cursor.Skip((int)skip.Value); + } + else + { + cursor = cursor.Skip(null); + } + + return cursor; + } + } +} diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/PropertyBuilder.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/PropertyBuilder.cs new file mode 100644 index 000000000..a9bfa8d54 --- /dev/null +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/PropertyBuilder.cs @@ -0,0 +1,33 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Linq; +using Microsoft.OData.UriParser; +using MongoDB.Driver; + +namespace Squidex.Infrastructure.MongoDb.OData +{ + public delegate string PropertyCalculator(string[] parts); + + public static class PropertyBuilder + { + private static readonly PropertyCalculator DefaultCalculator = parts => + { + return string.Join(".", parts); + }; + + public static StringFieldDefinition BuildFieldDefinition(this QueryNode node, PropertyCalculator propertyCalculator) + { + propertyCalculator = propertyCalculator ?? DefaultCalculator; + + var propertyParts = node.Accept(PropertyNameVisitor.Instance).ToArray(); + var propertyName = propertyCalculator(propertyParts); + + return new StringFieldDefinition(propertyName); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/PropertyVisitor.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/PropertyNameVisitor.cs similarity index 67% rename from src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/PropertyVisitor.cs rename to src/Squidex.Infrastructure.MongoDb/MongoDb/OData/PropertyNameVisitor.cs index 723d03c38..d6fe423db 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/PropertyVisitor.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/PropertyNameVisitor.cs @@ -1,26 +1,23 @@ // ========================================================================== -// PropertyVisitor.cs // Squidex Headless CMS // ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. // ========================================================================== + using System.Collections.Immutable; -using System.Linq; using Microsoft.OData.UriParser; -using MongoDB.Driver; -namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors +#pragma warning disable RECS0108 // Warns about static fields in generic types + +namespace Squidex.Infrastructure.MongoDb.OData { - public sealed class PropertyVisitor : QueryNodeVisitor> + public sealed class PropertyNameVisitor : QueryNodeVisitor> { - private static readonly PropertyVisitor Instance = new PropertyVisitor(); + public static readonly PropertyNameVisitor Instance = new PropertyNameVisitor(); - public static StringFieldDefinition Visit(QueryNode node) + private PropertyNameVisitor() { - var propertyNames = node.Accept(Instance).ToArray(); - - return new StringFieldDefinition(propertyNames.First()); } public override ImmutableList Visit(ConvertNode nodeIn) diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/SearchTermVisitor.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/SearchTermVisitor.cs similarity index 94% rename from src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/SearchTermVisitor.cs rename to src/Squidex.Infrastructure.MongoDb/MongoDb/OData/SearchTermVisitor.cs index 823d77768..85f897c80 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/SearchTermVisitor.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/SearchTermVisitor.cs @@ -8,7 +8,7 @@ using System; using Microsoft.OData.UriParser; -namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors +namespace Squidex.Infrastructure.MongoDb.OData { public class SearchTermVisitor : QueryNodeVisitor { diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/SortBuilder.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/SortBuilder.cs similarity index 53% rename from src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/SortBuilder.cs rename to src/Squidex.Infrastructure.MongoDb/MongoDb/OData/SortBuilder.cs index cf1cef451..c19ca4305 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/SortBuilder.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/OData/SortBuilder.cs @@ -1,59 +1,57 @@ // ========================================================================== -// SortBuilder.cs // Squidex Headless CMS // ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. // ========================================================================== + using System.Collections.Generic; using Microsoft.OData.UriParser; using MongoDB.Driver; -namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors +namespace Squidex.Infrastructure.MongoDb.OData { public static class SortBuilder { - private static readonly SortDefinitionBuilder Sort = Builders.Sort; - - public static SortDefinition BuildSort(ODataUriParser query) + public static SortDefinition BuildSort(this ODataUriParser query, PropertyCalculator propertyCalculator = null) { var orderBy = query.ParseOrderBy(); if (orderBy != null) { - var sorts = new List>(); + var sorts = new List>(); while (orderBy != null) { - sorts.Add(OrderBy(orderBy)); + sorts.Add(OrderBy(orderBy, propertyCalculator)); orderBy = orderBy.ThenBy; } if (sorts.Count > 1) { - return Sort.Combine(sorts); + return Builders.Sort.Combine(sorts); } else { return sorts[0]; } } - else - { - return Sort.Descending(x => x.LastModified); - } + + return null; } - public static SortDefinition OrderBy(OrderByClause clause) + public static SortDefinition OrderBy(OrderByClause clause, PropertyCalculator propertyCalculator = null) { + var propertyName = clause.Expression.BuildFieldDefinition(propertyCalculator); + if (clause.Direction == OrderByDirection.Ascending) { - return Sort.Ascending(PropertyVisitor.Visit(clause.Expression)); + return Builders.Sort.Ascending(propertyName); } else { - return Sort.Descending(PropertyVisitor.Visit(clause.Expression)); + return Builders.Sort.Descending(propertyName); } } } diff --git a/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj b/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj index 5eb282692..43fd73478 100644 --- a/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj +++ b/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Squidex/Docs/AddODataQueryParams.cs b/src/Squidex/Areas/Api/Config/Swagger/ODataQueryParamsProcessor.cs similarity index 55% rename from src/Squidex/Docs/AddODataQueryParams.cs rename to src/Squidex/Areas/Api/Config/Swagger/ODataQueryParamsProcessor.cs index f79e4b210..2c82b910f 100644 --- a/src/Squidex/Docs/AddODataQueryParams.cs +++ b/src/Squidex/Areas/Api/Config/Swagger/ODataQueryParamsProcessor.cs @@ -13,17 +13,32 @@ using NSwag.SwaggerGeneration.Processors.Contexts; using Squidex.Infrastructure.Tasks; using Squidex.Pipeline.Swagger; -namespace Squidex.Docs +namespace Squidex.Areas.Api.Config.Swagger { - public class AddODataQueryParams : IOperationProcessor + public sealed class ODataQueryParamsProcessor : IOperationProcessor { + private readonly string path; + private readonly string entity; + private readonly bool supportSearch; + + public ODataQueryParamsProcessor(string path, string entity, bool supportSearch) + { + this.path = path; + this.entity = entity; + this.supportSearch = supportSearch; + } + public Task ProcessAsync(OperationProcessorContext context) { - if (context.OperationDescription.Path == "/apps/{app}/assets") + if (context.OperationDescription.Path == path) { - context.OperationDescription.Operation.AddQueryParameter("$top", JsonObjectType.Number, "Optional number of contents to take."); - context.OperationDescription.Operation.AddQueryParameter("$skip", JsonObjectType.Number, "Optional number of contents to skip."); - context.OperationDescription.Operation.AddQueryParameter("$search", JsonObjectType.String, "Optional OData full text search."); + if (supportSearch) + { + context.OperationDescription.Operation.AddQueryParameter("$search", JsonObjectType.String, "Optional OData full text search."); + } + + context.OperationDescription.Operation.AddQueryParameter("$top", JsonObjectType.Number, $"Optional number of {entity} to take."); + context.OperationDescription.Operation.AddQueryParameter("$skip", JsonObjectType.Number, $"Optional number of {entity} to skip."); context.OperationDescription.Operation.AddQueryParameter("$orderby", JsonObjectType.String, "Optional OData order definition."); context.OperationDescription.Operation.AddQueryParameter("$filter", JsonObjectType.String, "Optional OData filter definition."); } diff --git a/src/Squidex/Areas/Api/Config/Swagger/SwaggerServices.cs b/src/Squidex/Areas/Api/Config/Swagger/SwaggerServices.cs index cc8bd4334..f04c2e960 100644 --- a/src/Squidex/Areas/Api/Config/Swagger/SwaggerServices.cs +++ b/src/Squidex/Areas/Api/Config/Swagger/SwaggerServices.cs @@ -29,16 +29,8 @@ namespace Squidex.Areas.Api.Config.Swagger var urlOptions = s.GetService>().Value; var settings = - new SwaggerSettings - { - Title = "Squidex API", - Version = "1.0", - IsAspNetCore = false, - OperationProcessors = - { - new Docs.AddODataQueryParams() - } - } + new SwaggerSettings { Title = "Squidex API", Version = "1.0", IsAspNetCore = false } + .AddAssetODataParams() .ConfigurePaths(urlOptions) .ConfigureSchemaSettings() .ConfigureIdentity(urlOptions); @@ -49,6 +41,13 @@ namespace Squidex.Areas.Api.Config.Swagger services.AddTransient(); } + private static SwaggerSettings AddAssetODataParams(this SwaggerSettings settings) + { + settings.OperationProcessors.Add(new ODataQueryParamsProcessor("/apps/{app}/assets", "assets", false)); + + return settings; + } + private static SwaggerSettings ConfigureIdentity(this SwaggerSettings settings, MyUrlsOptions urlOptions) { settings.DocumentProcessors.Add( diff --git a/src/Squidex/Config/Domain/ReadServices.cs b/src/Squidex/Config/Domain/ReadServices.cs index abdd6b4bc..1a24fedd5 100644 --- a/src/Squidex/Config/Domain/ReadServices.cs +++ b/src/Squidex/Config/Domain/ReadServices.cs @@ -120,9 +120,6 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .AsSelf(); - services.AddSingletonAs() - .AsSelf(); - services.AddSingletonAs() .AsSelf(); } diff --git a/src/Squidex/Config/Domain/StoreServices.cs b/src/Squidex/Config/Domain/StoreServices.cs index eb84e6d97..44ac3fe22 100644 --- a/src/Squidex/Config/Domain/StoreServices.cs +++ b/src/Squidex/Config/Domain/StoreServices.cs @@ -102,7 +102,7 @@ namespace Squidex.Config.Domain .As>() .As(); - services.AddSingletonAs(c => new MongoAssetRepository(mongoDatabase, c.GetRequiredService())) + services.AddSingletonAs(c => new MongoAssetRepository(mongoDatabase)) .As() .As>() .As();