diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Builder.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Builder.cs index 68f2c53d5..7022aec95 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Builder.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Builder.cs @@ -24,6 +24,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types private readonly Dictionary componentTypes = new Dictionary(ReferenceEqualityComparer.Instance); private readonly Dictionary contentTypes = new Dictionary(ReferenceEqualityComparer.Instance); private readonly Dictionary contentResultTypes = new Dictionary(ReferenceEqualityComparer.Instance); + private readonly Dictionary enumTypes = new Dictionary(); private readonly FieldVisitor fieldVisitor; private readonly FieldInputVisitor fieldInputVisitor; private readonly PartitionResolver partitionResolver; @@ -150,6 +151,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types return componentTypes.GetOrDefault(schema); } + public EnumerationGraphType GetEnumeration(string name, string prefix, IEnumerable values) + { + return enumTypes.GetOrAdd(name, x => new FieldEnumType(name, prefix, values)); + } + public IEnumerable> GetAllContentTypes() { return contentTypes; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldEnumType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldEnumType.cs new file mode 100644 index 000000000..3ed6f0da3 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldEnumType.cs @@ -0,0 +1,46 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using GraphQL.Types; +using Squidex.Text; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents +{ + public sealed class FieldEnumType : EnumerationGraphType + { + public FieldEnumType(string name, string prefix, IEnumerable values) + { + Name = name; + + var index = 0; + + foreach (var value in values) + { + AddValue(BuildName(value, prefix, index), null, value); + + index++; + } + } + + private static string BuildName(T value, string prefix, int index) + { + var name = value!.ToString()!.Slugify().ToPascalCase(); + + if (string.IsNullOrEmpty(name)) + { + name = $"{prefix}_{index}"; + } + + if (!char.IsLetter(name[0])) + { + name = $"{prefix}_{name}"; + } + + return name; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldInputVisitor.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldInputVisitor.cs index 91c542449..49f933377 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldInputVisitor.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldInputVisitor.cs @@ -69,23 +69,40 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents return AllTypes.Json; } - public IGraphType? Visit(IField field, FieldInfo args) + public IGraphType? Visit(IField field, FieldInfo args) { - return AllTypes.Float; + return AllTypes.Strings; } - public IGraphType? Visit(IField field, FieldInfo args) + public IGraphType? Visit(IField field, FieldInfo args) { - return AllTypes.Strings; + if (field.Properties?.AllowedValues?.Count > 0) + { + return builder.GetEnumeration(args.EnumName, "Number", field.Properties.AllowedValues); + } + + return AllTypes.Float; } public IGraphType? Visit(IField field, FieldInfo args) { + if (field.Properties?.AllowedValues?.Count > 0) + { + return builder.GetEnumeration(args.EnumName, "String", field.Properties.AllowedValues); + } + return AllTypes.String; } public IGraphType? Visit(IField field, FieldInfo args) { + if (field.Properties?.AllowedValues?.Count > 0) + { + var @enum = builder.GetEnumeration(args.EnumName, "Tag", field.Properties.AllowedValues); + + return new ListGraphType(new NonNullGraphType(@enum)); + } + return AllTypes.Strings; } 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 dd303dc26..698536671 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 @@ -164,17 +164,40 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents public (IGraphType?, IFieldResolver?, QueryArguments?) Visit(IField field, FieldInfo args) { - return (AllTypes.Float, JsonNumber, null); + var type = AllTypes.Float; + + if (field.Properties?.AllowedValues?.Count > 0) + { + type = builder.GetEnumeration(args.EnumName, "Number", field.Properties.AllowedValues); + } + + return (type, JsonNumber, null); } public (IGraphType?, IFieldResolver?, QueryArguments?) Visit(IField field, FieldInfo args) { - return (AllTypes.String, JsonString, null); + var type = AllTypes.String; + + if (field.Properties?.AllowedValues?.Count > 0) + { + type = builder.GetEnumeration(args.EnumName, "String", field.Properties.AllowedValues); + } + + return (type, JsonString, null); } public (IGraphType?, IFieldResolver?, QueryArguments?) Visit(IField field, FieldInfo args) { - return (AllTypes.Strings, JsonStrings, null); + var type = AllTypes.Strings; + + if (field.Properties?.AllowedValues?.Count > 0) + { + var @enum = builder.GetEnumeration(args.EnumName, "Tag", field.Properties.AllowedValues); + + type = new ListGraphType(new NonNullGraphType(@enum)); + } + + return (type, JsonStrings, null); } public (IGraphType?, IFieldResolver?, QueryArguments?) Visit(IField field, FieldInfo args) 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 48098e28c..366a03571 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 @@ -92,6 +92,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents public string DisplayName { get; } + public string EnumName { get; } + public string LocalizedType { get; } public string LocalizedInputType { get; } @@ -116,8 +118,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents Fields = fields; FieldName = fieldName; FieldNameDynamic = names[$"{fieldName}__Dynamic"]; - LocalizedType = names[$"{typeName}Dto"]; + EnumName = names[$"{fieldName}Enum"]; LocalizedInputType = names[$"{typeName}InputDto"]; + LocalizedType = names[$"{typeName}Dto"]; NestedInputType = names[$"{typeName}ChildInputDto"]; NestedType = names[$"{typeName}ChildDto"]; ReferenceType = names[$"{typeName}UnionDto"]; diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/EnvelopeExtensions.cs b/backend/src/Squidex.Infrastructure/EventSourcing/EnvelopeExtensions.cs index 0d83825cc..e6c03509c 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/EnvelopeExtensions.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/EnvelopeExtensions.cs @@ -117,7 +117,7 @@ namespace Squidex.Infrastructure.EventSourcing public static Guid GetGuid(this EnvelopeHeaders obj, string key) { - if (obj.TryGetValue(key, out var v) && v is JsonString s && Guid.TryParse(v.ToString(), out var guid)) + if (obj.TryGetValue(key, out var v) && v is JsonString s && Guid.TryParse(s.ToString(), out var guid)) { return guid; } diff --git a/backend/src/Squidex.Web/GraphQL/BufferingDocumentWriter.cs b/backend/src/Squidex.Web/GraphQL/BufferingDocumentWriter.cs new file mode 100644 index 000000000..7173ea0d9 --- /dev/null +++ b/backend/src/Squidex.Web/GraphQL/BufferingDocumentWriter.cs @@ -0,0 +1,35 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using GraphQL; +using GraphQL.NewtonsoftJson; +using Microsoft.AspNetCore.WebUtilities; +using Newtonsoft.Json; + +namespace Squidex.Web.GraphQL +{ + public sealed class BufferingDocumentWriter : IDocumentWriter + { + private readonly DocumentWriter documentWriter; + + public BufferingDocumentWriter(Action action) + { + documentWriter = new DocumentWriter(action); + } + + public async Task WriteAsync(Stream stream, T value, + CancellationToken cancellationToken = default) + { + await using (var bufferStream = new FileBufferingWriteStream()) + { + await documentWriter.WriteAsync(bufferStream, value, cancellationToken); + + await bufferStream.DrainBufferAsync(stream, cancellationToken); + } + } + } +} diff --git a/backend/src/Squidex.Web/Squidex.Web.csproj b/backend/src/Squidex.Web/Squidex.Web.csproj index 1f4a8fff5..8cb7249a1 100644 --- a/backend/src/Squidex.Web/Squidex.Web.csproj +++ b/backend/src/Squidex.Web/Squidex.Web.csproj @@ -17,6 +17,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/backend/src/Squidex/Config/Domain/SerializationServices.cs b/backend/src/Squidex/Config/Domain/SerializationServices.cs index 3a0bd7483..466563a38 100644 --- a/backend/src/Squidex/Config/Domain/SerializationServices.cs +++ b/backend/src/Squidex/Config/Domain/SerializationServices.cs @@ -30,6 +30,7 @@ using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Queries.Json; using Squidex.Infrastructure.Reflection; +using Squidex.Web.GraphQL; using IGraphQLBuilder = GraphQL.DI.IGraphQLBuilder; namespace Squidex.Config.Domain @@ -128,9 +129,9 @@ namespace Squidex.Config.Domain { var errorInfoProvider = c.GetRequiredService(); - return new DocumentWriter(options => + return new BufferingDocumentWriter(options => { - options.ContractResolver = new ExecutionResultContractResolver(new ErrorInfoProvider()); + options.ContractResolver = new ExecutionResultContractResolver(errorInfoProvider); options.Converters.Add(new JsonValueConverter()); options.Converters.Add(new WriteonlyGeoJsonConverter()); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestSchemas.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestSchemas.cs index f612e7e9f..d4c95db47 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestSchemas.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestSchemas.cs @@ -45,26 +45,32 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL new StringFieldProperties()) .AddString(3, "my-localized-string", Partitioning.Language, new StringFieldProperties()) - .AddNumber(4, "my-number", Partitioning.Invariant, + .AddString(4, "my-string-enum", Partitioning.Invariant, + new StringFieldProperties { AllowedValues = ReadonlyList.Create("A", "B", "C") }) + .AddNumber(5, "my-number", Partitioning.Invariant, new NumberFieldProperties()) - .AddAssets(5, "my-assets", Partitioning.Invariant, + .AddNumber(6, "my-number-enum", Partitioning.Invariant, + new NumberFieldProperties { AllowedValues = ReadonlyList.Create(1.0, 2.0, 3.0) }) + .AddAssets(7, "my-assets", Partitioning.Invariant, new AssetsFieldProperties()) - .AddBoolean(6, "my-boolean", Partitioning.Invariant, + .AddBoolean(8, "my-boolean", Partitioning.Invariant, new BooleanFieldProperties()) - .AddDateTime(7, "my-datetime", Partitioning.Invariant, + .AddDateTime(9, "my-datetime", Partitioning.Invariant, new DateTimeFieldProperties()) - .AddReferences(8, "my-references", Partitioning.Invariant, + .AddReferences(10, "my-references", Partitioning.Invariant, new ReferencesFieldProperties { SchemaId = Ref1Id.Id }) - .AddReferences(9, "my-union", Partitioning.Invariant, + .AddReferences(11, "my-union", Partitioning.Invariant, new ReferencesFieldProperties()) - .AddGeolocation(10, "my-geolocation", Partitioning.Invariant, + .AddGeolocation(12, "my-geolocation", Partitioning.Invariant, new GeolocationFieldProperties()) - .AddComponent(11, "my-component", Partitioning.Invariant, + .AddComponent(13, "my-component", Partitioning.Invariant, new ComponentFieldProperties { SchemaId = Ref1Id.Id }) - .AddComponents(12, "my-components", Partitioning.Invariant, + .AddComponents(14, "my-components", Partitioning.Invariant, new ComponentsFieldProperties { SchemaIds = ReadonlyList.Create(Ref1.Id, Ref2.Id) }) - .AddTags(13, "my-tags", Partitioning.Invariant, + .AddTags(15, "my-tags", Partitioning.Invariant, new TagsFieldProperties()) + .AddTags(16, "my-tags-enum", Partitioning.Invariant, + new TagsFieldProperties { AllowedValues = ReadonlyList.Create("A", "B", "C") }) .AddArray(100, "my-array", Partitioning.Invariant, f => f .AddBoolean(121, "nested-boolean", new BooleanFieldProperties())