From 61bb542f09ebe8945ddb686a1b395e758b60f9c7 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 16 Nov 2021 17:05:33 +0100 Subject: [PATCH] Fix json schema generation for components. (#790) * Fix json schema generation for components. * Simplified. * Fix OpenAPI. * Mapping fix. * Cleanup --- .../Schemas/FieldExtensions.cs | 11 +- .../Schemas/ResolvedComponents.cs | 23 ++- .../GenerateEdmSchema/EdmSchemaExtensions.cs | 32 ++-- .../JsonSchemaExtensions.cs | 148 ++++++++++++------ .../{SchemaBuilder.cs => JsonTypeBuilder.cs} | 50 ++++-- .../GenerateJsonSchema/JsonTypeVisitor.cs | 131 ++++++++-------- .../AppProviderExtensions.cs | 27 ++-- .../Contents/Queries/ContentQueryParser.cs | 2 +- .../Schemas/ContentJsonSchemaBuilder.cs | 27 ++-- .../CollectionExtensions.cs | 5 + .../Controllers/Contents/Generator/Builder.cs | 94 ++++++----- .../Contents/Generator/OperationsBuilder.cs | 13 +- .../Generator/SchemasOpenApiGenerator.cs | 19 ++- .../Areas/Api/Views/Shared/Docs.cshtml | 2 +- .../GenerateJsonSchema/JsonSchemaTests.cs | 13 +- 15 files changed, 348 insertions(+), 249 deletions(-) rename backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/{SchemaBuilder.cs => JsonTypeBuilder.cs} (72%) diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs index 8db08dc35..6abfa5330 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs @@ -32,14 +32,9 @@ namespace Squidex.Domain.Apps.Core.Schemas return Enumerable.Empty(); } - var allFields = - schemaIds - .Select(x => components.Get(x)).NotNull() - .SelectMany(x => x.Fields.ForApi(withHidden)) - .GroupBy(x => new { x.Name, Type = x.RawProperties.GetType() }).Where(x => x.Count() == 1) - .Select(x => x.First()); - - return allFields; + var allFields = components.Resolve(schemaIds).Values.SelectMany(x => x.Fields.ForApi(withHidden)); + + return allFields.GroupBy(x => new { x.Name, Type = x.RawProperties }).SingleGroups(); } public static bool IsForApi(this T field, bool withHidden = false) where T : IField diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ResolvedComponents.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ResolvedComponents.cs index 6008cf362..8a9afbf8a 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ResolvedComponents.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ResolvedComponents.cs @@ -24,9 +24,28 @@ namespace Squidex.Domain.Apps.Core.Schemas { } - public Schema? Get(DomainId schemaId) + public ResolvedComponents Resolve(IEnumerable? schemaIds) { - return this.GetOrDefault(schemaId); + var result = (Dictionary?)null; + + if (schemaIds != null) + { + foreach (var schemaId in schemaIds) + { + if (TryGetValue(schemaId, out var schema)) + { + result ??= new Dictionary(); + result[schemaId] = schema; + } + } + } + + if (result == null) + { + return Empty; + } + + return new ResolvedComponents(result); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs index 74164e963..e363f5afe 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs @@ -13,27 +13,17 @@ using Squidex.Text; namespace Squidex.Domain.Apps.Core.GenerateEdmSchema { - public delegate (EdmComplexType Type, bool Created) EdmTypeFactory(string names); + public delegate (EdmComplexType Type, bool Created) EdmTypeFactory(string name); public static class EdmSchemaExtensions { - public static string EscapeEdmField(this string field) - { - return field.Replace("-", "_", StringComparison.Ordinal); - } - - public static string UnescapeEdmField(this string field) - { - return field.Replace("_", "-", StringComparison.Ordinal); - } - - public static EdmComplexType BuildEdmType(this Schema schema, bool withHidden, PartitionResolver partitionResolver, EdmTypeFactory typeFactory, + public static EdmComplexType BuildEdmType(this Schema schema, bool withHidden, PartitionResolver partitionResolver, EdmTypeFactory factory, ResolvedComponents components) { - Guard.NotNull(typeFactory, nameof(typeFactory)); + Guard.NotNull(factory, nameof(factory)); Guard.NotNull(partitionResolver, nameof(partitionResolver)); - var (edmType, _) = typeFactory("Data"); + var (edmType, _) = factory("Data"); foreach (var field in schema.FieldsByName.Values) { @@ -42,14 +32,14 @@ namespace Squidex.Domain.Apps.Core.GenerateEdmSchema continue; } - var fieldEdmType = EdmTypeVisitor.BuildType(field, typeFactory, components); + var fieldEdmType = EdmTypeVisitor.BuildType(field, factory, components); if (fieldEdmType == null) { continue; } - var (partitionType, created) = typeFactory($"Data.{field.Name.ToPascalCase()}"); + var (partitionType, created) = factory($"Data.{field.Name.ToPascalCase()}"); if (created) { @@ -66,5 +56,15 @@ namespace Squidex.Domain.Apps.Core.GenerateEdmSchema return edmType; } + + public static string EscapeEdmField(this string field) + { + return field.Replace("-", "_", StringComparison.Ordinal); + } + + public static string UnescapeEdmField(this string field) + { + return field.Replace("_", "-", StringComparison.Ordinal); + } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs index 0f8974a2e..ce9fd25e2 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs @@ -12,51 +12,84 @@ using Squidex.Text; namespace Squidex.Domain.Apps.Core.GenerateJsonSchema { + public delegate (JsonSchema Reference, JsonSchema? Actual) JsonTypeFactory(string name); + public static class JsonSchemaExtensions { - public static JsonSchema BuildFlatJsonSchema(this Schema schema, SchemaResolver schemaResolver, - ResolvedComponents components) + private static readonly JsonTypeFactory DefaultFactory = _ => { - Guard.NotNull(schemaResolver, nameof(schemaResolver)); + var schema = JsonTypeBuilder.Object(); - var schemaName = schema.TypeName(); + return (schema, schema); + }; - var jsonSchema = SchemaBuilder.Object(); + public static JsonSchema BuildJsonSchemaFlat(this Schema schema, PartitionResolver partitionResolver, + ResolvedComponents components, + JsonTypeFactory? factory = null, + bool withHidden = false, + bool withComponents = false) + { + Guard.NotNull(partitionResolver, nameof(partitionResolver)); + Guard.NotNull(components, nameof(components)); - foreach (var field in schema.Fields.ForApi()) - { - var property = JsonTypeVisitor.BuildProperty(field, components); + factory ??= DefaultFactory; + var jsonSchema = JsonTypeBuilder.Object(); + + foreach (var field in schema.Fields.ForApi(withHidden)) + { + var property = + JsonTypeVisitor.BuildProperty( + field, components, schema, + factory, + withHidden, + withComponents); + + // Property is null for UI fields. if (property != null) { - var propertyReference = schemaResolver($"{schemaName}{field.Name.ToPascalCase()}FlatPropertyDto", () => property); + property.SetRequired(false); + property.SetDescription(field); - jsonSchema.Properties.Add(field.Name, CreateProperty(field, propertyReference)); + jsonSchema.Properties.Add(field.Name, property); } } return jsonSchema; } - public static JsonSchema BuildDynamicJsonSchema(this Schema schema, SchemaResolver schemaResolver, - ResolvedComponents components, bool withHidden = false) + public static JsonSchema BuildJsonSchemaDynamic(this Schema schema, PartitionResolver partitionResolver, + ResolvedComponents components, + JsonTypeFactory? factory = null, + bool withHidden = false, + bool withComponents = false) { - Guard.NotNull(schemaResolver, nameof(schemaResolver)); + Guard.NotNull(partitionResolver, nameof(partitionResolver)); + Guard.NotNull(components, nameof(components)); + + factory ??= DefaultFactory; - var jsonSchema = SchemaBuilder.Object(); + var jsonSchema = JsonTypeBuilder.Object(); foreach (var field in schema.Fields.ForApi(withHidden)) { - var propertyItem = JsonTypeVisitor.BuildProperty(field, components, schemaResolver, withHidden); - - if (propertyItem != null) + var property = + JsonTypeVisitor.BuildProperty( + field, components, schema, + factory, + withHidden, + withComponents); + + // Property is null for UI fields. + if (property != null) { - var property = - SchemaBuilder.ObjectProperty(propertyItem) - .SetDescription(field) - .SetRequired(field.RawProperties.IsRequired); + var propertyObj = JsonTypeBuilder.ObjectProperty(property); - jsonSchema.Properties.Add(field.Name, property); + // Property is not required because not all languages might be required. + propertyObj.SetRequired(false); + propertyObj.SetDescription(field); + + jsonSchema.Properties.Add(field.Name, propertyObj); } } @@ -64,55 +97,66 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema } public static JsonSchema BuildJsonSchema(this Schema schema, PartitionResolver partitionResolver, - ResolvedComponents components, bool withHidden = false) + ResolvedComponents components, + JsonTypeFactory? factory = null, + bool withHidden = false, + bool withComponents = false) { Guard.NotNull(partitionResolver, nameof(partitionResolver)); + Guard.NotNull(components, nameof(components)); + + factory ??= DefaultFactory; - var jsonSchema = SchemaBuilder.Object(); + var jsonSchema = JsonTypeBuilder.Object(); foreach (var field in schema.Fields.ForApi(withHidden)) { - var propertyObject = SchemaBuilder.Object(); + var typeName = $"{schema.TypeName()}{field.Name.ToPascalCase()}PropertyDto"; - var partitioning = partitionResolver(field.Partitioning); + // Create a reference to give it a nice name in code generation. + var (reference, actual) = factory(typeName); - foreach (var partitionKey in partitioning.AllKeys) + if (actual != null) { - var propertyItem = JsonTypeVisitor.BuildProperty(field, components, withHiddenFields: withHidden); + var partitioning = partitionResolver(field.Partitioning); - if (propertyItem != null) + foreach (var partitionKey in partitioning.AllKeys) { - var isOptional = partitioning.IsOptional(partitionKey); - - var name = partitioning.GetName(partitionKey); - - propertyItem.SetDescription(name); - propertyItem.SetRequired(field.RawProperties.IsRequired && !isOptional); - - propertyObject.Properties.Add(partitionKey, propertyItem); + var property = + JsonTypeVisitor.BuildProperty( + field, components, schema, + factory, + withHidden, + withComponents); + + // Property is null for UI fields. + if (property != null) + { + var isOptional = partitioning.IsOptional(partitionKey); + + var name = partitioning.GetName(partitionKey); + + // Required if property is required and language/partitioning is not optional. + property.SetRequired(field.RawProperties.IsRequired && !isOptional); + property.SetDescription(name); + + actual.Properties.Add(partitionKey, property); + } } } - if (propertyObject.Properties.Count > 0) - { - jsonSchema.Properties.Add(field.Name, CreateProperty(field, propertyObject)); - } + var propertyReference = + JsonTypeBuilder.ReferenceProperty(reference) + .SetDescription(field) + .SetRequired(field.RawProperties.IsRequired); + + jsonSchema.Properties.Add(field.Name, propertyReference); } return jsonSchema; } - public static JsonSchemaProperty CreateProperty(IField field, JsonSchema reference) - { - var jsonProperty = - SchemaBuilder.ReferenceProperty(reference) - .SetDescription(field) - .SetRequired(field.RawProperties.IsRequired); - - return jsonProperty; - } - - private static JsonSchemaProperty SetDescription(this JsonSchemaProperty jsonProperty, IField field) + public static JsonSchemaProperty SetDescription(this JsonSchemaProperty jsonProperty, IField field) { if (!string.IsNullOrWhiteSpace(field.RawProperties.Hints)) { diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/SchemaBuilder.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeBuilder.cs similarity index 72% rename from backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/SchemaBuilder.cs rename to backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeBuilder.cs index 03089056a..f40e2abc0 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/SchemaBuilder.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeBuilder.cs @@ -9,63 +9,79 @@ using NJsonSchema; namespace Squidex.Domain.Apps.Core.GenerateJsonSchema { - public static class SchemaBuilder + public static class JsonTypeBuilder { public static JsonSchema Object() { - return new JsonSchema { Type = JsonObjectType.Object }; + const JsonObjectType type = JsonObjectType.Object; + + return new JsonSchema { Type = type, AllowAdditionalProperties = false }; } public static JsonSchema String() { - return new JsonSchema { Type = JsonObjectType.String }; - } + const JsonObjectType type = JsonObjectType.String; - public static JsonSchemaProperty ArrayProperty(JsonSchema item, string? description = null, bool isRequired = false) - { - return new JsonSchemaProperty { Type = JsonObjectType.Array, Item = item } - .SetDescription(description) - .SetRequired(isRequired); + return new JsonSchema { Type = type }; } public static JsonSchemaProperty BooleanProperty(string? description = null, bool isRequired = false) { - return new JsonSchemaProperty { Type = JsonObjectType.Boolean } + const JsonObjectType type = JsonObjectType.Boolean; + + return new JsonSchemaProperty { Type = type } .SetDescription(description) .SetRequired(isRequired); } public static JsonSchemaProperty DateTimeProperty(string? description = null, bool isRequired = false) { - return new JsonSchemaProperty { Type = JsonObjectType.String, Format = JsonFormatStrings.DateTime } + const JsonObjectType type = JsonObjectType.String; + + return new JsonSchemaProperty { Type = type, Format = JsonFormatStrings.DateTime } .SetDescription(description) .SetRequired(isRequired); } public static JsonSchemaProperty NumberProperty(string? description = null, bool isRequired = false) { - return new JsonSchemaProperty { Type = JsonObjectType.Number } + const JsonObjectType type = JsonObjectType.Number; + + return new JsonSchemaProperty { Type = type } .SetDescription(description) .SetRequired(isRequired); } public static JsonSchemaProperty StringProperty(string? description = null, bool isRequired = false) { - return new JsonSchemaProperty { Type = JsonObjectType.String } + const JsonObjectType type = JsonObjectType.String; + + return new JsonSchemaProperty { Type = type } .SetDescription(description) .SetRequired(isRequired); } - public static JsonSchemaProperty ReferenceProperty(JsonSchema reference, string? description = null, bool isRequired = false) + public static JsonSchemaProperty ObjectProperty(JsonSchema? value = null, string? description = null, bool isRequired = false) { - return new JsonSchemaProperty { Reference = reference } + const JsonObjectType type = JsonObjectType.Object; + + return new JsonSchemaProperty { Type = type, AdditionalPropertiesSchema = value } .SetDescription(description) .SetRequired(isRequired); } - public static JsonSchemaProperty ObjectProperty(JsonSchema? value = null, string? description = null, bool isRequired = false) + public static JsonSchemaProperty ArrayProperty(JsonSchema item, string? description = null, bool isRequired = false) + { + const JsonObjectType type = JsonObjectType.Array; + + return new JsonSchemaProperty { Type = type, Item = item } + .SetDescription(description) + .SetRequired(isRequired); + } + + public static JsonSchemaProperty ReferenceProperty(JsonSchema reference, string? description = null, bool isRequired = false) { - return new JsonSchemaProperty { Type = JsonObjectType.Object, AdditionalPropertiesSchema = value } + return new JsonSchemaProperty { Reference = reference } .SetDescription(description) .SetRequired(isRequired); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs index ec249e09b..903c4895b 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using System.Collections.ObjectModel; using System.Linq; using NJsonSchema; @@ -14,76 +13,86 @@ using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure; using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Json; +using Squidex.Text; #pragma warning disable SA1313 // Parameter names should begin with lower-case letter namespace Squidex.Domain.Apps.Core.GenerateJsonSchema { - public delegate JsonSchema SchemaResolver(string name, Func schema); - internal sealed class JsonTypeVisitor : IFieldVisitor { private const int MaxDepth = 5; private static readonly JsonTypeVisitor Instance = new JsonTypeVisitor(); - public sealed record Args(ResolvedComponents Components, SchemaResolver? SchemaResolver, bool WithHiddenFields, int Level = 0); + public sealed record Args(ResolvedComponents Components, Schema Schema, + JsonTypeFactory Factory, + bool WithHidden, + bool WithComponents, + int Level = 0); private JsonTypeVisitor() { } - public static JsonSchemaProperty? BuildProperty(IField field, ResolvedComponents components, SchemaResolver? schemaResolver = null, bool withHiddenFields = false) + public static JsonSchemaProperty? BuildProperty(IField field, ResolvedComponents components, Schema schema, + JsonTypeFactory factory, + bool withHidden, + bool withComponents) { - var args = new Args(components, schemaResolver, withHiddenFields, 0); + var args = new Args(components, schema, factory, withHidden, withComponents); return field.Accept(Instance, args); } - public JsonSchemaProperty? Visit(IArrayField field, Args args) + private JsonSchemaProperty? Accept(Args args, NestedField nestedField) { if (args.Level > MaxDepth) { return null; } - var itemSchema = SchemaBuilder.Object(); + return nestedField.Accept(this, args); + } - var nestedArgs = args with { Level = args.Level + 1 }; + public JsonSchemaProperty? Visit(IArrayField field, Args args) + { + // Create a reference to give it a nice name in code generation. + var (reference, actual) = args.Factory($"{args.Schema.TypeName()}{field.Name.ToPascalCase()}ItemDto"); - foreach (var nestedField in field.Fields.ForApi(args.WithHiddenFields)) + if (actual != null) { - var nestedProperty = nestedField.Accept(this, nestedArgs); + var nestedArgs = args with { Level = args.Level + 1 }; - if (nestedProperty != null) + foreach (var nestedField in field.Fields.ForApi(args.WithHidden)) { - nestedProperty.Description = nestedField.RawProperties.Hints; - nestedProperty.SetRequired(nestedField.RawProperties.IsRequired); + var nestedProperty = Accept(nestedArgs, nestedField); + + if (nestedProperty != null) + { + nestedProperty.Description = nestedField.RawProperties.Hints; + nestedProperty.SetRequired(nestedField.RawProperties.IsRequired); - itemSchema.Properties.Add(nestedField.Name, nestedProperty); + actual.Properties.Add(nestedField.Name, nestedProperty); + } } } - return SchemaBuilder.ArrayProperty(itemSchema); + return JsonTypeBuilder.ArrayProperty(reference); } public JsonSchemaProperty? Visit(IField field, Args args) { - return SchemaBuilder.ArrayProperty(SchemaBuilder.String()); + return JsonTypeBuilder.ArrayProperty(JsonTypeBuilder.String()); } public JsonSchemaProperty? Visit(IField field, Args args) { - return SchemaBuilder.BooleanProperty(); + return JsonTypeBuilder.BooleanProperty(); } public JsonSchemaProperty? Visit(IField field, Args args) { - if (args.Level > MaxDepth) - { - return null; - } - - var property = SchemaBuilder.ObjectProperty(); + var property = JsonTypeBuilder.ObjectProperty(); BuildComponent(property, field.Properties.SchemaIds, args); @@ -92,26 +101,21 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema public JsonSchemaProperty? Visit(IField field, Args args) { - if (args.Level > MaxDepth) - { - return null; - } - - var itemSchema = SchemaBuilder.Object(); + var itemSchema = JsonTypeBuilder.Object(); BuildComponent(itemSchema, field.Properties.SchemaIds, args); - return SchemaBuilder.ArrayProperty(itemSchema); + return JsonTypeBuilder.ArrayProperty(itemSchema); } public JsonSchemaProperty? Visit(IField field, Args args) { - return SchemaBuilder.DateTimeProperty(); + return JsonTypeBuilder.DateTimeProperty(); } public JsonSchemaProperty? Visit(IField field, Args args) { - var property = SchemaBuilder.ObjectProperty(); + var property = JsonTypeBuilder.ObjectProperty(); property.Format = GeoJson.Format; @@ -120,12 +124,12 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema public JsonSchemaProperty? Visit(IField field, Args args) { - return SchemaBuilder.JsonProperty(); + return JsonTypeBuilder.JsonProperty(); } public JsonSchemaProperty? Visit(IField field, Args args) { - var property = SchemaBuilder.NumberProperty(); + var property = JsonTypeBuilder.NumberProperty(); if (field.Properties.MinValue != null) { @@ -142,12 +146,12 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema public JsonSchemaProperty? Visit(IField field, Args args) { - return SchemaBuilder.ArrayProperty(SchemaBuilder.String()); + return JsonTypeBuilder.ArrayProperty(JsonTypeBuilder.String()); } public JsonSchemaProperty? Visit(IField field, Args args) { - var property = SchemaBuilder.StringProperty(); + var property = JsonTypeBuilder.StringProperty(); property.MaxLength = field.Properties.MaxLength; property.MinLength = field.Properties.MinLength; @@ -169,7 +173,7 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema public JsonSchemaProperty? Visit(IField field, Args args) { - return SchemaBuilder.ArrayProperty(SchemaBuilder.String()); + return JsonTypeBuilder.ArrayProperty(JsonTypeBuilder.String()); } public JsonSchemaProperty? Visit(IField field, Args args) @@ -177,53 +181,50 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema return null; } - private void BuildComponent(JsonSchema jsonSchema, ReadonlyList? schemaIds, Args args) + private static void BuildComponent(JsonSchema jsonSchema, ReadonlyList? schemaIds, Args args) { - jsonSchema.Properties.Add(Component.Discriminator, SchemaBuilder.StringProperty(isRequired: true)); - - if (args.SchemaResolver != null) + if (args.WithComponents) { - var schemas = schemaIds?.Select(x => args.Components.Get(x)).NotNull() ?? Enumerable.Empty(); - var discriminator = new OpenApiDiscriminator { PropertyName = Component.Discriminator }; - foreach (var schema in schemas) + foreach (var schema in args.Components.Resolve(schemaIds).Values) { - var schemaName = $"{schema.TypeName()}ComponentDto"; + // Create a reference to give it a nice name in code generation. + var (reference, actual) = args.Factory($"{schema.TypeName()}ComponentDto"); - var componentSchema = args.SchemaResolver(schemaName, () => + if (actual != null) { - var nestedArgs = args with { Level = args.Level + 1 }; - - var componentSchema = SchemaBuilder.Object(); - - foreach (var sharedField in schema.Fields.ForApi(nestedArgs.WithHiddenFields)) + foreach (var field in schema.Fields.ForApi(args.WithHidden)) { - var sharedProperty = sharedField.Accept(this, nestedArgs); - - if (sharedProperty != null) + var property = + BuildProperty( + field, + args.Components, + schema, + args.Factory, + args.WithHidden, + args.WithComponents); + + if (property != null) { - sharedProperty.Description = sharedField.RawProperties.Hints; - sharedProperty.SetRequired(sharedField.RawProperties.IsRequired); + property.SetRequired(field.RawProperties.IsRequired); + property.SetDescription(field); - componentSchema.Properties.Add(sharedField.Name, sharedProperty); + actual.Properties.Add(field.Name, property); } } + } - componentSchema.Properties.Add(Component.Discriminator, SchemaBuilder.StringProperty(isRequired: true)); - - return componentSchema; - }); - - jsonSchema.OneOf.Add(componentSchema); + jsonSchema.OneOf.Add(reference); - discriminator.Mapping[schemaName] = componentSchema; + discriminator.Mapping[schema.Name] = reference; } jsonSchema.DiscriminatorObject = discriminator; + jsonSchema.Properties.Add(Component.Discriminator, JsonTypeBuilder.StringProperty(isRequired: true)); } else { diff --git a/backend/src/Squidex.Domain.Apps.Entities/AppProviderExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/AppProviderExtensions.cs index 39b3e198c..ca0fe4ece 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/AppProviderExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/AppProviderExtensions.cs @@ -26,26 +26,23 @@ namespace Squidex.Domain.Apps.Entities async Task ResolveWithIdsAsync(IField field, ReadonlyList? schemaIds) { - if (schemaIds != null) + foreach (var schemaId in schemaIds) { - foreach (var schemaId in schemaIds) + if (schemaId == schema.Id) { - if (schemaId == schema.Id) + result ??= new Dictionary(); + result[schemaId] = schema.SchemaDef; + } + else if (result == null || !result.TryGetValue(schemaId, out _)) + { + var resolvedEntity = await appProvider.GetSchemaAsync(appId, schemaId, false, ct); + + if (resolvedEntity != null) { result ??= new Dictionary(); - result[schemaId] = schema.SchemaDef; - } - else if (result == null || !result.TryGetValue(schemaId, out _)) - { - var resolvedEntity = await appProvider.GetSchemaAsync(appId, schemaId, false, ct); - - if (resolvedEntity != null) - { - result ??= new Dictionary(); - result[schemaId] = resolvedEntity.SchemaDef; + result[schemaId] = resolvedEntity.SchemaDef; - await ResolveSchemaAsync(resolvedEntity); - } + await ResolveSchemaAsync(resolvedEntity); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs index 4b3461d4a..fbdf13af1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs @@ -245,7 +245,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries private static JsonSchema BuildJsonSchema(Schema schema, IAppEntity app, ResolvedComponents components, bool withHiddenFields) { - var dataSchema = schema.BuildJsonSchema(app.PartitionResolver(), components, withHiddenFields); + var dataSchema = schema.BuildJsonSchema(app.PartitionResolver(), components, null, withHiddenFields); return ContentJsonSchemaBuilder.BuildSchema(dataSchema, false, true); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Schemas/ContentJsonSchemaBuilder.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Schemas/ContentJsonSchemaBuilder.cs index e29db2ead..57494684d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Schemas/ContentJsonSchemaBuilder.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Schemas/ContentJsonSchemaBuilder.cs @@ -15,35 +15,36 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema { var jsonSchema = new JsonSchema { + AllowAdditionalProperties = false, Properties = { - ["id"] = SchemaBuilder.StringProperty(FieldDescriptions.EntityId, true), - ["created"] = SchemaBuilder.DateTimeProperty(FieldDescriptions.EntityCreated, true), - ["createdBy"] = SchemaBuilder.StringProperty(FieldDescriptions.EntityCreatedBy, true), - ["lastModified"] = SchemaBuilder.DateTimeProperty(FieldDescriptions.EntityLastModified, true), - ["lastModifiedBy"] = SchemaBuilder.StringProperty(FieldDescriptions.EntityLastModifiedBy, true), - ["newStatus"] = SchemaBuilder.StringProperty(FieldDescriptions.ContentNewStatus), - ["status"] = SchemaBuilder.StringProperty(FieldDescriptions.ContentStatus, true) + ["id"] = JsonTypeBuilder.StringProperty(FieldDescriptions.EntityId, true), + ["created"] = JsonTypeBuilder.DateTimeProperty(FieldDescriptions.EntityCreated, true), + ["createdBy"] = JsonTypeBuilder.StringProperty(FieldDescriptions.EntityCreatedBy, true), + ["lastModified"] = JsonTypeBuilder.DateTimeProperty(FieldDescriptions.EntityLastModified, true), + ["lastModifiedBy"] = JsonTypeBuilder.StringProperty(FieldDescriptions.EntityLastModifiedBy, true), + ["newStatus"] = JsonTypeBuilder.StringProperty(FieldDescriptions.ContentNewStatus), + ["status"] = JsonTypeBuilder.StringProperty(FieldDescriptions.ContentStatus, true) }, Type = JsonObjectType.Object }; if (withDeleted) { - jsonSchema.Properties["isDeleted"] = SchemaBuilder.BooleanProperty(FieldDescriptions.EntityIsDeleted, false); + jsonSchema.Properties["isDeleted"] = JsonTypeBuilder.BooleanProperty(FieldDescriptions.EntityIsDeleted, false); } if (extended) { - jsonSchema.Properties["newStatusColor"] = SchemaBuilder.StringProperty(FieldDescriptions.ContentNewStatusColor, false); - jsonSchema.Properties["schema"] = SchemaBuilder.StringProperty(FieldDescriptions.ContentSchema, true); - jsonSchema.Properties["SchemaName"] = SchemaBuilder.StringProperty(FieldDescriptions.ContentSchemaName, true); - jsonSchema.Properties["statusColor"] = SchemaBuilder.StringProperty(FieldDescriptions.ContentStatusColor, true); + jsonSchema.Properties["newStatusColor"] = JsonTypeBuilder.StringProperty(FieldDescriptions.ContentNewStatusColor, false); + jsonSchema.Properties["schema"] = JsonTypeBuilder.StringProperty(FieldDescriptions.ContentSchema, true); + jsonSchema.Properties["SchemaName"] = JsonTypeBuilder.StringProperty(FieldDescriptions.ContentSchemaName, true); + jsonSchema.Properties["statusColor"] = JsonTypeBuilder.StringProperty(FieldDescriptions.ContentStatusColor, true); } if (dataSchema != null) { - jsonSchema.Properties["data"] = SchemaBuilder.ReferenceProperty(dataSchema, FieldDescriptions.ContentData, true); + jsonSchema.Properties["data"] = JsonTypeBuilder.ReferenceProperty(dataSchema, FieldDescriptions.ContentData, true); } return jsonSchema; diff --git a/backend/src/Squidex.Infrastructure/CollectionExtensions.cs b/backend/src/Squidex.Infrastructure/CollectionExtensions.cs index 617fbfd10..b923093a8 100644 --- a/backend/src/Squidex.Infrastructure/CollectionExtensions.cs +++ b/backend/src/Squidex.Infrastructure/CollectionExtensions.cs @@ -88,6 +88,11 @@ namespace Squidex.Infrastructure return false; } + public static IEnumerable SingleGroups(this IEnumerable> source) + { + return source.Where(x => x.Count() == 1).Select(x => x.First()); + } + public static bool SetEquals(this IReadOnlyCollection source, IReadOnlyCollection other) { return source.Count == other.Count && source.Intersect(other).Count() == other.Count; diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/Builder.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/Builder.cs index 8533e78ca..d1a663806 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/Builder.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/Builder.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Collections.Generic; using Namotion.Reflection; using NJsonSchema; using NSwag; @@ -21,40 +22,42 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator { internal sealed class Builder { - private const string ResultTotal = "total"; - private const string ResultItems = "items"; - public string AppName { get; } public JsonSchema ChangeStatusSchema { get; } - public OpenApiDocument Document { get; } + public OpenApiDocument OpenApiDocument { get; } + + public OpenApiSchemaResolver OpenApiSchemaResolver { get; } internal Builder(IAppEntity app, OpenApiDocument document, OpenApiSchemaResolver schemaResolver, OpenApiSchemaGenerator schemaGenerator) { - Document = document; - AppName = app.Name; - ChangeStatusSchema = schemaGenerator.GenerateWithReference(typeof(ChangeStatusDto).ToContextualType(), schemaResolver); + OpenApiDocument = document; + OpenApiSchemaResolver = schemaResolver; + + var changeStatusType = typeof(ChangeStatusDto).ToContextualType(); + + ChangeStatusSchema = schemaGenerator.GenerateWithReference(changeStatusType, schemaResolver); } public OperationsBuilder Shared() { - var dataSchema = ResolveSchema("DataDto", () => + var dataSchema = RegisterReference("DataDto", _ => { return JsonSchema.CreateAnySchema(); }); - var contentSchema = ResolveSchema("ContentDto", () => + var contentSchema = RegisterReference("ContentDto", _ => { return ContentJsonSchemaBuilder.BuildSchema(dataSchema, true); }); - var contentsSchema = ResolveSchema("ContentResultDto", () => + var contentsSchema = RegisterReference("ContentResultDto", _ => { return BuildResult(contentSchema); }); @@ -73,40 +76,31 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator SchemaTypeName = "__Shared" }; - var description = "API endpoints for operations across all schemas."; - - Document.Tags.Add(new OpenApiTag { Name = "__Shared", Description = description }); + builder.AddTag("API endpoints for operations across all schemas."); return builder; } - public OperationsBuilder Schema(Schema schema, ResolvedComponents components, bool flat) + public OperationsBuilder Schema(Schema schema, PartitionResolver partitionResolver, ResolvedComponents components, bool flat) { var typeName = schema.TypeName(); - var displayName = schema.DisplayName(); - - var dataSchema = ResolveSchema($"{typeName}DataDto", () => + var dataSchema = RegisterReference($"{typeName}DataDto", _ => { - return schema.BuildDynamicJsonSchema(ResolveSchema, components); + return schema.BuildJsonSchemaDynamic(partitionResolver, components, CreateReference, false, true); }); - var contentSchema = ResolveSchema($"{typeName}ContentDto", () => + var flatDataSchema = RegisterReference($"{typeName}FlatDataDto", _ => { - var contentDataSchema = dataSchema; - - if (flat) - { - contentDataSchema = ResolveSchema($"{typeName}FlatDataDto", () => - { - return schema.BuildFlatJsonSchema(ResolveSchema, components); - }); - } + return schema.BuildJsonSchemaFlat(partitionResolver, components, CreateReference, false, true); + }); - return ContentJsonSchemaBuilder.BuildSchema(contentDataSchema, true); + var contentSchema = RegisterReference($"{typeName}ContentDto", _ => + { + return ContentJsonSchemaBuilder.BuildSchema(flat ? flatDataSchema : dataSchema, true); }); - var contentsSchema = ResolveSchema($"{typeName}ContentResultDto", () => + var contentsSchema = RegisterReference($"{typeName}ContentResultDto", _ => { return BuildResult(contentSchema); }); @@ -120,28 +114,52 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator DataSchema = dataSchema, Path = path, Parent = this, - SchemaDisplayName = displayName, + SchemaDisplayName = schema.DisplayName(), SchemaName = schema.Name, SchemaTypeName = typeName }; - var description = builder.FormatText("API endpoints for schema content items."); - - Document.Tags.Add(new OpenApiTag { Name = displayName, Description = description }); + builder.AddTag("API endpoints for [schema] content items."); return builder; } - private JsonSchema ResolveSchema(string name, Func factory) + private JsonSchema RegisterReference(string name, Func creator) { name = char.ToUpperInvariant(name[0]) + name[1..]; + var reference = OpenApiDocument.Definitions.GetOrAdd(name, creator); + return new JsonSchema { - Reference = Document.Definitions.GetOrAdd(name, x => factory()) + Reference = reference }; } + private (JsonSchema, JsonSchema?) CreateReference(string name) + { + name = char.ToUpperInvariant(name[0]) + name[1..]; + + if (OpenApiDocument.Definitions.TryGetValue(name, out var definition)) + { + var reference = new JsonSchema + { + Reference = definition + }; + + return (reference, null); + } + + definition = JsonTypeBuilder.Object(); + + OpenApiDocument.Definitions.Add(name, definition); + + return (new JsonSchema + { + Reference = definition + }, definition); + } + private static JsonSchema BuildResult(JsonSchema contentSchema) { return new JsonSchema @@ -149,9 +167,9 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator AllowAdditionalProperties = false, Properties = { - [ResultTotal] = SchemaBuilder.NumberProperty( + ["total"] = JsonTypeBuilder.NumberProperty( FieldDescriptions.ContentsTotal, true), - [ResultItems] = SchemaBuilder.ArrayProperty(contentSchema, + ["items"] = JsonTypeBuilder.ArrayProperty(contentSchema, FieldDescriptions.ContentsItems, true) }, Type = JsonObjectType.Object diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/OperationsBuilder.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/OperationsBuilder.cs index 8d4ab59dd..3f9219ac0 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/OperationsBuilder.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/OperationsBuilder.cs @@ -36,17 +36,26 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator return text?.Replace("[schema]", $"'{SchemaDisplayName}'", StringComparison.Ordinal); } + public void AddTag(string description) + { + var tag = new OpenApiTag { Name = SchemaTypeName, Description = FormatText(description) }; + + Parent.OpenApiDocument.Tags.Add(tag); + } + public OperationBuilder AddOperation(string method, string path) { + var tag = SchemaTypeName; + var operation = new OpenApiOperation { Tags = new List { - SchemaDisplayName + tag } }; - var operations = Parent.Document.Paths.GetOrAddNew($"{Path}{path}"); + var operations = Parent.OpenApiDocument.Paths.GetOrAddNew($"{Path}{path}"); operations[method] = operation; diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs index dc665f9d6..681a5085b 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs @@ -60,22 +60,21 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator requestCache.AddDependency(schema.UniqueId, schema.Version); } - var builder = new Builder( - app, - document, - schemaResolver, - schemaGenerator); + var builder = new Builder(app, document, schemaResolver, schemaGenerator); - var validSchemas = schemas.Where(x => - x.SchemaDef.IsPublished && - x.SchemaDef.Type != SchemaDefType.Component && - x.SchemaDef.Fields.Count > 0); + var validSchemas = + schemas.Where(x => + x.SchemaDef.IsPublished && + x.SchemaDef.Type != SchemaDefType.Component && + x.SchemaDef.Fields.Count > 0); + + var partitionResolver = app.PartitionResolver(); foreach (var schema in validSchemas) { var components = await appProvider.GetComponentsAsync(schema, httpContext.RequestAborted); - GenerateSchemaOperations(builder.Schema(schema.SchemaDef, components, flat)); + GenerateSchemaOperations(builder.Schema(schema.SchemaDef, partitionResolver, components, true)); } GenerateSharedOperations(builder.Shared()); diff --git a/backend/src/Squidex/Areas/Api/Views/Shared/Docs.cshtml b/backend/src/Squidex/Areas/Api/Views/Shared/Docs.cshtml index d79a549d5..bb953d5cd 100644 --- a/backend/src/Squidex/Areas/Api/Views/Shared/Docs.cshtml +++ b/backend/src/Squidex/Areas/Api/Views/Shared/Docs.cshtml @@ -33,7 +33,7 @@
- +