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. 66
      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. 94
      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?>
{
}
}

66
backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverterFlat.cs

@ -7,71 +7,75 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.ConvertContent namespace Squidex.Domain.Apps.Core.ConvertContent
{ {
public static class ContentConverterFlat 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)) return result;
{
languagePreferences = languagePreferences.Union(languageConfig.LanguageFallbacks).ToList();
} }
var result = new Dictionary<string, IJsonValue>(); public static FlatContentData ToFlatten(this NamedContentData content, string fallback)
{
var result = new FlatContentData();
foreach (var fieldValue in content) foreach (var fieldValue in content)
{ {
var fieldData = fieldValue.Value; result[fieldValue.Key] = GetFirst(fieldValue.Value, fallback);
}
if (fieldData != null) return result;
{ }
foreach (var language in languagePreferences)
private static object? GetFirst(ContentFieldData? fieldData)
{ {
if (fieldData.TryGetValue(language, out var value) && value.Type != JsonValueType.Null) if (fieldData == null)
{ {
result[fieldValue.Key] = value; return null;
break;
}
}
} }
if (fieldData.Count == 1)
{
return fieldData.Values.First();
} }
return result; 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)
foreach (var fieldValue in content)
{ {
var fieldData = fieldValue.Value; return null;
}
if (fieldData?.Count == 1) if (fieldData.Count == 1)
{ {
result[fieldValue.Key] = fieldData.Values.First(); return fieldData.Values.First();
} }
else
if (fieldData.TryGetValue(fallback, out var value))
{ {
result[fieldValue.Key] = fieldData; return value;
} }
if (fieldData.Count > 1)
{
return fieldData.Values.First();
} }
return result; return null;
} }
} }
} }

1
backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs

@ -7,7 +7,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Text; using System.Text;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Contents; 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) private static FuncFieldResolver<object?> PartitionResolver(ValueResolver valueResolver, string key)
{ {
return new FuncFieldResolver<object?>(c => 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); 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 System.Linq;
using GraphQL.Resolvers; using GraphQL.Resolvers;
using GraphQL.Types; using GraphQL.Types;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types 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." 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) private static IFieldResolver Resolve(Func<IEnrichedContentEntity, object?> action)
{ {
return new FuncFieldResolver<IEnrichedContentEntity, object?>(c => action(c.Source)); 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 IAssetQueryService assetQuery;
private readonly Context context; private readonly Context context;
public Context Context
{
get { return context; }
}
public QueryExecutionContext(Context context, IAssetQueryService assetQuery, IContentQueryService contentQuery) public QueryExecutionContext(Context context, IAssetQueryService assetQuery, IContentQueryService contentQuery)
{ {
Guard.NotNull(assetQuery); 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.Comments.Commands;
using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Rules; 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.Indexes;
using Squidex.Domain.Apps.Entities.Rules.UsageTracking; using Squidex.Domain.Apps.Entities.Rules.UsageTracking;
using Squidex.Domain.Apps.Entities.Schemas; 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.Configuration;
using Microsoft.Extensions.Hosting;
namespace Squidex.Config.Domain namespace Squidex.Config.Domain
{ {

94
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionFlatTests.cs

@ -6,37 +6,17 @@
// ========================================================================== // ==========================================================================
using System.Collections.Generic; using System.Collections.Generic;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
using Xunit; using Xunit;
#pragma warning disable xUnit2013 // Do not use equality check to check for collection size.
namespace Squidex.Domain.Apps.Core.Operations.ConvertContent namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
{ {
public class ContentConversionFlatTests public class ContentConversionFlatTests
{ {
private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.EN, Language.DE); private readonly NamedContentData source =
[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));
}
[Fact]
public void Should_return_flatten_value()
{
var data =
new NamedContentData() new NamedContentData()
.AddField("field1", .AddField("field1",
new ContentFieldData() new ContentFieldData()
@ -45,7 +25,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
.AddField("field2", .AddField("field2",
new ContentFieldData() new ContentFieldData()
.AddValue("de", JsonValue.Null) .AddValue("de", JsonValue.Null)
.AddValue("en", 4)) .AddValue("it", 4))
.AddField("field3", .AddField("field3",
new ContentFieldData() new ContentFieldData()
.AddValue("en", 6)) .AddValue("en", 6))
@ -53,7 +33,10 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
new ContentFieldData() new ContentFieldData()
.AddValue("it", 7)); .AddValue("it", 7));
var output = data.ToFlatten(); [Fact]
public void Should_return_flatten_value()
{
var output = source.ToFlatten();
var expected = new Dictionary<string, object?> var expected = new Dictionary<string, object?>
{ {
@ -67,7 +50,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
"field2", "field2",
new ContentFieldData() new ContentFieldData()
.AddValue("de", JsonValue.Null) .AddValue("de", JsonValue.Null)
.AddValue("en", 4) .AddValue("it", 4)
}, },
{ "field3", JsonValue.Create(6) }, { "field3", JsonValue.Create(6) },
{ "field4", JsonValue.Create(7) } { "field4", JsonValue.Create(7) }
@ -77,69 +60,16 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
} }
[Fact] [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 expected = new Dictionary<string, IJsonValue>
{
{ "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 = var output = source.ToFlatten("it");
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 FlatContentData
var expected = new Dictionary<string, IJsonValue>
{ {
{ "field1", JsonValue.Create(1) }, { "field1", JsonValue.Create(1) },
{ "field2", JsonValue.Create(4) }, { "field2", JsonValue.Create(4) },
{ "field3", JsonValue.Create(6) } { "field3", JsonValue.Create(6) },
{ "field4", JsonValue.Create(7) }
}; };
Assert.True(expected.EqualsDictionary(output)); 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); 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] [Fact]
public async Task Should_return_multiple_contents_when_querying_contents() 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.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;

Loading…
Cancel
Save