Browse Source

Merge branch 'Odata' of github.com:dsbegnoche/squidex

pull/239/head
Sebastian Stehle 8 years ago
parent
commit
e14b755c01
  1. 59
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs
  2. 80
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
  3. 11
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs
  4. 67
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/ConstantVisitor.cs
  5. 57
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FilterBuilder.cs
  6. 155
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FilterVisitor.cs
  7. 88
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs
  8. 55
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/PropertyVisitor.cs
  9. 41
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/SearchTermVisitor.cs
  10. 60
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/SortBuilder.cs
  11. 55
      src/Squidex.Domain.Apps.Entities/Assets/Edm/EdmModelBuilder.cs
  12. 38
      src/Squidex.Domain.Apps.Entities/Assets/Edm/EdmModelExtensions.cs
  13. 4
      src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs
  14. 3
      src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs
  15. 12
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs
  16. 6
      src/Squidex.Domain.Apps.Entities/Contents/QueryContext.cs
  17. 11
      src/Squidex/Areas/Api/Config/Swagger/SwaggerServices.cs
  18. 32
      src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  19. 3
      src/Squidex/Config/Domain/ReadServices.cs
  20. 3
      src/Squidex/Config/Domain/StoreServices.cs
  21. 34
      src/Squidex/Docs/AddODataQueryParams.cs
  22. 10
      src/Squidex/app/shared/services/assets.service.spec.ts
  23. 15
      src/Squidex/app/shared/services/assets.service.ts
  24. 6
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs
  25. 36
      tools/Migrate_01/Migration04_FlattenAssetEntity.cs

59
src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs

@ -8,24 +8,71 @@
using System;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using Squidex.Domain.Apps.Entities.Assets.State;
using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb;
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
{
public sealed class MongoAssetEntity : IVersionedEntity<Guid>
public sealed class MongoAssetEntity :
MongoEntity,
IAssetEntity,
IUpdateableEntityWithVersion,
IUpdateableEntityWithCreatedBy,
IUpdateableEntityWithLastModifiedBy,
IUpdateableEntityWithAppRef
{
[BsonId]
[BsonRequired]
[BsonElement]
[BsonRepresentation(BsonType.String)]
public Guid Id { get; set; }
public string MimeType { get; set; }
[BsonRequired]
[BsonElement]
public string FileName { get; set; }
[BsonRequired]
public AssetState State { get; set; }
[BsonElement]
public long FileSize { get; set; }
[BsonRequired]
[BsonElement]
public long FileVersion { get; set; }
[BsonRequired]
[BsonElement]
public bool IsImage { get; set; }
[BsonRequired]
[BsonElement]
public long Version { get; set; }
[BsonRequired]
[BsonElement]
public int? PixelWidth { get; set; }
[BsonRequired]
[BsonElement]
public int? PixelHeight { get; set; }
[BsonRequired]
[BsonElement]
public Guid AppId { get; set; }
[BsonRequired]
[BsonElement]
public RefToken CreatedBy { get; set; }
[BsonRequired]
[BsonElement]
public RefToken LastModifiedBy { get; set; }
[BsonElement]
public bool IsDeleted { get; set; }
Guid IAssetInfo.AssetId
{
get { return Id; }
}
}
}

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

@ -9,10 +9,13 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using MongoDB.Bson;
using Microsoft.OData;
using Microsoft.OData.UriParser;
using MongoDB.Driver;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Assets.Edm;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors;
using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb;
@ -20,9 +23,12 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
{
public sealed partial class MongoAssetRepository : MongoRepositoryBase<MongoAssetEntity>, IAssetRepository
{
public MongoAssetRepository(IMongoDatabase database)
private readonly EdmModelBuilder modelBuilder;
public MongoAssetRepository(IMongoDatabase database, EdmModelBuilder modelBuilder)
: base(database)
{
this.modelBuilder = modelBuilder;
}
protected override string CollectionName()
@ -34,44 +40,40 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
{
return collection.Indexes.CreateOneAsync(
Index
.Ascending(x => x.State.AppId)
.Ascending(x => x.State.IsDeleted)
.Ascending(x => x.State.FileName)
.Ascending(x => x.State.MimeType)
.Descending(x => x.State.LastModified));
.Ascending(x => x.AppId)
.Ascending(x => x.IsDeleted)
.Ascending(x => x.FileName)
.Descending(x => x.LastModified));
}
public async Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, HashSet<string> mimeTypes = null, HashSet<Guid> ids = null, string query = null, int take = 10, int skip = 0)
public async Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, string query = null)
{
var filters = new List<FilterDefinition<MongoAssetEntity>>
{
Filter.Eq(x => x.State.AppId, appId),
Filter.Eq(x => x.State.IsDeleted, false)
};
if (ids != null && ids.Count > 0)
{
filters.Add(Filter.In(x => x.Id, ids));
}
var parsedQuery = ParseQuery(query);
if (mimeTypes != null && mimeTypes.Count > 0)
{
filters.Add(Filter.In(x => x.State.MimeType, mimeTypes));
}
var assetEntities =
await Collection
.Find(parsedQuery, appId)
.Skip(parsedQuery)
.Take(parsedQuery)
.SortByDescending(x => x.LastModified)
.ToListAsync();
if (!string.IsNullOrWhiteSpace(query))
{
filters.Add(Filter.Regex(x => x.State.FileName, new BsonRegularExpression(query, "i")));
}
var assetCount = await Collection.Find(parsedQuery, appId).CountAsync();
var filter = Filter.And(filters);
return ResultList.Create(assetEntities.OfType<IAssetEntity>().ToList(), assetCount);
}
var assetItems = Collection.Find(filter).Skip(skip).Limit(take).SortByDescending(x => x.State.LastModified).ToListAsync();
var assetCount = Collection.Find(filter).CountAsync();
public async Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, HashSet<Guid> ids)
{
var find = Collection
.Find(Filter.In(x => x.Id, ids))
.SortByDescending(x => x.LastModified);
await Task.WhenAll(assetItems, assetCount);
var assetEntities = find.ToListAsync();
var assetCount = find.CountAsync();
await Task.WhenAll(assetEntities, assetCount);
return ResultList.Create<IAssetEntity>(assetItems.Result.Select(x => x.State), assetCount.Result);
return ResultList.Create(assetEntities.Result.OfType<IAssetEntity>().ToList(), assetCount.Result);
}
public async Task<IAssetEntity> FindAssetAsync(Guid id)
@ -80,7 +82,21 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
await Collection.Find(x => x.Id == id)
.FirstOrDefaultAsync();
return assetEntity?.State;
return assetEntity;
}
private ODataUriParser ParseQuery(string query)
{
try
{
var model = modelBuilder.EdmModel;
return model.ParseQuery(query);
}
catch (ODataException ex)
{
throw new ValidationException($"Failed to parse query: {ex.Message}", ex);
}
}
}
}

11
src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs

@ -10,7 +10,7 @@ using System.Threading.Tasks;
using MongoDB.Driver;
using Squidex.Domain.Apps.Entities.Assets.State;
using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
@ -25,15 +25,18 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
if (existing != null)
{
return (existing.State, existing.Version);
return (SimpleMapper.Map(existing, new AssetState()), existing.Version);
}
return (null, EtagVersion.NotFound);
}
public Task WriteAsync(Guid key, AssetState value, long oldVersion, long newVersion)
public async Task WriteAsync(Guid key, AssetState value, long oldVersion, long newVersion)
{
return Collection.UpsertVersionedAsync(key, oldVersion, newVersion, u => u.Set(x => x.State, value));
var entity = SimpleMapper.Map(value, new MongoAssetEntity());
entity.Version = newVersion;
await Collection.ReplaceOneAsync(x => x.Id == key && x.Version == oldVersion, entity, Upsert);
}
}
}

67
src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/ConstantVisitor.cs

@ -0,0 +1,67 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Microsoft.OData.Edm;
using Microsoft.OData.UriParser;
using NodaTime;
using NodaTime.Text;
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors
{
public sealed class ConstantVisitor : QueryNodeVisitor<object>
{
private static readonly ConstantVisitor Instance = new ConstantVisitor();
private ConstantVisitor()
{
}
public static object Visit(QueryNode node)
{
return node.Accept(Instance);
}
public override object Visit(ConvertNode nodeIn)
{
var booleanType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Boolean);
if (nodeIn.TypeReference.Definition == booleanType)
{
return bool.Parse(Visit(nodeIn.Source).ToString());
}
var dateTimeType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.DateTimeOffset);
if (nodeIn.TypeReference.Definition == dateTimeType)
{
var value = Visit(nodeIn.Source);
if (value is DateTimeOffset dateTimeOffset)
{
return Instant.FromDateTimeOffset(dateTimeOffset);
}
return InstantPattern.General.Parse(Visit(nodeIn.Source).ToString()).Value;
}
var guidType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Guid);
if (nodeIn.TypeReference.Definition == guidType)
{
return Guid.Parse(Visit(nodeIn.Source).ToString());
}
return base.Visit(nodeIn);
}
public override object Visit(ConstantNode nodeIn)
{
return nodeIn.Value;
}
}
}

57
src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FilterBuilder.cs

@ -0,0 +1,57 @@
// ==========================================================================
// FilterBuilder.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using Microsoft.OData;
using Microsoft.OData.UriParser;
using MongoDB.Driver;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors
{
public static class FilterBuilder
{
private static readonly FilterDefinitionBuilder<MongoAssetEntity> Filter = Builders<MongoAssetEntity>.Filter;
public static List<FilterDefinition<MongoAssetEntity>> Build(ODataUriParser query)
{
List<FilterDefinition<MongoAssetEntity>> filters = new List<FilterDefinition<MongoAssetEntity>>();
SearchClause search;
try
{
search = query.ParseSearch();
}
catch (ODataException ex)
{
throw new ValidationException("Query $search clause not valid.", new ValidationError(ex.Message));
}
if (search != null)
{
filters.Add(Filter.Text(SearchTermVisitor.Visit(search.Expression).ToString()));
}
FilterClause filter;
try
{
filter = query.ParseFilter();
}
catch (ODataException ex)
{
throw new ValidationException("Query $filter clause not valid.", new ValidationError(ex.Message));
}
if (filter != null)
{
filters.Add(FilterVisitor.Visit(filter.Expression));
}
return filters;
}
}
}

155
src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FilterVisitor.cs

@ -0,0 +1,155 @@
// ==========================================================================
// FilterVisitor.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Linq;
using Microsoft.OData.UriParser;
using MongoDB.Bson;
using MongoDB.Driver;
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors
{
public class FilterVisitor : QueryNodeVisitor<FilterDefinition<MongoAssetEntity>>
{
private static readonly FilterDefinitionBuilder<MongoAssetEntity> Filter = Builders<MongoAssetEntity>.Filter;
public static FilterDefinition<MongoAssetEntity> Visit(QueryNode node)
{
var visitor = new FilterVisitor();
return node.Accept(visitor);
}
public override FilterDefinition<MongoAssetEntity> Visit(ConvertNode nodeIn)
{
return nodeIn.Source.Accept(this);
}
public override FilterDefinition<MongoAssetEntity> Visit(UnaryOperatorNode nodeIn)
{
if (nodeIn.OperatorKind == UnaryOperatorKind.Not)
{
return Filter.Not(nodeIn.Operand.Accept(this));
}
throw new NotSupportedException();
}
public override FilterDefinition<MongoAssetEntity> Visit(SingleValueFunctionCallNode nodeIn)
{
var fieldNode = nodeIn.Parameters.ElementAt(0);
var valueNode = nodeIn.Parameters.ElementAt(1);
if (string.Equals(nodeIn.Name, "endswith", StringComparison.OrdinalIgnoreCase))
{
var value = BuildRegex(valueNode, v => v + "$");
return Filter.Regex(BuildFieldDefinition(fieldNode), value);
}
if (string.Equals(nodeIn.Name, "startswith", StringComparison.OrdinalIgnoreCase))
{
var value = BuildRegex(valueNode, v => "^" + v);
return Filter.Regex(BuildFieldDefinition(fieldNode), value);
}
if (string.Equals(nodeIn.Name, "contains", StringComparison.OrdinalIgnoreCase))
{
var value = BuildRegex(valueNode, v => v);
return Filter.Regex(BuildFieldDefinition(fieldNode), value);
}
throw new NotSupportedException();
}
public override FilterDefinition<MongoAssetEntity> Visit(BinaryOperatorNode nodeIn)
{
if (nodeIn.OperatorKind == BinaryOperatorKind.And)
{
return Filter.And(nodeIn.Left.Accept(this), nodeIn.Right.Accept(this));
}
if (nodeIn.OperatorKind == BinaryOperatorKind.Or)
{
return Filter.Or(nodeIn.Left.Accept(this), nodeIn.Right.Accept(this));
}
if (nodeIn.Left is SingleValueFunctionCallNode functionNode)
{
var regexFilter = Visit(functionNode);
var value = BuildValue(nodeIn.Right);
if (value is bool booleanRight)
{
if ((nodeIn.OperatorKind == BinaryOperatorKind.Equal && !booleanRight) ||
(nodeIn.OperatorKind == BinaryOperatorKind.NotEqual && booleanRight))
{
regexFilter = Filter.Not(regexFilter);
}
return regexFilter;
}
}
else
{
if (nodeIn.OperatorKind == BinaryOperatorKind.NotEqual)
{
var field = BuildFieldDefinition(nodeIn.Left);
return Filter.Or(
Filter.Not(Filter.Exists(field)),
Filter.Ne(field, BuildValue(nodeIn.Right)));
}
if (nodeIn.OperatorKind == BinaryOperatorKind.Equal)
{
return Filter.Eq(BuildFieldDefinition(nodeIn.Left), BuildValue(nodeIn.Right));
}
if (nodeIn.OperatorKind == BinaryOperatorKind.LessThan)
{
return Filter.Lt(BuildFieldDefinition(nodeIn.Left), BuildValue(nodeIn.Right));
}
if (nodeIn.OperatorKind == BinaryOperatorKind.LessThanOrEqual)
{
return Filter.Lte(BuildFieldDefinition(nodeIn.Left), BuildValue(nodeIn.Right));
}
if (nodeIn.OperatorKind == BinaryOperatorKind.GreaterThan)
{
return Filter.Gt(BuildFieldDefinition(nodeIn.Left), BuildValue(nodeIn.Right));
}
if (nodeIn.OperatorKind == BinaryOperatorKind.GreaterThanOrEqual)
{
return Filter.Gte(BuildFieldDefinition(nodeIn.Left), BuildValue(nodeIn.Right));
}
}
throw new NotSupportedException();
}
private static BsonRegularExpression BuildRegex(QueryNode node, Func<string, string> formatter)
{
return new BsonRegularExpression(formatter(BuildValue(node).ToString()), "i");
}
private FieldDefinition<MongoAssetEntity, object> BuildFieldDefinition(QueryNode nodeIn)
{
return PropertyVisitor.Visit(nodeIn);
}
private static object BuildValue(QueryNode nodeIn)
{
return ConstantVisitor.Visit(nodeIn);
}
}
}

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

@ -0,0 +1,88 @@
// ==========================================================================
// FindExtensions.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using Microsoft.OData.UriParser;
using MongoDB.Bson;
using MongoDB.Driver;
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors
{
public static class FindExtensions
{
private static readonly FilterDefinitionBuilder<MongoAssetEntity> Filter = Builders<MongoAssetEntity>.Filter;
public static IFindFluent<MongoAssetEntity, MongoAssetEntity> Sort(this IFindFluent<MongoAssetEntity, MongoAssetEntity> cursor, ODataUriParser query)
{
return cursor.Sort(SortBuilder.BuildSort(query));
}
public static IFindFluent<MongoAssetEntity, MongoAssetEntity> Take(this IFindFluent<MongoAssetEntity, MongoAssetEntity> cursor, ODataUriParser query)
{
var top = query.ParseTop();
if (top.HasValue)
{
cursor = cursor.Limit(Math.Min((int)top.Value, 200));
}
else
{
cursor = cursor.Limit(20);
}
return cursor;
}
public static IFindFluent<MongoAssetEntity, MongoAssetEntity> Skip(this IFindFluent<MongoAssetEntity, MongoAssetEntity> cursor, ODataUriParser query)
{
var skip = query.ParseSkip();
if (skip.HasValue)
{
cursor = cursor.Skip((int)skip.Value);
}
else
{
cursor = cursor.Skip(null);
}
return cursor;
}
public static IFindFluent<MongoAssetEntity, MongoAssetEntity> Find(this IMongoCollection<MongoAssetEntity> cursor, ODataUriParser query, Guid appId)
{
var filter = BuildQuery(query, appId);
return cursor.Find(filter);
}
public static FilterDefinition<MongoAssetEntity> BuildQuery(ODataUriParser query, Guid appId)
{
var filters = new List<FilterDefinition<MongoAssetEntity>>
{
Filter.Eq(x => x.AppId, appId),
Filter.Eq(x => x.IsDeleted, false)
};
filters.AddRange(FilterBuilder.Build(query));
if (filters.Count > 1)
{
return Filter.And(filters);
}
else if (filters.Count == 1)
{
return filters[0];
}
else
{
return new BsonDocument();
}
}
}
}

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

@ -0,0 +1,55 @@
// ==========================================================================
// PropertyVisitor.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Immutable;
using System.Linq;
using Microsoft.OData.UriParser;
using MongoDB.Driver;
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors
{
public sealed class PropertyVisitor : QueryNodeVisitor<ImmutableList<string>>
{
private static readonly PropertyVisitor Instance = new PropertyVisitor();
public static StringFieldDefinition<MongoAssetEntity, object> Visit(QueryNode node)
{
var propertyNames = node.Accept(Instance).ToArray();
return new StringFieldDefinition<MongoAssetEntity, object>(propertyNames.First());
}
public override ImmutableList<string> Visit(ConvertNode nodeIn)
{
return nodeIn.Source.Accept(this);
}
public override ImmutableList<string> Visit(SingleComplexNode nodeIn)
{
if (nodeIn.Source is SingleComplexNode)
{
return nodeIn.Source.Accept(this).Add(nodeIn.Property.Name);
}
else
{
return ImmutableList.Create(nodeIn.Property.Name);
}
}
public override ImmutableList<string> Visit(SingleValuePropertyAccessNode nodeIn)
{
if (nodeIn.Source is SingleComplexNode)
{
return nodeIn.Source.Accept(this).Add(nodeIn.Property.Name);
}
else
{
return ImmutableList.Create(nodeIn.Property.Name);
}
}
}
}

41
src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/SearchTermVisitor.cs

@ -0,0 +1,41 @@
// ==========================================================================
// SearchTermVisitor.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Microsoft.OData.UriParser;
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors
{
public class SearchTermVisitor : QueryNodeVisitor<string>
{
private static readonly SearchTermVisitor Instance = new SearchTermVisitor();
private SearchTermVisitor()
{
}
public static object Visit(QueryNode node)
{
return node.Accept(Instance);
}
public override string Visit(BinaryOperatorNode nodeIn)
{
if (nodeIn.OperatorKind == BinaryOperatorKind.And)
{
return nodeIn.Left.Accept(this) + " " + nodeIn.Right.Accept(this);
}
throw new NotSupportedException();
}
public override string Visit(SearchTermNode nodeIn)
{
return nodeIn.Text;
}
}
}

60
src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/SortBuilder.cs

@ -0,0 +1,60 @@
// ==========================================================================
// SortBuilder.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using Microsoft.OData.UriParser;
using MongoDB.Driver;
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors
{
public static class SortBuilder
{
private static readonly SortDefinitionBuilder<MongoAssetEntity> Sort = Builders<MongoAssetEntity>.Sort;
public static SortDefinition<MongoAssetEntity> BuildSort(ODataUriParser query)
{
var orderBy = query.ParseOrderBy();
if (orderBy != null)
{
var sorts = new List<SortDefinition<MongoAssetEntity>>();
while (orderBy != null)
{
sorts.Add(OrderBy(orderBy));
orderBy = orderBy.ThenBy;
}
if (sorts.Count > 1)
{
return Sort.Combine(sorts);
}
else
{
return sorts[0];
}
}
else
{
return Sort.Descending(x => x.LastModified);
}
}
public static SortDefinition<MongoAssetEntity> OrderBy(OrderByClause clause)
{
if (clause.Direction == OrderByDirection.Ascending)
{
return Sort.Ascending(PropertyVisitor.Visit(clause.Expression));
}
else
{
return Sort.Descending(PropertyVisitor.Visit(clause.Expression));
}
}
}
}

55
src/Squidex.Domain.Apps.Entities/Assets/Edm/EdmModelBuilder.cs

@ -0,0 +1,55 @@
// ==========================================================================
// EdmModelBuilder.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Microsoft.OData.Edm;
using Squidex.Domain.Apps.Entities.Assets.State;
namespace Squidex.Domain.Apps.Entities.Assets.Edm
{
public class EdmModelBuilder
{
private readonly IEdmModel edmModel;
public EdmModelBuilder()
{
edmModel = BuildEdmModel();
}
public virtual IEdmModel EdmModel
{
get { return edmModel; }
}
private IEdmModel BuildEdmModel()
{
var model = new EdmModel();
var container = new EdmEntityContainer("Squidex", "Container");
var entityType = new EdmEntityType("Squidex", "Asset");
entityType.AddStructuralProperty(nameof(AssetState.Id), EdmPrimitiveTypeKind.Guid);
entityType.AddStructuralProperty(nameof(AssetState.AppId), EdmPrimitiveTypeKind.Guid);
entityType.AddStructuralProperty(nameof(AssetState.Created), EdmPrimitiveTypeKind.DateTimeOffset);
entityType.AddStructuralProperty(nameof(AssetState.CreatedBy), EdmPrimitiveTypeKind.String);
entityType.AddStructuralProperty(nameof(AssetState.LastModified), EdmPrimitiveTypeKind.DateTimeOffset);
entityType.AddStructuralProperty(nameof(AssetState.LastModifiedBy), EdmPrimitiveTypeKind.String);
entityType.AddStructuralProperty(nameof(AssetState.Version), EdmPrimitiveTypeKind.Int64);
entityType.AddStructuralProperty(nameof(AssetState.FileName), EdmPrimitiveTypeKind.String);
entityType.AddStructuralProperty(nameof(AssetState.FileSize), EdmPrimitiveTypeKind.Int64);
entityType.AddStructuralProperty(nameof(AssetState.FileVersion), EdmPrimitiveTypeKind.Int64);
entityType.AddStructuralProperty(nameof(AssetState.IsImage), EdmPrimitiveTypeKind.Boolean);
entityType.AddStructuralProperty(nameof(AssetState.MimeType), EdmPrimitiveTypeKind.String);
entityType.AddStructuralProperty(nameof(AssetState.PixelHeight), EdmPrimitiveTypeKind.Int32);
entityType.AddStructuralProperty(nameof(AssetState.PixelWidth), EdmPrimitiveTypeKind.Int32);
model.AddElement(container);
model.AddElement(entityType);
container.AddEntitySet("AssetSet", entityType);
return model;
}
}
}

38
src/Squidex.Domain.Apps.Entities/Assets/Edm/EdmModelExtensions.cs

@ -0,0 +1,38 @@
// ==========================================================================
// EdmModelExtensions.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Linq;
using Microsoft.OData.Edm;
using Microsoft.OData.UriParser;
namespace Squidex.Domain.Apps.Entities.Assets.Edm
{
public static class EdmModelExtensions
{
public static ODataUriParser ParseQuery(this IEdmModel model, string query)
{
if (!model.EntityContainer.EntitySets().Any())
{
return null;
}
query = query ?? string.Empty;
var path = model.EntityContainer.EntitySets().First().Path.Path.Split('.').Last();
if (query.StartsWith("?", StringComparison.Ordinal))
{
query = query.Substring(1);
}
var parser = new ODataUriParser(model, new Uri($"{path}?{query}", UriKind.Relative));
return parser;
}
}
}

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

@ -14,7 +14,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Repositories
{
public interface IAssetRepository
{
Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, HashSet<string> mimeTypes = null, HashSet<Guid> ids = null, string query = null, int take = 10, int skip = 0);
Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, string query = null);
Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, HashSet<Guid> ids);
Task<IAssetEntity> FindAssetAsync(Guid id);
}

3
src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs

@ -8,6 +8,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.EnrichContent;
using Squidex.Domain.Apps.Core.Scripting;
@ -106,7 +107,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
private async Task<IReadOnlyList<IAssetInfo>> QueryAssetsAsync(Guid appId, IEnumerable<Guid> assetIds)
{
return await assetRepository.QueryAsync(appId, null, new HashSet<Guid>(assetIds), null, int.MaxValue, 0);
return await assetRepository.QueryAsync(appId, new HashSet<Guid>(assetIds));
}
private async Task<IReadOnlyList<Guid>> QueryContentsAsync(Guid appId, Guid schemaId, IEnumerable<Guid> contentIds)

12
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs

@ -82,11 +82,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
ResolvedType = new ListGraphType(new NonNullGraphType(assetType)),
Resolver = ResolveAsync((c, e) =>
{
var argTake = c.GetArgument("take", 20);
var argSkip = c.GetArgument("skip", 0);
var argQuery = c.GetArgument("search", string.Empty);
var assetQuery = BuildODataQuery(c);
return e.QueryAssetsAsync(argQuery, argSkip, argTake);
return e.QueryAssetsAsync(assetQuery);
}),
Description = "Get assets."
});
@ -98,11 +96,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
ResolvedType = new AssetsResultGraphType(assetType),
Resolver = ResolveAsync((c, e) =>
{
var argTake = c.GetArgument("take", 20);
var argSkip = c.GetArgument("skip", 0);
var argQuery = c.GetArgument("search", string.Empty);
var assetQuery = BuildODataQuery(c);
return e.QueryAssetsAsync(argQuery, argSkip, argTake);
return e.QueryAssetsAsync(assetQuery);
}),
Description = "Get assets and total count."
});

6
src/Squidex.Domain.Apps.Entities/Contents/QueryContext.cs

@ -80,9 +80,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
return content;
}
public async Task<IResultList<IAssetEntity>> QueryAssetsAsync(string query, int skip = 0, int take = 10)
public async Task<IResultList<IAssetEntity>> QueryAssetsAsync(string query)
{
var assets = await assetRepository.QueryAsync(app.Id, null, null, query, take, skip);
var assets = await assetRepository.QueryAsync(app.Id, query);
foreach (var asset in assets)
{
@ -112,7 +112,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (notLoadedAssets.Count > 0)
{
var assets = await assetRepository.QueryAsync(app.Id, null, notLoadedAssets, null, int.MaxValue);
var assets = await assetRepository.QueryAsync(app.Id, notLoadedAssets);
foreach (var asset in assets)
{

11
src/Squidex/Areas/Api/Config/Swagger/SwaggerServices.cs

@ -29,7 +29,16 @@ namespace Squidex.Areas.Api.Config.Swagger
var urlOptions = s.GetService<IOptions<MyUrlsOptions>>().Value;
var settings =
new SwaggerSettings { Title = "Squidex API", Version = "1.0", IsAspNetCore = false }
new SwaggerSettings
{
Title = "Squidex API",
Version = "1.0",
IsAspNetCore = false,
OperationProcessors =
{
new Docs.AddODataQueryParams()
}
}
.ConfigurePaths(urlOptions)
.ConfigureSchemaSettings()
.ConfigureIdentity(urlOptions);

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

@ -59,11 +59,6 @@ namespace Squidex.Areas.Api.Controllers.Assets
/// Get assets.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="ids">The optional asset ids.</param>
/// <param name="skip">Optional number of assets to skip.</param>
/// <param name="take">Optional number of assets to take (Default: 20).</param>
/// <param name="query">Optional query to limit the files by name.</param>
/// <param name="mimeTypes">Comma separated list of mime types to get.</param>
/// <returns>
/// 200 => Assets returned.
/// 404 => App not found.
@ -76,32 +71,9 @@ namespace Squidex.Areas.Api.Controllers.Assets
[Route("apps/{app}/assets/")]
[ProducesResponseType(typeof(AssetsDto), 200)]
[ApiCosts(1)]
public async Task<IActionResult> GetAssets(string app, [FromQuery] string query = null, [FromQuery] string mimeTypes = null, [FromQuery] string ids = null, [FromQuery] int skip = 0, [FromQuery] int take = 20)
public async Task<IActionResult> GetAssets(string app)
{
var mimeTypeList = new HashSet<string>();
if (!string.IsNullOrWhiteSpace(mimeTypes))
{
foreach (var mimeType in mimeTypes.Split(','))
{
mimeTypeList.Add(mimeType.Trim());
}
}
var idsList = new HashSet<Guid>();
if (!string.IsNullOrWhiteSpace(ids))
{
foreach (var id in ids.Split(','))
{
if (Guid.TryParse(id, out var guid))
{
idsList.Add(guid);
}
}
}
var assets = await assetRepository.QueryAsync(App.Id, mimeTypeList, idsList, query, take, skip);
var assets = await assetRepository.QueryAsync(App.Id, Request.QueryString.ToString());
var response = new AssetsDto
{

3
src/Squidex/Config/Domain/ReadServices.cs

@ -120,6 +120,9 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<EdmModelBuilder>()
.AsSelf();
services.AddSingletonAs<Squidex.Domain.Apps.Entities.Assets.Edm.EdmModelBuilder>()
.AsSelf();
services.AddSingletonAs<RuleService>()
.AsSelf();
}

3
src/Squidex/Config/Domain/StoreServices.cs

@ -16,6 +16,7 @@ using Newtonsoft.Json;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps.Repositories;
using Squidex.Domain.Apps.Entities.Apps.State;
using Squidex.Domain.Apps.Entities.Assets.Edm;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.Assets.State;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
@ -101,7 +102,7 @@ namespace Squidex.Config.Domain
.As<ISnapshotStore<AppState, Guid>>()
.As<IInitializable>();
services.AddSingletonAs(c => new MongoAssetRepository(mongoDatabase))
services.AddSingletonAs(c => new MongoAssetRepository(mongoDatabase, c.GetRequiredService<EdmModelBuilder>()))
.As<IAssetRepository>()
.As<ISnapshotStore<AssetState, Guid>>()
.As<IInitializable>();

34
src/Squidex/Docs/AddODataQueryParams.cs

@ -0,0 +1,34 @@
// ==========================================================================
// AddODataQueryParams.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Threading.Tasks;
using NJsonSchema;
using NSwag.SwaggerGeneration.Processors;
using NSwag.SwaggerGeneration.Processors.Contexts;
using Squidex.Infrastructure.Tasks;
using Squidex.Pipeline.Swagger;
namespace Squidex.Docs
{
public class AddODataQueryParams : IOperationProcessor
{
public Task<bool> ProcessAsync(OperationProcessorContext context)
{
if (context.OperationDescription.Path == "/apps/{app}/assets")
{
context.OperationDescription.Operation.AddQueryParameter("$top", JsonObjectType.Number, "Optional number of contents to take.");
context.OperationDescription.Operation.AddQueryParameter("$skip", JsonObjectType.Number, "Optional number of contents to skip.");
context.OperationDescription.Operation.AddQueryParameter("$search", JsonObjectType.String, "Optional OData full text search.");
context.OperationDescription.Operation.AddQueryParameter("$orderby", JsonObjectType.String, "Optional OData order definition.");
context.OperationDescription.Operation.AddQueryParameter("$filter", JsonObjectType.String, "Optional OData filter definition.");
}
return TaskHelper.True;
}
}
}

10
src/Squidex/app/shared/services/assets.service.spec.ts

@ -88,8 +88,8 @@ describe('AssetsService', () => {
assets = result;
});
const req = httpMock.expectOne('http://service/p/api/apps/my-app/assets?take=17&skip=13');
const req = httpMock.expectOne('http://service/p/api/apps/my-app/assets?$top=17&$skip=13');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
@ -219,7 +219,7 @@ describe('AssetsService', () => {
assetsService.getAssets('my-app', 17, 13, 'my-query').subscribe();
const req = httpMock.expectOne('http://service/p/api/apps/my-app/assets?query=my-query&take=17&skip=13');
const req = httpMock.expectOne('http://service/p/api/apps/my-app/assets?$search=my-query&$top=17&$skip=13');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
@ -232,7 +232,7 @@ describe('AssetsService', () => {
assetsService.getAssets('my-app', 17, 13, undefined, ['image/png', 'image/png']).subscribe();
const req = httpMock.expectOne('http://service/p/api/apps/my-app/assets?mimeTypes=image/png,image/png&take=17&skip=13');
const req = httpMock.expectOne(`http://service/p/api/apps/my-app/assets?$filter=MimeType eq 'image/png' or MimeType eq 'image/png'&$top=17&$skip=13`);
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
@ -245,7 +245,7 @@ describe('AssetsService', () => {
assetsService.getAssets('my-app', 17, 13, undefined, undefined, ['12', '23']).subscribe();
const req = httpMock.expectOne('http://service/p/api/apps/my-app/assets?ids=12,23&take=17&skip=13');
const req = httpMock.expectOne('http://service/p/api/apps/my-app/assets?$filter=Id eq 12 or Id eq 23&$top=17&$skip=13');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();

15
src/Squidex/app/shared/services/assets.service.ts

@ -111,21 +111,26 @@ export class AssetsService {
public getAssets(appName: string, take: number, skip: number, query?: string, mimeTypes?: string[], ids?: string[]): Observable<AssetsDto> {
const queries: string[] = [];
const filters: string[] = [];
if (mimeTypes && mimeTypes.length > 0) {
queries.push(`mimeTypes=${mimeTypes.join(',')}`);
filters.push(mimeTypes.map(mimeType => `MimeType eq '${mimeType}'`).join(' or '));
}
if (ids && ids.length > 0) {
queries.push(`ids=${ids.join(',')}`);
filters.push(ids.map(id => `Id eq ${id}`).join(' or '));
}
if (filters.length > 0) {
queries.push(`$filter=${filters.join(' and ')}`);
}
if (query && query.length > 0) {
queries.push(`query=${query}`);
queries.push(`$search=${encodeURIComponent(query)}`);
}
queries.push(`take=${take}`);
queries.push(`skip=${skip}`);
queries.push(`$top=${take}`);
queries.push(`$skip=${skip}`);
const fullQuery = queries.join('&');

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

@ -65,7 +65,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var assets = new List<IAssetEntity> { asset };
A.CallTo(() => assetRepository.QueryAsync(app.Id, null, null, "my-query", 30, 5))
A.CallTo(() => assetRepository.QueryAsync(app.Id, "?$take=30&$skip=5&$search=my-query"))
.Returns(ResultList.Create(assets, 0));
var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query });
@ -134,7 +134,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var assets = new List<IAssetEntity> { asset };
A.CallTo(() => assetRepository.QueryAsync(app.Id, null, null, "my-query", 30, 5))
A.CallTo(() => assetRepository.QueryAsync(app.Id, "?$take=30&$skip=5&$search=my-query"))
.Returns(ResultList.Create(assets, 10));
var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query });
@ -667,7 +667,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId, EtagVersion.Any))
.Returns((schema, content));
A.CallTo(() => assetRepository.QueryAsync(app.Id, null, A<HashSet<Guid>>.That.Matches(x => x.Contains(assetRefId)), null, int.MaxValue, 0))
A.CallTo(() => assetRepository.QueryAsync(app.Id, A<HashSet<Guid>>.That.Matches(x => x.Contains(assetRefId))))
.Returns(ResultList.Create(refAssets, 0));
var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query });

36
tools/Migrate_01/Migration04_FlattenAssetEntity.cs

@ -0,0 +1,36 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Infrastructure.Migrations;
namespace Migrate_01
{
public class Migration04_FlattenAssetEntity : IMigration
{
private readonly Rebuilder rebuilder;
public int FromVersion { get; } = 3;
public int ToVersion { get; } = 4;
public Migration04_FlattenAssetEntity(Rebuilder rebuilder)
{
this.rebuilder = rebuilder;
}
public async Task UpdateAsync(IEnumerable<IMigration> previousMigrations)
{
if (!previousMigrations.Any(x => x is Migration01_FromCqrs))
{
await rebuilder.RebuildAssetsAsync();
}
}
}
}
Loading…
Cancel
Save