diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/FlatContentData.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/FlatContentData.cs new file mode 100644 index 000000000..6787d1038 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/FlatContentData.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Core.Contents +{ + public sealed class FlatContentData : Dictionary + { + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverterFlat.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverterFlat.cs index d313a4d5c..a5ac4874e 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverterFlat.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverterFlat.cs @@ -7,71 +7,75 @@ using System.Collections.Generic; using System.Linq; -using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Contents; -using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; namespace Squidex.Domain.Apps.Core.ConvertContent { public static class ContentConverterFlat { - public static object ToFlatLanguageModel(this NamedContentData content, LanguagesConfig languagesConfig, IReadOnlyCollection? languagePreferences = null) + public static Dictionary ToFlatten(this NamedContentData content) { - Guard.NotNull(languagesConfig); + var result = new Dictionary(); - if (languagePreferences == null || languagePreferences.Count == 0) + foreach (var fieldValue in content) { - return content; + result[fieldValue.Key] = GetFirst(fieldValue.Value); } - if (languagePreferences.Count == 1 && languagesConfig.TryGetConfig(languagePreferences.First(), out var languageConfig)) - { - languagePreferences = languagePreferences.Union(languageConfig.LanguageFallbacks).ToList(); - } + return result; + } - var result = new Dictionary(); + public static FlatContentData ToFlatten(this NamedContentData content, string fallback) + { + var result = new FlatContentData(); foreach (var fieldValue in content) { - var fieldData = fieldValue.Value; + result[fieldValue.Key] = GetFirst(fieldValue.Value, fallback); + } - if (fieldData != null) - { - foreach (var language in languagePreferences) - { - if (fieldData.TryGetValue(language, out var value) && value.Type != JsonValueType.Null) - { - result[fieldValue.Key] = value; + return result; + } - break; - } - } - } + private static object? GetFirst(ContentFieldData? fieldData) + { + if (fieldData == null) + { + return null; } - return result; + if (fieldData.Count == 1) + { + return fieldData.Values.First(); + } + + return fieldData; } - public static Dictionary ToFlatten(this NamedContentData content) + private static IJsonValue? GetFirst(ContentFieldData? fieldData, string fallback) { - var result = new Dictionary(); + if (fieldData == null) + { + return null; + } - foreach (var fieldValue in content) + if (fieldData.Count == 1) { - var fieldData = fieldValue.Value; + return fieldData.Values.First(); + } - if (fieldData?.Count == 1) - { - result[fieldValue.Key] = fieldData.Values.First(); - } - else - { - result[fieldValue.Key] = fieldData; - } + if (fieldData.TryGetValue(fallback, out var value)) + { + return value; } - return result; + if (fieldData.Count > 1) + { + return fieldData.Values.First(); + } + + return null; } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs index a5cafe930..e9a7e37d8 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs @@ -7,7 +7,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Text; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Contents; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataFlatGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataFlatGraphType.cs new file mode 100644 index 000000000..78e21c2b2 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataFlatGraphType.cs @@ -0,0 +1,57 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using GraphQL.Resolvers; +using GraphQL.Types; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Schemas; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +{ + public sealed class ContentDataFlatGraphType : ObjectGraphType + { + public ContentDataFlatGraphType(ISchemaEntity schema, string schemaName, string schemaType, IGraphModel model) + { + Name = $"{schemaType}DataFlatDto"; + + foreach (var (field, fieldName, typeName) in schema.SchemaDef.Fields.SafeFields()) + { + var (resolvedType, valueResolver) = model.GetGraphType(schema, field, fieldName); + + if (valueResolver != null) + { + AddField(new FieldType + { + Name = fieldName, + Resolver = PartitionResolver(valueResolver, field.Name), + ResolvedType = resolvedType, + Description = field.RawProperties.Hints + }); + } + } + + Description = $"The structure of the flat {schemaName} data type."; + } + + private static FuncFieldResolver PartitionResolver(ValueResolver valueResolver, string key) + { + return new FuncFieldResolver(c => + { + var source = (FlatContentData)c.Source; + + if (source.TryGetValue(key, out var value) && value != null) + { + return valueResolver(value, c); + } + else + { + return null; + } + }); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs index 97e299469..e675c8cd8 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs @@ -62,14 +62,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types } } - Description = $"The structure of the {schemaName} content type."; + Description = $"The structure of the {schemaName} data type."; } private static FuncFieldResolver PartitionResolver(ValueResolver valueResolver, string key) { return new FuncFieldResolver(c => { - if (((ContentFieldData)c.Source).TryGetValue(key, out var value) && value != null) + var source = (ContentFieldData)c.Source; + + if (source.TryGetValue(key, out var value) && value != null) { return valueResolver(value, c); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs index b0140d917..d6a8ddb3a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs @@ -9,6 +9,8 @@ using System; using System.Linq; using GraphQL.Resolvers; using GraphQL.Types; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Domain.Apps.Entities.Schemas; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types @@ -134,11 +136,42 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types Description = $"The draft data of the {schemaName} content." }); } + + var contentDataTypeFlat = new ContentDataFlatGraphType(schema, schemaName, schemaType, model); + + if (contentDataTypeFlat.Fields.Any()) + { + AddField(new FieldType + { + Name = "flatData", + ResolvedType = new NonNullGraphType(contentDataTypeFlat), + Resolver = ResolveFlat(x => x.Data), + Description = $"The flat data of the {schemaName} content." + }); + + AddField(new FieldType + { + Name = "flatDataDraft", + ResolvedType = contentDataTypeFlat, + Resolver = ResolveFlat(x => x.DataDraft), + Description = $"The flat draft data of the {schemaName} content." + }); + } } private static IFieldResolver Resolve(Func action) { return new FuncFieldResolver(c => action(c.Source)); } + + private static IFieldResolver ResolveFlat(Func action) + { + return new FuncFieldResolver(c => + { + var context = (GraphQLExecutionContext)c.UserContext; + + return action(c.Source)?.ToFlatten(context.Context.App.LanguagesConfig.Master.Language); + }); + } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs index b452e4667..68aa7ec73 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs @@ -23,6 +23,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries private readonly IAssetQueryService assetQuery; private readonly Context context; + public Context Context + { + get { return context; } + } + public QueryExecutionContext(Context context, IAssetQueryService assetQuery, IContentQueryService contentQuery) { Guard.NotNull(assetQuery); diff --git a/backend/src/Squidex/Config/Domain/CommandsServices.cs b/backend/src/Squidex/Config/Domain/CommandsServices.cs index 91526ff5d..36fc54e74 100644 --- a/backend/src/Squidex/Config/Domain/CommandsServices.cs +++ b/backend/src/Squidex/Config/Domain/CommandsServices.cs @@ -17,7 +17,6 @@ using Squidex.Domain.Apps.Entities.Comments; using Squidex.Domain.Apps.Entities.Comments.Commands; using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Rules; -using Squidex.Domain.Apps.Entities.Rules.Commands; using Squidex.Domain.Apps.Entities.Rules.Indexes; using Squidex.Domain.Apps.Entities.Rules.UsageTracking; using Squidex.Domain.Apps.Entities.Schemas; diff --git a/backend/src/Squidex/Config/Domain/ConfigurationExtensions.cs b/backend/src/Squidex/Config/Domain/ConfigurationExtensions.cs index d546c4820..f47aeb43f 100644 --- a/backend/src/Squidex/Config/Domain/ConfigurationExtensions.cs +++ b/backend/src/Squidex/Config/Domain/ConfigurationExtensions.cs @@ -6,7 +6,6 @@ // ========================================================================== using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; namespace Squidex.Config.Domain { diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionFlatTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionFlatTests.cs index bd7eea917..51541e835 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionFlatTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionFlatTests.cs @@ -6,54 +6,37 @@ // ========================================================================== using System.Collections.Generic; -using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; using Xunit; -#pragma warning disable xUnit2013 // Do not use equality check to check for collection size. - namespace Squidex.Domain.Apps.Core.Operations.ConvertContent { public class ContentConversionFlatTests { - private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.EN, Language.DE); - - [Fact] - public void Should_return_original_when_no_language_preferences_defined() - { - var data = - new NamedContentData() - .AddField("field1", - new ContentFieldData() - .AddValue("iv", 1)); - - Assert.Same(data, data.ToFlatLanguageModel(languagesConfig)); - } + private readonly NamedContentData source = + new NamedContentData() + .AddField("field1", + new ContentFieldData() + .AddValue("de", 1) + .AddValue("en", 2)) + .AddField("field2", + new ContentFieldData() + .AddValue("de", JsonValue.Null) + .AddValue("it", 4)) + .AddField("field3", + new ContentFieldData() + .AddValue("en", 6)) + .AddField("field4", + new ContentFieldData() + .AddValue("it", 7)); [Fact] public void Should_return_flatten_value() { - var data = - new NamedContentData() - .AddField("field1", - new ContentFieldData() - .AddValue("de", 1) - .AddValue("en", 2)) - .AddField("field2", - new ContentFieldData() - .AddValue("de", JsonValue.Null) - .AddValue("en", 4)) - .AddField("field3", - new ContentFieldData() - .AddValue("en", 6)) - .AddField("field4", - new ContentFieldData() - .AddValue("it", 7)); - - var output = data.ToFlatten(); + var output = source.ToFlatten(); var expected = new Dictionary { @@ -67,7 +50,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent "field2", new ContentFieldData() .AddValue("de", JsonValue.Null) - .AddValue("en", 4) + .AddValue("it", 4) }, { "field3", JsonValue.Create(6) }, { "field4", JsonValue.Create(7) } @@ -77,69 +60,16 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent } [Fact] - public void Should_return_flat_list_when_single_languages_specified() + public void Should_return_flatten_value_and_always_with_first() { - var data = - new NamedContentData() - .AddField("field1", - new ContentFieldData() - .AddValue("de", 1) - .AddValue("en", 2)) - .AddField("field2", - new ContentFieldData() - .AddValue("de", JsonValue.Null) - .AddValue("en", 4)) - .AddField("field3", - new ContentFieldData() - .AddValue("en", 6)) - .AddField("field4", - new ContentFieldData() - .AddValue("it", 7)); - - var fallbackConfig = - LanguagesConfig.Build( - new LanguageConfig(Language.EN), - new LanguageConfig(Language.DE, false, Language.EN)); - - var output = (Dictionary)data.ToFlatLanguageModel(fallbackConfig, new List { Language.DE }); + var output = source.ToFlatten("it"); - var expected = new Dictionary + var expected = new FlatContentData { { "field1", JsonValue.Create(1) }, { "field2", JsonValue.Create(4) }, - { "field3", JsonValue.Create(6) } - }; - - Assert.True(expected.EqualsDictionary(output)); - } - - [Fact] - public void Should_return_flat_list_when_languages_specified() - { - var data = - new NamedContentData() - .AddField("field1", - new ContentFieldData() - .AddValue("de", 1) - .AddValue("en", 2)) - .AddField("field2", - new ContentFieldData() - .AddValue("de", JsonValue.Null) - .AddValue("en", 4)) - .AddField("field3", - new ContentFieldData() - .AddValue("en", 6)) - .AddField("field4", - new ContentFieldData() - .AddValue("it", 7)); - - var output = (Dictionary)data.ToFlatLanguageModel(languagesConfig, new List { Language.DE, Language.EN }); - - var expected = new Dictionary - { - { "field1", JsonValue.Create(1) }, - { "field2", JsonValue.Create(4) }, - { "field3", JsonValue.Create(6) } + { "field3", JsonValue.Create(6) }, + { "field4", JsonValue.Create(7) } }; Assert.True(expected.EqualsDictionary(output)); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs index fb9005e86..1fba7f791 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs @@ -368,6 +368,105 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL AssertResult(expected, result); } + [Fact] + public async Task Should_return_multiple_flat_contents_when_querying_contents() + { + const string query = @" + query { + queryMySchemaContents(top: 30, skip: 5) { + id + version + created + createdBy + lastModified + lastModifiedBy + status + statusColor + url + flatData { + myString + myNumber + myBoolean + myDatetime + myJson + myGeolocation + myTags + myLocalized + myArray { + nestedNumber + nestedBoolean + } + } + } + }"; + + var content = CreateContent(Guid.NewGuid(), Guid.Empty, Guid.Empty); + + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.Id.ToString(), A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5"))) + .Returns(ResultList.CreateFrom(0, content)); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + queryMySchemaContents = new dynamic[] + { + new + { + id = content.Id, + version = 1, + created = content.Created, + createdBy = "subject:user1", + lastModified = content.LastModified, + lastModifiedBy = "subject:user2", + status = "DRAFT", + statusColor = "red", + url = $"contents/my-schema/{content.Id}", + flatData = new + { + myString = "value", + myNumber = 1, + myBoolean = true, + myDatetime = content.LastModified, + myJson = new + { + value = 1 + }, + myGeolocation = new + { + latitude = 10, + longitude = 20 + }, + myTags = new[] + { + "tag1", + "tag2" + }, + myLocalized = "de-DE", + myArray = new[] + { + new + { + nestedNumber = 10, + nestedBoolean = true + }, + new + { + nestedNumber = 20, + nestedBoolean = false + } + } + } + } + } + } + }; + + AssertResult(expected, result); + } + [Fact] public async Task Should_return_multiple_contents_when_querying_contents() { diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs index 274e21d50..6a01d5463 100644 --- a/backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs +++ b/backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs @@ -14,7 +14,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.Authorization; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Routing; using Squidex.Domain.Apps.Core.Apps;