Browse Source

Started with asset enricher.

pull/377/head
Sebastian Stehle 7 years ago
parent
commit
0cff67d2cc
  1. 2
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
  2. 27
      src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs
  3. 11
      src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs
  4. 78
      src/Squidex.Domain.Apps.Entities/Assets/AssetEnricher.cs
  5. 9
      src/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs
  6. 69
      src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs
  7. 19
      src/Squidex.Domain.Apps.Entities/Assets/IAssetEnricher.cs
  8. 6
      src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs
  9. 13
      src/Squidex.Domain.Apps.Entities/Assets/IEnrichedAssetEntity.cs
  10. 2
      src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs
  11. 54
      src/Squidex.Domain.Apps.Entities/Contents/ContentEnricher.cs
  12. 6
      src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs
  13. 10
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs
  14. 8
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs
  15. 17
      src/Squidex.Domain.Apps.Entities/Contents/IContentEnricher.cs
  16. 18
      src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs
  17. 8
      src/Squidex.Domain.Apps.Entities/Contents/QueryExecutionContext.cs
  18. 10
      src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  19. 7
      src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs
  20. 2
      src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs
  21. 3
      src/Squidex/Config/Domain/StoreServices.cs
  22. 23
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs
  23. 5
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetQueryServiceTests.cs
  24. 8
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs

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

@ -137,7 +137,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
} }
} }
public async Task<IList<IAssetEntity>> QueryByHashAsync(Guid appId, string hash) public async Task<IReadOnlyList<IAssetEntity>> QueryByHashAsync(Guid appId, string hash)
{ {
using (Profiler.TraceMethod<MongoAssetRepository>()) using (Profiler.TraceMethod<MongoAssetRepository>())
{ {

27
src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs

@ -10,7 +10,6 @@ using System.Collections.Generic;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Threading.Tasks; using System.Threading.Tasks;
using Orleans; using Orleans;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Tags; using Squidex.Domain.Apps.Entities.Tags;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -22,31 +21,31 @@ namespace Squidex.Domain.Apps.Entities.Assets
public sealed class AssetCommandMiddleware : GrainCommandMiddleware<AssetCommand, IAssetGrain> public sealed class AssetCommandMiddleware : GrainCommandMiddleware<AssetCommand, IAssetGrain>
{ {
private readonly IAssetStore assetStore; private readonly IAssetStore assetStore;
private readonly IAssetEnricher assetEnricher;
private readonly IAssetQueryService assetQuery; private readonly IAssetQueryService assetQuery;
private readonly IAssetThumbnailGenerator assetThumbnailGenerator; private readonly IAssetThumbnailGenerator assetThumbnailGenerator;
private readonly IEnumerable<ITagGenerator<CreateAsset>> tagGenerators; private readonly IEnumerable<ITagGenerator<CreateAsset>> tagGenerators;
private readonly ITagService tagService;
public AssetCommandMiddleware( public AssetCommandMiddleware(
IGrainFactory grainFactory, IGrainFactory grainFactory,
IAssetEnricher assetEnricher,
IAssetQueryService assetQuery, IAssetQueryService assetQuery,
IAssetStore assetStore, IAssetStore assetStore,
IAssetThumbnailGenerator assetThumbnailGenerator, IAssetThumbnailGenerator assetThumbnailGenerator,
IEnumerable<ITagGenerator<CreateAsset>> tagGenerators, IEnumerable<ITagGenerator<CreateAsset>> tagGenerators)
ITagService tagService)
: base(grainFactory) : base(grainFactory)
{ {
Guard.NotNull(assetEnricher, nameof(assetEnricher));
Guard.NotNull(assetStore, nameof(assetStore)); Guard.NotNull(assetStore, nameof(assetStore));
Guard.NotNull(assetQuery, nameof(assetQuery)); Guard.NotNull(assetQuery, nameof(assetQuery));
Guard.NotNull(assetThumbnailGenerator, nameof(assetThumbnailGenerator)); Guard.NotNull(assetThumbnailGenerator, nameof(assetThumbnailGenerator));
Guard.NotNull(tagGenerators, nameof(tagGenerators)); Guard.NotNull(tagGenerators, nameof(tagGenerators));
Guard.NotNull(tagService, nameof(tagService));
this.assetStore = assetStore; this.assetStore = assetStore;
this.assetEnricher = assetEnricher;
this.assetQuery = assetQuery; this.assetQuery = assetQuery;
this.assetThumbnailGenerator = assetThumbnailGenerator; this.assetThumbnailGenerator = assetThumbnailGenerator;
this.tagGenerators = tagGenerators; this.tagGenerators = tagGenerators;
this.tagService = tagService;
} }
public override async Task HandleAsync(CommandContext context, Func<Task> next) public override async Task HandleAsync(CommandContext context, Func<Task> next)
@ -73,9 +72,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
{ {
if (IsDuplicate(createAsset, existing)) if (IsDuplicate(createAsset, existing))
{ {
var denormalizedTags = await tagService.DenormalizeTagsAsync(createAsset.AppId.Id, TagGroups.Assets, existing.Tags); result = new AssetCreatedResult(existing, true);
result = new AssetCreatedResult(existing, true, new HashSet<string>(denormalizedTags.Values));
} }
break; break;
@ -88,9 +85,9 @@ namespace Squidex.Domain.Apps.Entities.Assets
tagGenerator.GenerateTags(createAsset, createAsset.Tags); tagGenerator.GenerateTags(createAsset, createAsset.Tags);
} }
var asset = (IAssetEntity)await ExecuteCommandAsync(createAsset); var asset = (IEnrichedAssetEntity)await ExecuteAndAdjustTagsAsync(createAsset);
result = new AssetCreatedResult(asset, false, createAsset.Tags); result = new AssetCreatedResult(asset, false);
await assetStore.CopyAsync(context.ContextId.ToString(), createAsset.AssetId.ToString(), asset.FileVersion, null); await assetStore.CopyAsync(context.ContextId.ToString(), createAsset.AssetId.ToString(), asset.FileVersion, null);
} }
@ -112,11 +109,11 @@ namespace Squidex.Domain.Apps.Entities.Assets
try try
{ {
var result = (AssetResult)await ExecuteAndAdjustTagsAsync(updateAsset); var result = (IEnrichedAssetEntity)await ExecuteAndAdjustTagsAsync(updateAsset);
context.Complete(result); context.Complete(result);
await assetStore.CopyAsync(context.ContextId.ToString(), updateAsset.AssetId.ToString(), result.Asset.FileVersion, null); await assetStore.CopyAsync(context.ContextId.ToString(), updateAsset.AssetId.ToString(), result.FileVersion, null);
} }
finally finally
{ {
@ -148,9 +145,9 @@ namespace Squidex.Domain.Apps.Entities.Assets
if (result is IAssetEntity asset) if (result is IAssetEntity asset)
{ {
var denormalizedTags = await tagService.DenormalizeTagsAsync(asset.AppId.Id, TagGroups.Assets, asset.Tags); var enriched = await assetEnricher.EnrichAsync(asset);
return new AssetResult(asset, new HashSet<string>(denormalizedTags.Values)); return enriched;
} }
return result; return result;

11
src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs

@ -5,17 +5,18 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.Generic;
namespace Squidex.Domain.Apps.Entities.Assets namespace Squidex.Domain.Apps.Entities.Assets
{ {
public sealed class AssetCreatedResult : AssetResult public sealed class AssetCreatedResult
{ {
public IEnrichedAssetEntity Asset { get; }
public bool IsDuplicate { get; } public bool IsDuplicate { get; }
public AssetCreatedResult(IAssetEntity asset, bool isDuplicate, HashSet<string> tags) public AssetCreatedResult(IEnrichedAssetEntity asset, bool isDuplicate)
: base(asset, tags)
{ {
Asset = asset;
IsDuplicate = isDuplicate; IsDuplicate = isDuplicate;
} }
} }

78
src/Squidex.Domain.Apps.Entities/Assets/AssetEnricher.cs

@ -0,0 +1,78 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Entities.Assets
{
public sealed class AssetEnricher : IAssetEnricher
{
private readonly ITagService tagService;
public AssetEnricher(ITagService tagService)
{
Guard.NotNull(tagService, nameof(tagService));
this.tagService = tagService;
}
public async Task<IEnrichedAssetEntity> EnrichAsync(IAssetEntity asset)
{
Guard.NotNull(asset, nameof(asset));
var enriched = await EnrichAsync(Enumerable.Repeat(asset, 1));
return enriched[0];
}
public async Task<IReadOnlyList<IEnrichedAssetEntity>> EnrichAsync(IEnumerable<IAssetEntity> assets)
{
Guard.NotNull(assets, nameof(assets));
var results = new List<IEnrichedAssetEntity>();
foreach (var group in assets.GroupBy(x => x.AppId.Id))
{
var tagsById = await CalculateTags(group);
foreach (var asset in group)
{
var result = SimpleMapper.Map(asset, new AssetEntity());
result.TagNames = new HashSet<string>();
if (asset.Tags != null)
{
foreach (var id in asset.Tags)
{
if (tagsById.TryGetValue(id, out var name))
{
result.TagNames.Add(name);
}
}
}
results.Add(result);
}
}
return results;
}
private async Task<Dictionary<string, string>> CalculateTags(IGrouping<System.Guid, IAssetEntity> group)
{
var uniqueIds = group.Where(x => x.Tags != null).SelectMany(x => x.Tags).ToHashSet();
return await tagService.DenormalizeTagsAsync(group.Key, TagGroups.Assets, uniqueIds);
}
}
}

9
tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeAssetEntity.cs → src/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs

@ -1,19 +1,18 @@
// ========================================================================== // ==========================================================================
// Squidex Headless CMS // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt) // Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using NodaTime; using NodaTime;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure; using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.TestData namespace Squidex.Domain.Apps.Entities.Assets
{ {
public sealed class FakeAssetEntity : IAssetEntity public sealed class AssetEntity : IEnrichedAssetEntity
{ {
public NamedId<Guid> AppId { get; set; } public NamedId<Guid> AppId { get; set; }
@ -31,6 +30,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.TestData
public HashSet<string> Tags { get; set; } public HashSet<string> Tags { get; set; }
public HashSet<string> TagNames { get; set; }
public long Version { get; set; } public long Version { get; set; }
public string MimeType { get; set; } public string MimeType { get; set; }

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

@ -21,9 +21,10 @@ using Squidex.Infrastructure.Queries.OData;
namespace Squidex.Domain.Apps.Entities.Assets namespace Squidex.Domain.Apps.Entities.Assets
{ {
public class AssetQueryService : IAssetQueryService public sealed class AssetQueryService : IAssetQueryService
{ {
private readonly ITagService tagService; private readonly ITagService tagService;
private readonly IAssetEnricher assetEnricher;
private readonly IAssetRepository assetRepository; private readonly IAssetRepository assetRepository;
private readonly AssetOptions options; private readonly AssetOptions options;
@ -32,48 +33,47 @@ namespace Squidex.Domain.Apps.Entities.Assets
get { return options.DefaultPageSizeGraphQl; } get { return options.DefaultPageSizeGraphQl; }
} }
public AssetQueryService(ITagService tagService, IAssetRepository assetRepository, IOptions<AssetOptions> options) public AssetQueryService(
ITagService tagService,
IAssetEnricher assetEnricher,
IAssetRepository assetRepository,
IOptions<AssetOptions> options)
{ {
Guard.NotNull(tagService, nameof(tagService)); Guard.NotNull(tagService, nameof(tagService));
Guard.NotNull(options, nameof(options)); Guard.NotNull(assetEnricher, nameof(assetEnricher));
Guard.NotNull(assetRepository, nameof(assetRepository)); Guard.NotNull(assetRepository, nameof(assetRepository));
Guard.NotNull(options, nameof(options));
this.tagService = tagService;
this.assetEnricher = assetEnricher;
this.assetRepository = assetRepository; this.assetRepository = assetRepository;
this.options = options.Value; this.options = options.Value;
this.tagService = tagService;
} }
public Task<IAssetEntity> FindAssetAsync(QueryContext context, Guid id) public async Task<IEnrichedAssetEntity> FindAssetAsync( Guid id)
{
Guard.NotNull(context, nameof(context));
return FindAssetAsync(context.App.Id, id);
}
public async Task<IAssetEntity> FindAssetAsync(Guid appId, Guid id)
{ {
var asset = await assetRepository.FindAssetAsync(id); var asset = await assetRepository.FindAssetAsync(id);
if (asset != null) if (asset != null)
{ {
await DenormalizeTagsAsync(appId, Enumerable.Repeat(asset, 1)); return await assetEnricher.EnrichAsync(asset);
} }
return asset; return null;
} }
public async Task<IList<IAssetEntity>> QueryByHashAsync(Guid appId, string hash) public async Task<IReadOnlyList<IEnrichedAssetEntity>> QueryByHashAsync(Guid appId, string hash)
{ {
Guard.NotNull(hash, nameof(hash)); Guard.NotNull(hash, nameof(hash));
var assets = await assetRepository.QueryByHashAsync(appId, hash); var assets = await assetRepository.QueryByHashAsync(appId, hash);
await DenormalizeTagsAsync(appId, assets); var enriched = await assetEnricher.EnrichAsync(assets);
return assets; return enriched;
} }
public async Task<IResultList<IAssetEntity>> QueryAsync(QueryContext context, Q query) public async Task<IResultList<IEnrichedAssetEntity>> QueryAsync(QueryContext context, Q query)
{ {
Guard.NotNull(context, nameof(context)); Guard.NotNull(context, nameof(context));
Guard.NotNull(query, nameof(query)); Guard.NotNull(query, nameof(query));
@ -92,9 +92,9 @@ namespace Squidex.Domain.Apps.Entities.Assets
assets = await assetRepository.QueryAsync(context.App.Id, parsedQuery); assets = await assetRepository.QueryAsync(context.App.Id, parsedQuery);
} }
await DenormalizeTagsAsync(context.App.Id, assets); var enriched = await assetEnricher.EnrichAsync(assets);
return assets; return ResultList.Create<IEnrichedAssetEntity>(assets.Total, enriched);
} }
private static IResultList<IAssetEntity> Sort(IResultList<IAssetEntity> assets, IReadOnlyList<Guid> ids) private static IResultList<IAssetEntity> Sort(IResultList<IAssetEntity> assets, IReadOnlyList<Guid> ids)
@ -140,34 +140,5 @@ namespace Squidex.Domain.Apps.Entities.Assets
throw new ValidationException($"Failed to parse query: {ex.Message}", ex); throw new ValidationException($"Failed to parse query: {ex.Message}", ex);
} }
} }
private async Task DenormalizeTagsAsync(Guid appId, IEnumerable<IAssetEntity> assets)
{
var tags = new HashSet<string>(assets.Where(x => x.Tags != null).SelectMany(x => x.Tags).Distinct());
var tagsById = await tagService.DenormalizeTagsAsync(appId, TagGroups.Assets, tags);
foreach (var asset in assets)
{
if (asset.Tags?.Count > 0)
{
var tagNames = asset.Tags.ToList();
asset.Tags.Clear();
foreach (var id in tagNames)
{
if (tagsById.TryGetValue(id, out var name))
{
asset.Tags.Add(name);
}
}
}
else
{
asset.Tags?.Clear();
}
}
}
} }
} }

19
src/Squidex.Domain.Apps.Entities/Assets/IAssetEnricher.cs

@ -0,0 +1,19 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Squidex.Domain.Apps.Entities.Assets
{
public interface IAssetEnricher
{
Task<IEnrichedAssetEntity> EnrichAsync(IAssetEntity asset);
Task<IReadOnlyList<IEnrichedAssetEntity>> EnrichAsync(IEnumerable<IAssetEntity> assets);
}
}

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

@ -16,10 +16,10 @@ namespace Squidex.Domain.Apps.Entities.Assets
{ {
int DefaultPageSizeGraphQl { get; } int DefaultPageSizeGraphQl { get; }
Task<IList<IAssetEntity>> QueryByHashAsync(Guid appId, string hash); Task<IReadOnlyList<IEnrichedAssetEntity>> QueryByHashAsync(Guid appId, string hash);
Task<IResultList<IAssetEntity>> QueryAsync(QueryContext contex, Q query); Task<IResultList<IEnrichedAssetEntity>> QueryAsync(QueryContext contex, Q query);
Task<IAssetEntity> FindAssetAsync(QueryContext context, Guid id); Task<IEnrichedAssetEntity> FindAssetAsync(Guid id);
} }
} }

13
src/Squidex.Domain.Apps.Entities/Assets/AssetResult.cs → src/Squidex.Domain.Apps.Entities/Assets/IEnrichedAssetEntity.cs

@ -9,17 +9,8 @@ using System.Collections.Generic;
namespace Squidex.Domain.Apps.Entities.Assets namespace Squidex.Domain.Apps.Entities.Assets
{ {
public class AssetResult public interface IEnrichedAssetEntity : IAssetEntity
{ {
public IAssetEntity Asset { get; } HashSet<string> TagNames { get; }
public HashSet<string> Tags { get; }
public AssetResult(IAssetEntity asset, HashSet<string> tags)
{
Asset = asset;
Tags = tags;
}
} }
} }

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

@ -15,7 +15,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Repositories
{ {
public interface IAssetRepository public interface IAssetRepository
{ {
Task<IList<IAssetEntity>> QueryByHashAsync(Guid appId, string hash); Task<IReadOnlyList<IAssetEntity>> QueryByHashAsync(Guid appId, string hash);
Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, Query query); Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, Query query);

54
src/Squidex.Domain.Apps.Entities/Contents/ContentEnricher.cs

@ -0,0 +1,54 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Entities.Contents
{
public sealed class ContentEnricher : IContentEnricher
{
private readonly IContentWorkflow contentWorkflow;
public ContentEnricher(IContentWorkflow contentWorkflow)
{
this.contentWorkflow = contentWorkflow;
}
public async Task<IReadOnlyList<IEnrichedContentEntity>> EnrichAsync(IEnumerable<IContentEntity> contents)
{
var results = new List<ContentEntity>();
using (Profiler.TraceMethod<ContentEnricher>())
{
var cache = new Dictionary<Status, StatusInfo>();
foreach (var content in contents)
{
var result = SimpleMapper.Map(content, new ContentEntity());
if (!cache.TryGetValue(content.Status, out var info))
{
info = await contentWorkflow.GetInfoAsync(content.Status);
cache[content.Status] = info;
}
result.StatusInfo = info;
result.Nexts = await contentWorkflow.GetNextsAsync(content);
results.Add(result);
}
}
return results;
}
}
}

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

@ -12,7 +12,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents namespace Squidex.Domain.Apps.Entities.Contents
{ {
public sealed class ContentEntity : IContentEntity public sealed class ContentEntity : IEnrichedContentEntity
{ {
public Guid Id { get; set; } public Guid Id { get; set; }
@ -38,6 +38,10 @@ namespace Squidex.Domain.Apps.Entities.Contents
public Status Status { get; set; } public Status Status { get; set; }
public StatusInfo StatusInfo { get; set; }
public StatusInfo[] Nexts { get; set; }
public bool IsPending { get; set; } public bool IsPending { get; set; }
} }
} }

10
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs

@ -20,7 +20,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{ {
public sealed class GraphQLExecutionContext : QueryExecutionContext public sealed class GraphQLExecutionContext : QueryExecutionContext
{ {
private static readonly List<IAssetEntity> EmptyAssets = new List<IAssetEntity>(); private static readonly List<IEnrichedAssetEntity> EmptyAssets = new List<IEnrichedAssetEntity>();
private static readonly List<IContentEntity> EmptyContents = new List<IContentEntity>(); private static readonly List<IContentEntity> EmptyContents = new List<IContentEntity>();
private readonly IDataLoaderContextAccessor dataLoaderContextAccessor; private readonly IDataLoaderContextAccessor dataLoaderContextAccessor;
private readonly IDependencyResolver resolver; private readonly IDependencyResolver resolver;
@ -53,7 +53,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
execution.UserContext = this; execution.UserContext = this;
} }
public override Task<IAssetEntity> FindAssetAsync(Guid id) public override Task<IEnrichedAssetEntity> FindAssetAsync(Guid id)
{ {
var dataLoader = GetAssetsLoader(); var dataLoader = GetAssetsLoader();
@ -67,7 +67,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
return dataLoader.LoadAsync(id); return dataLoader.LoadAsync(id);
} }
public async Task<IReadOnlyList<IAssetEntity>> GetReferencedAssetsAsync(IJsonValue value) public async Task<IReadOnlyList<IEnrichedAssetEntity>> GetReferencedAssetsAsync(IJsonValue value)
{ {
var ids = ParseIds(value); var ids = ParseIds(value);
@ -95,9 +95,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
return await dataLoader.LoadManyAsync(ids); return await dataLoader.LoadManyAsync(ids);
} }
private IDataLoader<Guid, IAssetEntity> GetAssetsLoader() private IDataLoader<Guid, IEnrichedAssetEntity> GetAssetsLoader()
{ {
return dataLoaderContextAccessor.Context.GetOrAddBatchLoader<Guid, IAssetEntity>("Assets", return dataLoaderContextAccessor.Context.GetOrAddBatchLoader<Guid, IEnrichedAssetEntity>("Assets",
async batch => async batch =>
{ {
var result = await GetReferencedAssetsAsync(new List<Guid>(batch)); var result = await GetReferencedAssetsAsync(new List<Guid>(batch));

8
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs

@ -13,7 +13,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
public sealed class AssetGraphType : ObjectGraphType<IAssetEntity> public sealed class AssetGraphType : ObjectGraphType<IEnrichedAssetEntity>
{ {
public AssetGraphType(IGraphModel model) public AssetGraphType(IGraphModel model)
{ {
@ -168,7 +168,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Name = "tags", Name = "tags",
ResolvedType = null, ResolvedType = null,
Resolver = Resolve(x => x.Tags), Resolver = Resolve(x => x.Tags),
Description = "The height of the image in pixels if the asset is an image.", Description = "The asset tags.",
Type = AllTypes.NonNullTagsType Type = AllTypes.NonNullTagsType
}); });
@ -186,9 +186,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Description = "An asset"; Description = "An asset";
} }
private static IFieldResolver Resolve(Func<IAssetEntity, object> action) private static IFieldResolver Resolve(Func<IEnrichedAssetEntity, object> action)
{ {
return new FuncFieldResolver<IAssetEntity, object>(c => action(c.Source)); return new FuncFieldResolver<IEnrichedAssetEntity, object>(c => action(c.Source));
} }
} }
} }

17
src/Squidex.Domain.Apps.Entities/Contents/IContentEnricher.cs

@ -0,0 +1,17 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Squidex.Domain.Apps.Entities.Contents
{
public interface IContentEnricher
{
Task<IReadOnlyList<IEnrichedContentEntity>> EnrichAsync(IEnumerable<IContentEntity> contents);
}
}

18
src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs

@ -0,0 +1,18 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Contents;
namespace Squidex.Domain.Apps.Entities.Contents
{
public interface IEnrichedContentEntity : IContentEntity
{
StatusInfo StatusInfo { get; }
StatusInfo[] Nexts { get; }
}
}

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

@ -18,7 +18,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
public class QueryExecutionContext public class QueryExecutionContext
{ {
private readonly ConcurrentDictionary<Guid, IContentEntity> cachedContents = new ConcurrentDictionary<Guid, IContentEntity>(); private readonly ConcurrentDictionary<Guid, IContentEntity> cachedContents = new ConcurrentDictionary<Guid, IContentEntity>();
private readonly ConcurrentDictionary<Guid, IAssetEntity> cachedAssets = new ConcurrentDictionary<Guid, IAssetEntity>(); private readonly ConcurrentDictionary<Guid, IEnrichedAssetEntity> cachedAssets = new ConcurrentDictionary<Guid, IEnrichedAssetEntity>();
private readonly IContentQueryService contentQuery; private readonly IContentQueryService contentQuery;
private readonly IAssetQueryService assetQuery; private readonly IAssetQueryService assetQuery;
private readonly QueryContext context; private readonly QueryContext context;
@ -34,13 +34,13 @@ namespace Squidex.Domain.Apps.Entities.Contents
this.context = context; this.context = context;
} }
public virtual async Task<IAssetEntity> FindAssetAsync(Guid id) public virtual async Task<IEnrichedAssetEntity> FindAssetAsync(Guid id)
{ {
var asset = cachedAssets.GetOrDefault(id); var asset = cachedAssets.GetOrDefault(id);
if (asset == null) if (asset == null)
{ {
asset = await assetQuery.FindAssetAsync(context, id); asset = await assetQuery.FindAssetAsync(id);
if (asset != null) if (asset != null)
{ {
@ -92,7 +92,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
return result; return result;
} }
public virtual async Task<IReadOnlyList<IAssetEntity>> GetReferencedAssetsAsync(ICollection<Guid> ids) public virtual async Task<IReadOnlyList<IEnrichedAssetEntity>> GetReferencedAssetsAsync(ICollection<Guid> ids)
{ {
Guard.NotNull(ids, nameof(ids)); Guard.NotNull(ids, nameof(ids));

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

@ -133,9 +133,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> GetAsset(string app, Guid id) public async Task<IActionResult> GetAsset(string app, Guid id)
{ {
var context = Context(); var asset = await assetQuery.FindAssetAsync(id);
var asset = await assetQuery.FindAssetAsync(context, id);
if (asset == null) if (asset == null)
{ {
@ -182,7 +180,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
var context = await CommandBus.PublishAsync(command); var context = await CommandBus.PublishAsync(command);
var result = context.Result<AssetCreatedResult>(); var result = context.Result<AssetCreatedResult>();
var response = AssetDto.FromAsset(result.Asset, this, app, result.Tags, result.IsDuplicate); var response = AssetDto.FromAsset(result.Asset, this, app, result.IsDuplicate);
return CreatedAtAction(nameof(GetAsset), new { app, id = response.Id }, response); return CreatedAtAction(nameof(GetAsset), new { app, id = response.Id }, response);
} }
@ -267,8 +265,8 @@ namespace Squidex.Areas.Api.Controllers.Assets
{ {
var context = await CommandBus.PublishAsync(command); var context = await CommandBus.PublishAsync(command);
var result = context.Result<AssetResult>(); var result = context.Result<IEnrichedAssetEntity>();
var response = AssetDto.FromAsset(result.Asset, this, app, result.Tags); var response = AssetDto.FromAsset(result, this, app);
return response; return response;
} }

7
src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs

@ -118,14 +118,11 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
[JsonProperty("_meta")] [JsonProperty("_meta")]
public AssetMetadata Metadata { get; set; } public AssetMetadata Metadata { get; set; }
public static AssetDto FromAsset(IAssetEntity asset, ApiController controller, string app, HashSet<string> tags = null, bool isDuplicate = false) public static AssetDto FromAsset(IEnrichedAssetEntity asset, ApiController controller, string app, bool isDuplicate = false)
{ {
var response = SimpleMapper.Map(asset, new AssetDto { FileType = asset.FileName.FileType() }); var response = SimpleMapper.Map(asset, new AssetDto { FileType = asset.FileName.FileType() });
if (tags != null) response.Tags = asset.TagNames;
{
response.Tags = tags;
}
if (isDuplicate) if (isDuplicate)
{ {

2
src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs

@ -37,7 +37,7 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
return Items.ToSurrogateKeys(); return Items.ToSurrogateKeys();
} }
public static AssetsDto FromAssets(IResultList<IAssetEntity> assets, ApiController controller, string app) public static AssetsDto FromAssets(IResultList<IEnrichedAssetEntity> assets, ApiController controller, string app)
{ {
var response = new AssetsDto var response = new AssetsDto
{ {

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

@ -67,9 +67,6 @@ namespace Squidex.Config.Domain
services.AddTransientAs(c => new RestructureContentCollection(c.GetRequiredService<IMongoClient>().GetDatabase(mongoContentDatabaseName))) services.AddTransientAs(c => new RestructureContentCollection(c.GetRequiredService<IMongoClient>().GetDatabase(mongoContentDatabaseName)))
.As<IMigration>(); .As<IMigration>();
services.AddTransientAs(c => new CreateStatusColors(c.GetRequiredService<IMongoClient>().GetDatabase(mongoContentDatabaseName)))
.As<IMigration>();
services.AddSingletonAs<MongoMigrationStatus>() services.AddSingletonAs<MongoMigrationStatus>()
.As<IMigrationStatus>(); .As<IMigrationStatus>();

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

@ -27,6 +27,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
public class AssetCommandMiddlewareTests : HandlerTestBase<AssetState> public class AssetCommandMiddlewareTests : HandlerTestBase<AssetState>
{ {
private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>(); private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
private readonly IAssetEnricher assetEnricher = A.Fake<IAssetEnricher>();
private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake<IAssetThumbnailGenerator>(); private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake<IAssetThumbnailGenerator>();
private readonly IAssetStore assetStore = A.Fake<MemoryAssetStore>(); private readonly IAssetStore assetStore = A.Fake<MemoryAssetStore>();
private readonly ITagService tagService = A.Fake<ITagService>(); private readonly ITagService tagService = A.Fake<ITagService>();
@ -52,19 +53,16 @@ namespace Squidex.Domain.Apps.Entities.Assets
asset.ActivateAsync(Id).Wait(); asset.ActivateAsync(Id).Wait();
A.CallTo(() => assetQuery.QueryByHashAsync(AppId, A<string>.Ignored)) A.CallTo(() => assetQuery.QueryByHashAsync(AppId, A<string>.Ignored))
.Returns(new List<IAssetEntity>()); .Returns(new List<IEnrichedAssetEntity>());
A.CallTo(() => tagService.DenormalizeTagsAsync(AppId, TagGroups.Assets, A<HashSet<string>>.Ignored))
.Returns(new Dictionary<string, string>
{
["1"] = "foundTag1",
["2"] = "foundTag2"
});
A.CallTo(() => grainFactory.GetGrain<IAssetGrain>(Id, null)) A.CallTo(() => grainFactory.GetGrain<IAssetGrain>(Id, null))
.Returns(asset); .Returns(asset);
sut = new AssetCommandMiddleware(grainFactory, assetQuery, assetStore, assetThumbnailGenerator, new[] { tagGenerator }, tagService); sut = new AssetCommandMiddleware(grainFactory,
assetEnricher,
assetQuery,
assetStore,
assetThumbnailGenerator, new[] { tagGenerator });
} }
[Fact] [Fact]
@ -84,7 +82,6 @@ namespace Squidex.Domain.Apps.Entities.Assets
Assert.Contains("tag1", command.Tags); Assert.Contains("tag1", command.Tags);
Assert.Contains("tag2", command.Tags); Assert.Contains("tag2", command.Tags);
Assert.Equal(new HashSet<string> { "tag1", "tag2" }, result.Tags);
AssertAssetHasBeenUploaded(0, context.ContextId); AssertAssetHasBeenUploaded(0, context.ContextId);
AssertAssetImageChecked(); AssertAssetImageChecked();
@ -257,15 +254,15 @@ namespace Squidex.Domain.Apps.Entities.Assets
.MustHaveHappened(); .MustHaveHappened();
} }
private void SetupSameHashAsset(string fileName, long fileSize, out IAssetEntity existing) private void SetupSameHashAsset(string fileName, long fileSize, out IEnrichedAssetEntity existing)
{ {
var temp = existing = A.Fake<IAssetEntity>(); var temp = existing = A.Fake<IEnrichedAssetEntity>();
A.CallTo(() => temp.FileName).Returns(fileName); A.CallTo(() => temp.FileName).Returns(fileName);
A.CallTo(() => temp.FileSize).Returns(fileSize); A.CallTo(() => temp.FileSize).Returns(fileSize);
A.CallTo(() => assetQuery.QueryByHashAsync(A<Guid>.Ignored, A<string>.Ignored)) A.CallTo(() => assetQuery.QueryByHashAsync(A<Guid>.Ignored, A<string>.Ignored))
.Returns(new List<IAssetEntity> { existing }); .Returns(new List<IEnrichedAssetEntity> { existing });
} }
private void SetupImageInfo() private void SetupImageInfo()

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

@ -25,6 +25,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
public class AssetQueryServiceTests public class AssetQueryServiceTests
{ {
private readonly ITagService tagService = A.Fake<ITagService>(); private readonly ITagService tagService = A.Fake<ITagService>();
private readonly IAssetEnricher assetEnricher = A.Fake<IAssetEnricher>();
private readonly IAssetRepository assetRepository = A.Fake<IAssetRepository>(); private readonly IAssetRepository assetRepository = A.Fake<IAssetRepository>();
private readonly IAppEntity app = A.Fake<IAppEntity>(); private readonly IAppEntity app = A.Fake<IAppEntity>();
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app"); private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app");
@ -52,7 +53,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
var options = Options.Create(new AssetOptions { DefaultPageSize = 30 }); var options = Options.Create(new AssetOptions { DefaultPageSize = 30 });
sut = new AssetQueryService(tagService, assetRepository, options); sut = new AssetQueryService(tagService, assetEnricher, assetRepository, options);
} }
[Fact] [Fact]
@ -71,7 +72,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
A.CallTo(() => assetRepository.FindAssetAsync(id, false)) A.CallTo(() => assetRepository.FindAssetAsync(id, false))
.Returns(CreateAsset(id, "id1", "id2", "id3")); .Returns(CreateAsset(id, "id1", "id2", "id3"));
var result = await sut.FindAssetAsync(context, id); var result = await sut.FindAssetAsync(id);
Assert.Equal(HashSet.Of("name1", "name2", "name3"), result.Tags); Assert.Equal(HashSet.Of("name1", "name2", "name3"), result.Tags);
} }

8
tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs

@ -158,17 +158,17 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
Data = data, Data = data,
DataDraft = dataDraft, DataDraft = dataDraft,
Status = Status.Draft, Status = Status.Draft,
StatusColor = "red" StatusInfo = new StatusInfo(Status.Draft, "red")
}; };
return content; return content;
} }
protected static IAssetEntity CreateAsset(Guid id) protected static IEnrichedAssetEntity CreateAsset(Guid id)
{ {
var now = SystemClock.Instance.GetCurrentInstant(); var now = SystemClock.Instance.GetCurrentInstant();
var asset = new FakeAssetEntity var asset = new AssetEntity
{ {
Id = id, Id = id,
Version = 1, Version = 1,
@ -185,7 +185,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
IsImage = true, IsImage = true,
PixelWidth = 800, PixelWidth = 800,
PixelHeight = 600, PixelHeight = 600,
Tags = new[] { "tag1", "tag2" }.ToHashSet() TagNames = new[] { "tag1", "tag2" }.ToHashSet()
}; };
return asset; return asset;

Loading…
Cancel
Save