From bce3f59afe851db0b0ac9e95ee200b36d42a8552 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 3 Oct 2023 14:56:29 +0530 Subject: [PATCH 01/20] adding support for query parameter in open api spec --- .../Services/OpenAPI/OpenApiDocumentor.cs | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs index ca8d2dcd01..7ede274f4e 100644 --- a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs +++ b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs @@ -285,6 +285,7 @@ private Dictionary CreateOperations( // which is returned when using Enum.ToString("D"). // The "D" format specified "displays the enumeration entry as an integer value in the shortest representation possible." OpenApiOperation getOperation = CreateBaseOperation(description: GETONE_DESCRIPTION, tags: tags); + getOperation.Parameters = CreateTableAndViewQueryParameters(); getOperation.Responses.Add(HttpStatusCode.OK.ToString("D"), CreateOpenApiResponse(description: nameof(HttpStatusCode.OK), responseObjectSchemaName: entityName)); openApiPathItemOperations.Add(OperationType.Get, getOperation); @@ -317,6 +318,7 @@ private Dictionary CreateOperations( { // Primary key(s) are not included in the URI paths of the GET (all) and POST operations. OpenApiOperation getAllOperation = CreateBaseOperation(description: GETALL_DESCRIPTION, tags: tags); + getAllOperation.Parameters = CreateTableAndViewQueryParameters(); getAllOperation.Responses.Add( HttpStatusCode.OK.ToString("D"), CreateOpenApiResponse(description: nameof(HttpStatusCode.OK), responseObjectSchemaName: entityName, includeNextLink: true)); @@ -360,6 +362,7 @@ private Dictionary CreateStoredProcedureOperati if (configuredRestOperations[OperationType.Get]) { OpenApiOperation getOperation = CreateBaseOperation(description: SP_EXECUTE_DESCRIPTION, tags: tags); + getOperation.Parameters = CreateStoredProcedureParameters((StoredProcedureDefinition)sourceDefinition); getOperation.Responses.Add( HttpStatusCode.OK.ToString("D"), CreateOpenApiResponse( @@ -429,6 +432,103 @@ private OpenApiOperation CreateBaseOperation(string description, List + /// This method creates a list of OpenApiParameter objects for the parameters of a stored procedure. + /// + private static List CreateStoredProcedureParameters(StoredProcedureDefinition spDefinition) + { + List parameters = new(); + + foreach ((string paramKey, ParameterDefinition parameterDefinition) in spDefinition.Parameters) + { + parameters.Add( + GetOpenApiParameter( + name: paramKey, + description: "Query parameter for stored procedure parameter", + required: !parameterDefinition.HasConfigDefault, + type: TypeHelper.GetJsonDataTypeFromSystemType(parameterDefinition.SystemType).ToString().ToLower() + ) + ); + } + + return parameters; + } + + /// + /// + /// + private static List CreateTableAndViewQueryParameters() + { + List parameters = new (); + + // Add $select query parameter + parameters.Add( + GetOpenApiParameter( + name: "$select", + description: "The query parameter $select allow to specify which fields must be returned.", + required: false, + type: "string" + ) + ); + + // Add $filter query parameter + parameters.Add( + GetOpenApiParameter( + name: "$filter", + description: "The value of the $filter option is predicate expression (an expression that returns a boolean value) using entity's fields.", + required: false, + type: "string" + ) + ); + + // Add $orderby query parameter + parameters.Add( + GetOpenApiParameter( + name: "$orderby", + description: "The value of the orderby parameter is a comma-separated list of expressions used to sort the items.", + required: false, + type: "string" + ) + ); + + // Add $first query parameter + parameters.Add( + GetOpenApiParameter( + name: "$first", + description: "The value of the $first option is an integer value that specifies the number of items to return.", + required: false, + type: "integer" + ) + ); + + // Add $after query parameter + parameters.Add( + GetOpenApiParameter( + name: "$after", + description: "The value of the $after option is a base64 encoded string value that specifies the cursor position after which items should be returned.", + required: false, + type: "string" + ) + ); + + return parameters; + } + + private static OpenApiParameter GetOpenApiParameter(string name, string description, bool required, string type) + { + return new OpenApiParameter + { + Name = name, + In = ParameterLocation.Query, + Description = description, + Required = required, + Schema = new OpenApiSchema + { + Type = type + } + }; } /// From fd7635e76ab68d693aec9dd3e6370770c48d0036 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 3 Oct 2023 18:15:27 +0530 Subject: [PATCH 02/20] fix formatting --- src/Core/Services/OpenAPI/OpenApiDocumentor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs index 7ede274f4e..e8c207b8df 100644 --- a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs +++ b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs @@ -432,7 +432,7 @@ private OpenApiOperation CreateBaseOperation(string description, List /// This method creates a list of OpenApiParameter objects for the parameters of a stored procedure. @@ -461,7 +461,7 @@ private static List CreateStoredProcedureParameters(StoredProc /// private static List CreateTableAndViewQueryParameters() { - List parameters = new (); + List parameters = new(); // Add $select query parameter parameters.Add( From c4c0fda5bb7136eaff44347d40e9068bd2b60913 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 4 Oct 2023 21:56:03 +0530 Subject: [PATCH 03/20] adding tests --- .../Services/OpenAPI/OpenApiDocumentor.cs | 36 ++++++--- .../OpenApiDocumentor/PathValidation.cs | 74 ------------------- 2 files changed, 26 insertions(+), 84 deletions(-) delete mode 100644 src/Service.Tests/OpenApiDocumentor/PathValidation.cs diff --git a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs index e8c207b8df..837ee7cf3d 100644 --- a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs +++ b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs @@ -376,6 +376,7 @@ private Dictionary CreateStoredProcedureOperati { // POST requests for stored procedure entities must include primary key(s) in request body. OpenApiOperation postOperation = CreateBaseOperation(description: SP_EXECUTE_DESCRIPTION, tags: tags); + postOperation.Parameters = CreateStoredProcedureParameters((StoredProcedureDefinition)sourceDefinition); postOperation.RequestBody = CreateOpenApiRequestBodyPayload(spRequestObjectSchemaName, IsRequestBodyRequired(sourceDefinition, considerPrimaryKeys: true, isStoredProcedure: true)); postOperation.Responses.Add(HttpStatusCode.Created.ToString("D"), CreateOpenApiResponse(description: nameof(HttpStatusCode.Created), responseObjectSchemaName: spResponseObjectSchemaName)); postOperation.Responses.Add(HttpStatusCode.Conflict.ToString("D"), CreateOpenApiResponse(description: nameof(HttpStatusCode.Conflict))); @@ -389,6 +390,7 @@ private Dictionary CreateStoredProcedureOperati { // PUT requests for stored procedure entities must include primary key(s) in request body. OpenApiOperation putOperation = CreateBaseOperation(description: SP_EXECUTE_DESCRIPTION, tags: tags); + putOperation.Parameters = CreateStoredProcedureParameters((StoredProcedureDefinition)sourceDefinition); putOperation.RequestBody = CreateOpenApiRequestBodyPayload(spRequestObjectSchemaName, requestBodyRequired); putOperation.Responses.Add(HttpStatusCode.OK.ToString("D"), CreateOpenApiResponse(description: nameof(HttpStatusCode.OK), responseObjectSchemaName: spResponseObjectSchemaName)); putOperation.Responses.Add(HttpStatusCode.Created.ToString("D"), CreateOpenApiResponse(description: nameof(HttpStatusCode.Created), responseObjectSchemaName: spResponseObjectSchemaName)); @@ -399,6 +401,7 @@ private Dictionary CreateStoredProcedureOperati { // PATCH requests for stored procedure entities must include primary key(s) in request body OpenApiOperation patchOperation = CreateBaseOperation(description: SP_EXECUTE_DESCRIPTION, tags: tags); + patchOperation.Parameters = CreateStoredProcedureParameters((StoredProcedureDefinition)sourceDefinition); patchOperation.RequestBody = CreateOpenApiRequestBodyPayload(spRequestObjectSchemaName, requestBodyRequired); patchOperation.Responses.Add(HttpStatusCode.OK.ToString("D"), CreateOpenApiResponse(description: nameof(HttpStatusCode.OK), responseObjectSchemaName: spResponseObjectSchemaName)); patchOperation.Responses.Add(HttpStatusCode.Created.ToString("D"), CreateOpenApiResponse(description: nameof(HttpStatusCode.Created), responseObjectSchemaName: spResponseObjectSchemaName)); @@ -408,6 +411,7 @@ private Dictionary CreateStoredProcedureOperati if (configuredRestOperations[OperationType.Delete]) { OpenApiOperation deleteOperation = CreateBaseOperation(description: SP_EXECUTE_DESCRIPTION, tags: tags); + deleteOperation.Parameters = CreateStoredProcedureParameters((StoredProcedureDefinition)sourceDefinition); deleteOperation.Responses.Add(HttpStatusCode.NoContent.ToString("D"), CreateOpenApiResponse(description: nameof(HttpStatusCode.NoContent))); openApiPathItemOperations.Add(OperationType.Delete, deleteOperation); } @@ -435,7 +439,8 @@ private OpenApiOperation CreateBaseOperation(string description, List - /// This method creates a list of OpenApiParameter objects for the parameters of a stored procedure. + /// This method creates a list of OpenApiParameter objects for the query parameters of a stored procedure. + /// A query parameter will be marked REQUIRED if default value is not available. /// private static List CreateStoredProcedureParameters(StoredProcedureDefinition spDefinition) { @@ -444,7 +449,7 @@ private static List CreateStoredProcedureParameters(StoredProc foreach ((string paramKey, ParameterDefinition parameterDefinition) in spDefinition.Parameters) { parameters.Add( - GetOpenApiParameter( + GetOpenApiQueryParameter( name: paramKey, description: "Query parameter for stored procedure parameter", required: !parameterDefinition.HasConfigDefault, @@ -457,15 +462,18 @@ private static List CreateStoredProcedureParameters(StoredProc } /// - /// - /// + /// Creates a list of OpenAPI parameters for querying tables and views. + /// The query parameters include $select, $filter, $orderby, $first, and $after, which allow the user to specify which fields to return, + /// filter the results based on a predicate expression, sort the results, and paginate the results. + /// + /// A list of OpenAPI parameters. private static List CreateTableAndViewQueryParameters() { List parameters = new(); // Add $select query parameter parameters.Add( - GetOpenApiParameter( + GetOpenApiQueryParameter( name: "$select", description: "The query parameter $select allow to specify which fields must be returned.", required: false, @@ -475,7 +483,7 @@ private static List CreateTableAndViewQueryParameters() // Add $filter query parameter parameters.Add( - GetOpenApiParameter( + GetOpenApiQueryParameter( name: "$filter", description: "The value of the $filter option is predicate expression (an expression that returns a boolean value) using entity's fields.", required: false, @@ -485,7 +493,7 @@ private static List CreateTableAndViewQueryParameters() // Add $orderby query parameter parameters.Add( - GetOpenApiParameter( + GetOpenApiQueryParameter( name: "$orderby", description: "The value of the orderby parameter is a comma-separated list of expressions used to sort the items.", required: false, @@ -495,7 +503,7 @@ private static List CreateTableAndViewQueryParameters() // Add $first query parameter parameters.Add( - GetOpenApiParameter( + GetOpenApiQueryParameter( name: "$first", description: "The value of the $first option is an integer value that specifies the number of items to return.", required: false, @@ -505,7 +513,7 @@ private static List CreateTableAndViewQueryParameters() // Add $after query parameter parameters.Add( - GetOpenApiParameter( + GetOpenApiQueryParameter( name: "$after", description: "The value of the $after option is a base64 encoded string value that specifies the cursor position after which items should be returned.", required: false, @@ -516,7 +524,15 @@ private static List CreateTableAndViewQueryParameters() return parameters; } - private static OpenApiParameter GetOpenApiParameter(string name, string description, bool required, string type) + /// + /// Creates a new OpenAPI query parameter with the specified name, description, required flag, and data type. + /// + /// The name of the query parameter. + /// The description of the query parameter. + /// A flag indicating whether the query parameter is required. + /// The data type of the query parameter. + /// A new OpenAPI query parameter. + private static OpenApiParameter GetOpenApiQueryParameter(string name, string description, bool required, string type) { return new OpenApiParameter { diff --git a/src/Service.Tests/OpenApiDocumentor/PathValidation.cs b/src/Service.Tests/OpenApiDocumentor/PathValidation.cs deleted file mode 100644 index 5ebf8c0ee2..0000000000 --- a/src/Service.Tests/OpenApiDocumentor/PathValidation.cs +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Collections.Generic; -using System.Threading.Tasks; -using Azure.DataApiBuilder.Config.ObjectModel; -using Microsoft.OpenApi.Models; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Azure.DataApiBuilder.Service.Tests.OpenApiIntegration -{ - /// - /// Integration tests validating correct keys are created for OpenApiDocument.Paths - /// which represent the path used to access an entity in DAB's REST API endpoint. - /// - [TestCategory(TestCategory.MSSQL)] - [TestClass] - public class PathValidation - { - private const string CUSTOM_CONFIG = "path-config.MsSql.json"; - private const string MSSQL_ENVIRONMENT = TestCategory.MSSQL; - - // Error messages - private const string PATH_GENERATION_ERROR = "Unexpected path value for entity in OpenAPI description document: "; - - /// - /// Validates that the OpenApiDocument object's Paths property for an entity is generated - /// with the entity's explicitly configured REST path, if set. Otherwise, the top level - /// entity name is used. - /// When OpenApiDocumentor.BuildPaths() is called, the entityBasePathComponent is created using - /// the formula "/{entityRestPath}" where {entityRestPath} has no starting slashes and is either - /// the entity name or the explicitly configured entity REST path - /// - /// Top level entity name defined in runtime config. - /// Entity's configured REST path. - /// Expected path generated for OpenApiDocument.Paths with format: "/{entityRestPath}" - [DataRow("entity", "/customEntityPath", "/customEntityPath", DisplayName = "Entity REST path has leading slash - REST path override used.")] - [DataRow("entity", "//customEntityPath", "/customEntityPath", DisplayName = "Entity REST path has two leading slashes - REST path override used.")] - [DataRow("entity", "///customEntityPath", "/customEntityPath", DisplayName = "Entity REST path has many leading slashes - REST path override used.")] - [DataRow("entity", "customEntityPath", "/customEntityPath", DisplayName = "Entity REST path has no leading slash(es) - REST path override used.")] - [DataRow("entity", "", "/entity", DisplayName = "Entity REST path is an emtpy string - top level entity name used.")] - [DataRow("entity", null, "/entity", DisplayName = "Entity REST path is null - top level entity name used.")] - [DataTestMethod] - public async Task ValidateEntityRestPath(string entityName, string configuredRestPath, string expectedOpenApiPath) - { - Entity entity = new( - Source: new(Object: "books", EntitySourceType.Table, null, null), - GraphQL: new(Singular: null, Plural: null, Enabled: false), - Rest: new(Methods: EntityRestOptions.DEFAULT_SUPPORTED_VERBS, Path: configuredRestPath), - Permissions: OpenApiTestBootstrap.CreateBasicPermissions(), - Mappings: null, - Relationships: null); - - Dictionary entities = new() - { - { entityName, entity } - }; - - RuntimeEntities runtimeEntities = new(entities); - OpenApiDocument openApiDocument = await OpenApiTestBootstrap.GenerateOpenApiDocument( - runtimeEntities: runtimeEntities, - configFileName: CUSTOM_CONFIG, - databaseEnvironment: MSSQL_ENVIRONMENT); - - // For a given table backed entity, there will be two paths: - // 1. GetById path: "/customEntityPath/id/{id}" - // 2. GetAll path: "/customEntityPath" - foreach (string actualPath in openApiDocument.Paths.Keys) - { - Assert.AreEqual(expected: true, actual: actualPath.StartsWith(expectedOpenApiPath), message: PATH_GENERATION_ERROR + actualPath); - } - } - } -} From 65377f60d25c919b744d4b0d121c080e7d9c4da0 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 4 Oct 2023 21:56:17 +0530 Subject: [PATCH 04/20] adding tests --- .../ParameterValidationTests.cs | 198 ++++++++++++++++++ .../OpenApiDocumentor/PathValidationTests.cs | 74 +++++++ 2 files changed, 272 insertions(+) create mode 100644 src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs create mode 100644 src/Service.Tests/OpenApiDocumentor/PathValidationTests.cs diff --git a/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs b/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs new file mode 100644 index 0000000000..81a7bd1a6f --- /dev/null +++ b/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs @@ -0,0 +1,198 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Azure.DataApiBuilder.Config.ObjectModel; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Azure.DataApiBuilder.Service.Tests.OpenApiIntegration +{ + /// + /// Integration tests validating correct path and query parameters are created + /// for different entities in DAB's REST API endpoint. + /// + [TestCategory(TestCategory.MSSQL)] + [TestClass] + public class ParameterValidationTests + { + private const string CUSTOM_CONFIG = "parameter-config.MsSql.json"; + private const string MSSQL_ENVIRONMENT = TestCategory.MSSQL; + + /// + /// Validates the path parameters are correctly generated for GET methods. + /// + /// + /// + /// + /// + [DataTestMethod] + [DataRow("BooksTable", "books", EntitySourceType.Table, DisplayName = "Table")] + [DataRow("BooksView", "books_view_all", EntitySourceType.View, DisplayName = "View")] + public async Task ValidatePathParametersForTablesAndViews(string entityName, string objectName, EntitySourceType entitySourceType) + { + Entity entity = new( + Source: new(Object: objectName, entitySourceType, null, null), + GraphQL: new(Singular: null, Plural: null, Enabled: false), + Rest: new(Methods: EntityRestOptions.DEFAULT_SUPPORTED_VERBS), + Permissions: OpenApiTestBootstrap.CreateBasicPermissions(), + Mappings: null, + Relationships: null); + + Dictionary entities = new() + { + { entityName, entity } + }; + + RuntimeEntities runtimeEntities = new(entities); + OpenApiDocument openApiDocument = await OpenApiTestBootstrap.GenerateOpenApiDocument( + runtimeEntities: runtimeEntities, + configFileName: CUSTOM_CONFIG, + databaseEnvironment: MSSQL_ENVIRONMENT); + + foreach ((string pathName, OpenApiPathItem pathItem) in openApiDocument.Paths) + { + string apiPathWithParam = $"/{entityName}/id/{{id}}"; + if (pathName.Equals(apiPathWithParam)) + { + Assert.IsTrue(pathItem.Parameters.Count is 1); + OpenApiParameter pathParameter = pathItem.Parameters.First(); + Assert.AreEqual("id", pathParameter.Name); + Assert.AreEqual(ParameterLocation.Path, pathParameter.In); + Assert.IsTrue(pathParameter.Required); + } + else + { + // Get All method with path /entityName, will have no path parameters. + Assert.IsTrue(pathItem.Parameters.Count is 0); + } + } + } + + /// + /// Validates that Query parameters are correctly generated for GET methods in Table/Views. + /// $select, $filter, $orderby, $first, $after are the query parameters. + /// + /// + /// + /// + /// + [DataTestMethod] + [DataRow("BooksTable", "books", EntitySourceType.Table, DisplayName = "Table")] + [DataRow("BooksView", "books_view_all", EntitySourceType.View, DisplayName = "View")] + public async Task ValidateQueryParametersForTablesAndViews(string entityName, string objectName, EntitySourceType entitySourceType) + { + Entity entity = new( + Source: new(Object: objectName, entitySourceType, null, null), + GraphQL: new(Singular: null, Plural: null, Enabled: false), + Rest: new(Methods: EntityRestOptions.DEFAULT_SUPPORTED_VERBS), + Permissions: OpenApiTestBootstrap.CreateBasicPermissions(), + Mappings: null, + Relationships: null); + + Dictionary entities = new() + { + { entityName, entity } + }; + + RuntimeEntities runtimeEntities = new(entities); + OpenApiDocument openApiDocument = await OpenApiTestBootstrap.GenerateOpenApiDocument( + runtimeEntities: runtimeEntities, + configFileName: CUSTOM_CONFIG, + databaseEnvironment: MSSQL_ENVIRONMENT); + + foreach (OpenApiPathItem pathItem in openApiDocument.Paths.Values) + { + foreach ((OperationType operationType, OpenApiOperation operation) in pathItem.Operations) + { + if (operationType is OperationType.Get) + { + Assert.IsTrue(operation.Parameters.Count is 5); + + // Assert that it contains all the query parameters with appropriate type + Assert.IsTrue(operation.Parameters.Any(param => param.In is ParameterLocation.Query && param.Name is "$select" && param.Schema.Type is "string")); + Assert.IsTrue(operation.Parameters.Any(param => param.In is ParameterLocation.Query && param.Name is "$filter" && param.Schema.Type is "string")); + Assert.IsTrue(operation.Parameters.Any(param => param.In is ParameterLocation.Query && param.Name is "$orderby" && param.Schema.Type is "string")); + Assert.IsTrue(operation.Parameters.Any(param => param.In is ParameterLocation.Query && param.Name is "$first" && param.Schema.Type is "integer")); + Assert.IsTrue(operation.Parameters.Any(param => param.In is ParameterLocation.Query && param.Name is "$after" && param.Schema.Type is "string")); + } + else + { + // No query parameters for other OperationTypes. + Assert.IsFalse(operation.Parameters.Any(param => param.In is ParameterLocation.Query)); + } + } + } + } + + /// + /// Validates that Query parameters are correctly generated for Stored Procedures. + /// Query Parameters for stored procedure will be generated for irrespective of the Operation Type. + /// If a parameter default value is not provided in the config, it will marked as required. + /// + /// + /// + /// + [DataTestMethod] + [DataRow("CountBooks", "count_books", DisplayName = "StoredProcedure with no parameters")] + [DataRow("UpdateBookTitle", "update_book_title", DisplayName = "StoredProcedure with parameters")] + public async Task ValidateQueryParametersForStoredProcedures(string entityName, string objectName) + { + Dictionary parameterDefaults = null; + if (entityName is "UpdateBookTitle") + { + // Adding a parameter default value. + parameterDefaults = new Dictionary { { "title", "Test Title" } }; + } + + Entity entity = new( + Source: new(Object: objectName, EntitySourceType.StoredProcedure, parameterDefaults, null), + GraphQL: new(Singular: null, Plural: null, Enabled: false), + Rest: new(Methods: EntityRestOptions.DEFAULT_SUPPORTED_VERBS), + Permissions: OpenApiTestBootstrap.CreateBasicPermissions(), + Mappings: null, + Relationships: null); + + Dictionary entities = new() + { + { entityName, entity } + }; + + RuntimeEntities runtimeEntities = new(entities); + OpenApiDocument openApiDocument = await OpenApiTestBootstrap.GenerateOpenApiDocument( + runtimeEntities: runtimeEntities, + configFileName: CUSTOM_CONFIG, + databaseEnvironment: MSSQL_ENVIRONMENT); + + OpenApiPathItem pathItem = openApiDocument.Paths.First().Value; + foreach (OpenApiOperation operation in pathItem.Operations.Values) + { + if (entityName is "UpdateBookTitle") + { + Assert.IsTrue(operation.Parameters.Any(param => + param.In is ParameterLocation.Query + && param.Name is "id" + && param.Schema.Type is "number" + && param.Required is true + )); + + // Parameter with default value will be an optional query parameter. + Assert.IsTrue(operation.Parameters.Any(param => + param.In is ParameterLocation.Query + && param.Name is "title" + && param.Schema.Type is "string" + && param.Required is false)); + } + else + { + // CountBookTitle doesn't require any query parameters. + Assert.IsTrue(operation.Parameters.IsNullOrEmpty()); + } + + } + } + } +} diff --git a/src/Service.Tests/OpenApiDocumentor/PathValidationTests.cs b/src/Service.Tests/OpenApiDocumentor/PathValidationTests.cs new file mode 100644 index 0000000000..46be369d58 --- /dev/null +++ b/src/Service.Tests/OpenApiDocumentor/PathValidationTests.cs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Threading.Tasks; +using Azure.DataApiBuilder.Config.ObjectModel; +using Microsoft.OpenApi.Models; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Azure.DataApiBuilder.Service.Tests.OpenApiIntegration +{ + /// + /// Integration tests validating correct keys are created for OpenApiDocument.Paths + /// which represent the path used to access an entity in DAB's REST API endpoint. + /// + [TestCategory(TestCategory.MSSQL)] + [TestClass] + public class PathValidationTests + { + private const string CUSTOM_CONFIG = "path-config.MsSql.json"; + private const string MSSQL_ENVIRONMENT = TestCategory.MSSQL; + + // Error messages + private const string PATH_GENERATION_ERROR = "Unexpected path value for entity in OpenAPI description document: "; + + /// + /// Validates that the OpenApiDocument object's Paths property for an entity is generated + /// with the entity's explicitly configured REST path, if set. Otherwise, the top level + /// entity name is used. + /// When OpenApiDocumentor.BuildPaths() is called, the entityBasePathComponent is created using + /// the formula "/{entityRestPath}" where {entityRestPath} has no starting slashes and is either + /// the entity name or the explicitly configured entity REST path + /// + /// Top level entity name defined in runtime config. + /// Entity's configured REST path. + /// Expected path generated for OpenApiDocument.Paths with format: "/{entityRestPath}" + [DataRow("entity", "/customEntityPath", "/customEntityPath", DisplayName = "Entity REST path has leading slash - REST path override used.")] + [DataRow("entity", "//customEntityPath", "/customEntityPath", DisplayName = "Entity REST path has two leading slashes - REST path override used.")] + [DataRow("entity", "///customEntityPath", "/customEntityPath", DisplayName = "Entity REST path has many leading slashes - REST path override used.")] + [DataRow("entity", "customEntityPath", "/customEntityPath", DisplayName = "Entity REST path has no leading slash(es) - REST path override used.")] + [DataRow("entity", "", "/entity", DisplayName = "Entity REST path is an emtpy string - top level entity name used.")] + [DataRow("entity", null, "/entity", DisplayName = "Entity REST path is null - top level entity name used.")] + [DataTestMethod] + public async Task ValidateEntityRestPath(string entityName, string configuredRestPath, string expectedOpenApiPath) + { + Entity entity = new( + Source: new(Object: "books", EntitySourceType.Table, null, null), + GraphQL: new(Singular: null, Plural: null, Enabled: false), + Rest: new(Methods: EntityRestOptions.DEFAULT_SUPPORTED_VERBS, Path: configuredRestPath), + Permissions: OpenApiTestBootstrap.CreateBasicPermissions(), + Mappings: null, + Relationships: null); + + Dictionary entities = new() + { + { entityName, entity } + }; + + RuntimeEntities runtimeEntities = new(entities); + OpenApiDocument openApiDocument = await OpenApiTestBootstrap.GenerateOpenApiDocument( + runtimeEntities: runtimeEntities, + configFileName: CUSTOM_CONFIG, + databaseEnvironment: MSSQL_ENVIRONMENT); + + // For a given table backed entity, there will be two paths: + // 1. GetById path: "/customEntityPath/id/{id}" + // 2. GetAll path: "/customEntityPath" + foreach (string actualPath in openApiDocument.Paths.Keys) + { + Assert.AreEqual(expected: true, actual: actualPath.StartsWith(expectedOpenApiPath), message: PATH_GENERATION_ERROR + actualPath); + } + } + } +} From f8f13f292323ff075258c3ce1296bd1ad600cb77 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 10 Oct 2023 13:03:30 +0530 Subject: [PATCH 05/20] fix formatting --- .../ParameterValidationTests.cs | 296 ++++++++---------- 1 file changed, 137 insertions(+), 159 deletions(-) diff --git a/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs b/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs index 81a7bd1a6f..bab9421615 100644 --- a/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs +++ b/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs @@ -9,190 +9,168 @@ using Microsoft.OpenApi.Models; using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace Azure.DataApiBuilder.Service.Tests.OpenApiIntegration +namespace Azure.DataApiBuilder.Service.Tests.OpenApiIntegration; + +/// +/// Integration tests validating correct path and query parameters are created +/// for different entities in DAB's REST API endpoint. +/// +[TestCategory(TestCategory.MSSQL)] +[TestClass] +public class ParameterValidationTests { + private const string CUSTOM_CONFIG = "parameter-config.MsSql.json"; + private const string MSSQL_ENVIRONMENT = TestCategory.MSSQL; + /// - /// Integration tests validating correct path and query parameters are created - /// for different entities in DAB's REST API endpoint. + /// Validates the path parameters are correctly generated for GET methods. /// - [TestCategory(TestCategory.MSSQL)] - [TestClass] - public class ParameterValidationTests + /// The name of the entity. + /// The name of the database object. + /// The source type of the entity. + /// + [DataTestMethod] + [DataRow("BooksTable", "books", EntitySourceType.Table, DisplayName = "Table with path parameter id")] + [DataRow("BooksView", "books_view_all", EntitySourceType.View, DisplayName = "View with path parameter id")] + public async Task ValidatePathParametersForTablesAndViews(string entityName, string objectName, EntitySourceType entitySourceType) { - private const string CUSTOM_CONFIG = "parameter-config.MsSql.json"; - private const string MSSQL_ENVIRONMENT = TestCategory.MSSQL; - - /// - /// Validates the path parameters are correctly generated for GET methods. - /// - /// - /// - /// - /// - [DataTestMethod] - [DataRow("BooksTable", "books", EntitySourceType.Table, DisplayName = "Table")] - [DataRow("BooksView", "books_view_all", EntitySourceType.View, DisplayName = "View")] - public async Task ValidatePathParametersForTablesAndViews(string entityName, string objectName, EntitySourceType entitySourceType) + EntitySource entitySource = new(Object: objectName, entitySourceType, null, null); + OpenApiDocument openApiDocument = await GenerateOpenApiDocumentForGivenEntity(entityName, entitySource); + + foreach ((string pathName, OpenApiPathItem pathItem) in openApiDocument.Paths) { - Entity entity = new( - Source: new(Object: objectName, entitySourceType, null, null), - GraphQL: new(Singular: null, Plural: null, Enabled: false), - Rest: new(Methods: EntityRestOptions.DEFAULT_SUPPORTED_VERBS), - Permissions: OpenApiTestBootstrap.CreateBasicPermissions(), - Mappings: null, - Relationships: null); - - Dictionary entities = new() + string apiPathWithParam = $"/{entityName}/id/{{id}}"; + if (pathName.Equals(apiPathWithParam)) + { + Assert.IsTrue(pathItem.Parameters.Count is 1); + OpenApiParameter pathParameter = pathItem.Parameters.First(); + Assert.AreEqual("id", pathParameter.Name); + Assert.AreEqual(ParameterLocation.Path, pathParameter.In); + Assert.IsTrue(pathParameter.Required); + } + else { - { entityName, entity } - }; + // Get All method with path /entityName, will have no path parameters. + Assert.IsTrue(pathItem.Parameters.Count is 0); + } + } + } - RuntimeEntities runtimeEntities = new(entities); - OpenApiDocument openApiDocument = await OpenApiTestBootstrap.GenerateOpenApiDocument( - runtimeEntities: runtimeEntities, - configFileName: CUSTOM_CONFIG, - databaseEnvironment: MSSQL_ENVIRONMENT); + /// + /// Validates that Query parameters are correctly generated for GET methods in Table/Views. + /// $select, $filter, $orderby, $first, $after are the query parameters. + /// + /// The name of the entity. + /// The name of the database object. + /// The source type of the entity. + /// + [DataTestMethod] + [DataRow("BooksTable", "books", EntitySourceType.Table, DisplayName = "Table with query parameters")] + [DataRow("BooksView", "books_view_all", EntitySourceType.View, DisplayName = "View with query parameters")] + public async Task ValidateQueryParametersForTablesAndViews(string entityName, string objectName, EntitySourceType entitySourceType) + { + EntitySource entitySource = new(Object: objectName, entitySourceType, null, null); + OpenApiDocument openApiDocument = await GenerateOpenApiDocumentForGivenEntity(entityName, entitySource); - foreach ((string pathName, OpenApiPathItem pathItem) in openApiDocument.Paths) + foreach (OpenApiPathItem pathItem in openApiDocument.Paths.Values) + { + foreach ((OperationType operationType, OpenApiOperation operation) in pathItem.Operations) { - string apiPathWithParam = $"/{entityName}/id/{{id}}"; - if (pathName.Equals(apiPathWithParam)) + if (operationType is OperationType.Get) { - Assert.IsTrue(pathItem.Parameters.Count is 1); - OpenApiParameter pathParameter = pathItem.Parameters.First(); - Assert.AreEqual("id", pathParameter.Name); - Assert.AreEqual(ParameterLocation.Path, pathParameter.In); - Assert.IsTrue(pathParameter.Required); + Assert.IsTrue(operation.Parameters.Count is 5); + + // Assert that it contains all the query parameters with appropriate type + Assert.IsTrue(operation.Parameters.Any(param => param.In is ParameterLocation.Query && param.Name is "$select" && param.Schema.Type is "string")); + Assert.IsTrue(operation.Parameters.Any(param => param.In is ParameterLocation.Query && param.Name is "$filter" && param.Schema.Type is "string")); + Assert.IsTrue(operation.Parameters.Any(param => param.In is ParameterLocation.Query && param.Name is "$orderby" && param.Schema.Type is "string")); + Assert.IsTrue(operation.Parameters.Any(param => param.In is ParameterLocation.Query && param.Name is "$first" && param.Schema.Type is "integer")); + Assert.IsTrue(operation.Parameters.Any(param => param.In is ParameterLocation.Query && param.Name is "$after" && param.Schema.Type is "string")); } else { - // Get All method with path /entityName, will have no path parameters. - Assert.IsTrue(pathItem.Parameters.Count is 0); + // No query parameters for other OperationTypes. + Assert.IsFalse(operation.Parameters.Any(param => param.In is ParameterLocation.Query)); } } } + } - /// - /// Validates that Query parameters are correctly generated for GET methods in Table/Views. - /// $select, $filter, $orderby, $first, $after are the query parameters. - /// - /// - /// - /// - /// - [DataTestMethod] - [DataRow("BooksTable", "books", EntitySourceType.Table, DisplayName = "Table")] - [DataRow("BooksView", "books_view_all", EntitySourceType.View, DisplayName = "View")] - public async Task ValidateQueryParametersForTablesAndViews(string entityName, string objectName, EntitySourceType entitySourceType) + /// + /// Validates that Query parameters are correctly generated for Stored Procedures. + /// Query parameters for stored procedures will be generated for each operation irrespective of its Operation Type. + /// If a parameter default value is not provided in the config, it will marked as required. + /// + /// The name of the entity. + /// The name of the database object. + /// + [DataTestMethod] + [DataRow("CountBooks", "count_books", DisplayName = "StoredProcedure with no parameters")] + [DataRow("UpdateBookTitle", "update_book_title", DisplayName = "StoredProcedure with parameters")] + public async Task ValidateQueryParametersForStoredProcedures(string entityName, string objectName) + { + Dictionary parameterDefaults = null; + if (entityName is "UpdateBookTitle") { - Entity entity = new( - Source: new(Object: objectName, entitySourceType, null, null), - GraphQL: new(Singular: null, Plural: null, Enabled: false), - Rest: new(Methods: EntityRestOptions.DEFAULT_SUPPORTED_VERBS), - Permissions: OpenApiTestBootstrap.CreateBasicPermissions(), - Mappings: null, - Relationships: null); - - Dictionary entities = new() - { - { entityName, entity } - }; - - RuntimeEntities runtimeEntities = new(entities); - OpenApiDocument openApiDocument = await OpenApiTestBootstrap.GenerateOpenApiDocument( - runtimeEntities: runtimeEntities, - configFileName: CUSTOM_CONFIG, - databaseEnvironment: MSSQL_ENVIRONMENT); - - foreach (OpenApiPathItem pathItem in openApiDocument.Paths.Values) - { - foreach ((OperationType operationType, OpenApiOperation operation) in pathItem.Operations) - { - if (operationType is OperationType.Get) - { - Assert.IsTrue(operation.Parameters.Count is 5); - - // Assert that it contains all the query parameters with appropriate type - Assert.IsTrue(operation.Parameters.Any(param => param.In is ParameterLocation.Query && param.Name is "$select" && param.Schema.Type is "string")); - Assert.IsTrue(operation.Parameters.Any(param => param.In is ParameterLocation.Query && param.Name is "$filter" && param.Schema.Type is "string")); - Assert.IsTrue(operation.Parameters.Any(param => param.In is ParameterLocation.Query && param.Name is "$orderby" && param.Schema.Type is "string")); - Assert.IsTrue(operation.Parameters.Any(param => param.In is ParameterLocation.Query && param.Name is "$first" && param.Schema.Type is "integer")); - Assert.IsTrue(operation.Parameters.Any(param => param.In is ParameterLocation.Query && param.Name is "$after" && param.Schema.Type is "string")); - } - else - { - // No query parameters for other OperationTypes. - Assert.IsFalse(operation.Parameters.Any(param => param.In is ParameterLocation.Query)); - } - } - } + // Adding a parameter default value. + parameterDefaults = new Dictionary { { "title", "Test Title" } }; } - /// - /// Validates that Query parameters are correctly generated for Stored Procedures. - /// Query Parameters for stored procedure will be generated for irrespective of the Operation Type. - /// If a parameter default value is not provided in the config, it will marked as required. - /// - /// - /// - /// - [DataTestMethod] - [DataRow("CountBooks", "count_books", DisplayName = "StoredProcedure with no parameters")] - [DataRow("UpdateBookTitle", "update_book_title", DisplayName = "StoredProcedure with parameters")] - public async Task ValidateQueryParametersForStoredProcedures(string entityName, string objectName) + EntitySource entitySource = new(Object: objectName, EntitySourceType.StoredProcedure, parameterDefaults, null); + OpenApiDocument openApiDocument = await GenerateOpenApiDocumentForGivenEntity(entityName, entitySource); + + OpenApiPathItem pathItem = openApiDocument.Paths.First().Value; + foreach (OpenApiOperation operation in pathItem.Operations.Values) { - Dictionary parameterDefaults = null; - if (entityName is "UpdateBookTitle") + if (entityName.Equals("UpdateBookTitle")) { - // Adding a parameter default value. - parameterDefaults = new Dictionary { { "title", "Test Title" } }; + Assert.IsTrue(operation.Parameters.Any(param => + param.In is ParameterLocation.Query + && param.Name is "id" + && param.Schema.Type is "number" + && param.Required is true + )); + + // Parameter with default value will be an optional query parameter. + Assert.IsTrue(operation.Parameters.Any(param => + param.In is ParameterLocation.Query + && param.Name is "title" + && param.Schema.Type is "string" + && param.Required is false)); } - - Entity entity = new( - Source: new(Object: objectName, EntitySourceType.StoredProcedure, parameterDefaults, null), - GraphQL: new(Singular: null, Plural: null, Enabled: false), - Rest: new(Methods: EntityRestOptions.DEFAULT_SUPPORTED_VERBS), - Permissions: OpenApiTestBootstrap.CreateBasicPermissions(), - Mappings: null, - Relationships: null); - - Dictionary entities = new() - { - { entityName, entity } - }; - - RuntimeEntities runtimeEntities = new(entities); - OpenApiDocument openApiDocument = await OpenApiTestBootstrap.GenerateOpenApiDocument( - runtimeEntities: runtimeEntities, - configFileName: CUSTOM_CONFIG, - databaseEnvironment: MSSQL_ENVIRONMENT); - - OpenApiPathItem pathItem = openApiDocument.Paths.First().Value; - foreach (OpenApiOperation operation in pathItem.Operations.Values) + else { - if (entityName is "UpdateBookTitle") - { - Assert.IsTrue(operation.Parameters.Any(param => - param.In is ParameterLocation.Query - && param.Name is "id" - && param.Schema.Type is "number" - && param.Required is true - )); - - // Parameter with default value will be an optional query parameter. - Assert.IsTrue(operation.Parameters.Any(param => - param.In is ParameterLocation.Query - && param.Name is "title" - && param.Schema.Type is "string" - && param.Required is false)); - } - else - { - // CountBookTitle doesn't require any query parameters. - Assert.IsTrue(operation.Parameters.IsNullOrEmpty()); - } - + // CountBookTitle doesn't require any query parameters. + Assert.IsTrue(operation.Parameters.IsNullOrEmpty()); } + } } + + /// + /// Generate and return the OpenApiDocument for a single entity. + /// + /// The name of the entity. + /// Database source for entity. + private async static Task GenerateOpenApiDocumentForGivenEntity(string entityName, EntitySource entitySource) + { + Entity entity = new( + Source: entitySource, + GraphQL: new(Singular: null, Plural: null, Enabled: false), + Rest: new(Methods: EntityRestOptions.DEFAULT_SUPPORTED_VERBS), + Permissions: OpenApiTestBootstrap.CreateBasicPermissions(), + Mappings: null, + Relationships: null); + + Dictionary entities = new() + { + { entityName, entity } + }; + + RuntimeEntities runtimeEntities = new(entities); + return await OpenApiTestBootstrap.GenerateOpenApiDocument( + runtimeEntities: runtimeEntities, + configFileName: CUSTOM_CONFIG, + databaseEnvironment: MSSQL_ENVIRONMENT); + } } From 9b93c14ee6baf90a70a84953687d03689d01fd8d Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Fri, 13 Oct 2023 17:13:06 +0530 Subject: [PATCH 06/20] fixing nits --- .../Services/OpenAPI/OpenApiDocumentor.cs | 31 +++++----- .../OpenApiDocumentor/OpenApiTestBootstrap.cs | 2 +- .../ParameterValidationTests.cs | 59 +++++++++++-------- .../OpenApiDocumentor/PathValidationTests.cs | 2 +- .../StoredProcedureGeneration.cs | 2 +- 5 files changed, 53 insertions(+), 43 deletions(-) diff --git a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs index 837ee7cf3d..391fccafbe 100644 --- a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs +++ b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs @@ -45,6 +45,9 @@ public class OpenApiDocumentor : IOpenApiDocumentor // Routing constant public const string OPENAPI_ROUTE = "openapi"; + // OpenApi query parameters + private static readonly List _tableAndViewQueryParameters = CreateTableAndViewQueryParameters(); + // Error messages public const string DOCUMENT_ALREADY_GENERATED_ERROR = "OpenAPI description document already generated."; public const string DOCUMENT_CREATION_UNSUPPORTED_ERROR = "OpenAPI description document can't be created when the REST endpoint is disabled globally."; @@ -285,7 +288,7 @@ private Dictionary CreateOperations( // which is returned when using Enum.ToString("D"). // The "D" format specified "displays the enumeration entry as an integer value in the shortest representation possible." OpenApiOperation getOperation = CreateBaseOperation(description: GETONE_DESCRIPTION, tags: tags); - getOperation.Parameters = CreateTableAndViewQueryParameters(); + getOperation.Parameters = _tableAndViewQueryParameters; getOperation.Responses.Add(HttpStatusCode.OK.ToString("D"), CreateOpenApiResponse(description: nameof(HttpStatusCode.OK), responseObjectSchemaName: entityName)); openApiPathItemOperations.Add(OperationType.Get, getOperation); @@ -318,7 +321,7 @@ private Dictionary CreateOperations( { // Primary key(s) are not included in the URI paths of the GET (all) and POST operations. OpenApiOperation getAllOperation = CreateBaseOperation(description: GETALL_DESCRIPTION, tags: tags); - getAllOperation.Parameters = CreateTableAndViewQueryParameters(); + getAllOperation.Parameters = _tableAndViewQueryParameters; getAllOperation.Responses.Add( HttpStatusCode.OK.ToString("D"), CreateOpenApiResponse(description: nameof(HttpStatusCode.OK), responseObjectSchemaName: entityName, includeNextLink: true)); @@ -362,7 +365,7 @@ private Dictionary CreateStoredProcedureOperati if (configuredRestOperations[OperationType.Get]) { OpenApiOperation getOperation = CreateBaseOperation(description: SP_EXECUTE_DESCRIPTION, tags: tags); - getOperation.Parameters = CreateStoredProcedureParameters((StoredProcedureDefinition)sourceDefinition); + getOperation.Parameters = CreateStoredProcedureInputParameters((StoredProcedureDefinition)sourceDefinition); getOperation.Responses.Add( HttpStatusCode.OK.ToString("D"), CreateOpenApiResponse( @@ -376,7 +379,6 @@ private Dictionary CreateStoredProcedureOperati { // POST requests for stored procedure entities must include primary key(s) in request body. OpenApiOperation postOperation = CreateBaseOperation(description: SP_EXECUTE_DESCRIPTION, tags: tags); - postOperation.Parameters = CreateStoredProcedureParameters((StoredProcedureDefinition)sourceDefinition); postOperation.RequestBody = CreateOpenApiRequestBodyPayload(spRequestObjectSchemaName, IsRequestBodyRequired(sourceDefinition, considerPrimaryKeys: true, isStoredProcedure: true)); postOperation.Responses.Add(HttpStatusCode.Created.ToString("D"), CreateOpenApiResponse(description: nameof(HttpStatusCode.Created), responseObjectSchemaName: spResponseObjectSchemaName)); postOperation.Responses.Add(HttpStatusCode.Conflict.ToString("D"), CreateOpenApiResponse(description: nameof(HttpStatusCode.Conflict))); @@ -390,7 +392,6 @@ private Dictionary CreateStoredProcedureOperati { // PUT requests for stored procedure entities must include primary key(s) in request body. OpenApiOperation putOperation = CreateBaseOperation(description: SP_EXECUTE_DESCRIPTION, tags: tags); - putOperation.Parameters = CreateStoredProcedureParameters((StoredProcedureDefinition)sourceDefinition); putOperation.RequestBody = CreateOpenApiRequestBodyPayload(spRequestObjectSchemaName, requestBodyRequired); putOperation.Responses.Add(HttpStatusCode.OK.ToString("D"), CreateOpenApiResponse(description: nameof(HttpStatusCode.OK), responseObjectSchemaName: spResponseObjectSchemaName)); putOperation.Responses.Add(HttpStatusCode.Created.ToString("D"), CreateOpenApiResponse(description: nameof(HttpStatusCode.Created), responseObjectSchemaName: spResponseObjectSchemaName)); @@ -401,7 +402,6 @@ private Dictionary CreateStoredProcedureOperati { // PATCH requests for stored procedure entities must include primary key(s) in request body OpenApiOperation patchOperation = CreateBaseOperation(description: SP_EXECUTE_DESCRIPTION, tags: tags); - patchOperation.Parameters = CreateStoredProcedureParameters((StoredProcedureDefinition)sourceDefinition); patchOperation.RequestBody = CreateOpenApiRequestBodyPayload(spRequestObjectSchemaName, requestBodyRequired); patchOperation.Responses.Add(HttpStatusCode.OK.ToString("D"), CreateOpenApiResponse(description: nameof(HttpStatusCode.OK), responseObjectSchemaName: spResponseObjectSchemaName)); patchOperation.Responses.Add(HttpStatusCode.Created.ToString("D"), CreateOpenApiResponse(description: nameof(HttpStatusCode.Created), responseObjectSchemaName: spResponseObjectSchemaName)); @@ -411,7 +411,6 @@ private Dictionary CreateStoredProcedureOperati if (configuredRestOperations[OperationType.Delete]) { OpenApiOperation deleteOperation = CreateBaseOperation(description: SP_EXECUTE_DESCRIPTION, tags: tags); - deleteOperation.Parameters = CreateStoredProcedureParameters((StoredProcedureDefinition)sourceDefinition); deleteOperation.Responses.Add(HttpStatusCode.NoContent.ToString("D"), CreateOpenApiResponse(description: nameof(HttpStatusCode.NoContent))); openApiPathItemOperations.Add(OperationType.Delete, deleteOperation); } @@ -439,10 +438,10 @@ private OpenApiOperation CreateBaseOperation(string description, List - /// This method creates a list of OpenApiParameter objects for the query parameters of a stored procedure. - /// A query parameter will be marked REQUIRED if default value is not available. + /// This method creates a list of OpenApiParameter objects for the input parameters of a stored procedure. + /// A input parameter will be marked REQUIRED if default value is not available. /// - private static List CreateStoredProcedureParameters(StoredProcedureDefinition spDefinition) + private static List CreateStoredProcedureInputParameters(StoredProcedureDefinition spDefinition) { List parameters = new(); @@ -451,7 +450,7 @@ private static List CreateStoredProcedureParameters(StoredProc parameters.Add( GetOpenApiQueryParameter( name: paramKey, - description: "Query parameter for stored procedure parameter", + description: "Input parameter for stored procedure parameter", required: !parameterDefinition.HasConfigDefault, type: TypeHelper.GetJsonDataTypeFromSystemType(parameterDefinition.SystemType).ToString().ToLower() ) @@ -475,7 +474,7 @@ private static List CreateTableAndViewQueryParameters() parameters.Add( GetOpenApiQueryParameter( name: "$select", - description: "The query parameter $select allow to specify which fields must be returned.", + description: "A comma separated list of fields to return in the response.", required: false, type: "string" ) @@ -485,7 +484,7 @@ private static List CreateTableAndViewQueryParameters() parameters.Add( GetOpenApiQueryParameter( name: "$filter", - description: "The value of the $filter option is predicate expression (an expression that returns a boolean value) using entity's fields.", + description: "A predicate expression (an expression that returns a boolean value) using entity's fields.", required: false, type: "string" ) @@ -495,7 +494,7 @@ private static List CreateTableAndViewQueryParameters() parameters.Add( GetOpenApiQueryParameter( name: "$orderby", - description: "The value of the orderby parameter is a comma-separated list of expressions used to sort the items.", + description: "A comma-separated list of expressions used to sort the items.", required: false, type: "string" ) @@ -505,7 +504,7 @@ private static List CreateTableAndViewQueryParameters() parameters.Add( GetOpenApiQueryParameter( name: "$first", - description: "The value of the $first option is an integer value that specifies the number of items to return.", + description: "An integer value that specifies the number of items to return.", required: false, type: "integer" ) @@ -515,7 +514,7 @@ private static List CreateTableAndViewQueryParameters() parameters.Add( GetOpenApiQueryParameter( name: "$after", - description: "The value of the $after option is a base64 encoded string value that specifies the cursor position after which items should be returned.", + description: "A base64 encoded string that specifies the cursor position after which items should be returned.", required: false, type: "string" ) diff --git a/src/Service.Tests/OpenApiDocumentor/OpenApiTestBootstrap.cs b/src/Service.Tests/OpenApiDocumentor/OpenApiTestBootstrap.cs index 21ff38fa5c..0b5cf19d9e 100644 --- a/src/Service.Tests/OpenApiDocumentor/OpenApiTestBootstrap.cs +++ b/src/Service.Tests/OpenApiDocumentor/OpenApiTestBootstrap.cs @@ -28,7 +28,7 @@ internal class OpenApiTestBootstrap /// /// /// Generated OpenApiDocument - internal static async Task GenerateOpenApiDocument( + internal static async Task GenerateOpenApiDocumentAsync( RuntimeEntities runtimeEntities, string configFileName, string databaseEnvironment) diff --git a/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs b/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs index bab9421615..f168e1fd95 100644 --- a/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs +++ b/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs @@ -12,8 +12,13 @@ namespace Azure.DataApiBuilder.Service.Tests.OpenApiIntegration; /// -/// Integration tests validating correct path and query parameters are created +/// Integration tests validating path and query parameters are created /// for different entities in DAB's REST API endpoint. +/// Path parameters are defined in the path of a URL using curly braces {}. +/// For example, in the URL /books/{id}, id is a path parameter. +/// Query parameters are defined in the query string of a URL using the ? character +/// followed by a list of key-value pairs separated by & characters. +/// For example, in the URL /books?author=John+Doe&page=2, author and page are query parameters. /// [TestCategory(TestCategory.MSSQL)] [TestClass] @@ -23,19 +28,18 @@ public class ParameterValidationTests private const string MSSQL_ENVIRONMENT = TestCategory.MSSQL; /// - /// Validates the path parameters are correctly generated for GET methods. + /// Validates the path parameters for GET methods. /// /// The name of the entity. /// The name of the database object. /// The source type of the entity. - /// [DataTestMethod] [DataRow("BooksTable", "books", EntitySourceType.Table, DisplayName = "Table with path parameter id")] [DataRow("BooksView", "books_view_all", EntitySourceType.View, DisplayName = "View with path parameter id")] public async Task ValidatePathParametersForTablesAndViews(string entityName, string objectName, EntitySourceType entitySourceType) { EntitySource entitySource = new(Object: objectName, entitySourceType, null, null); - OpenApiDocument openApiDocument = await GenerateOpenApiDocumentForGivenEntity(entityName, entitySource); + OpenApiDocument openApiDocument = await GenerateOpenApiDocumentForGivenEntityAsync(entityName, entitySource); foreach ((string pathName, OpenApiPathItem pathItem) in openApiDocument.Paths) { @@ -50,27 +54,26 @@ public async Task ValidatePathParametersForTablesAndViews(string entityName, str } else { - // Get All method with path /entityName, will have no path parameters. + // Get All and POST method with path /entityName, will have no path parameters. Assert.IsTrue(pathItem.Parameters.Count is 0); } } } /// - /// Validates that Query parameters are correctly generated for GET methods in Table/Views. + /// Validates that Query parameters are generated for GET methods in Table/Views. /// $select, $filter, $orderby, $first, $after are the query parameters. /// /// The name of the entity. /// The name of the database object. /// The source type of the entity. - /// [DataTestMethod] [DataRow("BooksTable", "books", EntitySourceType.Table, DisplayName = "Table with query parameters")] [DataRow("BooksView", "books_view_all", EntitySourceType.View, DisplayName = "View with query parameters")] public async Task ValidateQueryParametersForTablesAndViews(string entityName, string objectName, EntitySourceType entitySourceType) { EntitySource entitySource = new(Object: objectName, entitySourceType, null, null); - OpenApiDocument openApiDocument = await GenerateOpenApiDocumentForGivenEntity(entityName, entitySource); + OpenApiDocument openApiDocument = await GenerateOpenApiDocumentForGivenEntityAsync(entityName, entitySource); foreach (OpenApiPathItem pathItem in openApiDocument.Paths.Values) { @@ -97,17 +100,17 @@ public async Task ValidateQueryParametersForTablesAndViews(string entityName, st } /// - /// Validates that Query parameters are correctly generated for Stored Procedures. - /// Query parameters for stored procedures will be generated for each operation irrespective of its Operation Type. + /// Validates that Input parameters are generated for Stored Procedures with GET operation. + /// Input parameters for stored procedures will be generated for GET operation. /// If a parameter default value is not provided in the config, it will marked as required. + /// It also validates parameter metadatas like type, name, location, required, etc. /// /// The name of the entity. /// The name of the database object. - /// [DataTestMethod] [DataRow("CountBooks", "count_books", DisplayName = "StoredProcedure with no parameters")] [DataRow("UpdateBookTitle", "update_book_title", DisplayName = "StoredProcedure with parameters")] - public async Task ValidateQueryParametersForStoredProcedures(string entityName, string objectName) + public async Task ValidateInputParametersForStoredProcedures(string entityName, string objectName) { Dictionary parameterDefaults = null; if (entityName is "UpdateBookTitle") @@ -117,33 +120,41 @@ public async Task ValidateQueryParametersForStoredProcedures(string entityName, } EntitySource entitySource = new(Object: objectName, EntitySourceType.StoredProcedure, parameterDefaults, null); - OpenApiDocument openApiDocument = await GenerateOpenApiDocumentForGivenEntity(entityName, entitySource); + OpenApiDocument openApiDocument = await GenerateOpenApiDocumentForGivenEntityAsync(entityName, entitySource); OpenApiPathItem pathItem = openApiDocument.Paths.First().Value; - foreach (OpenApiOperation operation in pathItem.Operations.Values) + foreach ((OperationType operationType, OpenApiOperation operation) in pathItem.Operations) { if (entityName.Equals("UpdateBookTitle")) { - Assert.IsTrue(operation.Parameters.Any(param => + if (operationType is OperationType.Get) + { + Assert.IsTrue(operation.Parameters.Any(param => param.In is ParameterLocation.Query && param.Name is "id" && param.Schema.Type is "number" && param.Required is true )); - // Parameter with default value will be an optional query parameter. - Assert.IsTrue(operation.Parameters.Any(param => - param.In is ParameterLocation.Query - && param.Name is "title" - && param.Schema.Type is "string" - && param.Required is false)); + // Parameter with default value will be an optional query parameter. + Assert.IsTrue(operation.Parameters.Any(param => + param.In is ParameterLocation.Query + && param.Name is "title" + && param.Schema.Type is "string" + && param.Required is false)); + } + else + { + // Input Parameters are supported only for GET requests + // For Other requests requestBody is used. + Assert.IsTrue(operation.Parameters.IsNullOrEmpty()); + } } else { // CountBookTitle doesn't require any query parameters. Assert.IsTrue(operation.Parameters.IsNullOrEmpty()); } - } } @@ -152,7 +163,7 @@ param.In is ParameterLocation.Query /// /// The name of the entity. /// Database source for entity. - private async static Task GenerateOpenApiDocumentForGivenEntity(string entityName, EntitySource entitySource) + private async static Task GenerateOpenApiDocumentForGivenEntityAsync(string entityName, EntitySource entitySource) { Entity entity = new( Source: entitySource, @@ -168,7 +179,7 @@ private async static Task GenerateOpenApiDocumentForGivenEntity }; RuntimeEntities runtimeEntities = new(entities); - return await OpenApiTestBootstrap.GenerateOpenApiDocument( + return await OpenApiTestBootstrap.GenerateOpenApiDocumentAsync( runtimeEntities: runtimeEntities, configFileName: CUSTOM_CONFIG, databaseEnvironment: MSSQL_ENVIRONMENT); diff --git a/src/Service.Tests/OpenApiDocumentor/PathValidationTests.cs b/src/Service.Tests/OpenApiDocumentor/PathValidationTests.cs index 46be369d58..bff1333497 100644 --- a/src/Service.Tests/OpenApiDocumentor/PathValidationTests.cs +++ b/src/Service.Tests/OpenApiDocumentor/PathValidationTests.cs @@ -57,7 +57,7 @@ public async Task ValidateEntityRestPath(string entityName, string configuredRes }; RuntimeEntities runtimeEntities = new(entities); - OpenApiDocument openApiDocument = await OpenApiTestBootstrap.GenerateOpenApiDocument( + OpenApiDocument openApiDocument = await OpenApiTestBootstrap.GenerateOpenApiDocumentAsync( runtimeEntities: runtimeEntities, configFileName: CUSTOM_CONFIG, databaseEnvironment: MSSQL_ENVIRONMENT); diff --git a/src/Service.Tests/OpenApiDocumentor/StoredProcedureGeneration.cs b/src/Service.Tests/OpenApiDocumentor/StoredProcedureGeneration.cs index 3c5d4b4b6b..01ee1ac0a9 100644 --- a/src/Service.Tests/OpenApiDocumentor/StoredProcedureGeneration.cs +++ b/src/Service.Tests/OpenApiDocumentor/StoredProcedureGeneration.cs @@ -41,7 +41,7 @@ public class StoredProcedureGeneration public static async Task ClassInitialize(TestContext context) { CreateEntities(); - _openApiDocument = await OpenApiTestBootstrap.GenerateOpenApiDocument( + _openApiDocument = await OpenApiTestBootstrap.GenerateOpenApiDocumentAsync( runtimeEntities: _runtimeEntities, configFileName: CUSTOM_CONFIG, databaseEnvironment: MSSQL_ENVIRONMENT); From d352410fa05dcc9cca46d890682c6e668540e98b Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Fri, 13 Oct 2023 17:16:12 +0530 Subject: [PATCH 07/20] fixing nits --- .../OpenApiDocumentor/ParameterValidationTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs b/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs index f168e1fd95..6d1931f024 100644 --- a/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs +++ b/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs @@ -108,8 +108,8 @@ public async Task ValidateQueryParametersForTablesAndViews(string entityName, st /// The name of the entity. /// The name of the database object. [DataTestMethod] - [DataRow("CountBooks", "count_books", DisplayName = "StoredProcedure with no parameters")] - [DataRow("UpdateBookTitle", "update_book_title", DisplayName = "StoredProcedure with parameters")] + [DataRow("CountBooks", "count_books", DisplayName = "StoredProcudure with no parameters results in 0 created input query params.")] + [DataRow("UpdateBookTitle", "update_book_title", DisplayName = "StoredProcedure with parameters results in created input query params.")] public async Task ValidateInputParametersForStoredProcedures(string entityName, string objectName) { Dictionary parameterDefaults = null; From 3b0042c9f02707a23af039c85b643a6838685dac Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Mon, 30 Oct 2023 17:04:50 +0530 Subject: [PATCH 08/20] fixing nits --- .../ParameterValidationTests.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs b/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs index 6d1931f024..a863669177 100644 --- a/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs +++ b/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs @@ -84,11 +84,11 @@ public async Task ValidateQueryParametersForTablesAndViews(string entityName, st Assert.IsTrue(operation.Parameters.Count is 5); // Assert that it contains all the query parameters with appropriate type - Assert.IsTrue(operation.Parameters.Any(param => param.In is ParameterLocation.Query && param.Name is "$select" && param.Schema.Type is "string")); - Assert.IsTrue(operation.Parameters.Any(param => param.In is ParameterLocation.Query && param.Name is "$filter" && param.Schema.Type is "string")); - Assert.IsTrue(operation.Parameters.Any(param => param.In is ParameterLocation.Query && param.Name is "$orderby" && param.Schema.Type is "string")); - Assert.IsTrue(operation.Parameters.Any(param => param.In is ParameterLocation.Query && param.Name is "$first" && param.Schema.Type is "integer")); - Assert.IsTrue(operation.Parameters.Any(param => param.In is ParameterLocation.Query && param.Name is "$after" && param.Schema.Type is "string")); + Assert.IsTrue(operation.Parameters.Any(param => param.In is ParameterLocation.Query && param.Name.Equals("$select") && param.Schema.Type.Equals("string"))); + Assert.IsTrue(operation.Parameters.Any(param => param.In is ParameterLocation.Query && param.Name.Equals("$filter") && param.Schema.Type.Equals("string"))); + Assert.IsTrue(operation.Parameters.Any(param => param.In is ParameterLocation.Query && param.Name.Equals("$orderby") && param.Schema.Type.Equals("string"))); + Assert.IsTrue(operation.Parameters.Any(param => param.In is ParameterLocation.Query && param.Name.Equals("$first") && param.Schema.Type.Equals("integer"))); + Assert.IsTrue(operation.Parameters.Any(param => param.In is ParameterLocation.Query && param.Name.Equals("$after") && param.Schema.Type.Equals("string"))); } else { @@ -113,7 +113,7 @@ public async Task ValidateQueryParametersForTablesAndViews(string entityName, st public async Task ValidateInputParametersForStoredProcedures(string entityName, string objectName) { Dictionary parameterDefaults = null; - if (entityName is "UpdateBookTitle") + if (entityName.Equals("UpdateBookTitle")) { // Adding a parameter default value. parameterDefaults = new Dictionary { { "title", "Test Title" } }; @@ -131,16 +131,16 @@ public async Task ValidateInputParametersForStoredProcedures(string entityName, { Assert.IsTrue(operation.Parameters.Any(param => param.In is ParameterLocation.Query - && param.Name is "id" - && param.Schema.Type is "number" + && param.Name.Equals("id") + && param.Schema.Type.Equals("number") && param.Required is true )); // Parameter with default value will be an optional query parameter. Assert.IsTrue(operation.Parameters.Any(param => param.In is ParameterLocation.Query - && param.Name is "title" - && param.Schema.Type is "string" + && param.Name.Equals("title") + && param.Schema.Type.Equals("string") && param.Required is false)); } else From ca9981f1e9d6206cd21796cb05ff0c7fe54e6efa Mon Sep 17 00:00:00 2001 From: Abhishek Kumar <102276754+abhishekkumams@users.noreply.github.com> Date: Mon, 6 Nov 2023 15:49:25 +0530 Subject: [PATCH 09/20] Update src/Core/Services/OpenAPI/OpenApiDocumentor.cs Co-authored-by: Sean Leonard --- src/Core/Services/OpenAPI/OpenApiDocumentor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs index 3a9da95ad2..ba2ed6d350 100644 --- a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs +++ b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs @@ -494,7 +494,7 @@ private static List CreateTableAndViewQueryParameters() parameters.Add( GetOpenApiQueryParameter( name: "$orderby", - description: "A comma-separated list of expressions used to sort the items.", + description: "A comma-separated list of expressions used to sort items in the response.", required: false, type: "string" ) From bfe9dc66abdd1500782694fbdda5261239d1c369 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar <102276754+abhishekkumams@users.noreply.github.com> Date: Mon, 6 Nov 2023 15:49:36 +0530 Subject: [PATCH 10/20] Update src/Core/Services/OpenAPI/OpenApiDocumentor.cs Co-authored-by: Sean Leonard --- src/Core/Services/OpenAPI/OpenApiDocumentor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs index ba2ed6d350..45b5d67352 100644 --- a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs +++ b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs @@ -484,7 +484,7 @@ private static List CreateTableAndViewQueryParameters() parameters.Add( GetOpenApiQueryParameter( name: "$filter", - description: "A predicate expression (an expression that returns a boolean value) using entity's fields.", + description: "A predicate expression (an expression that returns a boolean value) using the entity's fields to retrieve a subset of the results.", required: false, type: "string" ) From 114ffcfa4181718e583e3abc13a756a130cfd759 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar <102276754+abhishekkumams@users.noreply.github.com> Date: Mon, 6 Nov 2023 15:51:30 +0530 Subject: [PATCH 11/20] Update src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs Co-authored-by: Sean Leonard --- src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs b/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs index a863669177..6e6223ab2f 100644 --- a/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs +++ b/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs @@ -61,7 +61,7 @@ public async Task ValidatePathParametersForTablesAndViews(string entityName, str } /// - /// Validates that Query parameters are generated for GET methods in Table/Views. + /// Validates that the default set of query parameters are generated for GET methods in Table/Views. /// $select, $filter, $orderby, $first, $after are the query parameters. /// /// The name of the entity. From c157e874644893ee86e14fbb8a07a3f042518d13 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar <102276754+abhishekkumams@users.noreply.github.com> Date: Mon, 6 Nov 2023 15:52:20 +0530 Subject: [PATCH 12/20] Update src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs Co-authored-by: Sean Leonard --- src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs b/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs index 6e6223ab2f..8b68a025e9 100644 --- a/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs +++ b/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs @@ -101,7 +101,6 @@ public async Task ValidateQueryParametersForTablesAndViews(string entityName, st /// /// Validates that Input parameters are generated for Stored Procedures with GET operation. - /// Input parameters for stored procedures will be generated for GET operation. /// If a parameter default value is not provided in the config, it will marked as required. /// It also validates parameter metadatas like type, name, location, required, etc. /// From b59923d7a5fe7cafeebab0ce4e1b0a96973a941a Mon Sep 17 00:00:00 2001 From: Abhishek Kumar <102276754+abhishekkumams@users.noreply.github.com> Date: Mon, 6 Nov 2023 15:52:39 +0530 Subject: [PATCH 13/20] Update src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs Co-authored-by: Sean Leonard --- src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs b/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs index 8b68a025e9..2ec60d79d6 100644 --- a/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs +++ b/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs @@ -102,7 +102,7 @@ public async Task ValidateQueryParametersForTablesAndViews(string entityName, st /// /// Validates that Input parameters are generated for Stored Procedures with GET operation. /// If a parameter default value is not provided in the config, it will marked as required. - /// It also validates parameter metadatas like type, name, location, required, etc. + /// It also validates parameter metadata like type, name, location, required, etc. /// /// The name of the entity. /// The name of the database object. From d1f839ff27d23863cb9513f7745e49760f11eb81 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar <102276754+abhishekkumams@users.noreply.github.com> Date: Mon, 6 Nov 2023 20:38:51 +0530 Subject: [PATCH 14/20] Update src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs Co-authored-by: Sean Leonard --- src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs b/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs index 2ec60d79d6..990fc2d05a 100644 --- a/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs +++ b/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs @@ -101,7 +101,7 @@ public async Task ValidateQueryParametersForTablesAndViews(string entityName, st /// /// Validates that Input parameters are generated for Stored Procedures with GET operation. - /// If a parameter default value is not provided in the config, it will marked as required. + /// If a parameter default value is not provided in the config, it will be marked as required. /// It also validates parameter metadata like type, name, location, required, etc. /// /// The name of the entity. From 7ae218278e6c48d365f7119a472b6f21cc213248 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Mon, 6 Nov 2023 23:24:18 +0530 Subject: [PATCH 15/20] fixing nits --- .../Services/OpenAPI/OpenApiDocumentor.cs | 26 ++- .../ParameterValidationTests.cs | 194 +++++++++++------- 2 files changed, 142 insertions(+), 78 deletions(-) diff --git a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs index 45b5d67352..5b6756a8fb 100644 --- a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs +++ b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs @@ -46,8 +46,8 @@ public class OpenApiDocumentor : IOpenApiDocumentor public const string OPENAPI_ROUTE = "openapi"; // OpenApi query parameters + private static readonly OpenApiParameter _tableAndViewSelectQueryParameter = CreateSelectQueryParameterForTableAndView(); private static readonly List _tableAndViewQueryParameters = CreateTableAndViewQueryParameters(); - // Error messages public const string DOCUMENT_ALREADY_GENERATED_ERROR = "OpenAPI description document already generated."; public const string DOCUMENT_CREATION_UNSUPPORTED_ERROR = "OpenAPI description document can't be created when the REST endpoint is disabled globally."; @@ -287,8 +287,9 @@ private Dictionary CreateOperations( // The OpenApiResponses dictionary key represents the integer value of the HttpStatusCode, // which is returned when using Enum.ToString("D"). // The "D" format specified "displays the enumeration entry as an integer value in the shortest representation possible." + // It will only contain $select query parameter to allow the user to specify which fields to return. OpenApiOperation getOperation = CreateBaseOperation(description: GETONE_DESCRIPTION, tags: tags); - getOperation.Parameters = _tableAndViewQueryParameters; + getOperation.Parameters = new List() { _tableAndViewSelectQueryParameter }; getOperation.Responses.Add(HttpStatusCode.OK.ToString("D"), CreateOpenApiResponse(description: nameof(HttpStatusCode.OK), responseObjectSchemaName: entityName)); openApiPathItemOperations.Add(OperationType.Get, getOperation); @@ -460,6 +461,20 @@ private static List CreateStoredProcedureInputParameters(Store return parameters; } + /// + /// Create an OpenApiParameter object for the input parameter to select + /// fields in a table or view to be returned in the response. + /// + private static OpenApiParameter CreateSelectQueryParameterForTableAndView() + { + return GetOpenApiQueryParameter( + name: "$select", + description: "A comma separated list of fields to return in the response.", + required: false, + type: "string" + ); + } + /// /// Creates a list of OpenAPI parameters for querying tables and views. /// The query parameters include $select, $filter, $orderby, $first, and $after, which allow the user to specify which fields to return, @@ -472,12 +487,7 @@ private static List CreateTableAndViewQueryParameters() // Add $select query parameter parameters.Add( - GetOpenApiQueryParameter( - name: "$select", - description: "A comma separated list of fields to return in the response.", - required: false, - type: "string" - ) + _tableAndViewSelectQueryParameter ); // Add $filter query parameter diff --git a/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs b/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs index 2ec60d79d6..b5324be946 100644 --- a/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs +++ b/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs @@ -36,11 +36,14 @@ public class ParameterValidationTests [DataTestMethod] [DataRow("BooksTable", "books", EntitySourceType.Table, DisplayName = "Table with path parameter id")] [DataRow("BooksView", "books_view_all", EntitySourceType.View, DisplayName = "View with path parameter id")] - public async Task ValidatePathParametersForTablesAndViews(string entityName, string objectName, EntitySourceType entitySourceType) + public async Task TestPathParametersForTablesAndViews(string entityName, string objectName, EntitySourceType entitySourceType) { EntitySource entitySource = new(Object: objectName, entitySourceType, null, null); OpenApiDocument openApiDocument = await GenerateOpenApiDocumentForGivenEntityAsync(entityName, entitySource); + Assert.AreEqual(2, openApiDocument.Paths.Count); + Assert.IsTrue(openApiDocument.Paths.ContainsKey($"/{entityName}")); + Assert.IsTrue(openApiDocument.Paths.ContainsKey($"/{entityName}/id/{{id}}")); foreach ((string pathName, OpenApiPathItem pathItem) in openApiDocument.Paths) { string apiPathWithParam = $"/{entityName}/id/{{id}}"; @@ -70,33 +73,75 @@ public async Task ValidatePathParametersForTablesAndViews(string entityName, str [DataTestMethod] [DataRow("BooksTable", "books", EntitySourceType.Table, DisplayName = "Table with query parameters")] [DataRow("BooksView", "books_view_all", EntitySourceType.View, DisplayName = "View with query parameters")] - public async Task ValidateQueryParametersForTablesAndViews(string entityName, string objectName, EntitySourceType entitySourceType) + public async Task TestQueryParametersAddedForGEToperationOnTablesAndViews(string entityName, string objectName, EntitySourceType entitySourceType) { EntitySource entitySource = new(Object: objectName, entitySourceType, null, null); OpenApiDocument openApiDocument = await GenerateOpenApiDocumentForGivenEntityAsync(entityName, entitySource); - foreach (OpenApiPathItem pathItem in openApiDocument.Paths.Values) - { - foreach ((OperationType operationType, OpenApiOperation operation) in pathItem.Operations) - { - if (operationType is OperationType.Get) - { - Assert.IsTrue(operation.Parameters.Count is 5); - - // Assert that it contains all the query parameters with appropriate type - Assert.IsTrue(operation.Parameters.Any(param => param.In is ParameterLocation.Query && param.Name.Equals("$select") && param.Schema.Type.Equals("string"))); - Assert.IsTrue(operation.Parameters.Any(param => param.In is ParameterLocation.Query && param.Name.Equals("$filter") && param.Schema.Type.Equals("string"))); - Assert.IsTrue(operation.Parameters.Any(param => param.In is ParameterLocation.Query && param.Name.Equals("$orderby") && param.Schema.Type.Equals("string"))); - Assert.IsTrue(operation.Parameters.Any(param => param.In is ParameterLocation.Query && param.Name.Equals("$first") && param.Schema.Type.Equals("integer"))); - Assert.IsTrue(operation.Parameters.Any(param => param.In is ParameterLocation.Query && param.Name.Equals("$after") && param.Schema.Type.Equals("string"))); - } - else - { - // No query parameters for other OperationTypes. - Assert.IsFalse(operation.Parameters.Any(param => param.In is ParameterLocation.Query)); - } - } - } + Assert.AreEqual(2, openApiDocument.Paths.Count); + Assert.IsTrue(openApiDocument.Paths.ContainsKey($"/{entityName}")); + Assert.IsTrue(openApiDocument.Paths.ContainsKey($"/{entityName}/id/{{id}}")); + + // Assert that path without id has all the query parameters. + Assert.IsTrue(openApiDocument.Paths[$"/{entityName}"].Operations.ContainsKey(OperationType.Get)); + Assert.AreEqual(5, openApiDocument.Paths[$"/{entityName}"].Operations[OperationType.Get].Parameters.Count); + + // Asserting on all the parameters + List openApiParameters = openApiDocument.Paths[$"/{entityName}"].Operations[OperationType.Get].Parameters.ToList(); + Assert.IsTrue(openApiParameters.Any(param => param.In is ParameterLocation.Query && param.Name.Equals("$select") && param.Schema.Type.Equals("string"))); + Assert.IsTrue(openApiParameters.Any(param => param.In is ParameterLocation.Query && param.Name.Equals("$filter") && param.Schema.Type.Equals("string"))); + Assert.IsTrue(openApiParameters.Any(param => param.In is ParameterLocation.Query && param.Name.Equals("$orderby") && param.Schema.Type.Equals("string"))); + Assert.IsTrue(openApiParameters.Any(param => param.In is ParameterLocation.Query && param.Name.Equals("$first") && param.Schema.Type.Equals("integer"))); + Assert.IsTrue(openApiParameters.Any(param => param.In is ParameterLocation.Query && param.Name.Equals("$after") && param.Schema.Type.Equals("string"))); + + // Assert that path with id has only one parameter. + Assert.IsTrue(openApiDocument.Paths[$"/{entityName}/id/{{id}}"].Operations.ContainsKey(OperationType.Get)); + OpenApiOperation openApiOperation = openApiDocument.Paths[$"/{entityName}/id/{{id}}"].Operations[OperationType.Get]; + Assert.AreEqual(1, openApiOperation.Parameters.Count); + + //Assert that it is $select + OpenApiParameter openApiParameter = openApiOperation.Parameters.First(); + Assert.AreEqual("$select", openApiParameter.Name); + Assert.AreEqual(ParameterLocation.Query, openApiParameter.In); + Assert.AreEqual("string", openApiParameter.Schema.Type); + } + + /// + /// Validates that the default set of query parameters are generated for GET methods in Table/Views. + /// $select, $filter, $orderby, $first, $after are the query parameters. + /// + /// The name of the entity. + /// The name of the database object. + /// The source type of the entity. + [DataTestMethod] + [DataRow("BooksTable", "books", EntitySourceType.Table, DisplayName = "Table with query parameters")] + [DataRow("BooksView", "books_view_all", EntitySourceType.View, DisplayName = "View with query parameters")] + public async Task TestQueryParametersExcludedFromNonReadOperationsOnTablesAndViews(string entityName, string objectName, EntitySourceType entitySourceType) + { + EntitySource entitySource = new(Object: objectName, entitySourceType, null, null); + OpenApiDocument openApiDocument = await GenerateOpenApiDocumentForGivenEntityAsync(entityName, entitySource); + + Assert.AreEqual(2, openApiDocument.Paths.Count); + Assert.IsTrue(openApiDocument.Paths.ContainsKey($"/{entityName}")); + Assert.IsTrue(openApiDocument.Paths.ContainsKey($"/{entityName}/id/{{id}}")); + + // Assert that Query Parameters Excluded From NonReadOperations for path without id. + OpenApiPathItem pathWithouId = openApiDocument.Paths[$"/{entityName}"]; + Assert.IsTrue(pathWithouId.Operations.ContainsKey(OperationType.Post)); + Assert.IsTrue(pathWithouId.Operations[OperationType.Post].Parameters.IsNullOrEmpty()); + Assert.IsFalse(pathWithouId.Operations.ContainsKey(OperationType.Put)); + Assert.IsFalse(pathWithouId.Operations.ContainsKey(OperationType.Patch)); + Assert.IsFalse(pathWithouId.Operations.ContainsKey(OperationType.Delete)); + + // Assert that Query Parameters Excluded From NonReadOperations for path with id. + OpenApiPathItem pathWithId = openApiDocument.Paths[$"/{entityName}/id/{{id}}"]; + Assert.IsFalse(pathWithId.Operations.ContainsKey(OperationType.Post)); + Assert.IsTrue(pathWithId.Operations.ContainsKey(OperationType.Put)); + Assert.IsTrue(pathWithId.Operations[OperationType.Put].Parameters.IsNullOrEmpty()); + Assert.IsTrue(pathWithId.Operations.ContainsKey(OperationType.Patch)); + Assert.IsTrue(pathWithId.Operations[OperationType.Patch].Parameters.IsNullOrEmpty()); + Assert.IsTrue(pathWithId.Operations.ContainsKey(OperationType.Delete)); + Assert.IsTrue(pathWithId.Operations[OperationType.Delete].Parameters.IsNullOrEmpty()); } /// @@ -106,55 +151,60 @@ public async Task ValidateQueryParametersForTablesAndViews(string entityName, st /// /// The name of the entity. /// The name of the database object. - [DataTestMethod] - [DataRow("CountBooks", "count_books", DisplayName = "StoredProcudure with no parameters results in 0 created input query params.")] - [DataRow("UpdateBookTitle", "update_book_title", DisplayName = "StoredProcedure with parameters results in created input query params.")] - public async Task ValidateInputParametersForStoredProcedures(string entityName, string objectName) + [TestMethod] + public async Task TestInputParametersForStoredProcedures() { - Dictionary parameterDefaults = null; - if (entityName.Equals("UpdateBookTitle")) - { - // Adding a parameter default value. - parameterDefaults = new Dictionary { { "title", "Test Title" } }; - } + string entityName = "UpdateBookTitle"; + string objectName = "update_book_title"; + + // Adding a parameter default value. + Dictionary parameterDefaults = new() { { "title", "Test Title" } }; EntitySource entitySource = new(Object: objectName, EntitySourceType.StoredProcedure, parameterDefaults, null); - OpenApiDocument openApiDocument = await GenerateOpenApiDocumentForGivenEntityAsync(entityName, entitySource); + OpenApiDocument openApiDocument = await GenerateOpenApiDocumentForGivenEntityAsync( + entityName, + entitySource, + supportedHttpMethods: new SupportedHttpVerb[] { SupportedHttpVerb.Get }); + Assert.AreEqual(1, openApiDocument.Paths.Count); + Assert.IsTrue(openApiDocument.Paths.ContainsKey($"/{entityName}")); OpenApiPathItem pathItem = openApiDocument.Paths.First().Value; - foreach ((OperationType operationType, OpenApiOperation operation) in pathItem.Operations) - { - if (entityName.Equals("UpdateBookTitle")) - { - if (operationType is OperationType.Get) - { - Assert.IsTrue(operation.Parameters.Any(param => - param.In is ParameterLocation.Query - && param.Name.Equals("id") - && param.Schema.Type.Equals("number") - && param.Required is true - )); - - // Parameter with default value will be an optional query parameter. - Assert.IsTrue(operation.Parameters.Any(param => - param.In is ParameterLocation.Query - && param.Name.Equals("title") - && param.Schema.Type.Equals("string") - && param.Required is false)); - } - else - { - // Input Parameters are supported only for GET requests - // For Other requests requestBody is used. - Assert.IsTrue(operation.Parameters.IsNullOrEmpty()); - } - } - else - { - // CountBookTitle doesn't require any query parameters. - Assert.IsTrue(operation.Parameters.IsNullOrEmpty()); - } - } + Assert.AreEqual(1, pathItem.Operations.Count); + Assert.IsTrue(pathItem.Operations.ContainsKey(OperationType.Get)); + + OpenApiOperation operation = pathItem.Operations[OperationType.Get]; + Assert.AreEqual(2, operation.Parameters.Count); + Assert.IsTrue(operation.Parameters.Any(param => + param.In is ParameterLocation.Query + && param.Name.Equals("id") + && param.Schema.Type.Equals("number") + && param.Required is true)); + + // Parameter with default value will be an optional query parameter. + Assert.IsTrue(operation.Parameters.Any(param => + param.In is ParameterLocation.Query + && param.Name.Equals("title") + && param.Schema.Type.Equals("string") + && param.Required is false)); + } + + /// + /// Validates that input query parameters are not generated for Stored Procedures with + /// no input parameters or no GET operations. + /// + [DataTestMethod] + [DataRow("CountBooks", "count_books", new SupportedHttpVerb[] { SupportedHttpVerb.Get }, DisplayName = "StoredProcudure with no input parameters results in 0 created input query params.")] + [DataRow("InsertBook", "insert_book", new SupportedHttpVerb[] { SupportedHttpVerb.Post }, DisplayName = "StoredProcedure without GET operations will results in 0 created input query params.")] + public async Task TestStoredProcedureForNoQueryParameters(string entityName, string objectName, SupportedHttpVerb[] supportedHttpVerbs) + { + EntitySource entitySource = new(Object: objectName, EntitySourceType.StoredProcedure, Parameters: null, KeyFields: null); + OpenApiDocument openApiDocument = await GenerateOpenApiDocumentForGivenEntityAsync(entityName, entitySource, supportedHttpVerbs); + + Assert.AreEqual(1, openApiDocument.Paths.Count); + OpenApiPathItem pathItem = openApiDocument.Paths.First().Value; + Assert.AreEqual(1, pathItem.Operations.Count); + Assert.AreEqual(supportedHttpVerbs.First().ToString(), pathItem.Operations.Keys.First().ToString()); + Assert.IsTrue(pathItem.Operations.Values.First().Parameters.IsNullOrEmpty()); } /// @@ -162,12 +212,16 @@ param.In is ParameterLocation.Query /// /// The name of the entity. /// Database source for entity. - private async static Task GenerateOpenApiDocumentForGivenEntityAsync(string entityName, EntitySource entitySource) + /// Supported HTTP verbs for the entity. + private async static Task GenerateOpenApiDocumentForGivenEntityAsync( + string entityName, + EntitySource entitySource, + SupportedHttpVerb[] supportedHttpMethods = null) { Entity entity = new( Source: entitySource, GraphQL: new(Singular: null, Plural: null, Enabled: false), - Rest: new(Methods: EntityRestOptions.DEFAULT_SUPPORTED_VERBS), + Rest: new(Methods: supportedHttpMethods ?? EntityRestOptions.DEFAULT_SUPPORTED_VERBS), Permissions: OpenApiTestBootstrap.CreateBasicPermissions(), Mappings: null, Relationships: null); From 4ab1f8feca3f88c1dbfb349ba3ddcafb5e0dfcea Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 7 Nov 2023 16:16:10 +0530 Subject: [PATCH 16/20] fixing parameter description --- .../Services/OpenAPI/OpenApiDocumentor.cs | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs index 5b6756a8fb..930fe2793b 100644 --- a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs +++ b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs @@ -42,6 +42,13 @@ public class OpenApiDocumentor : IOpenApiDocumentor public const string SCHEMA_OBJECT_TYPE = "object"; public const string RESPONSE_ARRAY_PROPERTY = "array"; + // Query parameters name + public const string SELECT_QUERY_PARAMETER_NAME = "$select"; + public const string FILTER_QUERY_PARAMETER_NAME = "$filter"; + public const string ORDERBY_QUERY_PARAMETER_NAME = "$orderby"; + public const string FIRST_QUERY_PARAMETER_NAME = "$first"; + public const string AFTER_QUERY_PARAMETER_NAME = "$after"; + // Routing constant public const string OPENAPI_ROUTE = "openapi"; @@ -451,7 +458,7 @@ private static List CreateStoredProcedureInputParameters(Store parameters.Add( GetOpenApiQueryParameter( name: paramKey, - description: "Input parameter for stored procedure parameter", + description: "Input parameter for stored procedure arguments", required: !parameterDefinition.HasConfigDefault, type: TypeHelper.GetJsonDataTypeFromSystemType(parameterDefinition.SystemType).ToString().ToLower() ) @@ -468,7 +475,7 @@ private static List CreateStoredProcedureInputParameters(Store private static OpenApiParameter CreateSelectQueryParameterForTableAndView() { return GetOpenApiQueryParameter( - name: "$select", + name: SELECT_QUERY_PARAMETER_NAME, description: "A comma separated list of fields to return in the response.", required: false, type: "string" @@ -493,8 +500,8 @@ private static List CreateTableAndViewQueryParameters() // Add $filter query parameter parameters.Add( GetOpenApiQueryParameter( - name: "$filter", - description: "A predicate expression (an expression that returns a boolean value) using the entity's fields to retrieve a subset of the results.", + name: FILTER_QUERY_PARAMETER_NAME, + description: "A OData expression (an expression that returns a boolean value) using the entity's fields to retrieve a subset of the results.", required: false, type: "string" ) @@ -503,8 +510,8 @@ private static List CreateTableAndViewQueryParameters() // Add $orderby query parameter parameters.Add( GetOpenApiQueryParameter( - name: "$orderby", - description: "A comma-separated list of expressions used to sort items in the response.", + name: ORDERBY_QUERY_PARAMETER_NAME, + description: "Uses a comma-separated list of expressions to sort response items. Add 'desc' for descending order, otherwise it's ascending by default.", required: false, type: "string" ) @@ -513,8 +520,8 @@ private static List CreateTableAndViewQueryParameters() // Add $first query parameter parameters.Add( GetOpenApiQueryParameter( - name: "$first", - description: "An integer value that specifies the number of items to return.", + name: FIRST_QUERY_PARAMETER_NAME, + description: "An integer value that specifies the number of items to return. Default is 100.", required: false, type: "integer" ) @@ -523,7 +530,7 @@ private static List CreateTableAndViewQueryParameters() // Add $after query parameter parameters.Add( GetOpenApiQueryParameter( - name: "$after", + name: AFTER_QUERY_PARAMETER_NAME, description: "A base64 encoded string that specifies the cursor position after which items should be returned.", required: false, type: "string" From 3f36e313819eb2a019c00d1446a928b362f28623 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar <102276754+abhishekkumams@users.noreply.github.com> Date: Wed, 8 Nov 2023 10:43:03 +0530 Subject: [PATCH 17/20] Update src/Core/Services/OpenAPI/OpenApiDocumentor.cs Co-authored-by: Sean Leonard --- src/Core/Services/OpenAPI/OpenApiDocumentor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs index 930fe2793b..9983cdebcc 100644 --- a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs +++ b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs @@ -531,7 +531,7 @@ private static List CreateTableAndViewQueryParameters() parameters.Add( GetOpenApiQueryParameter( name: AFTER_QUERY_PARAMETER_NAME, - description: "A base64 encoded string that specifies the cursor position after which items should be returned.", + description: "An opaque string that specifies the cursor position after which results should be returned.", required: false, type: "string" ) From db25bd89b83c55648167dae66aead9e9e2c8fe10 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 8 Nov 2023 10:53:36 +0530 Subject: [PATCH 18/20] fixing nits --- src/Core/Parsers/RequestParser.cs | 8 ++++---- .../Services/OpenAPI/OpenApiDocumentor.cs | 20 +++++++------------ 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/Core/Parsers/RequestParser.cs b/src/Core/Parsers/RequestParser.cs index 5cddbc45fa..9a3a602329 100644 --- a/src/Core/Parsers/RequestParser.cs +++ b/src/Core/Parsers/RequestParser.cs @@ -18,11 +18,11 @@ public class RequestParser /// /// Prefix used for specifying the fields in the query string of the URL. /// - private const string FIELDS_URL = "$select"; + public const string FIELDS_URL = "$select"; /// /// Prefix used for specifying the fields to be used to sort the result in the query string of the URL. /// - private const string SORT_URL = "$orderby"; + public const string SORT_URL = "$orderby"; /// /// Prefix used for specifying filter in the query string of the URL. /// @@ -30,11 +30,11 @@ public class RequestParser /// /// Prefix used for specifying limit in the query string of the URL. /// - private const string FIRST_URL = "$first"; + public const string FIRST_URL = "$first"; /// /// Prefix used for specifying paging in the query string of the URL. /// - private const string AFTER_URL = "$after"; + public const string AFTER_URL = "$after"; /// /// Parses the primary key string to identify the field names composing the key diff --git a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs index 930fe2793b..a8d132044d 100644 --- a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs +++ b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs @@ -13,6 +13,7 @@ using Azure.DataApiBuilder.Service.Exceptions; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Writers; +using Azure.DataApiBuilder.Core.Parsers; namespace Azure.DataApiBuilder.Core.Services { @@ -42,13 +43,6 @@ public class OpenApiDocumentor : IOpenApiDocumentor public const string SCHEMA_OBJECT_TYPE = "object"; public const string RESPONSE_ARRAY_PROPERTY = "array"; - // Query parameters name - public const string SELECT_QUERY_PARAMETER_NAME = "$select"; - public const string FILTER_QUERY_PARAMETER_NAME = "$filter"; - public const string ORDERBY_QUERY_PARAMETER_NAME = "$orderby"; - public const string FIRST_QUERY_PARAMETER_NAME = "$first"; - public const string AFTER_QUERY_PARAMETER_NAME = "$after"; - // Routing constant public const string OPENAPI_ROUTE = "openapi"; @@ -475,7 +469,7 @@ private static List CreateStoredProcedureInputParameters(Store private static OpenApiParameter CreateSelectQueryParameterForTableAndView() { return GetOpenApiQueryParameter( - name: SELECT_QUERY_PARAMETER_NAME, + name: RequestParser.FIELDS_URL, description: "A comma separated list of fields to return in the response.", required: false, type: "string" @@ -500,8 +494,8 @@ private static List CreateTableAndViewQueryParameters() // Add $filter query parameter parameters.Add( GetOpenApiQueryParameter( - name: FILTER_QUERY_PARAMETER_NAME, - description: "A OData expression (an expression that returns a boolean value) using the entity's fields to retrieve a subset of the results.", + name: RequestParser.FILTER_URL, + description: "An OData expression (an expression that returns a boolean value) using the entity's fields to retrieve a subset of the results.", required: false, type: "string" ) @@ -510,7 +504,7 @@ private static List CreateTableAndViewQueryParameters() // Add $orderby query parameter parameters.Add( GetOpenApiQueryParameter( - name: ORDERBY_QUERY_PARAMETER_NAME, + name: RequestParser.SORT_URL, description: "Uses a comma-separated list of expressions to sort response items. Add 'desc' for descending order, otherwise it's ascending by default.", required: false, type: "string" @@ -520,7 +514,7 @@ private static List CreateTableAndViewQueryParameters() // Add $first query parameter parameters.Add( GetOpenApiQueryParameter( - name: FIRST_QUERY_PARAMETER_NAME, + name: RequestParser.FIRST_URL, description: "An integer value that specifies the number of items to return. Default is 100.", required: false, type: "integer" @@ -530,7 +524,7 @@ private static List CreateTableAndViewQueryParameters() // Add $after query parameter parameters.Add( GetOpenApiQueryParameter( - name: AFTER_QUERY_PARAMETER_NAME, + name: RequestParser.AFTER_URL, description: "A base64 encoded string that specifies the cursor position after which items should be returned.", required: false, type: "string" From 4e05b01ba1216ffe963835d6b51c9b2e1226562b Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Mon, 13 Nov 2023 13:46:14 +0530 Subject: [PATCH 19/20] fixing nits --- src/Core/Services/OpenAPI/OpenApiDocumentor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs index e93005674b..879d42e58e 100644 --- a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs +++ b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs @@ -453,7 +453,7 @@ private static List CreateStoredProcedureInputParameters(Store GetOpenApiQueryParameter( name: paramKey, description: "Input parameter for stored procedure arguments", - required: !parameterDefinition.HasConfigDefault, + required: false, type: TypeHelper.GetJsonDataTypeFromSystemType(parameterDefinition.SystemType).ToString().ToLower() ) ); From 66d6e5ce9598c60186d03bdf694e6435102a7592 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Mon, 13 Nov 2023 23:42:35 +0530 Subject: [PATCH 20/20] fixing tests --- .../Services/OpenAPI/OpenApiDocumentor.cs | 26 ++++-------- .../ParameterValidationTests.cs | 41 +++++++++---------- 2 files changed, 28 insertions(+), 39 deletions(-) diff --git a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs index 879d42e58e..8338589c94 100644 --- a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs +++ b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs @@ -9,11 +9,11 @@ using Azure.DataApiBuilder.Config.DatabasePrimitives; using Azure.DataApiBuilder.Config.ObjectModel; using Azure.DataApiBuilder.Core.Configurations; +using Azure.DataApiBuilder.Core.Parsers; using Azure.DataApiBuilder.Core.Services.MetadataProviders; using Azure.DataApiBuilder.Service.Exceptions; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Writers; -using Azure.DataApiBuilder.Core.Parsers; namespace Azure.DataApiBuilder.Core.Services { @@ -47,7 +47,6 @@ public class OpenApiDocumentor : IOpenApiDocumentor public const string OPENAPI_ROUTE = "openapi"; // OpenApi query parameters - private static readonly OpenApiParameter _tableAndViewSelectQueryParameter = CreateSelectQueryParameterForTableAndView(); private static readonly List _tableAndViewQueryParameters = CreateTableAndViewQueryParameters(); // Error messages public const string DOCUMENT_ALREADY_GENERATED_ERROR = "OpenAPI description document already generated."; @@ -290,7 +289,7 @@ private Dictionary CreateOperations( // The "D" format specified "displays the enumeration entry as an integer value in the shortest representation possible." // It will only contain $select query parameter to allow the user to specify which fields to return. OpenApiOperation getOperation = CreateBaseOperation(description: GETONE_DESCRIPTION, tags: tags); - getOperation.Parameters = new List() { _tableAndViewSelectQueryParameter }; + getOperation.Parameters = _tableAndViewQueryParameters; getOperation.Responses.Add(HttpStatusCode.OK.ToString("D"), CreateOpenApiResponse(description: nameof(HttpStatusCode.OK), responseObjectSchemaName: entityName)); openApiPathItemOperations.Add(OperationType.Get, getOperation); @@ -462,20 +461,6 @@ private static List CreateStoredProcedureInputParameters(Store return parameters; } - /// - /// Create an OpenApiParameter object for the input parameter to select - /// fields in a table or view to be returned in the response. - /// - private static OpenApiParameter CreateSelectQueryParameterForTableAndView() - { - return GetOpenApiQueryParameter( - name: RequestParser.FIELDS_URL, - description: "A comma separated list of fields to return in the response.", - required: false, - type: "string" - ); - } - /// /// Creates a list of OpenAPI parameters for querying tables and views. /// The query parameters include $select, $filter, $orderby, $first, and $after, which allow the user to specify which fields to return, @@ -488,7 +473,12 @@ private static List CreateTableAndViewQueryParameters() // Add $select query parameter parameters.Add( - _tableAndViewSelectQueryParameter + GetOpenApiQueryParameter( + name: RequestParser.FIELDS_URL, + description: "A comma separated list of fields to return in the response.", + required: false, + type: "string" + ) ); // Add $filter query parameter diff --git a/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs b/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs index b0527427dc..a9f83f9da1 100644 --- a/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs +++ b/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs @@ -82,28 +82,15 @@ public async Task TestQueryParametersAddedForGEToperationOnTablesAndViews(string Assert.IsTrue(openApiDocument.Paths.ContainsKey($"/{entityName}")); Assert.IsTrue(openApiDocument.Paths.ContainsKey($"/{entityName}/id/{{id}}")); - // Assert that path without id has all the query parameters. + // Asserting on all the parameters for Get All operation. Assert.IsTrue(openApiDocument.Paths[$"/{entityName}"].Operations.ContainsKey(OperationType.Get)); - Assert.AreEqual(5, openApiDocument.Paths[$"/{entityName}"].Operations[OperationType.Get].Parameters.Count); + OpenApiOperation openApiOperationForGetAll = openApiDocument.Paths[$"/{entityName}"].Operations[OperationType.Get]; + AssertOnAllDefaultQueryParameters(openApiOperationForGetAll.Parameters.ToList()); - // Asserting on all the parameters - List openApiParameters = openApiDocument.Paths[$"/{entityName}"].Operations[OperationType.Get].Parameters.ToList(); - Assert.IsTrue(openApiParameters.Any(param => param.In is ParameterLocation.Query && param.Name.Equals("$select") && param.Schema.Type.Equals("string"))); - Assert.IsTrue(openApiParameters.Any(param => param.In is ParameterLocation.Query && param.Name.Equals("$filter") && param.Schema.Type.Equals("string"))); - Assert.IsTrue(openApiParameters.Any(param => param.In is ParameterLocation.Query && param.Name.Equals("$orderby") && param.Schema.Type.Equals("string"))); - Assert.IsTrue(openApiParameters.Any(param => param.In is ParameterLocation.Query && param.Name.Equals("$first") && param.Schema.Type.Equals("integer"))); - Assert.IsTrue(openApiParameters.Any(param => param.In is ParameterLocation.Query && param.Name.Equals("$after") && param.Schema.Type.Equals("string"))); - - // Assert that path with id has only one parameter. + // Assert that path with id has all the query parameters. Assert.IsTrue(openApiDocument.Paths[$"/{entityName}/id/{{id}}"].Operations.ContainsKey(OperationType.Get)); - OpenApiOperation openApiOperation = openApiDocument.Paths[$"/{entityName}/id/{{id}}"].Operations[OperationType.Get]; - Assert.AreEqual(1, openApiOperation.Parameters.Count); - - //Assert that it is $select - OpenApiParameter openApiParameter = openApiOperation.Parameters.First(); - Assert.AreEqual("$select", openApiParameter.Name); - Assert.AreEqual(ParameterLocation.Query, openApiParameter.In); - Assert.AreEqual("string", openApiParameter.Schema.Type); + OpenApiOperation openApiOperationForGetById = openApiDocument.Paths[$"/{entityName}/id/{{id}}"].Operations[OperationType.Get]; + AssertOnAllDefaultQueryParameters(openApiOperationForGetById.Parameters.ToList()); } /// @@ -146,7 +133,6 @@ public async Task TestQueryParametersExcludedFromNonReadOperationsOnTablesAndVie /// /// Validates that Input parameters are generated for Stored Procedures with GET operation. - /// If a parameter default value is not provided in the config, it will be marked as required. /// It also validates parameter metadata like type, name, location, required, etc. /// /// The name of the entity. @@ -178,7 +164,7 @@ public async Task TestInputParametersForStoredProcedures() param.In is ParameterLocation.Query && param.Name.Equals("id") && param.Schema.Type.Equals("number") - && param.Required is true)); + && param.Required is false)); // Parameter with default value will be an optional query parameter. Assert.IsTrue(operation.Parameters.Any(param => @@ -207,6 +193,19 @@ public async Task TestStoredProcedureForNoQueryParameters(string entityName, str Assert.IsTrue(pathItem.Operations.Values.First().Parameters.IsNullOrEmpty()); } + /// + /// validate that $select, $filter, $orderby, $first, $after query parameters are present in the openAPI document. + /// + private static void AssertOnAllDefaultQueryParameters(List openApiParameters) + { + Assert.AreEqual(5, openApiParameters.Count); + Assert.IsTrue(openApiParameters.Any(param => param.In is ParameterLocation.Query && param.Name.Equals("$select") && param.Schema.Type.Equals("string"))); + Assert.IsTrue(openApiParameters.Any(param => param.In is ParameterLocation.Query && param.Name.Equals("$filter") && param.Schema.Type.Equals("string"))); + Assert.IsTrue(openApiParameters.Any(param => param.In is ParameterLocation.Query && param.Name.Equals("$orderby") && param.Schema.Type.Equals("string"))); + Assert.IsTrue(openApiParameters.Any(param => param.In is ParameterLocation.Query && param.Name.Equals("$first") && param.Schema.Type.Equals("integer"))); + Assert.IsTrue(openApiParameters.Any(param => param.In is ParameterLocation.Query && param.Name.Equals("$after") && param.Schema.Type.Equals("string"))); + } + /// /// Generate and return the OpenApiDocument for a single entity. ///