Browse Source

Merge pull request #315 from Squidex/feature_odata2

Feature odata2
pull/316/head
Sebastian Stehle 8 years ago
committed by GitHub
parent
commit
a1346ce815
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      README.md
  2. 2
      src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs
  3. 2
      src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs
  4. 2
      src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj
  5. 4
      src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs
  6. 6
      src/Squidex.Domain.Apps.Core.Operations/Tags/ITagService.cs
  7. 2
      src/Squidex.Domain.Apps.Core.Operations/Tags/Tag.cs
  8. 19
      src/Squidex.Domain.Apps.Core.Operations/Tags/TagGroups.cs
  9. 130
      src/Squidex.Domain.Apps.Core.Operations/Tags/TagNormalizer.cs
  10. 2
      src/Squidex.Domain.Apps.Core.Operations/Tags/TagSet.cs
  11. 30
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
  12. 55
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs
  13. 22
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs
  14. 8
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs
  15. 79
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs
  16. 14
      src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs
  17. 49
      src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs
  18. 2
      src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs
  19. 2
      src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs
  20. 50
      src/Squidex.Domain.Apps.Entities/Assets/Queries/QueryTagVisitor.cs
  21. 3
      src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs
  22. 2
      src/Squidex.Domain.Apps.Entities/Backup/Helpers/ArchiveHelper.cs
  23. 28
      src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs
  24. 2
      src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs
  25. 8
      src/Squidex.Domain.Apps.Entities/Contents/QueryExecutionContext.cs
  26. 4
      src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs
  27. 18
      src/Squidex.Domain.Apps.Entities/Q.cs
  28. 5
      src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs
  29. 5
      src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs
  30. 15
      src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs
  31. 83
      src/Squidex.Infrastructure.MongoDb/MongoDb/OData/ConstantVisitor.cs
  32. 33
      src/Squidex.Infrastructure.MongoDb/MongoDb/OData/FilterBuilder.cs
  33. 174
      src/Squidex.Infrastructure.MongoDb/MongoDb/OData/FilterVisitor.cs
  34. 27
      src/Squidex.Infrastructure.MongoDb/MongoDb/OData/LimitExtensions.cs
  35. 32
      src/Squidex.Infrastructure.MongoDb/MongoDb/OData/PropertyBuilder.cs
  36. 20
      src/Squidex.Infrastructure.MongoDb/MongoDb/OData/SortBuilder.cs
  37. 1
      src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj
  38. 2
      src/Squidex.Infrastructure/Guard.cs
  39. 32
      src/Squidex.Infrastructure/Queries/FilterBuilder.cs
  40. 71
      src/Squidex.Infrastructure/Queries/FilterComparison.cs
  41. 45
      src/Squidex.Infrastructure/Queries/FilterJunction.cs
  42. 7
      src/Squidex.Infrastructure/Queries/FilterJunctionType.cs
  43. 31
      src/Squidex.Infrastructure/Queries/FilterNegate.cs
  44. 14
      src/Squidex.Infrastructure/Queries/FilterNode.cs
  45. 31
      src/Squidex.Infrastructure/Queries/FilterNodeVisitor.cs
  46. 22
      src/Squidex.Infrastructure/Queries/FilterOperator.cs
  47. 21
      src/Squidex.Infrastructure/Queries/FilterValueType.cs
  48. 30
      src/Squidex.Infrastructure/Queries/OData/ConstantVisitor.cs
  49. 113
      src/Squidex.Infrastructure/Queries/OData/ConstantWithTypeVisitor.cs
  50. 17
      src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs
  51. 48
      src/Squidex.Infrastructure/Queries/OData/FilterBuilder.cs
  52. 148
      src/Squidex.Infrastructure/Queries/OData/FilterVisitor.cs
  53. 34
      src/Squidex.Infrastructure/Queries/OData/LimitExtensions.cs
  54. 27
      src/Squidex.Infrastructure/Queries/OData/PropertyPathVisitor.cs
  55. 2
      src/Squidex.Infrastructure/Queries/OData/SearchTermVisitor.cs
  56. 43
      src/Squidex.Infrastructure/Queries/OData/SortBuilder.cs
  57. 30
      src/Squidex.Infrastructure/Queries/PascalCasePathConverter.cs
  58. 56
      src/Squidex.Infrastructure/Queries/Query.cs
  59. 24
      src/Squidex.Infrastructure/Queries/SortBuilder.cs
  60. 36
      src/Squidex.Infrastructure/Queries/SortNode.cs
  61. 15
      src/Squidex.Infrastructure/Queries/SortOrder.cs
  62. 29
      src/Squidex.Infrastructure/Queries/TransformVisitor.cs
  63. 1
      src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  64. 4
      src/Squidex.Infrastructure/Tasks/AsyncLock.cs
  65. 4
      src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  66. 2
      src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  67. 1
      src/Squidex/Config/Domain/EntitiesServices.cs
  68. 2
      src/Squidex/Squidex.csproj
  69. 2
      src/Squidex/app/features/content/pages/contents/contents-page.component.html
  70. 2
      src/Squidex/app/features/content/shared/content-item.component.html
  71. 2
      src/Squidex/app/features/content/shared/contents-selector.component.html
  72. 2
      src/Squidex/app/features/content/shared/field-editor.component.html
  73. 1
      src/Squidex/app/framework/angular/forms/tag-editor.component.scss
  74. 89
      src/Squidex/package.json
  75. 3
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs
  76. 4
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs
  77. 36
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetQueryServiceTests.cs
  78. 162
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/MongoDbQueryTests.cs
  79. 58
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/QueryTagVisitorTests.cs
  80. 10
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs
  81. 12
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs
  82. 267
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs
  83. 448
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/OData/ODataQueryTests.cs
  84. 1
      tests/Squidex.Domain.Apps.Entities.Tests/Tags/GrainTagServiceTests.cs
  85. 5
      tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagGrainTests.cs
  86. 6
      tests/Squidex.Infrastructure.Tests/Caching/LRUCacheTests.cs
  87. 1
      tests/Squidex.Infrastructure.Tests/Orleans/BootstrapTests.cs
  88. 316
      tests/Squidex.Infrastructure.Tests/Queries/ODataConversionTests.cs
  89. 32
      tests/Squidex.Infrastructure.Tests/Queries/PascalCasePathConverterTests.cs

2
README.md

@ -14,7 +14,7 @@ Please join our community forum: https://support.squidex.io
## Status ## Status
Current Version 1.8.0. Roadmap: https://trello.com/b/KakM4F3S/squidex-roadmap Current Version 1.10.0. Roadmap: https://trello.com/b/KakM4F3S/squidex-roadmap
## Prerequisites ## Prerequisites

2
src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs

@ -74,7 +74,7 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json
public Schema ToSchema(FieldRegistry registry) public Schema ToSchema(FieldRegistry registry)
{ {
RootField[] fields = Empty; var fields = Empty;
if (Fields != null) if (Fields != null)
{ {

2
src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs

@ -16,6 +16,8 @@ namespace Squidex.Domain.Apps.Core.Schemas
public int? MaxItems { get; set; } public int? MaxItems { get; set; }
public bool Normalize { get; set; }
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor) public override T Accept<T>(IFieldPropertiesVisitor<T> visitor)
{ {
return visitor.Visit(this); return visitor.Visit(this);

2
src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj

@ -8,7 +8,7 @@
<DebugSymbols>True</DebugSymbols> <DebugSymbols>True</DebugSymbols>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Fody" Version="3.2.0"> <PackageReference Include="Fody" Version="3.2.1">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference> </PackageReference>

4
src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs

@ -26,8 +26,10 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
{ {
private static readonly Func<IField, string> KeyNameResolver = f => f.Name; private static readonly Func<IField, string> KeyNameResolver = f => f.Name;
private static readonly Func<IField, string> KeyIdResolver = f => f.Id.ToString(); private static readonly Func<IField, string> KeyIdResolver = f => f.Id.ToString();
private static readonly Func<IArrayField, string, IField> FieldByIdResolver = private static readonly Func<IArrayField, string, IField> FieldByIdResolver =
(f, k) => long.TryParse(k, out var id) ? f.FieldsById.GetOrDefault(id) : null; (f, k) => long.TryParse(k, out var id) ? f.FieldsById.GetOrDefault(id) : null;
private static readonly Func<IArrayField, string, IField> FieldByNameResolver = private static readonly Func<IArrayField, string, IField> FieldByNameResolver =
(f, k) => f.FieldsByName.GetOrDefault(k); (f, k) => f.FieldsByName.GetOrDefault(k);
@ -251,7 +253,7 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
var newArray = new JArray(); var newArray = new JArray();
foreach (JObject item in jArray.OfType<JObject>()) foreach (var item in jArray.OfType<JObject>())
{ {
var newItem = new JObject(); var newItem = new JObject();

6
src/Squidex.Domain.Apps.Entities/Tags/ITagService.cs → src/Squidex.Domain.Apps.Core.Operations/Tags/ITagService.cs

@ -9,13 +9,13 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Squidex.Domain.Apps.Entities.Tags namespace Squidex.Domain.Apps.Core.Tags
{ {
public interface ITagService public interface ITagService
{ {
Task<HashSet<string>> NormalizeTagsAsync(Guid appId, string group, HashSet<string> names, HashSet<string> ids); Task<Dictionary<string, string>> GetTagIdsAsync(Guid appId, string group, HashSet<string> names);
Task<HashSet<string>> GetTagIdsAsync(Guid appId, string group, HashSet<string> names); Task<Dictionary<string, string>> NormalizeTagsAsync(Guid appId, string group, HashSet<string> names, HashSet<string> ids);
Task<Dictionary<string, string>> DenormalizeTagsAsync(Guid appId, string group, HashSet<string> ids); Task<Dictionary<string, string>> DenormalizeTagsAsync(Guid appId, string group, HashSet<string> ids);

2
src/Squidex.Domain.Apps.Entities/Tags/Tag.cs → src/Squidex.Domain.Apps.Core.Operations/Tags/Tag.cs

@ -5,7 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
namespace Squidex.Domain.Apps.Entities.Tags namespace Squidex.Domain.Apps.Core.Tags
{ {
public sealed class Tag public sealed class Tag
{ {

19
src/Squidex.Infrastructure.MongoDb/MongoDb/OData/ValueConversion.cs → src/Squidex.Domain.Apps.Core.Operations/Tags/TagGroups.cs

@ -5,20 +5,17 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
namespace Squidex.Infrastructure.MongoDb.OData using System;
{
public delegate object ConvertValue(string field, object value);
public static class ValueConversion namespace Squidex.Domain.Apps.Core.Tags
{
public static class TagGroups
{ {
public static object Convert(string field, object value, ConvertValue converter = null) public const string Assets = "Assets";
{
if (converter == null)
{
return value;
}
return converter(field, value); public static string Schemas(Guid schemaId)
{
return $"Schemas_{schemaId}";
} }
} }
} }

130
src/Squidex.Domain.Apps.Core.Operations/Tags/TagNormalizer.cs

@ -0,0 +1,130 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
namespace Squidex.Domain.Apps.Core.Tags
{
public static class TagNormalizer
{
public static async Task NormalizeAsync(ITagService service, Guid appId, Guid schemaId, Schema schema, params NamedContentData[] datas)
{
var tagsValues = new HashSet<string>();
var tagsArrays = new List<JArray>();
GetValues(schema, tagsValues, tagsArrays, datas);
if (tagsValues.Count > 0)
{
var normalized = await service.NormalizeTagsAsync(appId, $"Schemas_{schemaId}", tagsValues, null);
foreach (var array in tagsArrays)
{
for (var i = 0; i < array.Count; i++)
{
array[i] = normalized[array[i].ToString()];
}
}
}
}
public static async Task DeNormalizeAsync(ITagService service, Guid appId, Guid schemaId, Schema schema, params NamedContentData[] datas)
{
var tagsValues = new HashSet<string>();
var tagsArrays = new List<JArray>();
GetValues(schema, tagsValues, tagsArrays, datas);
if (tagsValues.Count > 0)
{
var denormalized = await service.DenormalizeTagsAsync(appId, $"Schemas_{schemaId}", tagsValues);
foreach (var array in tagsArrays)
{
for (var i = 0; i < array.Count; i++)
{
array[i] = denormalized[array[i].ToString()];
}
}
}
}
private static void GetValues(Schema schema, HashSet<string> values, List<JArray> arrays, params NamedContentData[] datas)
{
foreach (var field in schema.Fields)
{
if (field.RawProperties is TagsFieldProperties tags && tags.Normalize)
{
foreach (var data in datas)
{
if (data.TryGetValue(field.Name, out var fieldData))
{
foreach (var partition in fieldData)
{
ExtractTags(partition.Value, values, arrays);
}
}
}
}
else if (field is IArrayField arrayField)
{
foreach (var nestedField in arrayField.Fields)
{
if (field.RawProperties is TagsFieldProperties nestedTags && nestedTags.Normalize)
{
foreach (var data in datas)
{
if (data.TryGetValue(field.Name, out var fieldData))
{
foreach (var partition in fieldData)
{
if (partition.Value is JArray jArray)
{
foreach (var value in jArray)
{
if (value.Type == JTokenType.Object)
{
var nestedObject = (JObject)value;
if (nestedObject.TryGetValue(nestedField.Name, out var nestedArray))
{
ExtractTags(partition.Value, values, arrays);
}
}
}
}
}
}
}
}
}
}
}
}
private static void ExtractTags(JToken token, HashSet<string> values, List<JArray> arrays)
{
if (token is JArray jArray)
{
foreach (var value in jArray)
{
if (value.Type == JTokenType.String)
{
values.Add(value.ToString());
}
}
arrays.Add(jArray);
}
}
}
}

2
src/Squidex.Domain.Apps.Entities/Tags/TagSet.cs → src/Squidex.Domain.Apps.Core.Operations/Tags/TagSet.cs

@ -7,7 +7,7 @@
using System.Collections.Generic; using System.Collections.Generic;
namespace Squidex.Domain.Apps.Entities.Tags namespace Squidex.Domain.Apps.Core.Tags
{ {
public sealed class TagSet : Dictionary<string, Tag> public sealed class TagSet : Dictionary<string, Tag>
{ {

30
src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs

@ -11,26 +11,20 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Assets.Edm;
using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors; using Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors;
using Squidex.Domain.Apps.Entities.Tags;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
{ {
public sealed partial class MongoAssetRepository : MongoRepositoryBase<MongoAssetEntity>, IAssetRepository public sealed partial class MongoAssetRepository : MongoRepositoryBase<MongoAssetEntity>, IAssetRepository
{ {
private readonly ITagService tagService; public MongoAssetRepository(IMongoDatabase database)
public MongoAssetRepository(IMongoDatabase database, ITagService tagService)
: base(database) : base(database)
{ {
Guard.NotNull(tagService, nameof(tagService));
this.tagService = tagService;
} }
protected override string CollectionName() protected override string CollectionName()
@ -50,36 +44,28 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
.Descending(x => x.LastModified))); .Descending(x => x.LastModified)));
} }
public async Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, string query = null) public async Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, Query query)
{ {
using (Profiler.TraceMethod<MongoAssetRepository>("QueryAsyncByQuery")) using (Profiler.TraceMethod<MongoAssetRepository>("QueryAsyncByQuery"))
{ {
try try
{ {
var odataQuery = EdmAssetModel.Edm.ParseQuery(query); query = query.AdjustToModel();
var filter = FindExtensions.BuildQuery(odataQuery, appId, tagService); var filter = FindExtensions.BuildFilter(query, appId);
var contentCount = Collection.Find(filter).CountDocumentsAsync(); var contentCount = Collection.Find(filter).CountDocumentsAsync();
var contentItems = var contentItems =
Collection.Find(filter) Collection.Find(filter)
.AssetTake(odataQuery) .AssetTake(query)
.AssetSkip(odataQuery) .AssetSkip(query)
.AssetSort(odataQuery) .AssetSort(query)
.ToListAsync(); .ToListAsync();
await Task.WhenAll(contentItems, contentCount); await Task.WhenAll(contentItems, contentCount);
return ResultList.Create<IAssetEntity>(contentCount.Result, contentItems.Result); return ResultList.Create<IAssetEntity>(contentCount.Result, contentItems.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) catch (MongoQueryException ex)
{ {
if (ex.Message.Contains("17406")) if (ex.Message.Contains("17406"))

55
src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs

@ -8,59 +8,59 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Microsoft.OData.UriParser;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Domain.Apps.Entities.Tags;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb.OData; using Squidex.Infrastructure.MongoDb.OData;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors
{ {
public static class FindExtensions public static class FindExtensions
{ {
private static readonly FilterDefinitionBuilder<MongoAssetEntity> Filter = Builders<MongoAssetEntity>.Filter; private static readonly FilterDefinitionBuilder<MongoAssetEntity> Filter = Builders<MongoAssetEntity>.Filter;
private static readonly ConvertProperty PropertyCalculator = propertyNames =>
public static Query AdjustToModel(this Query query)
{ {
if (propertyNames.Length > 0) if (query.Filter != null)
{ {
propertyNames[0] = propertyNames[0].ToPascalCase(); query.Filter = PascalCasePathConverter.Transform(query.Filter);
} }
var propertyName = string.Join(".", propertyNames); query.Sort = query.Sort
.Select(x =>
new SortNode(
x.Path.Select(p => p.ToPascalCase()).ToList(),
x.SortOrder))
.ToList();
return propertyName; return query;
}; }
public static IFindFluent<MongoAssetEntity, MongoAssetEntity> AssetSort(this IFindFluent<MongoAssetEntity, MongoAssetEntity> cursor, ODataUriParser query) public static IFindFluent<MongoAssetEntity, MongoAssetEntity> AssetSort(this IFindFluent<MongoAssetEntity, MongoAssetEntity> cursor, Query query)
{ {
var sort = query.BuildSort<MongoAssetEntity>(PropertyCalculator); return cursor.Sort(query.BuildSort<MongoAssetEntity>());
return sort != null ? cursor.Sort(sort) : cursor.SortByDescending(x => x.LastModified);
} }
public static IFindFluent<MongoAssetEntity, MongoAssetEntity> AssetTake(this IFindFluent<MongoAssetEntity, MongoAssetEntity> cursor, ODataUriParser query) public static IFindFluent<MongoAssetEntity, MongoAssetEntity> AssetTake(this IFindFluent<MongoAssetEntity, MongoAssetEntity> cursor, Query query)
{ {
return cursor.Take(query, 200, 20); return cursor.Take(query);
} }
public static IFindFluent<MongoAssetEntity, MongoAssetEntity> AssetSkip(this IFindFluent<MongoAssetEntity, MongoAssetEntity> cursor, ODataUriParser query) public static IFindFluent<MongoAssetEntity, MongoAssetEntity> AssetSkip(this IFindFluent<MongoAssetEntity, MongoAssetEntity> cursor, Query query)
{ {
return cursor.Skip(query); return cursor.Skip(query);
} }
public static FilterDefinition<MongoAssetEntity> BuildQuery(ODataUriParser query, Guid appId, ITagService tagService) public static FilterDefinition<MongoAssetEntity> BuildFilter(this Query query, Guid appId)
{ {
var convertValue = CreateValueConverter(appId, tagService);
var filters = new List<FilterDefinition<MongoAssetEntity>> var filters = new List<FilterDefinition<MongoAssetEntity>>
{ {
Filter.Eq(x => x.IndexedAppId, appId), Filter.Eq(x => x.IndexedAppId, appId),
Filter.Eq(x => x.IsDeleted, false) Filter.Eq(x => x.IsDeleted, false)
}; };
var filter = query.BuildFilter<MongoAssetEntity>(PropertyCalculator, convertValue, false); var filter = query.BuildFilter<MongoAssetEntity>(false);
if (filter.Filter != null) if (filter.Filter != null)
{ {
@ -87,20 +87,5 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors
return new BsonDocument(); return new BsonDocument();
} }
} }
public static ConvertValue CreateValueConverter(Guid appId, ITagService tagService)
{
return new ConvertValue((field, value) =>
{
if (string.Equals(field, nameof(MongoAssetEntity.Tags), StringComparison.OrdinalIgnoreCase))
{
var tagNames = Task.Run(() => tagService.GetTagIdsAsync(appId, TagGroups.Assets, HashSet.Of(value.ToString()))).Result;
return tagNames?.FirstOrDefault() ?? value;
}
return value;
});
}
} }
} }

22
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs

@ -9,7 +9,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.OData.UriParser;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
@ -18,6 +17,7 @@ using Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{ {
@ -42,20 +42,20 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
return collectionName; return collectionName;
} }
public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, ODataUriParser odataQuery, Status[] status = null, bool useDraft = false) public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Query query, Status[] status = null, bool useDraft = false)
{ {
try try
{ {
var propertyCalculator = FindExtensions.CreatePropertyCalculator(schema.SchemaDef, useDraft); query = query.AdjustToModel(schema.SchemaDef, useDraft);
var filter = FindExtensions.BuildQuery(odataQuery, schema.Id, status, propertyCalculator); var filter = FindExtensions.BuildQuery(query, schema.Id, status);
var contentCount = Collection.Find(filter).CountDocumentsAsync(); var contentCount = Collection.Find(filter).CountDocumentsAsync();
var contentItems = var contentItems =
Collection.Find(filter) Collection.Find(filter)
.ContentTake(odataQuery) .ContentTake(query)
.ContentSkip(odataQuery) .ContentSkip(query)
.ContentSort(odataQuery, propertyCalculator) .ContentSort(query)
.Not(x => x.DataText) .Not(x => x.DataText)
.ToListAsync(); .ToListAsync();
@ -68,14 +68,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
return ResultList.Create<IContentEntity>(contentCount.Result, contentItems.Result); return ResultList.Create<IContentEntity>(contentCount.Result, contentItems.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) catch (MongoQueryException ex)
{ {
if (ex.Message.Contains("17406")) if (ex.Message.Contains("17406"))

8
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs

@ -8,7 +8,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.OData.UriParser;
using MongoDB.Driver; using MongoDB.Driver;
using NodaTime; using NodaTime;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
@ -18,6 +17,7 @@ using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{ {
@ -46,17 +46,17 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
contentsPublished.Initialize(); contentsPublished.Initialize();
} }
public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, ODataUriParser odataQuery) public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Query query)
{ {
using (Profiler.TraceMethod<MongoContentRepository>("QueryAsyncByQuery")) using (Profiler.TraceMethod<MongoContentRepository>("QueryAsyncByQuery"))
{ {
if (RequiresPublished(status)) if (RequiresPublished(status))
{ {
return await contentsPublished.QueryAsync(app, schema, odataQuery); return await contentsPublished.QueryAsync(app, schema, query);
} }
else else
{ {
return await contentsDraft.QueryAsync(app, schema, odataQuery, status, true); return await contentsDraft.QueryAsync(app, schema, query, status, true);
} }
} }
} }

79
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs

@ -9,7 +9,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using Microsoft.OData.UriParser;
using MongoDB.Bson.Serialization.Attributes; using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver; using MongoDB.Driver;
using NodaTime; using NodaTime;
@ -17,6 +16,7 @@ using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.GenerateEdmSchema; using Squidex.Domain.Apps.Core.GenerateEdmSchema;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure.MongoDb.OData; using Squidex.Infrastructure.MongoDb.OData;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors
{ {
@ -28,77 +28,98 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors
typeof(MongoContentEntity).GetProperties() typeof(MongoContentEntity).GetProperties()
.ToDictionary(x => x.Name, x => x.GetCustomAttribute<BsonElementAttribute>()?.ElementName ?? x.Name, StringComparer.OrdinalIgnoreCase); .ToDictionary(x => x.Name, x => x.GetCustomAttribute<BsonElementAttribute>()?.ElementName ?? x.Name, StringComparer.OrdinalIgnoreCase);
public static readonly ConvertValue ValueConverter = (field, value) => private sealed class AdaptionVisitor : TransformVisitor
{ {
if (value is Instant instant && private readonly Func<IReadOnlyList<string>, IReadOnlyList<string>> pathConverter;
!string.Equals(field, "mt", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(field, "ct", StringComparison.OrdinalIgnoreCase)) public AdaptionVisitor(Func<IReadOnlyList<string>, IReadOnlyList<string>> pathConverter)
{ {
return instant.ToString(); this.pathConverter = pathConverter;
} }
return value; public override FilterNode Visit(FilterComparison nodeIn)
}; {
var value = nodeIn.Value;
if (value is Instant instant &&
!string.Equals(nodeIn.Path[0], "mt", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(nodeIn.Path[0], "ct", StringComparison.OrdinalIgnoreCase))
{
value = instant.ToString();
}
return new FilterComparison(pathConverter(nodeIn.Path), nodeIn.Operator, value, nodeIn.ValueType);
}
}
public static ConvertProperty CreatePropertyCalculator(Schema schema, bool useDraft) public static Query AdjustToModel(this Query query, Schema schema, bool useDraft)
{ {
return propertyNames => var pathConverter = new Func<IReadOnlyList<string>, IReadOnlyList<string>>(propertyNames =>
{ {
if (propertyNames.Length > 1) var result = new List<string>(propertyNames);
if (result.Count > 1)
{ {
var edmName = propertyNames[1].UnescapeEdmField(); var edmName = result[1].UnescapeEdmField();
if (!schema.FieldsByName.TryGetValue(edmName, out var field)) if (!schema.FieldsByName.TryGetValue(edmName, out var field))
{ {
throw new NotSupportedException(); throw new NotSupportedException();
} }
propertyNames[1] = field.Id.ToString(); result[1] = field.Id.ToString();
} }
if (propertyNames.Length > 0) if (result.Count > 0)
{ {
if (propertyNames[0].Equals("Data", StringComparison.CurrentCultureIgnoreCase)) if (result[0].Equals("Data", StringComparison.CurrentCultureIgnoreCase))
{ {
if (useDraft) if (useDraft)
{ {
propertyNames[0] = "dd"; result[0] = "dd";
} }
else else
{ {
propertyNames[0] = "do"; result[0] = "do";
} }
} }
else else
{ {
propertyNames[0] = PropertyMap[propertyNames[0]]; result[0] = PropertyMap[propertyNames[0]];
} }
} }
var propertyName = string.Join(".", propertyNames); var propertyName = string.Join(".", propertyNames);
return propertyName; return result;
}; });
if (query.Filter != null)
{
query.Filter = query.Filter.Accept(new AdaptionVisitor(pathConverter));
}
query.Sort = query.Sort.Select(x => new SortNode(pathConverter(x.Path), x.SortOrder)).ToList();
return query;
} }
public static IFindFluent<MongoContentEntity, MongoContentEntity> ContentSort(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, ODataUriParser query, ConvertProperty propertyCalculator) public static IFindFluent<MongoContentEntity, MongoContentEntity> ContentSort(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, Query query)
{ {
var sort = query.BuildSort<MongoContentEntity>(propertyCalculator); return cursor.Sort(query.BuildSort<MongoContentEntity>());
return sort != null ? cursor.Sort(sort) : cursor.SortByDescending(x => x.LastModified);
} }
public static IFindFluent<MongoContentEntity, MongoContentEntity> ContentTake(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, ODataUriParser query) public static IFindFluent<MongoContentEntity, MongoContentEntity> ContentTake(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, Query query)
{ {
return cursor.Take(query, 200, 20); return cursor.Take(query);
} }
public static IFindFluent<MongoContentEntity, MongoContentEntity> ContentSkip(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, ODataUriParser query) public static IFindFluent<MongoContentEntity, MongoContentEntity> ContentSkip(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, Query query)
{ {
return cursor.Skip(query); return cursor.Skip(query);
} }
public static FilterDefinition<MongoContentEntity> BuildQuery(ODataUriParser query, Guid schemaId, Status[] status, ConvertProperty propertyCalculator) public static FilterDefinition<MongoContentEntity> BuildQuery(Query query, Guid schemaId, Status[] status)
{ {
var filters = new List<FilterDefinition<MongoContentEntity>> var filters = new List<FilterDefinition<MongoContentEntity>>
{ {
@ -111,7 +132,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors
filters.Add(Filter.In(x => x.Status, status)); filters.Add(Filter.In(x => x.Status, status));
} }
var filter = query.BuildFilter<MongoContentEntity>(propertyCalculator, ValueConverter); var filter = query.BuildFilter<MongoContentEntity>();
if (filter.Filter != null) if (filter.Filter != null)
{ {

14
src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs

@ -6,11 +6,12 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Assets.Guards; using Squidex.Domain.Apps.Entities.Assets.Guards;
using Squidex.Domain.Apps.Entities.Assets.State; using Squidex.Domain.Apps.Entities.Assets.State;
using Squidex.Domain.Apps.Entities.Tags;
using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Assets; using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -46,7 +47,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
{ {
GuardAsset.CanCreate(c); GuardAsset.CanCreate(c);
c.Tags = await tagService.NormalizeTagsAsync(c.AppId.Id, TagGroups.Assets, c.Tags, Snapshot.Tags); c.Tags = await NormalizeTagsAsync(c.AppId.Id, c.Tags);
Create(c); Create(c);
@ -66,7 +67,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
{ {
GuardAsset.CanTag(c); GuardAsset.CanTag(c);
c.Tags = await tagService.NormalizeTagsAsync(Snapshot.AppId.Id, TagGroups.Assets, c.Tags, Snapshot.Tags); c.Tags = await NormalizeTagsAsync(Snapshot.AppId.Id, c.Tags);
Tag(c); Tag(c);
}); });
@ -91,6 +92,13 @@ namespace Squidex.Domain.Apps.Entities.Assets
} }
} }
private async Task<HashSet<string>> NormalizeTagsAsync(Guid appId, HashSet<string> tags)
{
var normalized = await tagService.NormalizeTagsAsync(appId, TagGroups.Assets, tags, Snapshot.Tags);
return new HashSet<string>(normalized.Values);
}
public void Create(CreateAsset command) public void Create(CreateAsset command)
{ {
var @event = SimpleMapper.Map(command, new AssetCreated var @event = SimpleMapper.Map(command, new AssetCreated

49
src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs

@ -9,14 +9,20 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.OData;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities.Assets.Edm;
using Squidex.Domain.Apps.Entities.Assets.Queries;
using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.Tags;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.Queries.OData;
namespace Squidex.Domain.Apps.Entities.Assets namespace Squidex.Domain.Apps.Entities.Assets
{ {
public sealed class AssetQueryService : IAssetQueryService public sealed class AssetQueryService : IAssetQueryService
{ {
private const int MaxResults = 200;
private readonly ITagService tagService; private readonly ITagService tagService;
private readonly IAssetRepository assetRepository; private readonly IAssetRepository assetRepository;
@ -44,7 +50,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
return asset; return asset;
} }
public async Task<IResultList<IAssetEntity>> QueryAsync(QueryContext context, Query query) public async Task<IResultList<IAssetEntity>> QueryAsync(QueryContext context, Q query)
{ {
Guard.NotNull(context, nameof(context)); Guard.NotNull(context, nameof(context));
Guard.NotNull(query, nameof(query)); Guard.NotNull(query, nameof(query));
@ -58,7 +64,9 @@ namespace Squidex.Domain.Apps.Entities.Assets
} }
else else
{ {
assets = await assetRepository.QueryAsync(context.App.Id, query.ODataQuery); var parsedQuery = ParseQuery(context, query.ODataQuery);
assets = await assetRepository.QueryAsync(context.App.Id, parsedQuery);
} }
await DenormalizeTagsAsync(context.App.Id, assets); await DenormalizeTagsAsync(context.App.Id, assets);
@ -66,13 +74,46 @@ namespace Squidex.Domain.Apps.Entities.Assets
return assets; return assets;
} }
private IResultList<IAssetEntity> Sort(IResultList<IAssetEntity> assets, IList<Guid> ids) private IResultList<IAssetEntity> Sort(IResultList<IAssetEntity> assets, IReadOnlyList<Guid> ids)
{ {
var sorted = ids.Select(id => assets.FirstOrDefault(x => x.Id == id)).Where(x => x != null); var sorted = ids.Select(id => assets.FirstOrDefault(x => x.Id == id)).Where(x => x != null);
return ResultList.Create(assets.Total, sorted); return ResultList.Create(assets.Total, sorted);
} }
private Query ParseQuery(QueryContext context, string query)
{
try
{
var result = EdmAssetModel.Edm.ParseQuery(query).ToQuery();
if (result.Filter != null)
{
result.Filter = QueryTagVisitor.Transform(result.Filter, context.App.Id, tagService);
}
if (result.Sort.Count == 0)
{
result.Sort.Add(new SortNode(new List<string> { "lastModified" }, SortOrder.Descending));
}
if (result.Take > MaxResults)
{
result.Take = MaxResults;
}
return result;
}
catch (NotSupportedException)
{
throw new ValidationException($"OData operation is not supported.");
}
catch (ODataException ex)
{
throw new ValidationException($"Failed to parse query: {ex.Message}", ex);
}
}
private async Task DenormalizeTagsAsync(Guid appId, IEnumerable<IAssetEntity> assets) private async Task DenormalizeTagsAsync(Guid appId, IEnumerable<IAssetEntity> assets)
{ {
var tags = new HashSet<string>(assets.Where(x => x.Tags != null).SelectMany(x => x.Tags).Distinct()); var tags = new HashSet<string>(assets.Where(x => x.Tags != null).SelectMany(x => x.Tags).Distinct());

2
src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs

@ -9,10 +9,10 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.Assets.State; using Squidex.Domain.Apps.Entities.Assets.State;
using Squidex.Domain.Apps.Entities.Backup; using Squidex.Domain.Apps.Entities.Backup;
using Squidex.Domain.Apps.Entities.Tags;
using Squidex.Domain.Apps.Events.Assets; using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Assets;

2
src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs

@ -13,7 +13,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
{ {
public interface IAssetQueryService public interface IAssetQueryService
{ {
Task<IResultList<IAssetEntity>> QueryAsync(QueryContext contex, Query query); Task<IResultList<IAssetEntity>> QueryAsync(QueryContext contex, Q query);
Task<IAssetEntity> FindAssetAsync(QueryContext context, Guid id); Task<IAssetEntity> FindAssetAsync(QueryContext context, Guid id);
} }

50
src/Squidex.Domain.Apps.Entities/Assets/Queries/QueryTagVisitor.cs

@ -0,0 +1,50 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.Assets.Queries
{
public sealed class QueryTagVisitor : TransformVisitor
{
private readonly ITagService tagService;
private readonly Guid appId;
private QueryTagVisitor(Guid appId, ITagService tagService)
{
this.appId = appId;
this.tagService = tagService;
}
public static FilterNode Transform(FilterNode nodeIn, Guid appId, ITagService tagService)
{
Guard.NotNull(tagService, nameof(tagService));
return nodeIn.Accept(new QueryTagVisitor(appId, tagService));
}
public override FilterNode Visit(FilterComparison nodeIn)
{
if (string.Equals(nodeIn.Path[0], nameof(IAssetEntity.Tags), StringComparison.OrdinalIgnoreCase) && nodeIn.Value is string stringValue)
{
var tagNames = Task.Run(() => tagService.GetTagIdsAsync(appId, TagGroups.Assets, HashSet.Of(stringValue))).Result;
if (tagNames.TryGetValue(stringValue, out var normalized))
{
return new FilterComparison(nodeIn.Path, nodeIn.Operator, normalized, FilterValueType.String);
}
}
return nodeIn;
}
}
}

3
src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs

@ -9,12 +9,13 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.Assets.Repositories namespace Squidex.Domain.Apps.Entities.Assets.Repositories
{ {
public interface IAssetRepository public interface IAssetRepository
{ {
Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, string query = null); Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, Query query);
Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, HashSet<Guid> ids); Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, HashSet<Guid> ids);

2
src/Squidex.Domain.Apps.Entities/Backup/Helpers/ArchiveHelper.cs

@ -36,7 +36,7 @@ namespace Squidex.Domain.Apps.Entities.Backup.Archive
{ {
var hash = 17; var hash = 17;
foreach (char c in value) foreach (var c in value)
{ {
unchecked unchecked
{ {

28
src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs

@ -10,7 +10,6 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.OData; using Microsoft.OData;
using Microsoft.OData.UriParser;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting;
@ -19,6 +18,8 @@ using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.Queries.OData;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
#pragma warning disable RECS0147 #pragma warning disable RECS0147
@ -27,6 +28,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
public sealed class ContentQueryService : IContentQueryService public sealed class ContentQueryService : IContentQueryService
{ {
private const int MaxResults = 20;
private static readonly Status[] StatusAll = { Status.Archived, Status.Draft, Status.Published }; private static readonly Status[] StatusAll = { Status.Archived, Status.Draft, Status.Published };
private static readonly Status[] StatusArchived = { Status.Archived }; private static readonly Status[] StatusArchived = { Status.Archived };
private static readonly Status[] StatusPublished = { Status.Published }; private static readonly Status[] StatusPublished = { Status.Published };
@ -88,7 +90,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
} }
} }
public async Task<IResultList<IContentEntity>> QueryAsync(ContentQueryContext context, Query query) public async Task<IResultList<IContentEntity>> QueryAsync(ContentQueryContext context, Q query)
{ {
Guard.NotNull(context, nameof(context)); Guard.NotNull(context, nameof(context));
@ -128,7 +130,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
return ResultList.Create(contents.Total, transformed); return ResultList.Create(contents.Total, transformed);
} }
private IResultList<IContentEntity> Sort(IResultList<IContentEntity> contents, IList<Guid> ids) private IResultList<IContentEntity> Sort(IResultList<IContentEntity> contents, IReadOnlyList<Guid> ids)
{ {
var sorted = ids.Select(id => contents.FirstOrDefault(x => x.Id == id)).Where(x => x != null); var sorted = ids.Select(id => contents.FirstOrDefault(x => x.Id == id)).Where(x => x != null);
@ -197,7 +199,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
} }
} }
private ODataUriParser ParseQuery(QueryContext context, string query, ISchemaEntity schema) private Query ParseQuery(QueryContext context, string query, ISchemaEntity schema)
{ {
using (Profiler.TraceMethod<ContentQueryService>()) using (Profiler.TraceMethod<ContentQueryService>())
{ {
@ -205,7 +207,23 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
var model = modelBuilder.BuildEdmModel(schema, context.App); var model = modelBuilder.BuildEdmModel(schema, context.App);
return model.ParseQuery(query); var result = model.ParseQuery(query).ToQuery();
if (result.Sort.Count == 0)
{
result.Sort.Add(new SortNode(new List<string> { "lastModified" }, SortOrder.Descending));
}
if (result.Take > MaxResults)
{
result.Take = MaxResults;
}
return result;
}
catch (NotSupportedException)
{
throw new ValidationException($"OData operation is not supported.");
} }
catch (ODataException ex) catch (ODataException ex)
{ {

2
src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs

@ -13,7 +13,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
public interface IContentQueryService public interface IContentQueryService
{ {
Task<IResultList<IContentEntity>> QueryAsync(ContentQueryContext context, Query query); Task<IResultList<IContentEntity>> QueryAsync(ContentQueryContext context, Q query);
Task<IContentEntity> FindContentAsync(ContentQueryContext context, Guid id, long version = EtagVersion.Any); Task<IContentEntity> FindContentAsync(ContentQueryContext context, Guid id, long version = EtagVersion.Any);

8
src/Squidex.Domain.Apps.Entities/Contents/QueryExecutionContext.cs

@ -70,7 +70,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
public async Task<IResultList<IAssetEntity>> QueryAssetsAsync(string query) public async Task<IResultList<IAssetEntity>> QueryAssetsAsync(string query)
{ {
var assets = await assetQuery.QueryAsync(context, Query.Empty.WithODataQuery(query)); var assets = await assetQuery.QueryAsync(context, Q.Empty.WithODataQuery(query));
foreach (var asset in assets) foreach (var asset in assets)
{ {
@ -82,7 +82,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
public async Task<IResultList<IContentEntity>> QueryContentsAsync(string schemaIdOrName, string query) public async Task<IResultList<IContentEntity>> QueryContentsAsync(string schemaIdOrName, string query)
{ {
var result = await contentQuery.QueryAsync(new ContentQueryContext(context).WithSchemaName(schemaIdOrName), Query.Empty.WithODataQuery(query)); var result = await contentQuery.QueryAsync(new ContentQueryContext(context).WithSchemaName(schemaIdOrName), Q.Empty.WithODataQuery(query));
foreach (var content in result) foreach (var content in result)
{ {
@ -100,7 +100,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (notLoadedAssets.Count > 0) if (notLoadedAssets.Count > 0)
{ {
var assets = await assetQuery.QueryAsync(context, Query.Empty.WithIds(notLoadedAssets)); var assets = await assetQuery.QueryAsync(context, Q.Empty.WithIds(notLoadedAssets));
foreach (var asset in assets) foreach (var asset in assets)
{ {
@ -119,7 +119,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (notLoadedContents.Count > 0) if (notLoadedContents.Count > 0)
{ {
var result = await contentQuery.QueryAsync(new ContentQueryContext(context).WithSchemaId(schemaId), Query.Empty.WithIds(notLoadedContents)); var result = await contentQuery.QueryAsync(new ContentQueryContext(context).WithSchemaId(schemaId), Q.Empty.WithIds(notLoadedContents));
foreach (var content in result) foreach (var content in result)
{ {

4
src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs

@ -8,12 +8,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.OData.UriParser;
using NodaTime; using NodaTime;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.Contents.Repositories namespace Squidex.Domain.Apps.Entities.Contents.Repositories
{ {
@ -21,7 +21,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Repositories
{ {
Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet<Guid> ids); Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet<Guid> ids);
Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, ODataUriParser odataQuery); Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Query query);
Task<IReadOnlyList<Guid>> QueryNotFoundAsync(Guid appId, Guid schemaId, IList<Guid> ids); Task<IReadOnlyList<Guid>> QueryNotFoundAsync(Guid appId, Guid schemaId, IList<Guid> ids);

18
src/Squidex.Domain.Apps.Entities/Query.cs → src/Squidex.Domain.Apps.Entities/Q.cs

@ -12,39 +12,41 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities namespace Squidex.Domain.Apps.Entities
{ {
public sealed class Query : Cloneable<Query> public sealed class Q : Cloneable<Q>
{ {
public static readonly Query Empty = new Query(); public static readonly Q Empty = new Q();
public List<Guid> Ids { get; private set; } public IReadOnlyList<Guid> Ids { get; private set; }
public string ODataQuery { get; private set; } public string ODataQuery { get; private set; }
public Query WithODataQuery(string odataQuery) public Q WithODataQuery(string odataQuery)
{ {
return Clone(c => c.ODataQuery = odataQuery); return Clone(c => c.ODataQuery = odataQuery);
} }
public Query WithIds(IEnumerable<Guid> ids) public Q WithIds(IEnumerable<Guid> ids)
{ {
return Clone(c => c.Ids = ids.ToList()); return Clone(c => c.Ids = ids.ToList());
} }
public Query WithIds(string ids) public Q WithIds(string ids)
{ {
if (!string.IsNullOrEmpty(ids)) if (!string.IsNullOrEmpty(ids))
{ {
return Clone(c => return Clone(c =>
{ {
c.Ids = new List<Guid>(); var idsList = new List<Guid>();
foreach (var id in ids.Split(',')) foreach (var id in ids.Split(','))
{ {
if (Guid.TryParse(id, out var guid)) if (Guid.TryParse(id, out var guid))
{ {
c.Ids.Add(guid); idsList.Add(guid);
} }
} }
c.Ids = idsList;
}); });
} }

5
src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs

@ -9,6 +9,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Orleans; using Orleans;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Infrastructure; using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Tags namespace Squidex.Domain.Apps.Entities.Tags
@ -29,12 +30,12 @@ namespace Squidex.Domain.Apps.Entities.Tags
this.grainFactory = grainFactory; this.grainFactory = grainFactory;
} }
public Task<HashSet<string>> NormalizeTagsAsync(Guid appId, string group, HashSet<string> names, HashSet<string> ids) public Task<Dictionary<string, string>> NormalizeTagsAsync(Guid appId, string group, HashSet<string> names, HashSet<string> ids)
{ {
return GetGrain(appId, group).NormalizeTagsAsync(names, ids); return GetGrain(appId, group).NormalizeTagsAsync(names, ids);
} }
public Task<HashSet<string>> GetTagIdsAsync(Guid appId, string group, HashSet<string> names) public Task<Dictionary<string, string>> GetTagIdsAsync(Guid appId, string group, HashSet<string> names)
{ {
return GetGrain(appId, group).GetTagIdsAsync(names); return GetGrain(appId, group).GetTagIdsAsync(names);
} }

5
src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs

@ -8,14 +8,15 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Orleans; using Orleans;
using Squidex.Domain.Apps.Core.Tags;
namespace Squidex.Domain.Apps.Entities.Tags namespace Squidex.Domain.Apps.Entities.Tags
{ {
public interface ITagGrain : IGrainWithStringKey public interface ITagGrain : IGrainWithStringKey
{ {
Task<HashSet<string>> NormalizeTagsAsync(HashSet<string> names, HashSet<string> ids); Task<Dictionary<string, string>> NormalizeTagsAsync(HashSet<string> names, HashSet<string> ids);
Task<HashSet<string>> GetTagIdsAsync(HashSet<string> names); Task<Dictionary<string, string>> GetTagIdsAsync(HashSet<string> names);
Task<Dictionary<string, string>> DenormalizeTagsAsync(HashSet<string> ids); Task<Dictionary<string, string>> DenormalizeTagsAsync(HashSet<string> ids);

15
src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs

@ -9,6 +9,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.States; using Squidex.Infrastructure.States;
@ -58,9 +59,9 @@ namespace Squidex.Domain.Apps.Entities.Tags
return persistence.WriteSnapshotAsync(state); return persistence.WriteSnapshotAsync(state);
} }
public async Task<HashSet<string>> NormalizeTagsAsync(HashSet<string> names, HashSet<string> ids) public async Task<Dictionary<string, string>> NormalizeTagsAsync(HashSet<string> names, HashSet<string> ids)
{ {
var result = new HashSet<string>(); var result = new Dictionary<string, string>();
if (names != null) if (names != null)
{ {
@ -89,7 +90,7 @@ namespace Squidex.Domain.Apps.Entities.Tags
state.Tags.Add(tagId, new Tag { Name = tagName }); state.Tags.Add(tagId, new Tag { Name = tagName });
} }
result.Add(tagId); result.Add(tagName, tagId);
} }
} }
} }
@ -98,7 +99,7 @@ namespace Squidex.Domain.Apps.Entities.Tags
{ {
foreach (var id in ids) foreach (var id in ids)
{ {
if (!result.Contains(id)) if (!result.ContainsValue(id))
{ {
if (state.Tags.TryGetValue(id, out var tagInfo)) if (state.Tags.TryGetValue(id, out var tagInfo))
{ {
@ -118,9 +119,9 @@ namespace Squidex.Domain.Apps.Entities.Tags
return result; return result;
} }
public Task<HashSet<string>> GetTagIdsAsync(HashSet<string> names) public Task<Dictionary<string, string>> GetTagIdsAsync(HashSet<string> names)
{ {
var result = new HashSet<string>(); var result = new Dictionary<string, string>();
foreach (var name in names) foreach (var name in names)
{ {
@ -128,7 +129,7 @@ namespace Squidex.Domain.Apps.Entities.Tags
if (!string.IsNullOrWhiteSpace(id)) if (!string.IsNullOrWhiteSpace(id))
{ {
result.Add(id); result.Add(name, id);
} }
} }

83
src/Squidex.Infrastructure.MongoDb/MongoDb/OData/ConstantVisitor.cs

@ -1,83 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Microsoft.OData;
using Microsoft.OData.Edm;
using Microsoft.OData.UriParser;
using NodaTime;
using NodaTime.Text;
namespace Squidex.Infrastructure.MongoDb.OData
{
public sealed class ConstantVisitor : QueryNodeVisitor<object>
{
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()
{
}
public static object Visit(QueryNode node)
{
return node.Accept(Instance);
}
public override object Visit(ConvertNode nodeIn)
{
if (nodeIn.TypeReference.Definition == BooleanType)
{
return bool.Parse(Visit(nodeIn.Source).ToString());
}
if (nodeIn.TypeReference.Definition == GuidType)
{
return Guid.Parse(Visit(nodeIn.Source).ToString());
}
if (nodeIn.TypeReference.Definition == DateTimeType)
{
var value = Visit(nodeIn.Source);
if (value is DateTimeOffset dateTimeOffset)
{
return Instant.FromDateTimeOffset(dateTimeOffset);
}
if (value is DateTime dateTime)
{
return Instant.FromDateTimeUtc(DateTime.SpecifyKind(dateTime, DateTimeKind.Utc));
}
if (value is Date date)
{
return Instant.FromUtc(date.Year, date.Month, date.Day, 0, 0);
}
var parseResult = InstantPattern.General.Parse(Visit(nodeIn.Source).ToString());
if (!parseResult.Success)
{
throw new ODataException("Datetime is not in a valid format. Use ISO 8601");
}
return parseResult.Value;
}
return base.Visit(nodeIn);
}
public override object Visit(ConstantNode nodeIn)
{
return nodeIn.Value;
}
}
}

33
src/Squidex.Infrastructure.MongoDb/MongoDb/OData/FilterBuilder.cs

@ -5,49 +5,28 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using Microsoft.OData;
using Microsoft.OData.UriParser;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Infrastructure.Queries;
namespace Squidex.Infrastructure.MongoDb.OData namespace Squidex.Infrastructure.MongoDb.OData
{ {
public static class FilterBuilder public static class FilterBuilder
{ {
public static (FilterDefinition<T> Filter, bool Last) BuildFilter<T>(this ODataUriParser query, ConvertProperty convertProperty = null, ConvertValue convertValue = null, bool supportsSearch = true) public static (FilterDefinition<T> Filter, bool Last) BuildFilter<T>(this Query query, bool supportsSearch = true)
{ {
SearchClause search; if (query.FullText != null)
try
{
search = query.ParseSearch();
}
catch (ODataException ex)
{
throw new ValidationException("Query $search clause not valid.", new ValidationError(ex.Message));
}
if (search != null)
{ {
if (!supportsSearch) if (!supportsSearch)
{ {
throw new ValidationException("Query $search clause not supported."); throw new ValidationException("Query $search clause not supported.");
} }
return (Builders<T>.Filter.Text(SearchTermVisitor.Visit(search.Expression).ToString()), false); return (Builders<T>.Filter.Text(query.FullText), false);
}
FilterClause filter;
try
{
filter = query.ParseFilter();
}
catch (ODataException ex)
{
throw new ValidationException("Query $filter clause not valid.", new ValidationError(ex.Message));
} }
if (filter != null) if (query.Filter != null)
{ {
return (FilterVisitor<T>.Visit(filter.Expression, convertProperty, convertValue), true); return (FilterVisitor<T>.Visit(query.Filter), true);
} }
return (null, false); return (null, false);

174
src/Squidex.Infrastructure.MongoDb/MongoDb/OData/FilterVisitor.cs

@ -7,179 +7,77 @@
using System; using System;
using System.Linq; using System.Linq;
using Microsoft.OData.UriParser;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Infrastructure.Queries;
namespace Squidex.Infrastructure.MongoDb.OData namespace Squidex.Infrastructure.MongoDb.OData
{ {
public sealed class FilterVisitor<T> : QueryNodeVisitor<FilterDefinition<T>> public sealed class FilterVisitor<T> : FilterNodeVisitor<FilterDefinition<T>>
{ {
private static readonly FilterDefinitionBuilder<T> Filter = Builders<T>.Filter; private static readonly FilterDefinitionBuilder<T> Filter = Builders<T>.Filter;
private readonly ConvertProperty convertProperty; private static readonly FilterVisitor<T> Instance = new FilterVisitor<T>();
private readonly ConvertValue convertValue;
private FilterVisitor(ConvertProperty convertProperty, ConvertValue convertValue) private FilterVisitor()
{ {
this.convertProperty = convertProperty;
this.convertValue = convertValue;
} }
public static FilterDefinition<T> Visit(QueryNode node, ConvertProperty propertyCalculator, ConvertValue convertValue) public static FilterDefinition<T> Visit(FilterNode node)
{ {
var visitor = new FilterVisitor<T>(propertyCalculator, convertValue); return node.Accept(Instance);
return node.Accept(visitor);
}
public override FilterDefinition<T> Visit(ConvertNode nodeIn)
{
return nodeIn.Source.Accept(this);
} }
public override FilterDefinition<T> Visit(UnaryOperatorNode nodeIn) public override FilterDefinition<T> Visit(FilterNegate nodeIn)
{ {
if (nodeIn.OperatorKind == UnaryOperatorKind.Not) return Filter.Not(nodeIn.Operand.Accept(this));
{
return Filter.Not(nodeIn.Operand.Accept(this));
}
throw new NotSupportedException();
} }
public override FilterDefinition<T> Visit(SingleValueFunctionCallNode nodeIn) public override FilterDefinition<T> Visit(FilterJunction nodeIn)
{ {
var fieldNode = nodeIn.Parameters.ElementAt(0); if (nodeIn.JunctionType == FilterJunctionType.And)
var valueNode = nodeIn.Parameters.ElementAt(1);
if (string.Equals(nodeIn.Name, "endswith", StringComparison.OrdinalIgnoreCase))
{
var f = BuildFieldDefinition(fieldNode);
var v = BuildRegex(f, valueNode, s => s + "$");
return Filter.Regex(f, v);
}
if (string.Equals(nodeIn.Name, "startswith", StringComparison.OrdinalIgnoreCase))
{ {
var f = BuildFieldDefinition(fieldNode); return Filter.And(nodeIn.Operands.Select(x => x.Accept(this)));
var v = BuildRegex(f, valueNode, s => "^" + s);
return Filter.Regex(f, v);
} }
else
if (string.Equals(nodeIn.Name, "contains", StringComparison.OrdinalIgnoreCase))
{ {
var f = BuildFieldDefinition(fieldNode); return Filter.Or(nodeIn.Operands.Select(x => x.Accept(this)));
var v = BuildRegex(f, valueNode, s => s);
return Filter.Regex(f, v);
} }
throw new NotSupportedException(); throw new NotSupportedException();
} }
public override FilterDefinition<T> Visit(BinaryOperatorNode nodeIn) public override FilterDefinition<T> Visit(FilterComparison nodeIn)
{ {
if (nodeIn.OperatorKind == BinaryOperatorKind.And) var propertyName = string.Join(".", nodeIn.Path);
{
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); switch (nodeIn.Operator)
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) case FilterOperator.StartsWith:
{ return Filter.Regex(propertyName, BuildRegex(nodeIn, s => "$" + s));
var f = BuildFieldDefinition(nodeIn.Left); case FilterOperator.Contains:
var v = BuildValue(f, nodeIn.Right); return Filter.Regex(propertyName, BuildRegex(nodeIn, s => s));
case FilterOperator.EndsWith:
return Filter.Or(Filter.Not(Filter.Exists(f)), Filter.Ne(f, v)); return Filter.Regex(propertyName, BuildRegex(nodeIn, s => s + "$"));
} case FilterOperator.Equals:
return Filter.Eq(propertyName, nodeIn.Value);
if (nodeIn.OperatorKind == BinaryOperatorKind.Equal) case FilterOperator.GreaterThan:
{ return Filter.Gt(propertyName, nodeIn.Value);
var f = BuildFieldDefinition(nodeIn.Left); case FilterOperator.GreaterThanOrEqual:
var v = BuildValue(f, nodeIn.Right); return Filter.Gte(propertyName, nodeIn.Value);
case FilterOperator.LessThan:
return Filter.Eq(f, v); return Filter.Lt(propertyName, nodeIn.Value);
} case FilterOperator.LessThanOrEqual:
return Filter.Lte(propertyName, nodeIn.Value);
if (nodeIn.OperatorKind == BinaryOperatorKind.LessThan) case FilterOperator.NotEquals:
{ return Filter.Ne(propertyName, nodeIn.Value);
var f = BuildFieldDefinition(nodeIn.Left);
var v = BuildValue(f, nodeIn.Right);
return Filter.Lt(f, v);
}
if (nodeIn.OperatorKind == BinaryOperatorKind.LessThanOrEqual)
{
var f = BuildFieldDefinition(nodeIn.Left);
var v = BuildValue(f, nodeIn.Right);
return Filter.Lte(f, v);
}
if (nodeIn.OperatorKind == BinaryOperatorKind.GreaterThan)
{
var f = BuildFieldDefinition(nodeIn.Left);
var v = BuildValue(f, nodeIn.Right);
return Filter.Gt(f, v);
}
if (nodeIn.OperatorKind == BinaryOperatorKind.GreaterThanOrEqual)
{
var f = BuildFieldDefinition(nodeIn.Left);
var v = BuildValue(f, nodeIn.Right);
return Filter.Gte(f, v);
}
} }
throw new NotSupportedException(); throw new NotSupportedException();
} }
private BsonRegularExpression BuildRegex(string field, QueryNode node, Func<string, string> formatter) private BsonRegularExpression BuildRegex(FilterComparison node, Func<string, string> formatter)
{
return new BsonRegularExpression(formatter(BuildValue(field, node).ToString()), "i");
}
private string BuildFieldDefinition(QueryNode nodeIn)
{
return nodeIn.BuildFieldDefinition(convertProperty);
}
private object BuildValue(string field, QueryNode nodeIn)
{
return ValueConversion.Convert(field, ConstantVisitor.Visit(nodeIn), convertValue);
}
private object BuildValue(QueryNode nodeIn)
{ {
return ConstantVisitor.Visit(nodeIn); return new BsonRegularExpression(formatter(node.Value.ToString()), "i");
} }
} }
} }

27
src/Squidex.Infrastructure.MongoDb/MongoDb/OData/LimitExtensions.cs

@ -5,41 +5,28 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using Microsoft.OData.UriParser;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Infrastructure.Queries;
namespace Squidex.Infrastructure.MongoDb.OData namespace Squidex.Infrastructure.MongoDb.OData
{ {
public static class LimitExtensions public static class LimitExtensions
{ {
public static IFindFluent<T, T> Take<T>(this IFindFluent<T, T> cursor, ODataUriParser query, int maxValue = 200, int defaultValue = 20) public static IFindFluent<T, T> Take<T>(this IFindFluent<T, T> cursor, Query query)
{ {
var top = query.ParseTop(); if (query.Take < long.MaxValue)
if (top.HasValue)
{
cursor = cursor.Limit(Math.Min((int)top.Value, maxValue));
}
else
{ {
cursor = cursor.Limit(defaultValue); cursor = cursor.Limit((int)query.Take);
} }
return cursor; return cursor;
} }
public static IFindFluent<T, T> Skip<T>(this IFindFluent<T, T> cursor, ODataUriParser query) public static IFindFluent<T, T> Skip<T>(this IFindFluent<T, T> cursor, Query query)
{ {
var skip = query.ParseSkip(); if (query.Skip > 0)
if (skip.HasValue)
{
cursor = cursor.Skip((int)skip.Value);
}
else
{ {
cursor = cursor.Skip(null); cursor = cursor.Skip((int)query.Skip);
} }
return cursor; return cursor;

32
src/Squidex.Infrastructure.MongoDb/MongoDb/OData/PropertyBuilder.cs

@ -1,32 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Linq;
using Microsoft.OData.UriParser;
namespace Squidex.Infrastructure.MongoDb.OData
{
public delegate string ConvertProperty(string[] parts);
public static class PropertyBuilder
{
private static readonly ConvertProperty Default = parts =>
{
return string.Join(".", parts).ToPascalCase();
};
public static string BuildFieldDefinition(this QueryNode node, ConvertProperty convertProperty)
{
convertProperty = convertProperty ?? Default;
var propertyParts = node.Accept(PropertyNameVisitor.Instance).ToArray();
var propertyName = convertProperty(propertyParts);
return propertyName;
}
}
}

20
src/Squidex.Infrastructure.MongoDb/MongoDb/OData/SortBuilder.cs

@ -6,26 +6,22 @@
// ========================================================================== // ==========================================================================
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.OData.UriParser;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Infrastructure.Queries;
namespace Squidex.Infrastructure.MongoDb.OData namespace Squidex.Infrastructure.MongoDb.OData
{ {
public static class SortBuilder public static class SortBuilder
{ {
public static SortDefinition<T> BuildSort<T>(this ODataUriParser query, ConvertProperty propertyCalculator = null) public static SortDefinition<T> BuildSort<T>(this Query query)
{ {
var orderBy = query.ParseOrderBy(); if (query.Sort.Count > 0)
if (orderBy != null)
{ {
var sorts = new List<SortDefinition<T>>(); var sorts = new List<SortDefinition<T>>();
while (orderBy != null) foreach (var sort in query.Sort)
{ {
sorts.Add(OrderBy<T>(orderBy, propertyCalculator)); sorts.Add(OrderBy<T>(sort));
orderBy = orderBy.ThenBy;
} }
if (sorts.Count > 1) if (sorts.Count > 1)
@ -41,11 +37,11 @@ namespace Squidex.Infrastructure.MongoDb.OData
return null; return null;
} }
public static SortDefinition<T> OrderBy<T>(OrderByClause clause, ConvertProperty propertyCalculator = null) public static SortDefinition<T> OrderBy<T>(SortNode sort)
{ {
var propertyName = clause.Expression.BuildFieldDefinition(propertyCalculator); var propertyName = string.Join(".", sort.Path);
if (clause.Direction == OrderByDirection.Ascending) if (sort.SortOrder == SortOrder.Ascending)
{ {
return Builders<T>.Sort.Ascending(propertyName); return Builders<T>.Sort.Ascending(propertyName);
} }

1
src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj

@ -11,7 +11,6 @@
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" /> <ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.OData.Core" Version="7.5.0" />
<PackageReference Include="MongoDB.Driver" Version="2.7.0" /> <PackageReference Include="MongoDB.Driver" Version="2.7.0" />
<PackageReference Include="MongoDB.Driver.GridFS" Version="2.7.0" /> <PackageReference Include="MongoDB.Driver.GridFS" Version="2.7.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" />

2
src/Squidex.Infrastructure/Guard.cs

@ -142,7 +142,7 @@ namespace Squidex.Infrastructure
[DebuggerStepThrough] [DebuggerStepThrough]
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void NotEmpty<TType>(ICollection<TType> enumerable, string parameterName) public static void NotEmpty<TType>(IReadOnlyCollection<TType> enumerable, string parameterName)
{ {
NotNull(enumerable, parameterName); NotNull(enumerable, parameterName);

32
src/Squidex.Infrastructure/Queries/FilterBuilder.cs

@ -0,0 +1,32 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Infrastructure.Queries
{
public static class FilterBuilder
{
public static FilterJunction And(params FilterNode[] operands)
{
return new FilterJunction(FilterJunctionType.And, operands);
}
public static FilterJunction Or(params FilterNode[] operands)
{
return new FilterJunction(FilterJunctionType.Or, operands);
}
public static FilterComparison Eq(string path, object value)
{
return Binary(path, FilterOperator.Equals, value);
}
private static FilterComparison Binary(string path, FilterOperator @operator, object value)
{
return new FilterComparison(path.Split('.', '/'), @operator, value, FilterValueType.String);
}
}
}

71
src/Squidex.Infrastructure/Queries/FilterComparison.cs

@ -0,0 +1,71 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
namespace Squidex.Infrastructure.Queries
{
public sealed class FilterComparison : FilterNode
{
public IReadOnlyList<string> Path { get; }
public FilterOperator Operator { get; }
public FilterValueType ValueType { get; }
public object Value { get; }
public FilterComparison(IReadOnlyList<string> path, FilterOperator @operator, object value, FilterValueType valueType)
{
Guard.NotNull(path, nameof(path));
Guard.NotEmpty(path, nameof(path));
Guard.Enum(@operator, nameof(@operator));
Guard.Enum(valueType, nameof(valueType));
Path = path;
Value = value;
ValueType = valueType;
Operator = @operator;
}
public override T Accept<T>(FilterNodeVisitor<T> visitor)
{
return visitor.Visit(this);
}
public override string ToString()
{
var path = string.Join(".", Path);
switch (Operator)
{
case FilterOperator.Contains:
return $"contains({path}, {Value})";
case FilterOperator.EndsWith:
return $"endsWith({path}, {Value})";
case FilterOperator.StartsWith:
return $"startsWith({path}, {Value})";
case FilterOperator.Equals:
return $"{path} == {Value}";
case FilterOperator.NotEquals:
return $"{path} != {Value}";
case FilterOperator.GreaterThan:
return $"{path} > {Value}";
case FilterOperator.GreaterThanOrEqual:
return $"{path} >= {Value}";
case FilterOperator.LessThan:
return $"{path} < {Value}";
case FilterOperator.LessThanOrEqual:
return $"{path} <= {Value}";
default:
return string.Empty;
}
}
}
}

45
src/Squidex.Infrastructure/Queries/FilterJunction.cs

@ -0,0 +1,45 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
namespace Squidex.Infrastructure.Queries
{
public sealed class FilterJunction : FilterNode
{
public IReadOnlyList<FilterNode> Operands { get; }
public FilterJunctionType JunctionType { get; }
public FilterJunction(FilterJunctionType junctionType, IReadOnlyList<FilterNode> operands)
{
Guard.NotNull(operands, nameof(operands));
Guard.GreaterEquals(operands.Count, 2, nameof(operands.Count));
Guard.Enum(junctionType, nameof(junctionType));
Operands = operands;
JunctionType = junctionType;
}
public FilterJunction(FilterJunctionType junctionType, params FilterNode[] operands)
: this(junctionType, operands?.ToList())
{
}
public override T Accept<T>(FilterNodeVisitor<T> visitor)
{
return visitor.Visit(this);
}
public override string ToString()
{
return $"({string.Join(JunctionType == FilterJunctionType.And ? " && " : " || ", Operands)})";
}
}
}

7
src/Squidex.Domain.Apps.Entities/Tags/TagGroups.cs → src/Squidex.Infrastructure/Queries/FilterJunctionType.cs

@ -5,10 +5,11 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
namespace Squidex.Domain.Apps.Entities.Tags namespace Squidex.Infrastructure.Queries
{ {
public static class TagGroups public enum FilterJunctionType
{ {
public const string Assets = "Assets"; And,
Or
} }
} }

31
src/Squidex.Infrastructure/Queries/FilterNegate.cs

@ -0,0 +1,31 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Infrastructure.Queries
{
public sealed class FilterNegate : FilterNode
{
public FilterNode Operand { get; }
public FilterNegate(FilterNode operand)
{
Guard.NotNull(operand, nameof(operand));
Operand = operand;
}
public override T Accept<T>(FilterNodeVisitor<T> visitor)
{
return visitor.Visit(this);
}
public override string ToString()
{
return $"!({Operand})";
}
}
}

14
src/Squidex.Infrastructure/Queries/FilterNode.cs

@ -0,0 +1,14 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Infrastructure.Queries
{
public abstract class FilterNode
{
public abstract T Accept<T>(FilterNodeVisitor<T> visitor);
}
}

31
src/Squidex.Infrastructure/Queries/FilterNodeVisitor.cs

@ -0,0 +1,31 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
#pragma warning disable RECS0083 // Shows NotImplementedException throws in the quick task bar
namespace Squidex.Infrastructure.Queries
{
public abstract class FilterNodeVisitor<T>
{
public virtual T Visit(FilterComparison nodeIn)
{
throw new NotImplementedException();
}
public virtual T Visit(FilterJunction nodeIn)
{
throw new NotImplementedException();
}
public virtual T Visit(FilterNegate nodeIn)
{
throw new NotImplementedException();
}
}
}

22
src/Squidex.Infrastructure/Queries/FilterOperator.cs

@ -0,0 +1,22 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Infrastructure.Queries
{
public enum FilterOperator
{
Contains,
EndsWith,
Equals,
GreaterThan,
GreaterThanOrEqual,
LessThan,
LessThanOrEqual,
NotEquals,
StartsWith
}
}

21
src/Squidex.Infrastructure/Queries/FilterValueType.cs

@ -0,0 +1,21 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Infrastructure.Queries
{
public enum FilterValueType
{
Boolean,
Guid,
Double,
Instant,
Int32,
Int64,
Single,
String,
}
}

30
src/Squidex.Infrastructure/Queries/OData/ConstantVisitor.cs

@ -0,0 +1,30 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.OData.UriParser;
namespace Squidex.Infrastructure.Queries.OData
{
public sealed class ConstantVisitor : QueryNodeVisitor<object>
{
private static readonly ConstantVisitor Instance = new ConstantVisitor();
private ConstantVisitor()
{
}
public static object Visit(QueryNode node)
{
return node.Accept(Instance);
}
public override object Visit(ConstantNode nodeIn)
{
return nodeIn.Value;
}
}
}

113
src/Squidex.Infrastructure/Queries/OData/ConstantWithTypeVisitor.cs

@ -0,0 +1,113 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Microsoft.OData;
using Microsoft.OData.Edm;
using Microsoft.OData.UriParser;
using NodaTime;
using NodaTime.Text;
namespace Squidex.Infrastructure.Queries.OData
{
public sealed class ConstantWithTypeVisitor : QueryNodeVisitor<(object Value, FilterValueType ValueType)>
{
private static readonly IEdmPrimitiveType BooleanType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Boolean);
private static readonly IEdmPrimitiveType DateTimeType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.DateTimeOffset);
private static readonly IEdmPrimitiveType DoubleType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Double);
private static readonly IEdmPrimitiveType GuidType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Guid);
private static readonly IEdmPrimitiveType Int32Type = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int32);
private static readonly IEdmPrimitiveType Int64Type = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int64);
private static readonly IEdmPrimitiveType SingleType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Single);
private static readonly IEdmPrimitiveType StringType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.String);
private static readonly ConstantWithTypeVisitor Instance = new ConstantWithTypeVisitor();
private ConstantWithTypeVisitor()
{
}
public static (object Value, FilterValueType ValueType) Visit(QueryNode node)
{
return node.Accept(Instance);
}
public override (object Value, FilterValueType ValueType) Visit(ConvertNode nodeIn)
{
if (nodeIn.TypeReference.Definition == BooleanType)
{
return (bool.Parse(ConstantVisitor.Visit(nodeIn.Source).ToString()), FilterValueType.Boolean);
}
if (nodeIn.TypeReference.Definition == GuidType)
{
return (Guid.Parse(ConstantVisitor.Visit(nodeIn.Source).ToString()), FilterValueType.Guid);
}
if (nodeIn.TypeReference.Definition == DateTimeType)
{
var value = ConstantVisitor.Visit(nodeIn.Source);
if (value is DateTimeOffset dateTimeOffset)
{
return (Instant.FromDateTimeOffset(dateTimeOffset), FilterValueType.Instant);
}
if (value is DateTime dateTime)
{
return (Instant.FromDateTimeUtc(DateTime.SpecifyKind(dateTime, DateTimeKind.Utc)), FilterValueType.Instant);
}
if (value is Date date)
{
return (Instant.FromUtc(date.Year, date.Month, date.Day, 0, 0), FilterValueType.Instant);
}
var parseResult = InstantPattern.General.Parse(Visit(nodeIn.Source).ToString());
if (!parseResult.Success)
{
throw new ODataException("Datetime is not in a valid format. Use ISO 8601");
}
return (parseResult.Value, FilterValueType.Instant);
}
return base.Visit(nodeIn);
}
public override (object Value, FilterValueType ValueType) Visit(ConstantNode nodeIn)
{
if (nodeIn.TypeReference.Definition == BooleanType)
{
return (nodeIn.Value, FilterValueType.Boolean);
}
if (nodeIn.TypeReference.Definition == DoubleType)
{
return (nodeIn.Value, FilterValueType.Double);
}
if (nodeIn.TypeReference.Definition == Int32Type)
{
return (nodeIn.Value, FilterValueType.Int32);
}
if (nodeIn.TypeReference.Definition == Int64Type)
{
return (nodeIn.Value, FilterValueType.Int64);
}
if (nodeIn.TypeReference.Definition == StringType)
{
return (nodeIn.Value, FilterValueType.String);
}
throw new NotSupportedException();
}
}
}

17
src/Squidex.Domain.Apps.Entities/EdmModelExtensions.cs → src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs

@ -10,7 +10,7 @@ using System.Linq;
using Microsoft.OData.Edm; using Microsoft.OData.Edm;
using Microsoft.OData.UriParser; using Microsoft.OData.UriParser;
namespace Squidex.Domain.Apps.Entities namespace Squidex.Infrastructure.Queries.OData
{ {
public static class EdmModelExtensions public static class EdmModelExtensions
{ {
@ -34,5 +34,20 @@ namespace Squidex.Domain.Apps.Entities
return parser; return parser;
} }
public static Query ToQuery(this ODataUriParser parser)
{
var query = new Query();
if (parser != null)
{
parser.ParseTake(query);
parser.ParseSkip(query);
parser.ParseFilter(query);
parser.ParseSort(query);
}
return query;
}
} }
} }

48
src/Squidex.Infrastructure/Queries/OData/FilterBuilder.cs

@ -0,0 +1,48 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.OData;
using Microsoft.OData.UriParser;
namespace Squidex.Infrastructure.Queries.OData
{
public static class FilterBuilder
{
public static void ParseFilter(this ODataUriParser query, Query result)
{
SearchClause search;
try
{
search = query.ParseSearch();
}
catch (ODataException ex)
{
throw new ValidationException("Query $search clause not valid.", new ValidationError(ex.Message));
}
if (search != null)
{
result.FullText = 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)
{
result.Filter = FilterVisitor.Visit(filter.Expression);
}
}
}
}

148
src/Squidex.Infrastructure/Queries/OData/FilterVisitor.cs

@ -0,0 +1,148 @@
// ==========================================================================
// 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.UriParser;
namespace Squidex.Infrastructure.Queries.OData
{
public sealed class FilterVisitor : QueryNodeVisitor<FilterNode>
{
private static readonly FilterVisitor Instance = new FilterVisitor();
private FilterVisitor()
{
}
public static FilterNode Visit(QueryNode node)
{
return node.Accept(Instance);
}
public override FilterNode Visit(ConvertNode nodeIn)
{
return nodeIn.Source.Accept(this);
}
public override FilterNode Visit(UnaryOperatorNode nodeIn)
{
if (nodeIn.OperatorKind == UnaryOperatorKind.Not)
{
return new FilterNegate(nodeIn.Operand.Accept(this));
}
throw new NotSupportedException();
}
public override FilterNode 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, valueType) = ConstantWithTypeVisitor.Visit(valueNode);
return new FilterComparison(PropertyPathVisitor.Visit(fieldNode), FilterOperator.EndsWith, value, valueType);
}
if (string.Equals(nodeIn.Name, "startswith", StringComparison.OrdinalIgnoreCase))
{
var (value, valueType) = ConstantWithTypeVisitor.Visit(valueNode);
return new FilterComparison(PropertyPathVisitor.Visit(fieldNode), FilterOperator.StartsWith, value, valueType);
}
if (string.Equals(nodeIn.Name, "contains", StringComparison.OrdinalIgnoreCase))
{
var (value, valueType) = ConstantWithTypeVisitor.Visit(valueNode);
return new FilterComparison(PropertyPathVisitor.Visit(fieldNode), FilterOperator.Contains, value, valueType);
}
throw new NotSupportedException();
}
public override FilterNode Visit(BinaryOperatorNode nodeIn)
{
if (nodeIn.OperatorKind == BinaryOperatorKind.And)
{
return new FilterJunction(FilterJunctionType.And, nodeIn.Left.Accept(this), nodeIn.Right.Accept(this));
}
if (nodeIn.OperatorKind == BinaryOperatorKind.Or)
{
return new FilterJunction(FilterJunctionType.Or, nodeIn.Left.Accept(this), nodeIn.Right.Accept(this));
}
if (nodeIn.Left is SingleValueFunctionCallNode functionNode)
{
var regexFilter = Visit(functionNode);
var (value, valueType) = ConstantWithTypeVisitor.Visit(nodeIn.Right);
if (value is bool booleanRight)
{
if ((nodeIn.OperatorKind == BinaryOperatorKind.Equal && !booleanRight) ||
(nodeIn.OperatorKind == BinaryOperatorKind.NotEqual && booleanRight))
{
regexFilter = new FilterNegate(regexFilter);
}
return regexFilter;
}
}
else
{
if (nodeIn.OperatorKind == BinaryOperatorKind.NotEqual)
{
var (value, valueType) = ConstantWithTypeVisitor.Visit(nodeIn.Right);
return new FilterComparison(PropertyPathVisitor.Visit(nodeIn.Left), FilterOperator.NotEquals, value, valueType);
}
if (nodeIn.OperatorKind == BinaryOperatorKind.Equal)
{
var (value, valueType) = ConstantWithTypeVisitor.Visit(nodeIn.Right);
return new FilterComparison(PropertyPathVisitor.Visit(nodeIn.Left), FilterOperator.Equals, value, valueType);
}
if (nodeIn.OperatorKind == BinaryOperatorKind.LessThan)
{
var (value, valueType) = ConstantWithTypeVisitor.Visit(nodeIn.Right);
return new FilterComparison(PropertyPathVisitor.Visit(nodeIn.Left), FilterOperator.LessThan, value, valueType);
}
if (nodeIn.OperatorKind == BinaryOperatorKind.LessThanOrEqual)
{
var (value, valueType) = ConstantWithTypeVisitor.Visit(nodeIn.Right);
return new FilterComparison(PropertyPathVisitor.Visit(nodeIn.Left), FilterOperator.LessThanOrEqual, value, valueType);
}
if (nodeIn.OperatorKind == BinaryOperatorKind.GreaterThan)
{
var (value, valueType) = ConstantWithTypeVisitor.Visit(nodeIn.Right);
return new FilterComparison(PropertyPathVisitor.Visit(nodeIn.Left), FilterOperator.GreaterThan, value, valueType);
}
if (nodeIn.OperatorKind == BinaryOperatorKind.GreaterThanOrEqual)
{
var (value, valueType) = ConstantWithTypeVisitor.Visit(nodeIn.Right);
return new FilterComparison(PropertyPathVisitor.Visit(nodeIn.Left), FilterOperator.GreaterThanOrEqual, value, valueType);
}
}
throw new NotSupportedException();
}
}
}

34
src/Squidex.Infrastructure/Queries/OData/LimitExtensions.cs

@ -0,0 +1,34 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.OData.UriParser;
namespace Squidex.Infrastructure.Queries.OData
{
public static class LimitExtensions
{
public static void ParseTake(this ODataUriParser query, Query result)
{
var top = query.ParseTop();
if (top.HasValue)
{
result.Take = top.Value;
}
}
public static void ParseSkip(this ODataUriParser query, Query result)
{
var skip = query.ParseSkip();
if (skip.HasValue)
{
result.Skip = skip.Value;
}
}
}
}

27
src/Squidex.Infrastructure.MongoDb/MongoDb/OData/PropertyNameVisitor.cs → src/Squidex.Infrastructure/Queries/OData/PropertyPathVisitor.cs

@ -6,18 +6,24 @@
// ========================================================================== // ==========================================================================
using System.Collections.Immutable; using System.Collections.Immutable;
using Microsoft.OData.Edm;
using Microsoft.OData.UriParser; using Microsoft.OData.UriParser;
namespace Squidex.Infrastructure.MongoDb.OData namespace Squidex.Infrastructure.Queries.OData
{ {
public sealed class PropertyNameVisitor : QueryNodeVisitor<ImmutableList<string>> public sealed class PropertyPathVisitor : QueryNodeVisitor<ImmutableList<string>>
{ {
public static readonly PropertyNameVisitor Instance = new PropertyNameVisitor(); private static readonly PropertyPathVisitor Instance = new PropertyPathVisitor();
private PropertyNameVisitor() private PropertyPathVisitor()
{ {
} }
public static ImmutableList<string> Visit(QueryNode node)
{
return node.Accept(Instance);
}
public override ImmutableList<string> Visit(ConvertNode nodeIn) public override ImmutableList<string> Visit(ConvertNode nodeIn)
{ {
return nodeIn.Source.Accept(this); return nodeIn.Source.Accept(this);
@ -27,11 +33,11 @@ namespace Squidex.Infrastructure.MongoDb.OData
{ {
if (nodeIn.Source is SingleComplexNode) if (nodeIn.Source is SingleComplexNode)
{ {
return nodeIn.Source.Accept(this).Add(nodeIn.Property.Name); return nodeIn.Source.Accept(this).Add(UnescapeEdmField(nodeIn.Property));
} }
else else
{ {
return ImmutableList.Create(nodeIn.Property.Name); return ImmutableList.Create(UnescapeEdmField(nodeIn.Property));
} }
} }
@ -39,12 +45,17 @@ namespace Squidex.Infrastructure.MongoDb.OData
{ {
if (nodeIn.Source is SingleComplexNode) if (nodeIn.Source is SingleComplexNode)
{ {
return nodeIn.Source.Accept(this).Add(nodeIn.Property.Name); return nodeIn.Source.Accept(this).Add(UnescapeEdmField(nodeIn.Property));
} }
else else
{ {
return ImmutableList.Create(nodeIn.Property.Name); return ImmutableList.Create(UnescapeEdmField(nodeIn.Property));
} }
} }
private static string UnescapeEdmField(IEdmProperty property)
{
return property.Name;
}
} }
} }

2
src/Squidex.Infrastructure.MongoDb/MongoDb/OData/SearchTermVisitor.cs → src/Squidex.Infrastructure/Queries/OData/SearchTermVisitor.cs

@ -8,7 +8,7 @@
using System; using System;
using Microsoft.OData.UriParser; using Microsoft.OData.UriParser;
namespace Squidex.Infrastructure.MongoDb.OData namespace Squidex.Infrastructure.Queries.OData
{ {
public class SearchTermVisitor : QueryNodeVisitor<string> public class SearchTermVisitor : QueryNodeVisitor<string>
{ {

43
src/Squidex.Infrastructure/Queries/OData/SortBuilder.cs

@ -0,0 +1,43 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.OData.UriParser;
namespace Squidex.Infrastructure.Queries.OData
{
public static class SortBuilder
{
public static void ParseSort(this ODataUriParser query, Query result)
{
var orderBy = query.ParseOrderBy();
if (orderBy != null)
{
while (orderBy != null)
{
result.Sort.Add(OrderBy(orderBy));
orderBy = orderBy.ThenBy;
}
}
}
public static SortNode OrderBy(OrderByClause clause)
{
var path = PropertyPathVisitor.Visit(clause.Expression);
if (clause.Direction == OrderByDirection.Ascending)
{
return new SortNode(path, SortOrder.Ascending);
}
else
{
return new SortNode(path, SortOrder.Descending);
}
}
}
}

30
src/Squidex.Infrastructure/Queries/PascalCasePathConverter.cs

@ -0,0 +1,30 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Linq;
namespace Squidex.Infrastructure.Queries
{
public sealed class PascalCasePathConverter : TransformVisitor
{
private static readonly PascalCasePathConverter Instance = new PascalCasePathConverter();
private PascalCasePathConverter()
{
}
public static FilterNode Transform(FilterNode node)
{
return node.Accept(Instance);
}
public override FilterNode Visit(FilterComparison nodeIn)
{
return new FilterComparison(nodeIn.Path.Select(x => x.ToPascalCase()).ToList(), nodeIn.Operator, nodeIn.Value, nodeIn.ValueType);
}
}
}

56
src/Squidex.Infrastructure/Queries/Query.cs

@ -0,0 +1,56 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
namespace Squidex.Infrastructure.Queries
{
public sealed class Query
{
public FilterNode Filter { get; set; }
public string FullText { get; set; }
public long Skip { get; set; }
public long Take { get; set; } = long.MaxValue;
public List<SortNode> Sort { get; set; } = new List<SortNode>();
public override string ToString()
{
var parts = new List<string>();
if (Filter != null)
{
parts.Add($"Filter: {Filter}");
}
if (FullText != null)
{
parts.Add($"FullText: {FullText}");
}
if (Skip > 0)
{
parts.Add($"Skip: {Skip}");
}
if (Take < long.MaxValue)
{
parts.Add($"Take: {Take}");
}
if (Sort.Count > 0)
{
parts.Add($"Sort: {string.Join(", ", Sort)}");
}
return string.Join("; ", parts);
}
}
}

24
src/Squidex.Infrastructure/Queries/SortBuilder.cs

@ -0,0 +1,24 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Linq;
namespace Squidex.Infrastructure.Queries
{
public static class SortBuilder
{
public static SortNode Ascending(string path)
{
return new SortNode(path.Split('.', '/').ToList(), SortOrder.Ascending);
}
public static SortNode Descending(string path)
{
return new SortNode(path.Split('.', '/').ToList(), SortOrder.Descending);
}
}
}

36
src/Squidex.Infrastructure/Queries/SortNode.cs

@ -0,0 +1,36 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
namespace Squidex.Infrastructure.Queries
{
public sealed class SortNode
{
public IReadOnlyList<string> Path { get; }
public SortOrder SortOrder { get; set; }
public SortNode(IReadOnlyList<string> path, SortOrder sortOrder)
{
Guard.NotNull(path, nameof(path));
Guard.NotEmpty(path, nameof(path));
Guard.Enum(sortOrder, nameof(sortOrder));
Path = path;
SortOrder = sortOrder;
}
public override string ToString()
{
var path = string.Join(".", Path);
return $"{path} {SortOrder}";
}
}
}

15
src/Squidex.Infrastructure/Queries/SortOrder.cs

@ -0,0 +1,15 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Infrastructure.Queries
{
public enum SortOrder
{
Ascending,
Descending
}
}

29
src/Squidex.Infrastructure/Queries/TransformVisitor.cs

@ -0,0 +1,29 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Linq;
namespace Squidex.Infrastructure.Queries
{
public abstract class TransformVisitor : FilterNodeVisitor<FilterNode>
{
public override FilterNode Visit(FilterComparison nodeIn)
{
return nodeIn;
}
public override FilterNode Visit(FilterJunction nodeIn)
{
return new FilterJunction(nodeIn.JunctionType, nodeIn.Operands.Select(x => x.Accept(this)).ToList());
}
public override FilterNode Visit(FilterNegate nodeIn)
{
return new FilterNegate(nodeIn.Operand.Accept(this));
}
}
}

1
src/Squidex.Infrastructure/Squidex.Infrastructure.csproj

@ -10,6 +10,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="2.1.2" /> <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="2.1.2" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.1.1" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="2.1.1" />
<PackageReference Include="Microsoft.OData.Core" Version="7.5.0" />
<PackageReference Include="Microsoft.Orleans.Core" Version="2.1.0-beta1" /> <PackageReference Include="Microsoft.Orleans.Core" Version="2.1.0-beta1" />
<PackageReference Include="Microsoft.Orleans.OrleansCodeGenerator.Build" Version="2.1.0-beta1"> <PackageReference Include="Microsoft.Orleans.OrleansCodeGenerator.Build" Version="2.1.0-beta1">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>

4
src/Squidex.Infrastructure/Tasks/AsyncLock.cs

@ -24,7 +24,7 @@ namespace Squidex.Infrastructure.Tasks
public Task<IDisposable> LockAsync() public Task<IDisposable> LockAsync()
{ {
Task wait = semaphore.WaitAsync(); var wait = semaphore.WaitAsync();
if (wait.IsCompleted) if (wait.IsCompleted)
{ {
@ -50,7 +50,7 @@ namespace Squidex.Infrastructure.Tasks
public void Dispose() public void Dispose()
{ {
AsyncLock current = target; var current = target;
if (current == null) if (current == null)
{ {

4
src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs

@ -14,12 +14,12 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using NSwag.Annotations; using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.Assets.Models; using Squidex.Areas.Api.Controllers.Assets.Models;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps.Services; using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.Tags;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
@ -102,7 +102,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
{ {
var context = Context(); var context = Context();
var assets = await assetQuery.QueryAsync(context, Query.Empty.WithODataQuery(Request.QueryString.ToString()).WithIds(ids)); var assets = await assetQuery.QueryAsync(context, Q.Empty.WithODataQuery(Request.QueryString.ToString()).WithIds(ids));
var response = AssetsDto.FromAssets(assets); var response = AssetsDto.FromAssets(assets);

2
src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs

@ -130,7 +130,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
{ {
var context = Context().WithArchived(archived).WithSchemaName(name); var context = Context().WithArchived(archived).WithSchemaName(name);
var result = await contentQuery.QueryAsync(context, Query.Empty.WithIds(ids).WithODataQuery(Request.QueryString.ToString())); var result = await contentQuery.QueryAsync(context, Q.Empty.WithIds(ids).WithODataQuery(Request.QueryString.ToString()));
var response = new ContentsDto var response = new ContentsDto
{ {

1
src/Squidex/Config/Domain/EntitiesServices.cs

@ -15,6 +15,7 @@ using Orleans;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.Apps.Commands;

2
src/Squidex/Squidex.csproj

@ -80,7 +80,7 @@
<PackageReference Include="MongoDB.Driver" Version="2.7.0" /> <PackageReference Include="MongoDB.Driver" Version="2.7.0" />
<PackageReference Include="NJsonSchema" Version="9.10.72" /> <PackageReference Include="NJsonSchema" Version="9.10.72" />
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="2.0.0" /> <PackageReference Include="NodaTime.Serialization.JsonNet" Version="2.0.0" />
<PackageReference Include="NSwag.AspNetCore" Version="11.19.0" /> <PackageReference Include="NSwag.AspNetCore" Version="11.19.1" />
<PackageReference Include="OpenCover" Version="4.6.519" /> <PackageReference Include="OpenCover" Version="4.6.519" />
<PackageReference Include="Orleans.Providers.MongoDB" Version="2.0.0" /> <PackageReference Include="Orleans.Providers.MongoDB" Version="2.0.0" />
<PackageReference Include="OrleansDashboard" Version="2.0.9" /> <PackageReference Include="OrleansDashboard" Version="2.0.9" />

2
src/Squidex/app/features/content/pages/contents/contents-page.component.html

@ -51,7 +51,7 @@
<thead> <thead>
<tr> <tr>
<th class="cell-select"> <th class="cell-select">
<input type="checkbox" class="form-control" [ngModel]="isAllSelected" (ngModelChange)="selectAll($event)" /> <input type="checkbox" class="form-check" [ngModel]="isAllSelected" (ngModelChange)="selectAll($event)" />
</th> </th>
<th class="cell-auto" *ngFor="let field of schema.listFields"> <th class="cell-auto" *ngFor="let field of schema.listFields">
<span class="field">{{field.displayName}}</span> <span class="field">{{field.displayName}}</span>

2
src/Squidex/app/features/content/shared/content-item.component.html

@ -1,5 +1,5 @@
<td class="cell-select" *ngIf="!isReference" (click)="shouldStop($event)"> <td class="cell-select" *ngIf="!isReference" (click)="shouldStop($event)">
<input type="checkbox" class="form-control" <input type="checkbox" class="form-check"
[ngModel]="selected" [ngModel]="selected"
(ngModelChange)="selectedChange.emit($event);" (ngModelChange)="selectedChange.emit($event);"
(click)="$event.stopPropagation()" /> (click)="$event.stopPropagation()" />

2
src/Squidex/app/features/content/shared/contents-selector.component.html

@ -30,7 +30,7 @@
<thead> <thead>
<tr> <tr>
<th class="cell-select"> <th class="cell-select">
<input type="checkbox" class="form-control" [ngModel]="isAllSelected" (ngModelChange)="selectAll($event)" /> <input type="checkbox" class="form-check" [ngModel]="isAllSelected" (ngModelChange)="selectAll($event)" />
</th> </th>
<th class="cell-auto" *ngFor="let field of schema.listFields"> <th class="cell-auto" *ngFor="let field of schema.listFields">
<span class="field">{{field.displayName}}</span> <span class="field">{{field.displayName}}</span>

2
src/Squidex/app/features/content/shared/field-editor.component.html

@ -77,7 +77,7 @@
<sqx-toggle [formControl]="control"></sqx-toggle> <sqx-toggle [formControl]="control"></sqx-toggle>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'Checkbox'"> <ng-container *ngSwitchCase="'Checkbox'">
<input type="checkbox" [formControl]="control" sqxIndeterminateValue /> <input type="checkbox" [formControl]="control" class="form-check" sqxIndeterminateValue />
</ng-container> </ng-container>
</ng-container> </ng-container>
</ng-container> </ng-container>

1
src/Squidex/app/framework/angular/forms/tag-editor.component.scss

@ -30,6 +30,7 @@ $focus-color: #b3d3ff;
background: transparent; background: transparent;
min-width: 40%; min-width: 40%;
max-width: 100%; max-width: 100%;
height: auto !important;
} }
&:focus, &:focus,

89
src/Squidex/package.json

@ -16,90 +16,89 @@
"tslint": "tslint -c tslint.json -p tsconfig.json app/**/*.ts" "tslint": "tslint -c tslint.json -p tsconfig.json app/**/*.ts"
}, },
"dependencies": { "dependencies": {
"@angular/animations": "6.0.4", "@angular/animations": "6.1.7",
"@angular/common": "6.0.4", "@angular/common": "6.1.7",
"@angular/core": "6.0.4", "@angular/core": "6.1.7",
"@angular/forms": "6.0.4", "@angular/forms": "6.1.7",
"@angular/http": "6.0.4", "@angular/http": "6.1.7",
"@angular/platform-browser": "6.0.4", "@angular/platform-browser": "6.1.7",
"@angular/platform-browser-dynamic": "6.0.4", "@angular/platform-browser-dynamic": "6.1.7",
"@angular/platform-server": "6.0.4", "@angular/platform-server": "6.1.7",
"@angular/router": "6.0.4", "@angular/router": "6.1.7",
"angular2-chartjs": "0.5.1", "angular2-chartjs": "0.5.1",
"babel-polyfill": "6.26.0", "babel-polyfill": "6.26.0",
"bootstrap": "4.1.1", "bootstrap": "4.1.3",
"core-js": "2.5.7", "core-js": "2.5.7",
"graphiql": "0.11.11", "graphiql": "0.11.11",
"graphql": "0.13.2", "graphql": "14.0.2",
"moment": "2.22.2", "moment": "2.22.2",
"mousetrap": "1.6.2", "mousetrap": "1.6.2",
"ng2-dnd": "5.0.2", "ng2-dnd": "5.0.2",
"npm": "^6.2.0", "oidc-client": "1.5.2",
"oidc-client": "1.4.1",
"pikaday": "1.7.0", "pikaday": "1.7.0",
"progressbar.js": "1.0.1", "progressbar.js": "1.0.1",
"react": "16.4.0", "react": "16.5.0",
"react-dom": "16.4.0", "react-dom": "16.5.0",
"rxjs": "6.2.0", "rxjs": "6.3.2",
"slugify": "1.3.0", "slugify": "1.3.1",
"sortablejs": "1.7.0", "sortablejs": "1.7.0",
"tslib": "^1.9.3", "tslib": "^1.9.3",
"zone.js": "0.8.26" "zone.js": "0.8.26"
}, },
"devDependencies": { "devDependencies": {
"@angular/compiler": "6.0.4", "@angular/compiler": "6.1.7",
"@angular/compiler-cli": "6.0.4", "@angular/compiler-cli": "6.1.7",
"@ngtools/webpack": "6.0.8", "@ngtools/webpack": "6.2.1",
"@types/core-js": "2.5.0", "@types/core-js": "2.5.0",
"@types/jasmine": "2.8.8", "@types/jasmine": "2.8.8",
"@types/mousetrap": "1.6", "@types/mousetrap": "1.6",
"@types/node": "10.1.2", "@types/node": "10.9.4",
"@types/react": "16.3.16", "@types/react": "16.4.13",
"@types/react-dom": "16.0.6", "@types/react-dom": "16.0.7",
"@types/sortablejs": "1.3.32", "@types/sortablejs": "1.3.32",
"angular-router-loader": "0.8.5", "angular-router-loader": "0.8.5",
"angular2-template-loader": "0.6.2", "angular2-template-loader": "0.6.2",
"awesome-typescript-loader": "5.0.0", "awesome-typescript-loader": "5.2.1",
"codelyzer": "4.3.0", "codelyzer": "4.4.4",
"cpx": "1.5.0", "cpx": "1.5.0",
"css-loader": "0.28.11", "css-loader": "1.0.0",
"exports-loader": "0.7.0", "exports-loader": "0.7.0",
"file-loader": "1.1.11", "file-loader": "2.0.0",
"html-loader": "0.5.5", "html-loader": "0.5.5",
"html-webpack-plugin": "3.2.0", "html-webpack-plugin": "3.2.0",
"ignore-loader": "0.1.2", "ignore-loader": "0.1.2",
"istanbul-instrumenter-loader": "0.2.0", "istanbul-instrumenter-loader": "0.2.0",
"jasmine-core": "3.1.0", "jasmine-core": "3.2.1",
"karma": "2.0.2", "karma": "3.0.0",
"karma-chrome-launcher": "2.2.0", "karma-chrome-launcher": "2.2.0",
"karma-cli": "1.0.1", "karma-cli": "1.0.1",
"karma-coverage": "1.1.2", "karma-coverage": "1.1.2",
"karma-htmlfile-reporter": "0.3.5", "karma-htmlfile-reporter": "0.3.6",
"karma-jasmine": "1.1.2", "karma-jasmine": "1.1.2",
"karma-jasmine-html-reporter": "1.1.0", "karma-jasmine-html-reporter": "1.3.1",
"karma-mocha-reporter": "2.2.5", "karma-mocha-reporter": "2.2.5",
"karma-sourcemap-loader": "0.3.7", "karma-sourcemap-loader": "0.3.7",
"karma-webpack": "3.0.0", "karma-webpack": "3.0.4",
"mini-css-extract-plugin": "0.4.0", "mini-css-extract-plugin": "0.4.2",
"node-sass": "4.8.3", "node-sass": "4.9.3",
"noop-loader": "1.0.0", "noop-loader": "1.0.0",
"null-loader": "0.1.1", "null-loader": "0.1.1",
"raw-loader": "0.5.1", "raw-loader": "0.5.1",
"rimraf": "2.6.2", "rimraf": "2.6.2",
"rxjs-tslint": "0.1.4", "rxjs-tslint": "0.1.5",
"sass-lint": "1.12.1", "sass-lint": "1.12.1",
"sass-loader": "7.0.3", "sass-loader": "7.1.0",
"style-loader": "0.21.0", "style-loader": "0.23.0",
"tsconfig-paths-webpack-plugin": "3.1.4", "tsconfig-paths-webpack-plugin": "3.2.0",
"tslint": "5.10.0", "tslint": "5.11.0",
"tslint-loader": "3.6.0", "tslint-loader": "3.6.0",
"typemoq": "2.1.0", "typemoq": "2.1.0",
"typescript": "2.7.2", "typescript": "2.9.1",
"uglifyjs-webpack-plugin": "1.2.5", "uglifyjs-webpack-plugin": "1.3.0",
"underscore": "1.9.1", "underscore": "1.9.1",
"webpack": "4.11.1", "webpack": "4.17.2",
"webpack-cli": "3.0.3", "webpack-cli": "3.1.0",
"webpack-dev-server": "3.1.4", "webpack-dev-server": "3.1.8",
"webpack-merge": "4.1.2" "webpack-merge": "4.1.4"
} }
} }

3
tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs

@ -12,6 +12,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using Orleans; using Orleans;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Assets.State; using Squidex.Domain.Apps.Entities.Assets.State;
using Squidex.Domain.Apps.Entities.Tags; using Squidex.Domain.Apps.Entities.Tags;
@ -50,7 +51,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
asset.OnActivateAsync(Id).Wait(); asset.OnActivateAsync(Id).Wait();
A.CallTo(() => tagService.NormalizeTagsAsync(AppId, TagGroups.Assets, A<HashSet<string>>.Ignored, A<HashSet<string>>.Ignored)) A.CallTo(() => tagService.NormalizeTagsAsync(AppId, TagGroups.Assets, A<HashSet<string>>.Ignored, A<HashSet<string>>.Ignored))
.Returns(new HashSet<string>()); .Returns(new Dictionary<string, string>());
A.CallTo(() => grainFactory.GetGrain<IAssetGrain>(Id, null)) A.CallTo(() => grainFactory.GetGrain<IAssetGrain>(Id, null))
.Returns(asset); .Returns(asset);

4
tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs

@ -10,9 +10,9 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Assets.State; using Squidex.Domain.Apps.Entities.Assets.State;
using Squidex.Domain.Apps.Entities.Tags;
using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Domain.Apps.Events.Assets; using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -39,7 +39,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
public AssetGrainTests() public AssetGrainTests()
{ {
A.CallTo(() => tagService.NormalizeTagsAsync(AppId, TagGroups.Assets, A<HashSet<string>>.Ignored, A<HashSet<string>>.Ignored)) A.CallTo(() => tagService.NormalizeTagsAsync(AppId, TagGroups.Assets, A<HashSet<string>>.Ignored, A<HashSet<string>>.Ignored))
.Returns(new HashSet<string>()); .Returns(new Dictionary<string, string>());
sut = new AssetGrain(Store, tagService, A.Dummy<ISemanticLog>()); sut = new AssetGrain(Store, tagService, A.Dummy<ISemanticLog>());
sut.OnActivateAsync(Id).Wait(); sut.OnActivateAsync(Id).Wait();

36
tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetQueryServiceTests.cs

@ -11,10 +11,11 @@ using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.Tags;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Queries;
using Xunit; using Xunit;
namespace Squidex.Domain.Apps.Entities.Assets namespace Squidex.Domain.Apps.Entities.Assets
@ -78,7 +79,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
CreateAsset(id1, "id1", "id2", "id3"), CreateAsset(id1, "id1", "id2", "id3"),
CreateAsset(id2))); CreateAsset(id2)));
var result = await sut.QueryAsync(context, Query.Empty.WithIds(ids)); var result = await sut.QueryAsync(context, Q.Empty.WithIds(ids));
Assert.Equal(8, result.Total); Assert.Equal(8, result.Total);
Assert.Equal(2, result.Count); Assert.Equal(2, result.Count);
@ -90,12 +91,12 @@ namespace Squidex.Domain.Apps.Entities.Assets
[Fact] [Fact]
public async Task Should_load_assets_with_query_and_resolve_tags() public async Task Should_load_assets_with_query_and_resolve_tags()
{ {
A.CallTo(() => assetRepository.QueryAsync(appId, "my-query")) A.CallTo(() => assetRepository.QueryAsync(appId, A<Query>.Ignored))
.Returns(ResultList.Create(8, .Returns(ResultList.Create(8,
CreateAsset(Guid.NewGuid(), "id1", "id2"), CreateAsset(Guid.NewGuid(), "id1", "id2"),
CreateAsset(Guid.NewGuid(), "id2", "id3"))); CreateAsset(Guid.NewGuid(), "id2", "id3")));
var result = await sut.QueryAsync(context, Query.Empty.WithODataQuery("my-query")); var result = await sut.QueryAsync(context, Q.Empty);
Assert.Equal(8, result.Total); Assert.Equal(8, result.Total);
Assert.Equal(2, result.Count); Assert.Equal(2, result.Count);
@ -104,6 +105,33 @@ namespace Squidex.Domain.Apps.Entities.Assets
Assert.Equal(HashSet.Of("name2", "name3"), result[1].Tags); Assert.Equal(HashSet.Of("name2", "name3"), result[1].Tags);
} }
[Fact]
public async Task Should_transform_odata_query()
{
await sut.QueryAsync(context, Q.Empty.WithODataQuery("$top=100&$orderby=fileName asc&$search=Hello World"));
A.CallTo(() => assetRepository.QueryAsync(appId, A<Query>.That.Matches(x => x.ToString() == "FullText: Hello World; Take: 100; Sort: fileName Ascending")))
.MustHaveHappened();
}
[Fact]
public async Task Should_transform_odata_query_and_enrich_with_defaults()
{
await sut.QueryAsync(context, Q.Empty.WithODataQuery("$filter=fileName eq 'ABC'"));
A.CallTo(() => assetRepository.QueryAsync(appId, A<Query>.That.Matches(x => x.ToString() == "Filter: fileName == ABC; Take: 200; Sort: lastModified Descending")))
.MustHaveHappened();
}
[Fact]
public async Task Should_limit_number_of_assets()
{
await sut.QueryAsync(context, Q.Empty.WithODataQuery("$top=300&$skip=20"));
A.CallTo(() => assetRepository.QueryAsync(appId, A<Query>.That.Matches(x => x.ToString() == "Skip: 20; Take: 200; Sort: lastModified Descending")))
.MustHaveHappened();
}
private IAssetEntity CreateAsset(Guid id, params string[] tags) private IAssetEntity CreateAsset(Guid id, params string[] tags)
{ {
var asset = A.Fake<IAssetEntity>(); var asset = A.Fake<IAssetEntity>();

162
tests/Squidex.Domain.Apps.Entities.Tests/Assets/OData/ODataQueryTests.cs → tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/MongoDbQueryTests.cs

@ -6,56 +6,44 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic; using System.Linq;
using FakeItEasy; using FakeItEasy;
using Microsoft.OData.Edm;
using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Domain.Apps.Entities.Assets.Edm; using NodaTime.Text;
using Squidex.Domain.Apps.Entities.MongoDb.Assets; using Squidex.Domain.Apps.Entities.MongoDb.Assets;
using Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors; using Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors;
using Squidex.Domain.Apps.Entities.Tags;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.MongoDb.OData; using Squidex.Infrastructure.MongoDb.OData;
using Squidex.Infrastructure.Queries;
using Xunit; using Xunit;
using FilterBuilder = Squidex.Infrastructure.Queries.FilterBuilder;
using SortBuilder = Squidex.Infrastructure.Queries.SortBuilder;
namespace Squidex.Domain.Apps.Entities.Assets.OData namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
{ {
public class ODataQueryTests public class MongoDbQueryTests
{ {
private readonly ITagService tagService = A.Fake<ITagService>(); private static readonly IBsonSerializerRegistry Registry = BsonSerializer.SerializerRegistry;
private readonly IBsonSerializerRegistry registry = BsonSerializer.SerializerRegistry; private static readonly IBsonSerializer<MongoAssetEntity> Serializer = BsonSerializer.SerializerRegistry.GetSerializer<MongoAssetEntity>();
private readonly IBsonSerializer<MongoAssetEntity> serializer = BsonSerializer.SerializerRegistry.GetSerializer<MongoAssetEntity>();
private readonly IEdmModel edmModel = EdmAssetModel.Edm;
private readonly Guid appId = Guid.NewGuid(); private readonly Guid appId = Guid.NewGuid();
private readonly ConvertValue valueConverter;
static ODataQueryTests() static MongoDbQueryTests()
{ {
InstantSerializer.Register(); InstantSerializer.Register();
} }
public ODataQueryTests()
{
A.CallTo(() => tagService.GetTagIdsAsync(appId, TagGroups.Assets, A<HashSet<string>>.That.Contains("tag1")))
.Returns(HashSet.Of("normalized1"));
valueConverter = FindExtensions.CreateValueConverter(appId, tagService);
}
[Fact] [Fact]
public void Should_parse_query() public void Should_throw_exception_for_full_text_search()
{ {
var parser = edmModel.ParseQuery("$filter=lastModifiedBy eq 'Sebastian'"); Assert.Throws<ValidationException>(() => Q(new Query { FullText = "Full Text" }));
Assert.NotNull(parser);
} }
[Fact] [Fact]
public void Should_make_query_with_lastModified() public void Should_make_query_with_lastModified()
{ {
var i = F("$filter=lastModified eq 1988-01-19T12:00:00Z"); var i = F(FilterBuilder.Eq("lastModified", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value));
var o = C("{ 'LastModified' : ISODate('1988-01-19T12:00:00Z') }"); var o = C("{ 'LastModified' : ISODate('1988-01-19T12:00:00Z') }");
Assert.Equal(o, i); Assert.Equal(o, i);
@ -64,7 +52,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.OData
[Fact] [Fact]
public void Should_make_query_with_lastModifiedBy() public void Should_make_query_with_lastModifiedBy()
{ {
var i = F("$filter=lastModifiedBy eq 'Me'"); var i = F(FilterBuilder.Eq("lastModifiedBy", "Me"));
var o = C("{ 'LastModifiedBy' : 'Me' }"); var o = C("{ 'LastModifiedBy' : 'Me' }");
Assert.Equal(o, i); Assert.Equal(o, i);
@ -73,7 +61,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.OData
[Fact] [Fact]
public void Should_make_query_with_created() public void Should_make_query_with_created()
{ {
var i = F("$filter=created eq 1988-01-19T12:00:00Z"); var i = F(FilterBuilder.Eq("created", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value));
var o = C("{ 'Created' : ISODate('1988-01-19T12:00:00Z') }"); var o = C("{ 'Created' : ISODate('1988-01-19T12:00:00Z') }");
Assert.Equal(o, i); Assert.Equal(o, i);
@ -82,7 +70,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.OData
[Fact] [Fact]
public void Should_make_query_with_createdBy() public void Should_make_query_with_createdBy()
{ {
var i = F("$filter=createdBy eq 'Me'"); var i = F(FilterBuilder.Eq("createdBy", "Me"));
var o = C("{ 'CreatedBy' : 'Me' }"); var o = C("{ 'CreatedBy' : 'Me' }");
Assert.Equal(o, i); Assert.Equal(o, i);
@ -91,17 +79,17 @@ namespace Squidex.Domain.Apps.Entities.Assets.OData
[Fact] [Fact]
public void Should_make_query_with_version() public void Should_make_query_with_version()
{ {
var i = F("$filter=version eq 0"); var i = F(FilterBuilder.Eq("version", 0));
var o = C("{ 'Version' : NumberLong(0) }"); var o = C("{ 'Version' : NumberLong(0) }");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
[Fact] [Fact]
public void Should_make_query_with_normalized_tags() public void Should_make_query_with_fileVersion()
{ {
var i = F("$filter=tags eq 'tag1'"); var i = F(FilterBuilder.Eq("fileVersion", 2));
var o = C("{ 'Tags' : 'normalized1' }"); var o = C("{ 'FileVersion' : NumberLong(2) }");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -109,8 +97,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.OData
[Fact] [Fact]
public void Should_make_query_with_tags() public void Should_make_query_with_tags()
{ {
var i = F("$filter=tags eq 'tag2'"); var i = F(FilterBuilder.Eq("tags", "tag1"));
var o = C("{ 'Tags' : 'tag2' }"); var o = C("{ 'Tags' : 'tag1' }");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -118,44 +106,35 @@ namespace Squidex.Domain.Apps.Entities.Assets.OData
[Fact] [Fact]
public void Should_make_query_with_fileName() public void Should_make_query_with_fileName()
{ {
var i = F("$filter=fileName eq 'Logo.png'"); var i = F(FilterBuilder.Eq("fileName", "Logo.png"));
var o = C("{ 'FileName' : 'Logo.png' }"); var o = C("{ 'FileName' : 'Logo.png' }");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
[Fact] [Fact]
public void Should_make_query_with_fileSize() public void Should_make_query_with_isImage()
{
var i = F("$filter=fileSize eq 1024");
var o = C("{ 'FileSize' : NumberLong(1024) }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_fileVersion()
{ {
var i = F("$filter=fileVersion eq 2"); var i = F(FilterBuilder.Eq("isImage", true));
var o = C("{ 'FileVersion' : NumberLong(2) }"); var o = C("{ 'IsImage' : true }");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
[Fact] [Fact]
public void Should_make_query_with_isImage() public void Should_make_query_with_mimeType()
{ {
var i = F("$filter=isImage eq true"); var i = F(FilterBuilder.Eq("mimeType", "text/json"));
var o = C("{ 'IsImage' : true }"); var o = C("{ 'MimeType' : 'text/json' }");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
[Fact] [Fact]
public void Should_make_query_with_mimeType() public void Should_make_query_with_fileSize()
{ {
var i = F("$filter=mimeType eq 'text/json'"); var i = F(FilterBuilder.Eq("fileSize", 1024));
var o = C("{ 'MimeType' : 'text/json' }"); var o = C("{ 'FileSize' : NumberLong(1024) }");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -163,7 +142,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.OData
[Fact] [Fact]
public void Should_make_query_with_pixelHeight() public void Should_make_query_with_pixelHeight()
{ {
var i = F("$filter=pixelHeight eq 600"); var i = F(FilterBuilder.Eq("pixelHeight", 600));
var o = C("{ 'PixelHeight' : 600 }"); var o = C("{ 'PixelHeight' : 600 }");
Assert.Equal(o, i); Assert.Equal(o, i);
@ -172,8 +151,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.OData
[Fact] [Fact]
public void Should_make_query_with_pixelWidth() public void Should_make_query_with_pixelWidth()
{ {
var i = F("$filter=pixelWidth eq 600"); var i = F(FilterBuilder.Eq("pixelWidth", 800));
var o = C("{ 'PixelWidth' : 600 }"); var o = C("{ 'PixelWidth' : 800 }");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
@ -181,84 +160,55 @@ namespace Squidex.Domain.Apps.Entities.Assets.OData
[Fact] [Fact]
public void Should_make_orderby_with_single_field() public void Should_make_orderby_with_single_field()
{ {
var i = S("$orderby=lastModified desc"); var i = S(SortBuilder.Descending("lastModified"));
var o = C("{ 'LastModified' : -1 }"); var o = C("{ 'LastModified' : -1 }");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
[Fact] [Fact]
public void Should_make_orderby_with_multiple_field() public void Should_make_orderby_with_multiple_fields()
{ {
var i = S("$orderby=lastModified, lastModifiedBy desc"); var i = S(SortBuilder.Ascending("lastModified"), SortBuilder.Descending("lastModifiedBy"));
var o = C("{ 'LastModified' : 1, 'LastModifiedBy' : -1 }"); var o = C("{ 'LastModified' : 1, 'LastModifiedBy' : -1 }");
Assert.Equal(o, i); Assert.Equal(o, i);
} }
[Fact] [Fact]
public void Should_make_top_statement() public void Should_make_take_statement()
{ {
var parser = edmModel.ParseQuery("$top=3"); var query = new Query { Take = 3 };
var cursor = A.Fake<IFindFluent<MongoAssetEntity, MongoAssetEntity>>(); var cursor = A.Fake<IFindFluent<MongoAssetEntity, MongoAssetEntity>>();
cursor.AssetTake(parser); cursor.AssetTake(query.AdjustToModel());
A.CallTo(() => cursor.Limit(3)).MustHaveHappened(); A.CallTo(() => cursor.Limit(3)).MustHaveHappened();
} }
[Fact]
public void Should_make_top_statement_with_limit()
{
var parser = edmModel.ParseQuery("$top=300");
var cursor = A.Fake<IFindFluent<MongoAssetEntity, MongoAssetEntity>>();
cursor.AssetTake(parser);
A.CallTo(() => cursor.Limit(200)).MustHaveHappened();
}
[Fact]
public void Should_make_top_statement_with_default_value()
{
var parser = edmModel.ParseQuery(string.Empty);
var cursor = A.Fake<IFindFluent<MongoAssetEntity, MongoAssetEntity>>();
cursor.AssetTake(parser);
A.CallTo(() => cursor.Limit(20)).MustHaveHappened();
}
[Fact] [Fact]
public void Should_make_skip_statement() public void Should_make_skip_statement()
{ {
var parser = edmModel.ParseQuery("$skip=3"); var query = new Query { Skip = 3 };
var cursor = A.Fake<IFindFluent<MongoAssetEntity, MongoAssetEntity>>(); var cursor = A.Fake<IFindFluent<MongoAssetEntity, MongoAssetEntity>>();
cursor.AssetSkip(parser); cursor.AssetSkip(query.AdjustToModel());
A.CallTo(() => cursor.Skip(3)).MustHaveHappened(); A.CallTo(() => cursor.Skip(3)).MustHaveHappened();
} }
[Fact] private static string C(string value)
public void Should_make_skip_statement_with_default_value()
{ {
var parser = edmModel.ParseQuery(string.Empty); return value.Replace('\'', '"');
var cursor = A.Fake<IFindFluent<MongoAssetEntity, MongoAssetEntity>>();
cursor.AssetSkip(parser);
A.CallTo(() => cursor.Skip(A<int>.Ignored)).MustNotHaveHappened();
} }
private static string C(string value) private string F(FilterNode filter)
{ {
return value.Replace('\'', '"'); return Q(new Query { Filter = filter });
} }
private string S(string value) private string S(params SortNode[] sorts)
{ {
var parser = edmModel.ParseQuery(value);
var cursor = A.Fake<IFindFluent<MongoAssetEntity, MongoAssetEntity>>(); var cursor = A.Fake<IFindFluent<MongoAssetEntity, MongoAssetEntity>>();
var i = string.Empty; var i = string.Empty;
@ -266,23 +216,21 @@ namespace Squidex.Domain.Apps.Entities.Assets.OData
A.CallTo(() => cursor.Sort(A<SortDefinition<MongoAssetEntity>>.Ignored)) A.CallTo(() => cursor.Sort(A<SortDefinition<MongoAssetEntity>>.Ignored))
.Invokes((SortDefinition<MongoAssetEntity> sortDefinition) => .Invokes((SortDefinition<MongoAssetEntity> sortDefinition) =>
{ {
i = sortDefinition.Render(serializer, registry).ToString(); i = sortDefinition.Render(Serializer, Registry).ToString();
}); });
cursor.AssetSort(parser); cursor.AssetSort(new Query { Sort = sorts.ToList() }.AdjustToModel());
return i; return i;
} }
private string F(string value) private string Q(Query query)
{ {
var parser = edmModel.ParseQuery(value); var rendered =
query.AdjustToModel().BuildFilter<MongoAssetEntity>(false).Filter
var query = .Render(Serializer, Registry).ToString();
parser.BuildFilter<MongoAssetEntity>(convertValue: valueConverter)
.Filter.Render(serializer, registry).ToString();
return query; return rendered;
} }
} }
} }

58
tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/QueryTagVisitorTests.cs

@ -0,0 +1,58 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using FakeItEasy;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Infrastructure.Queries;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Assets.Queries
{
public class QueryTagVisitorTests
{
private readonly ITagService tagService = A.Fake<ITagService>();
private readonly Guid appId = Guid.NewGuid();
[Fact]
public void Should_normalize_tags()
{
A.CallTo(() => tagService.GetTagIdsAsync(appId, TagGroups.Assets, A<HashSet<string>>.That.Contains("tag1")))
.Returns(new Dictionary<string, string> { ["tag1"] = "normalized1" });
var source = FilterBuilder.Eq("tags", "tag1");
var result = QueryTagVisitor.Transform(source, appId, tagService);
Assert.Equal("tags == normalized1", result.ToString());
}
[Fact]
public void Should_not_not_fail_when_not_found()
{
A.CallTo(() => tagService.GetTagIdsAsync(appId, TagGroups.Assets, A<HashSet<string>>.That.Contains("tag1")))
.Returns(new Dictionary<string, string>());
var source = FilterBuilder.Eq("tags", "tag1");
var result = QueryTagVisitor.Transform(source, appId, tagService);
Assert.Equal("tags == tag1", result.ToString());
}
[Fact]
public void Should_not_normalize_other_field()
{
var source = FilterBuilder.Eq("other", "value");
var result = QueryTagVisitor.Transform(source, appId, tagService);
Assert.Equal("other == value", result.ToString());
A.CallTo(() => tagService.GetTagIdsAsync(appId, A<string>.Ignored, A<HashSet<string>>.Ignored))
.MustNotHaveHappened();
}
}
}

10
tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs

@ -12,7 +12,6 @@ using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using Microsoft.OData; using Microsoft.OData;
using Microsoft.OData.UriParser;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
@ -22,6 +21,7 @@ using Squidex.Domain.Apps.Entities.Contents.Edm;
using Squidex.Domain.Apps.Entities.Contents.Repositories; using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
using Xunit; using Xunit;
@ -194,12 +194,12 @@ namespace Squidex.Domain.Apps.Entities.Contents
A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false)) A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false))
.Returns(schema); .Returns(schema);
A.CallTo(() => contentRepository.QueryAsync(app, schema, A<Status[]>.That.IsSameSequenceAs(status), A<ODataUriParser>.Ignored)) A.CallTo(() => contentRepository.QueryAsync(app, schema, A<Status[]>.That.IsSameSequenceAs(status), A<Query>.Ignored))
.Returns(ResultList.Create(total, Enumerable.Repeat(content, count))); .Returns(ResultList.Create(total, Enumerable.Repeat(content, count)));
var ctx = context.WithSchemaId(schemaId).WithArchived(archive).WithUnpublished(unpublished); var ctx = context.WithSchemaId(schemaId).WithArchived(archive).WithUnpublished(unpublished);
var result = await sut.QueryAsync(ctx, Query.Empty); var result = await sut.QueryAsync(ctx, Q.Empty);
Assert.Equal(contentData, result[0].Data); Assert.Equal(contentData, result[0].Data);
Assert.Equal(content.Id, result[0].Id); Assert.Equal(content.Id, result[0].Id);
@ -227,7 +227,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
A.CallTo(() => modelBuilder.BuildEdmModel(schema, app)) A.CallTo(() => modelBuilder.BuildEdmModel(schema, app))
.Throws(new ODataException()); .Throws(new ODataException());
return Assert.ThrowsAsync<ValidationException>(() => sut.QueryAsync(context.WithSchemaId(schemaId), Query.Empty.WithODataQuery("query"))); return Assert.ThrowsAsync<ValidationException>(() => sut.QueryAsync(context.WithSchemaId(schemaId), Q.Empty.WithODataQuery("query")));
} }
public static IEnumerable<object[]> ManyIdRequestData = new[] public static IEnumerable<object[]> ManyIdRequestData = new[]
@ -261,7 +261,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var ctx = context.WithSchemaId(schemaId).WithArchived(archive).WithUnpublished(unpublished); var ctx = context.WithSchemaId(schemaId).WithArchived(archive).WithUnpublished(unpublished);
var result = await sut.QueryAsync(ctx, Query.Empty.WithIds(ids)); var result = await sut.QueryAsync(ctx, Q.Empty.WithIds(ids));
Assert.Equal(ids, result.Select(x => x.Id).ToList()); Assert.Equal(ids, result.Select(x => x.Id).ToList());
Assert.Equal(total, result.Total); Assert.Equal(total, result.Total);

12
tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs

@ -62,7 +62,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var asset = CreateAsset(Guid.NewGuid()); var asset = CreateAsset(Guid.NewGuid());
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A<Query>.That.Matches(x => x.ODataQuery == "?$take=30&$skip=5&$search=my-query"))) A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A<Q>.That.Matches(x => x.ODataQuery == "?$take=30&$skip=5&$search=my-query")))
.Returns(ResultList.Create(0, asset)); .Returns(ResultList.Create(0, asset));
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query }); var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });
@ -129,7 +129,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var asset = CreateAsset(Guid.NewGuid()); var asset = CreateAsset(Guid.NewGuid());
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A<Query>.That.Matches(x => x.ODataQuery == "?$take=30&$skip=5&$search=my-query"))) A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A<Q>.That.Matches(x => x.ODataQuery == "?$take=30&$skip=5&$search=my-query")))
.Returns(ResultList.Create(10, asset)); .Returns(ResultList.Create(10, asset));
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query }); var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });
@ -280,7 +280,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var content = CreateContent(Guid.NewGuid(), Guid.Empty, Guid.Empty); var content = CreateContent(Guid.NewGuid(), Guid.Empty, Guid.Empty);
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<Query>.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5"))) A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<Q>.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5")))
.Returns(ResultList.Create(0, content)); .Returns(ResultList.Create(0, content));
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query }); var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });
@ -412,7 +412,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var content = CreateContent(Guid.NewGuid(), Guid.Empty, Guid.Empty); var content = CreateContent(Guid.NewGuid(), Guid.Empty, Guid.Empty);
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<Query>.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5"))) A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<Q>.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5")))
.Returns(ResultList.Create(10, content)); .Returns(ResultList.Create(10, content));
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query }); var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });
@ -624,7 +624,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), contentId, EtagVersion.Any)) A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), contentId, EtagVersion.Any))
.Returns(content); .Returns(content);
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<Query>.Ignored)) A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<Q>.Ignored))
.Returns(ResultList.Create(0, contentRef)); .Returns(ResultList.Create(0, contentRef));
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query }); var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });
@ -682,7 +682,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), contentId, EtagVersion.Any)) A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), contentId, EtagVersion.Any))
.Returns(content); .Returns(content);
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A<Query>.Ignored)) A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A<Q>.Ignored))
.Returns(ResultList.Create(0, assetRef)); .Returns(ResultList.Create(0, assetRef));
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query }); var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });

267
tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs

@ -0,0 +1,267 @@
// ==========================================================================
// 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 FakeItEasy;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Microsoft.OData.Edm;
using MongoDB.Bson.Serialization;
using MongoDB.Driver;
using NodaTime.Text;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Contents.Edm;
using Squidex.Domain.Apps.Entities.MongoDb.Contents;
using Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.MongoDb.OData;
using Squidex.Infrastructure.Queries;
using Xunit;
using FilterBuilder = Squidex.Infrastructure.Queries.FilterBuilder;
using SortBuilder = Squidex.Infrastructure.Queries.SortBuilder;
namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
{
public class MongoDbQueryTests
{
private static readonly IBsonSerializerRegistry Registry = BsonSerializer.SerializerRegistry;
private static readonly IBsonSerializer<MongoContentEntity> Serializer = BsonSerializer.SerializerRegistry.GetSerializer<MongoContentEntity>();
private readonly Schema schemaDef;
private readonly IEdmModel edmModel;
private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.EN, Language.DE);
static MongoDbQueryTests()
{
InstantSerializer.Register();
}
public MongoDbQueryTests()
{
schemaDef =
new Schema("user")
.AddString(1, "firstName", Partitioning.Language,
new StringFieldProperties { Label = "FirstName", IsRequired = true, AllowedValues = ImmutableList.Create("1", "2") })
.AddString(2, "lastName", Partitioning.Language,
new StringFieldProperties { Hints = "Last Name", Editor = StringFieldEditor.Input })
.AddBoolean(3, "isAdmin", Partitioning.Invariant,
new BooleanFieldProperties())
.AddNumber(4, "age", Partitioning.Invariant,
new NumberFieldProperties { MinValue = 1, MaxValue = 10 })
.AddDateTime(5, "birthday", Partitioning.Invariant,
new DateTimeFieldProperties())
.AddAssets(6, "pictures", Partitioning.Invariant,
new AssetsFieldProperties())
.AddReferences(7, "friends", Partitioning.Invariant,
new ReferencesFieldProperties())
.AddString(8, "dashed-field", Partitioning.Invariant,
new StringFieldProperties())
.Update(new SchemaProperties { Hints = "The User" });
var builder = new EdmModelBuilder(new MemoryCache(Options.Create(new MemoryCacheOptions())));
var schema = A.Dummy<ISchemaEntity>();
A.CallTo(() => schema.Id).Returns(Guid.NewGuid());
A.CallTo(() => schema.Version).Returns(3);
A.CallTo(() => schema.SchemaDef).Returns(schemaDef);
var app = A.Dummy<IAppEntity>();
A.CallTo(() => app.Id).Returns(Guid.NewGuid());
A.CallTo(() => app.Version).Returns(3);
A.CallTo(() => app.LanguagesConfig).Returns(languagesConfig);
edmModel = builder.BuildEdmModel(schema, app);
}
[Fact]
public void Should_throw_exception_for_invalid_field()
{
Assert.Throws<NotSupportedException>(() => F(FilterBuilder.Eq("data/invalid/iv", "Me")));
}
[Fact]
public void Should_make_query_with_lastModified()
{
var i = F(FilterBuilder.Eq("lastModified", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value));
var o = C("{ 'mt' : '1988-01-19T12:00:00Z' }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_lastModifiedBy()
{
var i = F(FilterBuilder.Eq("lastModifiedBy", "Me"));
var o = C("{ 'mb' : 'Me' }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_created()
{
var i = F(FilterBuilder.Eq("created", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value));
var o = C("{ 'ct' : '1988-01-19T12:00:00Z' }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_createdBy()
{
var i = F(FilterBuilder.Eq("createdBy", "Me"));
var o = C("{ 'cb' : 'Me' }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_version()
{
var i = F(FilterBuilder.Eq("version", 0L));
var o = C("{ 'vs' : NumberLong(0) }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_from_draft()
{
var i = F(FilterBuilder.Eq("data/dashed_field/iv", "Value"), true);
var o = C("{ 'dd.8.iv' : 'Value' }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_date_field_created()
{
var i = F(FilterBuilder.Eq("data/birthday/iv", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value));
var o = C("{ 'do.5.iv' : '1988-01-19T12:00:00Z' }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_underscore_field()
{
var i = F(FilterBuilder.Eq("data/dashed_field/iv", "Value"));
var o = C("{ 'do.8.iv' : 'Value' }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_references_equals()
{
var i = F(FilterBuilder.Eq("data/friends/iv", "guid"));
var o = C("{ 'do.7.iv' : 'guid' }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_assets_equals()
{
var i = F(FilterBuilder.Eq("data/pictures/iv", "guid"));
var o = C("{ 'do.6.iv' : 'guid' }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_full_text()
{
var i = Q(new Query { FullText = "Hello my World" });
var o = C("{ '$text' : { '$search' : 'Hello my World' } }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_orderby_with_single_field()
{
var i = S(SortBuilder.Descending("data/age/iv"));
var o = C("{ 'do.4.iv' : -1 }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_orderby_with_multiple_fields()
{
var i = S(SortBuilder.Ascending("data/age/iv"), SortBuilder.Descending("data/firstName/en"));
var o = C("{ 'do.4.iv' : 1, 'do.1.en' : -1 }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_take_statement()
{
var query = new Query { Take = 3 };
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>();
cursor.ContentTake(query.AdjustToModel(schemaDef, false));
A.CallTo(() => cursor.Limit(3)).MustHaveHappened();
}
[Fact]
public void Should_make_skip_statement()
{
var query = new Query { Skip = 3 };
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>();
cursor.ContentSkip(query.AdjustToModel(schemaDef, false));
A.CallTo(() => cursor.Skip(3)).MustHaveHappened();
}
private static string C(string value)
{
return value.Replace('\'', '"');
}
private string F(FilterNode filter, bool useDraft = false)
{
return Q(new Query { Filter = filter }, useDraft);
}
private string S(params SortNode[] sorts)
{
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>();
var i = string.Empty;
A.CallTo(() => cursor.Sort(A<SortDefinition<MongoContentEntity>>.Ignored))
.Invokes((SortDefinition<MongoContentEntity> sortDefinition) =>
{
i = sortDefinition.Render(Serializer, Registry).ToString();
});
cursor.ContentSort(new Query { Sort = sorts.ToList() }.AdjustToModel(schemaDef, false));
return i;
}
private string Q(Query query, bool useDraft = false)
{
var rendered =
query.AdjustToModel(schemaDef, useDraft).BuildFilter<MongoContentEntity>().Filter
.Render(Serializer, Registry).ToString();
return rendered;
}
}
}

448
tests/Squidex.Domain.Apps.Entities.Tests/Contents/OData/ODataQueryTests.cs

@ -1,448 +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 FakeItEasy;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Microsoft.OData.Edm;
using MongoDB.Bson.Serialization;
using MongoDB.Driver;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Contents.Edm;
using Squidex.Domain.Apps.Entities.MongoDb.Contents;
using Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.MongoDb.OData;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Contents.OData
{
public class ODataQueryTests
{
private static readonly IBsonSerializerRegistry Registry = BsonSerializer.SerializerRegistry;
private static readonly IBsonSerializer<MongoContentEntity> Serializer = BsonSerializer.SerializerRegistry.GetSerializer<MongoContentEntity>();
private readonly Schema schemaDef;
private readonly IEdmModel edmModel;
private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.EN, Language.DE);
static ODataQueryTests()
{
InstantSerializer.Register();
}
public ODataQueryTests()
{
schemaDef =
new Schema("user")
.AddString(1, "firstName", Partitioning.Language,
new StringFieldProperties { Label = "FirstName", IsRequired = true, AllowedValues = ImmutableList.Create("1", "2") })
.AddString(2, "lastName", Partitioning.Language,
new StringFieldProperties { Hints = "Last Name", Editor = StringFieldEditor.Input })
.AddBoolean(3, "isAdmin", Partitioning.Invariant,
new BooleanFieldProperties())
.AddNumber(4, "age", Partitioning.Invariant,
new NumberFieldProperties { MinValue = 1, MaxValue = 10 })
.AddDateTime(5, "birthday", Partitioning.Invariant,
new DateTimeFieldProperties())
.AddAssets(6, "pictures", Partitioning.Invariant,
new AssetsFieldProperties())
.AddReferences(7, "friends", Partitioning.Invariant,
new ReferencesFieldProperties())
.AddString(8, "dashed-field", Partitioning.Invariant,
new StringFieldProperties())
.Update(new SchemaProperties { Hints = "The User" });
var builder = new EdmModelBuilder(new MemoryCache(Options.Create(new MemoryCacheOptions())));
var schema = A.Dummy<ISchemaEntity>();
A.CallTo(() => schema.Id).Returns(Guid.NewGuid());
A.CallTo(() => schema.Version).Returns(3);
A.CallTo(() => schema.SchemaDef).Returns(schemaDef);
var app = A.Dummy<IAppEntity>();
A.CallTo(() => app.Id).Returns(Guid.NewGuid());
A.CallTo(() => app.Version).Returns(3);
A.CallTo(() => app.LanguagesConfig).Returns(languagesConfig);
edmModel = builder.BuildEdmModel(schema, app);
}
[Fact]
public void Should_parse_query()
{
var parser = edmModel.ParseQuery("$filter=data/firstName/de eq 'Sebastian'");
Assert.NotNull(parser);
}
[Fact]
public void Should_make_query_with_created()
{
var i = F("$filter=created eq 1988-01-19T12:00:00Z");
var o = C("{ 'ct' : ISODate('1988-01-19T12:00:00Z') }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_created_and_date()
{
var i = F("$filter=created eq 1988-01-19");
var o = C("{ 'ct' : ISODate('1988-01-19T00:00:00Z') }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_createdBy()
{
var i = F("$filter=createdBy eq 'Me'");
var o = C("{ 'cb' : 'Me' }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_lastModified()
{
var i = F("$filter=lastModified eq 1988-01-19T12:00:00Z");
var o = C("{ 'mt' : ISODate('1988-01-19T12:00:00Z') }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_lastModifiedBy()
{
var i = F("$filter=lastModifiedBy eq 'Me'");
var o = C("{ 'mb' : 'Me' }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_version()
{
var i = F("$filter=version eq 0");
var o = C("{ 'vs' : 0 }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_underscore_field()
{
var i = F("$filter=data/dashed_field/iv eq 'Value'");
var o = C("{ 'do.8.iv' : 'Value' }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_not()
{
var i = F("$filter=not endswith(data/firstName/de, 'Sebastian')");
var o = C("{ 'do.1.de' : { '$not' : /Sebastian$/i } }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_startswith()
{
var i = F("$filter=startswith(data/firstName/de, 'Sebastian')");
var o = C("{ 'do.1.de' : /^Sebastian/i }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_endswith()
{
var i = F("$filter=endswith(data/firstName/de, 'Sebastian')");
var o = C("{ 'do.1.de' : /Sebastian$/i }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_cointains()
{
var i = F("$filter=contains(data/firstName/de, 'Sebastian')");
var o = C("{ 'do.1.de' : /Sebastian/i }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_equals()
{
var i = F("$filter=contains(data/firstName/de, 'Sebastian') eq true");
var o = C("{ 'do.1.de' : /Sebastian/i }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_wih_equals_to_false()
{
var i = F("$filter=contains(data/firstName/de, 'Sebastian') eq false");
var o = C("{ 'do.1.de' : { '$not' : /Sebastian/i } }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_conjunction_and_contains()
{
var i = F("$filter=contains(data/firstName/de, 'Sebastian') eq false and data/isAdmin/iv eq true");
var o = C("{ 'do.1.de' : { '$not' : /Sebastian/i }, 'do.3.iv' : true }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_string_equals()
{
var i = F("$filter=data/firstName/de eq 'Sebastian'");
var o = C("{ 'do.1.de' : 'Sebastian' }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_datetime_equals()
{
var i = F("$filter=data/birthday/iv eq 1988-01-19T12:00:00Z");
var o = C("{ 'do.5.iv' : '1988-01-19T12:00:00Z' }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_boolean_equals()
{
var i = F("$filter=data/isAdmin/iv eq true");
var o = C("{ 'do.3.iv' : true }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_notequals()
{
var i = F("$filter=data/firstName/de ne 'Sebastian'");
var o = C("{ '$or' : [{ 'do.1.de' : { '$exists' : false } }, { 'do.1.de' : { '$ne' : 'Sebastian' } }] }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_lessthan()
{
var i = F("$filter=data/age/iv lt 1");
var o = C("{ 'do.4.iv' : { '$lt' : 1.0 } }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_lessequals()
{
var i = F("$filter=data/age/iv le 1");
var o = C("{ 'do.4.iv' : { '$lte' : 1.0 } }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_greaterthan()
{
var i = F("$filter=data/age/iv gt 1");
var o = C("{ 'do.4.iv' : { '$gt' : 1.0 } }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_greaterequals()
{
var i = F("$filter=data/age/iv ge 1");
var o = C("{ 'do.4.iv' : { '$gte' : 1.0 } }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_references_equals()
{
var i = F("$filter=data/pictures/iv eq 'guid'");
var o = C("{ 'do.6.iv' : 'guid' }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_assets_equals()
{
var i = F("$filter=data/friends/iv eq 'guid'");
var o = C("{ 'do.7.iv' : 'guid' }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_conjunction()
{
var i = F("$filter=data/age/iv eq 1 and data/age/iv eq 2");
var o = C("{ '$and' : [{ 'do.4.iv' : 1.0 }, { 'do.4.iv' : 2.0 }] }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_disjunction()
{
var i = F("$filter=data/age/iv eq 1 or data/age/iv eq 2");
var o = C("{ '$or' : [{ 'do.4.iv' : 1.0 }, { 'do.4.iv' : 2.0 }] }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_full_text()
{
var i = F("$search=Hello my World");
var o = C("{ '$text' : { '$search' : 'Hello my World' } }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_query_with_full_text_and_multiple_terms()
{
var i = F("$search=A and B");
var o = C("{ '$text' : { '$search' : 'A and B' } }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_orderby_with_single_field()
{
var i = S("$orderby=data/age/iv desc");
var o = C("{ 'do.4.iv' : -1 }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_orderby_with_multiple_field()
{
var i = S("$orderby=data/age/iv, data/firstName/en desc");
var o = C("{ 'do.4.iv' : 1, 'do.1.en' : -1 }");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_top_statement()
{
var parser = edmModel.ParseQuery("$top=3");
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>();
cursor.ContentTake(parser);
A.CallTo(() => cursor.Limit(3)).MustHaveHappened();
}
[Fact]
public void Should_make_top_statement_with_limit()
{
var parser = edmModel.ParseQuery("$top=300");
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>();
cursor.ContentTake(parser);
A.CallTo(() => cursor.Limit(200)).MustHaveHappened();
}
[Fact]
public void Should_make_top_statement_with_default_value()
{
var parser = edmModel.ParseQuery(string.Empty);
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>();
cursor.ContentTake(parser);
A.CallTo(() => cursor.Limit(20)).MustHaveHappened();
}
[Fact]
public void Should_make_skip_statement()
{
var parser = edmModel.ParseQuery("$skip=3");
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>();
cursor.ContentSkip(parser);
A.CallTo(() => cursor.Skip(3)).MustHaveHappened();
}
[Fact]
public void Should_make_skip_statement_with_default_value()
{
var parser = edmModel.ParseQuery(string.Empty);
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>();
cursor.ContentSkip(parser);
A.CallTo(() => cursor.Skip(A<int>.Ignored)).MustNotHaveHappened();
}
private static string C(string value)
{
return value.Replace('\'', '"');
}
private string S(string value)
{
var parser = edmModel.ParseQuery(value);
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>();
var i = string.Empty;
A.CallTo(() => cursor.Sort(A<SortDefinition<MongoContentEntity>>.Ignored))
.Invokes((SortDefinition<MongoContentEntity> sortDefinition) =>
{
i = sortDefinition.Render(Serializer, Registry).ToString();
});
cursor.ContentSort(parser, FindExtensions.CreatePropertyCalculator(schemaDef, false));
return i;
}
private string F(string value)
{
var parser = edmModel.ParseQuery(value);
var query =
parser.BuildFilter<MongoContentEntity>(FindExtensions.CreatePropertyCalculator(schemaDef, false), FindExtensions.ValueConverter)
.Filter.Render(Serializer, Registry).ToString();
return query;
}
}
}

1
tests/Squidex.Domain.Apps.Entities.Tests/Tags/GrainTagServiceTests.cs

@ -10,6 +10,7 @@ using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using Orleans; using Orleans;
using Squidex.Domain.Apps.Core.Tags;
using Xunit; using Xunit;
namespace Squidex.Domain.Apps.Entities.Tags namespace Squidex.Domain.Apps.Entities.Tags

5
tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagGrainTests.cs

@ -9,6 +9,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.States; using Squidex.Infrastructure.States;
using Xunit; using Xunit;
@ -89,7 +90,7 @@ namespace Squidex.Domain.Apps.Entities.Tags
public async Task Should_not_add_tags_if_already_added() public async Task Should_not_add_tags_if_already_added()
{ {
var result1 = await sut.NormalizeTagsAsync(HashSet.Of("tag1", "tag2"), null); var result1 = await sut.NormalizeTagsAsync(HashSet.Of("tag1", "tag2"), null);
var result2 = await sut.NormalizeTagsAsync(HashSet.Of("tag1", "tag2", "tag3"), result1); var result2 = await sut.NormalizeTagsAsync(HashSet.Of("tag1", "tag2", "tag3"), new HashSet<string>(result1.Values));
var allTags = await sut.GetTagsAsync(); var allTags = await sut.GetTagsAsync();
@ -107,7 +108,7 @@ namespace Squidex.Domain.Apps.Entities.Tags
var result1 = await sut.NormalizeTagsAsync(HashSet.Of("tag1", "tag2"), null); var result1 = await sut.NormalizeTagsAsync(HashSet.Of("tag1", "tag2"), null);
var result2 = await sut.NormalizeTagsAsync(HashSet.Of("tag2", "tag3"), null); var result2 = await sut.NormalizeTagsAsync(HashSet.Of("tag2", "tag3"), null);
await sut.NormalizeTagsAsync(null, result1); await sut.NormalizeTagsAsync(null, new HashSet<string>(result1.Values));
var allTags = await sut.GetTagsAsync(); var allTags = await sut.GetTagsAsync();

6
tests/Squidex.Infrastructure.Tests/Caching/LRUCacheTests.cs

@ -29,18 +29,18 @@ namespace Squidex.Infrastructure.Caching
[Fact] [Fact]
public void Should_remove_old_items_when_capacity_reached() public void Should_remove_old_items_when_capacity_reached()
{ {
for (int i = 0; i < 15; i++) for (var i = 0; i < 15; i++)
{ {
sut.Set(i.ToString(), i); sut.Set(i.ToString(), i);
} }
for (int i = 0; i < 5; i++) for (var i = 0; i < 5; i++)
{ {
Assert.False(sut.TryGetValue(i.ToString(), out var value)); Assert.False(sut.TryGetValue(i.ToString(), out var value));
Assert.Null(value); Assert.Null(value);
} }
for (int i = 5; i < 15; i++) for (var i = 5; i < 15; i++)
{ {
Assert.True(sut.TryGetValue(i.ToString(), out var value)); Assert.True(sut.TryGetValue(i.ToString(), out var value));
Assert.NotNull(value); Assert.NotNull(value);

1
tests/Squidex.Infrastructure.Tests/Orleans/BootstrapTests.cs

@ -12,7 +12,6 @@ using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using Orleans; using Orleans;
using Orleans.Runtime; using Orleans.Runtime;
using Squidex.Infrastructure.Tasks;
using Xunit; using Xunit;
namespace Squidex.Infrastructure.Orleans namespace Squidex.Infrastructure.Orleans

316
tests/Squidex.Infrastructure.Tests/Queries/ODataConversionTests.cs

@ -0,0 +1,316 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.OData.Edm;
using Squidex.Infrastructure.Queries.OData;
using Xunit;
namespace Squidex.Infrastructure.Queries
{
public class ODataConversionTests
{
private static readonly IEdmModel EdmModel;
static ODataConversionTests()
{
var entityType = new EdmEntityType("Squidex", "Users");
entityType.AddStructuralProperty("id", EdmPrimitiveTypeKind.Guid);
entityType.AddStructuralProperty("created", EdmPrimitiveTypeKind.DateTimeOffset);
entityType.AddStructuralProperty("isComicFigure", EdmPrimitiveTypeKind.Boolean);
entityType.AddStructuralProperty("firstName", EdmPrimitiveTypeKind.String);
entityType.AddStructuralProperty("lastName", EdmPrimitiveTypeKind.String);
entityType.AddStructuralProperty("birthday", EdmPrimitiveTypeKind.Date);
entityType.AddStructuralProperty("incomeCents", EdmPrimitiveTypeKind.Int64);
entityType.AddStructuralProperty("incomeMio", EdmPrimitiveTypeKind.Double);
entityType.AddStructuralProperty("age", EdmPrimitiveTypeKind.Int32);
var container = new EdmEntityContainer("Squidex", "Container");
container.AddEntitySet("UserSet", entityType);
var model = new EdmModel();
model.AddElement(container);
model.AddElement(entityType);
EdmModel = model;
}
[Fact]
public void Should_parse_query()
{
var parser = EdmModel.ParseQuery("$filter=firstName eq 'Dagobert'");
Assert.NotNull(parser);
}
[Fact]
public void Should_parse_filter_when_type_is_datetime()
{
var i = Q("$filter=created eq 1988-01-19T12:00:00Z");
var o = C("Filter: created == 1988-01-19T12:00:00Z");
Assert.Equal(o, i);
}
[Fact]
public void Should_parse_filter_when_type_is_date()
{
var i = Q("$filter=created eq 1988-01-19");
var o = C("Filter: created == 1988-01-19T00:00:00Z");
Assert.Equal(o, i);
}
[Fact]
public void Should_parse_filter_when_type_is_guid()
{
var i = Q("$filter=id eq B5FE25E3-B262-4B17-91EF-B3772A6B62BB");
var o = C("Filter: id == b5fe25e3-b262-4b17-91ef-b3772a6b62bb");
Assert.Equal(o, i);
}
[Fact]
public void Should_parse_filter_when_type_is_string()
{
var i = Q("$filter=firstName eq 'Dagobert'");
var o = C("Filter: firstName == Dagobert");
Assert.Equal(o, i);
}
[Fact]
public void Should_parse_filter_when_type_is_boolean()
{
var i = Q("$filter=isComicFigure eq true");
var o = C("Filter: isComicFigure == True");
Assert.Equal(o, i);
}
[Fact]
public void Should_parse_filter_when_type_is_int32()
{
var i = Q("$filter=age eq 60");
var o = C("Filter: age == 60");
Assert.Equal(o, i);
}
[Fact]
public void Should_parse_filter_when_type_is_int64()
{
var i = Q("$filter=incomeCents eq 31543143513456789");
var o = C("Filter: incomeCents == 31543143513456789");
Assert.Equal(o, i);
}
[Fact]
public void Should_parse_filter_when_type_is_double()
{
var i = Q("$filter=incomeMio eq 5634474356.1233");
var o = C("Filter: incomeMio == 5634474356.1233");
Assert.Equal(o, i);
}
[Fact]
public void Should_parse_filter_with_negation()
{
var i = Q("$filter=not endswith(lastName, 'Duck')");
var o = C("Filter: !(endsWith(lastName, Duck))");
Assert.Equal(o, i);
}
[Fact]
public void Should_parse_filter_with_startswith()
{
var i = Q("$filter=startswith(lastName, 'Duck')");
var o = C("Filter: startsWith(lastName, Duck)");
Assert.Equal(o, i);
}
[Fact]
public void Should_parse_filter_with_endswith()
{
var i = Q("$filter=endswith(lastName, 'Duck')");
var o = C("Filter: endsWith(lastName, Duck)");
Assert.Equal(o, i);
}
[Fact]
public void Should_parse_filter_with_contains()
{
var i = Q("$filter=contains(lastName, 'Duck')");
var o = C("Filter: contains(lastName, Duck)");
Assert.Equal(o, i);
}
[Fact]
public void Should_parse_filter_with_contains_to_true()
{
var i = Q("$filter=contains(lastName, 'Duck') eq true");
var o = C("Filter: contains(lastName, Duck)");
Assert.Equal(o, i);
}
[Fact]
public void Should_parse_filter_with_contains_to_false()
{
var i = Q("$filter=contains(lastName, 'Duck') eq false");
var o = C("Filter: !(contains(lastName, Duck))");
Assert.Equal(o, i);
}
[Fact]
public void Should_parse_filter_with_equals()
{
var i = Q("$filter=age eq 1");
var o = C("Filter: age == 1");
Assert.Equal(o, i);
}
[Fact]
public void Should_parse_filter_with_notequals()
{
var i = Q("$filter=age ne 1");
var o = C("Filter: age != 1");
Assert.Equal(o, i);
}
[Fact]
public void Should_parse_filter_with_lessthan()
{
var i = Q("$filter=age lt 1");
var o = C("Filter: age < 1");
Assert.Equal(o, i);
}
[Fact]
public void Should_parse_filter_with_lessthanorequal()
{
var i = Q("$filter=age le 1");
var o = C("Filter: age <= 1");
Assert.Equal(o, i);
}
[Fact]
public void Should_parse_filter_with_greaterthan()
{
var i = Q("$filter=age gt 1");
var o = C("Filter: age > 1");
Assert.Equal(o, i);
}
[Fact]
public void Should_parse_filter_with_greaterthanorequal()
{
var i = Q("$filter=age ge 1");
var o = C("Filter: age >= 1");
Assert.Equal(o, i);
}
[Fact]
public void Should_parse_filter_with_conjunction_and_contains()
{
var i = Q("$filter=contains(firstName, 'Sebastian') eq false and isComicFigure eq true");
var o = C("Filter: (!(contains(firstName, Sebastian)) && isComicFigure == True)");
Assert.Equal(o, i);
}
[Fact]
public void Should_parse_filter_with_conjunction()
{
var i = Q("$filter=age eq 1 and age eq 2");
var o = C("Filter: (age == 1 && age == 2)");
Assert.Equal(o, i);
}
[Fact]
public void Should_parse_filter_with_disjunction()
{
var i = Q("$filter=age eq 1 or age eq 2");
var o = C("Filter: (age == 1 || age == 2)");
Assert.Equal(o, i);
}
[Fact]
public void Should_parse_filter_with_full_text()
{
var i = Q("$search=Duck");
var o = C("FullText: Duck");
Assert.Equal(o, i);
}
[Fact]
public void Should_parse_filter_with_full_text_and_multiple_terms()
{
var i = Q("$search=Dagobert or Donald");
var o = C("FullText: Dagobert or Donald");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_orderby_with_single_field()
{
var i = Q("$orderby=age desc");
var o = C("Sort: age Descending");
Assert.Equal(o, i);
}
[Fact]
public void Should_make_orderby_with_multiple_field()
{
var i = Q("$orderby=age, incomeMio desc");
var o = C("Sort: age Ascending, incomeMio Descending");
Assert.Equal(o, i);
}
[Fact]
public void Should_parse_filter_and_take()
{
var i = Q("$top=3&$skip=4");
var o = C("Skip: 4; Take: 3");
Assert.Equal(o, i);
}
private static string C(string value)
{
return value.Replace('\'', '"');
}
private string Q(string value)
{
var parser = EdmModel.ParseQuery(value);
return parser.ToQuery().ToString();
}
}
}

32
tests/Squidex.Infrastructure.Tests/Queries/PascalCasePathConverterTests.cs

@ -0,0 +1,32 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Xunit;
namespace Squidex.Infrastructure.Queries
{
public class PascalCasePathConverterTests
{
[Fact]
public void Should_convert_property()
{
var source = FilterBuilder.Eq("property", 1);
var result = PascalCasePathConverter.Transform(source);
Assert.Equal("Property == 1", result.ToString());
}
[Fact]
public void Should_convert_properties()
{
var source = FilterBuilder.Eq("root.child", 1);
var result = PascalCasePathConverter.Transform(source);
Assert.Equal("Root.Child == 1", result.ToString());
}
}
}
Loading…
Cancel
Save