From 59c4cfd439b7bc68c812212cce803357b664aff0 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Wed, 26 May 2021 17:27:23 +0200 Subject: [PATCH] Make components dynamic in graphql. --- .../Types/Contents/DataFlatGraphType.cs | 30 ++++++++--- .../GraphQL/Types/Contents/DataGraphType.cs | 50 ++++++++++++++++--- .../GraphQL/Types/Contents/FieldVisitor.cs | 4 +- .../GraphQL/Types/Contents/NestedGraphType.cs | 30 ++++++++--- .../GraphQL/Types/Contents/SchemaInfo.cs | 34 ++++++++++--- .../Contents/GraphQL/Types/Extensions.cs | 37 ++++++++++---- .../Contents/GraphQL/GraphQLMutationTests.cs | 4 +- .../Contents/GraphQL/TestContent.cs | 14 +++--- 8 files changed, 152 insertions(+), 51 deletions(-) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/DataFlatGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/DataFlatGraphType.cs index 572ecdb44..a6bba730b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/DataFlatGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/DataFlatGraphType.cs @@ -7,6 +7,7 @@ using GraphQL.Types; using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents { @@ -18,19 +19,34 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents foreach (var fieldInfo in schemaInfo.Fields) { - var (resolvedType, resolver, args) = builder.GetGraphType(fieldInfo); - - if (resolver != null) + if (fieldInfo.Field.RawProperties is ComponentFieldProperties || + fieldInfo.Field.RawProperties is ComponentsFieldProperties) { AddField(new FieldType { - Name = fieldInfo.FieldName, - Arguments = args, - ResolvedType = resolvedType, - Resolver = resolver, + Name = fieldInfo.FieldNameDynamic, + Arguments = ContentActions.Json.Arguments, + ResolvedType = AllTypes.Json, + Resolver = FieldVisitor.JsonPath, Description = fieldInfo.Field.RawProperties.Hints }).WithSourceName(fieldInfo); } + else + { + var (resolvedType, resolver, args) = builder.GetGraphType(fieldInfo); + + if (resolver != null) + { + AddField(new FieldType + { + Name = fieldInfo.FieldName, + Arguments = args, + ResolvedType = resolvedType, + Resolver = resolver, + Description = fieldInfo.Field.RawProperties.Hints + }).WithSourceName(fieldInfo); + } + } } Description = $"The structure of the flat {schemaInfo.DisplayName} data type."; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/DataGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/DataGraphType.cs index defa75356..aa0bc84ee 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/DataGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/DataGraphType.cs @@ -20,9 +20,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents foreach (var fieldInfo in schemaInfo.Fields) { - var (resolvedType, resolver, args) = builder.GetGraphType(fieldInfo); - - if (resolver != null) + if (fieldInfo.Field.RawProperties is ComponentFieldProperties || + fieldInfo.Field.RawProperties is ComponentsFieldProperties) { var fieldGraphType = new ObjectGraphType { @@ -36,22 +35,57 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents fieldGraphType.AddField(new FieldType { Name = partitionKey.EscapePartition(), - Arguments = args, - ResolvedType = resolvedType, - Resolver = resolver, + Arguments = ContentActions.Json.Arguments, + ResolvedType = AllTypes.Json, + Resolver = FieldVisitor.JsonPath, Description = fieldInfo.Field.RawProperties.Hints }).WithSourceName(partitionKey); } - fieldGraphType.Description = $"The structure of the {fieldInfo.DisplayName} field of the {schemaInfo.DisplayName} content type."; + fieldGraphType.Description = $"The dynamic structure of the {fieldInfo.DisplayName} field of the {schemaInfo.DisplayName} content type."; AddField(new FieldType { - Name = fieldInfo.FieldName, + Name = fieldInfo.FieldNameDynamic, ResolvedType = fieldGraphType, Resolver = ContentResolvers.Field }).WithSourceName(fieldInfo); } + else + { + var (resolvedType, resolver, args) = builder.GetGraphType(fieldInfo); + + if (resolver != null) + { + var fieldGraphType = new ObjectGraphType + { + Name = fieldInfo.LocalizedType + }; + + var partitioning = builder.ResolvePartition(((RootField)fieldInfo.Field).Partitioning); + + foreach (var partitionKey in partitioning.AllKeys) + { + fieldGraphType.AddField(new FieldType + { + Name = partitionKey.EscapePartition(), + Arguments = args, + ResolvedType = resolvedType, + Resolver = resolver, + Description = fieldInfo.Field.RawProperties.Hints + }).WithSourceName(partitionKey); + } + + fieldGraphType.Description = $"The structure of the {fieldInfo.DisplayName} field of the {schemaInfo.DisplayName} content type."; + + AddField(new FieldType + { + Name = fieldInfo.FieldName, + ResolvedType = fieldGraphType, + Resolver = ContentResolvers.Field + }).WithSourceName(fieldInfo); + } + } } Description = $"The structure of the {schemaInfo.DisplayName} data type."; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldVisitor.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldVisitor.cs index 47d0085e5..31b147516 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldVisitor.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldVisitor.cs @@ -20,8 +20,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents internal sealed class FieldVisitor : IFieldVisitor<(IGraphType?, IFieldResolver?, QueryArguments?), FieldInfo> { - private static readonly IFieldResolver JsonNoop = CreateValueResolver((value, fieldContext, contex) => value); - private static readonly IFieldResolver JsonPath = CreateValueResolver(ContentActions.Json.Resolver); + public static readonly IFieldResolver JsonNoop = CreateValueResolver((value, fieldContext, contex) => value); + public static readonly IFieldResolver JsonPath = CreateValueResolver(ContentActions.Json.Resolver); private static readonly IFieldResolver JsonBoolean = CreateValueResolver((value, fieldContext, contex) => { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/NestedGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/NestedGraphType.cs index 280878321..2454e8997 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/NestedGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/NestedGraphType.cs @@ -6,6 +6,7 @@ // ========================================================================== using GraphQL.Types; +using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Json.Objects; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents @@ -18,19 +19,34 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents foreach (var nestedFieldInfo in fieldInfo.Fields) { - var (resolvedType, resolver, args) = builder.GetGraphType(nestedFieldInfo); - - if (resolvedType != null && resolver != null) + if (nestedFieldInfo.Field.RawProperties is ComponentFieldProperties || + nestedFieldInfo.Field.RawProperties is ComponentsFieldProperties) { AddField(new FieldType { - Name = nestedFieldInfo.FieldName, - Arguments = args, - ResolvedType = resolvedType, - Resolver = resolver, + Name = nestedFieldInfo.FieldNameDynamic, + Arguments = ContentActions.Json.Arguments, + ResolvedType = AllTypes.Json, + Resolver = FieldVisitor.JsonPath, Description = nestedFieldInfo.Field.RawProperties.Hints }).WithSourceName(nestedFieldInfo); } + else + { + var (resolvedType, resolver, args) = builder.GetGraphType(nestedFieldInfo); + + if (resolvedType != null && resolver != null) + { + AddField(new FieldType + { + Name = nestedFieldInfo.FieldName, + Arguments = args, + ResolvedType = resolvedType, + Resolver = resolver, + Description = nestedFieldInfo.Field.RawProperties.Hints + }).WithSourceName(nestedFieldInfo); + } + } } Description = $"The structure of the {fieldInfo.DisplayName} nested schema."; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/SchemaInfo.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/SchemaInfo.cs index ef326d39a..6c8387a36 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/SchemaInfo.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/SchemaInfo.cs @@ -76,7 +76,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents foreach (var field in schema.SchemaDef.Fields) { - fields.Add(FieldInfo.Build(field, fieldNames[field], names[$"{typeName}Data{field.TypeName()}"], names)); + var fieldName = fieldNames[field]; + + fields.Add(FieldInfo.Build( + field, + fieldName, + fieldNames[fieldName.AsDynamic()], + names[$"{typeName}Data{field.TypeName()}"], + names)); } } @@ -93,6 +100,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents public string FieldName { get; } + public string FieldNameDynamic { get; } + public string DisplayName { get; } public string LocalizedType { get; } @@ -107,12 +116,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents public IReadOnlyList Fields { get; } - private FieldInfo(IField field, string fieldName, string typeName, IReadOnlyList fields, Names names) + private FieldInfo(IField field, string fieldName, string fieldNameDynamic, string typeName, IReadOnlyList fields, Names names) { DisplayName = field.DisplayName(); Field = field; Fields = fields; FieldName = fieldName; + FieldNameDynamic = fieldNameDynamic; LocalizedType = names[$"{typeName}Dto"]; LocalizedInputType = names[$"{typeName}InputDto"]; NestedInputType = names[$"{typeName}ChildInputDto"]; @@ -125,23 +135,30 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents return FieldName; } - internal static FieldInfo Build(IRootField rootField, string fieldName, string typeName, Names names) + internal static FieldInfo Build(IRootField rootField, string fieldName, string fieldNameDynamic, string typeName, Names names) { var fields = EmptyFields; if (rootField is IArrayField arrayField && arrayField.Fields.Count > 0) { - var fieldNames = new Names(); + var nestedNames = new Names(); fields = new List(arrayField.Fields.Count); foreach (var field in arrayField.Fields) { - fields.Add(new FieldInfo(field, fieldNames[field], $"{typeName}{field.TypeName()}", EmptyFields, names)); + var nestedName = nestedNames[field]; + + fields.Add(new FieldInfo(field, + nestedName, + nestedNames[nestedName.AsDynamic()], + $"{typeName}{field.TypeName()}", + EmptyFields, + names)); } } - return new FieldInfo(rootField, fieldName, typeName, fields, names); + return new FieldInfo(rootField, fieldName, fieldNameDynamic, typeName, fields, names); } } @@ -150,9 +167,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents // Reserver names that are used for other GraphQL types. private static readonly HashSet ReservedNames = new HashSet(StringComparer.OrdinalIgnoreCase) { - "Content", "Asset", "AssetResultDto", + "Content", + "EntityCreatedResultDto", "EntitySavedResultDto", "JsonScalar", "JsonPrimitive", @@ -192,7 +210,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents takenNames[name] = ++offset; - // Add + 1 to all offset for backwars compatibility. + // Add + 1 to all offsets for backwards-compatibility. return $"{name}{offset + 1}"; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Extensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Extensions.cs index dc365b027..d8bea1bb4 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Extensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Extensions.cs @@ -11,12 +11,27 @@ using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; using Squidex.Infrastructure.ObjectPool; +using Squidex.Text; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { - internal static class Extensions + public static class Extensions { - public static string BuildODataQuery(this IResolveFieldContext context) + public static string AsDynamic(this string value, bool input = false) + { +#pragma warning disable RECS0015 // If an extension method is called as static method convert it to method syntax + var result = CasingExtensions.ToCamelCase(value); +#pragma warning restore RECS0015 // If an extension method is called as static method convert it to method syntax + + if (!input) + { + result = $"{result}__Dynamic"; + } + + return result; + } + + internal static string BuildODataQuery(this IResolveFieldContext context) { var sb = DefaultPools.StringBuilder.Get(); try @@ -49,37 +64,37 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types } } - public static FieldType WithSourceName(this FieldType field, string value) + internal static FieldType WithSourceName(this FieldType field, string value) { return field.WithMetadata(nameof(SourceName), value); } - public static FieldType WithSourceName(this FieldType field, FieldInfo value) + internal static FieldType WithSourceName(this FieldType field, FieldInfo value) { return field.WithMetadata(nameof(SourceName), value.Field.Name); } - public static string SourceName(this FieldType field) + internal static string SourceName(this FieldType field) { return field.GetMetadata(nameof(SourceName)); } - public static FieldType WithSchemaId(this FieldType field, SchemaInfo value) + internal static FieldType WithSchemaId(this FieldType field, SchemaInfo value) { return field.WithMetadata(nameof(SchemaId), value.Schema.Id.ToString()); } - public static string SchemaId(this FieldType field) + internal static string SchemaId(this FieldType field) { return field.GetMetadata(nameof(SchemaId)); } - public static FieldType WithSchemaNamedId(this FieldType field, SchemaInfo value) + internal static FieldType WithSchemaNamedId(this FieldType field, SchemaInfo value) { return field.WithMetadata(nameof(SchemaNamedId), value.Schema.NamedId()); } - public static NamedId SchemaNamedId(this FieldType field) + internal static NamedId SchemaNamedId(this FieldType field) { return field.GetMetadata>(nameof(SchemaNamedId)); } @@ -91,12 +106,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types return field; } - public static IGraphType Flatten(this QueryArgument type) + internal static IGraphType Flatten(this QueryArgument type) { return type.ResolvedType.Flatten(); } - public static IGraphType Flatten(this IGraphType type) + internal static IGraphType Flatten(this IGraphType type) { if (type is IProvideResolvedType provider) { diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs index 78e90c868..16cbb0207 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs @@ -736,7 +736,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL if (query.Contains("")) { - var data = TestContent.Data(content, schemaRefId1.Id, schemaRefId2.Id); + var data = TestContent.Data(content, true, schemaRefId1.Id, schemaRefId2.Id); var dataJson = JsonConvert.SerializeObject(data, Formatting.Indented); var dataString = Regex.Replace(dataJson, "\"([^\"]+)\":", x => x.Groups[1].Value + ":").Replace(".0", string.Empty); @@ -751,7 +751,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL { var input = new { - data = TestContent.Data(content, schemaRefId1.Id, schemaRefId2.Id) + data = TestContent.Data(content, true, schemaRefId1.Id, schemaRefId2.Id) }; return JObject.FromObject(input).ToInputs(); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestContent.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestContent.cs index 74d509704..67f7f4fb7 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestContent.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestContent.cs @@ -8,8 +8,10 @@ using System.Collections.Generic; using NodaTime; using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; +using Squidex.Text; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL { @@ -69,10 +71,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL myGeolocation { iv } - myComponent { + myComponent__Dynamic { iv } - myComponents { + myComponents__Dynamic { iv } myTags { @@ -246,11 +248,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL status = "DRAFT", statusColor = "red", url = $"contents/my-schema/{content.Id}", - data = Data(content) + data = Data(content, false) }; } - public static object Data(IContentEntity content, DomainId refId = default, DomainId assetId = default) + public static object Data(IContentEntity content, bool input, DomainId refId = default, DomainId assetId = default) { var result = new Dictionary { @@ -305,7 +307,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL longitude = 20 } }, - ["myComponent"] = new + ["myComponent".AsDynamic(input)] = new { iv = new { @@ -314,7 +316,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL value2 = 200 } }, - ["myComponents"] = new + ["myComponents".AsDynamic(input)] = new { iv = new[] {