Browse Source

Flat content model for GraphQL. (#442)

pull/445/head
Sebastian Stehle 6 years ago
committed by GitHub
parent
commit
bb1881090a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 16
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/FlatContentData.cs
  2. 78
      backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverterFlat.cs
  3. 1
      backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs
  4. 57
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataFlatGraphType.cs
  5. 6
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs
  6. 33
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs
  7. 5
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs
  8. 1
      backend/src/Squidex/Config/Domain/CommandsServices.cs
  9. 1
      backend/src/Squidex/Config/Domain/ConfigurationExtensions.cs
  10. 116
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionFlatTests.cs
  11. 99
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs
  12. 1
      backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs

16
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<string, IJsonValue?>
{
}
}

78
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<Language>? languagePreferences = null)
public static Dictionary<string, object?> ToFlatten(this NamedContentData content)
{
Guard.NotNull(languagesConfig);
var result = new Dictionary<string, object?>();
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<string, IJsonValue>();
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<string, object?> ToFlatten(this NamedContentData content)
private static IJsonValue? GetFirst(ContentFieldData? fieldData, string fallback)
{
var result = new Dictionary<string, object?>();
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;
}
}
}

1
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;

57
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<FlatContentData>
{
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<object?> PartitionResolver(ValueResolver valueResolver, string key)
{
return new FuncFieldResolver<object?>(c =>
{
var source = (FlatContentData)c.Source;
if (source.TryGetValue(key, out var value) && value != null)
{
return valueResolver(value, c);
}
else
{
return null;
}
});
}
}
}

6
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<object?> PartitionResolver(ValueResolver valueResolver, string key)
{
return new FuncFieldResolver<object?>(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);
}

33
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<IEnrichedContentEntity, object?> action)
{
return new FuncFieldResolver<IEnrichedContentEntity, object?>(c => action(c.Source));
}
private static IFieldResolver ResolveFlat(Func<IEnrichedContentEntity, NamedContentData?> action)
{
return new FuncFieldResolver<IEnrichedContentEntity, FlatContentData?>(c =>
{
var context = (GraphQLExecutionContext)c.UserContext;
return action(c.Source)?.ToFlatten(context.Context.App.LanguagesConfig.Master.Language);
});
}
}
}

5
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);

1
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;

1
backend/src/Squidex/Config/Domain/ConfigurationExtensions.cs

@ -6,7 +6,6 @@
// ==========================================================================
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
namespace Squidex.Config.Domain
{

116
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<string, object?>
{
@ -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<string, IJsonValue>)data.ToFlatLanguageModel(fallbackConfig, new List<Language> { Language.DE });
var output = source.ToFlatten("it");
var expected = new Dictionary<string, IJsonValue>
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<string, IJsonValue>)data.ToFlatLanguageModel(languagesConfig, new List<Language> { Language.DE, Language.EN });
var expected = new Dictionary<string, IJsonValue>
{
{ "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));

99
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<Q>.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()
{

1
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;

Loading…
Cancel
Save