mirror of https://github.com/Squidex/squidex.git
59 changed files with 1987 additions and 437 deletions
@ -0,0 +1,173 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using Microsoft.OData; |
||||
|
using Microsoft.OData.Edm; |
||||
|
using NJsonSchema; |
||||
|
using Squidex.Domain.Apps.Core.Tags; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Json; |
||||
|
using Squidex.Infrastructure.Log; |
||||
|
using Squidex.Infrastructure.Queries; |
||||
|
using Squidex.Infrastructure.Queries.Json; |
||||
|
using Squidex.Infrastructure.Queries.OData; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Assets.Queries |
||||
|
{ |
||||
|
public class AssetQueryParser |
||||
|
{ |
||||
|
private readonly JsonSchema jsonSchema = BuildJsonSchema(); |
||||
|
private readonly IEdmModel edmModel = BuildEdmModel(); |
||||
|
private readonly IJsonSerializer jsonSerializer; |
||||
|
private readonly ITagService tagService; |
||||
|
private readonly AssetOptions options; |
||||
|
|
||||
|
public AssetQueryParser(IJsonSerializer jsonSerializer, ITagService tagService, IOptions<AssetOptions> options) |
||||
|
{ |
||||
|
Guard.NotNull(jsonSerializer, nameof(jsonSerializer)); |
||||
|
Guard.NotNull(options, nameof(options)); |
||||
|
Guard.NotNull(tagService, nameof(tagService)); |
||||
|
|
||||
|
this.jsonSerializer = jsonSerializer; |
||||
|
this.options = options.Value; |
||||
|
this.tagService = tagService; |
||||
|
} |
||||
|
|
||||
|
public virtual ClrQuery ParseQuery(Context context, Q q) |
||||
|
{ |
||||
|
Guard.NotNull(context, nameof(context)); |
||||
|
|
||||
|
using (Profiler.TraceMethod<AssetQueryParser>()) |
||||
|
{ |
||||
|
var result = new ClrQuery(); |
||||
|
|
||||
|
if (!string.IsNullOrWhiteSpace(q?.JsonQuery)) |
||||
|
{ |
||||
|
result = ParseJson(q?.JsonQuery); |
||||
|
} |
||||
|
else if (!string.IsNullOrWhiteSpace(q?.ODataQuery)) |
||||
|
{ |
||||
|
result = ParseOData(q.ODataQuery); |
||||
|
} |
||||
|
|
||||
|
if (result.Filter != null) |
||||
|
{ |
||||
|
result.Filter = FilterTagTransformer.Transform(result.Filter, context.App.Id, tagService); |
||||
|
} |
||||
|
|
||||
|
if (result.Sort.Count == 0) |
||||
|
{ |
||||
|
result.Sort.Add(new SortNode(new List<string> { "lastModified" }, SortOrder.Descending)); |
||||
|
} |
||||
|
|
||||
|
if (result.Take == long.MaxValue) |
||||
|
{ |
||||
|
result.Take = options.DefaultPageSize; |
||||
|
} |
||||
|
else if (result.Take > options.MaxResults) |
||||
|
{ |
||||
|
result.Take = options.MaxResults; |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private ClrQuery ParseJson(string json) |
||||
|
{ |
||||
|
return jsonSchema.Parse(json, jsonSerializer); |
||||
|
} |
||||
|
|
||||
|
private ClrQuery ParseOData(string odata) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
return edmModel.ParseQuery(odata).ToQuery(); |
||||
|
} |
||||
|
catch (NotSupportedException) |
||||
|
{ |
||||
|
throw new ValidationException("OData operation is not supported."); |
||||
|
} |
||||
|
catch (ODataException ex) |
||||
|
{ |
||||
|
throw new ValidationException($"Failed to parse query: {ex.Message}", ex); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static JsonSchema BuildJsonSchema() |
||||
|
{ |
||||
|
var schema = new JsonSchema { Title = "Asset", Type = JsonObjectType.Object }; |
||||
|
|
||||
|
void AddProperty(string name, JsonObjectType type, string format = null) |
||||
|
{ |
||||
|
var property = new JsonSchemaProperty { Type = type, Format = format }; |
||||
|
|
||||
|
schema.Properties[name.ToCamelCase()] = property; |
||||
|
} |
||||
|
|
||||
|
AddProperty(nameof(IAssetEntity.Id), JsonObjectType.String, JsonFormatStrings.Guid); |
||||
|
AddProperty(nameof(IAssetEntity.Created), JsonObjectType.String, JsonFormatStrings.DateTime); |
||||
|
AddProperty(nameof(IAssetEntity.CreatedBy), JsonObjectType.String); |
||||
|
AddProperty(nameof(IAssetEntity.LastModified), JsonObjectType.String, JsonFormatStrings.DateTime); |
||||
|
AddProperty(nameof(IAssetEntity.LastModifiedBy), JsonObjectType.String); |
||||
|
AddProperty(nameof(IAssetEntity.Version), JsonObjectType.Integer); |
||||
|
AddProperty(nameof(IAssetEntity.FileName), JsonObjectType.String); |
||||
|
AddProperty(nameof(IAssetEntity.FileHash), JsonObjectType.String); |
||||
|
AddProperty(nameof(IAssetEntity.FileSize), JsonObjectType.Integer); |
||||
|
AddProperty(nameof(IAssetEntity.FileVersion), JsonObjectType.Integer); |
||||
|
AddProperty(nameof(IAssetEntity.IsImage), JsonObjectType.Boolean); |
||||
|
AddProperty(nameof(IAssetEntity.MimeType), JsonObjectType.String); |
||||
|
AddProperty(nameof(IAssetEntity.PixelHeight), JsonObjectType.Integer); |
||||
|
AddProperty(nameof(IAssetEntity.PixelWidth), JsonObjectType.Integer); |
||||
|
AddProperty(nameof(IAssetEntity.Slug), JsonObjectType.String); |
||||
|
AddProperty(nameof(IAssetEntity.Tags), JsonObjectType.String); |
||||
|
|
||||
|
return schema; |
||||
|
} |
||||
|
|
||||
|
private static IEdmModel BuildEdmModel() |
||||
|
{ |
||||
|
var entityType = new EdmEntityType("Squidex", "Asset"); |
||||
|
|
||||
|
void AddProperty(string name, EdmPrimitiveTypeKind type) |
||||
|
{ |
||||
|
entityType.AddStructuralProperty(name.ToCamelCase(), type); |
||||
|
} |
||||
|
|
||||
|
AddProperty(nameof(IAssetEntity.Id), EdmPrimitiveTypeKind.String); |
||||
|
AddProperty(nameof(IAssetEntity.Created), EdmPrimitiveTypeKind.DateTimeOffset); |
||||
|
AddProperty(nameof(IAssetEntity.CreatedBy), EdmPrimitiveTypeKind.String); |
||||
|
AddProperty(nameof(IAssetEntity.LastModified), EdmPrimitiveTypeKind.DateTimeOffset); |
||||
|
AddProperty(nameof(IAssetEntity.LastModifiedBy), EdmPrimitiveTypeKind.String); |
||||
|
AddProperty(nameof(IAssetEntity.Version), EdmPrimitiveTypeKind.Int64); |
||||
|
AddProperty(nameof(IAssetEntity.FileName), EdmPrimitiveTypeKind.String); |
||||
|
AddProperty(nameof(IAssetEntity.FileHash), EdmPrimitiveTypeKind.String); |
||||
|
AddProperty(nameof(IAssetEntity.FileSize), EdmPrimitiveTypeKind.Int64); |
||||
|
AddProperty(nameof(IAssetEntity.FileVersion), EdmPrimitiveTypeKind.Int64); |
||||
|
AddProperty(nameof(IAssetEntity.IsImage), EdmPrimitiveTypeKind.Boolean); |
||||
|
AddProperty(nameof(IAssetEntity.MimeType), EdmPrimitiveTypeKind.String); |
||||
|
AddProperty(nameof(IAssetEntity.PixelHeight), EdmPrimitiveTypeKind.Int32); |
||||
|
AddProperty(nameof(IAssetEntity.PixelWidth), EdmPrimitiveTypeKind.Int32); |
||||
|
AddProperty(nameof(IAssetEntity.Slug), EdmPrimitiveTypeKind.String); |
||||
|
AddProperty(nameof(IAssetEntity.Tags), EdmPrimitiveTypeKind.String); |
||||
|
|
||||
|
var container = new EdmEntityContainer("Squidex", "Container"); |
||||
|
|
||||
|
container.AddEntitySet("AssetSet", entityType); |
||||
|
|
||||
|
var model = new EdmModel(); |
||||
|
|
||||
|
model.AddElement(container); |
||||
|
model.AddElement(entityType); |
||||
|
|
||||
|
return model; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,104 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.Linq; |
|
||||
using Microsoft.Extensions.Caching.Memory; |
|
||||
using Microsoft.OData.Edm; |
|
||||
using Squidex.Domain.Apps.Core.GenerateEdmSchema; |
|
||||
using Squidex.Domain.Apps.Core.Schemas; |
|
||||
using Squidex.Domain.Apps.Entities.Apps; |
|
||||
using Squidex.Domain.Apps.Entities.Schemas; |
|
||||
using Squidex.Infrastructure; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Entities.Contents.Edm |
|
||||
{ |
|
||||
public class EdmModelBuilder : CachingProviderBase |
|
||||
{ |
|
||||
private static readonly TimeSpan CacheTime = TimeSpan.FromMinutes(60); |
|
||||
|
|
||||
public EdmModelBuilder(IMemoryCache cache) |
|
||||
: base(cache) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public virtual IEdmModel BuildEdmModel(IAppEntity app, ISchemaEntity schema, bool withHidden) |
|
||||
{ |
|
||||
Guard.NotNull(schema, nameof(schema)); |
|
||||
|
|
||||
var cacheKey = BuildCacheKey(app, schema, withHidden); |
|
||||
|
|
||||
var result = Cache.GetOrCreate<IEdmModel>(cacheKey, entry => |
|
||||
{ |
|
||||
entry.AbsoluteExpirationRelativeToNow = CacheTime; |
|
||||
|
|
||||
return BuildEdmModel(schema.SchemaDef, app, withHidden); |
|
||||
}); |
|
||||
|
|
||||
return result; |
|
||||
} |
|
||||
|
|
||||
private static EdmModel BuildEdmModel(Schema schema, IAppEntity app, bool withHidden) |
|
||||
{ |
|
||||
var model = new EdmModel(); |
|
||||
|
|
||||
var pascalAppName = app.Name.ToPascalCase(); |
|
||||
var pascalSchemaName = schema.Name.ToPascalCase(); |
|
||||
|
|
||||
var typeFactory = new EdmTypeFactory(name => |
|
||||
{ |
|
||||
var finalName = pascalSchemaName; |
|
||||
|
|
||||
if (!string.IsNullOrWhiteSpace(name)) |
|
||||
{ |
|
||||
finalName += "."; |
|
||||
finalName += name; |
|
||||
} |
|
||||
|
|
||||
var result = model.SchemaElements.OfType<EdmComplexType>().FirstOrDefault(x => x.Name == finalName); |
|
||||
|
|
||||
if (result != null) |
|
||||
{ |
|
||||
return (result, false); |
|
||||
} |
|
||||
|
|
||||
result = new EdmComplexType(pascalAppName, finalName); |
|
||||
|
|
||||
model.AddElement(result); |
|
||||
|
|
||||
return (result, true); |
|
||||
}); |
|
||||
|
|
||||
var schemaType = schema.BuildEdmType(withHidden, app.PartitionResolver(), typeFactory); |
|
||||
|
|
||||
var entityType = new EdmEntityType(app.Name.ToPascalCase(), schema.Name); |
|
||||
entityType.AddStructuralProperty(nameof(IContentEntity.Id).ToCamelCase(), EdmPrimitiveTypeKind.String); |
|
||||
entityType.AddStructuralProperty(nameof(IContentEntity.Created).ToCamelCase(), EdmPrimitiveTypeKind.DateTimeOffset); |
|
||||
entityType.AddStructuralProperty(nameof(IContentEntity.CreatedBy).ToCamelCase(), EdmPrimitiveTypeKind.String); |
|
||||
entityType.AddStructuralProperty(nameof(IContentEntity.LastModified).ToCamelCase(), EdmPrimitiveTypeKind.DateTimeOffset); |
|
||||
entityType.AddStructuralProperty(nameof(IContentEntity.LastModifiedBy).ToCamelCase(), EdmPrimitiveTypeKind.String); |
|
||||
entityType.AddStructuralProperty(nameof(IContentEntity.Status).ToCamelCase(), EdmPrimitiveTypeKind.String); |
|
||||
entityType.AddStructuralProperty(nameof(IContentEntity.Version).ToCamelCase(), EdmPrimitiveTypeKind.Int32); |
|
||||
entityType.AddStructuralProperty(nameof(IContentEntity.Data).ToCamelCase(), new EdmComplexTypeReference(schemaType, false)); |
|
||||
|
|
||||
var container = new EdmEntityContainer("Squidex", "Container"); |
|
||||
|
|
||||
container.AddEntitySet("ContentSet", entityType); |
|
||||
|
|
||||
model.AddElement(container); |
|
||||
model.AddElement(schemaType); |
|
||||
model.AddElement(entityType); |
|
||||
|
|
||||
return model; |
|
||||
} |
|
||||
|
|
||||
private static string BuildCacheKey(IAppEntity app, ISchemaEntity schema, bool withHidden) |
|
||||
{ |
|
||||
return string.Join("_", schema.Id, schema.Version, app.Id, app.Version, withHidden); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,203 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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 Microsoft.Extensions.Caching.Memory; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using Microsoft.OData; |
||||
|
using Microsoft.OData.Edm; |
||||
|
using NJsonSchema; |
||||
|
using Squidex.Domain.Apps.Core.GenerateEdmSchema; |
||||
|
using Squidex.Domain.Apps.Core.GenerateJsonSchema; |
||||
|
using Squidex.Domain.Apps.Core.Schemas; |
||||
|
using Squidex.Domain.Apps.Entities.Apps; |
||||
|
using Squidex.Domain.Apps.Entities.Schemas; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Json; |
||||
|
using Squidex.Infrastructure.Log; |
||||
|
using Squidex.Infrastructure.Queries; |
||||
|
using Squidex.Infrastructure.Queries.Json; |
||||
|
using Squidex.Infrastructure.Queries.OData; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.Queries |
||||
|
{ |
||||
|
public class ContentQueryParser : CachingProviderBase |
||||
|
{ |
||||
|
private static readonly TimeSpan CacheTime = TimeSpan.FromMinutes(60); |
||||
|
private readonly IJsonSerializer jsonSerializer; |
||||
|
private readonly ContentOptions options; |
||||
|
|
||||
|
public ContentQueryParser(IMemoryCache cache, IJsonSerializer jsonSerializer, IOptions<ContentOptions> options) |
||||
|
: base(cache) |
||||
|
{ |
||||
|
this.jsonSerializer = jsonSerializer; |
||||
|
this.options = options.Value; |
||||
|
} |
||||
|
|
||||
|
public virtual ClrQuery ParseQuery(Context context, ISchemaEntity schema, Q q) |
||||
|
{ |
||||
|
Guard.NotNull(context, nameof(context)); |
||||
|
Guard.NotNull(schema, nameof(schema)); |
||||
|
|
||||
|
using (Profiler.TraceMethod<ContentQueryParser>()) |
||||
|
{ |
||||
|
var result = new ClrQuery(); |
||||
|
|
||||
|
if (!string.IsNullOrWhiteSpace(q?.JsonQuery)) |
||||
|
{ |
||||
|
result = ParseJson(context, schema, q.JsonQuery); |
||||
|
} |
||||
|
else if (!string.IsNullOrWhiteSpace(q?.ODataQuery)) |
||||
|
{ |
||||
|
result = ParseOData(context, schema, q.ODataQuery); |
||||
|
} |
||||
|
|
||||
|
if (result.Sort.Count == 0) |
||||
|
{ |
||||
|
result.Sort.Add(new SortNode(new List<string> { "lastModified" }, SortOrder.Descending)); |
||||
|
} |
||||
|
|
||||
|
if (result.Take == long.MaxValue) |
||||
|
{ |
||||
|
result.Take = options.DefaultPageSize; |
||||
|
} |
||||
|
else if (result.Take > options.MaxResults) |
||||
|
{ |
||||
|
result.Take = options.MaxResults; |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private ClrQuery ParseJson(Context context, ISchemaEntity schema, string json) |
||||
|
{ |
||||
|
var jsonSchema = BuildJsonSchema(context, schema); |
||||
|
|
||||
|
return jsonSchema.Parse(json, jsonSerializer); |
||||
|
} |
||||
|
|
||||
|
private ClrQuery ParseOData(Context context, ISchemaEntity schema, string odata) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var model = BuildEdmModel(context, schema); |
||||
|
|
||||
|
return model.ParseQuery(odata).ToQuery(); |
||||
|
} |
||||
|
catch (NotSupportedException) |
||||
|
{ |
||||
|
throw new ValidationException("OData operation is not supported."); |
||||
|
} |
||||
|
catch (ODataException ex) |
||||
|
{ |
||||
|
throw new ValidationException($"Failed to parse query: {ex.Message}", ex); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private JsonSchema BuildJsonSchema(Context context, ISchemaEntity schema) |
||||
|
{ |
||||
|
var cacheKey = BuildJsonCacheKey(context.App, schema, context.IsFrontendClient); |
||||
|
|
||||
|
var result = Cache.GetOrCreate<JsonSchema>(cacheKey, entry => |
||||
|
{ |
||||
|
entry.AbsoluteExpirationRelativeToNow = CacheTime; |
||||
|
|
||||
|
return BuildJsonSchema(schema.SchemaDef, context.App, context.IsFrontendClient); |
||||
|
}); |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
private IEdmModel BuildEdmModel(Context context, ISchemaEntity schema) |
||||
|
{ |
||||
|
var cacheKey = BuildEmdCacheKey(context.App, schema, context.IsFrontendClient); |
||||
|
|
||||
|
var result = Cache.GetOrCreate<IEdmModel>(cacheKey, entry => |
||||
|
{ |
||||
|
entry.AbsoluteExpirationRelativeToNow = CacheTime; |
||||
|
|
||||
|
return BuildEdmModel(schema.SchemaDef, context.App, context.IsFrontendClient); |
||||
|
}); |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
private static JsonSchema BuildJsonSchema(Schema schema, IAppEntity app, bool withHiddenFields) |
||||
|
{ |
||||
|
var dataSchema = schema.BuildJsonSchema(app.PartitionResolver(), (n, s) => s, withHiddenFields); |
||||
|
|
||||
|
return new ContentSchemaBuilder().CreateContentSchema(schema, dataSchema); |
||||
|
} |
||||
|
|
||||
|
private static EdmModel BuildEdmModel(Schema schema, IAppEntity app, bool withHiddenFields) |
||||
|
{ |
||||
|
var model = new EdmModel(); |
||||
|
|
||||
|
var pascalAppName = app.Name.ToPascalCase(); |
||||
|
var pascalSchemaName = schema.Name.ToPascalCase(); |
||||
|
|
||||
|
var typeFactory = new EdmTypeFactory(name => |
||||
|
{ |
||||
|
var finalName = pascalSchemaName; |
||||
|
|
||||
|
if (!string.IsNullOrWhiteSpace(name)) |
||||
|
{ |
||||
|
finalName += "."; |
||||
|
finalName += name; |
||||
|
} |
||||
|
|
||||
|
var result = model.SchemaElements.OfType<EdmComplexType>().FirstOrDefault(x => x.Name == finalName); |
||||
|
|
||||
|
if (result != null) |
||||
|
{ |
||||
|
return (result, false); |
||||
|
} |
||||
|
|
||||
|
result = new EdmComplexType(pascalAppName, finalName); |
||||
|
|
||||
|
model.AddElement(result); |
||||
|
|
||||
|
return (result, true); |
||||
|
}); |
||||
|
|
||||
|
var schemaType = schema.BuildEdmType(withHiddenFields, app.PartitionResolver(), typeFactory); |
||||
|
|
||||
|
var entityType = new EdmEntityType(app.Name.ToPascalCase(), schema.Name); |
||||
|
entityType.AddStructuralProperty(nameof(IContentEntity.Id).ToCamelCase(), EdmPrimitiveTypeKind.String); |
||||
|
entityType.AddStructuralProperty(nameof(IContentEntity.Created).ToCamelCase(), EdmPrimitiveTypeKind.DateTimeOffset); |
||||
|
entityType.AddStructuralProperty(nameof(IContentEntity.CreatedBy).ToCamelCase(), EdmPrimitiveTypeKind.String); |
||||
|
entityType.AddStructuralProperty(nameof(IContentEntity.LastModified).ToCamelCase(), EdmPrimitiveTypeKind.DateTimeOffset); |
||||
|
entityType.AddStructuralProperty(nameof(IContentEntity.LastModifiedBy).ToCamelCase(), EdmPrimitiveTypeKind.String); |
||||
|
entityType.AddStructuralProperty(nameof(IContentEntity.Status).ToCamelCase(), EdmPrimitiveTypeKind.String); |
||||
|
entityType.AddStructuralProperty(nameof(IContentEntity.Version).ToCamelCase(), EdmPrimitiveTypeKind.Int32); |
||||
|
entityType.AddStructuralProperty(nameof(IContentEntity.Data).ToCamelCase(), new EdmComplexTypeReference(schemaType, false)); |
||||
|
|
||||
|
var container = new EdmEntityContainer("Squidex", "Container"); |
||||
|
|
||||
|
container.AddEntitySet("ContentSet", entityType); |
||||
|
|
||||
|
model.AddElement(container); |
||||
|
model.AddElement(schemaType); |
||||
|
model.AddElement(entityType); |
||||
|
|
||||
|
return model; |
||||
|
} |
||||
|
|
||||
|
private static string BuildEmdCacheKey(IAppEntity app, ISchemaEntity schema, bool withHidden) |
||||
|
{ |
||||
|
return $"EDM/{app.Version}/{schema.Id}_{schema.Version}/{withHidden}"; |
||||
|
} |
||||
|
|
||||
|
private static string BuildJsonCacheKey(IAppEntity app, ISchemaEntity schema, bool withHidden) |
||||
|
{ |
||||
|
return $"EDM/{app.Version}/{schema.Id}_{schema.Version}/{withHidden}"; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities |
||||
|
{ |
||||
|
public interface IEntityWithCacheDependencies |
||||
|
{ |
||||
|
HashSet<string> CacheDependencies { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,84 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using NJsonSchema; |
||||
|
using Squidex.Infrastructure.Json.Objects; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Queries.Json |
||||
|
{ |
||||
|
public sealed class JsonFilterVisitor : FilterNodeVisitor<FilterNode<ClrValue>, IJsonValue> |
||||
|
{ |
||||
|
private readonly List<string> errors; |
||||
|
private readonly JsonSchema schema; |
||||
|
|
||||
|
private JsonFilterVisitor(JsonSchema schema, List<string> errors) |
||||
|
{ |
||||
|
this.schema = schema; |
||||
|
|
||||
|
this.errors = errors; |
||||
|
} |
||||
|
|
||||
|
public static FilterNode<ClrValue> Parse(FilterNode<IJsonValue> filter, JsonSchema schema, List<string> errors) |
||||
|
{ |
||||
|
var visitor = new JsonFilterVisitor(schema, errors); |
||||
|
|
||||
|
var parsed = filter.Accept(visitor); |
||||
|
|
||||
|
if (visitor.errors.Count > 0) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
return parsed; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public override FilterNode<ClrValue> Visit(NegateFilter<IJsonValue> nodeIn) |
||||
|
{ |
||||
|
return new NegateFilter<ClrValue>(nodeIn.Accept(this)); |
||||
|
} |
||||
|
|
||||
|
public override FilterNode<ClrValue> Visit(LogicalFilter<IJsonValue> nodeIn) |
||||
|
{ |
||||
|
return new LogicalFilter<ClrValue>(nodeIn.Type, nodeIn.Filters.Select(x => x.Accept(this)).ToList()); |
||||
|
} |
||||
|
|
||||
|
public override FilterNode<ClrValue> Visit(CompareFilter<IJsonValue> nodeIn) |
||||
|
{ |
||||
|
CompareFilter<ClrValue> result = null; |
||||
|
|
||||
|
if (nodeIn.Path.TryGetProperty(schema, errors, out var property)) |
||||
|
{ |
||||
|
var isValidOperator = OperatorValidator.IsAllowedOperator(property, nodeIn.Operator); |
||||
|
|
||||
|
if (!isValidOperator) |
||||
|
{ |
||||
|
errors.Add($"{nodeIn.Operator} is not a valid operator for type {property.Type} at {nodeIn.Path}."); |
||||
|
} |
||||
|
|
||||
|
var value = ValueConverter.Convert(property, nodeIn.Value, nodeIn.Path, errors); |
||||
|
|
||||
|
if (value != null && isValidOperator) |
||||
|
{ |
||||
|
if (value.IsList && nodeIn.Operator != CompareOperator.In) |
||||
|
{ |
||||
|
errors.Add($"Array value is not allowed for '{nodeIn.Operator}' operator and path '{nodeIn.Path}'."); |
||||
|
} |
||||
|
|
||||
|
result = new CompareFilter<ClrValue>(nodeIn.Path, nodeIn.Operator, value); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
result = result ?? new CompareFilter<ClrValue>(nodeIn.Path, nodeIn.Operator, ClrValue.Null); |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,71 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Linq; |
||||
|
using NJsonSchema; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Queries.Json |
||||
|
{ |
||||
|
public static class OperatorValidator |
||||
|
{ |
||||
|
private static readonly CompareOperator[] BooleanOperators = |
||||
|
{ |
||||
|
CompareOperator.Equals, |
||||
|
CompareOperator.In, |
||||
|
CompareOperator.NotEquals |
||||
|
}; |
||||
|
private static readonly CompareOperator[] NumberOperators = |
||||
|
{ |
||||
|
CompareOperator.Equals, |
||||
|
CompareOperator.LessThan, |
||||
|
CompareOperator.LessThanOrEqual, |
||||
|
CompareOperator.GreaterThan, |
||||
|
CompareOperator.GreaterThanOrEqual, |
||||
|
CompareOperator.In, |
||||
|
CompareOperator.NotEquals |
||||
|
}; |
||||
|
private static readonly CompareOperator[] StringOperators = |
||||
|
{ |
||||
|
CompareOperator.Contains, |
||||
|
CompareOperator.Empty, |
||||
|
CompareOperator.EndsWith, |
||||
|
CompareOperator.Equals, |
||||
|
CompareOperator.GreaterThan, |
||||
|
CompareOperator.GreaterThanOrEqual, |
||||
|
CompareOperator.In, |
||||
|
CompareOperator.LessThan, |
||||
|
CompareOperator.LessThanOrEqual, |
||||
|
CompareOperator.NotEquals, |
||||
|
CompareOperator.StartsWith |
||||
|
}; |
||||
|
private static readonly CompareOperator[] ArrayOperators = |
||||
|
{ |
||||
|
CompareOperator.Empty, |
||||
|
CompareOperator.Equals, |
||||
|
CompareOperator.In, |
||||
|
CompareOperator.NotEquals |
||||
|
}; |
||||
|
|
||||
|
public static bool IsAllowedOperator(JsonSchema schema, CompareOperator compareOperator) |
||||
|
{ |
||||
|
switch (schema.Type) |
||||
|
{ |
||||
|
case JsonObjectType.Boolean: |
||||
|
return BooleanOperators.Contains(compareOperator); |
||||
|
case JsonObjectType.Integer: |
||||
|
case JsonObjectType.Number: |
||||
|
return NumberOperators.Contains(compareOperator); |
||||
|
case JsonObjectType.String: |
||||
|
return StringOperators.Contains(compareOperator); |
||||
|
case JsonObjectType.Array: |
||||
|
return ArrayOperators.Contains(compareOperator); |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,72 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using Newtonsoft.Json; |
||||
|
using NJsonSchema; |
||||
|
using Squidex.Infrastructure.Json; |
||||
|
using Squidex.Infrastructure.Json.Objects; |
||||
|
using Squidex.Infrastructure.Reflection; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Queries.Json |
||||
|
{ |
||||
|
public static class QueryParser |
||||
|
{ |
||||
|
public static ClrQuery Parse(this JsonSchema schema, string json, IJsonSerializer jsonSerializer) |
||||
|
{ |
||||
|
var query = ParseFromJson(json, jsonSerializer); |
||||
|
|
||||
|
var result = SimpleMapper.Map(query, new ClrQuery()); |
||||
|
|
||||
|
var errors = new List<string>(); |
||||
|
|
||||
|
ConvertSorting(schema, result, errors); |
||||
|
ConvertFilters(schema, result, errors, query); |
||||
|
|
||||
|
if (errors.Count > 0) |
||||
|
{ |
||||
|
throw new ValidationException("Failed to parse json query", errors.Select(x => new ValidationError(x)).ToArray()); |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
private static void ConvertFilters(JsonSchema schema, ClrQuery result, List<string> errors, Query<IJsonValue> query) |
||||
|
{ |
||||
|
if (query.Filter != null) |
||||
|
{ |
||||
|
var filter = JsonFilterVisitor.Parse(query.Filter, schema, errors); |
||||
|
|
||||
|
result.Filter = Optimizer<ClrValue>.Optimize(filter); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static void ConvertSorting(JsonSchema schema, ClrQuery result, List<string> errors) |
||||
|
{ |
||||
|
if (result.Sort != null) |
||||
|
{ |
||||
|
foreach (var sorting in result.Sort) |
||||
|
{ |
||||
|
sorting.Path.TryGetProperty(schema, errors, out _); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static Query<IJsonValue> ParseFromJson(string json, IJsonSerializer jsonSerializer) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
return jsonSerializer.Deserialize<Query<IJsonValue>>(json); |
||||
|
} |
||||
|
catch (JsonException ex) |
||||
|
{ |
||||
|
throw new ValidationException("Failed to parse json query.", new ValidationError(ex.Message)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,228 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using NJsonSchema; |
||||
|
using NodaTime; |
||||
|
using NodaTime.Text; |
||||
|
using Squidex.Infrastructure.Json.Objects; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Queries.Json |
||||
|
{ |
||||
|
public static class ValueConverter |
||||
|
{ |
||||
|
private delegate bool Parser<T>(List<string> errors, PropertyPath path, IJsonValue value, out T result); |
||||
|
|
||||
|
public static ClrValue Convert(JsonSchema schema, IJsonValue value, PropertyPath path, List<string> errors) |
||||
|
{ |
||||
|
ClrValue result = null; |
||||
|
|
||||
|
switch (GetType(schema)) |
||||
|
{ |
||||
|
case JsonObjectType.Boolean: |
||||
|
{ |
||||
|
if (value is JsonArray jsonArray) |
||||
|
{ |
||||
|
result = ParseArray<bool>(errors, path, jsonArray, TryParseBoolean); |
||||
|
} |
||||
|
else if (TryParseBoolean(errors, path, value, out var temp)) |
||||
|
{ |
||||
|
result = temp; |
||||
|
} |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
case JsonObjectType.Integer: |
||||
|
case JsonObjectType.Number: |
||||
|
{ |
||||
|
if (value is JsonArray jsonArray) |
||||
|
{ |
||||
|
result = ParseArray<double>(errors, path, jsonArray, TryParseNumber); |
||||
|
} |
||||
|
else if (TryParseNumber(errors, path, value, out var temp)) |
||||
|
{ |
||||
|
result = temp; |
||||
|
} |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
case JsonObjectType.String: |
||||
|
{ |
||||
|
if (schema.Format == JsonFormatStrings.Guid) |
||||
|
{ |
||||
|
if (value is JsonArray jsonArray) |
||||
|
{ |
||||
|
result = ParseArray<Guid>(errors, path, jsonArray, TryParseGuid); |
||||
|
} |
||||
|
else if (TryParseGuid(errors, path, value, out var temp)) |
||||
|
{ |
||||
|
result = temp; |
||||
|
} |
||||
|
} |
||||
|
else if (schema.Format == JsonFormatStrings.DateTime) |
||||
|
{ |
||||
|
if (value is JsonArray jsonArray) |
||||
|
{ |
||||
|
result = ParseArray<Instant>(errors, path, jsonArray, TryParseDateTime); |
||||
|
} |
||||
|
else if (TryParseDateTime(errors, path, value, out var temp)) |
||||
|
{ |
||||
|
result = temp; |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
if (value is JsonArray jsonArray) |
||||
|
{ |
||||
|
result = ParseArray<string>(errors, path, jsonArray, TryParseString); |
||||
|
} |
||||
|
else if (TryParseString(errors, path, value, out var temp)) |
||||
|
{ |
||||
|
result = temp; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
default: |
||||
|
{ |
||||
|
errors.Add($"Unsupported type {schema.Type} for {path}."); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
private static List<T> ParseArray<T>(List<string> errors, PropertyPath path, JsonArray array, Parser<T> parser) |
||||
|
{ |
||||
|
var items = new List<T>(); |
||||
|
|
||||
|
foreach (var item in array) |
||||
|
{ |
||||
|
if (parser(errors, path, item, out var temp)) |
||||
|
{ |
||||
|
items.Add(temp); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return items; |
||||
|
} |
||||
|
|
||||
|
private static bool TryParseBoolean(List<string> errors, PropertyPath path, IJsonValue value, out bool result) |
||||
|
{ |
||||
|
result = default; |
||||
|
|
||||
|
if (value is JsonBoolean jsonBoolean) |
||||
|
{ |
||||
|
result = jsonBoolean.Value; |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
errors.Add($"Expected Boolean for path '{path}', but got {value.Type}."); |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
private static bool TryParseNumber(List<string> errors, PropertyPath path, IJsonValue value, out double result) |
||||
|
{ |
||||
|
result = default; |
||||
|
|
||||
|
if (value is JsonNumber jsonNumber) |
||||
|
{ |
||||
|
result = jsonNumber.Value; |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
errors.Add($"Expected Number for path '{path}', but got {value.Type}."); |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
private static bool TryParseString(List<string> errors, PropertyPath path, IJsonValue value, out string result) |
||||
|
{ |
||||
|
result = default; |
||||
|
|
||||
|
if (value is JsonString jsonString) |
||||
|
{ |
||||
|
result = jsonString.Value; |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
else if (value is JsonNull) |
||||
|
{ |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
errors.Add($"Expected String for path '{path}', but got {value.Type}."); |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
private static bool TryParseGuid(List<string> errors, PropertyPath path, IJsonValue value, out Guid result) |
||||
|
{ |
||||
|
result = default; |
||||
|
|
||||
|
if (value is JsonString jsonString) |
||||
|
{ |
||||
|
if (Guid.TryParse(jsonString.Value, out result)) |
||||
|
{ |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
errors.Add($"Expected Guid String for path '{path}', but got invalid String."); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
errors.Add($"Expected Guid String for path '{path}', but got {value.Type}."); |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
private static bool TryParseDateTime(List<string> errors, PropertyPath path, IJsonValue value, out Instant result) |
||||
|
{ |
||||
|
result = default; |
||||
|
|
||||
|
if (value is JsonString jsonString) |
||||
|
{ |
||||
|
var parsed = InstantPattern.General.Parse(jsonString.Value); |
||||
|
|
||||
|
if (parsed.Success) |
||||
|
{ |
||||
|
result = parsed.Value; |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
errors.Add($"Expected ISO8601 DateTime String for path '{path}', but got invalid String."); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
errors.Add($"Expected ISO8601 DateTime String for path '{path}', but got {value.Type}."); |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
private static JsonObjectType GetType(JsonSchema schema) |
||||
|
{ |
||||
|
if (schema.Item != null) |
||||
|
{ |
||||
|
return schema.Item.Type; |
||||
|
} |
||||
|
|
||||
|
return schema.Type; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,67 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Linq; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Queries |
||||
|
{ |
||||
|
public sealed class Optimizer<TValue> : TransformVisitor<TValue> |
||||
|
{ |
||||
|
private static readonly Optimizer<TValue> Instance = new Optimizer<TValue>(); |
||||
|
|
||||
|
private Optimizer() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public static FilterNode<TValue> Optimize(FilterNode<TValue> source) |
||||
|
{ |
||||
|
return source?.Accept(Instance); |
||||
|
} |
||||
|
|
||||
|
public override FilterNode<TValue> Visit(LogicalFilter<TValue> nodeIn) |
||||
|
{ |
||||
|
var pruned = nodeIn.Filters.Select(x => x.Accept(this)).Where(x => x != null).ToList(); |
||||
|
|
||||
|
if (pruned.Count == 1) |
||||
|
{ |
||||
|
return pruned[0]; |
||||
|
} |
||||
|
|
||||
|
if (pruned.Count == 0) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
return new LogicalFilter<TValue>(nodeIn.Type, pruned); |
||||
|
} |
||||
|
|
||||
|
public override FilterNode<TValue> Visit(NegateFilter<TValue> nodeIn) |
||||
|
{ |
||||
|
var pruned = nodeIn.Filter.Accept(this); |
||||
|
|
||||
|
if (pruned == null) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
if (pruned is CompareFilter<TValue> comparison) |
||||
|
{ |
||||
|
if (comparison.Operator == CompareOperator.Equals) |
||||
|
{ |
||||
|
return new CompareFilter<TValue>(comparison.Path, CompareOperator.NotEquals, comparison.Value); |
||||
|
} |
||||
|
|
||||
|
if (comparison.Operator == CompareOperator.NotEquals) |
||||
|
{ |
||||
|
return new CompareFilter<TValue>(comparison.Path, CompareOperator.Equals, comparison.Value); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return new NegateFilter<TValue>(pruned); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,129 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using FakeItEasy; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using Squidex.Domain.Apps.Core.Tags; |
||||
|
using Squidex.Domain.Apps.Entities.TestHelpers; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Assets.Queries |
||||
|
{ |
||||
|
public class AssetQueryParserTests |
||||
|
{ |
||||
|
private readonly ITagService tagService = A.Fake<ITagService>(); |
||||
|
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app"); |
||||
|
private readonly Context requestContext; |
||||
|
private readonly AssetQueryParser sut; |
||||
|
|
||||
|
public AssetQueryParserTests() |
||||
|
{ |
||||
|
requestContext = new Context(Mocks.FrontendUser(), Mocks.App(appId)); |
||||
|
|
||||
|
var options = Options.Create(new AssetOptions { DefaultPageSize = 30 }); |
||||
|
|
||||
|
sut = new AssetQueryParser(JsonHelper.DefaultSerializer, tagService, options); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_throw_if_odata_query_is_invalid() |
||||
|
{ |
||||
|
var query = Q.Empty.WithODataQuery("$filter=invalid"); |
||||
|
|
||||
|
Assert.Throws<ValidationException>(() => sut.ParseQuery(requestContext, query)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_throw_if_json_query_is_invalid() |
||||
|
{ |
||||
|
var query = Q.Empty.WithJsonQuery("invalid"); |
||||
|
|
||||
|
Assert.Throws<ValidationException>(() => sut.ParseQuery(requestContext, query)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_parse_odata_query() |
||||
|
{ |
||||
|
var query = Q.Empty.WithODataQuery("$top=100&$orderby=fileName asc&$search=Hello World"); |
||||
|
|
||||
|
var parsed = sut.ParseQuery(requestContext, query); |
||||
|
|
||||
|
Assert.Equal("FullText: 'Hello World'; Take: 100; Sort: fileName Ascending", parsed.ToString()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_parse_odata_query_and_enrich_with_defaults() |
||||
|
{ |
||||
|
var query = Q.Empty.WithODataQuery("$top=200&$filter=fileName eq 'ABC'"); |
||||
|
|
||||
|
var parsed = sut.ParseQuery(requestContext, query); |
||||
|
|
||||
|
Assert.Equal("Filter: fileName == 'ABC'; Take: 200; Sort: lastModified Descending", parsed.ToString()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_parse_json_query_and_enrich_with_defaults() |
||||
|
{ |
||||
|
var query = Q.Empty.WithJsonQuery(Json("{ 'filter': { 'path': 'fileName', 'op': 'eq', 'value': 'ABC' } }")); |
||||
|
|
||||
|
var parsed = sut.ParseQuery(requestContext, query); |
||||
|
|
||||
|
Assert.Equal("Filter: fileName == 'ABC'; Take: 30; Sort: lastModified Descending", parsed.ToString()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_parse_json_full_text_query_and_enrich_with_defaults() |
||||
|
{ |
||||
|
var query = Q.Empty.WithJsonQuery(Json("{ 'fullText': 'Hello' }")); |
||||
|
|
||||
|
var parsed = sut.ParseQuery(requestContext, query); |
||||
|
|
||||
|
Assert.Equal("FullText: 'Hello'; Take: 30; Sort: lastModified Descending", parsed.ToString()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_apply_default_page_size() |
||||
|
{ |
||||
|
var query = Q.Empty; |
||||
|
|
||||
|
var parsed = sut.ParseQuery(requestContext, query); |
||||
|
|
||||
|
Assert.Equal("Take: 30; Sort: lastModified Descending", parsed.ToString()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_limit_number_of_assets() |
||||
|
{ |
||||
|
var query = Q.Empty.WithODataQuery("$top=300&$skip=20"); |
||||
|
|
||||
|
var parsed = sut.ParseQuery(requestContext, query); |
||||
|
|
||||
|
Assert.Equal("Skip: 20; Take: 200; Sort: lastModified Descending", parsed.ToString()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_denormalize_tags() |
||||
|
{ |
||||
|
A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, TagGroups.Assets, A<HashSet<string>>.That.Contains("name1"))) |
||||
|
.Returns(new Dictionary<string, string> { ["name1"] = "id1" }); |
||||
|
|
||||
|
var query = Q.Empty.WithODataQuery("$filter=tags eq 'name1'"); |
||||
|
|
||||
|
var parsed = sut.ParseQuery(requestContext, query); |
||||
|
|
||||
|
Assert.Equal("Filter: tags == 'id1'; Take: 30; Sort: lastModified Descending", parsed.ToString()); |
||||
|
} |
||||
|
|
||||
|
private static string Json(string text) |
||||
|
{ |
||||
|
return text.Replace('\'', '"'); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,126 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using Microsoft.Extensions.Caching.Memory; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using Squidex.Domain.Apps.Core; |
||||
|
using Squidex.Domain.Apps.Core.Schemas; |
||||
|
using Squidex.Domain.Apps.Entities.Schemas; |
||||
|
using Squidex.Domain.Apps.Entities.TestHelpers; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.Queries |
||||
|
{ |
||||
|
public class ContentQueryParserTests |
||||
|
{ |
||||
|
private readonly ISchemaEntity schema; |
||||
|
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app"); |
||||
|
private readonly NamedId<Guid> schemaId = NamedId.Of(Guid.NewGuid(), "my-app"); |
||||
|
private readonly Context requestContext; |
||||
|
private readonly ContentQueryParser sut; |
||||
|
|
||||
|
public ContentQueryParserTests() |
||||
|
{ |
||||
|
var options = Options.Create(new ContentOptions { DefaultPageSize = 30 }); |
||||
|
|
||||
|
requestContext = new Context(Mocks.FrontendUser(), Mocks.App(appId)); |
||||
|
|
||||
|
var schemaDef = |
||||
|
new Schema(schemaId.Name) |
||||
|
.AddString(1, "firstName", Partitioning.Invariant); |
||||
|
|
||||
|
schema = Mocks.Schema(appId, schemaId, schemaDef); |
||||
|
|
||||
|
var cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); |
||||
|
|
||||
|
sut = new ContentQueryParser(cache, JsonHelper.DefaultSerializer, options); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_throw_if_odata_query_is_invalid() |
||||
|
{ |
||||
|
var query = Q.Empty.WithODataQuery("$filter=invalid"); |
||||
|
|
||||
|
Assert.Throws<ValidationException>(() => sut.ParseQuery(requestContext, schema, query)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_throw_if_json_query_is_invalid() |
||||
|
{ |
||||
|
var query = Q.Empty.WithJsonQuery("invalid"); |
||||
|
|
||||
|
Assert.Throws<ValidationException>(() => sut.ParseQuery(requestContext, schema, query)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_parse_odata_query() |
||||
|
{ |
||||
|
var query = Q.Empty.WithODataQuery("$top=100&$orderby=data/firstName/iv asc&$search=Hello World"); |
||||
|
|
||||
|
var parsed = sut.ParseQuery(requestContext, schema, query); |
||||
|
|
||||
|
Assert.Equal("FullText: 'Hello World'; Take: 100; Sort: data.firstName.iv Ascending", parsed.ToString()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_parse_odata_query_and_enrich_with_defaults() |
||||
|
{ |
||||
|
var query = Q.Empty.WithODataQuery("$top=200&$filter=data/firstName/iv eq 'ABC'"); |
||||
|
|
||||
|
var parsed = sut.ParseQuery(requestContext, schema, query); |
||||
|
|
||||
|
Assert.Equal("Filter: data.firstName.iv == 'ABC'; Take: 200; Sort: lastModified Descending", parsed.ToString()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_parse_json_query_and_enrich_with_defaults() |
||||
|
{ |
||||
|
var query = Q.Empty.WithJsonQuery(Json("{ 'filter': { 'path': 'data.firstName.iv', 'op': 'eq', 'value': 'ABC' } }")); |
||||
|
|
||||
|
var parsed = sut.ParseQuery(requestContext, schema, query); |
||||
|
|
||||
|
Assert.Equal("Filter: data.firstName.iv == 'ABC'; Take: 30; Sort: lastModified Descending", parsed.ToString()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_parse_json_full_text_query_and_enrich_with_defaults() |
||||
|
{ |
||||
|
var query = Q.Empty.WithJsonQuery(Json("{ 'fullText': 'Hello' }")); |
||||
|
|
||||
|
var parsed = sut.ParseQuery(requestContext, schema, query); |
||||
|
|
||||
|
Assert.Equal("FullText: 'Hello'; Take: 30; Sort: lastModified Descending", parsed.ToString()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_apply_default_page_size() |
||||
|
{ |
||||
|
var query = Q.Empty; |
||||
|
|
||||
|
var parsed = sut.ParseQuery(requestContext, schema, query); |
||||
|
|
||||
|
Assert.Equal("Take: 30; Sort: lastModified Descending", parsed.ToString()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_limit_number_of_contents() |
||||
|
{ |
||||
|
var query = Q.Empty.WithODataQuery("$top=300&$skip=20"); |
||||
|
|
||||
|
var parsed = sut.ParseQuery(requestContext, schema, query); |
||||
|
|
||||
|
Assert.Equal("Skip: 20; Take: 200; Sort: lastModified Descending", parsed.ToString()); |
||||
|
} |
||||
|
|
||||
|
private static string Json(string text) |
||||
|
{ |
||||
|
return text.Replace('\'', '"'); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,68 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using Newtonsoft.Json; |
||||
|
using Newtonsoft.Json.Converters; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Json; |
||||
|
using Squidex.Infrastructure.Json.Newtonsoft; |
||||
|
using Squidex.Infrastructure.Queries.Json; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.TestHelpers |
||||
|
{ |
||||
|
public static class JsonHelper |
||||
|
{ |
||||
|
public static readonly IJsonSerializer DefaultSerializer = CreateSerializer(); |
||||
|
|
||||
|
public static IJsonSerializer CreateSerializer(TypeNameRegistry typeNameRegistry = null) |
||||
|
{ |
||||
|
var serializerSettings = DefaultSettings(typeNameRegistry); |
||||
|
|
||||
|
return new NewtonsoftJsonSerializer(serializerSettings); |
||||
|
} |
||||
|
|
||||
|
public static JsonSerializerSettings DefaultSettings(TypeNameRegistry typeNameRegistry = null) |
||||
|
{ |
||||
|
return new JsonSerializerSettings |
||||
|
{ |
||||
|
SerializationBinder = new TypeNameSerializationBinder(typeNameRegistry ?? new TypeNameRegistry()), |
||||
|
|
||||
|
ContractResolver = new ConverterContractResolver( |
||||
|
new ClaimsPrincipalConverter(), |
||||
|
new InstantConverter(), |
||||
|
new EnvelopeHeadersConverter(), |
||||
|
new FilterConverter(), |
||||
|
new JsonValueConverter(), |
||||
|
new LanguageConverter(), |
||||
|
new NamedGuidIdConverter(), |
||||
|
new NamedLongIdConverter(), |
||||
|
new NamedStringIdConverter(), |
||||
|
new PropertyPathConverter(), |
||||
|
new RefTokenConverter(), |
||||
|
new StringEnumConverter()), |
||||
|
|
||||
|
TypeNameHandling = TypeNameHandling.Auto |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
public static T SerializeAndDeserialize<T>(this T value) |
||||
|
{ |
||||
|
return DefaultSerializer.Deserialize<Tuple<T>>(DefaultSerializer.Serialize(Tuple.Create(value))).Item1; |
||||
|
} |
||||
|
|
||||
|
public static T Deserialize<T>(string value) |
||||
|
{ |
||||
|
return DefaultSerializer.Deserialize<Tuple<T>>($"{{ \"Item1\": \"{value}\" }}").Item1; |
||||
|
} |
||||
|
|
||||
|
public static T Deserialize<T>(object value) |
||||
|
{ |
||||
|
return DefaultSerializer.Deserialize<Tuple<T>>($"{{ \"Item1\": {value} }}").Item1; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,373 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using NJsonSchema; |
||||
|
using Squidex.Infrastructure.Json.Objects; |
||||
|
using Squidex.Infrastructure.Queries.Json; |
||||
|
using Squidex.Infrastructure.TestHelpers; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Queries |
||||
|
{ |
||||
|
public sealed class QueryJsonConversionTests |
||||
|
{ |
||||
|
private readonly List<string> errors = new List<string>(); |
||||
|
private readonly JsonSchema schema = new JsonSchema(); |
||||
|
|
||||
|
public QueryJsonConversionTests() |
||||
|
{ |
||||
|
var nested = new JsonSchemaProperty { Title = "nested" }; |
||||
|
|
||||
|
nested.Properties["property"] = new JsonSchemaProperty |
||||
|
{ |
||||
|
Type = JsonObjectType.String |
||||
|
}; |
||||
|
|
||||
|
schema.Properties["boolean"] = new JsonSchemaProperty |
||||
|
{ |
||||
|
Type = JsonObjectType.Boolean |
||||
|
}; |
||||
|
|
||||
|
schema.Properties["datetime"] = new JsonSchemaProperty |
||||
|
{ |
||||
|
Type = JsonObjectType.String, Format = JsonFormatStrings.DateTime |
||||
|
}; |
||||
|
|
||||
|
schema.Properties["guid"] = new JsonSchemaProperty |
||||
|
{ |
||||
|
Type = JsonObjectType.String, Format = JsonFormatStrings.Guid |
||||
|
}; |
||||
|
|
||||
|
schema.Properties["integer"] = new JsonSchemaProperty |
||||
|
{ |
||||
|
Type = JsonObjectType.Integer |
||||
|
}; |
||||
|
|
||||
|
schema.Properties["number"] = new JsonSchemaProperty |
||||
|
{ |
||||
|
Type = JsonObjectType.Number |
||||
|
}; |
||||
|
|
||||
|
schema.Properties["string"] = new JsonSchemaProperty |
||||
|
{ |
||||
|
Type = JsonObjectType.String |
||||
|
}; |
||||
|
|
||||
|
schema.Properties["stringArray"] = new JsonSchemaProperty |
||||
|
{ |
||||
|
Item = new JsonSchema |
||||
|
{ |
||||
|
Type = JsonObjectType.String |
||||
|
}, |
||||
|
Type = JsonObjectType.Array |
||||
|
}; |
||||
|
|
||||
|
schema.Properties["object"] = nested; |
||||
|
|
||||
|
schema.Properties["reference"] = new JsonSchemaProperty |
||||
|
{ |
||||
|
Reference = nested |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_add_error_if_property_does_not_exist() |
||||
|
{ |
||||
|
var json = new { path = "notfound", op = "eq", value = 1 }; |
||||
|
|
||||
|
AssertErrors(json, "Path 'notfound' does not point to a valid property in the model."); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_add_error_if_nested_property_does_not_exist() |
||||
|
{ |
||||
|
var json = new { path = "object.notfound", op = "eq", value = 1 }; |
||||
|
|
||||
|
AssertErrors(json, "'notfound' is not a property of 'nested'."); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData("contains", "contains(datetime, 2012-11-10T09:08:07Z)")] |
||||
|
[InlineData("empty", "empty(datetime)")] |
||||
|
[InlineData("endswith", "endsWith(datetime, 2012-11-10T09:08:07Z)")] |
||||
|
[InlineData("eq", "datetime == 2012-11-10T09:08:07Z")] |
||||
|
[InlineData("ge", "datetime >= 2012-11-10T09:08:07Z")] |
||||
|
[InlineData("gt", "datetime > 2012-11-10T09:08:07Z")] |
||||
|
[InlineData("le", "datetime <= 2012-11-10T09:08:07Z")] |
||||
|
[InlineData("lt", "datetime < 2012-11-10T09:08:07Z")] |
||||
|
[InlineData("ne", "datetime != 2012-11-10T09:08:07Z")] |
||||
|
[InlineData("startswith", "startsWith(datetime, 2012-11-10T09:08:07Z)")] |
||||
|
public void Should_parse_datetime_string_filter(string op, string expected) |
||||
|
{ |
||||
|
var json = new { path = "datetime", op, value = "2012-11-10T09:08:07Z" }; |
||||
|
|
||||
|
AssertFilter(json, expected); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_add_error_if_datetime_string_property_got_invalid_string_value() |
||||
|
{ |
||||
|
var json = new { path = "datetime", op = "eq", value = "invalid" }; |
||||
|
|
||||
|
AssertErrors(json, "Expected ISO8601 DateTime String for path 'datetime', but got invalid String."); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_add_error_if_datetime_string_property_got_invalid_value() |
||||
|
{ |
||||
|
var json = new { path = "datetime", op = "eq", value = 1 }; |
||||
|
|
||||
|
AssertErrors(json, "Expected ISO8601 DateTime String for path 'datetime', but got Number."); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData("contains", "contains(guid, bf57d32c-d4dd-4217-8c16-6dcb16975cf3)")] |
||||
|
[InlineData("empty", "empty(guid)")] |
||||
|
[InlineData("endswith", "endsWith(guid, bf57d32c-d4dd-4217-8c16-6dcb16975cf3)")] |
||||
|
[InlineData("eq", "guid == bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] |
||||
|
[InlineData("ge", "guid >= bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] |
||||
|
[InlineData("gt", "guid > bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] |
||||
|
[InlineData("le", "guid <= bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] |
||||
|
[InlineData("lt", "guid < bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] |
||||
|
[InlineData("ne", "guid != bf57d32c-d4dd-4217-8c16-6dcb16975cf3")] |
||||
|
[InlineData("startswith", "startsWith(guid, bf57d32c-d4dd-4217-8c16-6dcb16975cf3)")] |
||||
|
public void Should_parse_guid_string_filter(string op, string expected) |
||||
|
{ |
||||
|
var json = new { path = "guid", op, value = "bf57d32c-d4dd-4217-8c16-6dcb16975cf3" }; |
||||
|
|
||||
|
AssertFilter(json, expected); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_add_error_if_guid_string_property_got_invalid_string_value() |
||||
|
{ |
||||
|
var json = new { path = "guid", op = "eq", value = "invalid" }; |
||||
|
|
||||
|
AssertErrors(json, "Expected Guid String for path 'guid', but got invalid String."); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_add_error_if_guid_string_property_got_invalid_value() |
||||
|
{ |
||||
|
var json = new { path = "guid", op = "eq", value = 1 }; |
||||
|
|
||||
|
AssertErrors(json, "Expected Guid String for path 'guid', but got Number."); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData("contains", "contains(string, 'Hello')")] |
||||
|
[InlineData("empty", "empty(string)")] |
||||
|
[InlineData("endswith", "endsWith(string, 'Hello')")] |
||||
|
[InlineData("eq", "string == 'Hello'")] |
||||
|
[InlineData("ge", "string >= 'Hello'")] |
||||
|
[InlineData("gt", "string > 'Hello'")] |
||||
|
[InlineData("le", "string <= 'Hello'")] |
||||
|
[InlineData("lt", "string < 'Hello'")] |
||||
|
[InlineData("ne", "string != 'Hello'")] |
||||
|
[InlineData("startswith", "startsWith(string, 'Hello')")] |
||||
|
public void Should_parse_string_filter(string op, string expected) |
||||
|
{ |
||||
|
var json = new { path = "string", op, value = "Hello" }; |
||||
|
|
||||
|
AssertFilter(json, expected); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_add_error_if_string_property_got_invalid_value() |
||||
|
{ |
||||
|
var json = new { path = "string", op = "eq", value = 1 }; |
||||
|
|
||||
|
AssertErrors(json, "Expected String for path 'string', but got Number."); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_parse_string_in_filter() |
||||
|
{ |
||||
|
var json = new { path = "string", op = "in", value = new[] { "Hello" } }; |
||||
|
|
||||
|
AssertFilter(json, "string in ['Hello']"); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_parse_nested_string_filter() |
||||
|
{ |
||||
|
var json = new { path = "object.property", op = "in", value = new[] { "Hello" } }; |
||||
|
|
||||
|
AssertFilter(json, "object.property in ['Hello']"); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_parse_referenced_string_filter() |
||||
|
{ |
||||
|
var json = new { path = "reference.property", op = "in", value = new[] { "Hello" } }; |
||||
|
|
||||
|
AssertFilter(json, "reference.property in ['Hello']"); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData("eq", "number == 12")] |
||||
|
[InlineData("ge", "number >= 12")] |
||||
|
[InlineData("gt", "number > 12")] |
||||
|
[InlineData("le", "number <= 12")] |
||||
|
[InlineData("lt", "number < 12")] |
||||
|
[InlineData("ne", "number != 12")] |
||||
|
public void Should_parse_number_filter(string op, string expected) |
||||
|
{ |
||||
|
var json = new { path = "number", op, value = 12 }; |
||||
|
|
||||
|
AssertFilter(json, expected); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_add_error_if_number_property_got_invalid_value() |
||||
|
{ |
||||
|
var json = new { path = "number", op = "eq", value = true }; |
||||
|
|
||||
|
AssertErrors(json, "Expected Number for path 'number', but got Boolean."); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_parse_number_in_filter() |
||||
|
{ |
||||
|
var json = new { path = "number", op = "in", value = new[] { 12 } }; |
||||
|
|
||||
|
AssertFilter(json, "number in [12]"); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData("eq", "boolean == True")] |
||||
|
[InlineData("ne", "boolean != True")] |
||||
|
public void Should_parse_boolean_filter(string op, string expected) |
||||
|
{ |
||||
|
var json = new { path = "boolean", op, value = true }; |
||||
|
|
||||
|
AssertFilter(json, expected); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_add_error_if_boolean_property_got_invalid_value() |
||||
|
{ |
||||
|
var json = new { path = "boolean", op = "eq", value = 1 }; |
||||
|
|
||||
|
AssertErrors(json, "Expected Boolean for path 'boolean', but got Number."); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_parse_boolean_in_filter() |
||||
|
{ |
||||
|
var json = new { path = "boolean", op = "in", value = new[] { true } }; |
||||
|
|
||||
|
AssertFilter(json, "boolean in [True]"); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData("empty", "empty(stringArray)")] |
||||
|
[InlineData("eq", "stringArray == 'Hello'")] |
||||
|
[InlineData("ne", "stringArray != 'Hello'")] |
||||
|
public void Should_parse_array_filter(string op, string expected) |
||||
|
{ |
||||
|
var json = new { path = "stringArray", op, value = "Hello" }; |
||||
|
|
||||
|
AssertFilter(json, expected); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_parse_array_in_filter() |
||||
|
{ |
||||
|
var json = new { path = "stringArray", op = "in", value = new[] { "Hello" } }; |
||||
|
|
||||
|
AssertFilter(json, "stringArray in ['Hello']"); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_add_error_when_using_array_value_for_non_allowed_operator() |
||||
|
{ |
||||
|
var json = new { path = "string", op = "eq", value = new[] { "Hello" } }; |
||||
|
|
||||
|
AssertErrors(json, "Array value is not allowed for 'Equals' operator and path 'string'."); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_parse_query() |
||||
|
{ |
||||
|
var json = new { skip = 10, take = 20, FullText = "Hello", Filter = new { path = "string", op = "eq", value = "Hello" } }; |
||||
|
|
||||
|
AssertQuery(json, "Filter: string == 'Hello'; FullText: 'Hello'; Skip: 10; Take: 20"); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_parse_query_with_sorting() |
||||
|
{ |
||||
|
var json = new { sort = new[] { new { path = "string", order = "ascending" } } }; |
||||
|
|
||||
|
AssertQuery(json, "Sort: string Ascending"); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_throw_exception_for_invalid_query() |
||||
|
{ |
||||
|
var json = new { sort = new[] { new { path = "invalid", order = "ascending" } } }; |
||||
|
|
||||
|
Assert.Throws<ValidationException>(() => AssertQuery(json, null)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_throw_exception_when_parsing_invalid_json() |
||||
|
{ |
||||
|
var json = "invalid"; |
||||
|
|
||||
|
Assert.Throws<ValidationException>(() => AssertQuery(json, null)); |
||||
|
} |
||||
|
|
||||
|
private void AssertQuery(object json, string expectedFilter) |
||||
|
{ |
||||
|
var filter = ConvertQuery(json); |
||||
|
|
||||
|
Assert.Empty(errors); |
||||
|
|
||||
|
Assert.Equal(expectedFilter, filter); |
||||
|
} |
||||
|
|
||||
|
private void AssertFilter(object json, string expectedFilter) |
||||
|
{ |
||||
|
var filter = ConvertFilter(json); |
||||
|
|
||||
|
Assert.Empty(errors); |
||||
|
|
||||
|
Assert.Equal(expectedFilter, filter); |
||||
|
} |
||||
|
|
||||
|
private void AssertErrors(object json, params string[] expectedErrors) |
||||
|
{ |
||||
|
var filter = ConvertFilter(json); |
||||
|
|
||||
|
Assert.Equal(expectedErrors.ToList(), errors); |
||||
|
|
||||
|
Assert.Null(filter); |
||||
|
} |
||||
|
|
||||
|
private string ConvertFilter<T>(T value) |
||||
|
{ |
||||
|
var json = JsonHelper.DefaultSerializer.Serialize(value, true); |
||||
|
|
||||
|
var jsonFilter = JsonHelper.DefaultSerializer.Deserialize<FilterNode<IJsonValue>>(json); |
||||
|
|
||||
|
return JsonFilterVisitor.Parse(jsonFilter, schema, errors)?.ToString(); |
||||
|
} |
||||
|
|
||||
|
private string ConvertQuery<T>(T value) |
||||
|
{ |
||||
|
var json = JsonHelper.DefaultSerializer.Serialize(value, true); |
||||
|
|
||||
|
var jsonFilter = schema.Parse(json, JsonHelper.DefaultSerializer); |
||||
|
|
||||
|
return jsonFilter.ToString(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,94 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Queries |
||||
|
{ |
||||
|
public class QueryOptimizationTests |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void Should_not_convert_optimize_valid_logical_filter() |
||||
|
{ |
||||
|
var source = ClrFilter.Or(ClrFilter.Eq("path", 2), ClrFilter.Eq("path", 3)); |
||||
|
|
||||
|
var result = Optimizer<ClrValue>.Optimize(source); |
||||
|
|
||||
|
Assert.Equal("(path == 2 || path == 3)", result.ToString()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_return_filter_When_logical_filter_has_one_child() |
||||
|
{ |
||||
|
var source = ClrFilter.And(ClrFilter.Eq("path", 1), ClrFilter.Or()); |
||||
|
|
||||
|
var result = Optimizer<ClrValue>.Optimize(source); |
||||
|
|
||||
|
Assert.Equal("path == 1", result.ToString()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_return_null_when_filters_of_logical_filter_get_optimized_away() |
||||
|
{ |
||||
|
var source = ClrFilter.And(ClrFilter.And()); |
||||
|
|
||||
|
var result = Optimizer<ClrValue>.Optimize(source); |
||||
|
|
||||
|
Assert.Null(result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_return_null_when_logical_filter_has_no_filter() |
||||
|
{ |
||||
|
var source = ClrFilter.And(); |
||||
|
|
||||
|
var result = Optimizer<ClrValue>.Optimize(source); |
||||
|
|
||||
|
Assert.Null(result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_return_null_when_filter_of_negation_get_optimized_away() |
||||
|
{ |
||||
|
var source = ClrFilter.Not(ClrFilter.And()); |
||||
|
|
||||
|
var result = Optimizer<ClrValue>.Optimize(source); |
||||
|
|
||||
|
Assert.Null(result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_invert_equals_not_filter() |
||||
|
{ |
||||
|
var source = ClrFilter.Not(ClrFilter.Eq("path", 1)); |
||||
|
|
||||
|
var result = Optimizer<ClrValue>.Optimize(source); |
||||
|
|
||||
|
Assert.Equal("path != 1", result.ToString()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_invert_notequals_not_filter() |
||||
|
{ |
||||
|
var source = ClrFilter.Not(ClrFilter.Ne("path", 1)); |
||||
|
|
||||
|
var result = Optimizer<ClrValue>.Optimize(source); |
||||
|
|
||||
|
Assert.Equal("path == 1", result.ToString()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_not_convert_number_operator() |
||||
|
{ |
||||
|
var source = ClrFilter.Not(ClrFilter.Lt("path", 1)); |
||||
|
|
||||
|
var result = Optimizer<ClrValue>.Optimize(source); |
||||
|
|
||||
|
Assert.Equal("!(path < 1)", result.ToString()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue