Browse Source

Support for duplicate names in GraphQL.

pull/370/head
Sebastian 7 years ago
parent
commit
cd8331bc18
  1. 9
      src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs
  2. 57
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs
  3. 41
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Extensions.cs
  4. 31
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs
  5. 253
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs
  6. 22
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs

9
src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs

@ -122,9 +122,14 @@ namespace Squidex.Domain.Apps.Core.Schemas
{ {
Guard.NotNull(field, nameof(field)); Guard.NotNull(field, nameof(field));
if (ByName.ContainsKey(field.Name) || ById.ContainsKey(field.Id)) if (ByName.ContainsKey(field.Name))
{ {
throw new ArgumentException($"A field with name '{field.Name}' and id {field.Id} already exists.", nameof(field)); throw new ArgumentException($"A field with name '{field.Name}' already exists.", nameof(field));
}
if (ById.ContainsKey(field.Id))
{
throw new ArgumentException($"A field with id {field.Id} already exists.", nameof(field));
} }
return Clone(clone => return Clone(clone =>

57
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs

@ -25,18 +25,17 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Name = $"{schemaType}DataDto"; Name = $"{schemaType}DataDto";
foreach (var field in schema.SchemaDef.Fields.ForApi()) foreach (var (field, fieldName, typeName) in schema.SchemaDef.Fields.SafeFields())
{ {
var (resolvedType, valueResolver) = model.GetGraphType(schema, field); var (resolvedType, valueResolver) = model.GetGraphType(schema, field);
if (valueResolver != null) if (valueResolver != null)
{ {
var fieldType = field.TypeName(); var displayName = field.DisplayName();
var fieldName = field.DisplayName();
var fieldGraphType = new ObjectGraphType var fieldGraphType = new ObjectGraphType
{ {
Name = $"{schemaType}Data{fieldType}Dto" Name = $"{schemaType}Data{typeName}Dto"
}; };
var partition = model.ResolvePartition(field.Partitioning); var partition = model.ResolvePartition(field.Partitioning);
@ -45,45 +44,51 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
var key = partitionItem.Key; var key = partitionItem.Key;
var partitionResolver = new FuncFieldResolver<object>(c =>
{
if (((ContentFieldData)c.Source).TryGetValue(key, out var value))
{
return valueResolver(value, c);
}
else
{
return null;
}
});
fieldGraphType.AddField(new FieldType fieldGraphType.AddField(new FieldType
{ {
Name = key.EscapePartition(), Name = key.EscapePartition(),
Resolver = partitionResolver, Resolver = PartitionResolver(valueResolver, key),
ResolvedType = resolvedType, ResolvedType = resolvedType,
Description = field.RawProperties.Hints Description = field.RawProperties.Hints
}); });
} }
fieldGraphType.Description = $"The structure of the {fieldName} field of the {schemaName} content type."; fieldGraphType.Description = $"The structure of the {displayName} field of the {schemaName} content type.";
var fieldResolver = new FuncFieldResolver<NamedContentData, IReadOnlyDictionary<string, IJsonValue>>(c =>
{
return c.Source.GetOrDefault(field.Name);
});
AddField(new FieldType AddField(new FieldType
{ {
Name = field.Name.ToCamelCase(), Name = fieldName,
Resolver = fieldResolver, Resolver = FieldResolver(field),
ResolvedType = fieldGraphType, ResolvedType = fieldGraphType,
Description = $"The {fieldName} field." Description = $"The {displayName} field."
}); });
} }
} }
Description = $"The structure of the {schemaName} content type."; Description = $"The structure of the {schemaName} content type.";
} }
private static FuncFieldResolver<object> PartitionResolver(ValueResolver valueResolver, string key)
{
return new FuncFieldResolver<object>(c =>
{
if (((ContentFieldData)c.Source).TryGetValue(key, out var value))
{
return valueResolver(value, c);
}
else
{
return null;
}
});
}
private static FuncFieldResolver<NamedContentData, IReadOnlyDictionary<string, IJsonValue>> FieldResolver(RootField field)
{
return new FuncFieldResolver<NamedContentData, IReadOnlyDictionary<string, IJsonValue>>(c =>
{
return c.Source.GetOrDefault(field.Name);
});
}
} }
} }

41
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Extensions.cs

@ -0,0 +1,41 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
public static class Extensions
{
public static IEnumerable<(T Field, string Name, string Type)> SafeFields<T>(this IEnumerable<T> fields) where T : IField
{
var allFields =
fields.ForApi()
.Select(f => (Field: f, Name: f.Name.ToCamelCase(), Type: f.TypeName())).GroupBy(x => x.Name)
.Select(g =>
{
return g.Select((f, i) => (f.Field, f.Name.SafeString(i), f.Type.SafeString(i)));
})
.SelectMany(x => x);
return allFields;
}
private static string SafeString(this string value, int index)
{
if (index > 0)
{
return value + (index + 1);
}
return value;
}
}
}

31
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs

@ -25,27 +25,17 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Name = $"{schemaType}{fieldName}ChildDto"; Name = $"{schemaType}{fieldName}ChildDto";
foreach (var nestedField in field.Fields.ForApi()) foreach (var (nestedField, nestedName, _) in field.Fields.SafeFields())
{ {
var fieldInfo = model.GetGraphType(schema, nestedField); var fieldInfo = model.GetGraphType(schema, nestedField);
if (fieldInfo.ResolveType != null) if (fieldInfo.ResolveType != null)
{ {
var resolver = new FuncFieldResolver<object>(c => var resolver = ValueResolver(nestedField, fieldInfo);
{
if (((JsonObject)c.Source).TryGetValue(nestedField.Name, out var value))
{
return fieldInfo.Resolver(value, c);
}
else
{
return fieldInfo;
}
});
AddField(new FieldType AddField(new FieldType
{ {
Name = nestedField.Name.ToCamelCase(), Name = nestedName,
Resolver = resolver, Resolver = resolver,
ResolvedType = fieldInfo.ResolveType, ResolvedType = fieldInfo.ResolveType,
Description = $"The {fieldName}/{nestedField.DisplayName()} nested field." Description = $"The {fieldName}/{nestedField.DisplayName()} nested field."
@ -55,5 +45,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Description = $"The structure of the {schemaName}.{fieldName} nested schema."; Description = $"The structure of the {schemaName}.{fieldName} nested schema.";
} }
private static FuncFieldResolver<object> ValueResolver(NestedField nestedField, (IGraphType ResolveType, ValueResolver Resolver) fieldInfo)
{
return new FuncFieldResolver<object>(c =>
{
if (((JsonObject)c.Source).TryGetValue(nestedField.Name, out var value))
{
return fieldInfo.Resolver(value, c);
}
else
{
return fieldInfo;
}
});
}
} }
} }

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

@ -187,9 +187,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var assetId = Guid.NewGuid(); var assetId = Guid.NewGuid();
var asset = CreateAsset(assetId); var asset = CreateAsset(assetId);
var query = $@" var query = @"
query {{ query {
findAsset(id: ""{assetId}"") {{ findAsset(id: ""<ID>"") {
id id
version version
created created
@ -209,8 +209,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
pixelHeight pixelHeight
tags tags
slug slug
}} }
}}"; }".Replace("<ID>", assetId.ToString());
A.CallTo(() => assetQuery.FindAssetAsync(MatchsAssetContext(), assetId)) A.CallTo(() => assetQuery.FindAssetAsync(MatchsAssetContext(), assetId))
.Returns(asset); .Returns(asset);
@ -372,12 +372,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{ {
new new
{ {
nestedNumber = 1, nestedNumber = 10,
nestedBoolean = true nestedBoolean = true
}, },
new new
{ {
nestedNumber = 2, nestedNumber = 20,
nestedBoolean = false nestedBoolean = false
} }
} }
@ -518,15 +518,86 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
AssertResult(expected, result); AssertResult(expected, result);
} }
[Fact]
public async Task Should_return_single_content_with_duplicate_names()
{
var contentId = Guid.NewGuid();
var content = CreateContent(contentId, Guid.Empty, Guid.Empty);
var query = @"
query {
findMySchemaContent(id: ""<ID>"") {
data {
myNumber {
iv
}
myNumber2 {
iv
}
myArray {
iv {
nestedNumber
nestedNumber2
}
}
}
}
}".Replace("<ID>", contentId.ToString());
A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), schemaId.ToString(), contentId, EtagVersion.Any))
.Returns(content);
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });
var expected = new
{
data = new
{
findMySchemaContent = new
{
data = new
{
myNumber = new
{
iv = 1
},
myNumber2 = new
{
iv = 2
},
myArray = new
{
iv = new[]
{
new
{
nestedNumber = 10,
nestedNumber2 = 11,
},
new
{
nestedNumber = 20,
nestedNumber2 = 21,
}
}
}
}
}
}
};
AssertResult(expected, result);
}
[Fact] [Fact]
public async Task Should_return_single_content_when_finding_content() public async Task Should_return_single_content_when_finding_content()
{ {
var contentId = Guid.NewGuid(); var contentId = Guid.NewGuid();
var content = CreateContent(contentId, Guid.Empty, Guid.Empty); var content = CreateContent(contentId, Guid.Empty, Guid.Empty);
var query = $@" var query = @"
query {{ query {
findMySchemaContent(id: ""{contentId}"") {{ findMySchemaContent(id: ""<ID>"") {
id id
version version
created created
@ -535,34 +606,34 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
lastModifiedBy lastModifiedBy
status status
url url
data {{ data {
myString {{ myString {
de de
}} }
myNumber {{ myNumber {
iv iv
}} }
myBoolean {{ myBoolean {
iv iv
}} }
myDatetime {{ myDatetime {
iv iv
}} }
myJson {{ myJson {
iv iv
}} }
myGeolocation {{ myGeolocation {
iv iv
}} }
myTags {{ myTags {
iv iv
}} }
myLocalized {{ myLocalized {
de_DE de_DE
}} }
}} }
}} }
}}"; }".Replace("<ID>", contentId.ToString());
A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), schemaId.ToString(), contentId, EtagVersion.Any)) A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), schemaId.ToString(), contentId, EtagVersion.Any))
.Returns(content); .Returns(content);
@ -645,19 +716,19 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var contentId = Guid.NewGuid(); var contentId = Guid.NewGuid();
var content = CreateContent(contentId, contentRefId, Guid.Empty); var content = CreateContent(contentId, contentRefId, Guid.Empty);
var query = $@" var query = @"
query {{ query {
findMySchemaContent(id: ""{contentId}"") {{ findMySchemaContent(id: ""<ID>"") {
id id
data {{ data {
myReferences {{ myReferences {
iv {{ iv {
id id
}} }
}} }
}} }
}} }
}}"; }".Replace("<ID>", contentId.ToString());
A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), schemaId.ToString(), contentId, EtagVersion.Any)) A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), schemaId.ToString(), contentId, EtagVersion.Any))
.Returns(content); .Returns(content);
@ -703,19 +774,19 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var contentId = Guid.NewGuid(); var contentId = Guid.NewGuid();
var content = CreateContent(contentId, Guid.Empty, assetRefId); var content = CreateContent(contentId, Guid.Empty, assetRefId);
var query = $@" var query = @"
query {{ query {
findMySchemaContent(id: ""{contentId}"") {{ findMySchemaContent(id: ""<ID>"") {
id id
data {{ data {
myAssets {{ myAssets {
iv {{ iv {
id id
}} }
}} }
}} }
}} }
}}"; }".Replace("<ID>", contentId.ToString());
A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), schemaId.ToString(), contentId, EtagVersion.Any)) A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), schemaId.ToString(), contentId, EtagVersion.Any))
.Returns(content); .Returns(content);
@ -760,18 +831,18 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var asset1 = CreateAsset(assetId1); var asset1 = CreateAsset(assetId1);
var asset2 = CreateAsset(assetId2); var asset2 = CreateAsset(assetId2);
var query1 = $@" var query1 = @"
query {{ query {
findAsset(id: ""{assetId1}"") {{ findAsset(id: ""<ID>"") {
id id
}} }
}}"; }".Replace("<ID>", assetId1.ToString());
var query2 = $@" var query2 = @"
query {{ query {
findAsset(id: ""{assetId2}"") {{ findAsset(id: ""<ID>"") {
id id
}} }
}}"; }".Replace("<ID>", assetId2.ToString());
A.CallTo(() => assetQuery.FindAssetAsync(MatchsAssetContext(), assetId1)) A.CallTo(() => assetQuery.FindAssetAsync(MatchsAssetContext(), assetId1))
.Returns(asset1); .Returns(asset1);
@ -813,9 +884,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var contentId = Guid.NewGuid(); var contentId = Guid.NewGuid();
var content = CreateContent(contentId, Guid.Empty, Guid.Empty, new NamedContentData()); var content = CreateContent(contentId, Guid.Empty, Guid.Empty, new NamedContentData());
var query = $@" var query = @"
query {{ query {
findMySchemaContent(id: ""{contentId}"") {{ findMySchemaContent(id: ""<ID>"") {
id id
version version
created created
@ -823,13 +894,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
lastModified lastModified
lastModifiedBy lastModifiedBy
url url
data {{ data {
myInvalid {{ myInvalid {
iv iv
}} }
}} }
}} }
}}"; }".Replace("<ID>", contentId.ToString());
A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), schemaId.ToString(), contentId, EtagVersion.Any)) A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), schemaId.ToString(), contentId, EtagVersion.Any))
.Returns(content); .Returns(content);
@ -855,19 +926,19 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var contentId = Guid.NewGuid(); var contentId = Guid.NewGuid();
var content = CreateContent(contentId, Guid.Empty, Guid.Empty, null, dataDraft); var content = CreateContent(contentId, Guid.Empty, Guid.Empty, null, dataDraft);
var query = $@" var query = @"
query {{ query {
findMySchemaContent(id: ""{contentId}"") {{ findMySchemaContent(id: ""<ID>"") {
dataDraft {{ dataDraft {
myString {{ myString {
de de
}} }
myNumber {{ myNumber {
iv iv
}} }
}} }
}} }
}}"; }".Replace("<ID>", contentId.ToString());
A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), schemaId.ToString(), contentId, EtagVersion.Any)) A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), schemaId.ToString(), contentId, EtagVersion.Any))
.Returns(content); .Returns(content);
@ -904,16 +975,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var contentId = Guid.NewGuid(); var contentId = Guid.NewGuid();
var content = CreateContent(contentId, Guid.Empty, Guid.Empty, null); var content = CreateContent(contentId, Guid.Empty, Guid.Empty, null);
var query = $@" var query = @"
query {{ query {
findMySchemaContent(id: ""{contentId}"") {{ findMySchemaContent(id: ""<ID>"") {
dataDraft {{ dataDraft {
myString {{ myString {
de de
}} }
}} }
}} }
}}"; }".Replace("<ID>", contentId.ToString());
A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), schemaId.ToString(), contentId, EtagVersion.Any)) A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), schemaId.ToString(), contentId, EtagVersion.Any))
.Returns(content); .Returns(content);

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

@ -58,13 +58,15 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
new StringFieldProperties()) new StringFieldProperties())
.AddNumber(3, "my-number", Partitioning.Invariant, .AddNumber(3, "my-number", Partitioning.Invariant,
new NumberFieldProperties()) new NumberFieldProperties())
.AddAssets(4, "my-assets", Partitioning.Invariant, .AddNumber(4, "my_number", Partitioning.Invariant,
new NumberFieldProperties())
.AddAssets(5, "my-assets", Partitioning.Invariant,
new AssetsFieldProperties()) new AssetsFieldProperties())
.AddBoolean(5, "my-boolean", Partitioning.Invariant, .AddBoolean(6, "my-boolean", Partitioning.Invariant,
new BooleanFieldProperties()) new BooleanFieldProperties())
.AddDateTime(6, "my-datetime", Partitioning.Invariant, .AddDateTime(7, "my-datetime", Partitioning.Invariant,
new DateTimeFieldProperties()) new DateTimeFieldProperties())
.AddReferences(7, "my-references", Partitioning.Invariant, .AddReferences(8, "my-references", Partitioning.Invariant,
new ReferencesFieldProperties { SchemaId = schemaId }) new ReferencesFieldProperties { SchemaId = schemaId })
.AddReferences(9, "my-invalid", Partitioning.Invariant, .AddReferences(9, "my-invalid", Partitioning.Invariant,
new ReferencesFieldProperties { SchemaId = Guid.NewGuid() }) new ReferencesFieldProperties { SchemaId = Guid.NewGuid() })
@ -76,7 +78,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
new StringFieldProperties()) new StringFieldProperties())
.AddArray(13, "my-array", Partitioning.Invariant, f => f .AddArray(13, "my-array", Partitioning.Invariant, f => f
.AddBoolean(121, "nested-boolean") .AddBoolean(121, "nested-boolean")
.AddNumber(122, "nested-number")) .AddNumber(122, "nested-number")
.AddNumber(123, "nested_number"))
.ConfigureScripts(new SchemaScripts { Query = "<query-script>" }) .ConfigureScripts(new SchemaScripts { Query = "<query-script>" })
.Publish(); .Publish();
@ -111,6 +114,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
.AddField("my-number", .AddField("my-number",
new ContentFieldData() new ContentFieldData()
.AddValue("iv", 1.0)) .AddValue("iv", 1.0))
.AddField("my_number",
new ContentFieldData()
.AddValue("iv", 2.0))
.AddField("my-boolean", .AddField("my-boolean",
new ContentFieldData() new ContentFieldData()
.AddValue("iv", true)) .AddValue("iv", true))
@ -137,10 +143,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
.AddValue("iv", JsonValue.Array( .AddValue("iv", JsonValue.Array(
JsonValue.Object() JsonValue.Object()
.Add("nested-boolean", true) .Add("nested-boolean", true)
.Add("nested-number", 1), .Add("nested-number", 10)
.Add("nested_number", 11),
JsonValue.Object() JsonValue.Object()
.Add("nested-boolean", false) .Add("nested-boolean", false)
.Add("nested-number", 2)))); .Add("nested-number", 20)
.Add("nested_number", 21))));
var content = new ContentEntity var content = new ContentEntity
{ {

Loading…
Cancel
Save