From bc1a784097b9ddf09e5a52cf89e461612162caf8 Mon Sep 17 00:00:00 2001 From: Derek Begnoche Date: Thu, 25 Jan 2018 17:03:55 -0600 Subject: [PATCH 1/4] Adding Odata For assets Need GraphQL Changes --- .../Assets/MongoAssetEntity.cs | 59 ++++++- .../Assets/MongoAssetRepository.cs | 76 +++++---- .../MongoAssetRepository_SnapshotStore.cs | 11 +- .../Assets/Visitors/ConstantVisitor.cs | 67 ++++++++ .../Assets/Visitors/FilterBuilder.cs | 57 +++++++ .../Assets/Visitors/FilterVisitor.cs | 155 ++++++++++++++++++ .../Assets/Visitors/FindExtensions.cs | 88 ++++++++++ .../Assets/Visitors/PropertyVisitor.cs | 55 +++++++ .../Assets/Visitors/SearchTermVisitor.cs | 41 +++++ .../Assets/Visitors/SortBuilder.cs | 60 +++++++ .../Assets/Edm/EdmModelBuilder.cs | 61 +++++++ .../Assets/Edm/EdmModelExtensions.cs | 38 +++++ .../Assets/Repositories/IAssetRepository.cs | 2 +- .../Contents/ContentOperationContext.cs | 16 +- .../GraphQL/Types/AppQueriesGraphType.cs | 12 +- .../Contents/QueryContext.cs | 6 +- .../Api/Config/Swagger/SwaggerServices.cs | 11 +- .../Controllers/Assets/AssetsController.cs | 32 +--- src/Squidex/Config/Domain/ReadServices.cs | 3 + src/Squidex/Config/Domain/StoreServices.cs | 3 +- src/Squidex/Docs/AddODataQueryParams.cs | 34 ++++ .../app/shared/services/assets.service.ts | 15 +- .../Contents/GraphQL/GraphQLQueriesTests.cs | 6 +- .../Migration04_FlattenAssetEntity.cs | 36 ++++ 24 files changed, 845 insertions(+), 99 deletions(-) create mode 100644 src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/ConstantVisitor.cs create mode 100644 src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FilterBuilder.cs create mode 100644 src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FilterVisitor.cs create mode 100644 src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs create mode 100644 src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/PropertyVisitor.cs create mode 100644 src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/SearchTermVisitor.cs create mode 100644 src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/SortBuilder.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Assets/Edm/EdmModelBuilder.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Assets/Edm/EdmModelExtensions.cs create mode 100644 src/Squidex/Docs/AddODataQueryParams.cs create mode 100644 tools/Migrate_01/Migration04_FlattenAssetEntity.cs diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs index 7a39e0329..468bd2110 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs +++ b/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 + 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; } + } } } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs index 1b3abce9c..87e593bfb 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs @@ -6,13 +6,16 @@ // ========================================================================== 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.Assets.State; +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, 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,28 @@ 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) + .Ascending(x => x.MimeType) + .Descending(x => x.LastModified)); } - public async Task> QueryAsync(Guid appId, HashSet mimeTypes = null, HashSet ids = null, string query = null, int take = 10, int skip = 0) + public async Task> QueryAsync(Guid appId, string query = null) { - var filters = new List> - { - 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)); - } - - if (mimeTypes != null && mimeTypes.Count > 0) - { - filters.Add(Filter.In(x => x.State.MimeType, mimeTypes)); - } - - if (!string.IsNullOrWhiteSpace(query)) - { - filters.Add(Filter.Regex(x => x.State.FileName, new BsonRegularExpression(query, "i"))); - } - - var filter = Filter.And(filters); + var parsedQuery = ParseQuery(query); - var assetItems = Collection.Find(filter).Skip(skip).Limit(take).SortByDescending(x => x.State.LastModified).ToListAsync(); - var assetCount = Collection.Find(filter).CountAsync(); + var assetEntities = + await Collection + .Find(parsedQuery, appId) + .Skip(parsedQuery) + .Take(parsedQuery) + .SortByDescending(x => x.LastModified) + .ToListAsync(); - await Task.WhenAll(assetItems, assetCount); + var assetCount = await Collection.Find(parsedQuery, appId).CountAsync(); - return ResultList.Create(assetItems.Result.Select(x => x.State), assetCount.Result); + return ResultList.Create(assetEntities.OfType().ToList(), assetCount); } public async Task FindAssetAsync(Guid id) @@ -80,7 +70,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.BuildEdmModel(new AssetState()); + + return model.ParseQuery(query); + } + catch (ODataException ex) + { + throw new ValidationException($"Failed to parse query: {ex.Message}", ex); + } } } } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs index d181e97ca..89bdc2c09 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs +++ b/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); } } } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/ConstantVisitor.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/ConstantVisitor.cs new file mode 100644 index 000000000..e6978a74d --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/ConstantVisitor.cs @@ -0,0 +1,67 @@ +// ========================================================================== +// ConstantVisitor.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== +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 + { + 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; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FilterBuilder.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FilterBuilder.cs new file mode 100644 index 000000000..00e192f2e --- /dev/null +++ b/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 Filter = Builders.Filter; + + public static List> Build(ODataUriParser query) + { + List> filters = new List>(); + + SearchClause search; + try + { + search = query.ParseSearch(); + } + catch (ODataException ex) + { + throw new ValidationException("Query $search clause not valid.", new ValidationError(ex.Message)); + } + + if (search != null) + { + filters.Add(Filter.Text(SearchTermVisitor.Visit(search.Expression).ToString())); + } + + FilterClause filter; + try + { + filter = query.ParseFilter(); + } + catch (ODataException ex) + { + throw new ValidationException("Query $filter clause not valid.", new ValidationError(ex.Message)); + } + + if (filter != null) + { + filters.Add(FilterVisitor.Visit(filter.Expression)); + } + + return filters; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FilterVisitor.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FilterVisitor.cs new file mode 100644 index 000000000..2ef2e94eb --- /dev/null +++ b/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> + { + private static readonly FilterDefinitionBuilder Filter = Builders.Filter; + + public static FilterDefinition Visit(QueryNode node) + { + var visitor = new FilterVisitor(); + + return node.Accept(visitor); + } + + public override FilterDefinition Visit(ConvertNode nodeIn) + { + return nodeIn.Source.Accept(this); + } + + public override FilterDefinition Visit(UnaryOperatorNode nodeIn) + { + if (nodeIn.OperatorKind == UnaryOperatorKind.Not) + { + return Filter.Not(nodeIn.Operand.Accept(this)); + } + + throw new NotSupportedException(); + } + + public override FilterDefinition Visit(SingleValueFunctionCallNode nodeIn) + { + var fieldNode = nodeIn.Parameters.ElementAt(0); + var valueNode = nodeIn.Parameters.ElementAt(1); + + if (string.Equals(nodeIn.Name, "endswith", StringComparison.OrdinalIgnoreCase)) + { + var value = BuildRegex(valueNode, v => v + "$"); + + return Filter.Regex(BuildFieldDefinition(fieldNode), value); + } + + if (string.Equals(nodeIn.Name, "startswith", StringComparison.OrdinalIgnoreCase)) + { + var value = BuildRegex(valueNode, v => "^" + v); + + return Filter.Regex(BuildFieldDefinition(fieldNode), value); + } + + if (string.Equals(nodeIn.Name, "contains", StringComparison.OrdinalIgnoreCase)) + { + var value = BuildRegex(valueNode, v => v); + + return Filter.Regex(BuildFieldDefinition(fieldNode), value); + } + + throw new NotSupportedException(); + } + + public override FilterDefinition Visit(BinaryOperatorNode nodeIn) + { + if (nodeIn.OperatorKind == BinaryOperatorKind.And) + { + return Filter.And(nodeIn.Left.Accept(this), nodeIn.Right.Accept(this)); + } + + if (nodeIn.OperatorKind == BinaryOperatorKind.Or) + { + return Filter.Or(nodeIn.Left.Accept(this), nodeIn.Right.Accept(this)); + } + + if (nodeIn.Left is SingleValueFunctionCallNode functionNode) + { + var regexFilter = Visit(functionNode); + + var value = BuildValue(nodeIn.Right); + + if (value is bool booleanRight) + { + if ((nodeIn.OperatorKind == BinaryOperatorKind.Equal && !booleanRight) || + (nodeIn.OperatorKind == BinaryOperatorKind.NotEqual && booleanRight)) + { + regexFilter = Filter.Not(regexFilter); + } + + return regexFilter; + } + } + else + { + if (nodeIn.OperatorKind == BinaryOperatorKind.NotEqual) + { + var field = BuildFieldDefinition(nodeIn.Left); + + return Filter.Or( + Filter.Not(Filter.Exists(field)), + Filter.Ne(field, BuildValue(nodeIn.Right))); + } + + if (nodeIn.OperatorKind == BinaryOperatorKind.Equal) + { + return Filter.Eq(BuildFieldDefinition(nodeIn.Left), BuildValue(nodeIn.Right)); + } + + if (nodeIn.OperatorKind == BinaryOperatorKind.LessThan) + { + return Filter.Lt(BuildFieldDefinition(nodeIn.Left), BuildValue(nodeIn.Right)); + } + + if (nodeIn.OperatorKind == BinaryOperatorKind.LessThanOrEqual) + { + return Filter.Lte(BuildFieldDefinition(nodeIn.Left), BuildValue(nodeIn.Right)); + } + + if (nodeIn.OperatorKind == BinaryOperatorKind.GreaterThan) + { + return Filter.Gt(BuildFieldDefinition(nodeIn.Left), BuildValue(nodeIn.Right)); + } + + if (nodeIn.OperatorKind == BinaryOperatorKind.GreaterThanOrEqual) + { + return Filter.Gte(BuildFieldDefinition(nodeIn.Left), BuildValue(nodeIn.Right)); + } + } + + throw new NotSupportedException(); + } + + private static BsonRegularExpression BuildRegex(QueryNode node, Func formatter) + { + return new BsonRegularExpression(formatter(BuildValue(node).ToString()), "i"); + } + + private FieldDefinition BuildFieldDefinition(QueryNode nodeIn) + { + return PropertyVisitor.Visit(nodeIn); + } + + private static object BuildValue(QueryNode nodeIn) + { + return ConstantVisitor.Visit(nodeIn); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs new file mode 100644 index 000000000..711a7f58f --- /dev/null +++ b/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 Filter = Builders.Filter; + + public static IFindFluent Sort(this IFindFluent cursor, ODataUriParser query) + { + return cursor.Sort(SortBuilder.BuildSort(query)); + } + + public static IFindFluent Take(this IFindFluent cursor, ODataUriParser query) + { + var top = query.ParseTop(); + + if (top.HasValue) + { + cursor = cursor.Limit(Math.Min((int)top.Value, 200)); + } + else + { + cursor = cursor.Limit(20); + } + + return cursor; + } + + public static IFindFluent Skip(this IFindFluent cursor, ODataUriParser query) + { + var skip = query.ParseSkip(); + + if (skip.HasValue) + { + cursor = cursor.Skip((int)skip.Value); + } + else + { + cursor = cursor.Skip(null); + } + + return cursor; + } + + public static IFindFluent Find(this IMongoCollection cursor, ODataUriParser query, Guid appId) + { + var filter = BuildQuery(query, appId); + + return cursor.Find(filter); + } + + public static FilterDefinition BuildQuery(ODataUriParser query, Guid appId) + { + var filters = new List> + { + 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(); + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/PropertyVisitor.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/PropertyVisitor.cs new file mode 100644 index 000000000..723d03c38 --- /dev/null +++ b/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> + { + private static readonly PropertyVisitor Instance = new PropertyVisitor(); + + public static StringFieldDefinition Visit(QueryNode node) + { + var propertyNames = node.Accept(Instance).ToArray(); + + return new StringFieldDefinition(propertyNames.First()); + } + + public override ImmutableList Visit(ConvertNode nodeIn) + { + return nodeIn.Source.Accept(this); + } + + public override ImmutableList Visit(SingleComplexNode nodeIn) + { + if (nodeIn.Source is SingleComplexNode) + { + return nodeIn.Source.Accept(this).Add(nodeIn.Property.Name); + } + else + { + return ImmutableList.Create(nodeIn.Property.Name); + } + } + + public override ImmutableList Visit(SingleValuePropertyAccessNode nodeIn) + { + if (nodeIn.Source is SingleComplexNode) + { + return nodeIn.Source.Accept(this).Add(nodeIn.Property.Name); + } + else + { + return ImmutableList.Create(nodeIn.Property.Name); + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/SearchTermVisitor.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/SearchTermVisitor.cs new file mode 100644 index 000000000..23366fde2 --- /dev/null +++ b/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 + { + private static readonly SearchTermVisitor Instance = new SearchTermVisitor(); + + private SearchTermVisitor() + { + } + + public static object Visit(QueryNode node) + { + return node.Accept(Instance); + } + + public override string Visit(BinaryOperatorNode nodeIn) + { + if (nodeIn.OperatorKind == BinaryOperatorKind.And) + { + return nodeIn.Left.Accept(this) + " " + nodeIn.Right.Accept(this); + } + + throw new NotSupportedException(); + } + + public override string Visit(SearchTermNode nodeIn) + { + return nodeIn.Text; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/SortBuilder.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/SortBuilder.cs new file mode 100644 index 000000000..cf1cef451 --- /dev/null +++ b/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 Sort = Builders.Sort; + + public static SortDefinition BuildSort(ODataUriParser query) + { + var orderBy = query.ParseOrderBy(); + + if (orderBy != null) + { + var sorts = new List>(); + + 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 OrderBy(OrderByClause clause) + { + if (clause.Direction == OrderByDirection.Ascending) + { + return Sort.Ascending(PropertyVisitor.Visit(clause.Expression)); + } + else + { + return Sort.Descending(PropertyVisitor.Visit(clause.Expression)); + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Edm/EdmModelBuilder.cs b/src/Squidex.Domain.Apps.Entities/Assets/Edm/EdmModelBuilder.cs new file mode 100644 index 000000000..d637c8e3a --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Assets/Edm/EdmModelBuilder.cs @@ -0,0 +1,61 @@ +// ========================================================================== +// EdmModelBuilder.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== +using Microsoft.Extensions.Caching.Memory; +using Microsoft.OData.Edm; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Assets.Edm +{ + public class EdmModelBuilder : CachingProviderBase + { + public EdmModelBuilder(IMemoryCache cache) + : base(cache) + { + } + + public virtual IEdmModel BuildEdmModel(IAssetEntity asset) + { + Guard.NotNull(asset, nameof(asset)); + + var cacheKey = $"Assets_EdmModel"; + + var result = Cache.GetOrCreate(cacheKey, entry => + { + var model = new EdmModel(); + + var container = new EdmEntityContainer("Squidex", "Container"); + + var entityType = new EdmEntityType("Squidex", "Asset"); + entityType.AddStructuralProperty(nameof(asset.Id), EdmPrimitiveTypeKind.Guid); + entityType.AddStructuralProperty(nameof(asset.AppId), EdmPrimitiveTypeKind.Guid); + entityType.AddStructuralProperty(nameof(asset.Created), EdmPrimitiveTypeKind.DateTimeOffset); + entityType.AddStructuralProperty(nameof(asset.CreatedBy), EdmPrimitiveTypeKind.String); + entityType.AddStructuralProperty(nameof(asset.LastModified), EdmPrimitiveTypeKind.DateTimeOffset); + entityType.AddStructuralProperty(nameof(asset.LastModifiedBy), EdmPrimitiveTypeKind.String); + entityType.AddStructuralProperty(nameof(asset.Version), EdmPrimitiveTypeKind.Int64); + + entityType.AddStructuralProperty(nameof(asset.FileName), EdmPrimitiveTypeKind.String); + entityType.AddStructuralProperty(nameof(asset.FileSize), EdmPrimitiveTypeKind.Int64); + entityType.AddStructuralProperty(nameof(asset.FileVersion), EdmPrimitiveTypeKind.Int64); + entityType.AddStructuralProperty(nameof(asset.IsImage), EdmPrimitiveTypeKind.Boolean); + entityType.AddStructuralProperty(nameof(asset.MimeType), EdmPrimitiveTypeKind.String); + entityType.AddStructuralProperty(nameof(asset.PixelHeight), EdmPrimitiveTypeKind.Int32); + entityType.AddStructuralProperty(nameof(asset.PixelWidth), EdmPrimitiveTypeKind.Int32); + + model.AddElement(container); + model.AddElement(entityType); + + container.AddEntitySet("AssetSet", entityType); + + return model; + }); + + return result; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Edm/EdmModelExtensions.cs b/src/Squidex.Domain.Apps.Entities/Assets/Edm/EdmModelExtensions.cs new file mode 100644 index 000000000..6bb691335 --- /dev/null +++ b/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; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs b/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs index c2bd7eca1..d940f838f 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs @@ -14,7 +14,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Repositories { public interface IAssetRepository { - Task> QueryAsync(Guid appId, HashSet mimeTypes = null, HashSet ids = null, string query = null, int take = 10, int skip = 0); + Task> QueryAsync(Guid appId, string query = null); Task FindAssetAsync(Guid id); } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs index d06df5e7c..bfb1a0b03 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs +++ b/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,20 @@ namespace Squidex.Domain.Apps.Entities.Contents private async Task> QueryAssetsAsync(Guid appId, IEnumerable assetIds) { - return await assetRepository.QueryAsync(appId, null, new HashSet(assetIds), null, int.MaxValue, 0); + StringBuilder sb = new StringBuilder(); + if (assetIds.Count() > 0) + { + sb.Append("$filter="); + + foreach (var assetId in assetIds) + { + sb.Append($"Id eq {assetId} or"); + } + + sb.Remove(sb.Length - 2, 2); + } + + return await assetRepository.QueryAsync(appId, sb.ToString()); } private async Task> QueryContentsAsync(Guid appId, Guid schemaId, IEnumerable contentIds) diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs index 40c64fdbf..fcc0f6110 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs +++ b/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." }); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/QueryContext.cs b/src/Squidex.Domain.Apps.Entities/Contents/QueryContext.cs index 8ed70d159..bac8fee4a 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/QueryContext.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/QueryContext.cs @@ -80,9 +80,9 @@ namespace Squidex.Domain.Apps.Entities.Contents return content; } - public async Task> QueryAssetsAsync(string query, int skip = 0, int take = 10) + public async Task> 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, null); foreach (var asset in assets) { diff --git a/src/Squidex/Areas/Api/Config/Swagger/SwaggerServices.cs b/src/Squidex/Areas/Api/Config/Swagger/SwaggerServices.cs index 7658b8011..cc8bd4334 100644 --- a/src/Squidex/Areas/Api/Config/Swagger/SwaggerServices.cs +++ b/src/Squidex/Areas/Api/Config/Swagger/SwaggerServices.cs @@ -29,7 +29,16 @@ namespace Squidex.Areas.Api.Config.Swagger var urlOptions = s.GetService>().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); diff --git a/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs b/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs index 8a1d43c1f..8938907c3 100644 --- a/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs @@ -59,11 +59,6 @@ namespace Squidex.Areas.Api.Controllers.Assets /// Get assets. /// /// The name of the app. - /// The optional asset ids. - /// Optional number of assets to skip. - /// Optional number of assets to take (Default: 20). - /// Optional query to limit the files by name. - /// Comma separated list of mime types to get. /// /// 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 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 GetAssets(string app) { - var mimeTypeList = new HashSet(); - - if (!string.IsNullOrWhiteSpace(mimeTypes)) - { - foreach (var mimeType in mimeTypes.Split(',')) - { - mimeTypeList.Add(mimeType.Trim()); - } - } - - var idsList = new HashSet(); - - 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 { diff --git a/src/Squidex/Config/Domain/ReadServices.cs b/src/Squidex/Config/Domain/ReadServices.cs index e1a4da864..c2a1bf3cd 100644 --- a/src/Squidex/Config/Domain/ReadServices.cs +++ b/src/Squidex/Config/Domain/ReadServices.cs @@ -111,6 +111,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .AsSelf(); + services.AddSingletonAs() + .AsSelf(); + services.AddSingletonAs() .AsSelf(); } diff --git a/src/Squidex/Config/Domain/StoreServices.cs b/src/Squidex/Config/Domain/StoreServices.cs index f9b849af6..eb84e6d97 100644 --- a/src/Squidex/Config/Domain/StoreServices.cs +++ b/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>() .As(); - services.AddSingletonAs(c => new MongoAssetRepository(mongoDatabase)) + services.AddSingletonAs(c => new MongoAssetRepository(mongoDatabase, c.GetRequiredService())) .As() .As>() .As(); diff --git a/src/Squidex/Docs/AddODataQueryParams.cs b/src/Squidex/Docs/AddODataQueryParams.cs new file mode 100644 index 000000000..f79e4b210 --- /dev/null +++ b/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 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; + } + } +} diff --git a/src/Squidex/app/shared/services/assets.service.ts b/src/Squidex/app/shared/services/assets.service.ts index 065103db8..c272a7c29 100644 --- a/src/Squidex/app/shared/services/assets.service.ts +++ b/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 { 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('&'); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs index 5b6d937a2..5cba90e5b 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs +++ b/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 { asset }; - A.CallTo(() => assetRepository.QueryAsync(app.Id, null, null, "my-query", 30, 5)) + A.CallTo(() => assetRepository.QueryAsync(app.Id, null)) .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 { asset }; - A.CallTo(() => assetRepository.QueryAsync(app.Id, null, null, "my-query", 30, 5)) + A.CallTo(() => assetRepository.QueryAsync(app.Id, "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>.That.Matches(x => x.Contains(assetRefId)), null, int.MaxValue, 0)) + A.CallTo(() => assetRepository.QueryAsync(app.Id, A.That.Matches(x => x.Contains(assetRef.Id.ToString())))) .Returns(ResultList.Create(refAssets, 0)); var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); diff --git a/tools/Migrate_01/Migration04_FlattenAssetEntity.cs b/tools/Migrate_01/Migration04_FlattenAssetEntity.cs new file mode 100644 index 000000000..dc01bfbb8 --- /dev/null +++ b/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 previousMigrations) + { + if (!previousMigrations.Any(x => x is Migration01_FromCqrs)) + { + await rebuilder.RebuildAssetsAsync(); + } + } + } +} From fe3629c1c5a54b23215e0de37fdb3cd651490a47 Mon Sep 17 00:00:00 2001 From: Derek Begnoche Date: Fri, 26 Jan 2018 00:20:51 -0600 Subject: [PATCH 2/4] Fixing GraphQl Tests --- .../Assets/MongoAssetRepository.cs | 13 +++++++++++++ .../Assets/Repositories/IAssetRepository.cs | 2 ++ .../Contents/QueryContext.cs | 2 +- .../Contents/GraphQL/GraphQLQueriesTests.cs | 6 +++--- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs index 87e593bfb..bf9c7312a 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.OData; @@ -64,6 +65,18 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets return ResultList.Create(assetEntities.OfType().ToList(), assetCount); } + public async Task> QueryAsync(Guid appId, HashSet ids) + { + var find = Collection + .Find(Filter.In(x => x.Id, ids)) + .SortByDescending(x => x.LastModified); + + var assetEntities = await find.ToListAsync(); + var assetCount = await find.CountAsync(); + + return ResultList.Create(assetEntities.OfType().ToList(), assetCount); + } + public async Task FindAssetAsync(Guid id) { var assetEntity = diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs b/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs index d940f838f..f748b4936 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs @@ -16,6 +16,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.Repositories { Task> QueryAsync(Guid appId, string query = null); + Task> QueryAsync(Guid appId, HashSet ids); + Task FindAssetAsync(Guid id); } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/QueryContext.cs b/src/Squidex.Domain.Apps.Entities/Contents/QueryContext.cs index bac8fee4a..bf5dbd7ab 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/QueryContext.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/QueryContext.cs @@ -112,7 +112,7 @@ namespace Squidex.Domain.Apps.Entities.Contents if (notLoadedAssets.Count > 0) { - var assets = await assetRepository.QueryAsync(app.Id, null); + var assets = await assetRepository.QueryAsync(app.Id, notLoadedAssets); foreach (var asset in assets) { diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs index 5cba90e5b..9cb71fc72 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs +++ b/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 { asset }; - A.CallTo(() => assetRepository.QueryAsync(app.Id, null)) + 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 { asset }; - A.CallTo(() => assetRepository.QueryAsync(app.Id, "my-query")) + 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, A.That.Matches(x => x.Contains(assetRef.Id.ToString())))) + A.CallTo(() => assetRepository.QueryAsync(app.Id, A>.That.Matches(x => x.Contains(assetRefId)))) .Returns(ResultList.Create(refAssets, 0)); var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); From 1cdcd69fc6e2dc3c09beadf28de450d44fa3657f Mon Sep 17 00:00:00 2001 From: Derek Begnoche Date: Fri, 26 Jan 2018 16:04:20 -0600 Subject: [PATCH 3/4] JS Tests Fixed --- src/Squidex/app/shared/services/assets.service.spec.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Squidex/app/shared/services/assets.service.spec.ts b/src/Squidex/app/shared/services/assets.service.spec.ts index 5e98293ae..00e5b575d 100644 --- a/src/Squidex/app/shared/services/assets.service.spec.ts +++ b/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(); From 5c592b63b7ca1efeef6fc22127c742d6e77ad9a2 Mon Sep 17 00:00:00 2001 From: Derek Begnoche Date: Fri, 26 Jan 2018 20:56:57 -0600 Subject: [PATCH 4/4] Remove Edm Builder using cache. --- .../Assets/MongoAssetRepository.cs | 11 ++- .../Assets/Visitors/ConstantVisitor.cs | 6 +- .../Assets/Edm/EdmModelBuilder.cs | 76 +++++++++---------- .../Contents/ContentOperationContext.cs | 15 +--- 4 files changed, 44 insertions(+), 64 deletions(-) diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs index bf9c7312a..b5f371186 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs @@ -15,7 +15,6 @@ 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.Assets.State; using Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors; using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; @@ -44,7 +43,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets .Ascending(x => x.AppId) .Ascending(x => x.IsDeleted) .Ascending(x => x.FileName) - .Ascending(x => x.MimeType) .Descending(x => x.LastModified)); } @@ -71,10 +69,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets .Find(Filter.In(x => x.Id, ids)) .SortByDescending(x => x.LastModified); - var assetEntities = await find.ToListAsync(); - var assetCount = await find.CountAsync(); + var assetEntities = find.ToListAsync(); + var assetCount = find.CountAsync(); + await Task.WhenAll(assetEntities, assetCount); - return ResultList.Create(assetEntities.OfType().ToList(), assetCount); + return ResultList.Create(assetEntities.Result.OfType().ToList(), assetCount.Result); } public async Task FindAssetAsync(Guid id) @@ -90,7 +89,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets { try { - var model = modelBuilder.BuildEdmModel(new AssetState()); + var model = modelBuilder.EdmModel; return model.ParseQuery(query); } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/ConstantVisitor.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/ConstantVisitor.cs index e6978a74d..3984c456e 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/ConstantVisitor.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/ConstantVisitor.cs @@ -1,10 +1,10 @@ // ========================================================================== -// ConstantVisitor.cs // Squidex Headless CMS // ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. // ========================================================================== + using System; using Microsoft.OData.Edm; using Microsoft.OData.UriParser; diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Edm/EdmModelBuilder.cs b/src/Squidex.Domain.Apps.Entities/Assets/Edm/EdmModelBuilder.cs index d637c8e3a..61516c68e 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/Edm/EdmModelBuilder.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/Edm/EdmModelBuilder.cs @@ -5,57 +5,51 @@ // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -using Microsoft.Extensions.Caching.Memory; using Microsoft.OData.Edm; -using Squidex.Infrastructure; +using Squidex.Domain.Apps.Entities.Assets.State; namespace Squidex.Domain.Apps.Entities.Assets.Edm { - public class EdmModelBuilder : CachingProviderBase + public class EdmModelBuilder { - public EdmModelBuilder(IMemoryCache cache) - : base(cache) + private readonly IEdmModel edmModel; + public EdmModelBuilder() { + edmModel = BuildEdmModel(); } - public virtual IEdmModel BuildEdmModel(IAssetEntity asset) + public virtual IEdmModel EdmModel { - Guard.NotNull(asset, nameof(asset)); - - var cacheKey = $"Assets_EdmModel"; - - var result = Cache.GetOrCreate(cacheKey, entry => - { - var model = new EdmModel(); - - var container = new EdmEntityContainer("Squidex", "Container"); - - var entityType = new EdmEntityType("Squidex", "Asset"); - entityType.AddStructuralProperty(nameof(asset.Id), EdmPrimitiveTypeKind.Guid); - entityType.AddStructuralProperty(nameof(asset.AppId), EdmPrimitiveTypeKind.Guid); - entityType.AddStructuralProperty(nameof(asset.Created), EdmPrimitiveTypeKind.DateTimeOffset); - entityType.AddStructuralProperty(nameof(asset.CreatedBy), EdmPrimitiveTypeKind.String); - entityType.AddStructuralProperty(nameof(asset.LastModified), EdmPrimitiveTypeKind.DateTimeOffset); - entityType.AddStructuralProperty(nameof(asset.LastModifiedBy), EdmPrimitiveTypeKind.String); - entityType.AddStructuralProperty(nameof(asset.Version), EdmPrimitiveTypeKind.Int64); - - entityType.AddStructuralProperty(nameof(asset.FileName), EdmPrimitiveTypeKind.String); - entityType.AddStructuralProperty(nameof(asset.FileSize), EdmPrimitiveTypeKind.Int64); - entityType.AddStructuralProperty(nameof(asset.FileVersion), EdmPrimitiveTypeKind.Int64); - entityType.AddStructuralProperty(nameof(asset.IsImage), EdmPrimitiveTypeKind.Boolean); - entityType.AddStructuralProperty(nameof(asset.MimeType), EdmPrimitiveTypeKind.String); - entityType.AddStructuralProperty(nameof(asset.PixelHeight), EdmPrimitiveTypeKind.Int32); - entityType.AddStructuralProperty(nameof(asset.PixelWidth), EdmPrimitiveTypeKind.Int32); - - model.AddElement(container); - model.AddElement(entityType); - - container.AddEntitySet("AssetSet", entityType); - - return model; - }); + get { return edmModel; } + } - return result; + private IEdmModel BuildEdmModel() + { + var model = new EdmModel(); + var container = new EdmEntityContainer("Squidex", "Container"); + var entityType = new EdmEntityType("Squidex", "Asset"); + + entityType.AddStructuralProperty(nameof(AssetState.Id), EdmPrimitiveTypeKind.Guid); + entityType.AddStructuralProperty(nameof(AssetState.AppId), EdmPrimitiveTypeKind.Guid); + entityType.AddStructuralProperty(nameof(AssetState.Created), EdmPrimitiveTypeKind.DateTimeOffset); + entityType.AddStructuralProperty(nameof(AssetState.CreatedBy), EdmPrimitiveTypeKind.String); + entityType.AddStructuralProperty(nameof(AssetState.LastModified), EdmPrimitiveTypeKind.DateTimeOffset); + entityType.AddStructuralProperty(nameof(AssetState.LastModifiedBy), EdmPrimitiveTypeKind.String); + entityType.AddStructuralProperty(nameof(AssetState.Version), EdmPrimitiveTypeKind.Int64); + entityType.AddStructuralProperty(nameof(AssetState.FileName), EdmPrimitiveTypeKind.String); + entityType.AddStructuralProperty(nameof(AssetState.FileSize), EdmPrimitiveTypeKind.Int64); + entityType.AddStructuralProperty(nameof(AssetState.FileVersion), EdmPrimitiveTypeKind.Int64); + entityType.AddStructuralProperty(nameof(AssetState.IsImage), EdmPrimitiveTypeKind.Boolean); + entityType.AddStructuralProperty(nameof(AssetState.MimeType), EdmPrimitiveTypeKind.String); + entityType.AddStructuralProperty(nameof(AssetState.PixelHeight), EdmPrimitiveTypeKind.Int32); + entityType.AddStructuralProperty(nameof(AssetState.PixelWidth), EdmPrimitiveTypeKind.Int32); + + model.AddElement(container); + model.AddElement(entityType); + + container.AddEntitySet("AssetSet", entityType); + + return model; } } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs index bfb1a0b03..c0bccc485 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs @@ -107,20 +107,7 @@ namespace Squidex.Domain.Apps.Entities.Contents private async Task> QueryAssetsAsync(Guid appId, IEnumerable assetIds) { - StringBuilder sb = new StringBuilder(); - if (assetIds.Count() > 0) - { - sb.Append("$filter="); - - foreach (var assetId in assetIds) - { - sb.Append($"Id eq {assetId} or"); - } - - sb.Remove(sb.Length - 2, 2); - } - - return await assetRepository.QueryAsync(appId, sb.ToString()); + return await assetRepository.QueryAsync(appId, new HashSet(assetIds)); } private async Task> QueryContentsAsync(Guid appId, Guid schemaId, IEnumerable contentIds)