From 727be9fa9a996c0f4edf415bb639d7f8ef38aa31 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 30 Jan 2020 20:00:10 +0100 Subject: [PATCH] Splitted the enrichment in several steps. --- .../Contents/ContentCommandMiddleware.cs | 1 + .../Contents/Queries/ContentEnricher.cs | 317 +----------------- .../Contents/Queries/ContentQueryService.cs | 64 +--- .../{ => Queries}/IContentEnricher.cs | 2 +- .../Contents/Queries/IContentEnricherStep.cs | 21 ++ .../Contents/Queries/Steps/ConvertData.cs | 93 +++++ .../Queries/Steps/EnrichForCaching.cs | 36 ++ .../Queries/Steps/EnrichWithSchema.cs | 44 +++ .../Queries/Steps/EnrichWithWorkflows.cs | 82 +++++ .../Contents/Queries/Steps/ResolveAssets.cs | 129 +++++++ .../Queries/Steps/ResolveReferences.cs | 167 +++++++++ .../Squidex/Config/Domain/ContentsServices.cs | 19 ++ .../Contents/ContentCommandMiddlewareTests.cs | 1 + .../Contents/Queries/ContentEnricherTests.cs | 176 +++------- .../Queries/ContentQueryServiceTests.cs | 2 - .../Contents/Queries/EnrichForCachingTests.cs | 57 ++++ .../Contents/Queries/EnrichWithSchemaTests.cs | 79 +++++ .../Queries/EnrichWithWorkflowsTests.cs | 109 ++++++ ...erAssetsTests.cs => ResolveAssetsTests.cs} | 80 +++-- ...ncesTests.cs => ResolveReferencesTests.cs} | 103 +++--- .../TestSuite.ApiTests/ContentCleanupTests.cs | 83 +++++ .../Fixtures/ContentFixture.cs | 4 +- 22 files changed, 1070 insertions(+), 599 deletions(-) rename backend/src/Squidex.Domain.Apps.Entities/Contents/{ => Queries}/IContentEnricher.cs (92%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/IContentEnricherStep.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichForCaching.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichWithSchema.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichWithWorkflows.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveAssets.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveReferences.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichForCachingTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichWithSchemaTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichWithWorkflowsTests.cs rename backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/{ContentEnricherAssetsTests.cs => ResolveAssetsTests.cs} (77%) rename backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/{ContentEnricherReferencesTests.cs => ResolveReferencesTests.cs} (79%) create mode 100644 backend/tools/TestSuite/TestSuite.ApiTests/ContentCleanupTests.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs index 46cbd5aa4..1a9f2bca8 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Orleans; using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.Contents.Queries; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs index 450959583..836fea5af 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs @@ -9,15 +9,8 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Assets; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.ConvertContent; -using Squidex.Domain.Apps.Core.ExtractReferenceIds; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Reflection; @@ -25,30 +18,22 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { public sealed class ContentEnricher : IContentEnricher { - private const string DefaultColor = StatusColors.Draft; - private static readonly ILookup EmptyContents = Enumerable.Empty().ToLookup(x => x.Id); - private static readonly ILookup EmptyAssets = Enumerable.Empty().ToLookup(x => x.Id); - private readonly IAssetQueryService assetQuery; - private readonly IAssetUrlGenerator assetUrlGenerator; + private readonly IEnumerable steps; private readonly Lazy contentQuery; - private readonly IContentWorkflow contentWorkflow; private IContentQueryService ContentQuery { get { return contentQuery.Value; } } - public ContentEnricher(IAssetQueryService assetQuery, IAssetUrlGenerator assetUrlGenerator, Lazy contentQuery, IContentWorkflow contentWorkflow) + public ContentEnricher(IEnumerable steps, Lazy contentQuery) { - Guard.NotNull(assetQuery); - Guard.NotNull(assetUrlGenerator); + Guard.NotNull(steps); Guard.NotNull(contentQuery); - Guard.NotNull(contentWorkflow); - this.assetQuery = assetQuery; - this.assetUrlGenerator = assetUrlGenerator; + this.steps = steps; + this.contentQuery = contentQuery; - this.contentWorkflow = contentWorkflow; } public async Task EnrichAsync(IContentEntity content, Context context) @@ -73,308 +58,28 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries if (contents.Any()) { - var cache = new Dictionary<(Guid, Status), StatusInfo>(); - foreach (var content in contents) { var result = SimpleMapper.Map(content, new ContentEntity()); - await EnrichColorAsync(content, result, cache); - - if (ShouldEnrichWithStatuses(context)) - { - await EnrichNextsAsync(content, result, context); - await EnrichCanUpdateAsync(content, result, context); - } - results.Add(result); } - foreach (var group in results.GroupBy(x => x.SchemaId.Id)) - { - var schema = await ContentQuery.GetSchemaOrThrowAsync(context, group.Key.ToString()); - - foreach (var content in group) - { - content.CacheDependencies = new HashSet - { - app.Id, - app.Version, - schema.Id, - schema.Version - }; - } - - if (ShouldEnrichWithSchema(context)) - { - var referenceFields = schema.SchemaDef.ReferencesFields().ToArray(); - - var schemaName = schema.SchemaDef.Name; - var schemaDisplayName = schema.SchemaDef.DisplayNameUnchanged(); - - foreach (var content in group) - { - content.ReferenceFields = referenceFields; - content.SchemaName = schemaName; - content.SchemaDisplayName = schemaDisplayName; - } - } - } - - if (ShouldEnrich(context)) - { - await EnrichReferencesAsync(context, results); - await EnrichAssetsAsync(context, results); - } - } - - return results; - } - } - - private async Task EnrichAssetsAsync(Context context, List contents) - { - var ids = new HashSet(); - - foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) - { - var schema = await ContentQuery.GetSchemaOrThrowAsync(context, group.Key.ToString()); - - AddAssetIds(ids, schema, group); - } - - var assets = await GetAssetsAsync(context, ids); - - foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) - { - var schema = await ContentQuery.GetSchemaOrThrowAsync(context, group.Key.ToString()); - - ResolveAssets(schema, group, assets); - } - } - - private async Task EnrichReferencesAsync(Context context, List contents) - { - var ids = new HashSet(); - - foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) - { - var schema = await ContentQuery.GetSchemaOrThrowAsync(context, group.Key.ToString()); + var schemaCache = new Dictionary>(); - AddReferenceIds(ids, schema, group); - } - - var references = await GetReferencesAsync(context, ids); - - foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) - { - var schema = await ContentQuery.GetSchemaOrThrowAsync(context, group.Key.ToString()); - - await ResolveReferencesAsync(context, schema, group, references); - } - } - - private async Task ResolveReferencesAsync(Context context, ISchemaEntity schema, IEnumerable contents, ILookup references) - { - var formatted = new Dictionary(); - - foreach (var field in schema.SchemaDef.ResolvingReferences()) - { - foreach (var content in contents) - { - if (content.ReferenceData == null) - { - content.ReferenceData = new NamedContentData(); - } - - var fieldReference = content.ReferenceData.GetOrAdd(field.Name, _ => new ContentFieldData())!; - - try - { - if (content.DataDraft.TryGetValue(field.Name, out var fieldData) && fieldData != null) - { - foreach (var (partition, partitionValue) in fieldData) - { - var referencedContents = - field.GetReferencedIds(partitionValue, Ids.ContentOnly) - .Select(x => references[x]) - .SelectMany(x => x) - .ToList(); - - if (referencedContents.Count == 1) - { - var reference = referencedContents[0]; - - var referencedSchema = await ContentQuery.GetSchemaOrThrowAsync(context, reference.SchemaId.Id.ToString()); - - content.CacheDependencies.Add(referencedSchema.Id); - content.CacheDependencies.Add(referencedSchema.Version); - content.CacheDependencies.Add(reference.Id); - content.CacheDependencies.Add(reference.Version); - - var value = formatted.GetOrAdd(reference, x => Format(x, context, referencedSchema)); - - fieldReference.AddJsonValue(partition, value); - } - else if (referencedContents.Count > 1) - { - var value = CreateFallback(context, referencedContents); - - fieldReference.AddJsonValue(partition, value); - } - } - } - } - catch (DomainObjectNotFoundException) - { - continue; - } - } - } - } - - private void ResolveAssets(ISchemaEntity schema, IGrouping contents, ILookup assets) - { - foreach (var field in schema.SchemaDef.ResolvingAssets()) - { - foreach (var content in contents) - { - if (content.ReferenceData == null) + Task GetSchema(Guid id) { - content.ReferenceData = new NamedContentData(); + return schemaCache.GetOrAdd(id, x => ContentQuery.GetSchemaOrThrowAsync(context, x.ToString())); } - var fieldReference = content.ReferenceData.GetOrAdd(field.Name, _ => new ContentFieldData())!; - - if (content.DataDraft.TryGetValue(field.Name, out var fieldData) && fieldData != null) + foreach (var step in steps) { - foreach (var (partitionKey, partitionValue) in fieldData) - { - var referencedImage = - field.GetReferencedIds(partitionValue, Ids.ContentOnly) - .Select(x => assets[x]) - .SelectMany(x => x) - .FirstOrDefault(x => x.Type == AssetType.Image); - - if (referencedImage != null) - { - var url = assetUrlGenerator.GenerateUrl(referencedImage.Id.ToString()); - - content.CacheDependencies.Add(referencedImage.Id); - content.CacheDependencies.Add(referencedImage.Version); - - fieldReference.AddJsonValue(partitionKey, JsonValue.Create(url)); - } - } + await step.EnrichAsync(context, results, GetSchema); } } - } - } - - private static JsonObject Format(IContentEntity content, Context context, ISchemaEntity referencedSchema) - { - return content.DataDraft.FormatReferences(referencedSchema.SchemaDef, context.App.LanguagesConfig); - } - - private static JsonObject CreateFallback(Context context, List referencedContents) - { - var text = $"{referencedContents.Count} Reference(s)"; - - var value = JsonValue.Object(); - - foreach (var partitionKey in context.App.LanguagesConfig.AllKeys) - { - value.Add(partitionKey, text); - } - - return value; - } - - private void AddReferenceIds(HashSet ids, ISchemaEntity schema, IEnumerable contents) - { - foreach (var content in contents) - { - ids.AddRange(content.DataDraft.GetReferencedIds(schema.SchemaDef.ResolvingReferences(), Ids.ContentOnly)); - } - } - - private void AddAssetIds(HashSet ids, ISchemaEntity schema, IEnumerable contents) - { - foreach (var content in contents) - { - ids.AddRange(content.DataDraft.GetReferencedIds(schema.SchemaDef.ResolvingAssets(), Ids.ContentOnly)); - } - } - - private async Task> GetReferencesAsync(Context context, HashSet ids) - { - if (ids.Count == 0) - { - return EmptyContents; - } - - var references = await ContentQuery.QueryAsync(context.Clone().WithoutContentEnrichment(true), ids.ToList()); - - return references.ToLookup(x => x.Id); - } - private async Task> GetAssetsAsync(Context context, HashSet ids) - { - if (ids.Count == 0) - { - return EmptyAssets; - } - - var assets = await assetQuery.QueryAsync(context.Clone().WithNoAssetEnrichment(true), null, Q.Empty.WithIds(ids)); - - return assets.ToLookup(x => x.Id); - } - - private async Task EnrichCanUpdateAsync(IContentEntity content, ContentEntity result, Context context) - { - result.CanUpdate = await contentWorkflow.CanUpdateAsync(content, context.User); - } - - private async Task EnrichNextsAsync(IContentEntity content, ContentEntity result, Context context) - { - result.Nexts = await contentWorkflow.GetNextsAsync(content, context.User); - } - - private async Task EnrichColorAsync(IContentEntity content, ContentEntity result, Dictionary<(Guid, Status), StatusInfo> cache) - { - result.StatusColor = await GetColorAsync(content, cache); - } - - private async Task GetColorAsync(IContentEntity content, Dictionary<(Guid, Status), StatusInfo> cache) - { - if (!cache.TryGetValue((content.SchemaId.Id, content.Status), out var info)) - { - info = await contentWorkflow.GetInfoAsync(content); - - if (info == null) - { - info = new StatusInfo(content.Status, DefaultColor); - } - - cache[(content.SchemaId.Id, content.Status)] = info; + return results; } - - return info.Color; - } - - private static bool ShouldEnrichWithSchema(Context context) - { - return context.IsFrontendClient; - } - - private static bool ShouldEnrichWithStatuses(Context context) - { - return context.IsFrontendClient || context.IsResolveFlow(); - } - - private static bool ShouldEnrich(Context context) - { - return context.IsFrontendClient && !context.IsNoEnrichment(); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs index db5125dc1..b6fc2faf3 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs @@ -10,7 +10,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Entities.Contents.Repositories; using Squidex.Domain.Apps.Entities.Schemas; @@ -29,7 +28,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries private static readonly Status[] StatusPublishedOnly = { Status.Published }; private static readonly IResultList EmptyContents = ResultList.CreateFrom(0); private readonly IAppProvider appProvider; - private readonly IAssetUrlGenerator assetUrlGenerator; private readonly IContentEnricher contentEnricher; private readonly IContentRepository contentRepository; private readonly IContentLoader contentVersionLoader; @@ -38,7 +36,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries public ContentQueryService( IAppProvider appProvider, - IAssetUrlGenerator assetUrlGenerator, IContentEnricher contentEnricher, IContentRepository contentRepository, IContentLoader contentVersionLoader, @@ -46,7 +43,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries ContentQueryParser queryParser) { Guard.NotNull(appProvider); - Guard.NotNull(assetUrlGenerator); Guard.NotNull(contentEnricher); Guard.NotNull(contentRepository); Guard.NotNull(contentVersionLoader); @@ -54,7 +50,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries Guard.NotNull(scriptEngine); this.appProvider = appProvider; - this.assetUrlGenerator = assetUrlGenerator; this.contentEnricher = contentEnricher; this.contentRepository = contentRepository; this.contentVersionLoader = contentVersionLoader; @@ -169,8 +164,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { var results = new List(); - var converters = GenerateConverters(context).ToArray(); - var script = schema.SchemaDef.Scripts.Query; var scripting = !string.IsNullOrWhiteSpace(script); @@ -180,25 +173,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { var result = SimpleMapper.Map(content, new ContentEntity()); - if (result.Data != null) + if (result.Data != null && !context.IsFrontendClient && scripting) { - if (!context.IsFrontendClient && scripting) - { - var ctx = new ScriptContext { User = context.User, Data = content.Data, ContentId = content.Id }; - - result.Data = scriptEngine.Transform(ctx, script); - } - - result.Data = result.Data.ConvertName2Name(schema.SchemaDef, converters); - } + var ctx = new ScriptContext { User = context.User, Data = content.Data, ContentId = content.Id }; - if (result.DataDraft != null && (context.IsUnpublished() || context.IsFrontendClient)) - { - result.DataDraft = result.DataDraft.ConvertName2Name(schema.SchemaDef, converters); - } - else - { - result.DataDraft = null!; + result.Data = scriptEngine.Transform(ctx, script); } results.Add(result); @@ -208,43 +187,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries } } - private IEnumerable GenerateConverters(Context context) - { - if (!context.IsFrontendClient) - { - yield return FieldConverters.ExcludeHidden(); - yield return FieldConverters.ForNestedName2Name(ValueConverters.ExcludeHidden()); - } - - yield return FieldConverters.ExcludeChangedTypes(); - yield return FieldConverters.ForNestedName2Name(ValueConverters.ExcludeChangedTypes()); - - yield return FieldConverters.ResolveInvariant(context.App.LanguagesConfig); - yield return FieldConverters.ResolveLanguages(context.App.LanguagesConfig); - - if (!context.IsFrontendClient) - { - if (!context.IsNoResolveLanguages()) - { - yield return FieldConverters.ResolveFallbackLanguages(context.App.LanguagesConfig); - } - - var languages = context.Languages(); - - if (languages.Any()) - { - yield return FieldConverters.FilterLanguages(context.App.LanguagesConfig, languages); - } - - var assetUrls = context.AssetUrls(); - - if (assetUrls.Any()) - { - yield return FieldConverters.ResolveAssetUrls(assetUrls.ToList(), assetUrlGenerator); - } - } - } - public async Task GetSchemaOrThrowAsync(Context context, string schemaIdOrName) { ISchemaEntity? schema = null; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/IContentEnricher.cs similarity index 92% rename from backend/src/Squidex.Domain.Apps.Entities/Contents/IContentEnricher.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/IContentEnricher.cs index e8b55d520..7a2a5a741 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentEnricher.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/IContentEnricher.cs @@ -8,7 +8,7 @@ using System.Collections.Generic; using System.Threading.Tasks; -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities.Contents.Queries { public interface IContentEnricher { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/IContentEnricherStep.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/IContentEnricherStep.cs new file mode 100644 index 000000000..ded12b831 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/IContentEnricherStep.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// 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 Squidex.Domain.Apps.Entities.Schemas; + +namespace Squidex.Domain.Apps.Entities.Contents.Queries +{ + public delegate Task ProvideSchema(Guid id); + + public interface IContentEnricherStep + { + Task EnrichAsync(Context context, IEnumerable contents, ProvideSchema schemas); + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs new file mode 100644 index 000000000..f1e0a3436 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs @@ -0,0 +1,93 @@ +// ========================================================================== +// 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.ConvertContent; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps +{ + public sealed class ConvertData : IContentEnricherStep + { + private readonly IAssetUrlGenerator assetUrlGenerator; + + public ConvertData(IAssetUrlGenerator assetUrlGenerator) + { + Guard.NotNull(assetUrlGenerator); + + this.assetUrlGenerator = assetUrlGenerator; + } + + public async Task EnrichAsync(Context context, IEnumerable contents, ProvideSchema schemas) + { + var converters = GenerateConverters(context).ToArray(); + + var resolveDataDraft = context.IsUnpublished() || context.IsFrontendClient; + + foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) + { + var schema = await schemas(group.Key); + + foreach (var content in group) + { + if (content.Data != null) + { + content.Data = content.Data.ConvertName2Name(schema.SchemaDef, converters); + } + + if (content.DataDraft != null && resolveDataDraft) + { + content.DataDraft = content.DataDraft.ConvertName2Name(schema.SchemaDef, converters); + } + else + { + content.DataDraft = null!; + } + } + } + } + + private IEnumerable GenerateConverters(Context context) + { + if (!context.IsFrontendClient) + { + yield return FieldConverters.ExcludeHidden(); + yield return FieldConverters.ForNestedName2Name(ValueConverters.ExcludeHidden()); + } + + yield return FieldConverters.ExcludeChangedTypes(); + yield return FieldConverters.ForNestedName2Name(ValueConverters.ExcludeChangedTypes()); + + yield return FieldConverters.ResolveInvariant(context.App.LanguagesConfig); + yield return FieldConverters.ResolveLanguages(context.App.LanguagesConfig); + + if (!context.IsFrontendClient) + { + if (!context.IsNoResolveLanguages()) + { + yield return FieldConverters.ResolveFallbackLanguages(context.App.LanguagesConfig); + } + + var languages = context.Languages(); + + if (languages.Any()) + { + yield return FieldConverters.FilterLanguages(context.App.LanguagesConfig, languages); + } + + var assetUrls = context.AssetUrls(); + + if (assetUrls.Any()) + { + yield return FieldConverters.ResolveAssetUrls(assetUrls.ToList(), assetUrlGenerator); + } + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichForCaching.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichForCaching.cs new file mode 100644 index 000000000..b78018fba --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichForCaching.cs @@ -0,0 +1,36 @@ +// ========================================================================== +// 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; + +namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps +{ + public sealed class EnrichForCaching : IContentEnricherStep + { + public async Task EnrichAsync(Context context, IEnumerable contents, ProvideSchema schemas) + { + var app = context.App; + + foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) + { + var schema = await schemas(group.Key); + + foreach (var content in group) + { + content.CacheDependencies ??= new HashSet(); + + content.CacheDependencies.Add(app.Id); + content.CacheDependencies.Add(app.Version); + content.CacheDependencies.Add(schema.Id); + content.CacheDependencies.Add(schema.Version); + } + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichWithSchema.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichWithSchema.cs new file mode 100644 index 000000000..f88021b0c --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichWithSchema.cs @@ -0,0 +1,44 @@ +// ========================================================================== +// 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.Schemas; + +namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps +{ + public sealed class EnrichWithSchema : IContentEnricherStep + { + public async Task EnrichAsync(Context context, IEnumerable contents, ProvideSchema schemas) + { + foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) + { + var schema = await schemas(group.Key); + + var schemaName = schema.SchemaDef.Name; + var schemaDisplayName = schema.SchemaDef.DisplayNameUnchanged(); + + foreach (var content in group) + { + content.SchemaName = schemaName; + content.SchemaDisplayName = schemaDisplayName; + } + + if (context.IsFrontendClient) + { + var referenceFields = schema.SchemaDef.ReferencesFields().ToArray(); + + foreach (var content in group) + { + content.ReferenceFields = referenceFields; + } + } + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichWithWorkflows.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichWithWorkflows.cs new file mode 100644 index 000000000..a89071812 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichWithWorkflows.cs @@ -0,0 +1,82 @@ +// ========================================================================== +// 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 Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps +{ + public sealed class EnrichWithWorkflows : IContentEnricherStep + { + private const string DefaultColor = StatusColors.Draft; + + private readonly IContentWorkflow contentWorkflow; + + public EnrichWithWorkflows(IContentWorkflow contentWorkflow) + { + Guard.NotNull(contentWorkflow); + + this.contentWorkflow = contentWorkflow; + } + + public async Task EnrichAsync(Context context, IEnumerable contents, ProvideSchema schemas) + { + var cache = new Dictionary<(Guid, Status), StatusInfo>(); + + foreach (var content in contents) + { + await EnrichColorAsync(content, content, cache); + + if (ShouldEnrichWithStatuses(context)) + { + await EnrichNextsAsync(content, context); + await EnrichCanUpdateAsync(content, context); + } + } + } + + private async Task EnrichNextsAsync(ContentEntity content, Context context) + { + content.Nexts = await contentWorkflow.GetNextsAsync(content, context.User); + } + + private async Task EnrichCanUpdateAsync( ContentEntity content, Context context) + { + content.CanUpdate = await contentWorkflow.CanUpdateAsync(content, context.User); + } + + private async Task EnrichColorAsync(IContentEntity content, ContentEntity result, Dictionary<(Guid, Status), StatusInfo> cache) + { + result.StatusColor = await GetColorAsync(content, cache); + } + + private async Task GetColorAsync(IContentEntity content, Dictionary<(Guid, Status), StatusInfo> cache) + { + if (!cache.TryGetValue((content.SchemaId.Id, content.Status), out var info)) + { + info = await contentWorkflow.GetInfoAsync(content); + + if (info == null) + { + info = new StatusInfo(content.Status, DefaultColor); + } + + cache[(content.SchemaId.Id, content.Status)] = info; + } + + return info.Color; + } + + private static bool ShouldEnrichWithStatuses(Context context) + { + return context.IsFrontendClient || context.IsResolveFlow(); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveAssets.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveAssets.cs new file mode 100644 index 000000000..e91e01ba1 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveAssets.cs @@ -0,0 +1,129 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Assets; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.ConvertContent; +using Squidex.Domain.Apps.Core.ExtractReferenceIds; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps +{ + public sealed class ResolveAssets : IContentEnricherStep + { + private static readonly ILookup EmptyAssets = Enumerable.Empty().ToLookup(x => x.Id); + + private readonly IAssetUrlGenerator assetUrlGenerator; + private readonly IAssetQueryService assetQuery; + + public ResolveAssets(IAssetUrlGenerator assetUrlGenerator, IAssetQueryService assetQuery) + { + Guard.NotNull(assetUrlGenerator); + Guard.NotNull(assetQuery); + + this.assetUrlGenerator = assetUrlGenerator; + this.assetQuery = assetQuery; + } + + public async Task EnrichAsync(Context context, IEnumerable contents, ProvideSchema schemas) + { + if (ShouldEnrich(context)) + { + var ids = new HashSet(); + + foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) + { + var schema = await schemas(group.Key); + + AddAssetIds(ids, schema, group); + } + + var assets = await GetAssetsAsync(context, ids); + + foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) + { + var schema = await schemas(group.Key); + + ResolveAssetsUrls(schema, group, assets); + } + } + } + + private void ResolveAssetsUrls(ISchemaEntity schema, IGrouping contents, ILookup assets) + { + foreach (var field in schema.SchemaDef.ResolvingAssets()) + { + foreach (var content in contents) + { + if (content.ReferenceData == null) + { + content.ReferenceData = new NamedContentData(); + } + + var fieldReference = content.ReferenceData.GetOrAdd(field.Name, _ => new ContentFieldData())!; + + if (content.DataDraft.TryGetValue(field.Name, out var fieldData) && fieldData != null) + { + foreach (var (partitionKey, partitionValue) in fieldData) + { + var referencedImage = + field.GetReferencedIds(partitionValue, Ids.ContentOnly) + .Select(x => assets[x]) + .SelectMany(x => x) + .FirstOrDefault(x => x.Type == AssetType.Image); + + if (referencedImage != null) + { + var url = assetUrlGenerator.GenerateUrl(referencedImage.Id.ToString()); + + content.CacheDependencies ??= new HashSet(); + + content.CacheDependencies.Add(referencedImage.Id); + content.CacheDependencies.Add(referencedImage.Version); + + fieldReference.AddJsonValue(partitionKey, JsonValue.Create(url)); + } + } + } + } + } + } + + private async Task> GetAssetsAsync(Context context, HashSet ids) + { + if (ids.Count == 0) + { + return EmptyAssets; + } + + var assets = await assetQuery.QueryAsync(context.Clone().WithNoAssetEnrichment(true), null, Q.Empty.WithIds(ids)); + + return assets.ToLookup(x => x.Id); + } + + private void AddAssetIds(HashSet ids, ISchemaEntity schema, IEnumerable contents) + { + foreach (var content in contents) + { + ids.AddRange(content.DataDraft.GetReferencedIds(schema.SchemaDef.ResolvingAssets(), Ids.ContentOnly)); + } + } + + private static bool ShouldEnrich(Context context) + { + return context.IsFrontendClient && !context.IsNoEnrichment(); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveReferences.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveReferences.cs new file mode 100644 index 000000000..1bad80603 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveReferences.cs @@ -0,0 +1,167 @@ +// ========================================================================== +// 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.Domain.Apps.Core.ExtractReferenceIds; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps +{ + public sealed class ResolveReferences : IContentEnricherStep + { + private static readonly ILookup EmptyContents = Enumerable.Empty().ToLookup(x => x.Id); + private readonly Lazy contentQuery; + + private IContentQueryService ContentQuery + { + get { return contentQuery.Value; } + } + + public ResolveReferences(Lazy contentQuery) + { + Guard.NotNull(contentQuery); + + this.contentQuery = contentQuery; + } + + public async Task EnrichAsync(Context context, IEnumerable contents, ProvideSchema schemas) + { + if (ShouldEnrich(context)) + { + var ids = new HashSet(); + + foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) + { + var schema = await schemas(group.Key); + + AddReferenceIds(ids, schema, group); + } + + var references = await GetReferencesAsync(context, ids); + + foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) + { + var schema = await schemas(group.Key); + + await ResolveReferencesAsync(context, schema, group, references, schemas); + } + } + } + + private async Task ResolveReferencesAsync(Context context, ISchemaEntity schema, IEnumerable contents, ILookup references, ProvideSchema schemas) + { + var formatted = new Dictionary(); + + foreach (var field in schema.SchemaDef.ResolvingReferences()) + { + foreach (var content in contents) + { + if (content.ReferenceData == null) + { + content.ReferenceData = new NamedContentData(); + } + + var fieldReference = content.ReferenceData.GetOrAdd(field.Name, _ => new ContentFieldData())!; + + try + { + if (content.DataDraft.TryGetValue(field.Name, out var fieldData) && fieldData != null) + { + foreach (var (partition, partitionValue) in fieldData) + { + var referencedContents = + field.GetReferencedIds(partitionValue, Ids.ContentOnly) + .Select(x => references[x]) + .SelectMany(x => x) + .ToList(); + + if (referencedContents.Count == 1) + { + var reference = referencedContents[0]; + + var referencedSchema = await schemas(reference.SchemaId.Id); + + content.CacheDependencies ??= new HashSet(); + + content.CacheDependencies.Add(referencedSchema.Id); + content.CacheDependencies.Add(referencedSchema.Version); + content.CacheDependencies.Add(reference.Id); + content.CacheDependencies.Add(reference.Version); + + var value = formatted.GetOrAdd(reference, x => Format(x, context, referencedSchema)); + + fieldReference.AddJsonValue(partition, value); + } + else if (referencedContents.Count > 1) + { + var value = CreateFallback(context, referencedContents); + + fieldReference.AddJsonValue(partition, value); + } + } + } + } + catch (DomainObjectNotFoundException) + { + continue; + } + } + } + } + + private static JsonObject Format(IContentEntity content, Context context, ISchemaEntity referencedSchema) + { + return content.DataDraft.FormatReferences(referencedSchema.SchemaDef, context.App.LanguagesConfig); + } + + private static JsonObject CreateFallback(Context context, List referencedContents) + { + var text = $"{referencedContents.Count} Reference(s)"; + + var value = JsonValue.Object(); + + foreach (var partitionKey in context.App.LanguagesConfig.AllKeys) + { + value.Add(partitionKey, text); + } + + return value; + } + + private void AddReferenceIds(HashSet ids, ISchemaEntity schema, IEnumerable contents) + { + foreach (var content in contents) + { + ids.AddRange(content.DataDraft.GetReferencedIds(schema.SchemaDef.ResolvingReferences(), Ids.ContentOnly)); + } + } + + private async Task> GetReferencesAsync(Context context, HashSet ids) + { + if (ids.Count == 0) + { + return EmptyContents; + } + + var references = await ContentQuery.QueryAsync(context.Clone().WithoutContentEnrichment(true), ids.ToList()); + + return references.ToLookup(x => x.Id); + } + + private static bool ShouldEnrich(Context context) + { + return context.IsFrontendClient && !context.IsNoEnrichment(); + } + } +} diff --git a/backend/src/Squidex/Config/Domain/ContentsServices.cs b/backend/src/Squidex/Config/Domain/ContentsServices.cs index 882090e91..352d83df0 100644 --- a/backend/src/Squidex/Config/Domain/ContentsServices.cs +++ b/backend/src/Squidex/Config/Domain/ContentsServices.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents.Queries; +using Squidex.Domain.Apps.Entities.Contents.Queries.Steps; using Squidex.Domain.Apps.Entities.Contents.Text; using Squidex.Domain.Apps.Entities.History; using Squidex.Infrastructure.EventSourcing; @@ -39,6 +40,24 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs index 9ac7bddf7..2281f9f51 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs @@ -9,6 +9,7 @@ using System; using System.Threading.Tasks; using FakeItEasy; using Orleans; +using Squidex.Domain.Apps.Entities.Contents.Queries; using Squidex.Domain.Apps.Entities.Contents.State; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure.Commands; diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs index 313524a26..6c5f5c4e4 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs @@ -6,11 +6,11 @@ // ========================================================================== using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using FakeItEasy; using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.ConvertContent; -using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; @@ -20,179 +20,89 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { public class ContentEnricherTests { - private readonly IContentWorkflow contentWorkflow = A.Fake(); private readonly IContentQueryService contentQuery = A.Fake(); - private readonly IAssetQueryService assetQuery = A.Fake(); - private readonly IAssetUrlGenerator assetUrlGenerator = A.Fake(); private readonly ISchemaEntity schema; private readonly Context requestContext; private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); - private readonly ContentEnricher sut; - public ContentEnricherTests() + private sealed class ResolveSchema : IContentEnricherStep { - requestContext = new Context(Mocks.ApiUser(), Mocks.App(appId)); + public ISchemaEntity Schema { get; private set; } - schema = Mocks.Schema(appId, schemaId); - - A.CallTo(() => contentQuery.GetSchemaOrThrowAsync(A.Ignored, schemaId.Id.ToString())) - .Returns(schema); - - sut = new ContentEnricher(assetQuery, assetUrlGenerator, new Lazy(() => contentQuery), contentWorkflow); - } - - [Fact] - public async Task Should_add_app_version_and_schema_as_dependency() - { - var source = PublishedContent(); - - A.CallTo(() => contentWorkflow.GetInfoAsync(source)) - .Returns(new StatusInfo(Status.Published, StatusColors.Published)); - - var result = await sut.EnrichAsync(source, requestContext); - - Assert.Contains(requestContext.App.Version, result.CacheDependencies); - - Assert.Contains(schema.Id, result.CacheDependencies); - Assert.Contains(schema.Version, result.CacheDependencies); + public async Task EnrichAsync(Context context, IEnumerable contents, ProvideSchema schemas) + { + foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) + { + Schema = await schemas(group.Key); + } + } } - [Fact] - public async Task Should_enrich_with_reference_fields() + public ContentEnricherTests() { - var ctx = new Context(Mocks.FrontendUser(), requestContext.App); - - var source = PublishedContent(); - - A.CallTo(() => contentWorkflow.GetInfoAsync(source)) - .Returns(new StatusInfo(Status.Published, StatusColors.Published)); + requestContext = new Context(Mocks.ApiUser(), Mocks.App(appId)); - var result = await sut.EnrichAsync(source, ctx); + schema = Mocks.Schema(appId, schemaId); - Assert.NotNull(result.ReferenceFields); + A.CallTo(() => contentQuery.GetSchemaOrThrowAsync(requestContext, schemaId.Id.ToString())) + .Returns(schema); } [Fact] - public async Task Should_not_enrich_with_reference_fields_when_not_frontend() + public async Task Should_not_invoke_steps() { - var source = PublishedContent(); + var source = new IContentEntity[0]; - A.CallTo(() => contentWorkflow.GetInfoAsync(source)) - .Returns(new StatusInfo(Status.Published, StatusColors.Published)); + var step1 = A.Fake(); + var step2 = A.Fake(); - var result = await sut.EnrichAsync(source, requestContext); + var sut = new ContentEnricher(new[] { step1, step2 }, new Lazy(() => contentQuery)); - Assert.Null(result.ReferenceFields); - } - - [Fact] - public async Task Should_enrich_with_schema_names() - { - var ctx = new Context(Mocks.FrontendUser(), requestContext.App); + await sut.EnrichAsync(source, requestContext); - var source = PublishedContent(); - - A.CallTo(() => contentWorkflow.GetInfoAsync(source)) - .Returns(new StatusInfo(Status.Published, StatusColors.Published)); - - var result = await sut.EnrichAsync(source, ctx); + A.CallTo(() => step1.EnrichAsync(requestContext, A>.Ignored, A.Ignored)) + .MustNotHaveHappened(); - Assert.Equal("my-schema", result.SchemaName); - Assert.Equal("my-schema", result.SchemaDisplayName); + A.CallTo(() => step2.EnrichAsync(requestContext, A>.Ignored, A.Ignored)) + .MustNotHaveHappened(); } [Fact] - public async Task Should_not_enrich_with_schema_names_when_not_frontend() + public async Task Should_invoke_steps() { var source = PublishedContent(); - A.CallTo(() => contentWorkflow.GetInfoAsync(source)) - .Returns(new StatusInfo(Status.Published, StatusColors.Published)); - - var result = await sut.EnrichAsync(source, requestContext); - - Assert.Null(result.SchemaName); - Assert.Null(result.SchemaDisplayName); - } + var step1 = A.Fake(); + var step2 = A.Fake(); - [Fact] - public async Task Should_enrich_content_with_status_color() - { - var source = PublishedContent(); + var sut = new ContentEnricher(new[] { step1, step2 }, new Lazy(() => contentQuery)); - A.CallTo(() => contentWorkflow.GetInfoAsync(source)) - .Returns(new StatusInfo(Status.Published, StatusColors.Published)); + await sut.EnrichAsync(source, requestContext); - var result = await sut.EnrichAsync(source, requestContext); + A.CallTo(() => step1.EnrichAsync(requestContext, A>.Ignored, A.Ignored)) + .MustHaveHappened(); - Assert.Equal(StatusColors.Published, result.StatusColor); + A.CallTo(() => step2.EnrichAsync(requestContext, A>.Ignored, A.Ignored)) + .MustHaveHappened(); } [Fact] - public async Task Should_enrich_content_with_default_color_if_not_found() + public async Task Should_provide_and_cache_schema() { var source = PublishedContent(); - A.CallTo(() => contentWorkflow.GetInfoAsync(source)) - .Returns(Task.FromResult(null!)); - - var result = await sut.EnrichAsync(source, requestContext); - - Assert.Equal(StatusColors.Draft, result.StatusColor); - } - - [Fact] - public async Task Should_enrich_content_with_can_update() - { - requestContext.WithResolveFlow(true); - - var source = new ContentEntity { SchemaId = schemaId }; - - A.CallTo(() => contentWorkflow.CanUpdateAsync(source, requestContext.User)) - .Returns(true); - - var result = await sut.EnrichAsync(source, requestContext); - - Assert.True(result.CanUpdate); - } - - [Fact] - public async Task Should_not_enrich_content_with_can_update_if_disabled_in_context() - { - requestContext.WithResolveFlow(false); - - var source = new ContentEntity { SchemaId = schemaId }; - - var result = await sut.EnrichAsync(source, requestContext); - - Assert.False(result.CanUpdate); - - A.CallTo(() => contentWorkflow.CanUpdateAsync(source, requestContext.User)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_enrich_multiple_contents_and_cache_color() - { - var source1 = PublishedContent(); - var source2 = PublishedContent(); - - var source = new IContentEntity[] - { - source1, - source2 - }; + var step1 = new ResolveSchema(); + var step2 = new ResolveSchema(); - A.CallTo(() => contentWorkflow.GetInfoAsync(source1)) - .Returns(new StatusInfo(Status.Published, StatusColors.Published)); + var sut = new ContentEnricher(new[] { step1, step2 }, new Lazy(() => contentQuery)); - var result = await sut.EnrichAsync(source, requestContext); + await sut.EnrichAsync(source, requestContext); - Assert.Equal(StatusColors.Published, result[0].StatusColor); - Assert.Equal(StatusColors.Published, result[1].StatusColor); + Assert.Same(schema, step1.Schema); + Assert.Same(schema, step1.Schema); - A.CallTo(() => contentWorkflow.GetInfoAsync(A.Ignored)) + A.CallTo(() => contentQuery.GetSchemaOrThrowAsync(requestContext, schemaId.Id.ToString())) .MustHaveHappenedOnceExactly(); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs index 80a5e9da7..756e19a45 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs @@ -35,7 +35,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { private readonly IAppEntity app; private readonly IAppProvider appProvider = A.Fake(); - private readonly IAssetUrlGenerator urlGenerator = A.Fake(); private readonly IContentEnricher contentEnricher = A.Fake(); private readonly IContentRepository contentRepository = A.Fake(); private readonly IContentLoader contentVersionLoader = A.Fake(); @@ -79,7 +78,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries sut = new ContentQueryService( appProvider, - urlGenerator, contentEnricher, contentRepository, contentVersionLoader, diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichForCachingTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichForCachingTests.cs new file mode 100644 index 000000000..e7934ea28 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichForCachingTests.cs @@ -0,0 +1,57 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Contents.Queries.Steps; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Contents.Queries +{ + public class EnrichForCachingTests + { + private readonly ISchemaEntity schema; + private readonly Context requestContext; + private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); + private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); + private readonly ProvideSchema schemaProvider; + private readonly EnrichForCaching sut; + + public EnrichForCachingTests() + { + requestContext = new Context(Mocks.ApiUser(), Mocks.App(appId)); + + schema = Mocks.Schema(appId, schemaId); + schemaProvider = x => Task.FromResult(schema); + + sut = new EnrichForCaching(); + } + + [Fact] + public async Task Should_add_app_version_and_schema_as_dependency() + { + var source = PublishedContent(); + + await sut.EnrichAsync(requestContext, Enumerable.Repeat(source, 1), schemaProvider); + + Assert.Contains(requestContext.App.Version, source.CacheDependencies); + + Assert.Contains(schema.Id, source.CacheDependencies); + Assert.Contains(schema.Version, source.CacheDependencies); + } + + private ContentEntity PublishedContent() + { + return new ContentEntity { Status = Status.Published, SchemaId = schemaId }; + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichWithSchemaTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichWithSchemaTests.cs new file mode 100644 index 000000000..c5e0f39dd --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichWithSchemaTests.cs @@ -0,0 +1,79 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Contents.Queries.Steps; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Contents.Queries +{ + public class EnrichWithSchemaTests + { + private readonly ISchemaEntity schema; + private readonly Context requestContext; + private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); + private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); + private readonly ProvideSchema schemaProvider; + private readonly EnrichWithSchema sut; + + public EnrichWithSchemaTests() + { + requestContext = new Context(Mocks.ApiUser(), Mocks.App(appId)); + + schema = Mocks.Schema(appId, schemaId); + schemaProvider = x => Task.FromResult(schema); + + sut = new EnrichWithSchema(); + } + + [Fact] + public async Task Should_enrich_with_reference_fields() + { + var ctx = new Context(Mocks.FrontendUser(), requestContext.App); + + var source = PublishedContent(); + + await sut.EnrichAsync(ctx, Enumerable.Repeat(source, 1), schemaProvider); + + Assert.NotNull(source.ReferenceFields); + } + + [Fact] + public async Task Should_not_enrich_with_reference_fields_when_not_frontend() + { + var source = PublishedContent(); + + await sut.EnrichAsync(requestContext, Enumerable.Repeat(source, 1), schemaProvider); + + Assert.Null(source.ReferenceFields); + } + + [Fact] + public async Task Should_enrich_with_schema_names() + { + var ctx = new Context(Mocks.FrontendUser(), requestContext.App); + + var source = PublishedContent(); + + await sut.EnrichAsync(requestContext, Enumerable.Repeat(source, 1), schemaProvider); + + Assert.Equal("my-schema", source.SchemaName); + Assert.Equal("my-schema", source.SchemaDisplayName); + } + + private ContentEntity PublishedContent() + { + return new ContentEntity { Status = Status.Published, SchemaId = schemaId }; + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichWithWorkflowsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichWithWorkflowsTests.cs new file mode 100644 index 000000000..0d6028b2f --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichWithWorkflowsTests.cs @@ -0,0 +1,109 @@ +// ========================================================================== +// 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.Domain.Apps.Entities.Contents.Queries.Steps; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Contents.Queries +{ + public class EnrichWithWorkflowsTests + { + private readonly IContentWorkflow contentWorkflow = A.Fake(); + private readonly IContentQueryService contentQuery = A.Fake(); + private readonly ISchemaEntity schema; + private readonly Context requestContext; + private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); + private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); + private readonly ProvideSchema schemaProvider; + private readonly EnrichWithWorkflows sut; + + public EnrichWithWorkflowsTests() + { + requestContext = new Context(Mocks.ApiUser(), Mocks.App(appId)); + + schema = Mocks.Schema(appId, schemaId); + schemaProvider = x => Task.FromResult(schema); + + A.CallTo(() => contentQuery.GetSchemaOrThrowAsync(A.Ignored, schemaId.Id.ToString())) + .Returns(schema); + + sut = new EnrichWithWorkflows(contentWorkflow); + } + + [Fact] + public async Task Should_enrich_content_with_status_color() + { + var source = PublishedContent(); + + A.CallTo(() => contentWorkflow.GetInfoAsync(source)) + .Returns(new StatusInfo(Status.Published, StatusColors.Published)); + + await sut.EnrichAsync(requestContext, new[] { source }, schemaProvider); + + Assert.Equal(StatusColors.Published, source.StatusColor); + } + + [Fact] + public async Task Should_enrich_content_with_default_color_if_not_found() + { + var source = PublishedContent(); + + A.CallTo(() => contentWorkflow.GetInfoAsync(source)) + .Returns(Task.FromResult(null!)); + + var ctx = requestContext.WithResolveFlow(true); + + await sut.EnrichAsync(ctx, new[] { source }, schemaProvider); + + Assert.Equal(StatusColors.Draft, source.StatusColor); + } + + [Fact] + public async Task Should_enrich_content_with_can_update() + { + var source = new ContentEntity { SchemaId = schemaId }; + + A.CallTo(() => contentWorkflow.CanUpdateAsync(source, requestContext.User)) + .Returns(true); + + var ctx = requestContext.WithResolveFlow(true); + + await sut.EnrichAsync(ctx, new[] { source }, schemaProvider); + + Assert.True(source.CanUpdate); + } + + [Fact] + public async Task Should_not_enrich_content_with_can_update_if_disabled_in_context() + { + requestContext.WithResolveFlow(false); + + var source = new ContentEntity { SchemaId = schemaId }; + + var ctx = requestContext.WithResolveFlow(false); + + await sut.EnrichAsync(ctx, new[] { source }, schemaProvider); + + Assert.False(source.CanUpdate); + + A.CallTo(() => contentWorkflow.CanUpdateAsync(source, requestContext.User)) + .MustNotHaveHappened(); + } + + private ContentEntity PublishedContent() + { + return new ContentEntity { Status = Status.Published, SchemaId = schemaId }; + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherAssetsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveAssetsTests.cs similarity index 77% rename from backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherAssetsTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveAssetsTests.cs index fb57307e2..326d867a7 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherAssetsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveAssetsTests.cs @@ -15,6 +15,8 @@ using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Contents.Queries.Steps; +using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; @@ -22,7 +24,7 @@ using Xunit; namespace Squidex.Domain.Apps.Entities.Contents.Queries { - public class ContentEnricherAssetsTests + public class ResolveAssetsTests { private readonly IContentWorkflow contentWorkflow = A.Fake(); private readonly IContentQueryService contentQuery = A.Fake(); @@ -30,10 +32,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries private readonly IAssetUrlGenerator assetUrlGenerator = A.Fake(); private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); + private readonly ProvideSchema schemaProvider; private readonly Context requestContext; - private readonly ContentEnricher sut; + private readonly ResolveAssets sut; - public ContentEnricherAssetsTests() + public ResolveAssetsTests() { requestContext = new Context(Mocks.FrontendUser(), Mocks.App(appId, Language.DE)); @@ -56,17 +59,19 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries A.CallTo(() => assetUrlGenerator.GenerateUrl(A.Ignored)) .ReturnsLazily(new Func(id => $"url/to/{id}")); - void SetupSchema(NamedId id, Schema def) + schemaProvider = x => { - var schemaEntity = Mocks.Schema(appId, id, def); - - A.CallTo(() => contentQuery.GetSchemaOrThrowAsync(requestContext, id.Id.ToString())) - .Returns(schemaEntity); - } - - SetupSchema(schemaId, schemaDef); + if (x == schemaId.Id) + { + return Task.FromResult(Mocks.Schema(appId, schemaId, schemaDef)); + } + else + { + throw new DomainObjectNotFoundException(x.ToString(), typeof(ISchemaEntity)); + } + }; - sut = new ContentEnricher(assetQuery, assetUrlGenerator, new Lazy(() => contentQuery), contentWorkflow); + sut = new ResolveAssets(assetUrlGenerator, assetQuery); } [Fact] @@ -78,7 +83,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries var document1 = CreateAsset(Guid.NewGuid(), 3, AssetType.Unknown); var document2 = CreateAsset(Guid.NewGuid(), 4, AssetType.Unknown); - var source = new IContentEntity[] + var source = new[] { CreateContent( new[] { document1.Id, image1.Id }, @@ -91,14 +96,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries A.CallTo(() => assetQuery.QueryAsync(A.That.Matches(x => x.IsNoAssetEnrichment()), null, A.That.Matches(x => x.Ids.Count == 4))) .Returns(ResultList.CreateFrom(4, image1, image2, document1, document2)); - var enriched = await sut.EnrichAsync(source, requestContext); + await sut.EnrichAsync(requestContext, source, schemaProvider); - var enriched1 = enriched.ElementAt(0); + var enriched1 = source[0]; Assert.Contains(image1.Id, enriched1.CacheDependencies); Assert.Contains(image1.Version, enriched1.CacheDependencies); - var enriched2 = enriched.ElementAt(1); + var enriched2 = source[1]; Assert.Contains(image2.Id, enriched2.CacheDependencies); Assert.Contains(image2.Version, enriched2.CacheDependencies); @@ -113,7 +118,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries var document1 = CreateAsset(Guid.NewGuid(), 3, AssetType.Unknown); var document2 = CreateAsset(Guid.NewGuid(), 4, AssetType.Unknown); - var source = new IContentEntity[] + var source = new[] { CreateContent( new[] { document1.Id, image1.Id }, @@ -126,7 +131,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries A.CallTo(() => assetQuery.QueryAsync(A.That.Matches(x => x.IsNoAssetEnrichment()), null, A.That.Matches(x => x.Ids.Count == 4))) .Returns(ResultList.CreateFrom(4, image1, image2, document1, document2)); - var enriched = await sut.EnrichAsync(source, requestContext); + await sut.EnrichAsync(requestContext, source, schemaProvider); Assert.Equal( new NamedContentData() @@ -136,7 +141,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries $"url/to/{image1.Id}")) .AddField("asset2", new ContentFieldData()), - enriched.ElementAt(0).ReferenceData); + source[0].ReferenceData); Assert.Equal( new NamedContentData() @@ -146,20 +151,22 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries new ContentFieldData() .AddValue("en", $"url/to/{image2.Id}")), - enriched.ElementAt(1).ReferenceData); + source[1].ReferenceData); } [Fact] public async Task Should_not_enrich_references_if_not_api_user() { - var source = new IContentEntity[] + var source = new[] { CreateContent(new Guid[] { Guid.NewGuid() }, new Guid[0]) }; - var enriched = await sut.EnrichAsync(source, new Context(Mocks.ApiUser(), Mocks.App(appId))); + var ctx = new Context(Mocks.ApiUser(), Mocks.App(appId)); + + await sut.EnrichAsync(ctx, source, schemaProvider); - Assert.Null(enriched.ElementAt(0).ReferenceData); + Assert.Null(source[0].ReferenceData); A.CallTo(() => assetQuery.QueryAsync(A.Ignored, null, A.Ignored)) .MustNotHaveHappened(); @@ -168,14 +175,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries [Fact] public async Task Should_not_enrich_references_if_disabled() { - var source = new IContentEntity[] + var source = new[] { CreateContent(new Guid[] { Guid.NewGuid() }, new Guid[0]) }; - var enriched = await sut.EnrichAsync(source, new Context(Mocks.ApiUser(), Mocks.App(appId))); + var ctx = new Context(Mocks.FrontendUser(), Mocks.App(appId)).WithoutContentEnrichment(true); - Assert.Null(enriched.ElementAt(0).ReferenceData); + await sut.EnrichAsync(ctx, source, schemaProvider); + + Assert.Null(source[0].ReferenceData); A.CallTo(() => assetQuery.QueryAsync(A.Ignored, null, A.Ignored)) .MustNotHaveHappened(); @@ -184,31 +193,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries [Fact] public async Task Should_not_invoke_query_service_if_no_assets_found() { - var source = new IContentEntity[] + var source = new[] { CreateContent(new Guid[0], new Guid[0]) }; - var enriched = await sut.EnrichAsync(source, requestContext); - - Assert.NotNull(enriched.ElementAt(0).ReferenceData); - - A.CallTo(() => assetQuery.QueryAsync(A.Ignored, null, A.Ignored)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_not_invoke_query_service_if_nothing_to_enrich() - { - var source = new IContentEntity[0]; + await sut.EnrichAsync(requestContext, source, schemaProvider); - await sut.EnrichAsync(source, requestContext); + Assert.NotNull(source[0].ReferenceData); A.CallTo(() => assetQuery.QueryAsync(A.Ignored, null, A.Ignored)) .MustNotHaveHappened(); } - private IEnrichedContentEntity CreateContent(Guid[] assets1, Guid[] assets2) + private ContentEntity CreateContent(Guid[] assets1, Guid[] assets2) { return new ContentEntity { diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherReferencesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveReferencesTests.cs similarity index 79% rename from backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherReferencesTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveReferencesTests.cs index 5817b98cd..90e5e3235 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherReferencesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveReferencesTests.cs @@ -12,9 +12,9 @@ using System.Threading.Tasks; using FakeItEasy; using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Contents.Queries.Steps; +using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; @@ -22,20 +22,18 @@ using Xunit; namespace Squidex.Domain.Apps.Entities.Contents.Queries { - public class ContentEnricherReferencesTests + public class ResolveReferencesTests { - private readonly IContentWorkflow contentWorkflow = A.Fake(); private readonly IContentQueryService contentQuery = A.Fake(); - private readonly IAssetQueryService assetQuery = A.Fake(); - private readonly IAssetUrlGenerator assetUrlGenerator = A.Fake(); private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); private readonly NamedId refSchemaId1 = NamedId.Of(Guid.NewGuid(), "my-ref1"); private readonly NamedId refSchemaId2 = NamedId.Of(Guid.NewGuid(), "my-ref2"); private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); + private readonly ProvideSchema schemaProvider; private readonly Context requestContext; - private readonly ContentEnricher sut; + private readonly ResolveReferences sut; - public ContentEnricherReferencesTests() + public ResolveReferencesTests() { requestContext = new Context(Mocks.FrontendUser(), Mocks.App(appId, Language.DE)); @@ -65,19 +63,27 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries }) .ConfigureFieldsInLists("ref1", "ref2"); - void SetupSchema(NamedId id, Schema def) + schemaProvider = x => { - var schemaEntity = Mocks.Schema(appId, id, def); - - A.CallTo(() => contentQuery.GetSchemaOrThrowAsync(requestContext, id.Id.ToString())) - .Returns(schemaEntity); - } - - SetupSchema(schemaId, schemaDef); - SetupSchema(refSchemaId1, refSchemaDef); - SetupSchema(refSchemaId2, refSchemaDef); + if (x == schemaId.Id) + { + return Task.FromResult(Mocks.Schema(appId, schemaId, schemaDef)); + } + else if (x == refSchemaId1.Id) + { + return Task.FromResult(Mocks.Schema(appId, refSchemaId1, refSchemaDef)); + } + else if (x == refSchemaId2.Id) + { + return Task.FromResult(Mocks.Schema(appId, refSchemaId2, refSchemaDef)); + } + else + { + throw new DomainObjectNotFoundException(x.ToString(), typeof(ISchemaEntity)); + } + }; - sut = new ContentEnricher(assetQuery, assetUrlGenerator, new Lazy(() => contentQuery), contentWorkflow); + sut = new ResolveReferences(new Lazy(() => contentQuery)); } [Fact] @@ -88,7 +94,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries var ref2_1 = CreateRefContent(Guid.NewGuid(), 3, "ref2_1", 23, refSchemaId2); var ref2_2 = CreateRefContent(Guid.NewGuid(), 4, "ref2_2", 29, refSchemaId2); - var source = new IContentEntity[] + var source = new[] { CreateContent(new[] { ref1_1.Id }, new[] { ref2_1.Id }), CreateContent(new[] { ref1_2.Id }, new[] { ref2_2.Id }) @@ -97,9 +103,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries A.CallTo(() => contentQuery.QueryAsync(A.Ignored, A>.That.Matches(x => x.Count == 4))) .Returns(ResultList.CreateFrom(4, ref1_1, ref1_2, ref2_1, ref2_2)); - var enriched = await sut.EnrichAsync(source, requestContext); + await sut.EnrichAsync(requestContext, source, schemaProvider); - var enriched1 = enriched.ElementAt(0); + var enriched1 = source[0]; Assert.Contains(refSchemaId1.Id, enriched1.CacheDependencies); Assert.Contains(refSchemaId2.Id, enriched1.CacheDependencies); @@ -110,7 +116,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries Assert.Contains(ref2_1.Id, enriched1.CacheDependencies); Assert.Contains(ref2_1.Version, enriched1.CacheDependencies); - var enriched2 = enriched.ElementAt(1); + var enriched2 = source[1]; Assert.Contains(refSchemaId1.Id, enriched2.CacheDependencies); Assert.Contains(refSchemaId2.Id, enriched2.CacheDependencies); @@ -130,7 +136,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries var ref2_1 = CreateRefContent(Guid.NewGuid(), 3, "ref2_1", 23, refSchemaId2); var ref2_2 = CreateRefContent(Guid.NewGuid(), 3, "ref2_2", 29, refSchemaId2); - var source = new IContentEntity[] + var source = new[] { CreateContent(new[] { ref1_1.Id }, new[] { ref2_1.Id }), CreateContent(new[] { ref1_2.Id }, new[] { ref2_2.Id }) @@ -139,7 +145,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries A.CallTo(() => contentQuery.QueryAsync(A.That.Matches(x => x.IsNoEnrichment()), A>.That.Matches(x => x.Count == 4))) .Returns(ResultList.CreateFrom(4, ref1_1, ref1_2, ref2_1, ref2_2)); - var enriched = await sut.EnrichAsync(source, requestContext); + await sut.EnrichAsync(requestContext, source, schemaProvider); Assert.Equal( new NamedContentData() @@ -155,7 +161,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries JsonValue.Object() .Add("en", "ref2_1, 23") .Add("de", "ref2_1, 23"))), - enriched.ElementAt(0).ReferenceData); + source[0].ReferenceData); Assert.Equal( new NamedContentData() @@ -171,7 +177,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries JsonValue.Object() .Add("en", "ref2_2, 29") .Add("de", "ref2_2, 29"))), - enriched.ElementAt(1).ReferenceData); + source[1].ReferenceData); } [Fact] @@ -182,7 +188,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries var ref2_1 = CreateRefContent(Guid.NewGuid(), 3, "ref2_1", 23, refSchemaId2); var ref2_2 = CreateRefContent(Guid.NewGuid(), 4, "ref2_2", 29, refSchemaId2); - var source = new IContentEntity[] + var source = new[] { CreateContent(new[] { ref1_1.Id }, new[] { ref2_1.Id, ref2_2.Id }), CreateContent(new[] { ref1_2.Id }, new[] { ref2_1.Id, ref2_2.Id }) @@ -191,7 +197,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries A.CallTo(() => contentQuery.QueryAsync(A.That.Matches(x => x.IsNoEnrichment()), A>.That.Matches(x => x.Count == 4))) .Returns(ResultList.CreateFrom(4, ref1_1, ref1_2, ref2_1, ref2_2)); - var enriched = await sut.EnrichAsync(source, requestContext); + await sut.EnrichAsync(requestContext, source, schemaProvider); Assert.Equal( new NamedContentData() @@ -207,7 +213,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries JsonValue.Object() .Add("en", "2 Reference(s)") .Add("de", "2 Reference(s)"))), - enriched.ElementAt(0).ReferenceData); + source[0].ReferenceData); Assert.Equal( new NamedContentData() @@ -223,20 +229,22 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries JsonValue.Object() .Add("en", "2 Reference(s)") .Add("de", "2 Reference(s)"))), - enriched.ElementAt(1).ReferenceData); + source[1].ReferenceData); } [Fact] public async Task Should_not_enrich_references_if_not_api_user() { - var source = new IContentEntity[] + var source = new[] { CreateContent(new Guid[] { Guid.NewGuid() }, new Guid[0]) }; - var enriched = await sut.EnrichAsync(source, new Context(Mocks.ApiUser(), Mocks.App(appId))); + var ctx = new Context(Mocks.ApiUser(), Mocks.App(appId)); + + await sut.EnrichAsync(ctx, source, schemaProvider); - Assert.Null(enriched.ElementAt(0).ReferenceData); + Assert.Null(source[0].ReferenceData); A.CallTo(() => contentQuery.QueryAsync(A.Ignored, A>.Ignored)) .MustNotHaveHappened(); @@ -245,14 +253,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries [Fact] public async Task Should_not_enrich_references_if_disabled() { - var source = new IContentEntity[] + var source = new[] { CreateContent(new Guid[] { Guid.NewGuid() }, new Guid[0]) }; - var enriched = await sut.EnrichAsync(source, new Context(Mocks.ApiUser(), Mocks.App(appId))); + var ctx = new Context(Mocks.FrontendUser(), Mocks.App(appId)).WithoutContentEnrichment(true); - Assert.Null(enriched.ElementAt(0).ReferenceData); + await sut.EnrichAsync(ctx, source, schemaProvider); + + Assert.Null(source[0].ReferenceData); A.CallTo(() => contentQuery.QueryAsync(A.Ignored, A>.Ignored)) .MustNotHaveHappened(); @@ -261,31 +271,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries [Fact] public async Task Should_not_invoke_query_service_if_no_references_found() { - var source = new IContentEntity[] + var source = new[] { CreateContent(new Guid[0], new Guid[0]) }; - var enriched = await sut.EnrichAsync(source, requestContext); - - Assert.NotNull(enriched.ElementAt(0).ReferenceData); - - A.CallTo(() => contentQuery.QueryAsync(A.Ignored, A>.Ignored)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_not_invoke_query_service_if_nothing_to_enrich() - { - var source = new IContentEntity[0]; + await sut.EnrichAsync(requestContext, source, schemaProvider); - await sut.EnrichAsync(source, requestContext); + Assert.NotNull(source[0].ReferenceData); A.CallTo(() => contentQuery.QueryAsync(A.Ignored, A>.Ignored)) .MustNotHaveHappened(); } - private IEnrichedContentEntity CreateContent(Guid[] ref1, Guid[] ref2) + private ContentEntity CreateContent(Guid[] ref1, Guid[] ref2) { return new ContentEntity { diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/ContentCleanupTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/ContentCleanupTests.cs new file mode 100644 index 000000000..5957654fc --- /dev/null +++ b/backend/tools/TestSuite/TestSuite.ApiTests/ContentCleanupTests.cs @@ -0,0 +1,83 @@ +// ========================================================================== +// 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.ClientLibrary.Management; +using TestSuite.Fixtures; +using TestSuite.Model; +using Xunit; + +#pragma warning disable SA1300 // Element should begin with upper-case letter +#pragma warning disable SA1507 // Code should not contain multiple blank lines in a row + +namespace TestSuite.ApiTests +{ + public class ContentCleanupTests : IClassFixture + { + public ClientFixture _ { get; } + + public ContentCleanupTests(ClientFixture fixture) + { + _ = fixture; + } + + [Fact] + public async Task Should_cleanup_old_data_from_update_response() + { + var schemaName = $"schema-{DateTime.UtcNow.Ticks}"; + + // STEP 1: Create a schema. + var schema = await _.Schemas.PostSchemaAsync(_.AppName, new CreateSchemaDto + { + Name = schemaName, + Fields = new List + { + new UpsertSchemaFieldDto + { + Name = "number", + Properties = new NumberFieldPropertiesDto + { + IsRequired = true + } + }, + new UpsertSchemaFieldDto + { + Name = "string", + Properties = new StringFieldPropertiesDto + { + IsRequired = false + } + } + }, + IsPublished = true + }); + + var contents = _.ClientManager.GetClient(schemaName); + + // STEP 2: Create a content for this schema. + var data = new TestEntityData { Number = 12, String = "hello" }; + + var content_1 = await contents.CreateAsync(data); + + Assert.Equal(data.String, content_1.DataDraft.String); + + + // STEP 3: Delete a field from schema. + await _.Schemas.DeleteFieldAsync(_.AppName, schema.Name, schema.Fields.ElementAt(1).FieldId); + + + // STEP 4: Make any update. + var content_2 = await contents.ChangeStatusAsync(content_1.Id, "Published"); + + // Should not return deleted field. + Assert.Null(content_2.DataDraft.String); + } + } +} diff --git a/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ContentFixture.cs b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ContentFixture.cs index 5572478fa..a1ce1d9e5 100644 --- a/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ContentFixture.cs +++ b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ContentFixture.cs @@ -36,9 +36,7 @@ namespace TestSuite.Fixtures { try { - var schemas = ClientManager.CreateSchemasClient(); - - await schemas.PostSchemaAsync(AppName, new CreateSchemaDto + await Schemas.PostSchemaAsync(AppName, new CreateSchemaDto { Name = SchemaName, Fields = new List