Browse Source

Merge branch 'master' of github.com:Squidex/squidex

pull/648/head
Sebastian 5 years ago
parent
commit
9114ad9c00
  1. 2
      .github/ISSUE_TEMPLATE/development-task-or-bug.md
  2. 44
      backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/ContentSchemaBuilder.cs
  3. 27
      backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs
  4. 49
      backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs
  5. 66
      backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs
  6. 49
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs
  7. 48
      backend/src/Squidex.Domain.Apps.Entities/Contents/Schemas/ContentJsonSchemaBuilder.cs
  8. 1
      backend/src/Squidex/Areas/Api/Config/OpenApi/CommonProcessor.cs
  9. 7
      backend/src/Squidex/Areas/Api/Config/OpenApi/ErrorDtoProcessor.cs
  10. 29
      backend/src/Squidex/Areas/Api/Config/OpenApi/ODataExtensions.cs
  11. 5
      backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs
  12. 87
      backend/src/Squidex/Areas/Api/Config/OpenApi/QueryExtensions.cs
  13. 18
      backend/src/Squidex/Areas/Api/Config/OpenApi/QueryParamsProcessor.cs
  14. 2
      backend/src/Squidex/Areas/Api/Config/OpenApi/SecurityProcessor.cs
  15. 27
      backend/src/Squidex/Areas/Api/Controllers/Contents/ContentOpenApiController.cs
  16. 131
      backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/Builder.cs
  17. 131
      backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/OperationBuilder.cs
  18. 54
      backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/OperationsBuilder.cs
  19. 241
      backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaOpenApiGenerator.cs
  20. 175
      backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs
  21. 0
      backend/src/Squidex/Docs/schema-body.md
  22. 27
      backend/src/Squidex/Docs/schema-query.md
  23. 11
      backend/src/Squidex/Docs/schemaquery.md
  24. 4
      backend/src/Squidex/Docs/security.md
  25. 48
      backend/src/Squidex/Pipeline/OpenApi/OpenApiHelper.cs
  26. 8
      backend/src/Squidex/Squidex.csproj
  27. 42
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs

2
.github/ISSUE_TEMPLATE/development-task-or-bug.md

@ -9,7 +9,7 @@ assignees: ''
Do NOT USE issues for
* Bug Reports
* Bug reports
* Hosting Questions
* Feature Requests
* anything that is not related to development.

44
backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/ContentSchemaBuilder.cs

@ -1,44 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using NJsonSchema;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
{
public static class ContentSchemaBuilder
{
public static JsonSchema CreateContentSchema(Schema schema, JsonSchema dataSchema)
{
Guard.NotNull(schema, nameof(schema));
Guard.NotNull(dataSchema, nameof(dataSchema));
var schemaName = schema.Properties.Label.Or(schema.Name);
var contentSchema = new JsonSchema
{
Properties =
{
["id"] = SchemaBuilder.StringProperty($"The id of the {schemaName} content.", true),
["data"] = SchemaBuilder.ObjectProperty(dataSchema, $"The data of the {schemaName}.", true),
["dataDraft"] = SchemaBuilder.ObjectProperty(dataSchema, $"The draft data of the {schemaName}."),
["version"] = SchemaBuilder.NumberProperty($"The version of the {schemaName}.", true),
["created"] = SchemaBuilder.DateTimeProperty($"The date and time when the {schemaName} content has been created.", true),
["createdBy"] = SchemaBuilder.StringProperty($"The user that has created the {schemaName} content.", true),
["lastModified"] = SchemaBuilder.DateTimeProperty($"The date and time when the {schemaName} content has been modified last.", true),
["lastModifiedBy"] = SchemaBuilder.StringProperty($"The user that has updated the {schemaName} content last.", true),
["newStatus"] = SchemaBuilder.StringProperty("The new status of the content."),
["status"] = SchemaBuilder.StringProperty("The status of the content.", true)
},
Type = JsonObjectType.Object
};
return contentSchema;
}
}
}

27
backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs

@ -14,12 +14,35 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
{
public static class JsonSchemaExtensions
{
public static JsonSchema BuildFlatJsonSchema(this Schema schema, SchemaResolver schemaResolver, bool withHidden = false)
{
Guard.NotNull(schemaResolver, nameof(schemaResolver));
var schemaName = schema.TypeName();
var jsonSchema = SchemaBuilder.Object();
foreach (var field in schema.Fields.ForApi(withHidden))
{
var property = JsonTypeVisitor.BuildProperty(field, schemaResolver, withHidden);
if (property != null)
{
var propertyReference = schemaResolver($"{schemaName}{field.Name.ToPascalCase()}FlatPropertyDto", () => property);
jsonSchema.Properties.Add(field.Name, CreateProperty(field, propertyReference));
}
}
return jsonSchema;
}
public static JsonSchema BuildJsonSchema(this Schema schema, PartitionResolver partitionResolver, SchemaResolver schemaResolver, bool withHidden = false)
{
Guard.NotNull(schemaResolver, nameof(schemaResolver));
Guard.NotNull(partitionResolver, nameof(partitionResolver));
var schemaName = schema.Name.ToPascalCase();
var schemaName = schema.TypeName();
var jsonSchema = SchemaBuilder.Object();
@ -47,7 +70,7 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
if (partitionObject.Properties.Count > 0)
{
var propertyReference = schemaResolver($"{schemaName}{field.Name.ToPascalCase()}Property", partitionObject);
var propertyReference = schemaResolver($"{schemaName}{field.Name.ToPascalCase()}PropertyDto", () => partitionObject);
jsonSchema.Properties.Add(field.Name, CreateProperty(field, propertyReference));
}

49
backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.ObjectModel;
using NJsonSchema;
using Squidex.Domain.Apps.Core.Schemas;
@ -12,7 +13,7 @@ using Squidex.Infrastructure.Json;
namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
{
public delegate JsonSchema SchemaResolver(string name, JsonSchema schema);
public delegate JsonSchema SchemaResolver(string name, Func<JsonSchema> schema);
internal sealed class JsonTypeVisitor : IFieldVisitor<JsonSchemaProperty?, JsonTypeVisitor.Args>
{
@ -65,9 +66,7 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
public JsonSchemaProperty? Visit(IField<AssetsFieldProperties> field, Args args)
{
var itemSchema = args.SchemaResolver("AssetItem", SchemaBuilder.String());
return SchemaBuilder.ArrayProperty(itemSchema);
return SchemaBuilder.ArrayProperty(SchemaBuilder.String());
}
public JsonSchemaProperty? Visit(IField<BooleanFieldProperties> field, Args args)
@ -82,25 +81,28 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
public JsonSchemaProperty? Visit(IField<GeolocationFieldProperties> field, Args args)
{
var geolocationSchema = SchemaBuilder.Object();
var reference = args.SchemaResolver("GeolocationDto", () =>
{
var geolocationSchema = SchemaBuilder.Object();
geolocationSchema.Format = GeoJson.Format;
geolocationSchema.Format = GeoJson.Format;
geolocationSchema.Properties.Add("latitude", new JsonSchemaProperty
{
Type = JsonObjectType.Number,
Maximum = 90,
Minimum = -90
}.SetRequired(true));
geolocationSchema.Properties.Add("latitude", new JsonSchemaProperty
{
Type = JsonObjectType.Number,
Maximum = 90,
Minimum = -90
}.SetRequired(true));
geolocationSchema.Properties.Add("longitude", new JsonSchemaProperty
{
Type = JsonObjectType.Number,
Maximum = 180,
Minimum = -180
}.SetRequired(true));
geolocationSchema.Properties.Add("longitude", new JsonSchemaProperty
{
Type = JsonObjectType.Number,
Maximum = 180,
Minimum = -180
}.SetRequired(true));
var reference = args.SchemaResolver("GeolocationDto", geolocationSchema);
return geolocationSchema;
});
return SchemaBuilder.ObjectProperty(reference);
}
@ -129,9 +131,7 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
public JsonSchemaProperty? Visit(IField<ReferencesFieldProperties> field, Args args)
{
var itemSchema = args.SchemaResolver("ReferenceItem", SchemaBuilder.String());
return SchemaBuilder.ArrayProperty(itemSchema);
return SchemaBuilder.ArrayProperty(SchemaBuilder.String());
}
public JsonSchemaProperty? Visit(IField<StringFieldProperties> field, Args args)
@ -140,6 +140,7 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
property.MaxLength = field.Properties.MaxLength;
property.MinLength = field.Properties.MinLength;
property.Pattern = field.Properties.Pattern;
if (field.Properties.AllowedValues != null)
@ -157,9 +158,7 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
public JsonSchemaProperty? Visit(IField<TagsFieldProperties> field, Args args)
{
var itemSchema = args.SchemaResolver("ReferenceItem", SchemaBuilder.String());
return SchemaBuilder.ArrayProperty(itemSchema);
return SchemaBuilder.ArrayProperty(SchemaBuilder.String());
}
public JsonSchemaProperty? Visit(IField<UIFieldProperties> field, Args args)

66
backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs

@ -152,22 +152,22 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
schema.Properties[name.ToCamelCase()] = property;
}
AddProperty(nameof(IAssetEntity.Id), JsonObjectType.String);
AddProperty(nameof(IAssetEntity.Created), JsonObjectType.String, JsonFormatStrings.DateTime);
AddProperty(nameof(IAssetEntity.CreatedBy), JsonObjectType.String);
AddProperty(nameof(IAssetEntity.FileHash), JsonObjectType.String);
AddProperty(nameof(IAssetEntity.FileName), JsonObjectType.String);
AddProperty(nameof(IAssetEntity.FileSize), JsonObjectType.Integer);
AddProperty(nameof(IAssetEntity.FileVersion), JsonObjectType.Integer);
AddProperty(nameof(IAssetEntity.IsProtected), JsonObjectType.Boolean);
AddProperty(nameof(IAssetEntity.LastModified), JsonObjectType.String, JsonFormatStrings.DateTime);
AddProperty(nameof(IAssetEntity.LastModifiedBy), JsonObjectType.String);
AddProperty(nameof(IAssetEntity.Metadata), JsonObjectType.None);
AddProperty(nameof(IAssetEntity.MimeType), JsonObjectType.String);
AddProperty(nameof(IAssetEntity.Slug), JsonObjectType.String);
AddProperty(nameof(IAssetEntity.Tags), JsonObjectType.String);
AddProperty(nameof(IAssetEntity.Type), JsonObjectType.String);
AddProperty(nameof(IAssetEntity.Version), JsonObjectType.Integer);
AddProperty("id", JsonObjectType.String);
AddProperty("version", JsonObjectType.Integer);
AddProperty("created", JsonObjectType.String, JsonFormatStrings.DateTime);
AddProperty("createdBy", JsonObjectType.String);
AddProperty("fileHash", JsonObjectType.String);
AddProperty("fileName", JsonObjectType.String);
AddProperty("fileSize", JsonObjectType.Integer);
AddProperty("fileVersion", JsonObjectType.Integer);
AddProperty("isProtected", JsonObjectType.Boolean);
AddProperty("lastModified", JsonObjectType.String, JsonFormatStrings.DateTime);
AddProperty("lastModifiedBy", JsonObjectType.String);
AddProperty("metadata", JsonObjectType.None);
AddProperty("mimeType", JsonObjectType.String);
AddProperty("slug", JsonObjectType.String);
AddProperty("tags", JsonObjectType.String);
AddProperty("type", JsonObjectType.String);
return schema;
}
@ -188,23 +188,23 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
var jsonType = new EdmComplexType("Squidex", "Json", null, false, true);
AddPropertyReference(nameof(IAssetEntity.Metadata), new EdmComplexTypeReference(jsonType, false));
AddProperty(nameof(IAssetEntity.Id), EdmPrimitiveTypeKind.String);
AddProperty(nameof(IAssetEntity.Created), EdmPrimitiveTypeKind.DateTimeOffset);
AddProperty(nameof(IAssetEntity.CreatedBy), EdmPrimitiveTypeKind.String);
AddProperty(nameof(IAssetEntity.FileHash), EdmPrimitiveTypeKind.String);
AddProperty(nameof(IAssetEntity.FileName), EdmPrimitiveTypeKind.String);
AddProperty(nameof(IAssetEntity.IsProtected), EdmPrimitiveTypeKind.Boolean);
AddProperty(nameof(IAssetEntity.FileSize), EdmPrimitiveTypeKind.Int64);
AddProperty(nameof(IAssetEntity.FileVersion), EdmPrimitiveTypeKind.Int64);
AddProperty(nameof(IAssetEntity.LastModified), EdmPrimitiveTypeKind.DateTimeOffset);
AddProperty(nameof(IAssetEntity.LastModifiedBy), EdmPrimitiveTypeKind.String);
AddProperty(nameof(IAssetEntity.MimeType), EdmPrimitiveTypeKind.String);
AddProperty(nameof(IAssetEntity.Slug), EdmPrimitiveTypeKind.String);
AddProperty(nameof(IAssetEntity.Tags), EdmPrimitiveTypeKind.String);
AddProperty(nameof(IAssetEntity.Type), EdmPrimitiveTypeKind.String);
AddProperty(nameof(IAssetEntity.Version), EdmPrimitiveTypeKind.Int64);
AddPropertyReference("Metadata", new EdmComplexTypeReference(jsonType, false));
AddProperty("id", EdmPrimitiveTypeKind.String);
AddProperty("version", EdmPrimitiveTypeKind.Int64);
AddProperty("created", EdmPrimitiveTypeKind.DateTimeOffset);
AddProperty("createdBy", EdmPrimitiveTypeKind.String);
AddProperty("fileHash", EdmPrimitiveTypeKind.String);
AddProperty("fileName", EdmPrimitiveTypeKind.String);
AddProperty("isProtected", EdmPrimitiveTypeKind.Boolean);
AddProperty("fileSize", EdmPrimitiveTypeKind.Int64);
AddProperty("fileVersion", EdmPrimitiveTypeKind.Int64);
AddProperty("lastModified", EdmPrimitiveTypeKind.DateTimeOffset);
AddProperty("lastModifiedBy", EdmPrimitiveTypeKind.String);
AddProperty("mimeType", EdmPrimitiveTypeKind.String);
AddProperty("slug", EdmPrimitiveTypeKind.String);
AddProperty("tags", EdmPrimitiveTypeKind.String);
AddProperty("type", EdmPrimitiveTypeKind.String);
var container = new EdmEntityContainer("Squidex", "Container");

49
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs

@ -37,7 +37,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{
private static readonly TimeSpan CacheTime = TimeSpan.FromMinutes(60);
private readonly EdmModel genericEdmModel = BuildEdmModel("Generic", "Content", new EdmModel(), null);
private readonly JsonSchema genericJsonSchema = BuildJsonSchema("Content", null);
private readonly JsonSchema genericJsonSchema = ContentJsonSchemaBuilder.BuildSchema("Content", null);
private readonly IMemoryCache cache;
private readonly IJsonSerializer jsonSerializer;
private readonly ITextIndex textIndex;
@ -235,36 +235,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
private static JsonSchema BuildJsonSchema(Schema schema, IAppEntity app, bool withHiddenFields)
{
var dataSchema = schema.BuildJsonSchema(app.PartitionResolver(), (n, s) => s, withHiddenFields);
var dataSchema = schema.BuildJsonSchema(app.PartitionResolver(), (n, action) => action(), withHiddenFields);
return BuildJsonSchema(schema.DisplayName(), dataSchema);
}
private static JsonSchema BuildJsonSchema(string name, JsonSchema? dataSchema)
{
var schema = new JsonSchema
{
Properties =
{
[nameof(IContentEntity.Id).ToCamelCase()] = SchemaBuilder.StringProperty($"The id of the {name} content.", true),
[nameof(IContentEntity.Version).ToCamelCase()] = SchemaBuilder.NumberProperty($"The version of the {name}.", true),
[nameof(IContentEntity.Created).ToCamelCase()] = SchemaBuilder.DateTimeProperty($"The date and time when the {name} content has been created.", true),
[nameof(IContentEntity.CreatedBy).ToCamelCase()] = SchemaBuilder.StringProperty($"The user that has created the {name} content.", true),
[nameof(IContentEntity.LastModified).ToCamelCase()] = SchemaBuilder.DateTimeProperty($"The date and time when the {name} content has been modified last.", true),
[nameof(IContentEntity.LastModifiedBy).ToCamelCase()] = SchemaBuilder.StringProperty($"The user that has updated the {name} content last.", true),
[nameof(IContentEntity.NewStatus).ToCamelCase()] = SchemaBuilder.StringProperty("The new status of the content."),
[nameof(IContentEntity.Status).ToCamelCase()] = SchemaBuilder.StringProperty("The status of the content.", true)
},
Type = JsonObjectType.Object
};
if (dataSchema != null)
{
schema.Properties["data"] = SchemaBuilder.ObjectProperty(dataSchema, $"The data of the {name}.", true);
schema.Properties["dataDraft"] = SchemaBuilder.ObjectProperty(dataSchema, $"The draft data of the {name}.");
}
return schema;
return ContentJsonSchemaBuilder.BuildSchema(schema.DisplayName(), dataSchema);
}
private static EdmModel BuildEdmModel(Schema schema, IAppEntity app, bool withHiddenFields)
@ -307,14 +280,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{
var entityType = new EdmEntityType(modelName, name);
entityType.AddStructuralProperty(nameof(IContentEntity.Id).ToCamelCase(), EdmPrimitiveTypeKind.String);
entityType.AddStructuralProperty(nameof(IContentEntity.Created).ToCamelCase(), EdmPrimitiveTypeKind.DateTimeOffset);
entityType.AddStructuralProperty(nameof(IContentEntity.CreatedBy).ToCamelCase(), EdmPrimitiveTypeKind.String);
entityType.AddStructuralProperty(nameof(IContentEntity.LastModified).ToCamelCase(), EdmPrimitiveTypeKind.DateTimeOffset);
entityType.AddStructuralProperty(nameof(IContentEntity.LastModifiedBy).ToCamelCase(), EdmPrimitiveTypeKind.String);
entityType.AddStructuralProperty(nameof(IContentEntity.NewStatus).ToCamelCase(), EdmPrimitiveTypeKind.String);
entityType.AddStructuralProperty(nameof(IContentEntity.Status).ToCamelCase(), EdmPrimitiveTypeKind.String);
entityType.AddStructuralProperty(nameof(IContentEntity.Version).ToCamelCase(), EdmPrimitiveTypeKind.Int32);
entityType.AddStructuralProperty("id", EdmPrimitiveTypeKind.String);
entityType.AddStructuralProperty("created", EdmPrimitiveTypeKind.DateTimeOffset);
entityType.AddStructuralProperty("createdBy", EdmPrimitiveTypeKind.String);
entityType.AddStructuralProperty("lastModified", EdmPrimitiveTypeKind.DateTimeOffset);
entityType.AddStructuralProperty("lastModifiedBy", EdmPrimitiveTypeKind.String);
entityType.AddStructuralProperty("newStatus", EdmPrimitiveTypeKind.String);
entityType.AddStructuralProperty("status", EdmPrimitiveTypeKind.String);
entityType.AddStructuralProperty("version", EdmPrimitiveTypeKind.Int32);
if (schemaType != null)
{

48
backend/src/Squidex.Domain.Apps.Entities/Contents/Schemas/ContentJsonSchemaBuilder.cs

@ -0,0 +1,48 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using NJsonSchema;
namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
{
public static class ContentJsonSchemaBuilder
{
public static JsonSchema BuildSchema(string name, JsonSchema? dataSchema, bool extended = false)
{
var jsonSchema = new JsonSchema
{
Properties =
{
["id"] = SchemaBuilder.StringProperty($"The id of the {name} content.", true),
["created"] = SchemaBuilder.DateTimeProperty($"The date and time when the {name} content has been created.", true),
["createdBy"] = SchemaBuilder.StringProperty($"The user that has created the {name} content.", true),
["lastModified"] = SchemaBuilder.DateTimeProperty($"The date and time when the {name} content has been modified last.", true),
["lastModifiedBy"] = SchemaBuilder.StringProperty($"The user that has updated the {name} content last.", true),
["newStatus"] = SchemaBuilder.StringProperty("The new status of the content."),
["status"] = SchemaBuilder.StringProperty("The status of the content.", true),
},
Type = JsonObjectType.Object
};
if (extended)
{
jsonSchema.Properties["newStatusColor"] = SchemaBuilder.StringProperty("The color of the new status.", false);
jsonSchema.Properties["schema"] = SchemaBuilder.StringProperty("The name of the schema.", true);
jsonSchema.Properties["SchemaName"] = SchemaBuilder.StringProperty("The display name of the schema.", true);
jsonSchema.Properties["statusColor"] = SchemaBuilder.StringProperty("The color of the status.", true);
}
if (dataSchema != null)
{
jsonSchema.Properties["data"] = SchemaBuilder.ObjectProperty(dataSchema, $"The data of the {name}.", true);
jsonSchema.Properties["dataDraft"] = SchemaBuilder.ObjectProperty(dataSchema, $"The draft data of the {name}.");
}
return jsonSchema;
}
}
}

1
backend/src/Squidex/Areas/Api/Config/OpenApi/CommonProcessor.cs

@ -19,6 +19,7 @@ namespace Squidex.Areas.Api.Config.OpenApi
private readonly string version;
private readonly string backgroundColor = "#3f83df";
private readonly string logoUrl;
private readonly OpenApiExternalDocumentation documentation = new OpenApiExternalDocumentation
{
Url = "https://docs.squidex.io"

7
backend/src/Squidex/Areas/Api/Config/OpenApi/ErrorDtoProcessor.cs

@ -12,7 +12,6 @@ using NJsonSchema;
using NSwag;
using NSwag.Generation.Processors;
using NSwag.Generation.Processors.Contexts;
using Squidex.Pipeline.OpenApi;
using Squidex.Web;
namespace Squidex.Areas.Api.Config.OpenApi
@ -35,7 +34,11 @@ namespace Squidex.Areas.Api.Config.OpenApi
{
if (!operation.Responses.ContainsKey("500"))
{
operation.AddResponse("500", "Operation failed", errorSchema);
const string description = "Operation failed.";
var response = new OpenApiResponse { Description = description, Schema = errorSchema };
operation.Responses["500"] = response;
}
foreach (var (code, response) in operation.Responses)

29
backend/src/Squidex/Areas/Api/Config/OpenApi/ODataExtensions.cs

@ -1,29 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using NJsonSchema;
using NSwag;
using Squidex.Pipeline.OpenApi;
namespace Squidex.Areas.Api.Config.OpenApi
{
public static class ODataExtensions
{
public static void AddOData(this OpenApiOperation operation, string entity, bool supportSearch)
{
if (supportSearch)
{
operation.AddQuery("$search", JsonObjectType.String, "Optional OData full text search.");
}
operation.AddQuery("$top", JsonObjectType.Integer, $"Optional number of {entity} to take.");
operation.AddQuery("$skip", JsonObjectType.Integer, $"Optional number of {entity} to skip.");
operation.AddQuery("$orderby", JsonObjectType.String, "Optional OData order definition.");
operation.AddQuery("$filter", JsonObjectType.String, "Optional OData filter definition.");
}
}
}

5
backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs

@ -15,7 +15,6 @@ using NJsonSchema.Generation.TypeMappers;
using NodaTime;
using NSwag.Generation;
using NSwag.Generation.Processors;
using Squidex.Areas.Api.Controllers.Contents.Generator;
using Squidex.Areas.Api.Controllers.Rules.Models;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Contents;
@ -72,10 +71,8 @@ namespace Squidex.Areas.Api.Config.OpenApi
settings.ConfigureName();
settings.ConfigureSchemaSettings();
settings.OperationProcessors.Add(new ODataQueryParamsProcessor("/apps/{app}/assets", "assets", false));
settings.OperationProcessors.Add(new QueryParamsProcessor("/apps/{app}/assets"));
});
services.AddTransient<SchemasOpenApiGenerator>();
}
private static void ConfigureName<T>(this T settings) where T : OpenApiDocumentGeneratorSettings

87
backend/src/Squidex/Areas/Api/Config/OpenApi/QueryExtensions.cs

@ -0,0 +1,87 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using NJsonSchema;
using NSwag;
namespace Squidex.Areas.Api.Config.OpenApi
{
public static class QueryExtensions
{
public static void AddQuery(this OpenApiOperation operation, bool supportSearch)
{
var @string = new JsonSchema
{
Type = JsonObjectType.String
};
var number = new JsonSchema
{
Type = JsonObjectType.String
};
if (supportSearch)
{
operation.Parameters.Add(new OpenApiParameter
{
Schema = @string,
Name = "$search",
Description = "Optional OData full text search.",
Kind = OpenApiParameterKind.Query
});
}
operation.Parameters.Add(new OpenApiParameter
{
Schema = number,
Name = "$top",
Description = "Optional OData parameter to define the number of items to retrieve.",
Kind = OpenApiParameterKind.Query
});
operation.Parameters.Add(new OpenApiParameter
{
Schema = number,
Name = "$skip",
Description = "Optional OData parameter to skip items.",
Kind = OpenApiParameterKind.Query
});
operation.Parameters.Add(new OpenApiParameter
{
Schema = @string,
Name = "$orderby",
Description = "Optional OData order definition to sort the result set.",
Kind = OpenApiParameterKind.Query
});
operation.Parameters.Add(new OpenApiParameter
{
Schema = @string,
Name = "$filter",
Description = "Optional OData order definition to filter the result set.",
Kind = OpenApiParameterKind.Query
});
operation.Parameters.Add(new OpenApiParameter
{
Schema = @string,
Name = "q",
Description = "JSON query as well formatted json string. Overrides all other query parameters, except 'ids'.",
Kind = OpenApiParameterKind.Query
});
operation.Parameters.Add(new OpenApiParameter
{
Schema = @string,
Name = "ids",
Description = "Comma separated list of content items. Overrides all other query parameters.",
Kind = OpenApiParameterKind.Query
});
}
}
}

18
backend/src/Squidex/Areas/Api/Config/OpenApi/ODataQueryParamsProcessor.cs → backend/src/Squidex/Areas/Api/Config/OpenApi/QueryParamsProcessor.cs

@ -5,32 +5,28 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using NSwag;
using NSwag.Generation.Processors;
using NSwag.Generation.Processors.Contexts;
namespace Squidex.Areas.Api.Config.OpenApi
{
public sealed class ODataQueryParamsProcessor : IOperationProcessor
public sealed class QueryParamsProcessor : IOperationProcessor
{
private readonly string supportedPath;
private readonly string entity;
private readonly bool supportSearch;
private readonly string path;
public ODataQueryParamsProcessor(string supportedPath, string entity, bool supportSearch)
public QueryParamsProcessor(string path)
{
this.entity = entity;
this.supportSearch = supportSearch;
this.supportedPath = supportedPath;
this.path = path;
}
public bool Process(OperationProcessorContext context)
{
if (context.OperationDescription.Path == supportedPath && context.OperationDescription.Method == "get")
if (context.OperationDescription.Path == path && context.OperationDescription.Method == OpenApiOperationMethod.Get)
{
var operation = context.OperationDescription.Operation;
operation.AddOData(entity, supportSearch);
operation.AddQuery(false);
}
return true;

2
backend/src/Squidex/Areas/Api/Config/OpenApi/SecurityProcessor.cs

@ -55,7 +55,7 @@ namespace Squidex.Areas.Api.Config.OpenApi
private static void SetupDescription(OpenApiSecurityScheme securityScheme, string tokenUrl)
{
var securityText = NSwagHelper.SecurityDocs.Replace("<TOKEN_URL>", tokenUrl);
var securityText = OpenApiHelper.SecurityDocs.Replace("<TOKEN_URL>", tokenUrl);
securityScheme.Description = securityText;
}

27
backend/src/Squidex/Areas/Api/Controllers/Contents/ContentOpenApiController.cs

@ -42,6 +42,20 @@ namespace Squidex.Areas.Api.Controllers.Contents
return View(nameof(Docs), vm);
}
[HttpGet]
[Route("content/{app}/docs/flat/")]
[ApiCosts(0)]
[AllowAnonymous]
public IActionResult DocsFlat(string app)
{
var vm = new DocsVM
{
Specification = $"~/content/{app}/flat/swagger/v1/swagger.json"
};
return View(nameof(Docs), vm);
}
[HttpGet]
[Route("content/{app}/swagger/v1/swagger.json")]
[ApiCosts(0)]
@ -54,5 +68,18 @@ namespace Squidex.Areas.Api.Controllers.Contents
return Content(openApiDocument.ToJson(), "application/json");
}
[HttpGet]
[Route("content/{app}/flat/swagger/v1/swagger.json")]
[ApiCosts(0)]
[AllowAnonymous]
public async Task<IActionResult> GetFlatOpenApi(string app)
{
var schemas = await appProvider.GetSchemasAsync(AppId);
var openApiDocument = schemasOpenApiGenerator.Generate(HttpContext, App, schemas, true);
return Content(openApiDocument.ToJson(), "application/json");
}
}
}

131
backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/Builder.cs

@ -0,0 +1,131 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Namotion.Reflection;
using NJsonSchema;
using NSwag;
using NSwag.Generation;
using Squidex.Areas.Api.Controllers.Contents.Models;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.GenerateJsonSchema;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Infrastructure;
namespace Squidex.Areas.Api.Controllers.Contents.Generator
{
internal sealed class Builder
{
private readonly PartitionResolver partitionResolver;
public string AppName { get; }
public JsonSchema ChangeStatusSchema { get; }
public OpenApiDocument Document { get; }
internal Builder(IAppEntity app,
OpenApiDocument document,
OpenApiSchemaResolver schemaResolver,
OpenApiSchemaGenerator schemaGenerator)
{
this.partitionResolver = app.PartitionResolver();
Document = document;
AppName = app.Name;
ChangeStatusSchema = schemaGenerator.GenerateWithReference<JsonSchema>(typeof(ChangeStatusDto).ToContextualType(), schemaResolver);
}
public OperationsBuilder Shared()
{
var dataSchema = ResolveSchema("DataDto", () =>
{
return JsonSchema.CreateAnySchema();
});
var contentSchema = ResolveSchema($"ContentDto", () =>
{
return ContentJsonSchemaBuilder.BuildSchema("Shared", dataSchema, true);
});
var path = $"/content/{AppName}";
var builder = new OperationsBuilder
{
ContentSchema = contentSchema,
DataSchema = dataSchema,
Path = path,
Parent = this,
SchemaDisplayName = "__Shared",
SchemaName = "__Shared",
SchemaTypeName = "__Shared",
};
var description = "API endpoints for operations across all schemas.";
Document.Tags.Add(new OpenApiTag { Name = "__Shared", Description = description });
return builder;
}
public OperationsBuilder Schema(Schema schema, bool flat)
{
var typeName = schema.TypeName();
var displayName = schema.DisplayName();
var dataSchema = ResolveSchema($"{typeName}DataDto", () =>
{
return schema.BuildJsonSchema(partitionResolver, ResolveSchema);
});
var dataFlatSchema = ResolveSchema($"{typeName}FlatDataDto", () =>
{
return schema.BuildFlatJsonSchema(ResolveSchema);
});
var contentSchema = ResolveSchema($"{typeName}ContentDto", () =>
{
var data = flat ? dataFlatSchema : dataSchema;
return ContentJsonSchemaBuilder.BuildSchema(displayName, dataFlatSchema, true);
});
var path = $"/content/{AppName}/{schema.Name}";
var builder = new OperationsBuilder
{
ContentSchema = contentSchema,
DataSchema = dataSchema,
Path = path,
Parent = this,
SchemaDisplayName = 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 });
return builder;
}
private JsonSchema ResolveSchema(string name, Func<JsonSchema> factory)
{
name = char.ToUpperInvariant(name[0]) + name[1..];
return new JsonSchema
{
Reference = Document.Definitions.GetOrAdd(name, x => factory())
};
}
}
}

131
backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/OperationBuilder.cs

@ -0,0 +1,131 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using NJsonSchema;
using NSwag;
using Squidex.Areas.Api.Config.OpenApi;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Contents.Generator
{
internal sealed class OperationBuilder
{
private readonly OpenApiOperation operation = new OpenApiOperation();
private readonly OperationsBuilder operations;
public OperationBuilder(OperationsBuilder operations, OpenApiOperation operation)
{
this.operations = operations;
this.operation = operation;
}
public OperationBuilder Operation(string name)
{
operation.OperationId = $"{name}{operations.SchemaTypeName}Content";
return this;
}
public OperationBuilder OperationSummary(string summary)
{
if (!string.IsNullOrWhiteSpace(summary))
{
operation.Summary = operations.FormatText(summary);
}
return this;
}
public OperationBuilder Describe(string description)
{
if (!string.IsNullOrWhiteSpace(description))
{
operation.Description = description;
}
return this;
}
public OperationBuilder HasId()
{
HasPath("id", JsonObjectType.String, $"The id of the schema content item.");
Responds(404, "Content item not found.");
return this;
}
private OperationBuilder AddParameter(string name, JsonSchema schema, OpenApiParameterKind kind, string description)
{
var parameter = new OpenApiParameter { Schema = schema, Name = name, Kind = kind };
if (!string.IsNullOrWhiteSpace(description))
{
parameter.Description = operations.FormatText(description);
}
parameter.IsRequired = kind != OpenApiParameterKind.Query;
operation.Parameters.Add(parameter);
return this;
}
public OperationBuilder HasQueryOptions(bool supportSearch)
{
operation.AddQuery(true);
return this;
}
public OperationBuilder HasQuery(string name, JsonObjectType type, string description)
{
var jsonSchema = new JsonSchema { Type = type };
return AddParameter(name, jsonSchema, OpenApiParameterKind.Query, description);
}
public OperationBuilder HasPath(string name, JsonObjectType type, string description, string? format = null)
{
var jsonSchema = new JsonSchema { Type = type, Format = format };
return AddParameter(name, jsonSchema, OpenApiParameterKind.Path, description);
}
public OperationBuilder HasBody(string name, JsonSchema jsonSchema, string description)
{
return AddParameter(name, jsonSchema, OpenApiParameterKind.Body, description);
}
public OperationBuilder Responds(int statusCode, string description, JsonSchema? schema = null)
{
var response = new OpenApiResponse { Description = description, Schema = schema };
operation.Responses.Add(statusCode.ToString(), response);
return this;
}
public OperationBuilder RequirePermission(string permissionId)
{
operation.Security = new List<OpenApiSecurityRequirement>
{
new OpenApiSecurityRequirement
{
[Constants.SecurityDefinition] = new[]
{
Permissions.ForApp(permissionId, operations.Parent.AppName, operations.SchemaName).Id
}
}
};
return this;
}
}
}

54
backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/OperationsBuilder.cs

@ -0,0 +1,54 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using NJsonSchema;
using NSwag;
using Squidex.Infrastructure;
namespace Squidex.Areas.Api.Controllers.Contents.Generator
{
internal sealed class OperationsBuilder
{
public Builder Parent { get; init; }
public string Path { get; init; }
public string SchemaName { get; init; }
public string SchemaTypeName { get; init; }
public string SchemaDisplayName { get; init; }
public JsonSchema ContentSchema { get; init; }
public JsonSchema DataSchema { get; init; }
public string FormatText(string text)
{
return text?.Replace("schema ", $"'{SchemaDisplayName}' ", StringComparison.OrdinalIgnoreCase)!;
}
public OperationBuilder AddOperation(string method, string path)
{
var operation = new OpenApiOperation
{
Tags = new List<string>
{
SchemaDisplayName
}
};
var operations = Parent.Document.Paths.GetOrAddNew($"{Path}{path}");
operations[method] = operation;
return new OperationBuilder(this, operation);
}
}
}

241
backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaOpenApiGenerator.cs

@ -1,241 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using NJsonSchema;
using NSwag;
using Squidex.Areas.Api.Config.OpenApi;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.GenerateJsonSchema;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Pipeline.OpenApi;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Contents.Generator
{
public sealed class SchemaOpenApiGenerator
{
private readonly OpenApiDocument document;
private readonly JsonSchema contentSchema;
private readonly JsonSchema dataSchema;
private readonly string schemaPath;
private readonly string schemaName;
private readonly string schemaType;
private readonly string appPath;
private readonly JsonSchema statusSchema;
private readonly string appName;
public SchemaOpenApiGenerator(
OpenApiDocument document,
string appName,
string appPath,
Schema schema,
SchemaResolver schemaResolver,
JsonSchema statusSchema,
PartitionResolver partitionResolver)
{
this.document = document;
this.appName = appName;
this.appPath = appPath;
this.statusSchema = statusSchema;
schemaPath = schema.Name;
schemaName = schema.DisplayName();
schemaType = schema.TypeName();
dataSchema = schemaResolver($"{schemaType}Dto", schema.BuildJsonSchema(partitionResolver, schemaResolver));
contentSchema = schemaResolver($"{schemaType}ContentDto", ContentSchemaBuilder.CreateContentSchema(schema, dataSchema));
}
public void GenerateSchemaOperations()
{
document.Tags.Add(
new OpenApiTag
{
Name = schemaName, Description = $"API to manage {schemaName} contents."
});
var schemaOperations = new[]
{
GenerateSchemaGetsOperation(),
GenerateSchemaGetOperation(),
GenerateSchemaCreateOperation(),
GenerateSchemaUpdateOperation(),
GenerateSchemaUpdatePatchOperation(),
GenerateSchemaStatusOperation(),
GenerateSchemaDeleteOperation()
};
foreach (var operation in schemaOperations.SelectMany(x => x.Values).Distinct())
{
operation.Tags = new List<string> { schemaName };
}
}
private OpenApiPathItem GenerateSchemaGetsOperation()
{
return Add(OpenApiOperationMethod.Get, Permissions.AppContentsReadOwn, "/",
operation =>
{
operation.OperationId = $"Query{schemaType}Contents";
operation.Summary = $"Queries {schemaName} contents.";
operation.Description = NSwagHelper.SchemaQueryDocs;
operation.AddOData("contents", true);
operation.AddResponse("200", $"{schemaName} contents retrieved.", CreateContentsSchema(schemaName, contentSchema));
operation.AddResponse("400", $"{schemaName} query not valid.");
});
}
private OpenApiPathItem GenerateSchemaGetOperation()
{
return Add(OpenApiOperationMethod.Get, Permissions.AppContentsReadOwn, "/{id}", operation =>
{
operation.OperationId = $"Get{schemaType}Content";
operation.Summary = $"Get a {schemaName} content.";
operation.AddResponse("200", $"{schemaName} content found.", contentSchema);
});
}
private OpenApiPathItem GenerateSchemaCreateOperation()
{
return Add(OpenApiOperationMethod.Post, Permissions.AppContentsCreate, "/",
operation =>
{
operation.OperationId = $"Create{schemaType}Content";
operation.Summary = $"Create a {schemaName} content.";
operation.AddBody("data", dataSchema, NSwagHelper.SchemaBodyDocs);
operation.AddQuery("publish", JsonObjectType.Boolean, "True to automatically publish the content.");
operation.AddQuery("id", JsonObjectType.String, "The optional custom content id.");
operation.AddResponse("201", $"{schemaName} content created.", contentSchema);
operation.AddResponse("400", $"{schemaName} content not valid.");
});
}
private OpenApiPathItem GenerateSchemaUpdateOperation()
{
return Add(OpenApiOperationMethod.Put, Permissions.AppContentsUpdateOwn, "/{id}",
operation =>
{
operation.OperationId = $"Update{schemaType}Content";
operation.Summary = $"Update a {schemaName} content.";
operation.AddBody("data", dataSchema, NSwagHelper.SchemaBodyDocs);
operation.AddResponse("200", $"{schemaName} content updated.", contentSchema);
operation.AddResponse("400", $"{schemaName} content not valid.");
});
}
private OpenApiPathItem GenerateSchemaUpdatePatchOperation()
{
return Add(OpenApiOperationMethod.Patch, Permissions.AppContentsUpdateOwn, "/{id}",
operation =>
{
operation.OperationId = $"Path{schemaType}Content";
operation.Summary = $"Patch a {schemaName} content.";
operation.AddBody("data", dataSchema, NSwagHelper.SchemaBodyDocs);
operation.AddResponse("200", $"{schemaName} content patched.", contentSchema);
operation.AddResponse("400", $"{schemaName} status not valid.");
});
}
private OpenApiPathItem GenerateSchemaStatusOperation()
{
return Add(OpenApiOperationMethod.Put, Permissions.AppContentsUpdateOwn, "/{id}/status",
operation =>
{
operation.OperationId = $"Change{schemaType}ContentStatus";
operation.Summary = $"Change status of {schemaName} content.";
operation.AddBody("request", statusSchema, "The request to change content status.");
operation.AddResponse("200", $"{schemaName} content status changed.", contentSchema);
operation.AddResponse("400", $"{schemaName} content not valid.");
});
}
private OpenApiPathItem GenerateSchemaDeleteOperation()
{
return Add(OpenApiOperationMethod.Delete, Permissions.AppContentsDeleteOwn, "/{id}",
operation =>
{
operation.OperationId = $"Delete{schemaType}Content";
operation.Summary = $"Delete a {schemaName} content.";
operation.AddResponse("204", $"{schemaName} content deleted.");
});
}
private OpenApiPathItem Add(string method, string permission, string path, Action<OpenApiOperation> updater)
{
var operations = document.Paths.GetOrAddNew($"{appPath}/{schemaPath}{path}");
var operation = new OpenApiOperation
{
Security = new List<OpenApiSecurityRequirement>
{
new OpenApiSecurityRequirement
{
[Constants.SecurityDefinition] = new[]
{
Permissions.ForApp(permission, appName, schemaPath).Id
}
}
}
};
updater(operation);
operations[method] = operation;
if (path.StartsWith("/{id}", StringComparison.OrdinalIgnoreCase))
{
operation.AddPathParameter("id", JsonObjectType.String, $"The id of the {schemaName} content.");
operation.AddResponse("404", $"App, schema or {schemaName} content not found.");
}
return operations;
}
private static JsonSchema CreateContentsSchema(string schemaName, JsonSchema contentSchema)
{
var schema = new JsonSchema
{
Properties =
{
["total"] = SchemaBuilder.NumberProperty($"The total number of {schemaName} contents.", true),
["items"] = SchemaBuilder.ArrayProperty(contentSchema, $"The {schemaName} contents.", true)
},
Type = JsonObjectType.Object
};
return schema;
}
}
}

175
backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs

@ -9,31 +9,26 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Namotion.Reflection;
using NJsonSchema;
using NJsonSchema.Generation;
using NSwag;
using NSwag.Generation;
using NSwag.Generation.Processors;
using NSwag.Generation.Processors.Contexts;
using Squidex.Areas.Api.Config.OpenApi;
using Squidex.Areas.Api.Controllers.Contents.Models;
using Squidex.Domain.Apps.Core.GenerateJsonSchema;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Pipeline.OpenApi;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Contents.Generator
{
public sealed class SchemasOpenApiGenerator
{
private readonly OpenApiDocumentGeneratorSettings settings = new OpenApiDocumentGeneratorSettings();
private readonly OpenApiSchemaGenerator schemaGenerator;
private readonly IRequestCache requestCache;
private OpenApiSchemaGenerator schemaGenerator;
private OpenApiDocument document;
private JsonSchema statusSchema;
private JsonSchemaResolver schemaResolver;
public SchemasOpenApiGenerator(IEnumerable<IDocumentProcessor> documentProcessors, IRequestCache requestCache)
{
@ -44,19 +39,33 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
settings.DocumentProcessors.Add(processor);
}
schemaGenerator = new OpenApiSchemaGenerator(settings);
this.requestCache = requestCache;
}
public OpenApiDocument Generate(HttpContext httpContext, IAppEntity app, IEnumerable<ISchemaEntity> schemas)
public OpenApiDocument Generate(HttpContext httpContext, IAppEntity app, IEnumerable<ISchemaEntity> schemas, bool flat = false)
{
document = NSwagHelper.CreateApiDocument(httpContext, app.Name);
var document = OpenApiHelper.CreateApiDocument(httpContext, app.Name);
schemaGenerator = new OpenApiSchemaGenerator(settings);
schemaResolver = new OpenApiSchemaResolver(document, settings);
var schemaResolver = new OpenApiSchemaResolver(document, settings);
requestCache.AddDependency(app.UniqueId, app.Version);
var builder = new Builder(
app,
document,
schemaResolver,
schemaGenerator);
statusSchema = GenerateStatusSchema();
foreach (var schema in schemas.Where(x => x.SchemaDef.IsPublished))
{
requestCache.AddDependency(schema.UniqueId, schema.Version);
GenerateSchemasOperations(schemas, app);
GenerateSchemaOperations(builder.Schema(schema.SchemaDef, flat));
}
GenerateSharedOperations(builder.Shared());
var context =
new DocumentProcessorContext(document,
@ -74,35 +83,129 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
return document;
}
private JsonSchema GenerateStatusSchema()
private static void GenerateSharedOperations(OperationsBuilder builder)
{
var statusDtoType = typeof(ChangeStatusDto);
return schemaGenerator.GenerateWithReference<JsonSchema>(statusDtoType.ToContextualType(), schemaResolver);
var contentsSchema = BuildResults(builder);
builder.AddOperation(OpenApiOperationMethod.Get, "/")
.RequirePermission(Permissions.AppContentsReadOwn)
.Operation("Query")
.OperationSummary("Query contents across all schemas.")
.HasQuery("ids", JsonObjectType.String, "Comma-separated list of content IDs.")
.Responds(200, "Content items retrieved.", contentsSchema)
.Responds(400, "Query not valid.");
}
private void GenerateSchemasOperations(IEnumerable<ISchemaEntity> schemas, IAppEntity app)
private static void GenerateSchemaOperations(OperationsBuilder builder)
{
requestCache.AddDependency(app.UniqueId, app.Version);
var appBasePath = $"/content/{app.Name}";
foreach (var schema in schemas.Where(x => x.SchemaDef.IsPublished))
{
requestCache.AddDependency(schema.UniqueId, schema.Version);
var partition = app.PartitionResolver();
new SchemaOpenApiGenerator(document, app.Name, appBasePath, schema.SchemaDef, AppendSchema, statusSchema, partition)
.GenerateSchemaOperations();
}
var contentsSchema = BuildResults(builder);
builder.AddOperation(OpenApiOperationMethod.Get, "/")
.RequirePermission(Permissions.AppContentsReadOwn)
.Operation("Query")
.OperationSummary("Query schema contents items.")
.Describe(OpenApiHelper.SchemaQueryDocs)
.HasQueryOptions(true)
.Responds(200, "Content items retrieved.", contentsSchema)
.Responds(400, "Query not valid.");
builder.AddOperation(OpenApiOperationMethod.Get, "/{id}")
.RequirePermission(Permissions.AppContentsReadOwn)
.Operation("Get")
.OperationSummary("Get a schema content item.")
.HasId()
.Responds(200, "Content item returned.", builder.ContentSchema);
builder.AddOperation(OpenApiOperationMethod.Get, "/{id}/{version}")
.RequirePermission(Permissions.AppContentsReadOwn)
.Operation("Get")
.OperationSummary("Get a schema content item by id and version.")
.HasPath("version", JsonObjectType.Number, "The version of the content item.")
.HasId()
.Responds(200, "Content item returned.", builder.ContentSchema);
builder.AddOperation(OpenApiOperationMethod.Get, "/{id}/validity")
.RequirePermission(Permissions.AppContentsReadOwn)
.Operation("Validate")
.OperationSummary("Validates a schema content item.")
.HasId()
.Responds(200, "Content item is valid.")
.Responds(400, "Content item is not valid.");
builder.AddOperation(OpenApiOperationMethod.Post, "/")
.RequirePermission(Permissions.AppContentsCreate)
.Operation("Create")
.OperationSummary("Create a schema content item.")
.HasQuery("publish", JsonObjectType.Boolean, "True to automatically publish the content.")
.HasQuery("id", JsonObjectType.String, "The optional custom content id.")
.HasBody("data", builder.DataSchema, OpenApiHelper.SchemaBodyDocs)
.Responds(201, "Content item created", builder.ContentSchema)
.Responds(400, "Content data not valid.");
builder.AddOperation(OpenApiOperationMethod.Post, "/{id}")
.RequirePermission(Permissions.AppContentsUpsert)
.Operation("Upsert")
.OperationSummary("Upsert a schema content item.")
.HasQuery("publish", JsonObjectType.Boolean, "True to automatically publish the content.")
.HasId()
.HasBody("data", builder.DataSchema, OpenApiHelper.SchemaBodyDocs)
.Responds(200, "Content item created or updated.", builder.ContentSchema)
.Responds(400, "Content data not valid.");
builder.AddOperation(OpenApiOperationMethod.Put, "/{id}")
.RequirePermission(Permissions.AppContentsUpdateOwn)
.Operation("Update")
.OperationSummary("Update a schema content item.")
.HasId()
.HasBody("data", builder.DataSchema, OpenApiHelper.SchemaBodyDocs)
.Responds(200, "Content item updated.", builder.ContentSchema)
.Responds(400, "Content data not valid.");
builder.AddOperation(OpenApiOperationMethod.Patch, "/{id}")
.RequirePermission(Permissions.AppContentsUpdateOwn)
.Operation("Patch")
.OperationSummary("Patch a schema content item.")
.HasId()
.HasBody("data", builder.DataSchema, OpenApiHelper.SchemaBodyDocs)
.Responds(200, "Content item updated.", builder.ContentSchema)
.Responds(400, "Content data not valid.");
builder.AddOperation(OpenApiOperationMethod.Put, "/{id}/status")
.RequirePermission(Permissions.AppContentsUpdateOwn)
.Operation("Patch")
.OperationSummary("Patch a schema content item.")
.HasId()
.HasBody("data", builder.DataSchema, OpenApiHelper.SchemaBodyDocs)
.Responds(200, "Content item updated.", builder.ContentSchema)
.Responds(400, "Content data not valid.");
builder.AddOperation(OpenApiOperationMethod.Delete, "/{id}")
.RequirePermission(Permissions.AppContentsChangeStatusOwn)
.Operation("Change")
.OperationSummary("Change the status of a schema content item.")
.HasId()
.HasBody("request", builder.Parent.ChangeStatusSchema, "The request to change content status.")
.Responds(200, "Content status updated.", builder.ContentSchema);
builder.AddOperation(OpenApiOperationMethod.Delete, "/{id}")
.RequirePermission(Permissions.AppContentsDeleteOwn)
.Operation("Delete")
.OperationSummary("Delete a schema content item.")
.HasId()
.Responds(204, "Content item deleted");
}
private JsonSchema AppendSchema(string name, JsonSchema schema)
private static JsonSchema BuildResults(OperationsBuilder builder)
{
name = char.ToUpperInvariant(name[0]) + name[1..];
return new JsonSchema { Reference = document.Definitions.GetOrAdd(name, schema, (k, c) => c) };
return new JsonSchema
{
Properties =
{
["total"] = SchemaBuilder.NumberProperty("The total number of content items.", true),
["items"] = SchemaBuilder.ArrayProperty(builder.ContentSchema, "The content items.", true)
},
Type = JsonObjectType.Object
};
}
}
}

0
backend/src/Squidex/Docs/schemabody.md → backend/src/Squidex/Docs/schema-body.md

27
backend/src/Squidex/Docs/schema-query.md

@ -0,0 +1,27 @@
How to make queries?
Read more about it at: https://docs.squidex.io/04-guides/02-api.html
The query endpoints support three options:
### Query with OData
Squidex supports a subset of the OData (https://www.odata.org/) syntax with with the following query options:
* **$top**: The $top query option requests the number of items in the queried collection to be included in the result. The default value is 20 and the maximum allowed value is 200. You can change the maximum in the app settings, when you host Squidex yourself.
* **$skip**: The $skip query option requests the number of items in the queried collection that are to be skipped and not included in the result. Use it together with $top to read the all your data page by page.
* **$search**: The $search query option allows clients to request entities matching a free-text search expression. We add the data of all fields for all languages to our full text engine.
* **$filter**: The $filter query option allows clients to filter a collection of resources that are addressed by a request URL.
* **$orderby**: The $orderby query option allows clients to request resources in a particular order.
### Query with JSON query
Squidex also supports a query syntax based on JSON. You have to pass in the query object as query parameter:
* **q**: A json text that represents the same query options as with OData, but is more performant to parse.
### Query by IDs
Query your items by passing in one or many IDs with the following query parameter:
* **ids**: A comma-separated list of ids. If you define this option all other settings are ignored.

11
backend/src/Squidex/Docs/schemaquery.md

@ -1,11 +0,0 @@
The squidex API the OData url convention to query data.
We support the following query options.
* **$top**: The $top query option requests the number of items in the queried collection to be included in the result. The default value is 20 and the maximum allowed value is 200.
* **$skip**: The $skip query option requests the number of items in the queried collection that are to be skipped and not included in the result. Use it together with $top to read the all your data page by page.
* **$search**: The $search query option allows clients to request entities matching a free-text search expression. We add the data of all fields for all languages to a single field in the database and use this combined field to implement the full text search.
* **$filter**: The $filter query option allows clients to filter a collection of resources that are addressed by a request URL.
* **$orderby**: The $orderby query option allows clients to request resources in a particular order.
Read more about it at: https://docs.squidex.io/04-guides/02-api.html

4
backend/src/Squidex/Docs/security.md

@ -6,12 +6,10 @@ To retrieve an access token, the client id must make a request to the token url.
-X POST '<TOKEN_URL>'
-H 'Content-Type: application/x-www-form-urlencoded'
-d 'grant_type=client_credentials&
client_id=[APP_NAME]:[CLIENT_ID]&
client_id=[CLIENT_ID]&
client_secret=[CLIENT_SECRET]&
scope=squidex-api'
`[APP_NAME]` is the name of your app. You have to create a client to generate an access token.
You must send this token in the `Authorization` header when making requests to the API:
Authorization: Bearer <token>

48
backend/src/Squidex/Pipeline/OpenApi/NSwagHelper.cs → backend/src/Squidex/Pipeline/OpenApi/OpenApiHelper.cs

@ -14,17 +14,17 @@ using NSwag;
namespace Squidex.Pipeline.OpenApi
{
public static class NSwagHelper
public static class OpenApiHelper
{
public static readonly string SecurityDocs = LoadDocs("security");
public static readonly string SchemaBodyDocs = LoadDocs("schemabody");
public static readonly string SchemaBodyDocs = LoadDocs("schema-body");
public static readonly string SchemaQueryDocs = LoadDocs("schemaquery");
public static readonly string SchemaQueryDocs = LoadDocs("schema-query");
private static string LoadDocs(string name)
{
var assembly = typeof(NSwagHelper).Assembly;
var assembly = typeof(OpenApiHelper).Assembly;
using (var resourceStream = assembly.GetManifestResourceStream($"Squidex.Docs.{name}.md"))
{
@ -70,45 +70,5 @@ namespace Squidex.Pipeline.OpenApi
return document;
}
public static void AddQuery(this OpenApiOperation operation, string name, JsonObjectType type, string description)
{
var schema = new JsonSchema { Type = type };
operation.AddParameter(name, schema, OpenApiParameterKind.Query, description, false);
}
public static void AddPathParameter(this OpenApiOperation operation, string name, JsonObjectType type, string description, string? format = null)
{
var schema = new JsonSchema { Type = type, Format = format };
operation.AddParameter(name, schema, OpenApiParameterKind.Path, description, true);
}
public static void AddBody(this OpenApiOperation operation, string name, JsonSchema schema, string description)
{
operation.AddParameter(name, schema, OpenApiParameterKind.Body, description, true);
}
private static void AddParameter(this OpenApiOperation operation, string name, JsonSchema schema, OpenApiParameterKind kind, string description, bool isRequired)
{
var parameter = new OpenApiParameter { Schema = schema, Name = name, Kind = kind };
if (!string.IsNullOrWhiteSpace(description))
{
parameter.Description = description;
}
parameter.IsRequired = isRequired;
operation.Parameters.Add(parameter);
}
public static void AddResponse(this OpenApiOperation operation, string statusCode, string description, JsonSchema? schema = null)
{
var response = new OpenApiResponse { Description = description, Schema = schema };
operation.Responses.Add(statusCode, response);
}
}
}

8
backend/src/Squidex/Squidex.csproj

@ -93,8 +93,8 @@
<ItemGroup>
<EmbeddedResource Include="Areas\Api\Controllers\Users\Assets\Avatar.png" />
<EmbeddedResource Include="Docs\schemabody.md" />
<EmbeddedResource Include="Docs\schemaquery.md" />
<EmbeddedResource Include="Docs\schema-body.md" />
<EmbeddedResource Include="Docs\schema-query.md" />
<EmbeddedResource Include="Docs\security.md" />
<EmbeddedResource Include="Pipeline\Squid\icon-happy-sm.svg" />
<EmbeddedResource Include="Pipeline\Squid\icon-happy.svg" />
@ -109,8 +109,8 @@
<ItemGroup>
<None Remove="Areas\Api\Controllers\Users\Assets\Avatar.png" />
<None Remove="Docs\schemabody.md" />
<None Remove="Docs\schemaquery.md" />
<None Remove="Docs\schema-body.md" />
<None Remove="Docs\schema-query.md" />
<None Remove="Docs\security.md" />
<None Remove="Pipeline\Squid\icon-happy-sm.svg" />
<None Remove="Pipeline\Squid\icon-happy.svg" />

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

@ -25,7 +25,12 @@ namespace Squidex.Domain.Apps.Core.Operations.GenerateJsonSchema
{
var languagesConfig = LanguagesConfig.English.Set(Language.DE);
var jsonSchema = schema.BuildJsonSchema(languagesConfig.ToResolver(), (n, s) => new JsonSchema { Reference = s });
var schemaResolver = new SchemaResolver((name, action) =>
{
return action();
});
var jsonSchema = schema.BuildJsonSchema(languagesConfig.ToResolver(), schemaResolver);
var jsonProperties = AllPropertyNames(jsonSchema);
void CheckField(IField field)
@ -55,13 +60,42 @@ namespace Squidex.Domain.Apps.Core.Operations.GenerateJsonSchema
}
[Fact]
public void Should_build_data_schema()
public void Should_build_flat_json_schema()
{
var languagesConfig = LanguagesConfig.English.Set(Language.DE);
var jsonSchema = schema.BuildJsonSchema(languagesConfig.ToResolver(), (n, s) => new JsonSchema { Reference = s });
var schemaResolver = new SchemaResolver((name, action) =>
{
return action();
});
var jsonSchema = schema.BuildFlatJsonSchema(schemaResolver);
var jsonProperties = AllPropertyNames(jsonSchema);
Assert.NotNull(jsonSchema);
void CheckField(IField field)
{
if (!field.IsForApi())
{
Assert.DoesNotContain(field.Name, jsonProperties);
}
else
{
Assert.Contains(field.Name, jsonProperties);
}
if (field is IArrayField array)
{
foreach (var nested in array.Fields)
{
CheckField(nested);
}
}
}
foreach (var field in schema.Fields)
{
CheckField(field);
}
}
private static HashSet<string> AllPropertyNames(JsonSchema schema)

Loading…
Cancel
Save