Browse Source

Fix json schema generation for components. (#790)

* Fix json schema generation for components.

* Simplified.

* Fix OpenAPI.

* Mapping fix.

* Cleanup
pull/793/head
Sebastian Stehle 4 years ago
committed by GitHub
parent
commit
61bb542f09
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 11
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs
  2. 23
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ResolvedComponents.cs
  3. 32
      backend/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs
  4. 148
      backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs
  5. 50
      backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeBuilder.cs
  6. 131
      backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs
  7. 27
      backend/src/Squidex.Domain.Apps.Entities/AppProviderExtensions.cs
  8. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs
  9. 27
      backend/src/Squidex.Domain.Apps.Entities/Contents/Schemas/ContentJsonSchemaBuilder.cs
  10. 5
      backend/src/Squidex.Infrastructure/CollectionExtensions.cs
  11. 94
      backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/Builder.cs
  12. 13
      backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/OperationsBuilder.cs
  13. 19
      backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs
  14. 2
      backend/src/Squidex/Areas/Api/Views/Shared/Docs.cshtml
  15. 13
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs

11
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs

@ -32,14 +32,9 @@ namespace Squidex.Domain.Apps.Core.Schemas
return Enumerable.Empty<IRootField>();
}
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<T>(this T field, bool withHidden = false) where T : IField

23
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<DomainId>? schemaIds)
{
return this.GetOrDefault(schemaId);
var result = (Dictionary<DomainId, Schema>?)null;
if (schemaIds != null)
{
foreach (var schemaId in schemaIds)
{
if (TryGetValue(schemaId, out var schema))
{
result ??= new Dictionary<DomainId, Schema>();
result[schemaId] = schema;
}
}
}
if (result == null)
{
return Empty;
}
return new ResolvedComponents(result);
}
}
}

32
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);
}
}
}

148
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))
{

50
backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/SchemaBuilder.cs → 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);
}

131
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<JsonSchema> schema);
internal sealed class JsonTypeVisitor : IFieldVisitor<JsonSchemaProperty?, JsonTypeVisitor.Args>
{
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<AssetsFieldProperties> field, Args args)
{
return SchemaBuilder.ArrayProperty(SchemaBuilder.String());
return JsonTypeBuilder.ArrayProperty(JsonTypeBuilder.String());
}
public JsonSchemaProperty? Visit(IField<BooleanFieldProperties> field, Args args)
{
return SchemaBuilder.BooleanProperty();
return JsonTypeBuilder.BooleanProperty();
}
public JsonSchemaProperty? Visit(IField<ComponentFieldProperties> 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<ComponentsFieldProperties> 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<DateTimeFieldProperties> field, Args args)
{
return SchemaBuilder.DateTimeProperty();
return JsonTypeBuilder.DateTimeProperty();
}
public JsonSchemaProperty? Visit(IField<GeolocationFieldProperties> 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<JsonFieldProperties> field, Args args)
{
return SchemaBuilder.JsonProperty();
return JsonTypeBuilder.JsonProperty();
}
public JsonSchemaProperty? Visit(IField<NumberFieldProperties> 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<ReferencesFieldProperties> field, Args args)
{
return SchemaBuilder.ArrayProperty(SchemaBuilder.String());
return JsonTypeBuilder.ArrayProperty(JsonTypeBuilder.String());
}
public JsonSchemaProperty? Visit(IField<StringFieldProperties> 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<TagsFieldProperties> field, Args args)
{
return SchemaBuilder.ArrayProperty(SchemaBuilder.String());
return JsonTypeBuilder.ArrayProperty(JsonTypeBuilder.String());
}
public JsonSchemaProperty? Visit(IField<UIFieldProperties> field, Args args)
@ -177,53 +181,50 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
return null;
}
private void BuildComponent(JsonSchema jsonSchema, ReadonlyList<DomainId>? schemaIds, Args args)
private static void BuildComponent(JsonSchema jsonSchema, ReadonlyList<DomainId>? 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<Schema>();
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
{

27
backend/src/Squidex.Domain.Apps.Entities/AppProviderExtensions.cs

@ -26,26 +26,23 @@ namespace Squidex.Domain.Apps.Entities
async Task ResolveWithIdsAsync(IField field, ReadonlyList<DomainId>? 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<DomainId, Schema>();
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<DomainId, Schema>();
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<DomainId, Schema>();
result[schemaId] = resolvedEntity.SchemaDef;
result[schemaId] = resolvedEntity.SchemaDef;
await ResolveSchemaAsync(resolvedEntity);
}
await ResolveSchemaAsync(resolvedEntity);
}
}
}

2
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);
}

27
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;

5
backend/src/Squidex.Infrastructure/CollectionExtensions.cs

@ -88,6 +88,11 @@ namespace Squidex.Infrastructure
return false;
}
public static IEnumerable<TElement> SingleGroups<TKey, TElement>(this IEnumerable<IGrouping<TKey, TElement>> source)
{
return source.Where(x => x.Count() == 1).Select(x => x.First());
}
public static bool SetEquals<T>(this IReadOnlyCollection<T> source, IReadOnlyCollection<T> other)
{
return source.Count == other.Count && source.Intersect(other).Count() == other.Count;

94
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<JsonSchema>(typeof(ChangeStatusDto).ToContextualType(), schemaResolver);
OpenApiDocument = document;
OpenApiSchemaResolver = schemaResolver;
var changeStatusType = typeof(ChangeStatusDto).ToContextualType();
ChangeStatusSchema = schemaGenerator.GenerateWithReference<JsonSchema>(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<JsonSchema> factory)
private JsonSchema RegisterReference(string name, Func<string, JsonSchema> 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

13
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<string>
{
SchemaDisplayName
tag
}
};
var operations = Parent.Document.Paths.GetOrAddNew($"{Path}{path}");
var operations = Parent.OpenApiDocument.Paths.GetOrAddNew($"{Path}{path}");
operations[method] = operation;

19
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());

2
backend/src/Squidex/Areas/Api/Views/Shared/Docs.cshtml

@ -33,7 +33,7 @@
<body>
<div id="redoc-container"></div>
<script src="https://cdn.jsdelivr.net/npm/redoc@2.0.0-rc.23/bundles/redoc.standalone.js"></script>
<script src="https://cdn.jsdelivr.net/npm/redoc@2.0.0-rc.57/bundles/redoc.standalone.js"></script>
<script>
Redoc.init('@Url.Content(Model.Specification)', {
theme: {

13
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs

@ -31,11 +31,11 @@ namespace Squidex.Domain.Apps.Core.Operations.GenerateJsonSchema
}
[Fact]
public void Should_build_json_schema_with_resolver()
public void Should_build_json_dynamic_json_schema()
{
var schemaResolver = new SchemaResolver((name, action) => action());
var languagesConfig = LanguagesConfig.English.Set(Language.DE);
var jsonSchema = schema.BuildDynamicJsonSchema(schemaResolver, ResolvedComponents.Empty);
var jsonSchema = schema.BuildJsonSchemaDynamic(languagesConfig.ToResolver(), ResolvedComponents.Empty);
CheckFields(jsonSchema);
}
@ -45,12 +45,7 @@ namespace Squidex.Domain.Apps.Core.Operations.GenerateJsonSchema
{
var languagesConfig = LanguagesConfig.English.Set(Language.DE);
var schemaResolver = new SchemaResolver((name, action) =>
{
return action();
});
var jsonSchema = schema.BuildFlatJsonSchema(schemaResolver, ResolvedComponents.Empty);
var jsonSchema = schema.BuildJsonSchemaFlat(languagesConfig.ToResolver(), ResolvedComponents.Empty);
CheckFields(jsonSchema);
}

Loading…
Cancel
Save