From 9beed532a9cdf291c48ecbb136a6ee95b899c84b Mon Sep 17 00:00:00 2001 From: Laura Neto <12862535+lauraneto@users.noreply.github.com> Date: Mon, 24 Nov 2025 12:06:03 +0100 Subject: [PATCH] Update Swashbuckle to v10 (#20925) * Update Swashbuckle to v10 * Regenerate backoffice api client * Add missing space for consistency * Simplify nullability check * Small improvement Didn't notice that these classes were internal, so tried keeping compatibility, but it wasn't needed. * Fix failing integration test * Apply suggestions from code review Co-authored-by: Andy Butland Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Remove unnecessary comma --------- Co-authored-by: Andy Butland Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Directory.Packages.props | 2 +- .../ConfigureUmbracoSwaggerGenOptions.cs | 2 +- .../OpenApi/EnumSchemaFilter.cs | 26 +- .../OpenApi/MimeTypeDocumentFilter.cs | 21 +- .../RemoveSecuritySchemesDocumentFilter.cs | 4 +- .../SwaggerRouteTemplatePipelineFilter.cs | 3 +- ...gureUmbracoDeliveryApiSwaggerGenOptions.cs | 2 +- ...henticationDeliveryApiSwaggerGenOptions.cs | 30 +- .../SwaggerContentDocumentationFilter.cs | 151 +++------ .../Filters/SwaggerDocumentationFilterBase.cs | 152 +++++---- .../SwaggerMediaDocumentationFilter.cs | 83 +---- ...reUmbracoManagementApiSwaggerGenOptions.cs | 15 +- src/Umbraco.Cms.Api.Management/OpenApi.json | 306 +++++++++++++++--- ...SecurityRequirementsOperationFilterBase.cs | 43 ++- .../OpenApi/NotificationHeaderFilter.cs | 33 +- .../OpenApi/ReponseHeaderOperationFilter.cs | 28 +- ...equireNonNullablePropertiesSchemaFilter.cs | 19 +- .../src/packages/core/backend-api/sdk.gen.ts | 3 + .../packages/core/backend-api/types.gen.ts | 16 +- .../Composers/UmbracoExtensionApiComposer.cs | 2 +- .../DeliveryApi/OpenApiContractTest.cs | 10 +- 21 files changed, 523 insertions(+), 428 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index e859c39080..2c9fa306e4 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -74,7 +74,7 @@ - + diff --git a/src/Umbraco.Cms.Api.Common/Configuration/ConfigureUmbracoSwaggerGenOptions.cs b/src/Umbraco.Cms.Api.Common/Configuration/ConfigureUmbracoSwaggerGenOptions.cs index a4cabf51bb..4b65bb12bf 100644 --- a/src/Umbraco.Cms.Api.Common/Configuration/ConfigureUmbracoSwaggerGenOptions.cs +++ b/src/Umbraco.Cms.Api.Common/Configuration/ConfigureUmbracoSwaggerGenOptions.cs @@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; using Umbraco.Cms.Api.Common.OpenApi; using Umbraco.Extensions; diff --git a/src/Umbraco.Cms.Api.Common/OpenApi/EnumSchemaFilter.cs b/src/Umbraco.Cms.Api.Common/OpenApi/EnumSchemaFilter.cs index e2ac5ab870..845adb4990 100644 --- a/src/Umbraco.Cms.Api.Common/OpenApi/EnumSchemaFilter.cs +++ b/src/Umbraco.Cms.Api.Common/OpenApi/EnumSchemaFilter.cs @@ -1,25 +1,27 @@ using System.Reflection; using System.Runtime.Serialization; -using Microsoft.OpenApi.Any; -using Microsoft.OpenApi.Models; +using System.Text.Json.Nodes; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; namespace Umbraco.Cms.Api.Common.OpenApi; public class EnumSchemaFilter : ISchemaFilter { - public void Apply(OpenApiSchema model, SchemaFilterContext context) + public void Apply(IOpenApiSchema model, SchemaFilterContext context) { - if (context.Type.IsEnum) + if (model is not OpenApiSchema schema || context.Type.IsEnum is false) { - model.Type = "string"; - model.Format = null; - model.Enum.Clear(); - foreach (var name in Enum.GetNames(context.Type)) - { - var actualName = context.Type.GetField(name)?.GetCustomAttribute()?.Value ?? name; - model.Enum.Add(new OpenApiString(actualName)); - } + return; + } + + schema.Type = JsonSchemaType.String; + schema.Format = null; + schema.Enum = new List(); + foreach (var name in Enum.GetNames(context.Type)) + { + var actualName = context.Type.GetField(name)?.GetCustomAttribute()?.Value ?? name; + schema.Enum.Add(actualName); } } } diff --git a/src/Umbraco.Cms.Api.Common/OpenApi/MimeTypeDocumentFilter.cs b/src/Umbraco.Cms.Api.Common/OpenApi/MimeTypeDocumentFilter.cs index a756c30f1f..2dfc6a88a4 100644 --- a/src/Umbraco.Cms.Api.Common/OpenApi/MimeTypeDocumentFilter.cs +++ b/src/Umbraco.Cms.Api.Common/OpenApi/MimeTypeDocumentFilter.cs @@ -1,4 +1,4 @@ -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; using Umbraco.Extensions; @@ -21,25 +21,32 @@ public class MimeTypeDocumentFilter : IDocumentFilter } OpenApiOperation[] operations = swaggerDoc.Paths - .SelectMany(path => path.Value.Operations.Values) + .SelectMany(path => path.Value.Operations?.Values ?? Enumerable.Empty()) .ToArray(); - void RemoveUnwantedMimeTypes(IDictionary content) + void RemoveUnwantedMimeTypes(IDictionary? content) { - if (content.ContainsKey("application/json")) + if (content is null || content.ContainsKey("application/json") is false) { - content.RemoveAll(r => r.Key != "application/json"); + return; } + content.RemoveAll(r => r.Key != "application/json"); } - OpenApiRequestBody[] requestBodies = operations.Select(operation => operation.RequestBody).WhereNotNull().ToArray(); + OpenApiRequestBody[] requestBodies = operations + .Select(operation => operation.RequestBody) + .OfType() + .ToArray(); foreach (OpenApiRequestBody requestBody in requestBodies) { RemoveUnwantedMimeTypes(requestBody.Content); } - OpenApiResponse[] responses = operations.SelectMany(operation => operation.Responses.Values).WhereNotNull().ToArray(); + OpenApiResponse[] responses = operations + .SelectMany(operation => operation.Responses?.Values ?? Enumerable.Empty()) + .OfType() + .ToArray(); foreach (OpenApiResponse response in responses) { RemoveUnwantedMimeTypes(response.Content); diff --git a/src/Umbraco.Cms.Api.Common/OpenApi/RemoveSecuritySchemesDocumentFilter.cs b/src/Umbraco.Cms.Api.Common/OpenApi/RemoveSecuritySchemesDocumentFilter.cs index 8ab2041f7d..c90c9f9c73 100644 --- a/src/Umbraco.Cms.Api.Common/OpenApi/RemoveSecuritySchemesDocumentFilter.cs +++ b/src/Umbraco.Cms.Api.Common/OpenApi/RemoveSecuritySchemesDocumentFilter.cs @@ -1,4 +1,4 @@ -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; namespace Umbraco.Cms.Api.Common.OpenApi; @@ -20,6 +20,6 @@ public class RemoveSecuritySchemesDocumentFilter : IDocumentFilter return; } - swaggerDoc.Components.SecuritySchemes.Clear(); + swaggerDoc.Components?.SecuritySchemes?.Clear(); } } diff --git a/src/Umbraco.Cms.Api.Common/OpenApi/SwaggerRouteTemplatePipelineFilter.cs b/src/Umbraco.Cms.Api.Common/OpenApi/SwaggerRouteTemplatePipelineFilter.cs index dba0e6f56d..2c45a36754 100644 --- a/src/Umbraco.Cms.Api.Common/OpenApi/SwaggerRouteTemplatePipelineFilter.cs +++ b/src/Umbraco.Cms.Api.Common/OpenApi/SwaggerRouteTemplatePipelineFilter.cs @@ -3,13 +3,12 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; using Swashbuckle.AspNetCore.SwaggerUI; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Web.Common.ApplicationBuilder; -using Umbraco.Extensions; using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; namespace Umbraco.Cms.Api.Common.OpenApi; diff --git a/src/Umbraco.Cms.Api.Delivery/Configuration/ConfigureUmbracoDeliveryApiSwaggerGenOptions.cs b/src/Umbraco.Cms.Api.Delivery/Configuration/ConfigureUmbracoDeliveryApiSwaggerGenOptions.cs index 9b87166300..8c7437e4bf 100644 --- a/src/Umbraco.Cms.Api.Delivery/Configuration/ConfigureUmbracoDeliveryApiSwaggerGenOptions.cs +++ b/src/Umbraco.Cms.Api.Delivery/Configuration/ConfigureUmbracoDeliveryApiSwaggerGenOptions.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; using Umbraco.Cms.Api.Common.OpenApi; using Umbraco.Cms.Api.Delivery.Filters; diff --git a/src/Umbraco.Cms.Api.Delivery/Configuration/ConfigureUmbracoMemberAuthenticationDeliveryApiSwaggerGenOptions.cs b/src/Umbraco.Cms.Api.Delivery/Configuration/ConfigureUmbracoMemberAuthenticationDeliveryApiSwaggerGenOptions.cs index 3161105dca..5922768462 100644 --- a/src/Umbraco.Cms.Api.Delivery/Configuration/ConfigureUmbracoMemberAuthenticationDeliveryApiSwaggerGenOptions.cs +++ b/src/Umbraco.Cms.Api.Delivery/Configuration/ConfigureUmbracoMemberAuthenticationDeliveryApiSwaggerGenOptions.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; using Umbraco.Cms.Api.Common.Security; using Umbraco.Cms.Api.Delivery.Controllers.Content; @@ -35,23 +35,9 @@ public class ConfigureUmbracoMemberAuthenticationDeliveryApiSwaggerGenOptions : return; } - operation.Security = new List - { - new OpenApiSecurityRequirement - { - { - new OpenApiSecurityScheme - { - Reference = new OpenApiReference - { - Type = ReferenceType.SecurityScheme, - Id = AuthSchemeName, - } - }, - [] - } - } - }; + var schemaRef = new OpenApiSecuritySchemeReference(AuthSchemeName, context.Document); + operation.Security ??= new List(); + operation.Security.Add(new OpenApiSecurityRequirement { [schemaRef] = [] }); } public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) @@ -61,6 +47,8 @@ public class ConfigureUmbracoMemberAuthenticationDeliveryApiSwaggerGenOptions : return; } + swaggerDoc.Components ??= new OpenApiComponents(); + swaggerDoc.Components.SecuritySchemes ??= new Dictionary(); swaggerDoc.Components.SecuritySchemes.Add( AuthSchemeName, new OpenApiSecurityScheme @@ -74,9 +62,9 @@ public class ConfigureUmbracoMemberAuthenticationDeliveryApiSwaggerGenOptions : AuthorizationCode = new OpenApiOAuthFlow { AuthorizationUrl = new Uri(Paths.MemberApi.AuthorizationEndpoint, UriKind.Relative), - TokenUrl = new Uri(Paths.MemberApi.TokenEndpoint, UriKind.Relative) - } - } + TokenUrl = new Uri(Paths.MemberApi.TokenEndpoint, UriKind.Relative), + }, + }, }); } } diff --git a/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerContentDocumentationFilter.cs b/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerContentDocumentationFilter.cs index 014aed28c8..290f6b66dc 100644 --- a/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerContentDocumentationFilter.cs +++ b/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerContentDocumentationFilter.cs @@ -1,9 +1,9 @@ -using Microsoft.OpenApi.Any; -using Microsoft.OpenApi.Models; +using System.Text.Json.Nodes; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; using Umbraco.Cms.Api.Delivery.Configuration; -using Umbraco.Cms.Api.Delivery.Controllers; using Umbraco.Cms.Api.Delivery.Controllers.Content; +using Umbraco.Cms.Core; namespace Umbraco.Cms.Api.Delivery.Filters; @@ -13,7 +13,7 @@ internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFi protected override void ApplyOperation(OpenApiOperation operation, OperationFilterContext context) { - operation.Parameters ??= new List(); + operation.Parameters ??= new List(); AddExpand(operation, context); @@ -21,50 +21,50 @@ internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFi operation.Parameters.Add(new OpenApiParameter { - Name = Core.Constants.DeliveryApi.HeaderNames.AcceptLanguage, + Name = Constants.DeliveryApi.HeaderNames.AcceptLanguage, In = ParameterLocation.Header, Required = false, Description = "Defines the language to return. Use this when querying language variant content items.", - Schema = new OpenApiSchema { Type = "string" }, - Examples = new Dictionary + Schema = new OpenApiSchema { Type = JsonSchemaType.String }, + Examples = new Dictionary { - { "Default", new OpenApiExample { Value = new OpenApiString(string.Empty) } }, - { "English culture", new OpenApiExample { Value = new OpenApiString("en-us") } } - } + { "Default", new OpenApiExample { Value = string.Empty } }, + { "English culture", new OpenApiExample { Value = "en-us" } }, + }, }); operation.Parameters.Add(new OpenApiParameter { - Name = Core.Constants.DeliveryApi.HeaderNames.AcceptSegment, + Name = Constants.DeliveryApi.HeaderNames.AcceptSegment, In = ParameterLocation.Header, Required = false, Description = "Defines the segment to return. Use this when querying segment variant content items.", - Schema = new OpenApiSchema { Type = "string" }, - Examples = new Dictionary + Schema = new OpenApiSchema { Type = JsonSchemaType.String }, + Examples = new Dictionary { - { "Default", new OpenApiExample { Value = new OpenApiString(string.Empty) } }, - { "Segment One", new OpenApiExample { Value = new OpenApiString("segment-one") } } - } + { "Default", new OpenApiExample { Value = string.Empty } }, + { "Segment One", new OpenApiExample { Value = "segment-one" } }, + }, }); AddApiKey(operation); operation.Parameters.Add(new OpenApiParameter { - Name = Core.Constants.DeliveryApi.HeaderNames.Preview, + Name = Constants.DeliveryApi.HeaderNames.Preview, In = ParameterLocation.Header, Required = false, Description = "Whether to request draft content.", - Schema = new OpenApiSchema { Type = "boolean" } + Schema = new OpenApiSchema { Type = JsonSchemaType.Boolean }, }); operation.Parameters.Add(new OpenApiParameter { - Name = Core.Constants.DeliveryApi.HeaderNames.StartItem, + Name = Constants.DeliveryApi.HeaderNames.StartItem, In = ParameterLocation.Header, Required = false, Description = "URL segment or GUID of a root content item.", - Schema = new OpenApiSchema { Type = "string" } + Schema = new OpenApiSchema { Type = JsonSchemaType.String }, }); } @@ -92,105 +92,36 @@ internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFi } } - private Dictionary FetchQueryParameterExamples() => + private Dictionary FetchQueryParameterExamples() => new() { - { "Select all", new OpenApiExample { Value = new OpenApiString(string.Empty) } }, - { - "Select all ancestors of a node by id", - new OpenApiExample { Value = new OpenApiString("ancestors:id") } - }, - { - "Select all ancestors of a node by path", - new OpenApiExample { Value = new OpenApiString("ancestors:path") } - }, - { - "Select all children of a node by id", - new OpenApiExample { Value = new OpenApiString("children:id") } - }, - { - "Select all children of a node by path", - new OpenApiExample { Value = new OpenApiString("children:path") } - }, - { - "Select all descendants of a node by id", - new OpenApiExample { Value = new OpenApiString("descendants:id") } - }, - { - "Select all descendants of a node by path", - new OpenApiExample { Value = new OpenApiString("descendants:path") } - } + { "Select all", new OpenApiExample { Value = string.Empty } }, + { "Select all ancestors of a node by id", new OpenApiExample { Value = "ancestors:id" } }, + { "Select all ancestors of a node by path", new OpenApiExample { Value = "ancestors:path" } }, + { "Select all children of a node by id", new OpenApiExample { Value = "children:id" } }, + { "Select all children of a node by path", new OpenApiExample { Value = "children:path" } }, + { "Select all descendants of a node by id", new OpenApiExample { Value = "descendants:id" } }, + { "Select all descendants of a node by path", new OpenApiExample { Value = "descendants:path" } }, }; - private Dictionary FilterQueryParameterExamples() => + private Dictionary FilterQueryParameterExamples() => new() { - { "Default filter", new OpenApiExample { Value = new OpenApiString(string.Empty) } }, - { - "Filter by content type (equals)", - new OpenApiExample { Value = new OpenApiArray { new OpenApiString("contentType:alias1") } } - }, - { - "Filter by name (contains)", - new OpenApiExample { Value = new OpenApiArray { new OpenApiString("name:nodeName") } } - }, - { - "Filter by creation date (less than)", - new OpenApiExample { Value = new OpenApiArray { new OpenApiString("createDate<2024-01-01") } } - }, - { - "Filter by update date (greater than or equal)", - new OpenApiExample { Value = new OpenApiArray { new OpenApiString("updateDate>:2023-01-01") } } - } + { "Default filter", new OpenApiExample { Value = string.Empty } }, + { "Filter by content type (equals)", new OpenApiExample { Value = new JsonArray { "contentType:alias1" } } }, + { "Filter by name (contains)", new OpenApiExample { Value = new JsonArray { "name:nodeName" } } }, + { "Filter by creation date (less than)", new OpenApiExample { Value = new JsonArray { "createDate<2024-01-01" } } }, + { "Filter by update date (greater than or equal)", new OpenApiExample { Value = new JsonArray { "updateDate>:2023-01-01" } } }, }; - private Dictionary SortQueryParameterExamples() => + private Dictionary SortQueryParameterExamples() => new() { - { "Default sort", new OpenApiExample { Value = new OpenApiString(string.Empty) } }, - { - "Sort by create date", - new OpenApiExample - { - Value = new OpenApiArray - { - new OpenApiString("createDate:asc"), new OpenApiString("createDate:desc") - } - } - }, - { - "Sort by level", - new OpenApiExample - { - Value = new OpenApiArray { new OpenApiString("level:asc"), new OpenApiString("level:desc") } - } - }, - { - "Sort by name", - new OpenApiExample - { - Value = new OpenApiArray { new OpenApiString("name:asc"), new OpenApiString("name:desc") } - } - }, - { - "Sort by sort order", - new OpenApiExample - { - Value = new OpenApiArray - { - new OpenApiString("sortOrder:asc"), new OpenApiString("sortOrder:desc") - } - } - }, - { - "Sort by update date", - new OpenApiExample - { - Value = new OpenApiArray - { - new OpenApiString("updateDate:asc"), new OpenApiString("updateDate:desc") - } - } - } + { "Default sort", new OpenApiExample { Value = string.Empty } }, + { "Sort by create date", new OpenApiExample { Value = new JsonArray { "createDate:asc", "createDate:desc" } } }, + { "Sort by level", new OpenApiExample { Value = new JsonArray { "level:asc", "level:desc" } } }, + { "Sort by name", new OpenApiExample { Value = new JsonArray { "name:asc", "name:desc" } } }, + { "Sort by sort order", new OpenApiExample { Value = new JsonArray { "sortOrder:asc", "sortOrder:desc" } } }, + { "Sort by update date", new OpenApiExample { Value = new JsonArray { "updateDate:asc", "updateDate:desc" } } }, }; } diff --git a/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerDocumentationFilterBase.cs b/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerDocumentationFilterBase.cs index 8a450a2f9b..2776f6a39e 100644 --- a/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerDocumentationFilterBase.cs +++ b/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerDocumentationFilterBase.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Mvc; -using Microsoft.OpenApi.Any; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; +using Umbraco.Cms.Core; namespace Umbraco.Cms.Api.Delivery.Filters; @@ -9,6 +9,8 @@ internal abstract class SwaggerDocumentationFilterBase : SwaggerFilterBase, IOperationFilter, IParameterFilter where TBaseController : Controller { + protected abstract string DocumentationLink { get; } + public void Apply(OpenApiOperation operation, OperationFilterContext context) { if (CanApply(context)) @@ -17,21 +19,19 @@ internal abstract class SwaggerDocumentationFilterBase } } - public void Apply(OpenApiParameter parameter, ParameterFilterContext context) + public void Apply(IOpenApiParameter parameter, ParameterFilterContext context) { - if (CanApply(context)) + if (CanApply(context) && parameter is OpenApiParameter openApiParameter) { - ApplyParameter(parameter, context); + ApplyParameter(openApiParameter, context); } } - protected abstract string DocumentationLink { get; } - protected abstract void ApplyOperation(OpenApiOperation operation, OperationFilterContext context); protected abstract void ApplyParameter(OpenApiParameter parameter, ParameterFilterContext context); - protected void AddQueryParameterDocumentation(OpenApiParameter parameter, Dictionary examples, string description) + protected void AddQueryParameterDocumentation(OpenApiParameter parameter, Dictionary examples, string description) { parameter.Description = QueryParameterDescription(description); parameter.Examples = examples; @@ -60,15 +60,19 @@ internal abstract class SwaggerDocumentationFilterBase AddFields(operation); } - protected void AddApiKey(OpenApiOperation operation) => - operation.Parameters.Add(new OpenApiParameter - { - Name = Core.Constants.DeliveryApi.HeaderNames.ApiKey, - In = ParameterLocation.Header, - Required = false, - Description = "API key specified through configuration to authorize access to the API.", - Schema = new OpenApiSchema { Type = "string" } - }); + protected void AddApiKey(OpenApiOperation operation) + { + operation.Parameters ??= new List(); + operation.Parameters.Add( + new OpenApiParameter + { + Name = Constants.DeliveryApi.HeaderNames.ApiKey, + In = ParameterLocation.Header, + Required = false, + Description = "API key specified through configuration to authorize access to the API.", + Schema = new OpenApiSchema { Type = JsonSchemaType.String }, + }); + } protected string PaginationDescription(bool skip, string itemType) => $"Specifies the number of found {itemType} items to {(skip ? "skip" : "take")}. Use this to control pagination of the response."; @@ -82,78 +86,70 @@ internal abstract class SwaggerDocumentationFilterBase // FIXME: remove this when Delivery API V1 has been removed (expectedly in V15) private void AddExpandV1(OpenApiOperation operation) - => operation.Parameters.Add(new OpenApiParameter - { - Name = "expand", - In = ParameterLocation.Query, - Required = false, - Description = QueryParameterDescription("Defines the properties that should be expanded in the response"), - Schema = new OpenApiSchema { Type = "string" }, - Examples = new Dictionary + { + operation.Parameters ??= new List(); + operation.Parameters.Add( + new OpenApiParameter { - { "Expand none", new OpenApiExample { Value = new OpenApiString(string.Empty) } }, - { "Expand all", new OpenApiExample { Value = new OpenApiString("all") } }, + Name = "expand", + In = ParameterLocation.Query, + Required = false, + Description = + QueryParameterDescription("Defines the properties that should be expanded in the response"), + Schema = new OpenApiSchema { Type = JsonSchemaType.String }, + Examples = new Dictionary { - "Expand specific property", - new OpenApiExample { Value = new OpenApiString("property:alias1") } + { "Expand none", new OpenApiExample { Value = string.Empty } }, + { "Expand all", new OpenApiExample { Value = "all" } }, + { "Expand specific property", new OpenApiExample { Value = "property:alias1" } }, + { "Expand specific properties", new OpenApiExample { Value = "property:alias1,alias2" } }, }, - { - "Expand specific properties", - new OpenApiExample { Value = new OpenApiString("property:alias1,alias2") } - } - } - }); + }); + } private void AddExpand(OpenApiOperation operation) - => operation.Parameters.Add(new OpenApiParameter - { - Name = "expand", - In = ParameterLocation.Query, - Required = false, - Description = QueryParameterDescription("Defines the properties that should be expanded in the response"), - Schema = new OpenApiSchema { Type = "string" }, - Examples = new Dictionary + { + operation.Parameters ??= new List(); + operation.Parameters.Add( + new OpenApiParameter { - { "Expand none", new OpenApiExample { Value = new OpenApiString(string.Empty) } }, - { "Expand all properties", new OpenApiExample { Value = new OpenApiString("properties[$all]") } }, + Name = "expand", + In = ParameterLocation.Query, + Required = false, + Description = + QueryParameterDescription("Defines the properties that should be expanded in the response"), + Schema = new OpenApiSchema { Type = JsonSchemaType.String }, + Examples = new Dictionary { - "Expand specific property", - new OpenApiExample { Value = new OpenApiString("properties[alias1]") } + { "Expand none", new OpenApiExample { Value = string.Empty } }, + { "Expand all properties", new OpenApiExample { Value = "properties[$all]" } }, + { "Expand specific property", new OpenApiExample { Value = "properties[alias1]" } }, + { "Expand specific properties", new OpenApiExample { Value = "properties[alias1,alias2]" } }, + { "Expand nested properties", new OpenApiExample { Value = "properties[alias1[properties[nestedAlias1,nestedAlias2]]]" } }, }, - { - "Expand specific properties", - new OpenApiExample { Value = new OpenApiString("properties[alias1,alias2]") } - }, - { - "Expand nested properties", - new OpenApiExample { Value = new OpenApiString("properties[alias1[properties[nestedAlias1,nestedAlias2]]]") } - } - } - }); + }); + } private void AddFields(OpenApiOperation operation) - => operation.Parameters.Add(new OpenApiParameter - { - Name = "fields", - In = ParameterLocation.Query, - Required = false, - Description = QueryParameterDescription("Explicitly defines which properties should be included in the response (by default all properties are included)"), - Schema = new OpenApiSchema { Type = "string" }, - Examples = new Dictionary + { + operation.Parameters ??= new List(); + operation.Parameters.Add( + new OpenApiParameter { - { "Include all properties", new OpenApiExample { Value = new OpenApiString("properties[$all]") } }, + Name = "fields", + In = ParameterLocation.Query, + Required = false, + Description = + QueryParameterDescription( + "Explicitly defines which properties should be included in the response (by default all properties are included)"), + Schema = new OpenApiSchema { Type = JsonSchemaType.String }, + Examples = new Dictionary { - "Include only specific property", - new OpenApiExample { Value = new OpenApiString("properties[alias1]") } + { "Include all properties", new OpenApiExample { Value = "properties[$all]" } }, + { "Include only specific property", new OpenApiExample { Value = "properties[alias1]" } }, + { "Include only specific properties", new OpenApiExample { Value = "properties[alias1,alias2]" } }, + { "Include only specific nested properties", new OpenApiExample { Value = "properties[alias1[properties[nestedAlias1,nestedAlias2]]]" } }, }, - { - "Include only specific properties", - new OpenApiExample { Value = new OpenApiString("properties[alias1,alias2]") } - }, - { - "Include only specific nested properties", - new OpenApiExample { Value = new OpenApiString("properties[alias1[properties[nestedAlias1,nestedAlias2]]]") } - } - } - }); + }); + } } diff --git a/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerMediaDocumentationFilter.cs b/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerMediaDocumentationFilter.cs index 85ba66e648..a8da861325 100644 --- a/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerMediaDocumentationFilter.cs +++ b/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerMediaDocumentationFilter.cs @@ -1,8 +1,7 @@ -using Microsoft.OpenApi.Any; -using Microsoft.OpenApi.Models; +using System.Text.Json.Nodes; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; using Umbraco.Cms.Api.Delivery.Configuration; -using Umbraco.Cms.Api.Delivery.Controllers; using Umbraco.Cms.Api.Delivery.Controllers.Media; namespace Umbraco.Cms.Api.Delivery.Filters; @@ -13,7 +12,7 @@ internal sealed class SwaggerMediaDocumentationFilter : SwaggerDocumentationFilt protected override void ApplyOperation(OpenApiOperation operation, OperationFilterContext context) { - operation.Parameters ??= new List(); + operation.Parameters ??= new List(); AddExpand(operation, context); @@ -46,77 +45,29 @@ internal sealed class SwaggerMediaDocumentationFilter : SwaggerDocumentationFilt } } - private Dictionary FetchQueryParameterExamples() => + private Dictionary FetchQueryParameterExamples() => new() { - { - "Select all children at root level", - new OpenApiExample { Value = new OpenApiString("children:/") } - }, - { - "Select all children of a media item by id", - new OpenApiExample { Value = new OpenApiString("children:id") } - }, - { - "Select all children of a media item by path", - new OpenApiExample { Value = new OpenApiString("children:path") } - } + { "Select all children at root level", new OpenApiExample { Value = "children:/" } }, + { "Select all children of a media item by id", new OpenApiExample { Value = "children:id" } }, + { "Select all children of a media item by path", new OpenApiExample { Value = "children:path" } }, }; - private Dictionary FilterQueryParameterExamples() => + private Dictionary FilterQueryParameterExamples() => new() { - { "Default filter", new OpenApiExample { Value = new OpenApiString(string.Empty) } }, - { - "Filter by media type", - new OpenApiExample { Value = new OpenApiArray { new OpenApiString("mediaType:alias1") } } - }, - { - "Filter by name", - new OpenApiExample { Value = new OpenApiArray { new OpenApiString("name:nodeName") } } - } + { "Default filter", new OpenApiExample { Value = string.Empty } }, + { "Filter by media type", new OpenApiExample { Value = new JsonArray { "mediaType:alias1" } } }, + { "Filter by name", new OpenApiExample { Value = new JsonArray { "name:nodeName" } } }, }; - private Dictionary SortQueryParameterExamples() => + private Dictionary SortQueryParameterExamples() => new() { - { "Default sort", new OpenApiExample { Value = new OpenApiString(string.Empty) } }, - { - "Sort by create date", - new OpenApiExample - { - Value = new OpenApiArray - { - new OpenApiString("createDate:asc"), new OpenApiString("createDate:desc") - } - } - }, - { - "Sort by name", - new OpenApiExample - { - Value = new OpenApiArray { new OpenApiString("name:asc"), new OpenApiString("name:desc") } - } - }, - { - "Sort by sort order", - new OpenApiExample - { - Value = new OpenApiArray - { - new OpenApiString("sortOrder:asc"), new OpenApiString("sortOrder:desc") - } - } - }, - { - "Sort by update date", - new OpenApiExample - { - Value = new OpenApiArray - { - new OpenApiString("updateDate:asc"), new OpenApiString("updateDate:desc") - } - } - } + { "Default sort", new OpenApiExample { Value = string.Empty } }, + { "Sort by create date", new OpenApiExample { Value = new JsonArray { "createDate:asc", "createDate:desc" } } }, + { "Sort by name", new OpenApiExample { Value = new JsonArray { "name:asc", "name:desc" } } }, + { "Sort by sort order", new OpenApiExample { Value = new JsonArray { "sortOrder:asc", "sortOrder:desc" } } }, + { "Sort by update date", new OpenApiExample { Value = new JsonArray { "updateDate:asc", "updateDate:desc" } } }, }; } diff --git a/src/Umbraco.Cms.Api.Management/Configuration/ConfigureUmbracoManagementApiSwaggerGenOptions.cs b/src/Umbraco.Cms.Api.Management/Configuration/ConfigureUmbracoManagementApiSwaggerGenOptions.cs index 74862e3bab..98ed51f42d 100644 --- a/src/Umbraco.Cms.Api.Management/Configuration/ConfigureUmbracoManagementApiSwaggerGenOptions.cs +++ b/src/Umbraco.Cms.Api.Management/Configuration/ConfigureUmbracoManagementApiSwaggerGenOptions.cs @@ -1,8 +1,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; -using Umbraco.Cms.Api.Common.OpenApi; +using Umbraco.Cms.Api.Common.Security; using Umbraco.Cms.Api.Common.Serialization; using Umbraco.Cms.Api.Management.DependencyInjection; using Umbraco.Cms.Api.Management.OpenApi; @@ -11,7 +11,7 @@ namespace Umbraco.Cms.Api.Management.Configuration; public class ConfigureUmbracoManagementApiSwaggerGenOptions : IConfigureOptions { - private IUmbracoJsonTypeInfoResolver _umbracoJsonTypeInfoResolver; + private readonly IUmbracoJsonTypeInfoResolver _umbracoJsonTypeInfoResolver; public ConfigureUmbracoManagementApiSwaggerGenOptions(IUmbracoJsonTypeInfoResolver umbracoJsonTypeInfoResolver) { @@ -20,7 +20,6 @@ public class ConfigureUmbracoManagementApiSwaggerGenOptions : IConfigureOptions< public void Configure(SwaggerGenOptions swaggerGenOptions) { - swaggerGenOptions.SwaggerDoc( ManagementApiConfiguration.ApiName, new OpenApiInfo @@ -51,10 +50,10 @@ public class ConfigureUmbracoManagementApiSwaggerGenOptions : IConfigureOptions< AuthorizationCode = new OpenApiOAuthFlow { AuthorizationUrl = - new Uri(Common.Security.Paths.BackOfficeApi.AuthorizationEndpoint, UriKind.Relative), - TokenUrl = new Uri(Common.Security.Paths.BackOfficeApi.TokenEndpoint, UriKind.Relative) - } - } + new Uri(Paths.BackOfficeApi.AuthorizationEndpoint, UriKind.Relative), + TokenUrl = new Uri(Paths.BackOfficeApi.TokenEndpoint, UriKind.Relative), + }, + }, }); // Sets Security requirement on backoffice apis diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index f64726a323..86d522dd3a 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -11898,6 +11898,95 @@ ] } }, + "/umbraco/management/api/v1/help": { + "get": { + "tags": [ + "Help" + ], + "operationId": "GetHelp", + "parameters": [ + { + "name": "section", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "tree", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + } + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + } + }, + { + "name": "baseUrl", + "in": "query", + "schema": { + "type": "string", + "default": "https://our.umbraco.com" + } + } + ], + "responses": { + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ProblemDetails" + } + ] + } + } + } + }, + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/PagedHelpPageResponseModel" + } + ] + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + } + }, + "deprecated": true, + "security": [ + { + "Backoffice-User": [ ] + } + ] + } + }, "/umbraco/management/api/v1/imaging/resize/urls": { "get": { "tags": [ @@ -20712,6 +20801,14 @@ "format": "int32", "default": 100 } + }, + { + "name": "foldersOnly", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } } ], "responses": { @@ -20773,6 +20870,14 @@ "type": "integer", "format": "int32" } + }, + { + "name": "foldersOnly", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } } ], "responses": { @@ -37753,9 +37858,6 @@ { "$ref": "#/components/schemas/DocumentPropertyValuePermissionPresentationModel" }, - { - "$ref": "#/components/schemas/DocumentTypePermissionPresentationModel" - }, { "$ref": "#/components/schemas/UnknownTypePermissionPresentationModel" } @@ -38042,9 +38144,6 @@ { "$ref": "#/components/schemas/DocumentPropertyValuePermissionPresentationModel" }, - { - "$ref": "#/components/schemas/DocumentTypePermissionPresentationModel" - }, { "$ref": "#/components/schemas/UnknownTypePermissionPresentationModel" } @@ -39443,36 +39542,6 @@ }, "additionalProperties": false }, - "DocumentTypePermissionPresentationModel": { - "required": [ - "$type", - "documentTypeAlias", - "verbs" - ], - "type": "object", - "properties": { - "$type": { - "type": "string" - }, - "verbs": { - "uniqueItems": true, - "type": "array", - "items": { - "type": "string" - } - }, - "documentTypeAlias": { - "type": "string" - } - }, - "additionalProperties": false, - "discriminator": { - "propertyName": "$type", - "mapping": { - "DocumentTypePermissionPresentationModel": "#/components/schemas/DocumentTypePermissionPresentationModel" - } - } - }, "DocumentTypePropertyTypeContainerResponseModel": { "required": [ "id", @@ -48232,9 +48301,6 @@ { "$ref": "#/components/schemas/DocumentPropertyValuePermissionPresentationModel" }, - { - "$ref": "#/components/schemas/DocumentTypePermissionPresentationModel" - }, { "$ref": "#/components/schemas/UnknownTypePermissionPresentationModel" } @@ -48672,9 +48738,6 @@ { "$ref": "#/components/schemas/DocumentPropertyValuePermissionPresentationModel" }, - { - "$ref": "#/components/schemas/DocumentTypePermissionPresentationModel" - }, { "$ref": "#/components/schemas/UnknownTypePermissionPresentationModel" } @@ -49356,5 +49419,160 @@ } } } - } -} + }, + "tags": [ + { + "name": "Culture" + }, + { + "name": "Data Type" + }, + { + "name": "Dictionary" + }, + { + "name": "Document Blueprint" + }, + { + "name": "Document Type" + }, + { + "name": "Document Version" + }, + { + "name": "Document" + }, + { + "name": "Dynamic Root" + }, + { + "name": "Health Check" + }, + { + "name": "Help" + }, + { + "name": "Imaging" + }, + { + "name": "Import" + }, + { + "name": "Indexer" + }, + { + "name": "Install" + }, + { + "name": "Language" + }, + { + "name": "Log Viewer" + }, + { + "name": "Manifest" + }, + { + "name": "Media Type" + }, + { + "name": "Media" + }, + { + "name": "Member Group" + }, + { + "name": "Member Type" + }, + { + "name": "Member" + }, + { + "name": "Models Builder" + }, + { + "name": "News Dashboard" + }, + { + "name": "Object Types" + }, + { + "name": "oEmbed" + }, + { + "name": "Package" + }, + { + "name": "Partial View" + }, + { + "name": "Preview" + }, + { + "name": "Profiling" + }, + { + "name": "Property Type" + }, + { + "name": "Published Cache" + }, + { + "name": "Redirect Management" + }, + { + "name": "Relation Type" + }, + { + "name": "Relation" + }, + { + "name": "Script" + }, + { + "name": "Searcher" + }, + { + "name": "Security" + }, + { + "name": "Segment" + }, + { + "name": "Server" + }, + { + "name": "Static File" + }, + { + "name": "Stylesheet" + }, + { + "name": "Tag" + }, + { + "name": "Telemetry" + }, + { + "name": "Template" + }, + { + "name": "Temporary File" + }, + { + "name": "Upgrade" + }, + { + "name": "User Data" + }, + { + "name": "User Group" + }, + { + "name": "User" + }, + { + "name": "Webhook" + } + ] +} \ No newline at end of file diff --git a/src/Umbraco.Cms.Api.Management/OpenApi/BackOfficeSecurityRequirementsOperationFilterBase.cs b/src/Umbraco.Cms.Api.Management/OpenApi/BackOfficeSecurityRequirementsOperationFilterBase.cs index e2ff1e609a..bcc069f90b 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi/BackOfficeSecurityRequirementsOperationFilterBase.cs +++ b/src/Umbraco.Cms.Api.Management/OpenApi/BackOfficeSecurityRequirementsOperationFilterBase.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; using Umbraco.Cms.Api.Management.DependencyInjection; using Umbraco.Extensions; @@ -21,27 +21,17 @@ public abstract class BackOfficeSecurityRequirementsOperationFilterBase : IOpera if (!context.MethodInfo.GetCustomAttributes(true).Any(x => x is AllowAnonymousAttribute) && !(context.MethodInfo.DeclaringType?.GetCustomAttributes(true).Any(x => x is AllowAnonymousAttribute) ?? false)) { - operation.Responses.Add(StatusCodes.Status401Unauthorized.ToString(), new OpenApiResponse - { - Description = "The resource is protected and requires an authentication token" - }); - - operation.Security = new List - { - new OpenApiSecurityRequirement + operation.Responses ??= new OpenApiResponses(); + operation.Responses.Add( + StatusCodes.Status401Unauthorized.ToString(), + new OpenApiResponse { - { - new OpenApiSecurityScheme - { - Reference = new OpenApiReference - { - Type = ReferenceType.SecurityScheme, - Id = ManagementApiConfiguration.ApiSecurityName - } - }, [] - } - } - }; + Description = "The resource is protected and requires an authentication token", + }); + + var schemaRef = new OpenApiSecuritySchemeReference(ManagementApiConfiguration.ApiSecurityName, context.Document); + operation.Security ??= new List(); + operation.Security.Add(new OpenApiSecurityRequirement { [schemaRef] = [] }); } // Assuming if and endpoint have more then one AuthorizeAttribute, there is a risk the user do not have access while still being authorized. @@ -57,10 +47,13 @@ public abstract class BackOfficeSecurityRequirementsOperationFilterBase : IOpera if (numberOfAuthorizeAttributes > 2 || hasConstructorInjectingIAuthorizationService) { - operation.Responses.Add(StatusCodes.Status403Forbidden.ToString(), new OpenApiResponse() - { - Description = "The authenticated user does not have access to this resource" - }); + operation.Responses ??= new OpenApiResponses(); + operation.Responses.Add( + StatusCodes.Status403Forbidden.ToString(), + new OpenApiResponse + { + Description = "The authenticated user does not have access to this resource", + }); } } } diff --git a/src/Umbraco.Cms.Api.Management/OpenApi/NotificationHeaderFilter.cs b/src/Umbraco.Cms.Api.Management/OpenApi/NotificationHeaderFilter.cs index f11c350aae..5872c36b45 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi/NotificationHeaderFilter.cs +++ b/src/Umbraco.Cms.Api.Management/OpenApi/NotificationHeaderFilter.cs @@ -1,5 +1,5 @@ using Microsoft.AspNetCore.Http; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; using Umbraco.Cms.Api.Management.ViewModels; using Umbraco.Cms.Core; @@ -24,28 +24,25 @@ internal sealed class NotificationHeaderFilter : IOperationFilter // filter out irrelevant responses (401 will never produce notifications) IEnumerable relevantResponses = operation - .Responses + .Responses? .Where(pair => pair.Key != StatusCodes.Status401Unauthorized.ToString()) - .Select(pair => pair.Value); + .Select(pair => pair.Value) + .OfType() + ?? Enumerable.Empty(); foreach (OpenApiResponse response in relevantResponses) { - response.Headers.TryAdd(Constants.Headers.Notifications, new OpenApiHeader - { - Description = "The list of notifications produced during the request.", - Schema = new OpenApiSchema + response.Headers ??= new Dictionary(); + response.Headers.TryAdd( + Constants.Headers.Notifications, + new OpenApiHeader { - Type = "array", - Nullable = true, - Items = new OpenApiSchema() + Description = "The list of notifications produced during the request.", + Schema = new OpenApiSchema { - Reference = new OpenApiReference() - { - Type = ReferenceType.Schema, - Id = notificationModelType.Name - }, - } - } - }); + Type = JsonSchemaType.Array | JsonSchemaType.Null, + Items = new OpenApiSchemaReference(notificationModelType.Name), + }, + }); } } } diff --git a/src/Umbraco.Cms.Api.Management/OpenApi/ReponseHeaderOperationFilter.cs b/src/Umbraco.Cms.Api.Management/OpenApi/ReponseHeaderOperationFilter.cs index e639a64091..4bbc032398 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi/ReponseHeaderOperationFilter.cs +++ b/src/Umbraco.Cms.Api.Management/OpenApi/ReponseHeaderOperationFilter.cs @@ -1,5 +1,5 @@ using Microsoft.AspNetCore.Http; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; using Umbraco.Cms.Api.Management.DependencyInjection; using Umbraco.Cms.Core; @@ -11,36 +11,36 @@ internal sealed class ResponseHeaderOperationFilter : IOperationFilter { public void Apply(OpenApiOperation operation, OperationFilterContext context) { - if (context.MethodInfo.HasMapToApiAttribute(ManagementApiConfiguration.ApiName) == false) + if (context.MethodInfo.HasMapToApiAttribute(ManagementApiConfiguration.ApiName) is false || operation.Responses is null) { return; } - foreach ((var key, OpenApiResponse? value) in operation.Responses) + foreach ((var key, IOpenApiResponse value) in operation.Responses) { + if (value is not OpenApiResponse openApiResponse) + { + continue; + } + switch (int.Parse(key)) { case StatusCodes.Status201Created: // NOTE: The header order matters to the back-office client. Do not change. - SetHeader(value, Constants.Headers.GeneratedResource, "Identifier of the newly created resource", "string"); - SetHeader(value, Constants.Headers.Location, "Location of the newly created resource", "string", "uri"); + SetHeader(openApiResponse, Constants.Headers.GeneratedResource, "Identifier of the newly created resource", JsonSchemaType.String); + SetHeader(openApiResponse, Constants.Headers.Location, "Location of the newly created resource", JsonSchemaType.String, "uri"); break; } } } - private static void SetHeader(OpenApiResponse value, string headerName, string description, string type, string? format = null) + private static void SetHeader(OpenApiResponse value, string headerName, string description, JsonSchemaType type, string? format = null) { - - if (value.Headers is null) - { - value.Headers = new Dictionary(); - } - - value.Headers[headerName] = new OpenApiHeader() + value.Headers ??= new Dictionary(); + value.Headers[headerName] = new OpenApiHeader { Description = description, - Schema = new OpenApiSchema { Description = description, Type = type, Format = format } + Schema = new OpenApiSchema { Description = description, Type = type, Format = format }, }; } } diff --git a/src/Umbraco.Cms.Api.Management/OpenApi/RequireNonNullablePropertiesSchemaFilter.cs b/src/Umbraco.Cms.Api.Management/OpenApi/RequireNonNullablePropertiesSchemaFilter.cs index 0d06da8bf6..5c957725d3 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi/RequireNonNullablePropertiesSchemaFilter.cs +++ b/src/Umbraco.Cms.Api.Management/OpenApi/RequireNonNullablePropertiesSchemaFilter.cs @@ -1,4 +1,4 @@ -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; namespace Umbraco.Cms.Api.Management.OpenApi; @@ -8,14 +8,21 @@ public class RequireNonNullablePropertiesSchemaFilter : ISchemaFilter /// /// Add to model.Required all properties where Nullable is false. /// - public void Apply(OpenApiSchema model, SchemaFilterContext context) + public void Apply(IOpenApiSchema model, SchemaFilterContext context) { - var additionalRequiredProps = model.Properties - .Where(x => !x.Value.Nullable && !model.Required.Contains(x.Key)) - .Select(x => x.Key); + if (model is not OpenApiSchema schema) + { + return; + } + + IEnumerable additionalRequiredProps = schema.Properties + ?.Where(x => x.Value.Type?.HasFlag(JsonSchemaType.Null) is not true && model.Required?.Contains(x.Key) is not true) + .Select(x => x.Key) + ?? []; + schema.Required ??= new SortedSet(); foreach (var propKey in additionalRequiredProps) { - model.Required.Add(propKey); + schema.Required.Add(propKey); } } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/sdk.gen.ts b/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/sdk.gen.ts index 65dfc31492..3ddc93f1d6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/sdk.gen.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/sdk.gen.ts @@ -1905,6 +1905,9 @@ export class HealthCheckService { } export class HelpService { + /** + * @deprecated + */ public static getHelp(options?: Options) { return (options?.client ?? client).get({ security: [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/types.gen.ts b/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/types.gen.ts index 9ea9d69e56..9be02f73c0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/types.gen.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/types.gen.ts @@ -412,7 +412,7 @@ export type CreateUserGroupRequestModel = { mediaStartNode?: ReferenceByIdModel | null; mediaRootAccess: boolean; fallbackPermissions: Array; - permissions: Array; + permissions: Array; id?: string | null; }; @@ -471,7 +471,7 @@ export type CurrentUserResponseModel = { hasAccessToAllLanguages: boolean; hasAccessToSensitiveData: boolean; fallbackPermissions: Array; - permissions: Array; + permissions: Array; allowedSections: Array; isAdmin: boolean; }; @@ -773,12 +773,6 @@ export type DocumentTypeItemResponseModel = { description?: string | null; }; -export type DocumentTypePermissionPresentationModel = { - $type: string; - verbs: Array; - documentTypeAlias: string; -}; - export type DocumentTypePropertyTypeContainerResponseModel = { id: string; parent?: ReferenceByIdModel | null; @@ -2812,7 +2806,7 @@ export type UpdateUserGroupRequestModel = { mediaStartNode?: ReferenceByIdModel | null; mediaRootAccess: boolean; fallbackPermissions: Array; - permissions: Array; + permissions: Array; }; export type UpdateUserGroupsOnUserRequestModel = { @@ -2913,7 +2907,7 @@ export type UserGroupResponseModel = { mediaStartNode?: ReferenceByIdModel | null; mediaRootAccess: boolean; fallbackPermissions: Array; - permissions: Array; + permissions: Array; id: string; isDeletable: boolean; aliasCanBeChanged: boolean; @@ -10958,6 +10952,7 @@ export type GetTreeMemberTypeRootData = { query?: { skip?: number; take?: number; + foldersOnly?: boolean; }; url: '/umbraco/management/api/v1/tree/member-type/root'; }; @@ -10989,6 +10984,7 @@ export type GetTreeMemberTypeSiblingsData = { target?: string; before?: number; after?: number; + foldersOnly?: boolean; }; url: '/umbraco/management/api/v1/tree/member-type/siblings'; }; diff --git a/templates/UmbracoExtension/Composers/UmbracoExtensionApiComposer.cs b/templates/UmbracoExtension/Composers/UmbracoExtensionApiComposer.cs index 481e1d19bb..e7112e959c 100644 --- a/templates/UmbracoExtension/Composers/UmbracoExtensionApiComposer.cs +++ b/templates/UmbracoExtension/Composers/UmbracoExtensionApiComposer.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.Extensions.Options; using Microsoft.Extensions.DependencyInjection; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.DependencyInjection; diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/OpenApiContractTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/OpenApiContractTest.cs index 3514f32d2b..6d74f5ff5e 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/OpenApiContractTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/OpenApiContractTest.cs @@ -1596,7 +1596,15 @@ internal sealed class OpenApiContractTest : UmbracoTestServerTestBase "additionalProperties": { } } } - } + }, + "tags": [ + { + "name": "Content" + }, + { + "name": "Media" + } + ] } """; }