Browse Source

Merge pull request #377 from Squidex/temp2

Enrich content
pull/372/head
Sebastian Stehle 7 years ago
committed by GitHub
parent
commit
9eb8846b71
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs
  2. 16
      src/Squidex.Domain.Apps.Core.Model/Contents/StatusColors.cs
  3. 23
      src/Squidex.Domain.Apps.Core.Model/Contents/StatusInfo.cs
  4. 2
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
  5. 2
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs
  6. 9
      src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs
  7. 67
      src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs
  8. 11
      src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs
  9. 82
      src/Squidex.Domain.Apps.Entities/Assets/AssetEnricher.cs
  10. 9
      src/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs
  11. 97
      src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs
  12. 19
      src/Squidex.Domain.Apps.Entities/Assets/IAssetEnricher.cs
  13. 6
      src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs
  14. 13
      src/Squidex.Domain.Apps.Entities/Assets/IEnrichedAssetEntity.cs
  15. 2
      src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs
  16. 41
      src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs
  17. 95
      src/Squidex.Domain.Apps.Entities/Contents/ContentEnricher.cs
  18. 8
      src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs
  19. 4
      src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs
  20. 144
      src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs
  21. 61
      src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs
  22. 10
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs
  23. 1
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs
  24. 10
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs
  25. 14
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs
  26. 19
      src/Squidex.Domain.Apps.Entities/Contents/IContentEnricher.cs
  27. 6
      src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs
  28. 8
      src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs
  29. 20
      src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs
  30. 8
      src/Squidex.Domain.Apps.Entities/Contents/QueryExecutionContext.cs
  31. 4
      src/Squidex.Domain.Apps.Entities/Contents/ScheduleJob.cs
  32. 7
      src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs
  33. 13
      src/Squidex.Domain.Apps.Entities/History/ParsedHistoryEvent.cs
  34. 6
      src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs
  35. 2
      src/Squidex.Domain.Apps.Events/Contents/ContentCreated.cs
  36. 2
      src/Squidex.Domain.Apps.Events/Contents/ContentStatusChanged.cs
  37. 10
      src/Squidex.Infrastructure/CollectionExtensions.cs
  38. 11
      src/Squidex.Infrastructure/EventSourcing/DefaultEventDataFormatter.cs
  39. 2
      src/Squidex.Infrastructure/ResultList.cs
  40. 24
      src/Squidex.Web/Resource.cs
  41. 4
      src/Squidex.Web/ResourceLink.cs
  42. 2
      src/Squidex.Web/UrlHelperExtensions.cs
  43. 10
      src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  44. 7
      src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs
  45. 2
      src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs
  46. 12
      src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  47. 32
      src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs
  48. 24
      src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs
  49. 32
      src/Squidex/Areas/Api/Controllers/Contents/Models/StatusInfoDto.cs
  50. 3
      src/Squidex/Areas/Api/Controllers/History/HistoryController.cs
  51. 13
      src/Squidex/Config/Domain/EntitiesServices.cs
  52. 2
      src/Squidex/app-config/webpack.config.js
  53. 5
      src/Squidex/app/features/content/pages/content/content-page.component.html
  54. 2
      src/Squidex/app/features/content/pages/contents/contents-filters-page.component.html
  55. 4
      src/Squidex/app/features/content/pages/contents/contents-page.component.ts
  56. 5
      src/Squidex/app/features/content/shared/content-item.component.html
  57. 4
      src/Squidex/app/features/content/shared/content-status.component.html
  58. 12
      src/Squidex/app/features/content/shared/content-status.component.scss
  59. 3
      src/Squidex/app/features/content/shared/content-status.component.ts
  60. 2
      src/Squidex/app/framework/utils/hateos.ts
  61. 2
      src/Squidex/app/shared/components/schema-category.component.ts
  62. 8
      src/Squidex/app/shared/services/contents.service.spec.ts
  63. 12
      src/Squidex/app/shared/services/contents.service.ts
  64. 14
      src/Squidex/app/shared/state/contents.state.ts
  65. 4
      src/Squidex/app/theme/_bootstrap.scss
  66. 33
      tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusTests.cs
  67. 2
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs
  68. 155
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs
  69. 101
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetEnricherTests.cs
  70. 92
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetQueryServiceTests.cs
  71. 2
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs
  72. 91
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs
  73. 85
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentEnricherTests.cs
  74. 60
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs
  75. 31
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs
  76. 18
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentVersionLoaderTests.cs
  77. 69
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs
  78. 38
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs
  79. 11
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs
  80. 7
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs
  81. 7
      tools/Migrate_01/OldEvents/AppPlanChanged.cs
  82. 38
      tools/Migrate_01/OldEvents/ContentCreated.cs
  83. 47
      tools/Migrate_01/OldEvents/ContentStatusChanged.cs

3
src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure;
using System;
namespace Squidex.Domain.Apps.Core.Contents
@ -45,7 +44,7 @@ namespace Squidex.Domain.Apps.Core.Contents
public override string ToString()
{
return name;
return Name;
}
public static bool operator ==(Status lhs, Status rhs)

16
src/Squidex.Domain.Apps.Core.Model/Contents/StatusColors.cs

@ -0,0 +1,16 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Contents
{
public static class StatusColors
{
public const string Archived = "#eb3142";
public const string Draft = "#8091a5";
public const string Published = "#4bb958";
}
}

23
src/Squidex.Domain.Apps.Core.Model/Contents/StatusInfo.cs

@ -0,0 +1,23 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Contents
{
public sealed class StatusInfo
{
public Status Status { get; }
public string Color { get; }
public StatusInfo(Status status, string color)
{
Status = status;
Color = color;
}
}
}

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>())
{

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

@ -77,7 +77,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
if (fullTextIds?.Count == 0)
{
return ResultList.Create<IContentEntity>(0);
return ResultList.CreateFrom<IContentEntity>(0);
}
return await contents.QueryAsync(schema, query, fullTextIds, status, inDraft, includeDraft);

9
src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs

@ -19,15 +19,6 @@ namespace Squidex.Domain.Apps.Entities.Apps
public AppHistoryEventsCreator(TypeNameRegistry typeNameRegistry)
: base(typeNameRegistry)
{
AddEventMessage("AppContributorAssignedEvent",
"assigned {user:[Contributor]} as {[Role]}");
AddEventMessage("AppClientUpdatedEvent",
"updated client {[Id]}");
AddEventMessage("AppPlanChanged",
"changed plan to {[Plan]}");
AddEventMessage<AppContributorAssigned>(
"assigned {user:[Contributor]} as {[Role]}");

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

@ -10,7 +10,6 @@ using System.Collections.Generic;
using System.Security.Cryptography;
using System.Threading.Tasks;
using Orleans;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Tags;
using Squidex.Infrastructure;
@ -22,31 +21,31 @@ namespace Squidex.Domain.Apps.Entities.Assets
public sealed class AssetCommandMiddleware : GrainCommandMiddleware<AssetCommand, IAssetGrain>
{
private readonly IAssetStore assetStore;
private readonly IAssetEnricher assetEnricher;
private readonly IAssetQueryService assetQuery;
private readonly IAssetThumbnailGenerator assetThumbnailGenerator;
private readonly IEnumerable<ITagGenerator<CreateAsset>> tagGenerators;
private readonly ITagService tagService;
public AssetCommandMiddleware(
IGrainFactory grainFactory,
IAssetEnricher assetEnricher,
IAssetQueryService assetQuery,
IAssetStore assetStore,
IAssetThumbnailGenerator assetThumbnailGenerator,
IEnumerable<ITagGenerator<CreateAsset>> tagGenerators,
ITagService tagService)
IEnumerable<ITagGenerator<CreateAsset>> tagGenerators)
: base(grainFactory)
{
Guard.NotNull(assetEnricher, nameof(assetEnricher));
Guard.NotNull(assetStore, nameof(assetStore));
Guard.NotNull(assetQuery, nameof(assetQuery));
Guard.NotNull(assetThumbnailGenerator, nameof(assetThumbnailGenerator));
Guard.NotNull(tagGenerators, nameof(tagGenerators));
Guard.NotNull(tagService, nameof(tagService));
this.assetStore = assetStore;
this.assetEnricher = assetEnricher;
this.assetQuery = assetQuery;
this.assetThumbnailGenerator = assetThumbnailGenerator;
this.tagGenerators = tagGenerators;
this.tagService = tagService;
}
public override async Task HandleAsync(CommandContext context, Func<Task> next)
@ -67,35 +66,30 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
var existings = await assetQuery.QueryByHashAsync(createAsset.AppId.Id, createAsset.FileHash);
AssetCreatedResult result = null;
foreach (var existing in existings)
{
if (IsDuplicate(createAsset, existing))
{
var denormalizedTags = await tagService.DenormalizeTagsAsync(createAsset.AppId.Id, TagGroups.Assets, existing.Tags);
var result = new AssetCreatedResult(existing, true);
result = new AssetCreatedResult(existing, true, new HashSet<string>(denormalizedTags.Values));
context.Complete(result);
await next();
return;
}
break;
}
if (result == null)
foreach (var tagGenerator in tagGenerators)
{
foreach (var tagGenerator in tagGenerators)
{
tagGenerator.GenerateTags(createAsset, createAsset.Tags);
}
tagGenerator.GenerateTags(createAsset, createAsset.Tags);
}
var asset = (IAssetEntity)await ExecuteCommandAsync(createAsset);
await HandleCoreAsync(context, next);
result = new AssetCreatedResult(asset, false, createAsset.Tags);
var asset = context.PlainResult as IEnrichedAssetEntity;
await assetStore.CopyAsync(context.ContextId.ToString(), createAsset.AssetId.ToString(), asset.FileVersion, null);
}
context.Complete(new AssetCreatedResult(asset, false));
context.Complete(result);
await assetStore.CopyAsync(context.ContextId.ToString(), createAsset.AssetId.ToString(), asset.FileVersion, null);
}
finally
{
@ -112,11 +106,11 @@ namespace Squidex.Domain.Apps.Entities.Assets
try
{
var result = (AssetResult)await ExecuteAndAdjustTagsAsync(updateAsset);
await HandleCoreAsync(context, next);
context.Complete(result);
var asset = context.PlainResult as IAssetEntity;
await assetStore.CopyAsync(context.ContextId.ToString(), updateAsset.AssetId.ToString(), result.Asset.FileVersion, null);
await assetStore.CopyAsync(context.ContextId.ToString(), updateAsset.AssetId.ToString(), asset.FileVersion, null);
}
finally
{
@ -126,34 +120,23 @@ namespace Squidex.Domain.Apps.Entities.Assets
break;
}
case AssetCommand command:
{
var result = await ExecuteAndAdjustTagsAsync(command);
context.Complete(result);
break;
}
default:
await base.HandleAsync(context, next);
await HandleCoreAsync(context, next);
break;
}
}
private async Task<object> ExecuteAndAdjustTagsAsync(AssetCommand command)
private async Task HandleCoreAsync(CommandContext context, Func<Task> next)
{
var result = await ExecuteCommandAsync(command);
await base.HandleAsync(context, next);
if (result is IAssetEntity asset)
if (context.PlainResult is IAssetEntity asset && !(context.PlainResult is IEnrichedAssetEntity))
{
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));
context.Complete(enriched);
}
return result;
}
private static bool IsDuplicate(CreateAsset createAsset, IAssetEntity asset)

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

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

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

@ -0,0 +1,82 @@
// ==========================================================================
// 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.Log;
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));
using (Profiler.TraceMethod<AssetEnricher>())
{
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
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using NodaTime;
using Squidex.Domain.Apps.Entities.Assets;
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; }
@ -31,6 +30,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.TestData
public HashSet<string> Tags { get; set; }
public HashSet<string> TagNames { get; set; }
public long Version { get; set; }
public string MimeType { get; set; }

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

@ -7,7 +7,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Microsoft.OData;
@ -21,9 +20,10 @@ using Squidex.Infrastructure.Queries.OData;
namespace Squidex.Domain.Apps.Entities.Assets
{
public class AssetQueryService : IAssetQueryService
public sealed class AssetQueryService : IAssetQueryService
{
private readonly ITagService tagService;
private readonly IAssetEnricher assetEnricher;
private readonly IAssetRepository assetRepository;
private readonly AssetOptions options;
@ -32,76 +32,82 @@ namespace Squidex.Domain.Apps.Entities.Assets
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(options, nameof(options));
Guard.NotNull(assetEnricher, nameof(assetEnricher));
Guard.NotNull(assetRepository, nameof(assetRepository));
Guard.NotNull(options, nameof(options));
this.tagService = tagService;
this.assetEnricher = assetEnricher;
this.assetRepository = assetRepository;
this.options = options.Value;
this.tagService = tagService;
}
public Task<IAssetEntity> FindAssetAsync(QueryContext context, Guid id)
{
Guard.NotNull(context, nameof(context));
return FindAssetAsync(context.App.Id, id);
}
public async Task<IAssetEntity> FindAssetAsync(Guid appId, Guid id)
public async Task<IEnrichedAssetEntity> FindAssetAsync( Guid id)
{
var asset = await assetRepository.FindAssetAsync(id);
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));
var assets = await assetRepository.QueryByHashAsync(appId, hash);
await DenormalizeTagsAsync(appId, assets);
return assets;
return await assetEnricher.EnrichAsync(assets);
}
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(query, nameof(query));
IResultList<IAssetEntity> assets;
if (query.Ids != null)
if (query.Ids != null && query.Ids.Count > 0)
{
assets = await assetRepository.QueryAsync(context.App.Id, new HashSet<Guid>(query.Ids));
assets = Sort(assets, query.Ids);
assets = await QueryByIdsAsync(context, query);
}
else
{
var parsedQuery = ParseQuery(context, query.ODataQuery);
assets = await assetRepository.QueryAsync(context.App.Id, parsedQuery);
assets = await QueryByQueryAsync(context, query);
}
await DenormalizeTagsAsync(context.App.Id, assets);
var enriched = await assetEnricher.EnrichAsync(assets);
return assets;
return ResultList.Create(assets.Total, enriched);
}
private static IResultList<IAssetEntity> Sort(IResultList<IAssetEntity> assets, IReadOnlyList<Guid> ids)
private async Task<IResultList<IAssetEntity>> QueryByQueryAsync(QueryContext context, Q query)
{
var parsedQuery = ParseQuery(context, query.ODataQuery);
return await assetRepository.QueryAsync(context.App.Id, parsedQuery);
}
private async Task<IResultList<IAssetEntity>> QueryByIdsAsync(QueryContext context, Q query)
{
var sorted = ids.Select(id => assets.FirstOrDefault(x => x.Id == id)).Where(x => x != null);
var assets = await assetRepository.QueryAsync(context.App.Id, new HashSet<Guid>(query.Ids));
return ResultList.Create(assets.Total, sorted);
return Sort(assets, query.Ids);
}
private static IResultList<IAssetEntity> Sort(IResultList<IAssetEntity> assets, IReadOnlyList<Guid> ids)
{
return assets.SortSet(x => x.Id, ids);
}
private Query ParseQuery(QueryContext context, string query)
@ -140,34 +146,5 @@ namespace Squidex.Domain.Apps.Entities.Assets
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; }
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
{
public class AssetResult
public interface IEnrichedAssetEntity : IAssetEntity
{
public IAssetEntity Asset { get; }
public HashSet<string> Tags { get; }
public AssetResult(IAssetEntity asset, HashSet<string> tags)
{
Asset = asset;
Tags = tags;
}
HashSet<string> TagNames { get; }
}
}

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

@ -15,7 +15,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Repositories
{
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);

41
src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs

@ -0,0 +1,41 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Orleans;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Contents
{
public sealed class ContentCommandMiddleware : GrainCommandMiddleware<ContentCommand, IContentGrain>
{
private readonly IContentEnricher contentEnricher;
public ContentCommandMiddleware(IGrainFactory grainFactory, IContentEnricher contentEnricher)
: base(grainFactory)
{
Guard.NotNull(contentEnricher, nameof(contentEnricher));
this.contentEnricher = contentEnricher;
}
public override async Task HandleAsync(CommandContext context, Func<Task> next)
{
await base.HandleAsync(context, next);
if (context.PlainResult is IContentEntity content && !(context.PlainResult is IEnrichedContentEntity))
{
var enriched = await contentEnricher.EnrichAsync(content);
context.Complete(enriched);
}
}
}
}

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

@ -0,0 +1,95 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Entities.Contents
{
public sealed class ContentEnricher : IContentEnricher
{
private const string DefaultColor = StatusColors.Draft;
private readonly IContentWorkflow contentWorkflow;
public ContentEnricher(IContentWorkflow contentWorkflow)
{
this.contentWorkflow = contentWorkflow;
}
public async Task<IEnrichedContentEntity> EnrichAsync(IContentEntity content)
{
Guard.NotNull(content, nameof(content));
var enriched = await EnrichAsync(Enumerable.Repeat(content, 1));
return enriched[0];
}
public async Task<IReadOnlyList<IEnrichedContentEntity>> EnrichAsync(IEnumerable<IContentEntity> contents)
{
Guard.NotNull(contents, nameof(contents));
using (Profiler.TraceMethod<ContentEnricher>())
{
var results = new List<ContentEntity>();
var cache = new Dictionary<(Guid, Status), StatusInfo>();
foreach (var content in contents)
{
var result = SimpleMapper.Map(content, new ContentEntity());
await ResolveColorAsync(content, result, cache);
await ResolveNextsAsync(content, result);
await ResolveCanUpdateAsync(content, result);
results.Add(result);
}
return results;
}
}
private async Task ResolveCanUpdateAsync(IContentEntity content, ContentEntity result)
{
result.CanUpdate = await contentWorkflow.CanUpdateAsync(content);
}
private async Task ResolveNextsAsync(IContentEntity content, ContentEntity result)
{
result.Nexts = await contentWorkflow.GetNextsAsync(content);
}
private async Task ResolveColorAsync(IContentEntity content, ContentEntity result, Dictionary<(Guid, Status), StatusInfo> cache)
{
result.StatusColor = await GetColorAsync(content.SchemaId, content.Status, cache);
}
private async Task<string> GetColorAsync(NamedId<Guid> schemaId, Status status, Dictionary<(Guid, Status), StatusInfo> cache)
{
if (!cache.TryGetValue((schemaId.Id, status), out var info))
{
info = await contentWorkflow.GetInfoAsync(status);
if (info == null)
{
info = new StatusInfo(status, DefaultColor);
}
cache[(schemaId.Id, status)] = info;
}
return info.Color;
}
}
}

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

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

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

@ -83,9 +83,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
await ctx.ExecuteScriptAsync(s => s.Change, "Published", c, c.Data);
}
var status = await contentWorkflow.GetInitialStatusAsync(ctx.Schema);
var statusInfo = await contentWorkflow.GetInitialStatusAsync(ctx.Schema);
Create(c, status);
Create(c, statusInfo.Status);
return Snapshot;
});

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

@ -34,10 +34,12 @@ namespace Squidex.Domain.Apps.Entities.Contents
public sealed class ContentQueryService : IContentQueryService
{
private static readonly Status[] StatusPublishedOnly = { Status.Published };
private readonly IContentRepository contentRepository;
private readonly IContentVersionLoader contentVersionLoader;
private static readonly IResultList<IEnrichedContentEntity> EmptyContents = ResultList.CreateFrom<IEnrichedContentEntity>(0);
private readonly IAppProvider appProvider;
private readonly IAssetUrlGenerator assetUrlGenerator;
private readonly IContentEnricher contentEnricher;
private readonly IContentRepository contentRepository;
private readonly IContentVersionLoader contentVersionLoader;
private readonly IScriptEngine scriptEngine;
private readonly ContentOptions options;
private readonly EdmModelBuilder modelBuilder;
@ -50,6 +52,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
public ContentQueryService(
IAppProvider appProvider,
IAssetUrlGenerator assetUrlGenerator,
IContentEnricher contentEnricher,
IContentRepository contentRepository,
IContentVersionLoader contentVersionLoader,
IScriptEngine scriptEngine,
@ -58,6 +61,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(assetUrlGenerator, nameof(assetUrlGenerator));
Guard.NotNull(contentEnricher, nameof(contentEnricher));
Guard.NotNull(contentRepository, nameof(contentRepository));
Guard.NotNull(contentVersionLoader, nameof(contentVersionLoader));
Guard.NotNull(modelBuilder, nameof(modelBuilder));
@ -66,6 +70,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
this.appProvider = appProvider;
this.assetUrlGenerator = assetUrlGenerator;
this.contentEnricher = contentEnricher;
this.contentRepository = contentRepository;
this.contentVersionLoader = contentVersionLoader;
this.modelBuilder = modelBuilder;
@ -73,7 +78,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
this.scriptEngine = scriptEngine;
}
public async Task<IContentEntity> FindContentAsync(QueryContext context, string schemaIdOrName, Guid id, long version = -1)
public async Task<IEnrichedContentEntity> FindContentAsync(QueryContext context, string schemaIdOrName, Guid id, long version = -1)
{
Guard.NotNull(context, nameof(context));
@ -83,25 +88,27 @@ namespace Squidex.Domain.Apps.Entities.Contents
using (Profiler.TraceMethod<ContentQueryService>())
{
var isVersioned = version > EtagVersion.Empty;
var status = GetStatus(context);
IContentEntity content;
var content =
isVersioned ?
await FindContentByVersionAsync(id, version) :
await FindContentAsync(context, id, status, schema);
if (version > EtagVersion.Empty)
{
content = await FindByVersionAsync(id, version);
}
else
{
content = await FindCoreAsync(context, id, schema);
}
if (content == null || (content.Status != Status.Published && !context.IsFrontendClient) || content.SchemaId.Id != schema.Id)
{
throw new DomainObjectNotFoundException(id.ToString(), typeof(IContentEntity));
}
return Transform(context, schema, content);
return await TransformAsync(context, schema, content);
}
}
public async Task<IResultList<IContentEntity>> QueryAsync(QueryContext context, string schemaIdOrName, Q query)
public async Task<IResultList<IEnrichedContentEntity>> QueryAsync(QueryContext context, string schemaIdOrName, Q query)
{
Guard.NotNull(context, nameof(context));
@ -111,95 +118,88 @@ namespace Squidex.Domain.Apps.Entities.Contents
using (Profiler.TraceMethod<ContentQueryService>())
{
var status = GetStatus(context);
IResultList<IContentEntity> contents;
if (query.Ids?.Count > 0)
if (query.Ids != null && query.Ids.Count > 0)
{
contents = await QueryAsync(context, schema, query.Ids.ToHashSet(), status);
contents = SortSet(contents, query.Ids);
contents = await QueryByIdsAsync(context, query, schema);
}
else
{
var parsedQuery = ParseQuery(context, query.ODataQuery, schema);
contents = await QueryAsync(context, schema, parsedQuery, status);
contents = await QueryByQueryAsync(context, query, schema);
}
return Transform(context, schema, contents);
return await TransformAsync(context, schema, contents);
}
}
public async Task<IReadOnlyList<IContentEntity>> QueryAsync(QueryContext context, IReadOnlyList<Guid> ids)
public async Task<IResultList<IEnrichedContentEntity>> QueryAsync(QueryContext context, IReadOnlyList<Guid> ids)
{
Guard.NotNull(context, nameof(context));
using (Profiler.TraceMethod<ContentQueryService>())
{
var status = GetStatus(context);
List<IContentEntity> result;
if (ids?.Count > 0)
if (ids == null || ids.Count == 0)
{
var contents = await QueryAsync(context, ids, status);
return EmptyContents;
}
var permissions = context.User.Permissions();
var results = new List<IEnrichedContentEntity>();
contents = contents.Where(x => HasPermission(permissions, x.Schema)).ToList();
var contents = await QueryCoreAsync(context, ids);
result = contents.Select(x => Transform(context, x.Schema, x.Content)).ToList();
result = SortList(result, ids).ToList();
}
else
var permissions = context.User.Permissions();
foreach (var group in contents.GroupBy(x => x.Schema.Id))
{
result = new List<IContentEntity>();
var schema = group.First().Schema;
if (HasPermission(permissions, schema))
{
var enriched = await TransformCoreAsync(context, schema, group.Select(x => x.Content));
results.AddRange(enriched);
}
}
return result;
return ResultList.Create(results.Count, results.SortList(x => x.Id, ids));
}
}
private IResultList<IContentEntity> Transform(QueryContext context, ISchemaEntity schema, IResultList<IContentEntity> contents)
private async Task<IResultList<IEnrichedContentEntity>> TransformAsync(QueryContext context, ISchemaEntity schema, IResultList<IContentEntity> contents)
{
var transformed = TransformCore(context, schema, contents);
var transformed = await TransformCoreAsync(context, schema, contents);
return ResultList.Create(contents.Total, transformed);
}
private IContentEntity Transform(QueryContext context, ISchemaEntity schema, IContentEntity content)
private async Task<IEnrichedContentEntity> TransformAsync(QueryContext context, ISchemaEntity schema, IContentEntity content)
{
return TransformCore(context, schema, Enumerable.Repeat(content, 1)).FirstOrDefault();
}
var transformed = await TransformCoreAsync(context, schema, Enumerable.Repeat(content, 1));
private static IResultList<IContentEntity> SortSet(IResultList<IContentEntity> contents, IReadOnlyList<Guid> ids)
{
return ResultList.Create(contents.Total, SortList(contents, ids));
return transformed[0];
}
private static IEnumerable<IContentEntity> SortList(IEnumerable<IContentEntity> contents, IReadOnlyList<Guid> ids)
{
return ids.Select(id => contents.FirstOrDefault(x => x.Id == id)).Where(x => x != null);
}
private IEnumerable<IContentEntity> TransformCore(QueryContext context, ISchemaEntity schema, IEnumerable<IContentEntity> contents)
private async Task<IReadOnlyList<IEnrichedContentEntity>> TransformCoreAsync(QueryContext context, ISchemaEntity schema, IEnumerable<IContentEntity> contents)
{
using (Profiler.TraceMethod<ContentQueryService>())
{
var results = new List<IEnrichedContentEntity>();
var converters = GenerateConverters(context).ToArray();
var scriptText = schema.SchemaDef.Scripts.Query;
var scripting = !string.IsNullOrWhiteSpace(scriptText);
var isScripting = !string.IsNullOrWhiteSpace(scriptText);
var enriched = await contentEnricher.EnrichAsync(contents);
foreach (var content in contents)
foreach (var content in enriched)
{
var result = SimpleMapper.Map(content, new ContentEntity());
if (result.Data != null)
{
if (!context.IsFrontendClient && isScripting)
if (!context.IsFrontendClient && scripting)
{
var ctx = new ScriptContext { User = context.User, Data = content.Data, ContentId = content.Id };
@ -218,8 +218,10 @@ namespace Squidex.Domain.Apps.Entities.Contents
result.DataDraft = null;
}
yield return result;
results.Add(result);
}
return results;
}
}
@ -344,32 +346,46 @@ namespace Squidex.Domain.Apps.Entities.Contents
}
}
private Task<List<(IContentEntity Content, ISchemaEntity Schema)>> QueryAsync(QueryContext context, IReadOnlyList<Guid> ids, Status[] status)
private async Task<IResultList<IContentEntity>> QueryByQueryAsync(QueryContext context, Q query, ISchemaEntity schema)
{
var parsedQuery = ParseQuery(context, query.ODataQuery, schema);
return await QueryCoreAsync(context, schema, parsedQuery);
}
private async Task<IResultList<IContentEntity>> QueryByIdsAsync(QueryContext context, Q query, ISchemaEntity schema)
{
var contents = await QueryCoreAsync(context, schema, query.Ids.ToHashSet());
return contents.SortSet(x => x.Id, query.Ids);
}
private Task<List<(IContentEntity Content, ISchemaEntity Schema)>> QueryCoreAsync(QueryContext context, IReadOnlyList<Guid> ids)
{
return contentRepository.QueryAsync(context.App, status, new HashSet<Guid>(ids), ShouldIncludeDraft(context));
return contentRepository.QueryAsync(context.App, GetStatus(context), new HashSet<Guid>(ids), WithDraft(context));
}
private Task<IResultList<IContentEntity>> QueryAsync(QueryContext context, ISchemaEntity schema, Query query, Status[] status)
private Task<IResultList<IContentEntity>> QueryCoreAsync(QueryContext context, ISchemaEntity schema, Query query)
{
return contentRepository.QueryAsync(context.App, schema, status, context.IsFrontendClient, query, ShouldIncludeDraft(context));
return contentRepository.QueryAsync(context.App, schema, GetStatus(context), context.IsFrontendClient, query, WithDraft(context));
}
private Task<IResultList<IContentEntity>> QueryAsync(QueryContext context, ISchemaEntity schema, HashSet<Guid> ids, Status[] status)
private Task<IResultList<IContentEntity>> QueryCoreAsync(QueryContext context, ISchemaEntity schema, HashSet<Guid> ids)
{
return contentRepository.QueryAsync(context.App, schema, status, ids, ShouldIncludeDraft(context));
return contentRepository.QueryAsync(context.App, schema, GetStatus(context), ids, WithDraft(context));
}
private Task<IContentEntity> FindContentAsync(QueryContext context, Guid id, Status[] status, ISchemaEntity schema)
private Task<IContentEntity> FindCoreAsync(QueryContext context, Guid id, ISchemaEntity schema)
{
return contentRepository.FindContentAsync(context.App, schema, status, id, ShouldIncludeDraft(context));
return contentRepository.FindContentAsync(context.App, schema, GetStatus(context), id, WithDraft(context));
}
private Task<IContentEntity> FindContentByVersionAsync(Guid id, long version)
private Task<IContentEntity> FindByVersionAsync(Guid id, long version)
{
return contentVersionLoader.LoadAsync(id, version);
}
private static bool ShouldIncludeDraft(QueryContext context)
private static bool WithDraft(QueryContext context)
{
return context.ApiStatus == StatusForApi.All || context.IsFrontendClient;
}

61
src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs

@ -11,42 +11,77 @@ using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.Contents
{
public sealed class DefaultContentWorkflow : IContentWorkflow
{
private static readonly Status[] All = { Status.Archived, Status.Draft, Status.Published };
private static readonly StatusInfo InfoArchived = new StatusInfo(Status.Archived, StatusColors.Archived);
private static readonly StatusInfo InfoDraft = new StatusInfo(Status.Draft, StatusColors.Draft);
private static readonly StatusInfo InfoPublished = new StatusInfo(Status.Published, StatusColors.Published);
private static readonly Dictionary<Status, Status[]> Flow = new Dictionary<Status, Status[]>
private static readonly StatusInfo[] All =
{
[Status.Draft] = new[] { Status.Archived, Status.Published },
[Status.Archived] = new[] { Status.Draft },
[Status.Published] = new[] { Status.Draft, Status.Archived }
InfoArchived,
InfoDraft,
InfoPublished
};
public Task<Status> GetInitialStatusAsync(ISchemaEntity schema)
private static readonly Dictionary<Status, (StatusInfo Info, StatusInfo[] Transitions)> Flow =
new Dictionary<Status, (StatusInfo Info, StatusInfo[] Transitions)>
{
[Status.Archived] = (InfoArchived, new[]
{
InfoDraft
}),
[Status.Draft] = (InfoDraft, new[]
{
InfoArchived,
InfoPublished
}),
[Status.Published] = (InfoPublished, new[]
{
InfoDraft,
InfoArchived
})
};
public Task<StatusInfo> GetInitialStatusAsync(ISchemaEntity schema)
{
return Task.FromResult(Status.Draft);
var result = InfoDraft;
return Task.FromResult(result);
}
public Task<bool> CanMoveToAsync(IContentEntity content, Status next)
{
return Task.FromResult(Flow.TryGetValue(content.Status, out var state) && state.Contains(next));
var result = Flow.TryGetValue(content.Status, out var step) && step.Transitions.Any(x => x.Status == next);
return Task.FromResult(result);
}
public Task<bool> CanUpdateAsync(IContentEntity content)
{
return Task.FromResult(content.Status != Status.Archived);
var result = content.Status != Status.Archived;
return Task.FromResult(result);
}
public Task<Status[]> GetNextsAsync(IContentEntity content)
public Task<StatusInfo> GetInfoAsync(Status status)
{
return Task.FromResult(Flow.TryGetValue(content.Status, out var result) ? result : Array.Empty<Status>());
var result = Flow[status].Info;
return Task.FromResult(result);
}
public Task<StatusInfo[]> GetNextsAsync(IContentEntity content)
{
var result = Flow.TryGetValue(content.Status, out var step) ? step.Transitions : Array.Empty<StatusInfo>();
return Task.FromResult(result);
}
public Task<Status[]> GetAllAsync(ISchemaEntity schema)
public Task<StatusInfo[]> GetAllAsync(ISchemaEntity schema)
{
return Task.FromResult(All);
}

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
{
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 readonly IDataLoaderContextAccessor dataLoaderContextAccessor;
private readonly IDependencyResolver resolver;
@ -53,7 +53,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
execution.UserContext = this;
}
public override Task<IAssetEntity> FindAssetAsync(Guid id)
public override Task<IEnrichedAssetEntity> FindAssetAsync(Guid id)
{
var dataLoader = GetAssetsLoader();
@ -67,7 +67,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
return dataLoader.LoadAsync(id);
}
public async Task<IReadOnlyList<IAssetEntity>> GetReferencedAssetsAsync(IJsonValue value)
public async Task<IReadOnlyList<IEnrichedAssetEntity>> GetReferencedAssetsAsync(IJsonValue value)
{
var ids = ParseIds(value);
@ -95,9 +95,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
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 =>
{
var result = await GetReferencedAssetsAsync(new List<Guid>(batch));

1
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs

@ -7,7 +7,6 @@
using System;
using GraphQL.Types;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types

10
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
{
public sealed class AssetGraphType : ObjectGraphType<IAssetEntity>
public sealed class AssetGraphType : ObjectGraphType<IEnrichedAssetEntity>
{
public AssetGraphType(IGraphModel model)
{
@ -167,8 +167,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
Name = "tags",
ResolvedType = null,
Resolver = Resolve(x => x.Tags),
Description = "The height of the image in pixels if the asset is an image.",
Resolver = Resolve(x => x.TagNames),
Description = "The asset tags.",
Type = AllTypes.NonNullTagsType
});
@ -186,9 +186,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
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));
}
}
}

14
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs

@ -13,7 +13,7 @@ using Squidex.Domain.Apps.Entities.Schemas;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
public sealed class ContentGraphType : ObjectGraphType<IContentEntity>
public sealed class ContentGraphType : ObjectGraphType<IEnrichedContentEntity>
{
public void Initialize(IGraphModel model, ISchemaEntity schema, IComplexGraphType contentDataType)
{
@ -78,6 +78,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Description = $"The the status of the {schemaName} content."
});
AddField(new FieldType
{
Name = "statusColor",
ResolvedType = AllTypes.NonNullString,
Resolver = Resolve(x => x.StatusColor),
Description = $"The color status of the {schemaName} content."
});
AddField(new FieldType
{
Name = "url",
@ -108,9 +116,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Description = $"The structure of a {schemaName} content type.";
}
private static IFieldResolver Resolve(Func<IContentEntity, object> action)
private static IFieldResolver Resolve(Func<IEnrichedContentEntity, object> action)
{
return new FuncFieldResolver<IContentEntity, object>(c => action(c.Source));
return new FuncFieldResolver<IEnrichedContentEntity, object>(c => action(c.Source));
}
}
}

19
src/Squidex.Domain.Apps.Entities/Contents/IContentEnricher.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.Contents
{
public interface IContentEnricher
{
Task<IEnrichedContentEntity> EnrichAsync(IContentEntity content);
Task<IReadOnlyList<IEnrichedContentEntity>> EnrichAsync(IEnumerable<IContentEntity> contents);
}
}

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

@ -17,11 +17,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
int DefaultPageSizeGraphQl { get; }
Task<IReadOnlyList<IContentEntity>> QueryAsync(QueryContext context, IReadOnlyList<Guid> ids);
Task<IResultList<IEnrichedContentEntity>> QueryAsync(QueryContext context, IReadOnlyList<Guid> ids);
Task<IResultList<IContentEntity>> QueryAsync(QueryContext context, string schemaIdOrName, Q query);
Task<IResultList<IEnrichedContentEntity>> QueryAsync(QueryContext context, string schemaIdOrName, Q query);
Task<IContentEntity> FindContentAsync(QueryContext context, string schemaIdOrName, Guid id, long version = EtagVersion.Any);
Task<IEnrichedContentEntity> FindContentAsync(QueryContext context, string schemaIdOrName, Guid id, long version = EtagVersion.Any);
Task<ISchemaEntity> GetSchemaOrThrowAsync(QueryContext context, string schemaIdOrName);
}

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

@ -13,14 +13,16 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
public interface IContentWorkflow
{
Task<Status> GetInitialStatusAsync(ISchemaEntity schema);
Task<StatusInfo> GetInitialStatusAsync(ISchemaEntity schema);
Task<bool> CanMoveToAsync(IContentEntity content, Status next);
Task<bool> CanUpdateAsync(IContentEntity content);
Task<Status[]> GetNextsAsync(IContentEntity content);
Task<StatusInfo> GetInfoAsync(Status status);
Task<Status[]> GetAllAsync(ISchemaEntity schema);
Task<StatusInfo[]> GetNextsAsync(IContentEntity content);
Task<StatusInfo[]> GetAllAsync(ISchemaEntity schema);
}
}

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

@ -0,0 +1,20 @@
// ==========================================================================
// 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
{
bool CanUpdate { get; }
string StatusColor { 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
{
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 IAssetQueryService assetQuery;
private readonly QueryContext context;
@ -34,13 +34,13 @@ namespace Squidex.Domain.Apps.Entities.Contents
this.context = context;
}
public virtual async Task<IAssetEntity> FindAssetAsync(Guid id)
public virtual async Task<IEnrichedAssetEntity> FindAssetAsync(Guid id)
{
var asset = cachedAssets.GetOrDefault(id);
if (asset == null)
{
asset = await assetQuery.FindAssetAsync(context, id);
asset = await assetQuery.FindAssetAsync(id);
if (asset != null)
{
@ -92,7 +92,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
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));

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

@ -16,12 +16,12 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
public Guid Id { get; }
public Instant DueTime { get; }
public Status Status { get; }
public RefToken ScheduledBy { get; }
public Instant DueTime { get; }
public ScheduleJob(Guid id, Status status, RefToken scheduledBy, Instant dueTime)
{
Id = id;

7
src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs

@ -50,11 +50,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.State
SimpleMapper.Map(@event, this);
UpdateData(null, @event.Data, false);
if (Status == default)
{
Status = Status.Draft;
}
}
protected void On(ContentChangesPublished @event)
@ -68,7 +63,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.State
{
ScheduleJob = null;
Status = @event.Status;
SimpleMapper.Map(@event, this);
if (@event.Status == Status.Published)
{

13
src/Squidex.Domain.Apps.Entities/History/ParsedHistoryEvent.cs

@ -53,14 +53,17 @@ namespace Squidex.Domain.Apps.Entities.History
message = new Lazy<string>(() =>
{
var result = texts[item.Message];
foreach (var kvp in item.Parameters)
if (texts.TryGetValue(item.Message, out var result))
{
result = result.Replace("[" + kvp.Key + "]", kvp.Value);
foreach (var kvp in item.Parameters)
{
result = result.Replace("[" + kvp.Key + "]", kvp.Value);
}
return result;
}
return result;
return null;
});
}
}

6
src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs

@ -19,12 +19,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas
public SchemaHistoryEventsCreator(TypeNameRegistry typeNameRegistry)
: base(typeNameRegistry)
{
AddEventMessage("SchemaCreatedEvent",
"created schema {[Name]}.");
AddEventMessage("ScriptsConfiguredEvent",
"configured script of schema {[Name]}.");
AddEventMessage<SchemaFieldsReordered>(
"reordered fields of schema {[Name]}.");

2
src/Squidex.Domain.Apps.Events/Contents/ContentCreated.cs

@ -10,7 +10,7 @@ using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Contents
{
[EventType(nameof(ContentCreated))]
[EventType(nameof(ContentCreated), 2)]
public sealed class ContentCreated : ContentEvent
{
public Status Status { get; set; }

2
src/Squidex.Domain.Apps.Events/Contents/ContentStatusChanged.cs

@ -10,7 +10,7 @@ using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Contents
{
[EventType(nameof(ContentStatusChanged))]
[EventType(nameof(ContentStatusChanged), 2)]
public sealed class ContentStatusChanged : ContentEvent
{
public StatusChange Change { get; set; }

10
src/Squidex.Infrastructure/CollectionExtensions.cs

@ -13,6 +13,16 @@ namespace Squidex.Infrastructure
{
public static class CollectionExtensions
{
public static IResultList<T> SortSet<T, TKey>(this IResultList<T> input, Func<T, TKey> idProvider, IReadOnlyList<TKey> ids) where T : class
{
return ResultList.Create(input.Total, SortList(input, idProvider, ids));
}
public static IEnumerable<T> SortList<T, TKey>(this IEnumerable<T> input, Func<T, TKey> idProvider, IReadOnlyList<TKey> ids) where T : class
{
return ids.Select(id => input.FirstOrDefault(x => Equals(idProvider(x), id))).Where(x => x != null);
}
public static void AddRange<T>(this ICollection<T> target, IEnumerable<T> source)
{
foreach (var value in source)

11
src/Squidex.Infrastructure/EventSourcing/DefaultEventDataFormatter.cs

@ -6,6 +6,7 @@
// ==========================================================================
using System;
using System.Diagnostics;
using Squidex.Infrastructure.Json;
namespace Squidex.Infrastructure.EventSourcing
@ -33,6 +34,11 @@ namespace Squidex.Infrastructure.EventSourcing
if (payloadObj is IMigrated<IEvent> migratedEvent)
{
payloadObj = migratedEvent.Migrate();
if (ReferenceEquals(migratedEvent, payloadObj))
{
Debug.WriteLine("Migration should return new event.");
}
}
var envelope = new Envelope<IEvent>(payloadObj, eventData.Headers);
@ -47,6 +53,11 @@ namespace Squidex.Infrastructure.EventSourcing
if (migrate && eventPayload is IMigrated<IEvent> migratedEvent)
{
eventPayload = migratedEvent.Migrate();
if (ReferenceEquals(migratedEvent, eventPayload))
{
Debug.WriteLine("Migration should return new event.");
}
}
var payloadType = typeNameRegistry.GetName(eventPayload.GetType());

2
src/Squidex.Infrastructure/ResultList.cs

@ -27,7 +27,7 @@ namespace Squidex.Infrastructure
return new Impl<T>(items, total);
}
public static IResultList<T> Create<T>(long total, params T[] items)
public static IResultList<T> CreateFrom<T>(long total, params T[] items)
{
return new Impl<T>(items, total);
}

24
src/Squidex.Web/Resource.cs

@ -24,38 +24,38 @@ namespace Squidex.Web
AddGetLink("self", href);
}
public void AddGetLink(string rel, string href)
public void AddGetLink(string rel, string href, string metadata = null)
{
AddLink(rel, "GET", href);
AddLink(rel, "GET", href, metadata);
}
public void AddPatchLink(string rel, string href)
public void AddPatchLink(string rel, string href, string metadata = null)
{
AddLink(rel, "PATCH", href);
AddLink(rel, "PATCH", href, metadata);
}
public void AddPostLink(string rel, string href)
public void AddPostLink(string rel, string href, string metadata = null)
{
AddLink(rel, "POST", href);
AddLink(rel, "POST", href, metadata);
}
public void AddPutLink(string rel, string href)
public void AddPutLink(string rel, string href, string metadata = null)
{
AddLink(rel, "PUT", href);
AddLink(rel, "PUT", href, metadata);
}
public void AddDeleteLink(string rel, string href)
public void AddDeleteLink(string rel, string href, string metadata = null)
{
AddLink(rel, "DELETE", href);
AddLink(rel, "DELETE", href, metadata);
}
public void AddLink(string rel, string method, string href)
public void AddLink(string rel, string method, string href, string metadata = null)
{
Guard.NotNullOrEmpty(rel, nameof(rel));
Guard.NotNullOrEmpty(href, nameof(href));
Guard.NotNullOrEmpty(method, nameof(method));
Links[rel] = new ResourceLink { Href = href, Method = method };
Links[rel] = new ResourceLink { Href = href, Method = method, Metadata = metadata };
}
}
}

4
src/Squidex.Web/ResourceLink.cs

@ -18,5 +18,9 @@ namespace Squidex.Web
[Required]
[Display(Description = "The link method.")]
public string Method { get; set; }
[Required]
[Display(Description = "Additional data about the link.")]
public string Metadata { get; set; }
}
}

2
src/Squidex.Web/UrlHelperExtensions.cs

@ -38,7 +38,7 @@ namespace Squidex.Web
public static string Url<T>(this Controller controller, Func<T, string> action, object values = null) where T : Controller
{
return controller.Url.Url<T>(action, values);
return controller.Url.Url(action, values);
}
}
}

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

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

12
src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs

@ -16,7 +16,6 @@ using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.GraphQL;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Shared;
using Squidex.Web;
@ -127,9 +126,8 @@ namespace Squidex.Areas.Api.Controllers.Contents
{
var context = Context();
var contents = await contentQuery.QueryAsync(context, Q.Empty.WithIds(ids).Ids);
var contentsList = ResultList.Create<IContentEntity>(contents.Count, contents);
var response = await ContentsDto.FromContentsAsync(contentsList, context, this, null, contentWorkflow);
var response = await ContentsDto.FromContentsAsync(contents, context, this, null, contentWorkflow);
if (controllerOptions.Value.EnableSurrogateKeys && response.Items.Length <= controllerOptions.Value.MaxItemsForSurrogateKeys)
{
@ -201,7 +199,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
var context = Context();
var content = await contentQuery.FindContentAsync(context, name, id);
var response = await ContentDto.FromContentAsync(context, content, contentWorkflow, this);
var response = ContentDto.FromContent(context, content, this);
if (controllerOptions.Value.EnableSurrogateKeys)
{
@ -237,7 +235,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
var context = Context();
var content = await contentQuery.FindContentAsync(context, name, id, version);
var response = await ContentDto.FromContentAsync(context, content, contentWorkflow, this);
var response = ContentDto.FromContent(context, content, this);
if (controllerOptions.Value.EnableSurrogateKeys)
{
@ -447,8 +445,8 @@ namespace Squidex.Areas.Api.Controllers.Contents
{
var context = await CommandBus.PublishAsync(command);
var result = context.Result<IContentEntity>();
var response = await ContentDto.FromContentAsync(null, result, contentWorkflow, this);
var result = context.Result<IEnrichedContentEntity>();
var response = ContentDto.FromContent(null, result, this);
return response;
}

32
src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs

@ -7,7 +7,6 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent;
@ -71,20 +70,21 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
public Instant LastModified { get; set; }
/// <summary>
/// The the status of the content.
/// The status of the content.
/// </summary>
public Status Status { get; set; }
/// <summary>
/// The color of the status.
/// </summary>
public string StatusColor { get; set; }
/// <summary>
/// The version of the content.
/// </summary>
public long Version { get; set; }
public static ValueTask<ContentDto> FromContentAsync(
QueryContext context,
IContentEntity content,
IContentWorkflow contentWorkflow,
ApiController controller)
public static ContentDto FromContent(QueryContext context, IEnrichedContentEntity content, ApiController controller)
{
var response = SimpleMapper.Map(content, new ContentDto());
@ -104,14 +104,10 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
response.ScheduleJob = SimpleMapper.Map(content.ScheduleJob, new ScheduleJobDto());
}
return response.CreateLinksAsync(content, controller, content.AppId.Name, content.SchemaId.Name, contentWorkflow);
return response.CreateLinksAsync(content, controller, content.AppId.Name, content.SchemaId.Name);
}
private async ValueTask<ContentDto> CreateLinksAsync(IContentEntity content,
ApiController controller,
string app,
string schema,
IContentWorkflow contentWorkflow)
private ContentDto CreateLinksAsync(IEnrichedContentEntity content, ApiController controller, string app, string schema)
{
var values = new { app, name = schema, id = Id };
@ -139,7 +135,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
if (controller.HasPermission(Permissions.AppContentsUpdate, app, schema))
{
if (await contentWorkflow.CanUpdateAsync(content))
if (content.CanUpdate)
{
AddPutLink("update", controller.Url<ContentsController>(x => nameof(x.PutContent), values));
}
@ -157,13 +153,11 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
AddDeleteLink("delete", controller.Url<ContentsController>(x => nameof(x.DeleteContent), values));
}
var nextStatuses = await contentWorkflow.GetNextsAsync(content);
foreach (var next in nextStatuses)
foreach (var next in content.Nexts)
{
if (controller.HasPermission(Helper.StatusPermission(app, schema, next)))
if (controller.HasPermission(Helper.StatusPermission(app, schema, next.Status)))
{
AddPutLink($"status/{next}", controller.Url<ContentsController>(x => nameof(x.PutContentStatus), values));
AddPutLink($"status/{next.Status}", controller.Url<ContentsController>(x => nameof(x.PutContentStatus), values), next.Color);
}
}

24
src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs

@ -35,7 +35,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
/// The possible statuses.
/// </summary>
[Required]
public Status[] Statuses { get; set; }
public StatusInfoDto[] Statuses { get; set; }
public string ToEtag()
{
@ -47,20 +47,16 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
return Items.ToSurrogateKeys();
}
public static async Task<ContentsDto> FromContentsAsync(IResultList<IContentEntity> contents, QueryContext context,
ApiController controller,
ISchemaEntity schema,
IContentWorkflow contentWorkflow)
public static async Task<ContentsDto> FromContentsAsync(IResultList<IEnrichedContentEntity> contents,
QueryContext context, ApiController controller, ISchemaEntity schema, IContentWorkflow contentWorkflow)
{
var result = new ContentsDto
{
Total = contents.Total,
Items = new ContentDto[contents.Count]
Items = contents.Select(x => ContentDto.FromContent(context, x, controller)).ToArray()
};
await Task.WhenAll(
result.AssignContentsAsync(contentWorkflow, contents, context, controller),
result.AssignStatusesAsync(contentWorkflow, schema));
await result.AssignStatusesAsync(contentWorkflow, schema);
return result.CreateLinks(controller, schema.AppId.Name, schema.SchemaDef.Name);
}
@ -69,15 +65,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
{
var allStatuses = await contentWorkflow.GetAllAsync(schema);
Statuses = allStatuses.ToArray();
}
private async Task AssignContentsAsync(IContentWorkflow contentWorkflow, IResultList<IContentEntity> contents, QueryContext context, ApiController controller)
{
for (var i = 0; i < Items.Length; i++)
{
Items[i] = await ContentDto.FromContentAsync(context, contents[i], contentWorkflow, controller);
}
Statuses = allStatuses.Select(StatusInfoDto.FromStatusInfo).ToArray();
}
private ContentsDto CreateLinks(ApiController controller, string app, string schema)

32
src/Squidex/Areas/Api/Controllers/Contents/Models/StatusInfoDto.cs

@ -0,0 +1,32 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Core.Contents;
namespace Squidex.Areas.Api.Controllers.Contents.Models
{
public sealed class StatusInfoDto
{
/// <summary>
/// The name of the status.
/// </summary>
[Required]
public Status Status { get; set; }
/// <summary>
/// The color of the status.
/// </summary>
[Required]
public string Color { get; set; }
public static StatusInfoDto FromStatusInfo(StatusInfo statusInfo)
{
return new StatusInfoDto { Status = statusInfo.Status, Color = statusInfo.Color };
}
}
}

3
src/Squidex/Areas/Api/Controllers/History/HistoryController.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Squidex.Areas.Api.Controllers.History.Models;
@ -48,7 +49,7 @@ namespace Squidex.Areas.Api.Controllers.History
{
var events = await historyService.QueryByChannelAsync(AppId, channel, 100);
var response = events.ToArray(HistoryEventDto.FromHistoryEvent);
var response = events.Select(HistoryEventDto.FromHistoryEvent).Where(x => x.Message != null).ToArray();
return Ok(response);
}

13
src/Squidex/Config/Domain/EntitiesServices.cs

@ -32,7 +32,6 @@ using Squidex.Domain.Apps.Entities.Backup;
using Squidex.Domain.Apps.Entities.Comments;
using Squidex.Domain.Apps.Entities.Comments.Commands;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.Edm;
using Squidex.Domain.Apps.Entities.Contents.GraphQL;
using Squidex.Domain.Apps.Entities.Contents.Text;
@ -97,9 +96,15 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<AppProvider>()
.As<IAppProvider>();
services.AddSingletonAs<AssetEnricher>()
.As<IAssetEnricher>();
services.AddSingletonAs<AssetQueryService>()
.As<IAssetQueryService>();
services.AddSingletonAs<ContentEnricher>()
.As<IContentEnricher>();
services.AddSingletonAs<ContentQueryService>()
.As<IContentQueryService>();
@ -222,6 +227,9 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<AssetCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddSingletonAs<ContentCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddSingletonAs<AppsByNameIndexCommandMiddleware>()
.As<ICommandMiddleware>();
@ -231,9 +239,6 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<GrainCommandMiddleware<CommentsCommand, ICommentGrain>>()
.As<ICommandMiddleware>();
services.AddSingletonAs<GrainCommandMiddleware<ContentCommand, IContentGrain>>()
.As<ICommandMiddleware>();
services.AddSingletonAs<GrainCommandMiddleware<SchemaCommand, ISchemaGrain>>()
.As<ICommandMiddleware>();

2
src/Squidex/app-config/webpack.config.js

@ -251,7 +251,9 @@ module.exports = function (env) {
waitForLinting: isProduction
})
);
}
if (!isCoverage) {
config.plugins.push(
new plugins.NgToolsWebpack.AngularCompilerPlugin({
directTemplateLoading: true,

5
src/Squidex/app/features/content/pages/content/content-page.component.html

@ -36,6 +36,7 @@
[class.active]="dropdown.isOpen | async" #optionsButton>
<sqx-content-status
[status]="content.status"
[statusColor]="content.statusColor"
[scheduledTo]="content.scheduleJob?.status"
[scheduledAt]="content.scheduleJob?.dueTime"
[isPending]="content.isPending"
@ -56,8 +57,8 @@
</a>
<ng-container *ngIf="!schema.isSingleton">
<a class="dropdown-item" *ngFor="let status of content.statusUpdates" (click)="changeStatus(status)">
Status to {{status}}
<a class="dropdown-item" *ngFor="let info of content.statusUpdates" (click)="changeStatus(info.status)">
Change to <i class="icon-circle icon-sm" [style.color]="info.color"></i> {{info.status}}
</a>
<div class="dropdown-divider"></div>

2
src/Squidex/app/features/content/pages/contents/contents-filters-page.component.html

@ -17,7 +17,7 @@
<a class="sidebar-item" *ngFor="let query of contentsState.statusQueries | async; trackBy: trackByQuery" (click)="search(query.filter)"
[class.active]="isSelectedQuery(query.filter)">
{{query.name}}
<i class="icon-circle" [style.color]="query.color"></i> {{query.name}}
</a>
</div>

4
src/Squidex/app/features/content/pages/contents/contents-page.component.ts

@ -207,8 +207,8 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
const allActions = {};
for (let content of this.contentsState.snapshot.contents.values) {
for (let status of content.statusUpdates) {
allActions[status] = true;
for (let info of content.statusUpdates) {
allActions[info.status] = info.color;
}
}

5
src/Squidex/app/features/content/shared/content-item.component.html

@ -24,6 +24,7 @@
<td class="cell-time" *ngIf="!isCompact" [sqxStopClick]="isDirty">
<sqx-content-status
[status]="content.status"
[statusColor]="content.statusColor"
[scheduledTo]="content.scheduleJob?.status"
[scheduledAt]="content.scheduleJob?.dueTime"
[isPending]="content.isPending">
@ -55,8 +56,8 @@
<i class="icon-dots"></i>
</button>
<div class="dropdown-menu" *sqxModalView="dropdown;closeAlways:true" [sqxModalTarget]="optionsButton" @fade>
<a class="dropdown-item" *ngFor="let status of content.statusUpdates" (click)="emitChangeStatus(status)">
Status to {{status}}
<a class="dropdown-item" *ngFor="let info of content.statusUpdates" (click)="emitChangeStatus(info.status)">
Change to <i class="icon-circle icon-sm" [style.color]="info.color"></i> {{info.status}}
</a>
<a class="dropdown-item" (click)="emitClone(); dropdown.hide()" *ngIf="canClone">
Clone

4
src/Squidex/app/features/content/shared/content-status.component.html

@ -1,11 +1,11 @@
<ng-container *ngIf="scheduledTo; else noSchedule">
<span class="content-status content-status-{{scheduledTo | lowercase}} mr-1" title="{{tooltipText}}" titlePosition="top">
<span class="content-status pending mr-1"title="{{tooltipText}}" titlePosition="top">
<i class="icon-clock"></i>
</span>
</ng-container>
<ng-template #noSchedule>
<span class="content-status content-status-{{displayStatus | lowercase}} mr-1" title="{{tooltipText}}" titlePosition="top">
<span class="content-status default mr-1" [style.color]="statusColor" title="{{tooltipText}}" titlePosition="top">
<i class="icon-circle"></i>
</span>
</ng-template>

12
src/Squidex/app/features/content/shared/content-status.component.scss

@ -6,19 +6,11 @@
vertical-align: middle;
}
&-published {
color: $color-theme-green;
}
&-draft {
&.default {
color: $color-text-decent;
}
&-archived {
color: $color-theme-error;
}
&-pending {
&.pending {
color: $color-dark-black;
}

3
src/Squidex/app/features/content/shared/content-status.component.ts

@ -19,6 +19,9 @@ export class ContentStatusComponent {
@Input()
public status: string;
@Input()
public statusColor: string;
@Input()
public scheduledTo?: string;

2
src/Squidex/app/framework/utils/hateos.ts

@ -13,7 +13,7 @@ export interface Resource {
}
export type ResourceLinks = { [rel: string]: ResourceLink };
export type ResourceLink = { href: string; method: ResourceMethod; };
export type ResourceLink = { href: string; method: ResourceMethod; metadata?: string; };
export type Metadata = { [rel: string]: string };

2
src/Squidex/app/shared/components/schema-category.component.ts

@ -74,7 +74,7 @@ export class SchemaCategoryComponent extends StatefulComponent<State> implements
let filtered = this.schemaCategory.schemas;
if (this.forContent) {
filtered = filtered.filter(x => x.canReadContents);
filtered = filtered.filter(x => x.canReadContents && x.isPublished);
}
let isOpen = false;

8
src/Squidex/app/shared/services/contents.service.spec.ts

@ -62,11 +62,13 @@ describe('ContentsService', () => {
contentResponse(12),
contentResponse(13)
],
statuses: ['Draft', 'Published']
statuses: [{
status: 'Draft', color: 'Gray'
}]
});
expect(contents!).toEqual(
new ContentsDto(['Draft', 'Published'], 10, [
new ContentsDto([{ status: 'Draft', color: 'Gray' }], 10, [
createContent(12),
createContent(13)
]));
@ -351,6 +353,7 @@ describe('ContentsService', () => {
return {
id: `id${id}`,
status: `Status${id}`,
statusColor: 'black',
created: `${id % 1000 + 2000}-12-12T10:10:00`,
createdBy: `creator-${id}`,
lastModified: `${id % 1000 + 2000}-11-11T10:10:00`,
@ -379,6 +382,7 @@ export function createContent(id: number, suffix = '') {
return new ContentDto(links,
`id${id}`,
`Status${id}${suffix}`,
'black',
DateTime.parseISO_UTC(`${id % 1000 + 2000}-12-12T10:10:00`), `creator-${id}`,
DateTime.parseISO_UTC(`${id % 1000 + 2000}-11-11T10:10:00`), `modifier-${id}`,
new ScheduleDto('Draft', `Scheduler${id}`, DateTime.parseISO_UTC(`${id % 1000 + 2000}-11-11T10:10:00`)),

12
src/Squidex/app/shared/services/contents.service.ts

@ -34,9 +34,11 @@ export class ScheduleDto {
}
}
export type StatusInfo = { status: string; color: string; };
export class ContentsDto extends ResultSet<ContentDto> {
constructor(
public readonly statuses: string[],
public readonly statuses: StatusInfo[],
total: number,
items: ContentDto[],
links?: ResourceLinks
@ -56,7 +58,7 @@ export class ContentsDto extends ResultSet<ContentDto> {
export class ContentDto {
public readonly _links: ResourceLinks;
public readonly statusUpdates: string[];
public readonly statusUpdates: StatusInfo[];
public readonly canDelete: boolean;
public readonly canDraftDiscard: boolean;
@ -67,6 +69,7 @@ export class ContentDto {
constructor(links: ResourceLinks,
public readonly id: string,
public readonly status: string,
public readonly statusColor: string,
public readonly created: DateTime,
public readonly createdBy: string,
public readonly lastModified: DateTime,
@ -85,7 +88,7 @@ export class ContentDto {
this.canDraftPublish = hasAnyLink(links, 'draft/publish');
this.canUpdate = hasAnyLink(links, 'update');
this.statusUpdates = Object.keys(links).filter(x => x.startsWith('status/')).map(x => x.substr(7));
this.statusUpdates = Object.keys(links).filter(x => x.startsWith('status/')).map(x => ({ status: x.substr(7), color: links[x].metadata! }));
}
}
@ -133,7 +136,7 @@ export class ContentsService {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}?${fullQuery}`);
return this.http.get<{ total: number, items: [], statuses: string[] } & Resource>(url).pipe(
return this.http.get<{ total: number, items: [], statuses: StatusInfo[] } & Resource>(url).pipe(
map(({ total, items, statuses, _links }) => {
const contents = items.map(x => parseContent(x));
@ -282,6 +285,7 @@ function parseContent(response: any) {
return new ContentDto(response._links,
response.id,
response.status,
response.statusColor,
DateTime.parseISO_UTC(response.created), response.createdBy,
DateTime.parseISO_UTC(response.lastModified), response.lastModifiedBy,
response.scheduleJob

14
src/Squidex/app/shared/state/contents.state.ts

@ -24,7 +24,7 @@ import { SchemaDto } from './../services/schemas.service';
import { AppsState } from './apps.state';
import { SchemasState } from './schemas.state';
import { ContentDto, ContentsService } from './../services/contents.service';
import { ContentDto, ContentsService, StatusInfo } from './../services/contents.service';
interface Snapshot {
// The current comments.
@ -40,7 +40,7 @@ interface Snapshot {
isLoaded?: boolean;
// The statuses.
statuses?: string[];
statuses?: StatusInfo[];
// The selected content.
selectedContent?: ContentDto | null;
@ -348,10 +348,12 @@ export class ManualContentsState extends ContentsStateBase {
}
}
function buildQueries(x: string[] | undefined): { name: string; filter: string; }[] {
return x ? x.map(s => buildQuery(s)) : [];
export type ContentQuery = { color: string; name: string; filter: string; };
function buildQueries(statuses: StatusInfo[] | undefined): ContentQuery[] {
return statuses ? statuses.map(s => buildQuery(s)) : [];
}
function buildQuery(s: string) {
return ({ name: s, filter: `$filter=status eq '${s}'` });
function buildQuery(s: StatusInfo) {
return ({ name: s.status, color: s.color, filter: `$filter=status eq '${s}'` });
}

4
src/Squidex/app/theme/_bootstrap.scss

@ -304,6 +304,10 @@ a {
}
}
.icon-sm {
font-size: 70%;
}
//
// Button improvements
//

33
tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusTests.cs

@ -12,40 +12,49 @@ namespace Squidex.Domain.Apps.Core.Model.Contents
{
public class StatusTests
{
[Fact]
public void Should_initialize_default()
{
Status status = default;
Assert.Equal("Unknown", status.Name);
Assert.Equal("Unknown", status.ToString());
}
[Fact]
public void Should_initialize_status_from_string()
{
var result = new Status("Custom");
var status = new Status("Custom");
Assert.Equal("Custom", result.Name);
Assert.Equal("Custom", result.ToString());
Assert.Equal("Custom", status.Name);
Assert.Equal("Custom", status.ToString());
}
[Fact]
public void Should_provide_draft_status()
{
var result = Status.Draft;
var status = Status.Draft;
Assert.Equal("Draft", result.Name);
Assert.Equal("Draft", result.ToString());
Assert.Equal("Draft", status.Name);
Assert.Equal("Draft", status.ToString());
}
[Fact]
public void Should_provide_archived_status()
{
var result = Status.Archived;
var status = Status.Archived;
Assert.Equal("Archived", result.Name);
Assert.Equal("Archived", result.ToString());
Assert.Equal("Archived", status.Name);
Assert.Equal("Archived", status.ToString());
}
[Fact]
public void Should_provide_published_status()
{
var result = Status.Published;
var status = Status.Published;
Assert.Equal("Published", result.Name);
Assert.Equal("Published", result.ToString());
Assert.Equal("Published", status.Name);
Assert.Equal("Published", status.ToString());
}
[Fact]

2
tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs

@ -62,7 +62,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
.Returns(assetGrain);
A.CallTo(() => assetGrain.GetStateAsync(12))
.Returns(A.Fake<IAssetEntity>().AsJ());
.Returns(J.Of<IAssetEntity>(new AssetEntity()));
var result = await sut.CreateEnrichedEventAsync(envelope);

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

@ -11,6 +11,7 @@ using System.IO;
using System.Threading;
using System.Threading.Tasks;
using FakeItEasy;
using FluentAssertions;
using Orleans;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities.Assets.Commands;
@ -20,6 +21,7 @@ using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Reflection;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Assets
@ -27,6 +29,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
public class AssetCommandMiddlewareTests : HandlerTestBase<AssetState>
{
private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
private readonly IAssetEnricher assetEnricher = A.Fake<IAssetEnricher>();
private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake<IAssetThumbnailGenerator>();
private readonly IAssetStore assetStore = A.Fake<MemoryAssetStore>();
private readonly ITagService tagService = A.Fake<ITagService>();
@ -39,6 +42,10 @@ namespace Squidex.Domain.Apps.Entities.Assets
private readonly AssetFile file;
private readonly AssetCommandMiddleware sut;
public sealed class MyCommand : SquidexCommand
{
}
protected override Guid Id
{
get { return assetId; }
@ -51,52 +58,95 @@ namespace Squidex.Domain.Apps.Entities.Assets
asset = new AssetGrain(Store, tagService, A.Dummy<ISemanticLog>());
asset.ActivateAsync(Id).Wait();
A.CallTo(() => assetQuery.QueryByHashAsync(AppId, A<string>.Ignored))
.Returns(new List<IAssetEntity>());
A.CallTo(() => assetEnricher.EnrichAsync(A<IAssetEntity>.Ignored))
.ReturnsLazily(() => SimpleMapper.Map(asset.Snapshot, new AssetEntity()));
A.CallTo(() => tagService.DenormalizeTagsAsync(AppId, TagGroups.Assets, A<HashSet<string>>.Ignored))
.Returns(new Dictionary<string, string>
{
["1"] = "foundTag1",
["2"] = "foundTag2"
});
A.CallTo(() => assetQuery.QueryByHashAsync(AppId, A<string>.Ignored))
.Returns(new List<IEnrichedAssetEntity>());
A.CallTo(() => grainFactory.GetGrain<IAssetGrain>(Id, null))
.Returns(asset);
sut = new AssetCommandMiddleware(grainFactory, assetQuery, assetStore, assetThumbnailGenerator, new[] { tagGenerator }, tagService);
A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream))
.Returns(image);
sut = new AssetCommandMiddleware(grainFactory,
assetEnricher,
assetQuery,
assetStore,
assetThumbnailGenerator, new[] { tagGenerator });
}
[Fact]
public async Task Create_should_create_domain_object()
public async Task Should_not_invoke_enricher_for_other_result()
{
var command = CreateCommand(new CreateAsset { AssetId = assetId, File = file });
var command = CreateCommand(new MyCommand());
var context = CreateContextForCommand(command);
SetupTags(command);
SetupImageInfo();
context.Complete(12);
await sut.HandleAsync(context);
var result = context.Result<AssetCreatedResult>();
A.CallTo(() => assetEnricher.EnrichAsync(A<IEnrichedAssetEntity>.Ignored))
.MustNotHaveHappened();
}
Assert.Equal(assetId, result.Asset.Id);
Assert.Contains("tag1", command.Tags);
Assert.Contains("tag2", command.Tags);
[Fact]
public async Task Should_not_invoke_enricher_if_already_enriched()
{
var result = new AssetEntity();
Assert.Equal(new HashSet<string> { "tag1", "tag2" }, result.Tags);
var command = CreateCommand(new MyCommand());
var context = CreateContextForCommand(command);
AssertAssetHasBeenUploaded(0, context.ContextId);
AssertAssetImageChecked();
context.Complete(result);
await sut.HandleAsync(context);
Assert.Same(result, context.Result<IEnrichedAssetEntity>());
A.CallTo(() => assetEnricher.EnrichAsync(A<IEnrichedAssetEntity>.Ignored))
.MustNotHaveHappened();
}
[Fact]
public async Task Create_should_calculate_hash()
public async Task Should_enrich_asset_result()
{
var result = A.Fake<IAssetEntity>();
var command = CreateCommand(new MyCommand());
var context = CreateContextForCommand(command);
context.Complete(result);
var enriched = new AssetEntity();
A.CallTo(() => assetEnricher.EnrichAsync(result))
.Returns(enriched);
await sut.HandleAsync(context);
Assert.Same(enriched, context.Result<IEnrichedAssetEntity>());
}
[Fact]
public async Task Create_should_create_domain_object()
{
var command = CreateCommand(new CreateAsset { AssetId = assetId, File = file });
var context = CreateContextForCommand(command);
SetupImageInfo();
await sut.HandleAsync(context);
var result = context.Result<AssetCreatedResult>();
result.Asset.Should().BeEquivalentTo(asset.Snapshot, x => x.ExcludingMissingMembers());
}
[Fact]
public async Task Create_should_calculate_hash()
{
var command = CreateCommand(new CreateAsset { AssetId = assetId, File = file });
var context = CreateContextForCommand(command);
await sut.HandleAsync(context);
@ -110,7 +160,6 @@ namespace Squidex.Domain.Apps.Entities.Assets
var context = CreateContextForCommand(command);
SetupSameHashAsset(file.FileName, file.FileSize, out _);
SetupImageInfo();
await sut.HandleAsync(context);
@ -126,7 +175,6 @@ namespace Squidex.Domain.Apps.Entities.Assets
var context = CreateContextForCommand(command);
SetupSameHashAsset("other-name", file.FileSize, out _);
SetupImageInfo();
await sut.HandleAsync(context);
@ -136,19 +184,20 @@ namespace Squidex.Domain.Apps.Entities.Assets
}
[Fact]
public async Task Create_should_resolve_tag_names_for_duplicate()
public async Task Create_should_pass_through_duplicate()
{
var command = CreateCommand(new CreateAsset { AssetId = assetId, File = file });
var context = CreateContextForCommand(command);
SetupSameHashAsset(file.FileName, file.FileSize, out _);
SetupImageInfo();
SetupSameHashAsset(file.FileName, file.FileSize, out var duplicate);
await sut.HandleAsync(context);
var result = context.Result<AssetCreatedResult>();
Assert.Equal(new HashSet<string> { "foundTag1", "foundTag2" }, result.Tags);
Assert.True(result.IsDuplicate);
result.Should().BeEquivalentTo(duplicate, x => x.ExcludingMissingMembers());
}
[Fact]
@ -158,7 +207,6 @@ namespace Squidex.Domain.Apps.Entities.Assets
var context = CreateContextForCommand(command);
SetupSameHashAsset(file.FileName, 12345, out _);
SetupImageInfo();
await sut.HandleAsync(context);
@ -171,8 +219,6 @@ namespace Squidex.Domain.Apps.Entities.Assets
var command = CreateCommand(new UpdateAsset { AssetId = assetId, File = file });
var context = CreateContextForCommand(command);
SetupImageInfo();
await ExecuteCreateAsync();
await sut.HandleAsync(context);
@ -187,8 +233,6 @@ namespace Squidex.Domain.Apps.Entities.Assets
var command = CreateCommand(new UpdateAsset { AssetId = assetId, File = file });
var context = CreateContextForCommand(command);
SetupImageInfo();
await ExecuteCreateAsync();
await sut.HandleAsync(context);
@ -197,37 +241,33 @@ namespace Squidex.Domain.Apps.Entities.Assets
}
[Fact]
public async Task Update_should_resolve_tags()
public async Task Update_should_enrich_asset()
{
var command = CreateCommand(new UpdateAsset { AssetId = assetId, File = file });
var context = CreateContextForCommand(command);
SetupImageInfo();
await ExecuteCreateAsync();
await sut.HandleAsync(context);
var result = context.Result<AssetResult>();
var result = context.Result<IEnrichedAssetEntity>();
Assert.Equal(new HashSet<string> { "foundTag1", "foundTag2" }, result.Tags);
result.Should().BeEquivalentTo(asset.Snapshot, x => x.ExcludingMissingMembers());
}
[Fact]
public async Task AnnotateAsset_should_resolve_tags()
public async Task AnnotateAsset_should_enrich_asset()
{
var command = CreateCommand(new AnnotateAsset { AssetId = assetId, FileName = "newName" });
var context = CreateContextForCommand(command);
SetupImageInfo();
await ExecuteCreateAsync();
await sut.HandleAsync(context);
var result = context.Result<AssetResult>();
var result = context.Result<IEnrichedAssetEntity>();
Assert.Equal(new HashSet<string> { "foundTag1", "foundTag2" }, result.Tags);
result.Should().BeEquivalentTo(asset.Snapshot, x => x.ExcludingMissingMembers());
}
private Task ExecuteCreateAsync()
@ -235,16 +275,6 @@ namespace Squidex.Domain.Apps.Entities.Assets
return asset.ExecuteAsync(CreateCommand(new CreateAsset { AssetId = Id, File = file }));
}
private void SetupTags(CreateAsset command)
{
A.CallTo(() => tagGenerator.GenerateTags(command, A<HashSet<string>>.Ignored))
.Invokes(new Action<CreateAsset, HashSet<string>>((c, tags) =>
{
tags.Add("tag1");
tags.Add("tag2");
}));
}
private void AssertAssetHasBeenUploaded(long version, Guid commitId)
{
var fileName = AssetStoreExtensions.GetFileName(assetId.ToString(), version);
@ -257,21 +287,16 @@ namespace Squidex.Domain.Apps.Entities.Assets
.MustHaveHappened();
}
private void SetupSameHashAsset(string fileName, long fileSize, out IAssetEntity existing)
private void SetupSameHashAsset(string fileName, long fileSize, out IEnrichedAssetEntity duplicate)
{
var temp = existing = A.Fake<IAssetEntity>();
A.CallTo(() => temp.FileName).Returns(fileName);
A.CallTo(() => temp.FileSize).Returns(fileSize);
duplicate = new AssetEntity
{
FileName = fileName,
FileSize = fileSize
};
A.CallTo(() => assetQuery.QueryByHashAsync(A<Guid>.Ignored, A<string>.Ignored))
.Returns(new List<IAssetEntity> { existing });
}
private void SetupImageInfo()
{
A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream))
.Returns(image);
.Returns(new List<IEnrichedAssetEntity> { duplicate });
}
private void AssertAssetImageChecked()

101
tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetEnricherTests.cs

@ -0,0 +1,101 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Infrastructure;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Assets
{
public class AssetEnricherTests
{
private readonly ITagService tagService = A.Fake<ITagService>();
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app");
private readonly AssetEnricher sut;
public AssetEnricherTests()
{
sut = new AssetEnricher(tagService);
}
[Fact]
public async Task Should_not_enrich_if_asset_contains_null_tags()
{
var source = new AssetEntity { AppId = appId };
var result = await sut.EnrichAsync(source);
Assert.Empty(result.TagNames);
}
[Fact]
public async Task Should_enrich_asset_with_tag_names()
{
var source = new AssetEntity
{
Tags = new HashSet<string>
{
"id1",
"id2"
},
AppId = appId
};
A.CallTo(() => tagService.DenormalizeTagsAsync(appId.Id, TagGroups.Assets, A<HashSet<string>>.That.IsSameSequenceAs("id1", "id2")))
.Returns(new Dictionary<string, string>
{
["id1"] = "name1",
["id2"] = "name2"
});
var result = await sut.EnrichAsync(source);
Assert.Equal(new HashSet<string> { "name1", "name2" }, result.TagNames);
}
[Fact]
public async Task Should_enrich_multiple_assets_with_tag_names()
{
var source1 = new AssetEntity
{
Tags = new HashSet<string>
{
"id1",
"id2"
},
AppId = appId
};
var source2 = new AssetEntity
{
Tags = new HashSet<string>
{
"id2",
"id3"
},
AppId = appId
};
A.CallTo(() => tagService.DenormalizeTagsAsync(appId.Id, TagGroups.Assets, A<HashSet<string>>.That.IsSameSequenceAs("id1", "id2", "id3")))
.Returns(new Dictionary<string, string>
{
["id1"] = "name1",
["id2"] = "name2",
["id3"] = "name3"
});
var result = await sut.EnrichAsync(new[] { source1, source2 });
Assert.Equal(new HashSet<string> { "name1", "name2" }, result[0].TagNames);
Assert.Equal(new HashSet<string> { "name2", "name3" }, result[1].TagNames);
}
}
}

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

@ -7,6 +7,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using FakeItEasy;
@ -25,16 +26,16 @@ namespace Squidex.Domain.Apps.Entities.Assets
public class AssetQueryServiceTests
{
private readonly ITagService tagService = A.Fake<ITagService>();
private readonly IAssetEnricher assetEnricher = A.Fake<IAssetEnricher>();
private readonly IAssetRepository assetRepository = A.Fake<IAssetRepository>();
private readonly IAppEntity app = A.Fake<IAppEntity>();
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app");
private readonly ClaimsIdentity identity = new ClaimsIdentity();
private readonly QueryContext context;
private readonly AssetQueryService sut;
public AssetQueryServiceTests()
{
var user = new ClaimsPrincipal(identity);
var user = new ClaimsPrincipal(new ClaimsIdentity());
A.CallTo(() => app.Id).Returns(appId.Id);
A.CallTo(() => app.Name).Returns(appId.Name);
@ -42,17 +43,9 @@ namespace Squidex.Domain.Apps.Entities.Assets
context = QueryContext.Create(app, user);
A.CallTo(() => tagService.DenormalizeTagsAsync(appId.Id, TagGroups.Assets, A<HashSet<string>>.That.IsSameSequenceAs("id1", "id2", "id3")))
.Returns(new Dictionary<string, string>
{
["id1"] = "name1",
["id2"] = "name2",
["id3"] = "name3"
});
var options = Options.Create(new AssetOptions { DefaultPageSize = 30 });
sut = new AssetQueryService(tagService, assetRepository, options);
sut = new AssetQueryService(tagService, assetEnricher, assetRepository, options);
}
[Fact]
@ -64,68 +57,85 @@ namespace Squidex.Domain.Apps.Entities.Assets
}
[Fact]
public async Task Should_find_asset_by_id_and_resolve_tags()
public async Task Should_find_asset_by_id_and_enrich_it()
{
var id = Guid.NewGuid();
var found = new AssetEntity { Id = Guid.NewGuid() };
var enriched = new AssetEntity();
A.CallTo(() => assetRepository.FindAssetAsync(id, false))
.Returns(CreateAsset(id, "id1", "id2", "id3"));
A.CallTo(() => assetRepository.FindAssetAsync(found.Id, false))
.Returns(found);
var result = await sut.FindAssetAsync(context, id);
A.CallTo(() => assetEnricher.EnrichAsync(found))
.Returns(enriched);
Assert.Equal(HashSet.Of("name1", "name2", "name3"), result.Tags);
var result = await sut.FindAssetAsync(found.Id);
Assert.Same(enriched, result);
}
[Fact]
public async Task Should_find_asset_by_hash_and_resolve_tags()
public async Task Should_find_assets_by_hash_and_and_enrich_it()
{
var id = Guid.NewGuid();
var found = new AssetEntity { Id = Guid.NewGuid() };
var enriched = new AssetEntity();
A.CallTo(() => assetRepository.QueryByHashAsync(appId.Id, "hash"))
.Returns(new List<IAssetEntity> { CreateAsset(id, "id1", "id2", "id3") });
.Returns(new List<IAssetEntity> { found });
A.CallTo(() => assetEnricher.EnrichAsync(A<IEnumerable<IAssetEntity>>.That.IsSameSequenceAs(found)))
.Returns(new List<IEnrichedAssetEntity> { enriched });
var result = await sut.QueryByHashAsync(appId.Id, "hash");
Assert.Equal(HashSet.Of("name1", "name2", "name3"), result[0].Tags);
Assert.Same(enriched, result.Single());
}
[Fact]
public async Task Should_load_assets_from_ids_and_resolve_tags()
{
var id1 = Guid.NewGuid();
var id2 = Guid.NewGuid();
var found1 = new AssetEntity { Id = Guid.NewGuid() };
var found2 = new AssetEntity { Id = Guid.NewGuid() };
var enriched1 = new AssetEntity();
var enriched2 = new AssetEntity();
var ids = HashSet.Of(id1, id2);
var ids = HashSet.Of(found1.Id, found2.Id);
A.CallTo(() => assetRepository.QueryAsync(appId.Id, A<HashSet<Guid>>.That.IsSameSequenceAs(ids)))
.Returns(ResultList.Create(8,
CreateAsset(id1, "id1", "id2", "id3"),
CreateAsset(id2)));
.Returns(ResultList.CreateFrom(8, found1, found2));
A.CallTo(() => assetEnricher.EnrichAsync(A<IEnumerable<IAssetEntity>>.That.IsSameSequenceAs(found1, found2)))
.Returns(new List<IEnrichedAssetEntity> { enriched1, enriched2 });
var result = await sut.QueryAsync(context, Q.Empty.WithIds(ids));
Assert.Equal(8, result.Total);
Assert.Equal(2, result.Count);
Assert.Equal(HashSet.Of("name1", "name2", "name3"), result[0].Tags);
Assert.Empty(result[1].Tags);
Assert.Equal(new[] { enriched1, enriched2 }, result.ToArray());
}
[Fact]
public async Task Should_load_assets_with_query_and_resolve_tags()
{
var found1 = new AssetEntity { Id = Guid.NewGuid() };
var found2 = new AssetEntity { Id = Guid.NewGuid() };
var enriched1 = new AssetEntity();
var enriched2 = new AssetEntity();
A.CallTo(() => assetRepository.QueryAsync(appId.Id, A<Query>.Ignored))
.Returns(ResultList.Create(8,
CreateAsset(Guid.NewGuid(), "id1", "id2"),
CreateAsset(Guid.NewGuid(), "id2", "id3")));
.Returns(ResultList.CreateFrom(8, found1, found2));
A.CallTo(() => assetEnricher.EnrichAsync(A<IEnumerable<IAssetEntity>>.That.IsSameSequenceAs(found1, found2)))
.Returns(new List<IEnrichedAssetEntity> { enriched1, enriched2 });
var result = await sut.QueryAsync(context, Q.Empty);
Assert.Equal(8, result.Total);
Assert.Equal(2, result.Count);
Assert.Equal(HashSet.Of("name1", "name2"), result[0].Tags);
Assert.Equal(HashSet.Of("name2", "name3"), result[1].Tags);
Assert.Equal(new[] { enriched1, enriched2 }, result.ToArray());
}
[Fact]
@ -171,15 +181,5 @@ namespace Squidex.Domain.Apps.Entities.Assets
A.CallTo(() => assetRepository.QueryAsync(appId.Id, A<Query>.That.Is("Skip: 20; Take: 200; Sort: lastModified Descending")))
.MustHaveHappened();
}
private static IAssetEntity CreateAsset(Guid id, params string[] tags)
{
var asset = A.Fake<IAssetEntity>();
A.CallTo(() => asset.Id).Returns(id);
A.CallTo(() => asset.Tags).Returns(HashSet.Of(tags));
return asset;
}
}
}

2
tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs

@ -71,7 +71,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
.Returns(contentGrain);
A.CallTo(() => contentGrain.GetStateAsync(12))
.Returns(A.Fake<IContentEntity>().AsJ());
.Returns(J.Of<IContentEntity>(new ContentEntity { SchemaId = SchemaMatch }));
var result = await sut.CreateEnrichedEventAsync(envelope);

91
tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs

@ -0,0 +1,91 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using FakeItEasy;
using Orleans;
using Squidex.Domain.Apps.Entities.Contents.State;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure.Commands;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Contents
{
public sealed class ContentCommandMiddlewareTests : HandlerTestBase<ContentState>
{
private readonly IContentEnricher contentEnricher = A.Fake<IContentEnricher>();
private readonly Guid contentId = Guid.NewGuid();
private readonly ContentCommandMiddleware sut;
public sealed class MyCommand : SquidexCommand
{
}
protected override Guid Id
{
get { return contentId; }
}
public ContentCommandMiddlewareTests()
{
sut = new ContentCommandMiddleware(A.Fake<IGrainFactory>(), contentEnricher);
}
[Fact]
public async Task Should_not_invoke_enricher_for_other_result()
{
var command = CreateCommand(new MyCommand());
var context = CreateContextForCommand(command);
context.Complete(12);
await sut.HandleAsync(context);
A.CallTo(() => contentEnricher.EnrichAsync(A<IEnrichedContentEntity>.Ignored))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_not_invoke_enricher_if_already_enriched()
{
var result = new ContentEntity();
var command = CreateCommand(new MyCommand());
var context = CreateContextForCommand(command);
context.Complete(result);
await sut.HandleAsync(context);
Assert.Same(result, context.Result<IEnrichedContentEntity>());
A.CallTo(() => contentEnricher.EnrichAsync(A<IEnrichedContentEntity>.Ignored))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_enrich_content_result()
{
var result = A.Fake<IContentEntity>();
var command = CreateCommand(new MyCommand());
var context = CreateContextForCommand(command);
context.Complete(result);
var enriched = new ContentEntity();
A.CallTo(() => contentEnricher.EnrichAsync(result))
.Returns(enriched);
await sut.HandleAsync(context);
Assert.Same(enriched, context.Result<IEnrichedContentEntity>());
}
}
}

85
tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentEnricherTests.cs

@ -0,0 +1,85 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Contents
{
public class ContentEnricherTests
{
private readonly IContentWorkflow workflow = A.Fake<IContentWorkflow>();
private readonly NamedId<Guid> schemaId = NamedId.Of(Guid.NewGuid(), "my-schema");
private readonly ContentEnricher sut;
public ContentEnricherTests()
{
sut = new ContentEnricher(workflow);
}
[Fact]
public async Task Should_enrich_content_with_status_color()
{
var source = new ContentEntity { Status = Status.Published, SchemaId = schemaId };
A.CallTo(() => workflow.GetInfoAsync(Status.Published))
.Returns(new StatusInfo(Status.Published, StatusColors.Published));
var result = await sut.EnrichAsync(source);
Assert.Equal(StatusColors.Published, result.StatusColor);
}
[Fact]
public async Task Should_enrich_content_with_default_color_if_not_found()
{
var source = new ContentEntity { Status = Status.Published, SchemaId = schemaId };
A.CallTo(() => workflow.GetInfoAsync(Status.Published))
.Returns(Task.FromResult<StatusInfo>(null));
var result = await sut.EnrichAsync(source);
Assert.Equal(StatusColors.Draft, result.StatusColor);
}
[Fact]
public async Task Should_enrich_content_with_can_update()
{
var source = new ContentEntity { SchemaId = schemaId };
A.CallTo(() => workflow.CanUpdateAsync(source))
.Returns(true);
var result = await sut.EnrichAsync(source);
Assert.True(result.CanUpdate);
}
[Fact]
public async Task Should_enrich_multiple_contents_and_cache_color()
{
var source1 = new ContentEntity { Status = Status.Published, SchemaId = schemaId };
var source2 = new ContentEntity { Status = Status.Published, SchemaId = schemaId };
A.CallTo(() => workflow.GetInfoAsync(Status.Published))
.Returns(new StatusInfo(Status.Published, StatusColors.Published));
var result = await sut.EnrichAsync(new[] { source1, source2 });
Assert.Equal(StatusColors.Published, result[0].StatusColor);
Assert.Equal(StatusColors.Published, result[1].StatusColor);
A.CallTo(() => workflow.GetInfoAsync(Status.Published))
.MustHaveHappenedOnceExactly();
}
}
}

60
tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs

@ -34,7 +34,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
private readonly ISchemaEntity schema = A.Fake<ISchemaEntity>();
private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>();
private readonly IContentRepository contentRepository = A.Dummy<IContentRepository>();
private readonly IContentWorkflow contentWorkflow = A.Fake<IContentWorkflow>();
private readonly IContentWorkflow contentWorkflow = A.Fake<IContentWorkflow>(x => x.Wrapping(new DefaultContentWorkflow()));
private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly IAppEntity app = A.Fake<IAppEntity>();
private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.DE);
@ -102,12 +102,6 @@ namespace Squidex.Domain.Apps.Entities.Contents
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, A<string>.Ignored))
.ReturnsLazily(x => x.GetArgument<ScriptContext>(0).Data);
A.CallTo(() => contentWorkflow.CanUpdateAsync(A<IContentEntity>.Ignored))
.Returns(true);
A.CallTo(() => contentWorkflow.CanMoveToAsync(A<IContentEntity>.Ignored, A<Status>.Ignored))
.Returns(true);
patched = patch.MergeInto(data);
sut = new ContentGrain(Store, A.Dummy<ISemanticLog>(), appProvider, A.Dummy<IAssetRepository>(), scriptEngine, contentWorkflow, contentRepository);
@ -132,9 +126,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(Status.Draft, sut.Snapshot.Status);
LastEvents
.ShouldHaveSameEvents(
CreateContentEvent(new ContentCreated { Data = data })
CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft })
);
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, "<create-script>"))
@ -143,26 +139,6 @@ namespace Squidex.Domain.Apps.Entities.Contents
.MustNotHaveHappened();
}
[Fact]
public async Task Create_should_create_events_and_update_state_with_custom_initial_status()
{
var command = new CreateContent { Data = data };
A.CallTo(() => contentWorkflow.GetInitialStatusAsync(schema))
.Returns(Status.Archived);
var result = await sut.ExecuteAsync(CreateContentCommand(command));
result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(Status.Archived, sut.Snapshot.Status);
LastEvents
.ShouldHaveSameEvents(
CreateContentEvent(new ContentCreated { Data = data, Status = Status.Archived })
);
}
[Fact]
public async Task Create_should_also_publish()
{
@ -172,9 +148,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(Status.Published, sut.Snapshot.Status);
LastEvents
.ShouldHaveSameEvents(
CreateContentEvent(new ContentCreated { Data = data }),
CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft }),
CreateContentEvent(new ContentStatusChanged { Status = Status.Published })
);
@ -246,10 +224,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
result.ShouldBeEquivalent(sut.Snapshot);
LastEvents
.ShouldHaveSameEvents(
CreateContentEvent(new ContentCreated { Data = data })
);
Assert.Single(LastEvents);
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, "<update-script>"))
.MustNotHaveHappened();
@ -319,10 +294,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
result.ShouldBeEquivalent(sut.Snapshot);
LastEvents
.ShouldHaveSameEvents(
CreateContentEvent(new ContentCreated { Data = data })
);
Assert.Single(LastEvents);
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, "<update-script>"))
.MustNotHaveHappened();
@ -343,7 +315,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
LastEvents
.ShouldHaveSameEvents(
CreateContentEvent(new ContentStatusChanged { Status = Status.Published, Change = StatusChange.Published })
CreateContentEvent(new ContentStatusChanged { Change = StatusChange.Published, Status = Status.Published })
);
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>.Ignored, "<change-script>"))
@ -388,7 +360,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
LastEvents
.ShouldHaveSameEvents(
CreateContentEvent(new ContentStatusChanged { Status = Status.Draft, Change = StatusChange.Unpublished })
CreateContentEvent(new ContentStatusChanged { Change = StatusChange.Unpublished, Status = Status.Draft })
);
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>.Ignored, "<change-script>"))
@ -472,11 +444,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
public async Task ChangeStatus_should_refresh_properties_and_revert_scheduling_when_invoked_by_scheduler()
{
await ExecuteCreateAsync();
await ExecuteScheduledAsync();
await ExecuteChangeStatusAsync(Status.Published, Instant.MaxValue);
var command = new ChangeContentStatus { Status = Status.Draft, JobId = sut.Snapshot.ScheduleJob.Id };
var command = new ChangeContentStatus { Status = Status.Published, JobId = sut.Snapshot.ScheduleJob.Id };
A.CallTo(() => contentWorkflow.CanMoveToAsync(sut.Snapshot, command.Status))
A.CallTo(() => contentWorkflow.CanMoveToAsync(A<IContentEntity>.Ignored, Status.Published))
.Returns(false);
var result = await sut.ExecuteAsync(CreateContentCommand(command));
@ -549,9 +521,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
return sut.ExecuteAsync(CreateContentCommand(new UpdateContent { Data = otherData, AsDraft = true }));
}
private Task ExecuteScheduledAsync()
private Task ExecuteChangeStatusAsync(Status status, Instant? dueTime = null)
{
return sut.ExecuteAsync(CreateContentCommand(new ChangeContentStatus { Status = Status.Published, DueTime = Instant.MaxValue }));
return sut.ExecuteAsync(CreateContentCommand(new ChangeContentStatus { Status = status, DueTime = dueTime }));
}
private Task ExecuteDeleteAsync()

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

@ -25,6 +25,7 @@ using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.Security;
using Squidex.Shared;
using Squidex.Shared.Identity;
@ -36,6 +37,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
public class ContentQueryServiceTests
{
private readonly IContentEnricher contentEnricher = A.Fake<IContentEnricher>();
private readonly IContentRepository contentRepository = A.Fake<IContentRepository>();
private readonly IContentVersionLoader contentVersionLoader = A.Fake<IContentVersionLoader>();
private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>();
@ -76,6 +78,8 @@ namespace Squidex.Domain.Apps.Entities.Contents
A.CallTo(() => schema.AppId).Returns(appId);
A.CallTo(() => schema.SchemaDef).Returns(schemaDef);
SetupEnricher();
context = QueryContext.Create(app, user);
var options = Options.Create(new ContentOptions { DefaultPageSize = 30 });
@ -83,6 +87,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
sut = new ContentQueryService(
appProvider,
urlGenerator,
contentEnricher,
contentRepository,
contentVersionLoader,
scriptEngine,
@ -520,6 +525,17 @@ namespace Squidex.Domain.Apps.Entities.Contents
.Returns((ISchemaEntity)null);
}
private void SetupEnricher()
{
A.CallTo(() => contentEnricher.EnrichAsync(A<IEnumerable<IContentEntity>>.Ignored))
.ReturnsLazily(x =>
{
var input = (IEnumerable<IContentEntity>)x.Arguments[0];
return Task.FromResult<IReadOnlyList<IEnrichedContentEntity>>(input.Select(c => SimpleMapper.Map(c, new ContentEntity())).ToList());
});
}
private IContentEntity CreateContent(Guid id)
{
return CreateContent(id, Status.Published);
@ -527,13 +543,14 @@ namespace Squidex.Domain.Apps.Entities.Contents
private IContentEntity CreateContent(Guid id, Status status)
{
var content = A.Fake<IContentEntity>();
A.CallTo(() => content.Id).Returns(id);
A.CallTo(() => content.Data).Returns(contentData);
A.CallTo(() => content.DataDraft).Returns(contentData);
A.CallTo(() => content.Status).Returns(status);
A.CallTo(() => content.SchemaId).Returns(schemaId);
var content = new ContentEntity
{
Id = id,
Data = contentData,
DataDraft = contentData,
SchemaId = schemaId,
Status = status,
};
return content;
}

18
tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentVersionLoaderTests.cs

@ -34,7 +34,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
public async Task Should_throw_exception_if_no_state_returned()
{
A.CallTo(() => grain.GetStateAsync(10))
.Returns(new J<IContentEntity>(null));
.Returns(J.Of<IContentEntity>(null));
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.LoadAsync(id, 10));
}
@ -42,13 +42,10 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Fact]
public async Task Should_throw_exception_if_state_has_other_version()
{
var entity = A.Fake<IContentEntity>();
A.CallTo(() => entity.Version)
.Returns(5);
var content = new ContentEntity { Version = 5 };
A.CallTo(() => grain.GetStateAsync(10))
.Returns(J.Of(entity));
.Returns(J.Of<IContentEntity>(content));
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.LoadAsync(id, 10));
}
@ -56,17 +53,14 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Fact]
public async Task Should_return_content_from_state()
{
var entity = A.Fake<IContentEntity>();
A.CallTo(() => entity.Version)
.Returns(10);
var content = new ContentEntity { Version = 10 };
A.CallTo(() => grain.GetStateAsync(10))
.Returns(J.Of(entity));
.Returns(J.Of<IContentEntity>(content));
var result = await sut.LoadAsync(id, 10);
Assert.Same(entity, result);
Assert.Same(content, result);
}
}
}

69
tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs

@ -6,7 +6,7 @@
// ==========================================================================
using System.Threading.Tasks;
using FakeItEasy;
using FluentAssertions;
using Squidex.Domain.Apps.Core.Contents;
using Xunit;
@ -19,17 +19,19 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Fact]
public async Task Should_draft_as_initial_status()
{
var expected = new StatusInfo(Status.Draft, StatusColors.Draft);
var result = await sut.GetInitialStatusAsync(null);
Assert.Equal(Status.Draft, result);
result.Should().BeEquivalentTo(expected);
}
[Fact]
public async Task Should_check_is_valid_next()
{
var entity = CreateContent(Status.Published);
var content = new ContentEntity { Status = Status.Published };
var result = await sut.CanMoveToAsync(entity, Status.Draft);
var result = await sut.CanMoveToAsync(content, Status.Draft);
Assert.True(result);
}
@ -37,9 +39,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Fact]
public async Task Should_be_able_to_update_published()
{
var entity = CreateContent(Status.Published);
var content = new ContentEntity { Status = Status.Published };
var result = await sut.CanUpdateAsync(entity);
var result = await sut.CanUpdateAsync(content);
Assert.True(result);
}
@ -47,9 +49,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Fact]
public async Task Should_be_able_to_update_draft()
{
var entity = CreateContent(Status.Published);
var content = new ContentEntity { Status = Status.Published };
var result = await sut.CanUpdateAsync(entity);
var result = await sut.CanUpdateAsync(content);
Assert.True(result);
}
@ -57,9 +59,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Fact]
public async Task Should_not_be_able_to_update_archived()
{
var entity = CreateContent(Status.Archived);
var content = new ContentEntity { Status = Status.Archived };
var result = await sut.CanUpdateAsync(entity);
var result = await sut.CanUpdateAsync(content);
Assert.False(result);
}
@ -67,56 +69,63 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Fact]
public async Task Should_get_next_statuses_for_draft()
{
var content = CreateContent(Status.Draft);
var content = new ContentEntity { Status = Status.Draft };
var expected = new[] { Status.Archived, Status.Published };
var expected = new[]
{
new StatusInfo(Status.Archived, StatusColors.Archived),
new StatusInfo(Status.Published, StatusColors.Published)
};
var result = await sut.GetNextsAsync(content);
Assert.Equal(expected, result);
result.Should().BeEquivalentTo(expected);
}
[Fact]
public async Task Should_get_next_statuses_for_archived()
{
var content = CreateContent(Status.Archived);
var content = new ContentEntity { Status = Status.Archived };
var expected = new[] { Status.Draft };
var expected = new[]
{
new StatusInfo(Status.Draft, StatusColors.Draft)
};
var result = await sut.GetNextsAsync(content);
Assert.Equal(expected, result);
result.Should().BeEquivalentTo(expected);
}
[Fact]
public async Task Should_get_next_statuses_for_published()
{
var content = CreateContent(Status.Published);
var content = new ContentEntity { Status = Status.Published };
var expected = new[] { Status.Draft, Status.Archived };
var expected = new[]
{
new StatusInfo(Status.Archived, StatusColors.Archived),
new StatusInfo(Status.Draft, StatusColors.Draft)
};
var result = await sut.GetNextsAsync(content);
Assert.Equal(expected, result);
result.Should().BeEquivalentTo(expected);
}
[Fact]
public async Task Should_return_all_statuses()
{
var expected = new[] { Status.Archived, Status.Draft, Status.Published };
var expected = new[]
{
new StatusInfo(Status.Archived, StatusColors.Archived),
new StatusInfo(Status.Draft, StatusColors.Draft),
new StatusInfo(Status.Published, StatusColors.Published)
};
var result = await sut.GetAllAsync(null);
Assert.Equal(expected, result);
}
private IContentEntity CreateContent(Status status)
{
var content = A.Fake<IContentEntity>();
A.CallTo(() => content.Status).Returns(status);
return content;
result.Should().BeEquivalentTo(expected);
}
}
}

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

@ -65,7 +65,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var asset = CreateAsset(Guid.NewGuid());
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A<Q>.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5&$filter=my-query")))
.Returns(ResultList.Create(0, asset));
.Returns(ResultList.CreateFrom(0, asset));
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });
@ -138,7 +138,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var asset = CreateAsset(Guid.NewGuid());
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A<Q>.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5&$filter=my-query")))
.Returns(ResultList.Create(10, asset));
.Returns(ResultList.CreateFrom(10, asset));
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });
@ -213,7 +213,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}".Replace("<ID>", assetId.ToString());
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), MatchId(assetId)))
.Returns(ResultList.Create(1, asset));
.Returns(ResultList.CreateFrom(1, asset));
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });
@ -262,6 +262,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
lastModified
lastModifiedBy
status
statusColor
url
data {
myString {
@ -301,7 +302,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var content = CreateContent(Guid.NewGuid(), Guid.Empty, Guid.Empty);
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.ToString(), A<Q>.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5")))
.Returns(ResultList.Create(0, content));
.Returns(ResultList.CreateFrom(0, content));
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });
@ -320,6 +321,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
lastModified = content.LastModified,
lastModifiedBy = "subject:user2",
status = "DRAFT",
statusColor = "red",
url = $"contents/my-schema/{content.Id}",
data = new
{
@ -406,6 +408,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
lastModified
lastModifiedBy
status
statusColor
url
data {
myString {
@ -440,7 +443,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var content = CreateContent(Guid.NewGuid(), Guid.Empty, Guid.Empty);
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.ToString(), A<Q>.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5")))
.Returns(ResultList.Create(10, content));
.Returns(ResultList.CreateFrom(10, content));
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });
@ -462,6 +465,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
lastModified = content.LastModified,
lastModifiedBy = "subject:user2",
status = "DRAFT",
statusColor = "red",
url = $"contents/my-schema/{content.Id}",
data = new
{
@ -545,7 +549,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}".Replace("<ID>", contentId.ToString());
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.ToString(), MatchId(contentId)))
.Returns(ResultList.Create(1, content));
.Returns(ResultList.CreateFrom(1, content));
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });
@ -605,6 +609,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
lastModified
lastModifiedBy
status
statusColor
url
data {
myString {
@ -636,7 +641,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}".Replace("<ID>", contentId.ToString());
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.ToString(), MatchId(contentId)))
.Returns(ResultList.Create(1, content));
.Returns(ResultList.CreateFrom(1, content));
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });
@ -653,6 +658,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
lastModified = content.LastModified,
lastModifiedBy = "subject:user2",
status = "DRAFT",
statusColor = "red",
url = $"contents/my-schema/{content.Id}",
data = new
{
@ -731,10 +737,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}".Replace("<ID>", contentId.ToString());
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.ToString(), A<Q>.Ignored))
.Returns(ResultList.Create(0, contentRef));
.Returns(ResultList.CreateFrom(0, contentRef));
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.ToString(), MatchId(contentId)))
.Returns(ResultList.Create(1, content));
.Returns(ResultList.CreateFrom(1, content));
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });
@ -789,10 +795,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}".Replace("<ID>", contentId.ToString());
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.ToString(), MatchId(contentId)))
.Returns(ResultList.Create(1, content));
.Returns(ResultList.CreateFrom(1, content));
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A<Q>.Ignored))
.Returns(ResultList.Create(0, assetRef));
.Returns(ResultList.CreateFrom(0, assetRef));
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });
@ -845,10 +851,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}".Replace("<ID>", assetId2.ToString());
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), MatchId(assetId1)))
.Returns(ResultList.Create(0, asset1));
.Returns(ResultList.CreateFrom(0, asset1));
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), MatchId(assetId2)))
.Returns(ResultList.Create(0, asset2));
.Returns(ResultList.CreateFrom(0, asset2));
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query1 }, new GraphQLQuery { Query = query2 });
@ -904,7 +910,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}".Replace("<ID>", contentId.ToString());
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.ToString(), MatchId(contentId)))
.Returns(ResultList.Create(1, content));
.Returns(ResultList.CreateFrom(1, content));
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });
@ -942,7 +948,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}".Replace("<ID>", contentId.ToString());
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.ToString(), MatchId(contentId)))
.Returns(ResultList.Create(1, content));
.Returns(ResultList.CreateFrom(1, content));
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });
@ -988,7 +994,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}".Replace("<ID>", contentId.ToString());
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.ToString(), MatchId(contentId)))
.Returns(ResultList.Create(1, content));
.Returns(ResultList.CreateFrom(1, content));
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });

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

@ -96,7 +96,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
sut = CreateSut();
}
protected static IContentEntity CreateContent(Guid id, Guid refId, Guid assetId, NamedContentData data = null, NamedContentData dataDraft = null)
protected static IEnrichedContentEntity CreateContent(Guid id, Guid refId, Guid assetId, NamedContentData data = null, NamedContentData dataDraft = null)
{
var now = SystemClock.Instance.GetCurrentInstant();
@ -157,17 +157,18 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
LastModifiedBy = new RefToken(RefTokenType.Subject, "user2"),
Data = data,
DataDraft = dataDraft,
Status = Status.Draft
Status = Status.Draft,
StatusColor = "red"
};
return content;
}
protected static IAssetEntity CreateAsset(Guid id)
protected static IEnrichedAssetEntity CreateAsset(Guid id)
{
var now = SystemClock.Instance.GetCurrentInstant();
var asset = new FakeAssetEntity
var asset = new AssetEntity
{
Id = id,
Version = 1,
@ -184,7 +185,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
IsImage = true,
PixelWidth = 800,
PixelHeight = 600,
Tags = new[] { "tag1", "tag2" }.ToHashSet()
TagNames = new[] { "tag1", "tag2" }.ToHashSet()
};
return asset;

7
tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs

@ -270,12 +270,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard
private IContentEntity CreateContent(Status status, bool isPending)
{
var content = A.Fake<IContentEntity>();
A.CallTo(() => content.Status).Returns(status);
A.CallTo(() => content.IsPending).Returns(isPending);
return content;
return new ContentEntity { Status = status, IsPending = isPending };
}
}
}

7
tools/Migrate_01/OldEvents/AppPlanChangedOld.cs → tools/Migrate_01/OldEvents/AppPlanChanged.cs

@ -5,16 +5,19 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Apps;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection;
using AppPlanChangedV2 = Squidex.Domain.Apps.Events.Apps.AppPlanChanged;
namespace Migrate_01.OldEvents
{
[TypeName("AppPlanChanged")]
public sealed class AppPlanChangedOld : AppEvent, IMigrated<IEvent>
[Obsolete]
public sealed class AppPlanChanged : AppEvent, IMigrated<IEvent>
{
public string PlanId { get; set; }
@ -22,7 +25,7 @@ namespace Migrate_01.OldEvents
{
if (!string.IsNullOrWhiteSpace(PlanId))
{
return SimpleMapper.Map(this, new AppPlanChanged());
return SimpleMapper.Map(this, new AppPlanChangedV2());
}
else
{

38
tools/Migrate_01/OldEvents/ContentCreated.cs

@ -0,0 +1,38 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection;
using ContentCreatedV2 = Squidex.Domain.Apps.Events.Contents.ContentCreated;
namespace Migrate_01.OldEvents
{
[EventType(nameof(ContentCreated))]
[Obsolete]
public sealed class ContentCreated : ContentEvent, IMigrated<IEvent>
{
public Status Status { get; set; }
public NamedContentData Data { get; set; }
public IEvent Migrate()
{
var migrated = SimpleMapper.Map(this, new ContentCreatedV2());
if (migrated.Status == default)
{
migrated.Status = Status.Draft;
}
return migrated;
}
}
}

47
tools/Migrate_01/OldEvents/ContentStatusChanged.cs

@ -0,0 +1,47 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection;
using ContentStatusChangedV2 = Squidex.Domain.Apps.Events.Contents.ContentStatusChanged;
namespace Migrate_01.OldEvents
{
[EventType(nameof(ContentStatusChanged))]
[Obsolete]
public sealed class ContentStatusChanged : ContentEvent, IMigrated<IEvent>
{
public string Change { get; set; }
public Status Status { get; set; }
public IEvent Migrate()
{
var migrated = SimpleMapper.Map(this, new ContentStatusChangedV2());
if (migrated.Status == default)
{
migrated.Status = Status.Draft;
}
if (Enum.TryParse<StatusChange>(Change, out var result))
{
migrated.Change = result;
}
else
{
migrated.Change = StatusChange.Change;
}
return migrated;
}
}
}
Loading…
Cancel
Save