Browse Source

Merge pull request #391 from Squidex/feature/query-json

Feature/query json
pull/396/head
Sebastian Stehle 7 years ago
committed by GitHub
parent
commit
a14bc3d115
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs
  2. 4
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs
  3. 6
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs
  4. 2
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
  5. 12
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs
  6. 4
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs
  7. 4
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs
  8. 24
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/AdaptionVisitor.cs
  9. 19
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs
  10. 2
      src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs
  11. 10
      src/Squidex.Domain.Apps.Entities/Assets/Queries/FilterTagTransformer.cs
  12. 2
      src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs
  13. 2
      src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs
  14. 4
      src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs
  15. 10
      src/Squidex.Domain.Apps.Entities/Contents/Queries/FilterTagTransformer.cs
  16. 4
      src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs
  17. 4
      src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterBuilder.cs
  18. 60
      src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterVisitor.cs
  19. 4
      src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/LimitExtensions.cs
  20. 2
      src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/SortBuilder.cs
  21. 2
      src/Squidex.Infrastructure/Json/Newtonsoft/JsonClassConverter.cs
  22. 7
      src/Squidex.Infrastructure/Json/Newtonsoft/LanguageConverter.cs
  23. 7
      src/Squidex.Infrastructure/Json/Newtonsoft/NamedGuidIdConverter.cs
  24. 7
      src/Squidex.Infrastructure/Json/Newtonsoft/NamedLongIdConverter.cs
  25. 7
      src/Squidex.Infrastructure/Json/Newtonsoft/NamedStringIdConverter.cs
  26. 7
      src/Squidex.Infrastructure/Json/Newtonsoft/RefTokenConverter.cs
  27. 87
      src/Squidex.Infrastructure/Queries/ClrFilter.cs
  28. 13
      src/Squidex.Infrastructure/Queries/ClrQuery.cs
  29. 140
      src/Squidex.Infrastructure/Queries/ClrValue.cs
  30. 2
      src/Squidex.Infrastructure/Queries/ClrValueType.cs
  31. 67
      src/Squidex.Infrastructure/Queries/CompareFilter.cs
  32. 2
      src/Squidex.Infrastructure/Queries/CompareOperator.cs
  33. 85
      src/Squidex.Infrastructure/Queries/FilterBuilder.cs
  34. 70
      src/Squidex.Infrastructure/Queries/FilterComparison.cs
  35. 45
      src/Squidex.Infrastructure/Queries/FilterJunction.cs
  36. 4
      src/Squidex.Infrastructure/Queries/FilterNode.cs
  37. 8
      src/Squidex.Infrastructure/Queries/FilterNodeVisitor.cs
  38. 149
      src/Squidex.Infrastructure/Queries/FilterValue.cs
  39. 162
      src/Squidex.Infrastructure/Queries/Json/FilterConverter.cs
  40. 29
      src/Squidex.Infrastructure/Queries/Json/PropertyPathConverter.cs
  41. 39
      src/Squidex.Infrastructure/Queries/LogicalFilter.cs
  42. 2
      src/Squidex.Infrastructure/Queries/LogicalFilterType.cs
  43. 14
      src/Squidex.Infrastructure/Queries/NegateFilter.cs
  44. 46
      src/Squidex.Infrastructure/Queries/OData/ConstantWithTypeVisitor.cs
  45. 4
      src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs
  46. 2
      src/Squidex.Infrastructure/Queries/OData/FilterBuilder.cs
  47. 46
      src/Squidex.Infrastructure/Queries/OData/FilterVisitor.cs
  48. 4
      src/Squidex.Infrastructure/Queries/OData/LimitExtensions.cs
  49. 2
      src/Squidex.Infrastructure/Queries/OData/SortBuilder.cs
  50. 10
      src/Squidex.Infrastructure/Queries/PascalCasePathConverter.cs
  51. 54
      src/Squidex.Infrastructure/Queries/PropertyPath.cs
  52. 4
      src/Squidex.Infrastructure/Queries/Query.cs
  53. 7
      src/Squidex.Infrastructure/Queries/SortNode.cs
  54. 12
      src/Squidex.Infrastructure/Queries/TransformVisitor.cs
  55. 12
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetQueryServiceTests.cs
  56. 42
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/MongoDbQueryTests.cs
  57. 6
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/FilterTagTransformerTests.cs
  58. 6
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs
  59. 45
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs
  60. 10
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/FilterTagTransformerTests.cs
  61. 2
      tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs
  62. 8
      tests/Squidex.Infrastructure.Tests/Queries/PascalCasePathConverterTests.cs
  63. 181
      tests/Squidex.Infrastructure.Tests/Queries/QueryJsonTests.cs
  64. 4
      tests/Squidex.Infrastructure.Tests/Queries/QueryODataConversionTests.cs
  65. 3
      tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs

4
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs

@ -14,7 +14,7 @@ using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Core.ValidateContent namespace Squidex.Domain.Apps.Core.ValidateContent
{ {
public delegate Task<IReadOnlyList<Guid>> CheckContents(Guid schemaId, FilterNode filter); public delegate Task<IReadOnlyList<Guid>> CheckContents(Guid schemaId, FilterNode<ClrValue> filter);
public delegate Task<IReadOnlyList<IAssetInfo>> CheckAssets(IEnumerable<Guid> ids); public delegate Task<IReadOnlyList<IAssetInfo>> CheckAssets(IEnumerable<Guid> ids);
@ -84,7 +84,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
return new ValidationContext(contentId, schemaId, checkContent, checkAsset, propertyPath.Enqueue(property), IsOptional); return new ValidationContext(contentId, schemaId, checkContent, checkAsset, propertyPath.Enqueue(property), IsOptional);
} }
public Task<IReadOnlyList<Guid>> GetContentIdsAsync(Guid validatedSchemaId, FilterNode filter) public Task<IReadOnlyList<Guid>> GetContentIdsAsync(Guid validatedSchemaId, FilterNode<ClrValue> filter)
{ {
return checkContent(validatedSchemaId, filter); return checkContent(validatedSchemaId, filter);
} }

4
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs

@ -15,7 +15,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{ {
public sealed class ReferencesValidator : IValidator public sealed class ReferencesValidator : IValidator
{ {
private static readonly IReadOnlyList<string> Path = new List<string> { "Id" }; private static readonly PropertyPath Path = "Id";
private readonly Guid schemaId; private readonly Guid schemaId;
@ -28,7 +28,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{ {
if (value is ICollection<Guid> contentIds) if (value is ICollection<Guid> contentIds)
{ {
var filter = new FilterComparison(Path, FilterOperator.In, new FilterValue(contentIds.ToList())); var filter = ClrFilter.In(Path, contentIds.ToList());
var foundIds = await context.GetContentIdsAsync(schemaId, filter); var foundIds = await context.GetContentIdsAsync(schemaId, filter);

6
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs

@ -20,15 +20,15 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
if (value != null && (count == 0 || (count == 2 && context.Path.Last() == InvariantPartitioning.Key))) if (value != null && (count == 0 || (count == 2 && context.Path.Last() == InvariantPartitioning.Key)))
{ {
FilterNode filter = null; FilterNode<ClrValue> filter = null;
if (value is string s) if (value is string s)
{ {
filter = new FilterComparison(Path(context), FilterOperator.Equals, new FilterValue(s)); filter = ClrFilter.Eq(Path(context), s);
} }
else if (value is double d) else if (value is double d)
{ {
filter = new FilterComparison(Path(context), FilterOperator.Equals, new FilterValue(d)); filter = ClrFilter.Eq(Path(context), d);
} }
if (filter != null) if (filter != null)

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

@ -77,7 +77,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
ct); ct);
} }
public async Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, Query query) public async Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, ClrQuery query)
{ {
using (Profiler.TraceMethod<MongoAssetRepository>("QueryAsyncByQuery")) using (Profiler.TraceMethod<MongoAssetRepository>("QueryAsyncByQuery"))
{ {

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

@ -20,11 +20,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors
{ {
private static readonly FilterDefinitionBuilder<MongoAssetEntity> Filter = Builders<MongoAssetEntity>.Filter; private static readonly FilterDefinitionBuilder<MongoAssetEntity> Filter = Builders<MongoAssetEntity>.Filter;
public static Query AdjustToModel(this Query query) public static ClrQuery AdjustToModel(this ClrQuery query)
{ {
if (query.Filter != null) if (query.Filter != null)
{ {
query.Filter = PascalCasePathConverter.Transform(query.Filter); query.Filter = PascalCasePathConverter<ClrValue>.Transform(query.Filter);
} }
query.Sort = query.Sort query.Sort = query.Sort
@ -37,22 +37,22 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors
return query; return query;
} }
public static IFindFluent<MongoAssetEntity, MongoAssetEntity> AssetSort(this IFindFluent<MongoAssetEntity, MongoAssetEntity> cursor, Query query) public static IFindFluent<MongoAssetEntity, MongoAssetEntity> AssetSort(this IFindFluent<MongoAssetEntity, MongoAssetEntity> cursor, ClrQuery query)
{ {
return cursor.Sort(query.BuildSort<MongoAssetEntity>()); return cursor.Sort(query.BuildSort<MongoAssetEntity>());
} }
public static IFindFluent<MongoAssetEntity, MongoAssetEntity> AssetTake(this IFindFluent<MongoAssetEntity, MongoAssetEntity> cursor, Query query) public static IFindFluent<MongoAssetEntity, MongoAssetEntity> AssetTake(this IFindFluent<MongoAssetEntity, MongoAssetEntity> cursor, ClrQuery query)
{ {
return cursor.Take(query); return cursor.Take(query);
} }
public static IFindFluent<MongoAssetEntity, MongoAssetEntity> AssetSkip(this IFindFluent<MongoAssetEntity, MongoAssetEntity> cursor, Query query) public static IFindFluent<MongoAssetEntity, MongoAssetEntity> AssetSkip(this IFindFluent<MongoAssetEntity, MongoAssetEntity> cursor, ClrQuery query)
{ {
return cursor.Skip(query); return cursor.Skip(query);
} }
public static FilterDefinition<MongoAssetEntity> BuildFilter(this Query query, Guid appId) public static FilterDefinition<MongoAssetEntity> BuildFilter(this ClrQuery query, Guid appId)
{ {
var filters = new List<FilterDefinition<MongoAssetEntity>> var filters = new List<FilterDefinition<MongoAssetEntity>>
{ {

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

@ -68,7 +68,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
return "State_Contents"; return "State_Contents";
} }
public async Task<IResultList<IContentEntity>> QueryAsync(ISchemaEntity schema, Query query, List<Guid> ids, Status[] status, bool inDraft, bool includeDraft = true) public async Task<IResultList<IContentEntity>> QueryAsync(ISchemaEntity schema, ClrQuery query, List<Guid> ids, Status[] status, bool inDraft, bool includeDraft = true)
{ {
try try
{ {
@ -169,7 +169,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
}); });
} }
public async Task<IReadOnlyList<Guid>> QueryIdsAsync(ISchemaEntity schema, FilterNode filterNode) public async Task<IReadOnlyList<Guid>> QueryIdsAsync(ISchemaEntity schema, FilterNode<ClrValue> filterNode)
{ {
var filter = filterNode.AdjustToModel(schema.SchemaDef, true).ToFilter(schema.Id); var filter = filterNode.AdjustToModel(schema.SchemaDef, true).ToFilter(schema.Id);

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

@ -65,7 +65,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
return contents.InitializeAsync(ct); return contents.InitializeAsync(ct);
} }
public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, bool inDraft, Query query, bool includeDraft = true) public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, bool inDraft, ClrQuery query, bool includeDraft = true)
{ {
Guard.NotNull(app, nameof(app)); Guard.NotNull(app, nameof(app));
Guard.NotNull(schema, nameof(schema)); Guard.NotNull(schema, nameof(schema));
@ -118,7 +118,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
} }
} }
public async Task<IReadOnlyList<Guid>> QueryIdsAsync(Guid appId, Guid schemaId, FilterNode filterNode) public async Task<IReadOnlyList<Guid>> QueryIdsAsync(Guid appId, Guid schemaId, FilterNode<ClrValue> filterNode)
{ {
using (Profiler.TraceMethod<MongoContentRepository>()) using (Profiler.TraceMethod<MongoContentRepository>())
{ {

24
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/AdaptionVisitor.cs

@ -13,35 +13,35 @@ using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors
{ {
internal sealed class AdaptionVisitor : TransformVisitor internal sealed class AdaptionVisitor : TransformVisitor<ClrValue>
{ {
private readonly Func<IReadOnlyList<string>, IReadOnlyList<string>> pathConverter; private readonly Func<PropertyPath, PropertyPath> pathConverter;
public AdaptionVisitor(Func<IReadOnlyList<string>, IReadOnlyList<string>> pathConverter) public AdaptionVisitor(Func<PropertyPath, PropertyPath> pathConverter)
{ {
this.pathConverter = pathConverter; this.pathConverter = pathConverter;
} }
public override FilterNode Visit(FilterComparison nodeIn) public override FilterNode<ClrValue> Visit(CompareFilter<ClrValue> nodeIn)
{ {
FilterComparison result; CompareFilter<ClrValue> result;
var value = nodeIn.Rhs.Value; var value = nodeIn.Value.Value;
if (value is Instant && if (value is Instant &&
!string.Equals(nodeIn.Lhs[0], "mt", StringComparison.OrdinalIgnoreCase) && !string.Equals(nodeIn.Path[0], "mt", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(nodeIn.Lhs[0], "ct", StringComparison.OrdinalIgnoreCase)) !string.Equals(nodeIn.Path[0], "ct", StringComparison.OrdinalIgnoreCase))
{ {
result = new FilterComparison(pathConverter(nodeIn.Lhs), nodeIn.Operator, new FilterValue(value.ToString())); result = new CompareFilter<ClrValue>(pathConverter(nodeIn.Path), nodeIn.Operator, value.ToString());
} }
else else
{ {
result = new FilterComparison(pathConverter(nodeIn.Lhs), nodeIn.Operator, nodeIn.Rhs); result = new CompareFilter<ClrValue>(pathConverter(nodeIn.Path), nodeIn.Operator, nodeIn.Value);
} }
if (result.Lhs.Count == 1 && result.Lhs[0] == "_id" && result.Rhs.Value is List<Guid> guidList) if (result.Path.Count == 1 && result.Path[0] == "_id" && result.Value.Value is List<Guid> guidList)
{ {
result = new FilterComparison(nodeIn.Lhs, nodeIn.Operator, new FilterValue(guidList.Select(x => x.ToString()).ToList())); result = new CompareFilter<ClrValue>(nodeIn.Path, nodeIn.Operator, guidList.Select(x => x.ToString()).ToList());
} }
return result; return result;

19
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs

@ -28,7 +28,7 @@ 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 Query AdjustToModel(this Query query, Schema schema, bool useDraft) public static ClrQuery AdjustToModel(this ClrQuery query, Schema schema, bool useDraft)
{ {
var pathConverter = PathConverter(schema, useDraft); var pathConverter = PathConverter(schema, useDraft);
@ -42,14 +42,14 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors
return query; return query;
} }
public static FilterNode AdjustToModel(this FilterNode filterNode, Schema schema, bool inDraft) public static FilterNode<ClrValue> AdjustToModel(this FilterNode<ClrValue> filterNode, Schema schema, bool inDraft)
{ {
var pathConverter = PathConverter(schema, inDraft); var pathConverter = PathConverter(schema, inDraft);
return filterNode.Accept(new AdaptionVisitor(pathConverter)); return filterNode.Accept(new AdaptionVisitor(pathConverter));
} }
private static Func<IReadOnlyList<string>, IReadOnlyList<string>> PathConverter(Schema schema, bool inDraft) private static Func<PropertyPath, PropertyPath> PathConverter(Schema schema, bool inDraft)
{ {
return propertyNames => return propertyNames =>
{ {
@ -107,17 +107,17 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors
}; };
} }
public static IFindFluent<MongoContentEntity, MongoContentEntity> ContentSort(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, Query query) public static IFindFluent<MongoContentEntity, MongoContentEntity> ContentSort(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, ClrQuery query)
{ {
return cursor.Sort(query.BuildSort<MongoContentEntity>()); return cursor.Sort(query.BuildSort<MongoContentEntity>());
} }
public static IFindFluent<MongoContentEntity, MongoContentEntity> ContentTake(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, Query query) public static IFindFluent<MongoContentEntity, MongoContentEntity> ContentTake(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, ClrQuery query)
{ {
return cursor.Take(query); return cursor.Take(query);
} }
public static IFindFluent<MongoContentEntity, MongoContentEntity> ContentSkip(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, Query query) public static IFindFluent<MongoContentEntity, MongoContentEntity> ContentSkip(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, ClrQuery query)
{ {
return cursor.Skip(query); return cursor.Skip(query);
} }
@ -142,12 +142,13 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors
return CreateFilter(null, schemaId, ids, status, null); return CreateFilter(null, schemaId, ids, status, null);
} }
public static FilterDefinition<MongoContentEntity> ToFilter(this Query query, Guid schemaId, ICollection<Guid> ids, Status[] status) public static FilterDefinition<MongoContentEntity> ToFilter(this ClrQuery query, Guid schemaId, ICollection<Guid> ids, Status[] status)
{ {
return CreateFilter(null, schemaId, ids, status, query); return CreateFilter(null, schemaId, ids, status, query);
} }
private static FilterDefinition<MongoContentEntity> CreateFilter(Guid? appId, Guid? schemaId, ICollection<Guid> ids, Status[] status, Query query) private static FilterDefinition<MongoContentEntity> CreateFilter(Guid? appId, Guid? schemaId, ICollection<Guid> ids, Status[] status,
ClrQuery query)
{ {
var filters = new List<FilterDefinition<MongoContentEntity>>(); var filters = new List<FilterDefinition<MongoContentEntity>>();
@ -188,7 +189,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors
return Filter.And(filters); return Filter.And(filters);
} }
public static FilterDefinition<MongoContentEntity> ToFilter(this FilterNode filterNode, Guid schemaId) public static FilterDefinition<MongoContentEntity> ToFilter(this FilterNode<ClrValue> filterNode, Guid schemaId)
{ {
var filters = new List<FilterDefinition<MongoContentEntity>> var filters = new List<FilterDefinition<MongoContentEntity>>
{ {

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

@ -110,7 +110,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
return assets.SortSet(x => x.Id, ids); return assets.SortSet(x => x.Id, ids);
} }
private Query ParseQuery(Context context, string query) private ClrQuery ParseQuery(Context context, string query)
{ {
try try
{ {

10
src/Squidex.Domain.Apps.Entities/Assets/Queries/FilterTagTransformer.cs

@ -13,7 +13,7 @@ using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.Assets.Queries namespace Squidex.Domain.Apps.Entities.Assets.Queries
{ {
public sealed class FilterTagTransformer : TransformVisitor public sealed class FilterTagTransformer : TransformVisitor<ClrValue>
{ {
private readonly ITagService tagService; private readonly ITagService tagService;
private readonly Guid appId; private readonly Guid appId;
@ -25,22 +25,22 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
this.tagService = tagService; this.tagService = tagService;
} }
public static FilterNode Transform(FilterNode nodeIn, Guid appId, ITagService tagService) public static FilterNode<ClrValue> Transform(FilterNode<ClrValue> nodeIn, Guid appId, ITagService tagService)
{ {
Guard.NotNull(tagService, nameof(tagService)); Guard.NotNull(tagService, nameof(tagService));
return nodeIn.Accept(new FilterTagTransformer(appId, tagService)); return nodeIn.Accept(new FilterTagTransformer(appId, tagService));
} }
public override FilterNode Visit(FilterComparison nodeIn) public override FilterNode<ClrValue> Visit(CompareFilter<ClrValue> nodeIn)
{ {
if (string.Equals(nodeIn.Lhs[0], nameof(IAssetEntity.Tags), StringComparison.OrdinalIgnoreCase) && nodeIn.Rhs.Value is string stringValue) if (string.Equals(nodeIn.Path[0], nameof(IAssetEntity.Tags), StringComparison.OrdinalIgnoreCase) && nodeIn.Value.Value is string stringValue)
{ {
var tagNames = Task.Run(() => tagService.GetTagIdsAsync(appId, TagGroups.Assets, HashSet.Of(stringValue))).Result; var tagNames = Task.Run(() => tagService.GetTagIdsAsync(appId, TagGroups.Assets, HashSet.Of(stringValue))).Result;
if (tagNames.TryGetValue(stringValue, out var normalized)) if (tagNames.TryGetValue(stringValue, out var normalized))
{ {
return new FilterComparison(nodeIn.Lhs, nodeIn.Operator, new FilterValue(normalized)); return new CompareFilter<ClrValue>(nodeIn.Path, nodeIn.Operator, normalized);
} }
} }

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

@ -17,7 +17,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Repositories
{ {
Task<IReadOnlyList<IAssetEntity>> QueryByHashAsync(Guid appId, string hash); Task<IReadOnlyList<IAssetEntity>> QueryByHashAsync(Guid appId, string hash);
Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, Query query); Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, ClrQuery 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/Contents/ContentOperationContext.cs

@ -122,7 +122,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
return await assetRepository.QueryAsync(appEntity.Id, new HashSet<Guid>(assetIds)); return await assetRepository.QueryAsync(appEntity.Id, new HashSet<Guid>(assetIds));
} }
private async Task<IReadOnlyList<Guid>> QueryContentsAsync(Guid filterSchemaId, FilterNode filterNode) private async Task<IReadOnlyList<Guid>> QueryContentsAsync(Guid filterSchemaId, FilterNode<ClrValue> filterNode)
{ {
return await contentRepository.QueryIdsAsync(appEntity.Id, filterSchemaId, filterNode); return await contentRepository.QueryIdsAsync(appEntity.Id, filterSchemaId, filterNode);
} }

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

@ -254,7 +254,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
} }
} }
private Query ParseQuery(Context context, string query, ISchemaEntity schema) private ClrQuery ParseQuery(Context context, string query, ISchemaEntity schema)
{ {
using (Profiler.TraceMethod<ContentQueryService>()) using (Profiler.TraceMethod<ContentQueryService>())
{ {
@ -362,7 +362,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
return contentRepository.QueryAsync(context.App, GetStatus(context), new HashSet<Guid>(ids), WithDraft(context)); return contentRepository.QueryAsync(context.App, GetStatus(context), new HashSet<Guid>(ids), WithDraft(context));
} }
private Task<IResultList<IContentEntity>> QueryCoreAsync(Context context, ISchemaEntity schema, Query query) private Task<IResultList<IContentEntity>> QueryCoreAsync(Context context, ISchemaEntity schema, ClrQuery query)
{ {
return contentRepository.QueryAsync(context.App, schema, GetStatus(context), context.IsFrontendClient, query, WithDraft(context)); return contentRepository.QueryAsync(context.App, schema, GetStatus(context), context.IsFrontendClient, query, WithDraft(context));
} }

10
src/Squidex.Domain.Apps.Entities/Contents/Queries/FilterTagTransformer.cs

@ -16,7 +16,7 @@ using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.Contents.Queries namespace Squidex.Domain.Apps.Entities.Contents.Queries
{ {
public sealed class FilterTagTransformer : TransformVisitor public sealed class FilterTagTransformer : TransformVisitor<ClrValue>
{ {
private readonly ITagService tagService; private readonly ITagService tagService;
private readonly ISchemaEntity schema; private readonly ISchemaEntity schema;
@ -29,7 +29,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
this.tagService = tagService; this.tagService = tagService;
} }
public static FilterNode Transform(FilterNode nodeIn, Guid appId, ISchemaEntity schema, ITagService tagService) public static FilterNode<ClrValue> Transform(FilterNode<ClrValue> nodeIn, Guid appId, ISchemaEntity schema, ITagService tagService)
{ {
Guard.NotNull(tagService, nameof(tagService)); Guard.NotNull(tagService, nameof(tagService));
Guard.NotNull(schema, nameof(schema)); Guard.NotNull(schema, nameof(schema));
@ -37,15 +37,15 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
return nodeIn.Accept(new FilterTagTransformer(appId, schema, tagService)); return nodeIn.Accept(new FilterTagTransformer(appId, schema, tagService));
} }
public override FilterNode Visit(FilterComparison nodeIn) public override FilterNode<ClrValue> Visit(CompareFilter<ClrValue> nodeIn)
{ {
if (nodeIn.Rhs.Value is string stringValue && IsDataPath(nodeIn.Lhs) && IsTagField(nodeIn.Lhs)) if (nodeIn.Value.Value is string stringValue && IsDataPath(nodeIn.Path) && IsTagField(nodeIn.Path))
{ {
var tagNames = Task.Run(() => tagService.GetTagIdsAsync(appId, TagGroups.Schemas(schema.Id), HashSet.Of(stringValue))).Result; var tagNames = Task.Run(() => tagService.GetTagIdsAsync(appId, TagGroups.Schemas(schema.Id), HashSet.Of(stringValue))).Result;
if (tagNames.TryGetValue(stringValue, out var normalized)) if (tagNames.TryGetValue(stringValue, out var normalized))
{ {
return new FilterComparison(nodeIn.Lhs, nodeIn.Operator, new FilterValue(normalized)); return new CompareFilter<ClrValue>(nodeIn.Path, nodeIn.Operator, normalized);
} }
} }

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

@ -23,9 +23,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Repositories
Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet<Guid> ids, bool includeDraft); Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet<Guid> ids, bool includeDraft);
Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, bool inDraft, Query query, bool includeDraft); Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, bool inDraft, ClrQuery query, bool includeDraft);
Task<IReadOnlyList<Guid>> QueryIdsAsync(Guid appId, Guid schemaId, FilterNode filterNode); Task<IReadOnlyList<Guid>> QueryIdsAsync(Guid appId, Guid schemaId, FilterNode<ClrValue> filterNode);
Task<IContentEntity> FindContentAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Guid id, bool includeDraft); Task<IContentEntity> FindContentAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Guid id, bool includeDraft);

4
src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterBuilder.cs

@ -12,7 +12,7 @@ namespace Squidex.Infrastructure.MongoDb.Queries
{ {
public static class FilterBuilder public static class FilterBuilder
{ {
public static (FilterDefinition<T> Filter, bool Last) BuildFilter<T>(this Query query, bool supportsSearch = true) public static (FilterDefinition<T> Filter, bool Last) BuildFilter<T>(this ClrQuery query, bool supportsSearch = true)
{ {
if (query.FullText != null) if (query.FullText != null)
{ {
@ -32,7 +32,7 @@ namespace Squidex.Infrastructure.MongoDb.Queries
return (null, false); return (null, false);
} }
public static FilterDefinition<T> BuildFilter<T>(this FilterNode filterNode) public static FilterDefinition<T> BuildFilter<T>(this FilterNode<ClrValue> filterNode)
{ {
return FilterVisitor<T>.Visit(filterNode); return FilterVisitor<T>.Visit(filterNode);
} }

60
src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterVisitor.cs

@ -14,7 +14,7 @@ using Squidex.Infrastructure.Queries;
namespace Squidex.Infrastructure.MongoDb.Queries namespace Squidex.Infrastructure.MongoDb.Queries
{ {
public sealed class FilterVisitor<T> : FilterNodeVisitor<FilterDefinition<T>> public sealed class FilterVisitor<T> : FilterNodeVisitor<FilterDefinition<T>, ClrValue>
{ {
private static readonly FilterDefinitionBuilder<T> Filter = Builders<T>.Filter; private static readonly FilterDefinitionBuilder<T> Filter = Builders<T>.Filter;
private static readonly FilterVisitor<T> Instance = new FilterVisitor<T>(); private static readonly FilterVisitor<T> Instance = new FilterVisitor<T>();
@ -23,64 +23,64 @@ namespace Squidex.Infrastructure.MongoDb.Queries
{ {
} }
public static FilterDefinition<T> Visit(FilterNode node) public static FilterDefinition<T> Visit(FilterNode<ClrValue> node)
{ {
return node.Accept(Instance); return node.Accept(Instance);
} }
public override FilterDefinition<T> Visit(FilterNegate nodeIn) public override FilterDefinition<T> Visit(NegateFilter<ClrValue> nodeIn)
{ {
return Filter.Not(nodeIn.Operand.Accept(this)); return Filter.Not(nodeIn.Filter.Accept(this));
} }
public override FilterDefinition<T> Visit(FilterJunction nodeIn) public override FilterDefinition<T> Visit(LogicalFilter<ClrValue> nodeIn)
{ {
if (nodeIn.JunctionType == FilterJunctionType.And) if (nodeIn.Type == LogicalFilterType.And)
{ {
return Filter.And(nodeIn.Operands.Select(x => x.Accept(this))); return Filter.And(nodeIn.Filters.Select(x => x.Accept(this)));
} }
else else
{ {
return Filter.Or(nodeIn.Operands.Select(x => x.Accept(this))); return Filter.Or(nodeIn.Filters.Select(x => x.Accept(this)));
} }
} }
public override FilterDefinition<T> Visit(FilterComparison nodeIn) public override FilterDefinition<T> Visit(CompareFilter<ClrValue> nodeIn)
{ {
var propertyName = string.Join(".", nodeIn.Lhs); var propertyName = nodeIn.Path.ToString();
switch (nodeIn.Operator) switch (nodeIn.Operator)
{ {
case FilterOperator.Empty: case CompareOperator.Empty:
return Filter.Or(Filter.Exists(propertyName, false), Filter.Eq(propertyName, default(T)), Filter.Eq(propertyName, string.Empty), Filter.Eq(propertyName, new T[0])); return Filter.Or(Filter.Exists(propertyName, false), Filter.Eq(propertyName, default(T)), Filter.Eq(propertyName, string.Empty), Filter.Eq(propertyName, new T[0]));
case FilterOperator.StartsWith: case CompareOperator.StartsWith:
return Filter.Regex(propertyName, BuildRegex(nodeIn, s => "^" + s)); return Filter.Regex(propertyName, BuildRegex(nodeIn, s => "^" + s));
case FilterOperator.Contains: case CompareOperator.Contains:
return Filter.Regex(propertyName, BuildRegex(nodeIn, s => s)); return Filter.Regex(propertyName, BuildRegex(nodeIn, s => s));
case FilterOperator.EndsWith: case CompareOperator.EndsWith:
return Filter.Regex(propertyName, BuildRegex(nodeIn, s => s + "$")); return Filter.Regex(propertyName, BuildRegex(nodeIn, s => s + "$"));
case FilterOperator.Equals: case CompareOperator.Equals:
return Filter.Eq(propertyName, nodeIn.Rhs.Value); return Filter.Eq(propertyName, nodeIn.Value.Value);
case FilterOperator.GreaterThan: case CompareOperator.GreaterThan:
return Filter.Gt(propertyName, nodeIn.Rhs.Value); return Filter.Gt(propertyName, nodeIn.Value.Value);
case FilterOperator.GreaterThanOrEqual: case CompareOperator.GreaterThanOrEqual:
return Filter.Gte(propertyName, nodeIn.Rhs.Value); return Filter.Gte(propertyName, nodeIn.Value.Value);
case FilterOperator.LessThan: case CompareOperator.LessThan:
return Filter.Lt(propertyName, nodeIn.Rhs.Value); return Filter.Lt(propertyName, nodeIn.Value.Value);
case FilterOperator.LessThanOrEqual: case CompareOperator.LessThanOrEqual:
return Filter.Lte(propertyName, nodeIn.Rhs.Value); return Filter.Lte(propertyName, nodeIn.Value.Value);
case FilterOperator.NotEquals: case CompareOperator.NotEquals:
return Filter.Ne(propertyName, nodeIn.Rhs.Value); return Filter.Ne(propertyName, nodeIn.Value.Value);
case FilterOperator.In: case CompareOperator.In:
return Filter.In(propertyName, ((IList)nodeIn.Rhs.Value).OfType<object>()); return Filter.In(propertyName, ((IList)nodeIn.Value.Value).OfType<object>());
} }
throw new NotSupportedException(); throw new NotSupportedException();
} }
private static BsonRegularExpression BuildRegex(FilterComparison node, Func<string, string> formatter) private static BsonRegularExpression BuildRegex(CompareFilter<ClrValue> node, Func<string, string> formatter)
{ {
return new BsonRegularExpression(formatter(node.Rhs.Value.ToString()), "i"); return new BsonRegularExpression(formatter(node.Value.Value.ToString()), "i");
} }
} }
} }

4
src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/LimitExtensions.cs

@ -12,7 +12,7 @@ namespace Squidex.Infrastructure.MongoDb.Queries
{ {
public static class LimitExtensions public static class LimitExtensions
{ {
public static IFindFluent<T, T> Take<T>(this IFindFluent<T, T> cursor, Query query) public static IFindFluent<T, T> Take<T>(this IFindFluent<T, T> cursor, ClrQuery query)
{ {
if (query.Take < long.MaxValue) if (query.Take < long.MaxValue)
{ {
@ -22,7 +22,7 @@ namespace Squidex.Infrastructure.MongoDb.Queries
return cursor; return cursor;
} }
public static IFindFluent<T, T> Skip<T>(this IFindFluent<T, T> cursor, Query query) public static IFindFluent<T, T> Skip<T>(this IFindFluent<T, T> cursor, ClrQuery query)
{ {
if (query.Skip > 0) if (query.Skip > 0)
{ {

2
src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/SortBuilder.cs

@ -13,7 +13,7 @@ namespace Squidex.Infrastructure.MongoDb.Queries
{ {
public static class SortBuilder public static class SortBuilder
{ {
public static SortDefinition<T> BuildSort<T>(this Query query) public static SortDefinition<T> BuildSort<T>(this ClrQuery query)
{ {
if (query.Sort.Count > 0) if (query.Sort.Count > 0)
{ {

2
src/Squidex.Infrastructure/Json/Newtonsoft/JsonClassConverter.cs

@ -13,7 +13,7 @@ namespace Squidex.Infrastructure.Json.Newtonsoft
{ {
public abstract class JsonClassConverter<T> : JsonConverter, ISupportedTypes where T : class public abstract class JsonClassConverter<T> : JsonConverter, ISupportedTypes where T : class
{ {
public IEnumerable<Type> SupportedTypes public virtual IEnumerable<Type> SupportedTypes
{ {
get { yield return typeof(T); } get { yield return typeof(T); }
} }

7
src/Squidex.Infrastructure/Json/Newtonsoft/LanguageConverter.cs

@ -19,12 +19,9 @@ namespace Squidex.Infrastructure.Json.Newtonsoft
protected override Language ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) protected override Language ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer)
{ {
if (reader.TokenType != JsonToken.String) var value = serializer.Deserialize<string>(reader);
{
throw new JsonException($"Expected String, but got {reader.TokenType}.");
}
return Language.GetLanguage(reader.Value.ToString()); return Language.GetLanguage(value);
} }
} }
} }

7
src/Squidex.Infrastructure/Json/Newtonsoft/NamedGuidIdConverter.cs

@ -19,12 +19,9 @@ namespace Squidex.Infrastructure.Json.Newtonsoft
protected override NamedId<Guid> ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) protected override NamedId<Guid> ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer)
{ {
if (reader.TokenType != JsonToken.String) var value = serializer.Deserialize<string>(reader);
{
throw new JsonException($"Expected String, but got {reader.TokenType}.");
}
if (!NamedId<Guid>.TryParse(reader.Value.ToString(), Guid.TryParse, out var result)) if (!NamedId<Guid>.TryParse(value, Guid.TryParse, out var result))
{ {
throw new JsonException("Named id must have more than 2 parts divided by commata."); throw new JsonException("Named id must have more than 2 parts divided by commata.");
} }

7
src/Squidex.Infrastructure/Json/Newtonsoft/NamedLongIdConverter.cs

@ -19,12 +19,9 @@ namespace Squidex.Infrastructure.Json.Newtonsoft
protected override NamedId<long> ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) protected override NamedId<long> ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer)
{ {
if (reader.TokenType != JsonToken.String) var value = serializer.Deserialize<string>(reader);
{
throw new JsonException($"Expected String, but got {reader.TokenType}.");
}
if (!NamedId<long>.TryParse(reader.Value.ToString(), long.TryParse, out var result)) if (!NamedId<long>.TryParse(value, long.TryParse, out var result))
{ {
throw new JsonException("Named id must have at least 2 parts divided by commata."); throw new JsonException("Named id must have at least 2 parts divided by commata.");
} }

7
src/Squidex.Infrastructure/Json/Newtonsoft/NamedStringIdConverter.cs

@ -19,12 +19,9 @@ namespace Squidex.Infrastructure.Json.Newtonsoft
protected override NamedId<string> ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) protected override NamedId<string> ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer)
{ {
if (reader.TokenType != JsonToken.String) var value = serializer.Deserialize<string>(reader);
{
throw new JsonException($"Expected String, but got {reader.TokenType}.");
}
if (!NamedId<string>.TryParse(reader.Value.ToString(), ParseString, out var result)) if (!NamedId<string>.TryParse(value, ParseString, out var result))
{ {
throw new JsonException("Named id must have at least 2 parts divided by commata."); throw new JsonException("Named id must have at least 2 parts divided by commata.");
} }

7
src/Squidex.Infrastructure/Json/Newtonsoft/RefTokenConverter.cs

@ -19,12 +19,9 @@ namespace Squidex.Infrastructure.Json.Newtonsoft
protected override RefToken ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) protected override RefToken ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer)
{ {
if (reader.TokenType != JsonToken.String) var value = serializer.Deserialize<string>(reader);
{
throw new JsonException($"Expected String, but got {reader.TokenType}.");
}
if (!RefToken.TryParse(reader.Value.ToString(), out var result)) if (!RefToken.TryParse(value, out var result))
{ {
throw new JsonException("Named id must have at least 2 parts divided by colon."); throw new JsonException("Named id must have at least 2 parts divided by colon.");
} }

87
src/Squidex.Infrastructure/Queries/ClrFilter.cs

@ -0,0 +1,87 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Infrastructure.Queries
{
public static class ClrFilter
{
public static LogicalFilter<ClrValue> And(params FilterNode<ClrValue>[] filters)
{
return new LogicalFilter<ClrValue>(LogicalFilterType.And, filters);
}
public static LogicalFilter<ClrValue> Or(params FilterNode<ClrValue>[] filters)
{
return new LogicalFilter<ClrValue>(LogicalFilterType.Or, filters);
}
public static NegateFilter<ClrValue> Not(FilterNode<ClrValue> filter)
{
return new NegateFilter<ClrValue>(filter);
}
public static CompareFilter<ClrValue> Eq(PropertyPath path, ClrValue value)
{
return Binary(path, CompareOperator.Equals, value);
}
public static CompareFilter<ClrValue> Ne(PropertyPath path, ClrValue value)
{
return Binary(path, CompareOperator.NotEquals, value);
}
public static CompareFilter<ClrValue> Lt(PropertyPath path, ClrValue value)
{
return Binary(path, CompareOperator.LessThan, value);
}
public static CompareFilter<ClrValue> Le(PropertyPath path, ClrValue value)
{
return Binary(path, CompareOperator.LessThanOrEqual, value);
}
public static CompareFilter<ClrValue> Gt(PropertyPath path, ClrValue value)
{
return Binary(path, CompareOperator.GreaterThan, value);
}
public static CompareFilter<ClrValue> Ge(PropertyPath path, ClrValue value)
{
return Binary(path, CompareOperator.GreaterThanOrEqual, value);
}
public static CompareFilter<ClrValue> Contains(PropertyPath path, ClrValue value)
{
return Binary(path, CompareOperator.Contains, value);
}
public static CompareFilter<ClrValue> EndsWith(PropertyPath path, ClrValue value)
{
return Binary(path, CompareOperator.EndsWith, value);
}
public static CompareFilter<ClrValue> StartsWith(PropertyPath path, ClrValue value)
{
return Binary(path, CompareOperator.StartsWith, value);
}
public static CompareFilter<ClrValue> Empty(PropertyPath path)
{
return Binary(path, CompareOperator.Empty, null);
}
public static CompareFilter<ClrValue> In(PropertyPath path, ClrValue value)
{
return Binary(path, CompareOperator.In, value);
}
private static CompareFilter<ClrValue> Binary(PropertyPath path, CompareOperator @operator, ClrValue value)
{
return new CompareFilter<ClrValue>(path, @operator, value ?? ClrValue.Null);
}
}
}

13
src/Squidex.Infrastructure/Queries/ClrQuery.cs

@ -0,0 +1,13 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Infrastructure.Queries
{
public sealed class ClrQuery : Query<ClrValue>
{
}
}

140
src/Squidex.Infrastructure/Queries/ClrValue.cs

@ -0,0 +1,140 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using NodaTime;
namespace Squidex.Infrastructure.Queries
{
public sealed class ClrValue
{
public static readonly ClrValue Null = new ClrValue(null, ClrValueType.Null, false);
public object Value { get; }
public ClrValueType ValueType { get; }
public bool IsList { get; }
private ClrValue(object value, ClrValueType valueType, bool isList)
{
Value = value;
ValueType = valueType;
IsList = isList;
}
public static implicit operator ClrValue(Instant value)
{
return new ClrValue(value, ClrValueType.Instant, false);
}
public static implicit operator ClrValue(Guid value)
{
return new ClrValue(value, ClrValueType.Guid, false);
}
public static implicit operator ClrValue(bool value)
{
return new ClrValue(value, ClrValueType.Boolean, false);
}
public static implicit operator ClrValue(float value)
{
return new ClrValue(value, ClrValueType.Single, false);
}
public static implicit operator ClrValue(double value)
{
return new ClrValue(value, ClrValueType.Double, false);
}
public static implicit operator ClrValue(int value)
{
return new ClrValue(value, ClrValueType.Int32, false);
}
public static implicit operator ClrValue(long value)
{
return new ClrValue(value, ClrValueType.Int64, false);
}
public static implicit operator ClrValue(string value)
{
return value != null ? new ClrValue(value, ClrValueType.String, false) : Null;
}
public static implicit operator ClrValue(List<Instant> value)
{
return value != null ? new ClrValue(value, ClrValueType.Instant, true) : Null;
}
public static implicit operator ClrValue(List<Guid> value)
{
return value != null ? new ClrValue(value, ClrValueType.Guid, true) : Null;
}
public static implicit operator ClrValue(List<bool> value)
{
return value != null ? new ClrValue(value, ClrValueType.Boolean, true) : Null;
}
public static implicit operator ClrValue(List<float> value)
{
return value != null ? new ClrValue(value, ClrValueType.Single, true) : Null;
}
public static implicit operator ClrValue(List<double> value)
{
return value != null ? new ClrValue(value, ClrValueType.Double, true) : Null;
}
public static implicit operator ClrValue(List<int> value)
{
return value != null ? new ClrValue(value, ClrValueType.Int32, true) : Null;
}
public static implicit operator ClrValue(List<long> value)
{
return value != null ? new ClrValue(value, ClrValueType.Int64, true) : Null;
}
public static implicit operator ClrValue(List<string> value)
{
return value != null ? new ClrValue(value, ClrValueType.String, true) : Null;
}
public override string ToString()
{
if (Value is IList list)
{
return $"[{string.Join(", ", list.OfType<object>().Select(ToString).ToArray())}]";
}
return ToString(Value);
}
private static string ToString(object value)
{
if (value == null)
{
return "null";
}
if (value is string s)
{
return $"'{s.Replace("'", "\\'")}'";
}
return string.Format(CultureInfo.InvariantCulture, "{0}", value);
}
}
}

2
src/Squidex.Infrastructure/Queries/FilterValueType.cs → src/Squidex.Infrastructure/Queries/ClrValueType.cs

@ -7,7 +7,7 @@
namespace Squidex.Infrastructure.Queries namespace Squidex.Infrastructure.Queries
{ {
public enum FilterValueType public enum ClrValueType
{ {
Boolean, Boolean,
Guid, Guid,

67
src/Squidex.Infrastructure/Queries/CompareFilter.cs

@ -0,0 +1,67 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Infrastructure.Queries
{
public sealed class CompareFilter<TValue> : FilterNode<TValue>
{
public PropertyPath Path { get; }
public CompareOperator Operator { get; }
public TValue Value { get; }
public CompareFilter(PropertyPath path, CompareOperator @operator, TValue value)
{
Guard.NotNull(path, nameof(path));
Guard.NotNull(value, nameof(value));
Guard.Enum(@operator, nameof(@operator));
Path = path;
Operator = @operator;
Value = value;
}
public override T Accept<T>(FilterNodeVisitor<T, TValue> visitor)
{
return visitor.Visit(this);
}
public override string ToString()
{
switch (Operator)
{
case CompareOperator.Contains:
return $"contains({Path}, {Value})";
case CompareOperator.Empty:
return $"empty({Path})";
case CompareOperator.EndsWith:
return $"endsWith({Path}, {Value})";
case CompareOperator.StartsWith:
return $"startsWith({Path}, {Value})";
case CompareOperator.Equals:
return $"{Path} == {Value}";
case CompareOperator.NotEquals:
return $"{Path} != {Value}";
case CompareOperator.GreaterThan:
return $"{Path} > {Value}";
case CompareOperator.GreaterThanOrEqual:
return $"{Path} >= {Value}";
case CompareOperator.LessThan:
return $"{Path} < {Value}";
case CompareOperator.LessThanOrEqual:
return $"{Path} <= {Value}";
case CompareOperator.In:
return $"{Path} in {Value}";
default:
return string.Empty;
}
}
}
}

2
src/Squidex.Infrastructure/Queries/FilterOperator.cs → src/Squidex.Infrastructure/Queries/CompareOperator.cs

@ -7,7 +7,7 @@
namespace Squidex.Infrastructure.Queries namespace Squidex.Infrastructure.Queries
{ {
public enum FilterOperator public enum CompareOperator
{ {
Contains, Contains,
Empty, Empty,

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

@ -1,85 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Linq;
using NodaTime;
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, string value)
{
return Binary(path, FilterOperator.Equals, value);
}
public static FilterComparison Eq(string path, bool value)
{
return Binary(path, FilterOperator.Equals, value);
}
public static FilterComparison Eq(string path, long value)
{
return Binary(path, FilterOperator.Equals, value);
}
public static FilterComparison Eq(string path, int value)
{
return Binary(path, FilterOperator.Equals, value);
}
public static FilterComparison Eq(string path, Instant value)
{
return Binary(path, FilterOperator.Equals, value);
}
public static FilterComparison Empty(string path)
{
return new FilterComparison(path.Split('.', '/'), FilterOperator.Empty, FilterValue.Null);
}
public static FilterComparison In(string path, params long[] value)
{
return new FilterComparison(path.Split('.', '/'), FilterOperator.In, new FilterValue(value.ToList()));
}
private static FilterComparison Binary(string path, FilterOperator @operator, string value)
{
return new FilterComparison(path.Split('.', '/'), @operator, new FilterValue(value));
}
private static FilterComparison Binary(string path, FilterOperator @operator, bool value)
{
return new FilterComparison(path.Split('.', '/'), @operator, new FilterValue(value));
}
private static FilterComparison Binary(string path, FilterOperator @operator, long value)
{
return new FilterComparison(path.Split('.', '/'), @operator, new FilterValue(value));
}
private static FilterComparison Binary(string path, FilterOperator @operator, int value)
{
return new FilterComparison(path.Split('.', '/'), @operator, new FilterValue(value));
}
private static FilterComparison Binary(string path, FilterOperator @operator, Instant value)
{
return new FilterComparison(path.Split('.', '/'), @operator, new FilterValue(value));
}
}
}

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

@ -1,70 +0,0 @@
// ==========================================================================
// 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> Lhs { get; }
public FilterOperator Operator { get; }
public FilterValue Rhs { get; }
public FilterComparison(IReadOnlyList<string> lhs, FilterOperator @operator, FilterValue rhs)
{
Guard.NotNull(lhs, nameof(lhs));
Guard.NotEmpty(lhs, nameof(lhs));
Guard.Enum(@operator, nameof(@operator));
Lhs = lhs;
Rhs = rhs;
Operator = @operator;
}
public override T Accept<T>(FilterNodeVisitor<T> visitor)
{
return visitor.Visit(this);
}
public override string ToString()
{
var path = string.Join(".", Lhs);
switch (Operator)
{
case FilterOperator.Contains:
return $"contains({path}, {Rhs})";
case FilterOperator.Empty:
return $"empty({path})";
case FilterOperator.EndsWith:
return $"endsWith({path}, {Rhs})";
case FilterOperator.StartsWith:
return $"startsWith({path}, {Rhs})";
case FilterOperator.Equals:
return $"{path} == {Rhs}";
case FilterOperator.NotEquals:
return $"{path} != {Rhs}";
case FilterOperator.GreaterThan:
return $"{path} > {Rhs}";
case FilterOperator.GreaterThanOrEqual:
return $"{path} >= {Rhs}";
case FilterOperator.LessThan:
return $"{path} < {Rhs}";
case FilterOperator.LessThanOrEqual:
return $"{path} <= {Rhs}";
case FilterOperator.In:
return $"{path} in {Rhs}";
default:
return string.Empty;
}
}
}
}

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

@ -1,45 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
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)})";
}
}
}

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

@ -7,8 +7,8 @@
namespace Squidex.Infrastructure.Queries namespace Squidex.Infrastructure.Queries
{ {
public abstract class FilterNode public abstract class FilterNode<TValue>
{ {
public abstract T Accept<T>(FilterNodeVisitor<T> visitor); public abstract T Accept<T>(FilterNodeVisitor<T, TValue> visitor);
} }
} }

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

@ -11,19 +11,19 @@ using System;
namespace Squidex.Infrastructure.Queries namespace Squidex.Infrastructure.Queries
{ {
public abstract class FilterNodeVisitor<T> public abstract class FilterNodeVisitor<T, TValue>
{ {
public virtual T Visit(FilterComparison nodeIn) public virtual T Visit(CompareFilter<TValue> nodeIn)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public virtual T Visit(FilterJunction nodeIn) public virtual T Visit(LogicalFilter<TValue> nodeIn)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public virtual T Visit(FilterNegate nodeIn) public virtual T Visit(NegateFilter<TValue> nodeIn)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }

149
src/Squidex.Infrastructure/Queries/FilterValue.cs

@ -1,149 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using NodaTime;
namespace Squidex.Infrastructure.Queries
{
public sealed class FilterValue
{
public static readonly FilterValue Null = new FilterValue(null, FilterValueType.Null, false);
public object Value { get; }
public FilterValueType ValueType { get; }
public bool IsList { get; }
public FilterValue(Guid value)
: this(value, FilterValueType.Guid, false)
{
}
public FilterValue(Instant value)
: this(value, FilterValueType.Instant, false)
{
}
public FilterValue(bool value)
: this(value, FilterValueType.Boolean, false)
{
}
public FilterValue(float value)
: this(value, FilterValueType.Single, false)
{
}
public FilterValue(double value)
: this(value, FilterValueType.Double, false)
{
}
public FilterValue(int value)
: this(value, FilterValueType.Int32, false)
{
}
public FilterValue(long value)
: this(value, FilterValueType.Int64, false)
{
}
public FilterValue(string value)
: this(value, FilterValueType.String, false)
{
Guard.NotNull(value, nameof(value));
}
public FilterValue(List<Guid> value)
: this(value, FilterValueType.Guid, true)
{
Guard.NotNull(value, nameof(value));
}
public FilterValue(List<Instant> value)
: this(value, FilterValueType.Instant, true)
{
Guard.NotNull(value, nameof(value));
}
public FilterValue(List<bool> value)
: this(value, FilterValueType.Boolean, true)
{
Guard.NotNull(value, nameof(value));
}
public FilterValue(List<float> value)
: this(value, FilterValueType.Single, true)
{
Guard.NotNull(value, nameof(value));
}
public FilterValue(List<double> value)
: this(value, FilterValueType.Double, true)
{
Guard.NotNull(value, nameof(value));
}
public FilterValue(List<int> value)
: this(value, FilterValueType.Int32, true)
{
Guard.NotNull(value, nameof(value));
}
public FilterValue(List<long> value)
: this(value, FilterValueType.Int64, true)
{
Guard.NotNull(value, nameof(value));
}
public FilterValue(List<string> value)
: this(value, FilterValueType.String, true)
{
Guard.NotNull(value, nameof(value));
}
private FilterValue(object value, FilterValueType valueType, bool isList)
{
Value = value;
ValueType = valueType;
IsList = isList;
}
public override string ToString()
{
if (Value is IList list)
{
return $"[{string.Join(", ", list.OfType<object>().Select(ToString).ToArray())}]";
}
return ToString(Value);
}
private static string ToString(object value)
{
if (value == null)
{
return "null";
}
if (value is string s)
{
return $"'{s.Replace("'", "\\'")}'";
}
return string.Format(CultureInfo.InvariantCulture, "{0}", value);
}
}
}

162
src/Squidex.Infrastructure/Queries/Json/FilterConverter.cs

@ -0,0 +1,162 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Squidex.Infrastructure.Json.Newtonsoft;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Infrastructure.Queries.Json
{
public sealed class FilterConverter : JsonClassConverter<FilterNode<IJsonValue>>
{
public override IEnumerable<Type> SupportedTypes
{
get
{
yield return typeof(CompareFilter<IJsonValue>);
yield return typeof(FilterNode<IJsonValue>);
yield return typeof(LogicalFilter<IJsonValue>);
yield return typeof(NegateFilter<IJsonValue>);
}
}
public override bool CanConvert(Type objectType)
{
return SupportedTypes.Contains(objectType);
}
protected override FilterNode<IJsonValue> ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer)
{
if (reader.TokenType != JsonToken.StartObject)
{
throw new JsonException($"Expected StartObject, but got {reader.TokenType}.");
}
FilterNode<IJsonValue> result = null;
var comparePath = (PropertyPath)null;
var compareOperator = (CompareOperator)99;
var compareValue = (IJsonValue)null;
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonToken.PropertyName:
string propertyName = reader.Value.ToString();
if (!reader.Read())
{
throw new JsonSerializationException("Unexpected end when reading filter.");
}
if (result != null)
{
throw new JsonSerializationException($"Unexpected property {propertyName}");
}
switch (propertyName.ToLowerInvariant())
{
case "not":
var filter = serializer.Deserialize<FilterNode<IJsonValue>>(reader);
result = new NegateFilter<IJsonValue>(filter);
break;
case "and":
var andFilters = serializer.Deserialize<List<FilterNode<IJsonValue>>>(reader);
result = new LogicalFilter<IJsonValue>(LogicalFilterType.And, andFilters);
break;
case "or":
var orFilters = serializer.Deserialize<List<FilterNode<IJsonValue>>>(reader);
result = new LogicalFilter<IJsonValue>(LogicalFilterType.Or, orFilters);
break;
case "path":
comparePath = serializer.Deserialize<PropertyPath>(reader);
break;
case "op":
compareOperator = ReadOperator(reader, serializer);
break;
case "value":
compareValue = serializer.Deserialize<IJsonValue>(reader);
break;
}
break;
case JsonToken.Comment:
break;
case JsonToken.EndObject:
if (result != null)
{
return result;
}
if (comparePath == null)
{
throw new JsonSerializationException("Path not defined.");
}
if (compareValue == null && compareOperator != CompareOperator.Empty)
{
throw new JsonSerializationException("Value not defined.");
}
if (!compareOperator.IsEnumValue())
{
throw new JsonSerializationException("Operator not defined.");
}
return new CompareFilter<IJsonValue>(comparePath, compareOperator, compareValue ?? JsonValue.Null);
}
}
throw new JsonSerializationException("Unexpected end when reading filter.");
}
private static CompareOperator ReadOperator(JsonReader reader, JsonSerializer serializer)
{
var value = serializer.Deserialize<string>(reader);
switch (value.ToLowerInvariant())
{
case "eq":
return CompareOperator.Equals;
case "ne":
return CompareOperator.NotEquals;
case "lt":
return CompareOperator.LessThan;
case "le":
return CompareOperator.LessThanOrEqual;
case "gt":
return CompareOperator.GreaterThan;
case "ge":
return CompareOperator.GreaterThanOrEqual;
case "empty":
return CompareOperator.Empty;
case "contains":
return CompareOperator.Contains;
case "endswith":
return CompareOperator.EndsWith;
case "startswith":
return CompareOperator.StartsWith;
case "in":
return CompareOperator.In;
}
throw new JsonSerializationException($"Unexpected compare operator, got {value}.");
}
protected override void WriteValue(JsonWriter writer, FilterNode<IJsonValue> value, JsonSerializer serializer)
{
throw new NotSupportedException();
}
}
}

29
src/Squidex.Infrastructure/Queries/Json/PropertyPathConverter.cs

@ -0,0 +1,29 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Squidex.Infrastructure.Json.Newtonsoft;
namespace Squidex.Infrastructure.Queries.Json
{
public sealed class PropertyPathConverter : JsonClassConverter<PropertyPath>
{
protected override PropertyPath ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer)
{
var value = serializer.Deserialize<string>(reader);
return value;
}
protected override void WriteValue(JsonWriter writer, PropertyPath value, JsonSerializer serializer)
{
serializer.Serialize(writer, (IEnumerable<string>)value);
}
}
}

39
src/Squidex.Infrastructure/Queries/LogicalFilter.cs

@ -0,0 +1,39 @@
// ==========================================================================
// 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 LogicalFilter<TValue> : FilterNode<TValue>
{
public IReadOnlyList<FilterNode<TValue>> Filters { get; }
public LogicalFilterType Type { get; }
public LogicalFilter(LogicalFilterType type, IReadOnlyList<FilterNode<TValue>> filters)
{
Guard.NotNull(filters, nameof(filters));
Guard.GreaterEquals(filters.Count, 2, nameof(filters.Count));
Guard.Enum(type, nameof(type));
Filters = filters;
Type = type;
}
public override T Accept<T>(FilterNodeVisitor<T, TValue> visitor)
{
return visitor.Visit(this);
}
public override string ToString()
{
return $"({string.Join(Type == LogicalFilterType.And ? " && " : " || ", Filters)})";
}
}
}

2
src/Squidex.Infrastructure/Queries/FilterJunctionType.cs → src/Squidex.Infrastructure/Queries/LogicalFilterType.cs

@ -7,7 +7,7 @@
namespace Squidex.Infrastructure.Queries namespace Squidex.Infrastructure.Queries
{ {
public enum FilterJunctionType public enum LogicalFilterType
{ {
And, And,
Or Or

14
src/Squidex.Infrastructure/Queries/FilterNegate.cs → src/Squidex.Infrastructure/Queries/NegateFilter.cs

@ -7,25 +7,25 @@
namespace Squidex.Infrastructure.Queries namespace Squidex.Infrastructure.Queries
{ {
public sealed class FilterNegate : FilterNode public sealed class NegateFilter<TValue> : FilterNode<TValue>
{ {
public FilterNode Operand { get; } public FilterNode<TValue> Filter { get; }
public FilterNegate(FilterNode operand) public NegateFilter(FilterNode<TValue> filter)
{ {
Guard.NotNull(operand, nameof(operand)); Guard.NotNull(filter, nameof(filter));
Operand = operand; Filter = filter;
} }
public override T Accept<T>(FilterNodeVisitor<T> visitor) public override T Accept<T>(FilterNodeVisitor<T, TValue> visitor)
{ {
return visitor.Visit(this); return visitor.Visit(this);
} }
public override string ToString() public override string ToString()
{ {
return $"!({Operand})"; return $"!({Filter})";
} }
} }
} }

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

@ -15,7 +15,7 @@ using NodaTime.Text;
namespace Squidex.Infrastructure.Queries.OData namespace Squidex.Infrastructure.Queries.OData
{ {
public sealed class ConstantWithTypeVisitor : QueryNodeVisitor<FilterValue> public sealed class ConstantWithTypeVisitor : QueryNodeVisitor<ClrValue>
{ {
private static readonly IEdmPrimitiveType BooleanType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Boolean); private static readonly IEdmPrimitiveType BooleanType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Boolean);
private static readonly IEdmPrimitiveType DateTimeType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.DateTimeOffset); private static readonly IEdmPrimitiveType DateTimeType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.DateTimeOffset);
@ -32,117 +32,117 @@ namespace Squidex.Infrastructure.Queries.OData
{ {
} }
public static FilterValue Visit(QueryNode node) public static ClrValue Visit(QueryNode node)
{ {
return node.Accept(Instance); return node.Accept(Instance);
} }
public override FilterValue Visit(ConvertNode nodeIn) public override ClrValue Visit(ConvertNode nodeIn)
{ {
if (nodeIn.TypeReference.Definition == BooleanType) if (nodeIn.TypeReference.Definition == BooleanType)
{ {
var value = ConstantVisitor.Visit(nodeIn.Source); var value = ConstantVisitor.Visit(nodeIn.Source);
return new FilterValue(bool.Parse(value.ToString())); return bool.Parse(value.ToString());
} }
if (nodeIn.TypeReference.Definition == GuidType) if (nodeIn.TypeReference.Definition == GuidType)
{ {
var value = ConstantVisitor.Visit(nodeIn.Source); var value = ConstantVisitor.Visit(nodeIn.Source);
return new FilterValue(Guid.Parse(value.ToString())); return Guid.Parse(value.ToString());
} }
if (nodeIn.TypeReference.Definition == DateTimeType) if (nodeIn.TypeReference.Definition == DateTimeType)
{ {
var value = ConstantVisitor.Visit(nodeIn.Source); var value = ConstantVisitor.Visit(nodeIn.Source);
return new FilterValue(ParseInstant(value)); return ParseInstant(value);
} }
if (ConstantVisitor.Visit(nodeIn.Source) == null) if (ConstantVisitor.Visit(nodeIn.Source) == null)
{ {
return FilterValue.Null; return ClrValue.Null;
} }
throw new NotSupportedException(); throw new NotSupportedException();
} }
public override FilterValue Visit(CollectionConstantNode nodeIn) public override ClrValue Visit(CollectionConstantNode nodeIn)
{ {
if (nodeIn.ItemType.Definition == DateTimeType) if (nodeIn.ItemType.Definition == DateTimeType)
{ {
return new FilterValue(nodeIn.Collection.Select(x => ParseInstant(x.Value)).ToList()); return nodeIn.Collection.Select(x => ParseInstant(x.Value)).ToList();
} }
if (nodeIn.ItemType.Definition == GuidType) if (nodeIn.ItemType.Definition == GuidType)
{ {
return new FilterValue(nodeIn.Collection.Select(x => (Guid)x.Value).ToList()); return nodeIn.Collection.Select(x => (Guid)x.Value).ToList();
} }
if (nodeIn.ItemType.Definition == BooleanType) if (nodeIn.ItemType.Definition == BooleanType)
{ {
return new FilterValue(nodeIn.Collection.Select(x => (bool)x.Value).ToList()); return nodeIn.Collection.Select(x => (bool)x.Value).ToList();
} }
if (nodeIn.ItemType.Definition == SingleType) if (nodeIn.ItemType.Definition == SingleType)
{ {
return new FilterValue(nodeIn.Collection.Select(x => (float)x.Value).ToList()); return nodeIn.Collection.Select(x => (float)x.Value).ToList();
} }
if (nodeIn.ItemType.Definition == DoubleType) if (nodeIn.ItemType.Definition == DoubleType)
{ {
return new FilterValue(nodeIn.Collection.Select(x => (double)x.Value).ToList()); return nodeIn.Collection.Select(x => (double)x.Value).ToList();
} }
if (nodeIn.ItemType.Definition == Int32Type) if (nodeIn.ItemType.Definition == Int32Type)
{ {
return new FilterValue(nodeIn.Collection.Select(x => (int)x.Value).ToList()); return nodeIn.Collection.Select(x => (int)x.Value).ToList();
} }
if (nodeIn.ItemType.Definition == Int64Type) if (nodeIn.ItemType.Definition == Int64Type)
{ {
return new FilterValue(nodeIn.Collection.Select(x => (long)x.Value).ToList()); return nodeIn.Collection.Select(x => (long)x.Value).ToList();
} }
if (nodeIn.ItemType.Definition == StringType) if (nodeIn.ItemType.Definition == StringType)
{ {
return new FilterValue(nodeIn.Collection.Select(x => (string)x.Value).ToList()); return nodeIn.Collection.Select(x => (string)x.Value).ToList();
} }
throw new NotSupportedException(); throw new NotSupportedException();
} }
public override FilterValue Visit(ConstantNode nodeIn) public override ClrValue Visit(ConstantNode nodeIn)
{ {
if (nodeIn.TypeReference.Definition == BooleanType) if (nodeIn.TypeReference.Definition == BooleanType)
{ {
return new FilterValue((bool)nodeIn.Value); return (bool)nodeIn.Value;
} }
if (nodeIn.TypeReference.Definition == SingleType) if (nodeIn.TypeReference.Definition == SingleType)
{ {
return new FilterValue((float)nodeIn.Value); return (float)nodeIn.Value;
} }
if (nodeIn.TypeReference.Definition == DoubleType) if (nodeIn.TypeReference.Definition == DoubleType)
{ {
return new FilterValue((double)nodeIn.Value); return (double)nodeIn.Value;
} }
if (nodeIn.TypeReference.Definition == Int32Type) if (nodeIn.TypeReference.Definition == Int32Type)
{ {
return new FilterValue((int)nodeIn.Value); return (int)nodeIn.Value;
} }
if (nodeIn.TypeReference.Definition == Int64Type) if (nodeIn.TypeReference.Definition == Int64Type)
{ {
return new FilterValue((long)nodeIn.Value); return (long)nodeIn.Value;
} }
if (nodeIn.TypeReference.Definition == StringType) if (nodeIn.TypeReference.Definition == StringType)
{ {
return new FilterValue((string)nodeIn.Value); return (string)nodeIn.Value;
} }
throw new NotSupportedException(); throw new NotSupportedException();

4
src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs

@ -43,9 +43,9 @@ namespace Squidex.Infrastructure.Queries.OData
return parser; return parser;
} }
public static Query ToQuery(this ODataUriParser parser) public static ClrQuery ToQuery(this ODataUriParser parser)
{ {
var query = new Query(); var query = new ClrQuery();
if (parser != null) if (parser != null)
{ {

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

@ -12,7 +12,7 @@ namespace Squidex.Infrastructure.Queries.OData
{ {
public static class FilterBuilder public static class FilterBuilder
{ {
public static void ParseFilter(this ODataUriParser query, Query result) public static void ParseFilter(this ODataUriParser query, ClrQuery result)
{ {
SearchClause search; SearchClause search;
try try

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

@ -11,7 +11,7 @@ using Microsoft.OData.UriParser;
namespace Squidex.Infrastructure.Queries.OData namespace Squidex.Infrastructure.Queries.OData
{ {
public sealed class FilterVisitor : QueryNodeVisitor<FilterNode> public sealed class FilterVisitor : QueryNodeVisitor<FilterNode<ClrValue>>
{ {
private static readonly FilterVisitor Instance = new FilterVisitor(); private static readonly FilterVisitor Instance = new FilterVisitor();
@ -19,40 +19,40 @@ namespace Squidex.Infrastructure.Queries.OData
{ {
} }
public static FilterNode Visit(QueryNode node) public static FilterNode<ClrValue> Visit(QueryNode node)
{ {
return node.Accept(Instance); return node.Accept(Instance);
} }
public override FilterNode Visit(ConvertNode nodeIn) public override FilterNode<ClrValue> Visit(ConvertNode nodeIn)
{ {
return nodeIn.Source.Accept(this); return nodeIn.Source.Accept(this);
} }
public override FilterNode Visit(UnaryOperatorNode nodeIn) public override FilterNode<ClrValue> Visit(UnaryOperatorNode nodeIn)
{ {
if (nodeIn.OperatorKind == UnaryOperatorKind.Not) if (nodeIn.OperatorKind == UnaryOperatorKind.Not)
{ {
return new FilterNegate(nodeIn.Operand.Accept(this)); return ClrFilter.Not(nodeIn.Operand.Accept(this));
} }
throw new NotSupportedException(); throw new NotSupportedException();
} }
public override FilterNode Visit(InNode nodeIn) public override FilterNode<ClrValue> Visit(InNode nodeIn)
{ {
var value = ConstantWithTypeVisitor.Visit(nodeIn.Right); var value = ConstantWithTypeVisitor.Visit(nodeIn.Right);
return new FilterComparison(PropertyPathVisitor.Visit(nodeIn.Left), FilterOperator.In, value); return ClrFilter.In(PropertyPathVisitor.Visit(nodeIn.Left), value);
} }
public override FilterNode Visit(SingleValueFunctionCallNode nodeIn) public override FilterNode<ClrValue> Visit(SingleValueFunctionCallNode nodeIn)
{ {
var fieldNode = nodeIn.Parameters.ElementAt(0); var fieldNode = nodeIn.Parameters.ElementAt(0);
if (string.Equals(nodeIn.Name, "empty", StringComparison.OrdinalIgnoreCase)) if (string.Equals(nodeIn.Name, "empty", StringComparison.OrdinalIgnoreCase))
{ {
return new FilterComparison(PropertyPathVisitor.Visit(fieldNode), FilterOperator.Empty, FilterValue.Null); return ClrFilter.Empty(PropertyPathVisitor.Visit(fieldNode));
} }
var valueNode = nodeIn.Parameters.ElementAt(1); var valueNode = nodeIn.Parameters.ElementAt(1);
@ -61,36 +61,36 @@ namespace Squidex.Infrastructure.Queries.OData
{ {
var value = ConstantWithTypeVisitor.Visit(valueNode); var value = ConstantWithTypeVisitor.Visit(valueNode);
return new FilterComparison(PropertyPathVisitor.Visit(fieldNode), FilterOperator.EndsWith, value); return ClrFilter.EndsWith(PropertyPathVisitor.Visit(fieldNode), value);
} }
if (string.Equals(nodeIn.Name, "startswith", StringComparison.OrdinalIgnoreCase)) if (string.Equals(nodeIn.Name, "startswith", StringComparison.OrdinalIgnoreCase))
{ {
var value = ConstantWithTypeVisitor.Visit(valueNode); var value = ConstantWithTypeVisitor.Visit(valueNode);
return new FilterComparison(PropertyPathVisitor.Visit(fieldNode), FilterOperator.StartsWith, value); return ClrFilter.StartsWith(PropertyPathVisitor.Visit(fieldNode), value);
} }
if (string.Equals(nodeIn.Name, "contains", StringComparison.OrdinalIgnoreCase)) if (string.Equals(nodeIn.Name, "contains", StringComparison.OrdinalIgnoreCase))
{ {
var value = ConstantWithTypeVisitor.Visit(valueNode); var value = ConstantWithTypeVisitor.Visit(valueNode);
return new FilterComparison(PropertyPathVisitor.Visit(fieldNode), FilterOperator.Contains, value); return ClrFilter.Contains(PropertyPathVisitor.Visit(fieldNode), value);
} }
throw new NotSupportedException(); throw new NotSupportedException();
} }
public override FilterNode Visit(BinaryOperatorNode nodeIn) public override FilterNode<ClrValue> Visit(BinaryOperatorNode nodeIn)
{ {
if (nodeIn.OperatorKind == BinaryOperatorKind.And) if (nodeIn.OperatorKind == BinaryOperatorKind.And)
{ {
return new FilterJunction(FilterJunctionType.And, nodeIn.Left.Accept(this), nodeIn.Right.Accept(this)); return ClrFilter.And(nodeIn.Left.Accept(this), nodeIn.Right.Accept(this));
} }
if (nodeIn.OperatorKind == BinaryOperatorKind.Or) if (nodeIn.OperatorKind == BinaryOperatorKind.Or)
{ {
return new FilterJunction(FilterJunctionType.Or, nodeIn.Left.Accept(this), nodeIn.Right.Accept(this)); return ClrFilter.Or(nodeIn.Left.Accept(this), nodeIn.Right.Accept(this));
} }
if (nodeIn.Left is SingleValueFunctionCallNode functionNode) if (nodeIn.Left is SingleValueFunctionCallNode functionNode)
@ -99,12 +99,12 @@ namespace Squidex.Infrastructure.Queries.OData
var value = ConstantWithTypeVisitor.Visit(nodeIn.Right); var value = ConstantWithTypeVisitor.Visit(nodeIn.Right);
if (value.ValueType == FilterValueType.Boolean && value.Value is bool booleanRight) if (value.ValueType == ClrValueType.Boolean && value.Value is bool booleanRight)
{ {
if ((nodeIn.OperatorKind == BinaryOperatorKind.Equal && !booleanRight) || if ((nodeIn.OperatorKind == BinaryOperatorKind.Equal && !booleanRight) ||
(nodeIn.OperatorKind == BinaryOperatorKind.NotEqual && booleanRight)) (nodeIn.OperatorKind == BinaryOperatorKind.NotEqual && booleanRight))
{ {
regexFilter = new FilterNegate(regexFilter); regexFilter = ClrFilter.Not(regexFilter);
} }
return regexFilter; return regexFilter;
@ -116,42 +116,42 @@ namespace Squidex.Infrastructure.Queries.OData
{ {
var value = ConstantWithTypeVisitor.Visit(nodeIn.Right); var value = ConstantWithTypeVisitor.Visit(nodeIn.Right);
return new FilterComparison(PropertyPathVisitor.Visit(nodeIn.Left), FilterOperator.NotEquals, value); return ClrFilter.Ne(PropertyPathVisitor.Visit(nodeIn.Left), value);
} }
if (nodeIn.OperatorKind == BinaryOperatorKind.Equal) if (nodeIn.OperatorKind == BinaryOperatorKind.Equal)
{ {
var value = ConstantWithTypeVisitor.Visit(nodeIn.Right); var value = ConstantWithTypeVisitor.Visit(nodeIn.Right);
return new FilterComparison(PropertyPathVisitor.Visit(nodeIn.Left), FilterOperator.Equals, value); return ClrFilter.Eq(PropertyPathVisitor.Visit(nodeIn.Left), value);
} }
if (nodeIn.OperatorKind == BinaryOperatorKind.LessThan) if (nodeIn.OperatorKind == BinaryOperatorKind.LessThan)
{ {
var value = ConstantWithTypeVisitor.Visit(nodeIn.Right); var value = ConstantWithTypeVisitor.Visit(nodeIn.Right);
return new FilterComparison(PropertyPathVisitor.Visit(nodeIn.Left), FilterOperator.LessThan, value); return ClrFilter.Lt(PropertyPathVisitor.Visit(nodeIn.Left), value);
} }
if (nodeIn.OperatorKind == BinaryOperatorKind.LessThanOrEqual) if (nodeIn.OperatorKind == BinaryOperatorKind.LessThanOrEqual)
{ {
var value = ConstantWithTypeVisitor.Visit(nodeIn.Right); var value = ConstantWithTypeVisitor.Visit(nodeIn.Right);
return new FilterComparison(PropertyPathVisitor.Visit(nodeIn.Left), FilterOperator.LessThanOrEqual, value); return ClrFilter.Le(PropertyPathVisitor.Visit(nodeIn.Left), value);
} }
if (nodeIn.OperatorKind == BinaryOperatorKind.GreaterThan) if (nodeIn.OperatorKind == BinaryOperatorKind.GreaterThan)
{ {
var value = ConstantWithTypeVisitor.Visit(nodeIn.Right); var value = ConstantWithTypeVisitor.Visit(nodeIn.Right);
return new FilterComparison(PropertyPathVisitor.Visit(nodeIn.Left), FilterOperator.GreaterThan, value); return ClrFilter.Gt(PropertyPathVisitor.Visit(nodeIn.Left), value);
} }
if (nodeIn.OperatorKind == BinaryOperatorKind.GreaterThanOrEqual) if (nodeIn.OperatorKind == BinaryOperatorKind.GreaterThanOrEqual)
{ {
var value = ConstantWithTypeVisitor.Visit(nodeIn.Right); var value = ConstantWithTypeVisitor.Visit(nodeIn.Right);
return new FilterComparison(PropertyPathVisitor.Visit(nodeIn.Left), FilterOperator.GreaterThanOrEqual, value); return ClrFilter.Ge(PropertyPathVisitor.Visit(nodeIn.Left), value);
} }
} }

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

@ -11,7 +11,7 @@ namespace Squidex.Infrastructure.Queries.OData
{ {
public static class LimitExtensions public static class LimitExtensions
{ {
public static void ParseTake(this ODataUriParser query, Query result) public static void ParseTake(this ODataUriParser query, ClrQuery result)
{ {
var top = query.ParseTop(); var top = query.ParseTop();
@ -21,7 +21,7 @@ namespace Squidex.Infrastructure.Queries.OData
} }
} }
public static void ParseSkip(this ODataUriParser query, Query result) public static void ParseSkip(this ODataUriParser query, ClrQuery result)
{ {
var skip = query.ParseSkip(); var skip = query.ParseSkip();

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

@ -11,7 +11,7 @@ namespace Squidex.Infrastructure.Queries.OData
{ {
public static class SortBuilder public static class SortBuilder
{ {
public static void ParseSort(this ODataUriParser query, Query result) public static void ParseSort(this ODataUriParser query, ClrQuery result)
{ {
var orderBy = query.ParseOrderBy(); var orderBy = query.ParseOrderBy();

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

@ -9,22 +9,22 @@ using System.Linq;
namespace Squidex.Infrastructure.Queries namespace Squidex.Infrastructure.Queries
{ {
public sealed class PascalCasePathConverter : TransformVisitor public sealed class PascalCasePathConverter<TValue> : TransformVisitor<TValue>
{ {
private static readonly PascalCasePathConverter Instance = new PascalCasePathConverter(); private static readonly PascalCasePathConverter<TValue> Instance = new PascalCasePathConverter<TValue>();
private PascalCasePathConverter() private PascalCasePathConverter()
{ {
} }
public static FilterNode Transform(FilterNode node) public static FilterNode<TValue> Transform(FilterNode<TValue> node)
{ {
return node.Accept(Instance); return node.Accept(Instance);
} }
public override FilterNode Visit(FilterComparison nodeIn) public override FilterNode<TValue> Visit(CompareFilter<TValue> nodeIn)
{ {
return new FilterComparison(nodeIn.Lhs.Select(x => x.ToPascalCase()).ToList(), nodeIn.Operator, nodeIn.Rhs); return new CompareFilter<TValue>(nodeIn.Path.Select(x => x.ToPascalCase()).ToList(), nodeIn.Operator, nodeIn.Value);
} }
} }
} }

54
src/Squidex.Infrastructure/Queries/PropertyPath.cs

@ -0,0 +1,54 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Linq;
namespace Squidex.Infrastructure.Queries
{
public sealed class PropertyPath : ReadOnlyCollection<string>
{
private static readonly char[] Separators = { '.', '/' };
public PropertyPath(IList<string> items)
: base(items)
{
if (items.Count == 0)
{
throw new ArgumentException("Path cannot be empty.", nameof(items));
}
}
public static implicit operator PropertyPath(string path)
{
return new PropertyPath(path?.Split(Separators, StringSplitOptions.RemoveEmptyEntries)?.ToList());
}
public static implicit operator PropertyPath(string[] path)
{
return new PropertyPath(path?.ToList());
}
public static implicit operator PropertyPath(List<string> path)
{
return new PropertyPath(path);
}
public static implicit operator PropertyPath(ImmutableList<string> path)
{
return new PropertyPath(path);
}
public override string ToString()
{
return string.Join(".", this);
}
}
}

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

@ -9,9 +9,9 @@ using System.Collections.Generic;
namespace Squidex.Infrastructure.Queries namespace Squidex.Infrastructure.Queries
{ {
public sealed class Query public class Query<TValue>
{ {
public FilterNode Filter { get; set; } public FilterNode<TValue> Filter { get; set; }
public string FullText { get; set; } public string FullText { get; set; }

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

@ -5,20 +5,17 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.Generic;
namespace Squidex.Infrastructure.Queries namespace Squidex.Infrastructure.Queries
{ {
public sealed class SortNode public sealed class SortNode
{ {
public IReadOnlyList<string> Path { get; } public PropertyPath Path { get; }
public SortOrder SortOrder { get; set; } public SortOrder SortOrder { get; set; }
public SortNode(IReadOnlyList<string> path, SortOrder sortOrder) public SortNode(PropertyPath path, SortOrder sortOrder)
{ {
Guard.NotNull(path, nameof(path)); Guard.NotNull(path, nameof(path));
Guard.NotEmpty(path, nameof(path));
Guard.Enum(sortOrder, nameof(sortOrder)); Guard.Enum(sortOrder, nameof(sortOrder));
Path = path; Path = path;

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

@ -9,21 +9,21 @@ using System.Linq;
namespace Squidex.Infrastructure.Queries namespace Squidex.Infrastructure.Queries
{ {
public abstract class TransformVisitor : FilterNodeVisitor<FilterNode> public abstract class TransformVisitor<TValue> : FilterNodeVisitor<FilterNode<TValue>, TValue>
{ {
public override FilterNode Visit(FilterComparison nodeIn) public override FilterNode<TValue> Visit(CompareFilter<TValue> nodeIn)
{ {
return nodeIn; return nodeIn;
} }
public override FilterNode Visit(FilterJunction nodeIn) public override FilterNode<TValue> Visit(LogicalFilter<TValue> nodeIn)
{ {
return new FilterJunction(nodeIn.JunctionType, nodeIn.Operands.Select(x => x.Accept(this)).ToList()); return new LogicalFilter<TValue>(nodeIn.Type, nodeIn.Filters.Select(x => x.Accept(this)).ToList());
} }
public override FilterNode Visit(FilterNegate nodeIn) public override FilterNode<TValue> Visit(NegateFilter<TValue> nodeIn)
{ {
return new FilterNegate(nodeIn.Operand.Accept(this)); return new NegateFilter<TValue>(nodeIn.Filter.Accept(this));
} }
} }
} }

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

@ -115,7 +115,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
var enriched1 = new AssetEntity(); var enriched1 = new AssetEntity();
var enriched2 = new AssetEntity(); var enriched2 = new AssetEntity();
A.CallTo(() => assetRepository.QueryAsync(appId.Id, A<Query>.Ignored)) A.CallTo(() => assetRepository.QueryAsync(appId.Id, A<ClrQuery>.Ignored))
.Returns(ResultList.CreateFrom(8, found1, found2)); .Returns(ResultList.CreateFrom(8, found1, found2));
A.CallTo(() => assetEnricher.EnrichAsync(A<IEnumerable<IAssetEntity>>.That.IsSameSequenceAs(found1, found2))) A.CallTo(() => assetEnricher.EnrichAsync(A<IEnumerable<IAssetEntity>>.That.IsSameSequenceAs(found1, found2)))
@ -135,7 +135,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
await sut.QueryAsync(requestContext, query); await sut.QueryAsync(requestContext, query);
A.CallTo(() => assetRepository.QueryAsync(appId.Id, A<Query>.That.Is("FullText: 'Hello World'; Take: 100; Sort: fileName Ascending"))) A.CallTo(() => assetRepository.QueryAsync(appId.Id, A<ClrQuery>.That.Is("FullText: 'Hello World'; Take: 100; Sort: fileName Ascending")))
.MustHaveHappened(); .MustHaveHappened();
} }
@ -146,7 +146,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
await sut.QueryAsync(requestContext, query); await sut.QueryAsync(requestContext, query);
A.CallTo(() => assetRepository.QueryAsync(appId.Id, A<Query>.That.Is("Filter: fileName == 'ABC'; Take: 200; Sort: lastModified Descending"))) A.CallTo(() => assetRepository.QueryAsync(appId.Id, A<ClrQuery>.That.Is("Filter: fileName == 'ABC'; Take: 200; Sort: lastModified Descending")))
.MustHaveHappened(); .MustHaveHappened();
} }
@ -157,7 +157,8 @@ namespace Squidex.Domain.Apps.Entities.Assets
await sut.QueryAsync(requestContext, query); await sut.QueryAsync(requestContext, query);
A.CallTo(() => assetRepository.QueryAsync(appId.Id, A<Query>.That.Is("Take: 30; Sort: lastModified Descending"))) A.CallTo(() => assetRepository.QueryAsync(appId.Id,
A<ClrQuery>.That.Is("Take: 30; Sort: lastModified Descending")))
.MustHaveHappened(); .MustHaveHappened();
} }
@ -168,7 +169,8 @@ namespace Squidex.Domain.Apps.Entities.Assets
await sut.QueryAsync(requestContext, query); await sut.QueryAsync(requestContext, query);
A.CallTo(() => assetRepository.QueryAsync(appId.Id, A<Query>.That.Is("Skip: 20; Take: 200; Sort: lastModified Descending"))) A.CallTo(() => assetRepository.QueryAsync(appId.Id,
A<ClrQuery>.That.Is("Skip: 20; Take: 200; Sort: lastModified Descending")))
.MustHaveHappened(); .MustHaveHappened();
} }
} }

42
tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/MongoDbQueryTests.cs

@ -17,7 +17,7 @@ using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.MongoDb.Queries; using Squidex.Infrastructure.MongoDb.Queries;
using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Queries;
using Xunit; using Xunit;
using FilterBuilder = Squidex.Infrastructure.Queries.FilterBuilder; using ClrFilter = Squidex.Infrastructure.Queries.ClrFilter;
using SortBuilder = Squidex.Infrastructure.Queries.SortBuilder; using SortBuilder = Squidex.Infrastructure.Queries.SortBuilder;
namespace Squidex.Domain.Apps.Entities.Assets.MongoDb namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
@ -35,13 +35,13 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
[Fact] [Fact]
public void Should_throw_exception_for_full_text_search() public void Should_throw_exception_for_full_text_search()
{ {
Assert.Throws<ValidationException>(() => Q(new Query { FullText = "Full Text" })); Assert.Throws<ValidationException>(() => Q(new ClrQuery { FullText = "Full Text" }));
} }
[Fact] [Fact]
public void Should_make_query_with_lastModified() public void Should_make_query_with_lastModified()
{ {
var i = F(FilterBuilder.Eq("lastModified", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value)); var i = F(ClrFilter.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);
@ -50,7 +50,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
[Fact] [Fact]
public void Should_make_query_with_lastModifiedBy() public void Should_make_query_with_lastModifiedBy()
{ {
var i = F(FilterBuilder.Eq("lastModifiedBy", "Me")); var i = F(ClrFilter.Eq("lastModifiedBy", "Me"));
var o = C("{ 'LastModifiedBy' : 'Me' }"); var o = C("{ 'LastModifiedBy' : 'Me' }");
Assert.Equal(o, i); Assert.Equal(o, i);
@ -59,7 +59,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
[Fact] [Fact]
public void Should_make_query_with_created() public void Should_make_query_with_created()
{ {
var i = F(FilterBuilder.Eq("created", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value)); var i = F(ClrFilter.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);
@ -68,7 +68,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
[Fact] [Fact]
public void Should_make_query_with_createdBy() public void Should_make_query_with_createdBy()
{ {
var i = F(FilterBuilder.Eq("createdBy", "Me")); var i = F(ClrFilter.Eq("createdBy", "Me"));
var o = C("{ 'CreatedBy' : 'Me' }"); var o = C("{ 'CreatedBy' : 'Me' }");
Assert.Equal(o, i); Assert.Equal(o, i);
@ -77,7 +77,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
[Fact] [Fact]
public void Should_make_query_with_version() public void Should_make_query_with_version()
{ {
var i = F(FilterBuilder.Eq("version", 0)); var i = F(ClrFilter.Eq("version", 0));
var o = C("{ 'Version' : NumberLong(0) }"); var o = C("{ 'Version' : NumberLong(0) }");
Assert.Equal(o, i); Assert.Equal(o, i);
@ -86,7 +86,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
[Fact] [Fact]
public void Should_make_query_with_fileVersion() public void Should_make_query_with_fileVersion()
{ {
var i = F(FilterBuilder.Eq("fileVersion", 2)); var i = F(ClrFilter.Eq("fileVersion", 2));
var o = C("{ 'FileVersion' : NumberLong(2) }"); var o = C("{ 'FileVersion' : NumberLong(2) }");
Assert.Equal(o, i); Assert.Equal(o, i);
@ -95,7 +95,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
[Fact] [Fact]
public void Should_make_query_with_tags() public void Should_make_query_with_tags()
{ {
var i = F(FilterBuilder.Eq("tags", "tag1")); var i = F(ClrFilter.Eq("tags", "tag1"));
var o = C("{ 'Tags' : 'tag1' }"); var o = C("{ 'Tags' : 'tag1' }");
Assert.Equal(o, i); Assert.Equal(o, i);
@ -104,7 +104,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
[Fact] [Fact]
public void Should_make_query_with_fileName() public void Should_make_query_with_fileName()
{ {
var i = F(FilterBuilder.Eq("fileName", "Logo.png")); var i = F(ClrFilter.Eq("fileName", "Logo.png"));
var o = C("{ 'FileName' : 'Logo.png' }"); var o = C("{ 'FileName' : 'Logo.png' }");
Assert.Equal(o, i); Assert.Equal(o, i);
@ -113,7 +113,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
[Fact] [Fact]
public void Should_make_query_with_isImage() public void Should_make_query_with_isImage()
{ {
var i = F(FilterBuilder.Eq("isImage", true)); var i = F(ClrFilter.Eq("isImage", true));
var o = C("{ 'IsImage' : true }"); var o = C("{ 'IsImage' : true }");
Assert.Equal(o, i); Assert.Equal(o, i);
@ -122,7 +122,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
[Fact] [Fact]
public void Should_make_query_with_mimeType() public void Should_make_query_with_mimeType()
{ {
var i = F(FilterBuilder.Eq("mimeType", "text/json")); var i = F(ClrFilter.Eq("mimeType", "text/json"));
var o = C("{ 'MimeType' : 'text/json' }"); var o = C("{ 'MimeType' : 'text/json' }");
Assert.Equal(o, i); Assert.Equal(o, i);
@ -131,7 +131,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
[Fact] [Fact]
public void Should_make_query_with_fileSize() public void Should_make_query_with_fileSize()
{ {
var i = F(FilterBuilder.Eq("fileSize", 1024)); var i = F(ClrFilter.Eq("fileSize", 1024));
var o = C("{ 'FileSize' : NumberLong(1024) }"); var o = C("{ 'FileSize' : NumberLong(1024) }");
Assert.Equal(o, i); Assert.Equal(o, i);
@ -140,7 +140,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
[Fact] [Fact]
public void Should_make_query_with_pixelHeight() public void Should_make_query_with_pixelHeight()
{ {
var i = F(FilterBuilder.Eq("pixelHeight", 600)); var i = F(ClrFilter.Eq("pixelHeight", 600));
var o = C("{ 'PixelHeight' : 600 }"); var o = C("{ 'PixelHeight' : 600 }");
Assert.Equal(o, i); Assert.Equal(o, i);
@ -149,7 +149,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
[Fact] [Fact]
public void Should_make_query_with_pixelWidth() public void Should_make_query_with_pixelWidth()
{ {
var i = F(FilterBuilder.Eq("pixelWidth", 800)); var i = F(ClrFilter.Eq("pixelWidth", 800));
var o = C("{ 'PixelWidth' : 800 }"); var o = C("{ 'PixelWidth' : 800 }");
Assert.Equal(o, i); Assert.Equal(o, i);
@ -176,7 +176,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
[Fact] [Fact]
public void Should_make_take_statement() public void Should_make_take_statement()
{ {
var query = new Query { Take = 3 }; var query = new ClrQuery { Take = 3 };
var cursor = A.Fake<IFindFluent<MongoAssetEntity, MongoAssetEntity>>(); var cursor = A.Fake<IFindFluent<MongoAssetEntity, MongoAssetEntity>>();
cursor.AssetTake(query.AdjustToModel()); cursor.AssetTake(query.AdjustToModel());
@ -188,7 +188,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
[Fact] [Fact]
public void Should_make_skip_statement() public void Should_make_skip_statement()
{ {
var query = new Query { Skip = 3 }; var query = new ClrQuery { Skip = 3 };
var cursor = A.Fake<IFindFluent<MongoAssetEntity, MongoAssetEntity>>(); var cursor = A.Fake<IFindFluent<MongoAssetEntity, MongoAssetEntity>>();
cursor.AssetSkip(query.AdjustToModel()); cursor.AssetSkip(query.AdjustToModel());
@ -202,9 +202,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
return value.Replace('\'', '"'); return value.Replace('\'', '"');
} }
private static string F(FilterNode filter) private static string F(FilterNode<ClrValue> filter)
{ {
return Q(new Query { Filter = filter }); return Q(new ClrQuery { Filter = filter });
} }
private static string S(params SortNode[] sorts) private static string S(params SortNode[] sorts)
@ -219,12 +219,12 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
i = sortDefinition.Render(Serializer, Registry).ToString(); i = sortDefinition.Render(Serializer, Registry).ToString();
}); });
cursor.AssetSort(new Query { Sort = sorts.ToList() }.AdjustToModel()); cursor.AssetSort(new ClrQuery { Sort = sorts.ToList() }.AdjustToModel());
return i; return i;
} }
private static string Q(Query query) private static string Q(ClrQuery query)
{ {
var rendered = var rendered =
query.AdjustToModel().BuildFilter<MongoAssetEntity>(false).Filter query.AdjustToModel().BuildFilter<MongoAssetEntity>(false).Filter

6
tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/FilterTagTransformerTests.cs

@ -25,7 +25,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
A.CallTo(() => tagService.GetTagIdsAsync(appId, TagGroups.Assets, A<HashSet<string>>.That.Contains("name1"))) A.CallTo(() => tagService.GetTagIdsAsync(appId, TagGroups.Assets, A<HashSet<string>>.That.Contains("name1")))
.Returns(new Dictionary<string, string> { ["name1"] = "id1" }); .Returns(new Dictionary<string, string> { ["name1"] = "id1" });
var source = FilterBuilder.Eq("tags", "name1"); var source = ClrFilter.Eq("tags", "name1");
var result = FilterTagTransformer.Transform(source, appId, tagService); var result = FilterTagTransformer.Transform(source, appId, tagService);
Assert.Equal("tags == 'id1'", result.ToString()); Assert.Equal("tags == 'id1'", result.ToString());
@ -37,7 +37,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
A.CallTo(() => tagService.GetTagIdsAsync(appId, TagGroups.Assets, A<HashSet<string>>.That.Contains("name1"))) A.CallTo(() => tagService.GetTagIdsAsync(appId, TagGroups.Assets, A<HashSet<string>>.That.Contains("name1")))
.Returns(new Dictionary<string, string>()); .Returns(new Dictionary<string, string>());
var source = FilterBuilder.Eq("tags", "name1"); var source = ClrFilter.Eq("tags", "name1");
var result = FilterTagTransformer.Transform(source, appId, tagService); var result = FilterTagTransformer.Transform(source, appId, tagService);
Assert.Equal("tags == 'name1'", result.ToString()); Assert.Equal("tags == 'name1'", result.ToString());
@ -46,7 +46,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
[Fact] [Fact]
public void Should_not_normalize_other_field() public void Should_not_normalize_other_field()
{ {
var source = FilterBuilder.Eq("other", "value"); var source = ClrFilter.Eq("other", "value");
var result = FilterTagTransformer.Transform(source, appId, tagService); var result = FilterTagTransformer.Transform(source, appId, tagService);
Assert.Equal("other == 'value'", result.ToString()); Assert.Equal("other == 'value'", result.ToString());

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

@ -149,7 +149,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
await sut.QueryAsync(requestContext, schemaId.Name, query); await sut.QueryAsync(requestContext, schemaId.Name, query);
A.CallTo(() => contentRepository.QueryAsync(app, schema, A<Status[]>.That.Is(Status.Published), false, A.CallTo(() => contentRepository.QueryAsync(app, schema, A<Status[]>.That.Is(Status.Published), false,
A<Query>.That.Is("Take: 30; Sort: lastModified Descending"), false)) A<ClrQuery>.That.Is("Take: 30; Sort: lastModified Descending"), false))
.MustHaveHappened(); .MustHaveHappened();
} }
@ -166,7 +166,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
await sut.QueryAsync(requestContext, schemaId.Name, query); await sut.QueryAsync(requestContext, schemaId.Name, query);
A.CallTo(() => contentRepository.QueryAsync(app, schema, A<Status[]>.That.Is(status), false, A.CallTo(() => contentRepository.QueryAsync(app, schema, A<Status[]>.That.Is(status), false,
A<Query>.That.Is("Skip: 20; Take: 200; Sort: lastModified Descending"), false)) A<ClrQuery>.That.Is("Skip: 20; Take: 200; Sort: lastModified Descending"), false))
.MustHaveHappened(); .MustHaveHappened();
} }
@ -482,7 +482,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
private void SetupContents(Status[] status, int count, int total, IContentEntity content, bool inDraft, bool includeDraft) private void SetupContents(Status[] status, int count, int total, IContentEntity content, bool inDraft, bool includeDraft)
{ {
A.CallTo(() => contentRepository.QueryAsync(app, schema, A<Status[]>.That.Is(status), inDraft, A<Query>.Ignored, includeDraft)) A.CallTo(() => contentRepository.QueryAsync(app, schema, A<Status[]>.That.Is(status), inDraft, A<ClrQuery>.Ignored, includeDraft))
.Returns(ResultList.Create(total, Enumerable.Repeat(content, count))); .Returns(ResultList.Create(total, Enumerable.Repeat(content, count)));
} }

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

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using FakeItEasy; using FakeItEasy;
using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization;
@ -23,7 +24,7 @@ using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.MongoDb.Queries; using Squidex.Infrastructure.MongoDb.Queries;
using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Queries;
using Xunit; using Xunit;
using FilterBuilder = Squidex.Infrastructure.Queries.FilterBuilder; using ClrFilter = Squidex.Infrastructure.Queries.ClrFilter;
using SortBuilder = Squidex.Infrastructure.Queries.SortBuilder; using SortBuilder = Squidex.Infrastructure.Queries.SortBuilder;
namespace Squidex.Domain.Apps.Entities.Contents.MongoDb namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
@ -78,13 +79,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[Fact] [Fact]
public void Should_throw_exception_for_invalid_field() public void Should_throw_exception_for_invalid_field()
{ {
Assert.Throws<NotSupportedException>(() => F(FilterBuilder.Eq("data/invalid/iv", "Me"))); Assert.Throws<NotSupportedException>(() => F(ClrFilter.Eq("data/invalid/iv", "Me")));
} }
[Fact] [Fact]
public void Should_make_query_with_lastModified() public void Should_make_query_with_lastModified()
{ {
var i = F(FilterBuilder.Eq("lastModified", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value)); var i = F(ClrFilter.Eq("lastModified", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value));
var o = C("{ 'mt' : '1988-01-19T12:00:00Z' }"); var o = C("{ 'mt' : '1988-01-19T12:00:00Z' }");
Assert.Equal(o, i); Assert.Equal(o, i);
@ -93,7 +94,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[Fact] [Fact]
public void Should_make_query_with_lastModifiedBy() public void Should_make_query_with_lastModifiedBy()
{ {
var i = F(FilterBuilder.Eq("lastModifiedBy", "Me")); var i = F(ClrFilter.Eq("lastModifiedBy", "Me"));
var o = C("{ 'mb' : 'Me' }"); var o = C("{ 'mb' : 'Me' }");
Assert.Equal(o, i); Assert.Equal(o, i);
@ -102,7 +103,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[Fact] [Fact]
public void Should_make_query_with_created() public void Should_make_query_with_created()
{ {
var i = F(FilterBuilder.Eq("created", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value)); var i = F(ClrFilter.Eq("created", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value));
var o = C("{ 'ct' : '1988-01-19T12:00:00Z' }"); var o = C("{ 'ct' : '1988-01-19T12:00:00Z' }");
Assert.Equal(o, i); Assert.Equal(o, i);
@ -111,7 +112,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[Fact] [Fact]
public void Should_make_query_with_createdBy() public void Should_make_query_with_createdBy()
{ {
var i = F(FilterBuilder.Eq("createdBy", "Me")); var i = F(ClrFilter.Eq("createdBy", "Me"));
var o = C("{ 'cb' : 'Me' }"); var o = C("{ 'cb' : 'Me' }");
Assert.Equal(o, i); Assert.Equal(o, i);
@ -120,7 +121,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[Fact] [Fact]
public void Should_make_query_with_version() public void Should_make_query_with_version()
{ {
var i = F(FilterBuilder.Eq("version", 0L)); var i = F(ClrFilter.Eq("version", 0L));
var o = C("{ 'vs' : NumberLong(0) }"); var o = C("{ 'vs' : NumberLong(0) }");
Assert.Equal(o, i); Assert.Equal(o, i);
@ -129,7 +130,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[Fact] [Fact]
public void Should_make_query_with_version_and_list() public void Should_make_query_with_version_and_list()
{ {
var i = F(FilterBuilder.In("version", 0L, 2L, 5L)); var i = F(ClrFilter.In("version", new List<long> { 0L, 2L, 5L }));
var o = C("{ 'vs' : { '$in' : [NumberLong(0), NumberLong(2), NumberLong(5)] } }"); var o = C("{ 'vs' : { '$in' : [NumberLong(0), NumberLong(2), NumberLong(5)] } }");
Assert.Equal(o, i); Assert.Equal(o, i);
@ -138,7 +139,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[Fact] [Fact]
public void Should_make_query_from_draft() public void Should_make_query_from_draft()
{ {
var i = F(FilterBuilder.Eq("data/dashed_field/iv", "Value"), true); var i = F(ClrFilter.Eq("data/dashed_field/iv", "Value"), true);
var o = C("{ 'dd.8.iv' : 'Value' }"); var o = C("{ 'dd.8.iv' : 'Value' }");
Assert.Equal(o, i); Assert.Equal(o, i);
@ -147,7 +148,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[Fact] [Fact]
public void Should_make_query_with_empty_test() public void Should_make_query_with_empty_test()
{ {
var i = F(FilterBuilder.Empty("data/firstName/iv"), true); var i = F(ClrFilter.Empty("data/firstName/iv"), true);
var o = C("{ '$or' : [{ 'dd.1.iv' : { '$exists' : false } }, { 'dd.1.iv' : null }, { 'dd.1.iv' : '' }, { 'dd.1.iv' : [] }] }"); var o = C("{ '$or' : [{ 'dd.1.iv' : { '$exists' : false } }, { 'dd.1.iv' : null }, { 'dd.1.iv' : '' }, { 'dd.1.iv' : [] }] }");
Assert.Equal(o, i); Assert.Equal(o, i);
@ -156,7 +157,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[Fact] [Fact]
public void Should_make_query_with_datetime_data() public void Should_make_query_with_datetime_data()
{ {
var i = F(FilterBuilder.Eq("data/birthday/iv", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value)); var i = F(ClrFilter.Eq("data/birthday/iv", InstantPattern.General.Parse("1988-01-19T12:00:00Z").Value));
var o = C("{ 'do.5.iv' : '1988-01-19T12:00:00Z' }"); var o = C("{ 'do.5.iv' : '1988-01-19T12:00:00Z' }");
Assert.Equal(o, i); Assert.Equal(o, i);
@ -165,7 +166,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[Fact] [Fact]
public void Should_make_query_with_underscore_field() public void Should_make_query_with_underscore_field()
{ {
var i = F(FilterBuilder.Eq("data/dashed_field/iv", "Value")); var i = F(ClrFilter.Eq("data/dashed_field/iv", "Value"));
var o = C("{ 'do.8.iv' : 'Value' }"); var o = C("{ 'do.8.iv' : 'Value' }");
Assert.Equal(o, i); Assert.Equal(o, i);
@ -174,7 +175,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[Fact] [Fact]
public void Should_make_query_with_references_equals() public void Should_make_query_with_references_equals()
{ {
var i = F(FilterBuilder.Eq("data/friends/iv", "guid")); var i = F(ClrFilter.Eq("data/friends/iv", "guid"));
var o = C("{ 'do.7.iv' : 'guid' }"); var o = C("{ 'do.7.iv' : 'guid' }");
Assert.Equal(o, i); Assert.Equal(o, i);
@ -183,7 +184,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[Fact] [Fact]
public void Should_make_query_with_array_field() public void Should_make_query_with_array_field()
{ {
var i = F(FilterBuilder.Eq("data/hobbies/iv/name", "PC")); var i = F(ClrFilter.Eq("data/hobbies/iv/name", "PC"));
var o = C("{ 'do.9.iv.91' : 'PC' }"); var o = C("{ 'do.9.iv.91' : 'PC' }");
Assert.Equal(o, i); Assert.Equal(o, i);
@ -192,7 +193,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[Fact] [Fact]
public void Should_make_query_with_assets_equals() public void Should_make_query_with_assets_equals()
{ {
var i = F(FilterBuilder.Eq("data/pictures/iv", "guid")); var i = F(ClrFilter.Eq("data/pictures/iv", "guid"));
var o = C("{ 'do.6.iv' : 'guid' }"); var o = C("{ 'do.6.iv' : 'guid' }");
Assert.Equal(o, i); Assert.Equal(o, i);
@ -201,7 +202,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[Fact] [Fact]
public void Should_make_query_with_full_text() public void Should_make_query_with_full_text()
{ {
var i = Q(new Query { FullText = "Hello my World" }); var i = Q(new ClrQuery { FullText = "Hello my World" });
var o = C("{ '$text' : { '$search' : 'Hello my World' } }"); var o = C("{ '$text' : { '$search' : 'Hello my World' } }");
Assert.Equal(o, i); Assert.Equal(o, i);
@ -228,7 +229,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[Fact] [Fact]
public void Should_make_take_statement() public void Should_make_take_statement()
{ {
var query = new Query { Take = 3 }; var query = new ClrQuery { Take = 3 };
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>(); var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>();
cursor.ContentTake(query.AdjustToModel(schemaDef, false)); cursor.ContentTake(query.AdjustToModel(schemaDef, false));
@ -240,7 +241,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
[Fact] [Fact]
public void Should_make_skip_statement() public void Should_make_skip_statement()
{ {
var query = new Query { Skip = 3 }; var query = new ClrQuery { Skip = 3 };
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>(); var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>();
cursor.ContentSkip(query.AdjustToModel(schemaDef, false)); cursor.ContentSkip(query.AdjustToModel(schemaDef, false));
@ -254,9 +255,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
return value.Replace('\'', '"'); return value.Replace('\'', '"');
} }
private string F(FilterNode filter, bool useDraft = false) private string F(FilterNode<ClrValue> filter, bool useDraft = false)
{ {
return Q(new Query { Filter = filter }, useDraft); return Q(new ClrQuery { Filter = filter }, useDraft);
} }
private string S(params SortNode[] sorts) private string S(params SortNode[] sorts)
@ -271,12 +272,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
i = sortDefinition.Render(Serializer, Registry).ToString(); i = sortDefinition.Render(Serializer, Registry).ToString();
}); });
cursor.ContentSort(new Query { Sort = sorts.ToList() }.AdjustToModel(schemaDef, false)); cursor.ContentSort(new ClrQuery { Sort = sorts.ToList() }.AdjustToModel(schemaDef, false));
return i; return i;
} }
private string Q(Query query, bool useDraft = false) private string Q(ClrQuery query, bool useDraft = false)
{ {
var rendered = var rendered =
query.AdjustToModel(schemaDef, useDraft).BuildFilter<MongoContentEntity>().Filter query.AdjustToModel(schemaDef, useDraft).BuildFilter<MongoContentEntity>().Filter

10
tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/FilterTagTransformerTests.cs

@ -43,7 +43,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, TagGroups.Schemas(schemaId.Id), A<HashSet<string>>.That.Contains("name1"))) A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, TagGroups.Schemas(schemaId.Id), A<HashSet<string>>.That.Contains("name1")))
.Returns(new Dictionary<string, string> { ["name1"] = "id1" }); .Returns(new Dictionary<string, string> { ["name1"] = "id1" });
var source = FilterBuilder.Eq("data.tags2.iv", "name1"); var source = ClrFilter.Eq("data.tags2.iv", "name1");
var result = FilterTagTransformer.Transform(source, appId.Id, schema, tagService); var result = FilterTagTransformer.Transform(source, appId.Id, schema, tagService);
Assert.Equal("data.tags2.iv == 'id1'", result.ToString()); Assert.Equal("data.tags2.iv == 'id1'", result.ToString());
@ -55,7 +55,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, TagGroups.Assets, A<HashSet<string>>.That.Contains("name1"))) A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, TagGroups.Assets, A<HashSet<string>>.That.Contains("name1")))
.Returns(new Dictionary<string, string>()); .Returns(new Dictionary<string, string>());
var source = FilterBuilder.Eq("data.tags2.iv", "name1"); var source = ClrFilter.Eq("data.tags2.iv", "name1");
var result = FilterTagTransformer.Transform(source, appId.Id, schema, tagService); var result = FilterTagTransformer.Transform(source, appId.Id, schema, tagService);
Assert.Equal("data.tags2.iv == 'name1'", result.ToString()); Assert.Equal("data.tags2.iv == 'name1'", result.ToString());
@ -64,7 +64,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
[Fact] [Fact]
public void Should_not_normalize_other_tags_field() public void Should_not_normalize_other_tags_field()
{ {
var source = FilterBuilder.Eq("data.tags1.iv", "value"); var source = ClrFilter.Eq("data.tags1.iv", "value");
var result = FilterTagTransformer.Transform(source, appId.Id, schema, tagService); var result = FilterTagTransformer.Transform(source, appId.Id, schema, tagService);
Assert.Equal("data.tags1.iv == 'value'", result.ToString()); Assert.Equal("data.tags1.iv == 'value'", result.ToString());
@ -76,7 +76,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
[Fact] [Fact]
public void Should_not_normalize_other_typed_field() public void Should_not_normalize_other_typed_field()
{ {
var source = FilterBuilder.Eq("data.string.iv", "value"); var source = ClrFilter.Eq("data.string.iv", "value");
var result = FilterTagTransformer.Transform(source, appId.Id, schema, tagService); var result = FilterTagTransformer.Transform(source, appId.Id, schema, tagService);
Assert.Equal("data.string.iv == 'value'", result.ToString()); Assert.Equal("data.string.iv == 'value'", result.ToString());
@ -88,7 +88,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
[Fact] [Fact]
public void Should_not_normalize_non_data_field() public void Should_not_normalize_non_data_field()
{ {
var source = FilterBuilder.Eq("no.data", "value"); var source = ClrFilter.Eq("no.data", "value");
var result = FilterTagTransformer.Transform(source, appId.Id, schema, tagService); var result = FilterTagTransformer.Transform(source, appId.Id, schema, tagService);
Assert.Equal("no.data == 'value'", result.ToString()); Assert.Equal("no.data == 'value'", result.ToString());

2
tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs

@ -12,7 +12,7 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers
{ {
public static class AExtensions public static class AExtensions
{ {
public static Query Is(this INegatableArgumentConstraintManager<Query> that, string query) public static ClrQuery Is(this INegatableArgumentConstraintManager<ClrQuery> that, string query)
{ {
return that.Matches(x => x.ToString() == query); return that.Matches(x => x.ToString() == query);
} }

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

@ -14,8 +14,8 @@ namespace Squidex.Infrastructure.Queries
[Fact] [Fact]
public void Should_convert_property() public void Should_convert_property()
{ {
var source = FilterBuilder.Eq("property", 1); var source = ClrFilter.Eq("property", 1);
var result = PascalCasePathConverter.Transform(source); var result = PascalCasePathConverter<ClrValue>.Transform(source);
Assert.Equal("Property == 1", result.ToString()); Assert.Equal("Property == 1", result.ToString());
} }
@ -23,8 +23,8 @@ namespace Squidex.Infrastructure.Queries
[Fact] [Fact]
public void Should_convert_properties() public void Should_convert_properties()
{ {
var source = FilterBuilder.Eq("root.child", 1); var source = ClrFilter.Eq("root.child", 1);
var result = PascalCasePathConverter.Transform(source); var result = PascalCasePathConverter<ClrValue>.Transform(source);
Assert.Equal("Root.Child == 1", result.ToString()); Assert.Equal("Root.Child == 1", result.ToString());
} }

181
tests/Squidex.Infrastructure.Tests/Queries/QueryJsonTests.cs

@ -0,0 +1,181 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Newtonsoft.Json;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.TestHelpers;
using Xunit;
namespace Squidex.Infrastructure.Queries
{
public class QueryJsonTests
{
[Theory]
[InlineData("eq", "property == 12")]
[InlineData("ne", "property != 12")]
[InlineData("le", "property <= 12")]
[InlineData("lt", "property < 12")]
[InlineData("ge", "property >= 12")]
[InlineData("gt", "property > 12")]
[InlineData("contains", "contains(property, 12)")]
[InlineData("endswith", "endsWith(property, 12)")]
[InlineData("startswith", "startsWith(property, 12)")]
public void Should_convert_comparison(string op, string expected)
{
var json = new
{
path = "property",
op,
value = 12
};
var filter = SerializeAndDeserialize(json);
Assert.Equal(expected, filter.ToString());
}
[Fact]
public void Should_convert_comparison_empty()
{
var json = new { path = "property", op = "empty" };
var filter = SerializeAndDeserialize(json);
Assert.Equal("empty(property)", filter.ToString());
}
[Fact]
public void Should_convert_comparison_in()
{
var json = new { path = "property", op = "in", value = new[] { 12, 13 } };
var filter = SerializeAndDeserialize(json);
Assert.Equal("property in [12, 13]", filter.ToString());
}
[Fact]
public void Should_convert_comparison_with_deep_path()
{
var json = new { path = "property.nested", op = "eq", value = 12 };
var filter = SerializeAndDeserialize(json);
Assert.Equal("property.nested == 12", filter.ToString());
}
[Fact]
public void Should_convert_logical_and()
{
var json = new
{
and = new[]
{
new { path = "property", op = "ge", value = 10 },
new { path = "property", op = "lt", value = 20 }
}
};
var filter = SerializeAndDeserialize(json);
Assert.Equal("(property >= 10 && property < 20)", filter.ToString());
}
[Fact]
public void Should_convert_logical_or()
{
var json = new
{
or = new[]
{
new { path = "property", op = "ge", value = 10 },
new { path = "property", op = "lt", value = 20 }
}
};
var filter = SerializeAndDeserialize(json);
Assert.Equal("(property >= 10 || property < 20)", filter.ToString());
}
[Fact]
public void Should_convert_logical_not()
{
var json = new
{
not = new { path = "property", op = "ge", value = 10 }
};
var filter = SerializeAndDeserialize(json);
Assert.Equal("!(property >= 10)", filter.ToString());
}
[Fact]
public void Should_throw_exception_for_invalid_operator()
{
var json = new { path = "property", op = "invalid", value = 12 };
Assert.ThrowsAny<JsonException>(() => SerializeAndDeserialize(json));
}
[Fact]
public void Should_throw_exception_for_missing_path()
{
var json = new { op = "invalid", value = 12 };
Assert.ThrowsAny<JsonException>(() => SerializeAndDeserialize(json));
}
[Fact]
public void Should_throw_exception_for_missing_operator()
{
var json = new { path = "property", value = 12 };
Assert.ThrowsAny<JsonException>(() => SerializeAndDeserialize(json));
}
[Fact]
public void Should_throw_exception_for_missing_value()
{
var json = new { path = "property", op = "invalid" };
Assert.ThrowsAny<JsonException>(() => SerializeAndDeserialize(json));
}
[Fact]
public void Should_throw_exception_for_invalid_property()
{
var json = new { path = "property", op = "invalid", value = 12, other = 4 };
Assert.ThrowsAny<JsonException>(() => SerializeAndDeserialize(json));
}
[Fact]
public void Should_throw_exception_for_invalid_property_after_filter()
{
var json = new
{
and = new[]
{
new { path = "property", op = "ge", value = 10 },
new { path = "property", op = "lt", value = 20 }
},
additional = 1
};
Assert.ThrowsAny<JsonException>(() => SerializeAndDeserialize(json));
}
private static FilterNode<IJsonValue> SerializeAndDeserialize<T>(T value)
{
var json = JsonHelper.DefaultSerializer.Serialize(value, true);
return JsonHelper.DefaultSerializer.Deserialize<FilterNode<IJsonValue>>(json);
}
}
}

4
tests/Squidex.Infrastructure.Tests/Queries/ODataConversionTests.cs → tests/Squidex.Infrastructure.Tests/Queries/QueryODataConversionTests.cs

@ -11,11 +11,11 @@ using Xunit;
namespace Squidex.Infrastructure.Queries namespace Squidex.Infrastructure.Queries
{ {
public class ODataConversionTests public class QueryODataConversionTests
{ {
private static readonly IEdmModel EdmModel; private static readonly IEdmModel EdmModel;
static ODataConversionTests() static QueryODataConversionTests()
{ {
var entityType = new EdmEntityType("Squidex", "Users"); var entityType = new EdmEntityType("Squidex", "Users");

3
tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs

@ -10,6 +10,7 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Converters; using Newtonsoft.Json.Converters;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Json.Newtonsoft; using Squidex.Infrastructure.Json.Newtonsoft;
using Squidex.Infrastructure.Queries.Json;
namespace Squidex.Infrastructure.TestHelpers namespace Squidex.Infrastructure.TestHelpers
{ {
@ -34,11 +35,13 @@ namespace Squidex.Infrastructure.TestHelpers
new ClaimsPrincipalConverter(), new ClaimsPrincipalConverter(),
new InstantConverter(), new InstantConverter(),
new EnvelopeHeadersConverter(), new EnvelopeHeadersConverter(),
new FilterConverter(),
new JsonValueConverter(), new JsonValueConverter(),
new LanguageConverter(), new LanguageConverter(),
new NamedGuidIdConverter(), new NamedGuidIdConverter(),
new NamedLongIdConverter(), new NamedLongIdConverter(),
new NamedStringIdConverter(), new NamedStringIdConverter(),
new PropertyPathConverter(),
new RefTokenConverter(), new RefTokenConverter(),
new StringEnumConverter()), new StringEnumConverter()),

Loading…
Cancel
Save