From 411f5c46ac72a5e610d5698e37d439a61de79014 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Mon, 14 Nov 2022 11:18:11 +0530 Subject: [PATCH 01/42] adding support for graphQL Queries --- docs/internals/StoredProcedureDesignDoc.md | 20 ++++ src/Config/DatabaseObject.cs | 23 +++- .../Mutations/MutationBuilder.cs | 5 + .../Queries/QueryBuilder.cs | 58 +++++++++- .../Sql/SchemaConverter.cs | 100 +++++++++++------- .../Configurations/RuntimeConfigValidator.cs | 8 +- src/Service/Resolvers/IQueryBuilder.cs | 7 ++ src/Service/Resolvers/MsSqlQueryBuilder.cs | 12 +++ src/Service/Resolvers/MySqlQueryBuilder.cs | 6 ++ src/Service/Resolvers/PostgresQueryBuilder.cs | 6 ++ src/Service/Resolvers/SqlPaginationUtil.cs | 22 ++++ src/Service/Resolvers/SqlQueryEngine.cs | 41 ++++--- src/Service/Services/GraphQLSchemaCreator.cs | 29 +++-- .../MsSqlMetadataProvider.cs | 1 + .../MetadataProviders/SqlMetadataProvider.cs | 54 ++++++++-- .../dab-config.MsSql.overrides.example.json | 20 +++- 16 files changed, 333 insertions(+), 79 deletions(-) diff --git a/docs/internals/StoredProcedureDesignDoc.md b/docs/internals/StoredProcedureDesignDoc.md index 6fcb00e8a8..ad845eba80 100644 --- a/docs/internals/StoredProcedureDesignDoc.md +++ b/docs/internals/StoredProcedureDesignDoc.md @@ -35,11 +35,19 @@ Thus a stored procedure entity might look like: parameters can either be fixed as above or passed at runtime through - query parameters for GET request - request body for POST, PUT, PATCH, DELETE +- For GraphQL, the stored-procedure will look something like this passed in the body +``` +{ + GetBooks(param1:value, param2:value) {} +} +``` > **Spec Interpretation**: > - since not explicitly stated in the specification, request body for GET will be ignored altogether. > - Parameter resolution will go as follows, from highest to lowest priority: request (query string or body) > config defaults > sql defaults > - NOTE: sql defaults not so easy to infer for parameters, so we explicitly require all parameters to be provided either in the request or config in this first version. +> - GRAPHQL +> - if the request doesn't contain the parameter values, default values from the config will be picked up. ### Stored Procedure Permissions @@ -94,6 +102,8 @@ Implementation was segmented into 5 main sections: > - Stored procedures must be specified as "stored-procedure" source type > - See [sample source config](#sample-source) > - Unfortunately, System.Text.Json does not natively support Kebab-casing for converting hyphenated strings ("stored-procedure") to a CamelCase enum (StoredProcedure). As such, Newtonsoft is used for deserialization. If we want to migrate to System.Text.Json, we either need to change the spec and only accept non-hyphenated strings or write our own custom string-to-enum converter, which was out of scope for v1. +> GRAPHQL +> - No change required here. ### 2. Metadata Validation @@ -106,6 +116,13 @@ Implementation was segmented into 5 main sections:
+> ### `RuntimeConfigValidator.cs` +> - `ValidateEntitiesDoNotGenerateDuplicateQueries` should skip check for Stored-Procedures. + + +> ### `SchemaConverter.cs` and `GraphQLSchemaCreator.cs` +> - Generate a GraphQL object type from a SQL table/view/stored-procedure definition, combined with the runtime config entity information + > ### `SqlMetadataProvider.cs` > - Problem: when we draw metadata from the database schema, we implicitly check if each entity specified in config exists in the database. Path: in `Startup.cs`, the `PerformOnConfigChangeAsync()` method invokes `InitializeAsync()` of the metadata provider bound at runtime, which then invokes `PopulateTableDefinitionForEntities()`. Several steps down the stack `FillSchemaForTableAsync()` is called, which performs a `SELECT * FROM {table_name}` for the given entity and adds the resulting `DataTable` object to the `EntitiesDataSet` class variable. Unfortunately, stored procedure metadata cannot be queried using the same syntax. As such, running the select statement on a stored procedure source name will cause a Sql exception to surface and result in runtime initialization failure. > - Instead, we introduce the `PopulateStoredProcedureDefinitionForEntities()` method, which iterates over only entites labeled as stored procedures to fill their metadata, and we simply skip over entities labeled as Stored Procedures in the `PopulateTableDefinitionForEntities()` method. @@ -160,6 +177,9 @@ Implementation was segmented into 5 main sections:
+> ### `QueryBuilder.cs` +> - It should not generate Both FindAll and FindByPK query for Stored-Procedure. + > ### `MsSqlQueryBuilder.cs` > - Added the `Build(SqlExecuteStructure)` that builds the query string for execute requests as `EXECUTE {schema_name}.{stored_proc_name} {parameters}` > - Added `BuildProcedureParameterList` to build the list of parameters from the `ProcedureParameters` dictionary. The result of this method might look like `@id = @param0, @title = @param1`. diff --git a/src/Config/DatabaseObject.cs b/src/Config/DatabaseObject.cs index dd11c60cfd..07702316d8 100644 --- a/src/Config/DatabaseObject.cs +++ b/src/Config/DatabaseObject.cs @@ -77,13 +77,19 @@ public DatabaseStoredProcedure(string schemaName, string tableName) public StoredProcedureDefinition StoredProcedureDefinition { get; set; } = null!; } - public class StoredProcedureDefinition + public class StoredProcedureDefinition: SourceDefinition { /// /// The list of input parameters /// Key: parameter name, Value: ParameterDefinition object /// public Dictionary Parameters { get; set; } = new(); + + /// + /// The list of fields with their type in the Stored Procedure result + /// Key: ResultSet field name, Value: ResultSet field Type + /// + public Dictionary ResultSet { get; set; } = new(); } public class ParameterDefinition @@ -132,6 +138,21 @@ public bool IsAnyColumnNullable(List columnsToCheck) .Where(isNullable => isNullable == true) .Any(); } + + /// + /// Get the underlying SourceDefinition based on database object source type + /// + public static SourceDefinition GetSourceDefinitionForDatabaseObject(DatabaseObject databaseObject) + { + return databaseObject.SourceType switch + { + SourceType.Table => ((DatabaseTable)databaseObject).TableDefinition, + SourceType.View => ((DatabaseView)databaseObject).ViewDefinition, + SourceType.StoredProcedure => ((DatabaseStoredProcedure)databaseObject).StoredProcedureDefinition, + _ => throw new Exception( + message: $"Unsupported SourceType. It can either be Table,View, or Stored Procedure.") + }; + } } /// diff --git a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs index b0db304682..0920aaad6f 100644 --- a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs @@ -39,6 +39,11 @@ public static DocumentNode Build( NameNode name = objectTypeDefinitionNode.Name; string dbEntityName = ObjectTypeToEntityName(objectTypeDefinitionNode); + if (entities[dbEntityName].ObjectType is SourceType.StoredProcedure) + { + continue; + } + AddMutations(dbEntityName, operation: Operation.Create, entityPermissionsMap, name, inputs, objectTypeDefinitionNode, root, databaseType, entities, mutationFields); AddMutations(dbEntityName, operation: Operation.Update, entityPermissionsMap, name, inputs, objectTypeDefinitionNode, root, databaseType, entities, mutationFields); AddMutations(dbEntityName, operation: Operation.Delete, entityPermissionsMap, name, inputs, objectTypeDefinitionNode, root, databaseType, entities, mutationFields); diff --git a/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs b/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs index 3e637ac02d..b0e4e826f5 100644 --- a/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs +++ b/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs @@ -50,15 +50,23 @@ public static DocumentNode Build( Entity entity = entities[entityName]; ObjectTypeDefinitionNode returnType = GenerateReturnType(name); - returnTypes.Add(returnType); IEnumerable rolesAllowedForRead = IAuthorizationResolver.GetRolesForOperation(entityName, operation: Operation.Read, entityPermissionsMap); if (rolesAllowedForRead.Count() > 0) { - queryFields.Add(GenerateGetAllQuery(objectTypeDefinitionNode, name, returnType, inputTypes, entity, rolesAllowedForRead)); - queryFields.Add(GenerateByPKQuery(objectTypeDefinitionNode, name, databaseType, entity, rolesAllowedForRead)); + if(entity.ObjectType is SourceType.StoredProcedure) + { + queryFields.Add(GenerateStoredProcedureQuery(name, entity, rolesAllowedForRead)); + } + else + { + queryFields.Add(GenerateGetAllQuery(objectTypeDefinitionNode, name, returnType, inputTypes, entity, rolesAllowedForRead)); + queryFields.Add(GenerateByPKQuery(objectTypeDefinitionNode, name, databaseType, entity, rolesAllowedForRead)); + } } + + returnTypes.Add(returnType); } } @@ -70,6 +78,50 @@ public static DocumentNode Build( return new(definitionNodes); } + /// + /// Generates the StoredProcedure Query with input types, description, and return type. + /// + public static FieldDefinitionNode GenerateStoredProcedureQuery( + NameNode name, + Entity entity, + IEnumerable? rolesAllowedForRead = null) + { + List inputValues = new(); + List fieldDefinitionNodeDirectives = new(); + + if (entity.Parameters is not null) + { + foreach (string param in entity.Parameters.Keys) + { + inputValues.Add( + new( + location: null, + new (param), + new StringValueNode($"parameters for {name.Value} stored-procedure"), + new NamedTypeNode("String"), + defaultValue: new StringValueNode($"{entity.Parameters[param]}"), + new List()) + ); + } + } + + if (CreateAuthorizationDirectiveIfNecessary( + rolesAllowedForRead, + out DirectiveNode? authorizeDirective)) + { + fieldDefinitionNodeDirectives.Add(authorizeDirective!); + } + + return new( + location: null, + new NameNode(name.Value), + new StringValueNode($"Execute Stored-Procedure {name.Value} and get results from the database"), + inputValues, + new NonNullTypeNode(new ListTypeNode(new NonNullTypeNode(new NamedTypeNode(name)))), + fieldDefinitionNodeDirectives + ); + } + public static FieldDefinitionNode GenerateByPKQuery( ObjectTypeDefinitionNode objectTypeDefinitionNode, NameNode name, diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs index d530277549..3b54a628cf 100644 --- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -6,6 +6,7 @@ using Azure.DataApiBuilder.Service.GraphQLBuilder.CustomScalars; using Azure.DataApiBuilder.Service.GraphQLBuilder.Directives; using Azure.DataApiBuilder.Service.GraphQLBuilder.Queries; +using Azure.DataApiBuilder.Service; using HotChocolate.Language; using HotChocolate.Types; using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLNaming; @@ -16,7 +17,7 @@ namespace Azure.DataApiBuilder.Service.GraphQLBuilder.Sql public static class SchemaConverter { /// - /// Generate a GraphQL object type from a SQL table definition, combined with the runtime config entity information + /// Generate a GraphQL object type from a SQL table/view/stored-procedure definition, combined with the runtime config entity information /// /// Name of the entity in the runtime config to generate the GraphQL object type for. /// SQL database object information. @@ -36,26 +37,48 @@ public static ObjectTypeDefinitionNode FromDatabaseObject( { Dictionary fields = new(); List objectTypeDirectives = new(); - SourceDefinition sourceDefinition = - databaseObject.SourceType is SourceType.Table ? - ((DatabaseTable)databaseObject).TableDefinition : - ((DatabaseView)databaseObject).ViewDefinition; - foreach ((string columnName, ColumnDefinition column) in sourceDefinition.Columns) + SourceDefinition sourceDefinition = SourceDefinition.GetSourceDefinitionForDatabaseObject(databaseObject); + NameNode nameNode; + + if (databaseObject.SourceType is SourceType.StoredProcedure) { - List directives = new(); + nameNode = new (entityName); + Dictionary resultSet = ((StoredProcedureDefinition)sourceDefinition).ResultSet; - if (sourceDefinition.PrimaryKey.Contains(columnName)) + foreach ((string fieldName, Type fieldType) in resultSet) { - directives.Add(new DirectiveNode(PrimaryKeyDirectiveType.DirectiveName, new ArgumentNode("databaseType", column.SystemType.Name))); + NamedTypeNode nodeFieldType = new(GetGraphQLTypeForColumnType(fieldType)); + FieldDefinitionNode field = new( + location: null, + new(fieldName), + description: null, + new List(), + nodeFieldType, + new List()); + + fields.Add(fieldName, field); } + } + else + { + nameNode = new(value: GetDefinedSingularName(entityName, configEntity)); - if (column.IsAutoGenerated) + foreach ((string columnName, ColumnDefinition column) in sourceDefinition.Columns) { - directives.Add(new DirectiveNode(AutoGeneratedDirectiveType.DirectiveName)); - } + List directives = new(); - if (column.DefaultValue is not null) - { + if (sourceDefinition.PrimaryKey.Contains(columnName)) + { + directives.Add(new DirectiveNode(PrimaryKeyDirectiveType.DirectiveName, new ArgumentNode("databaseType", column.SystemType.Name))); + } + + if (column.IsAutoGenerated) + { + directives.Add(new DirectiveNode(AutoGeneratedDirectiveType.DirectiveName)); + } + + if (column.DefaultValue is not null) + { IValueNode arg = column.DefaultValue switch { byte value => new ObjectValueNode(new ObjectFieldNode(BYTE_TYPE, new IntValueNode(value))), @@ -76,34 +99,35 @@ databaseObject.SourceType is SourceType.Table ? subStatusCode: DataApiBuilderException.SubStatusCodes.GraphQLMapping) }; - directives.Add(new DirectiveNode(DefaultValueDirectiveType.DirectiveName, new ArgumentNode("value", arg))); - } + directives.Add(new DirectiveNode(DefaultValueDirectiveType.DirectiveName, new ArgumentNode("value", arg))); + } - // If no roles are allowed for the field, we should not include it in the schema. - // Consequently, the field is only added to schema if this conditional evaluates to TRUE. - if (rolesAllowedForFields.TryGetValue(key: columnName, out IEnumerable? roles)) - { - // Roles will not be null here if TryGetValue evaluates to true, so here we check if there are any roles to process. - if (roles.Count() > 0) + // If no roles are allowed for the field, we should not include it in the schema. + // Consequently, the field is only added to schema if this conditional evaluates to TRUE. + if (rolesAllowedForFields.TryGetValue(key: columnName, out IEnumerable? roles)) { - - if (GraphQLUtils.CreateAuthorizationDirectiveIfNecessary( - roles, - out DirectiveNode? authZDirective)) + // Roles will not be null here if TryGetValue evaluates to true, so here we check if there are any roles to process. + if (roles.Count() > 0) { - directives.Add(authZDirective!); - } - NamedTypeNode fieldType = new(GetGraphQLTypeForColumnType(column.SystemType)); - FieldDefinitionNode field = new( - location: null, - new(columnName), - description: null, - new List(), - column.IsNullable ? fieldType : new NonNullTypeNode(fieldType), - directives); + if (GraphQLUtils.CreateAuthorizationDirectiveIfNecessary( + roles, + out DirectiveNode? authZDirective)) + { + directives.Add(authZDirective!); + } + + NamedTypeNode fieldType = new(GetGraphQLTypeForColumnType(column.SystemType)); + FieldDefinitionNode field = new( + location: null, + new(columnName), + description: null, + new List(), + column.IsNullable ? fieldType : new NonNullTypeNode(fieldType), + directives); - fields.Add(columnName, field); + fields.Add(columnName, field); + } } } } @@ -198,7 +222,7 @@ databaseObject.SourceType is SourceType.Table ? // if the top-level entity name is already plural. return new ObjectTypeDefinitionNode( location: null, - name: new(value: GetDefinedSingularName(entityName, configEntity)), + name: nameNode, description: null, objectTypeDirectives, new List(), diff --git a/src/Service/Configurations/RuntimeConfigValidator.cs b/src/Service/Configurations/RuntimeConfigValidator.cs index b2556f48d9..270b98aeda 100644 --- a/src/Service/Configurations/RuntimeConfigValidator.cs +++ b/src/Service/Configurations/RuntimeConfigValidator.cs @@ -161,6 +161,8 @@ public static void ValidateDatabaseType( /// All these entities will create queries with the following field names /// pk query name: book_by_pk /// List query name: books + /// NOTE: we don't do this check for storedProcedure, because the name of the query is same + /// as that provided in the config, and two different entity can't have same name in the config. /// /// Entity definitions /// @@ -170,7 +172,10 @@ public static void ValidateEntitiesDoNotGenerateDuplicateQueries(IDictionary entity foreach (string entityName in entityCollection.Keys) { Entity entity = entityCollection[entityName]; + entity.TryPopulateSourceFields(); if (entity.GraphQL is null) { diff --git a/src/Service/Resolvers/IQueryBuilder.cs b/src/Service/Resolvers/IQueryBuilder.cs index 2f05b22892..06e39710ae 100644 --- a/src/Service/Resolvers/IQueryBuilder.cs +++ b/src/Service/Resolvers/IQueryBuilder.cs @@ -60,6 +60,13 @@ public interface IQueryBuilder /// public string BuildViewColumnsDetailsQuery(int numberOfParameters); + /// + /// Builds the query to obtain details about the result set for stored-procedure + /// + /// Name of stored-procedure + /// + public string BuildStoredProcedureResultDetailsQuery(string databaseObjectName); + /// /// Adds database specific quotes to string identifier /// diff --git a/src/Service/Resolvers/MsSqlQueryBuilder.cs b/src/Service/Resolvers/MsSqlQueryBuilder.cs index 6e5dcf8a67..990f417cd0 100644 --- a/src/Service/Resolvers/MsSqlQueryBuilder.cs +++ b/src/Service/Resolvers/MsSqlQueryBuilder.cs @@ -211,5 +211,17 @@ public string BuildViewColumnsDetailsQuery(int numberOfParameters) $"AND source_schema is not NULL"; return query; } + + /// + public string BuildStoredProcedureResultDetailsQuery(string databaseObjectName) + { + string query = "SELECT " + + "name as result_field_name, system_type_name, column_ordinal " + + "FROM " + + "sys.dm_exec_describe_first_result_set_for_object (" + + $"OBJECT_ID('{databaseObjectName}'), 0) " + + "WHERE is_hidden is not NULL AND is_hidden = 0"; + return query; + } } } diff --git a/src/Service/Resolvers/MySqlQueryBuilder.cs b/src/Service/Resolvers/MySqlQueryBuilder.cs index 4e34793884..bf85ebb120 100644 --- a/src/Service/Resolvers/MySqlQueryBuilder.cs +++ b/src/Service/Resolvers/MySqlQueryBuilder.cs @@ -334,5 +334,11 @@ public string BuildViewColumnsDetailsQuery(int numberOfParameters) { throw new NotImplementedException(); } + + /// + public string BuildStoredProcedureResultDetailsQuery(string databaseObjectName) + { + throw new NotImplementedException(); + } } } diff --git a/src/Service/Resolvers/PostgresQueryBuilder.cs b/src/Service/Resolvers/PostgresQueryBuilder.cs index 7d09430971..21b28b5723 100644 --- a/src/Service/Resolvers/PostgresQueryBuilder.cs +++ b/src/Service/Resolvers/PostgresQueryBuilder.cs @@ -208,5 +208,11 @@ public string BuildViewColumnsDetailsQuery(int numberOfParameters) { throw new NotImplementedException(); } + + /// + public string BuildStoredProcedureResultDetailsQuery(string databaseObjectName) + { + throw new NotImplementedException(); + } } } diff --git a/src/Service/Resolvers/SqlPaginationUtil.cs b/src/Service/Resolvers/SqlPaginationUtil.cs index 2a6745c034..0fffa7daf1 100644 --- a/src/Service/Resolvers/SqlPaginationUtil.cs +++ b/src/Service/Resolvers/SqlPaginationUtil.cs @@ -109,6 +109,28 @@ public static JsonDocument CreatePaginationConnectionFromJsonDocument(JsonDocume return result; } + /// + /// Takes the result from DB as JsonDocument and formats it in a way that can be filtered by column + /// name. It parses the Json document into a list of Dictionary with key as result_column_name + /// with it's corresponding value. + /// + public static List FormatStoredProcedureResultAsJsonList(JsonDocument jsonDocument) + { + if (jsonDocument is null) + { + return new List(); + } + + List resultJson = new(); + List> resultList = JsonSerializer.Deserialize>>(jsonDocument.RootElement.ToString())!; + foreach(Dictionary dict in resultList) + { + resultJson.Add(JsonDocument.Parse(JsonSerializer.Serialize(dict))); + } + + return resultJson; + } + /// /// Extracts the columns from the json element needed for pagination, represents them as a string in json format and base64 encodes. /// The JSON is encoded in base64 for opaqueness. The cursor should function as a token that the user copies and pastes diff --git a/src/Service/Resolvers/SqlQueryEngine.cs b/src/Service/Resolvers/SqlQueryEngine.cs index 748240a674..ded2c12a3f 100644 --- a/src/Service/Resolvers/SqlQueryEngine.cs +++ b/src/Service/Resolvers/SqlQueryEngine.cs @@ -5,6 +5,7 @@ using System.Text.Json; using System.Text.Json.Nodes; using System.Threading.Tasks; +using Azure.DataApiBuilder.Config; using Azure.DataApiBuilder.Auth; using Azure.DataApiBuilder.Service.Configurations; using Azure.DataApiBuilder.Service.Models; @@ -62,7 +63,7 @@ public SqlQueryEngine( public async Task> ExecuteAsync(IMiddlewareContext context, IDictionary parameters) { SqlQueryStructure structure = new(context, parameters, _sqlMetadataProvider, _authorizationResolver, _runtimeConfigProvider); - + if (structure.PaginationMetadata.IsPaginated) { return new Tuple( @@ -78,27 +79,39 @@ await ExecuteAsync(structure), } /// - /// Executes the given IMiddlewareContext of the GraphQL and expecting a + /// Executes the given IMiddlewareContext of the GraphQL and expecting result of stored-procedure execution as /// list of Jsons and the relevant pagination metadata back. /// public async Task, IMetadata>> ExecuteListAsync(IMiddlewareContext context, IDictionary parameters) { - SqlQueryStructure structure = new(context, parameters, _sqlMetadataProvider, _authorizationResolver, _runtimeConfigProvider); - string queryString = _queryBuilder.Build(structure); - _logger.LogInformation(queryString); - List jsonListResult = - await _queryExecutor.ExecuteQueryAsync( - queryString, - structure.Parameters, - _queryExecutor.GetJsonResultAsync>); - - if (jsonListResult is null) + _sqlMetadataProvider.EntityToDatabaseObject.TryGetValue(context.Field.Name.Value, out DatabaseObject? databaseObject); + if(databaseObject is not null && databaseObject.SourceType is SourceType.StoredProcedure) { - return new Tuple, IMetadata>(new List(), null); + SqlExecuteStructure sqlExecuteStructure = new(context.FieldSelection.Name.Value, _sqlMetadataProvider, parameters); + + return new Tuple, IMetadata>( + SqlPaginationUtil.FormatStoredProcedureResultAsJsonList(await ExecuteAsync(sqlExecuteStructure)), + PaginationMetadata.MakeEmptyPaginationMetadata()); } else { - return new Tuple, IMetadata>(jsonListResult, structure.PaginationMetadata); + SqlQueryStructure structure = new(context, parameters, _sqlMetadataProvider, _authorizationResolver, _runtimeConfigProvider); + string queryString = _queryBuilder.Build(structure); + _logger.LogInformation(queryString); + List jsonListResult = + await _queryExecutor.ExecuteQueryAsync( + queryString, + structure.Parameters, + _queryExecutor.GetJsonResultAsync>); + + if (jsonListResult is null) + { + return new Tuple, IMetadata>(new List(), null); + } + else + { + return new Tuple, IMetadata>(jsonListResult, structure.PaginationMetadata); + } } } diff --git a/src/Service/Services/GraphQLSchemaCreator.cs b/src/Service/Services/GraphQLSchemaCreator.cs index 8841141219..de2b978e3d 100644 --- a/src/Service/Services/GraphQLSchemaCreator.cs +++ b/src/Service/Services/GraphQLSchemaCreator.cs @@ -134,8 +134,7 @@ DatabaseType.postgresql or { // Skip creating the GraphQL object for the current entity due to configuration // explicitly excluding the entity from the GraphQL endpoint. - if (entity.ObjectType is SourceType.StoredProcedure - || entity.GraphQL is not null && entity.GraphQL is bool graphql && graphql == false) + if (entity.GraphQL is not null && entity.GraphQL is bool graphql && graphql == false) { continue; } @@ -147,18 +146,22 @@ DatabaseType.postgresql or IEnumerable rolesAllowedForEntity = _authorizationResolver.GetRolesForEntity(entityName); Dictionary> rolesAllowedForFields = new(); SourceDefinition sourceDefinition = _sqlMetadataProvider.GetSourceDefinition(entityName); - foreach (string column in sourceDefinition.Columns.Keys) + if (databaseObject.SourceType is not SourceType.StoredProcedure) { - IEnumerable roles = _authorizationResolver.GetRolesForField(entityName, field: column, operation: Operation.Read); - if (!rolesAllowedForFields.TryAdd(key: column, value: roles)) + foreach (string column in sourceDefinition.Columns.Keys) { - throw new DataApiBuilderException( - message: "Column already processed for building ObjectTypeDefinition authorization definition.", - statusCode: System.Net.HttpStatusCode.InternalServerError, - subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization - ); + IEnumerable roles = _authorizationResolver.GetRolesForField(entityName, field: column, operation: Operation.Read); + if (!rolesAllowedForFields.TryAdd(key: column, value: roles)) + { + throw new DataApiBuilderException( + message: "Column already processed for building ObjectTypeDefinition authorization definition.", + statusCode: System.Net.HttpStatusCode.InternalServerError, + subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization + ); + } } } + // The roles allowed for Fields are the roles allowed to READ the fields, so any role that has a read definition for the field. // Only add objectTypeDefinition for GraphQL if it has a role definition defined for access. @@ -173,7 +176,11 @@ DatabaseType.postgresql or rolesAllowedForFields ); - InputTypeBuilder.GenerateInputTypesForObjectType(node, inputObjects); + if (databaseObject.SourceType is not SourceType.StoredProcedure) + { + InputTypeBuilder.GenerateInputTypesForObjectType(node, inputObjects); + } + objectTypes.Add(entityName, node); } } diff --git a/src/Service/Services/MetadataProviders/MsSqlMetadataProvider.cs b/src/Service/Services/MetadataProviders/MsSqlMetadataProvider.cs index 7360c0a9f5..715c2807fa 100644 --- a/src/Service/Services/MetadataProviders/MsSqlMetadataProvider.cs +++ b/src/Service/Services/MetadataProviders/MsSqlMetadataProvider.cs @@ -68,6 +68,7 @@ public override Type SqlToCLRType(string sqlType) return typeof(DateTimeOffset); case "char": case "varchar": + case "varchar(max)": case "text": case "nchar": case "nvarchar": diff --git a/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs index 2683086433..749f86dd7b 100644 --- a/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs @@ -142,16 +142,7 @@ public SourceDefinition GetSourceDefinition(string entityName) subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound); } - switch (databaseObject!.SourceType) - { - case SourceType.Table: - return ((DatabaseTable)databaseObject).TableDefinition; - case SourceType.View: - return ((DatabaseView)databaseObject).ViewDefinition; - default: - // For stored procedures - return null!; - } + return SourceDefinition.GetSourceDefinitionForDatabaseObject(databaseObject); } /// @@ -668,6 +659,14 @@ await FillSchemaForStoredProcedureAsync( GetSchemaName(entityName), GetDatabaseObjectName(entityName), GetStoredProcedureDefinition(entityName)); + + if (GetDatabaseType() == DatabaseType.mssql) + { + await PopulateResultSetDefinitionsForStoredProcedureAsync( + GetSchemaName(entityName), + GetDatabaseObjectName(entityName), + GetStoredProcedureDefinition(entityName)); + } } else if (entitySourceType is SourceType.Table) { @@ -699,6 +698,41 @@ await PopulateBaseTableDefinitionsForViewAsync( } + /// + /// Queries DB to get the result fields name and type to + /// populate the result set definition for entities specified as stored procedures + /// + private async Task PopulateResultSetDefinitionsForStoredProcedureAsync( + string schemaName, + string storedProcedureName, + SourceDefinition sourceDefinition) + { + StoredProcedureDefinition storedProcedureDefinition = (StoredProcedureDefinition)sourceDefinition; + string dbStoredProcedureName = $"{schemaName}.{storedProcedureName}"; + // Generate query to get result set details + // of the stored procedure. + string queryForResultSetDetails = SqlQueryBuilder.BuildStoredProcedureResultDetailsQuery( + dbStoredProcedureName); + + // Execute the query to get columns' details. + JsonArray? resultArray = await QueryExecutor.ExecuteQueryAsync( + sqltext: queryForResultSetDetails, + parameters: null!, + dataReaderHandler: QueryExecutor.GetJsonArrayAsync); + using JsonDocument sqlResult = JsonDocument.Parse(resultArray!.ToJsonString()); + + // Iterate through each row returned by the query which corresponds to + // one row in the result set. + foreach (JsonElement element in sqlResult.RootElement.EnumerateArray()) + { + string resultFieldName = element.GetProperty("result_field_name").ToString(); + Type resultFieldType = SqlToCLRType(element.GetProperty("system_type_name").ToString()); + + // Store the dictionary containing result set field with it's type + storedProcedureDefinition.ResultSet.Add(resultFieldName, resultFieldType); + } + } + private async Task PopulateBaseTableDefinitionsForViewAsync( string schemaName, string viewName, diff --git a/src/Service/dab-config.MsSql.overrides.example.json b/src/Service/dab-config.MsSql.overrides.example.json index a8add52ca8..62b5e1ed41 100644 --- a/src/Service/dab-config.MsSql.overrides.example.json +++ b/src/Service/dab-config.MsSql.overrides.example.json @@ -2,7 +2,7 @@ "$schema": "../schemas/dab.draft-01.schema.json", "data-source": { "database-type": "mssql", - "connection-string": "Server=tcp:127.0.0.1,1433;Persist Security Info=False;User ID=sa;Password=REPLACEME;MultipleActiveResultSets=False;Connection Timeout=5;" + "connection-string": "Server=tcp:127.0.0.1,1433;Persist Security Info=False;Trusted_Connection=True;MultipleActiveResultSets=False;Connection Timeout=5;" }, "mssql": { "set-session-context": false @@ -883,6 +883,24 @@ "actions": [ "*" ] } ] + }, + "CountTestBooks": { + "source": { + "type": "stored-procedure", + "object": "count_test_books" + }, + "rest": true, + "graphql": false, + "permissions": [ + { + "role": "anonymous", + "actions": [ "read" ] + }, + { + "role": "authenticated", + "actions": [ "*" ] + } + ] } } } From 7b43e4733fabc082f897d05a4fa08fb63a54760b Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Mon, 14 Nov 2022 14:33:14 +0530 Subject: [PATCH 02/42] fixing doc --- docs/internals/StoredProcedureDesignDoc.md | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/docs/internals/StoredProcedureDesignDoc.md b/docs/internals/StoredProcedureDesignDoc.md index ad845eba80..6fcb00e8a8 100644 --- a/docs/internals/StoredProcedureDesignDoc.md +++ b/docs/internals/StoredProcedureDesignDoc.md @@ -35,19 +35,11 @@ Thus a stored procedure entity might look like: parameters can either be fixed as above or passed at runtime through - query parameters for GET request - request body for POST, PUT, PATCH, DELETE -- For GraphQL, the stored-procedure will look something like this passed in the body -``` -{ - GetBooks(param1:value, param2:value) {} -} -``` > **Spec Interpretation**: > - since not explicitly stated in the specification, request body for GET will be ignored altogether. > - Parameter resolution will go as follows, from highest to lowest priority: request (query string or body) > config defaults > sql defaults > - NOTE: sql defaults not so easy to infer for parameters, so we explicitly require all parameters to be provided either in the request or config in this first version. -> - GRAPHQL -> - if the request doesn't contain the parameter values, default values from the config will be picked up. ### Stored Procedure Permissions @@ -102,8 +94,6 @@ Implementation was segmented into 5 main sections: > - Stored procedures must be specified as "stored-procedure" source type > - See [sample source config](#sample-source) > - Unfortunately, System.Text.Json does not natively support Kebab-casing for converting hyphenated strings ("stored-procedure") to a CamelCase enum (StoredProcedure). As such, Newtonsoft is used for deserialization. If we want to migrate to System.Text.Json, we either need to change the spec and only accept non-hyphenated strings or write our own custom string-to-enum converter, which was out of scope for v1. -> GRAPHQL -> - No change required here. ### 2. Metadata Validation @@ -116,13 +106,6 @@ Implementation was segmented into 5 main sections:
-> ### `RuntimeConfigValidator.cs` -> - `ValidateEntitiesDoNotGenerateDuplicateQueries` should skip check for Stored-Procedures. - - -> ### `SchemaConverter.cs` and `GraphQLSchemaCreator.cs` -> - Generate a GraphQL object type from a SQL table/view/stored-procedure definition, combined with the runtime config entity information - > ### `SqlMetadataProvider.cs` > - Problem: when we draw metadata from the database schema, we implicitly check if each entity specified in config exists in the database. Path: in `Startup.cs`, the `PerformOnConfigChangeAsync()` method invokes `InitializeAsync()` of the metadata provider bound at runtime, which then invokes `PopulateTableDefinitionForEntities()`. Several steps down the stack `FillSchemaForTableAsync()` is called, which performs a `SELECT * FROM {table_name}` for the given entity and adds the resulting `DataTable` object to the `EntitiesDataSet` class variable. Unfortunately, stored procedure metadata cannot be queried using the same syntax. As such, running the select statement on a stored procedure source name will cause a Sql exception to surface and result in runtime initialization failure. > - Instead, we introduce the `PopulateStoredProcedureDefinitionForEntities()` method, which iterates over only entites labeled as stored procedures to fill their metadata, and we simply skip over entities labeled as Stored Procedures in the `PopulateTableDefinitionForEntities()` method. @@ -177,9 +160,6 @@ Implementation was segmented into 5 main sections:
-> ### `QueryBuilder.cs` -> - It should not generate Both FindAll and FindByPK query for Stored-Procedure. - > ### `MsSqlQueryBuilder.cs` > - Added the `Build(SqlExecuteStructure)` that builds the query string for execute requests as `EXECUTE {schema_name}.{stored_proc_name} {parameters}` > - Added `BuildProcedureParameterList` to build the list of parameters from the `ProcedureParameters` dictionary. The result of this method might look like `@id = @param0, @title = @param1`. From 5774c0f75c222b3283046240a4c185547e37fd10 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Mon, 14 Nov 2022 14:37:31 +0530 Subject: [PATCH 03/42] fix formatting --- src/Config/DatabaseObject.cs | 2 +- .../Queries/QueryBuilder.cs | 6 +-- .../Sql/SchemaConverter.cs | 43 +++++++++---------- src/Service/Resolvers/SqlPaginationUtil.cs | 4 +- src/Service/Resolvers/SqlQueryEngine.cs | 6 +-- src/Service/Services/GraphQLSchemaCreator.cs | 3 +- .../MetadataProviders/SqlMetadataProvider.cs | 4 +- 7 files changed, 33 insertions(+), 35 deletions(-) diff --git a/src/Config/DatabaseObject.cs b/src/Config/DatabaseObject.cs index 83de5a70ff..69e41e3061 100644 --- a/src/Config/DatabaseObject.cs +++ b/src/Config/DatabaseObject.cs @@ -77,7 +77,7 @@ public DatabaseStoredProcedure(string schemaName, string tableName) public StoredProcedureDefinition StoredProcedureDefinition { get; set; } = null!; } - public class StoredProcedureDefinition: SourceDefinition + public class StoredProcedureDefinition : SourceDefinition { /// /// The list of input parameters diff --git a/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs b/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs index b0e4e826f5..193146366b 100644 --- a/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs +++ b/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs @@ -55,7 +55,7 @@ public static DocumentNode Build( if (rolesAllowedForRead.Count() > 0) { - if(entity.ObjectType is SourceType.StoredProcedure) + if (entity.ObjectType is SourceType.StoredProcedure) { queryFields.Add(GenerateStoredProcedureQuery(name, entity, rolesAllowedForRead)); } @@ -65,7 +65,7 @@ public static DocumentNode Build( queryFields.Add(GenerateByPKQuery(objectTypeDefinitionNode, name, databaseType, entity, rolesAllowedForRead)); } } - + returnTypes.Add(returnType); } } @@ -96,7 +96,7 @@ public static FieldDefinitionNode GenerateStoredProcedureQuery( inputValues.Add( new( location: null, - new (param), + new(param), new StringValueNode($"parameters for {name.Value} stored-procedure"), new NamedTypeNode("String"), defaultValue: new StringValueNode($"{entity.Parameters[param]}"), diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs index 3b54a628cf..93ea247d48 100644 --- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -6,7 +6,6 @@ using Azure.DataApiBuilder.Service.GraphQLBuilder.CustomScalars; using Azure.DataApiBuilder.Service.GraphQLBuilder.Directives; using Azure.DataApiBuilder.Service.GraphQLBuilder.Queries; -using Azure.DataApiBuilder.Service; using HotChocolate.Language; using HotChocolate.Types; using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLNaming; @@ -42,7 +41,7 @@ public static ObjectTypeDefinitionNode FromDatabaseObject( if (databaseObject.SourceType is SourceType.StoredProcedure) { - nameNode = new (entityName); + nameNode = new(entityName); Dictionary resultSet = ((StoredProcedureDefinition)sourceDefinition).ResultSet; foreach ((string fieldName, Type fieldType) in resultSet) @@ -55,7 +54,7 @@ public static ObjectTypeDefinitionNode FromDatabaseObject( new List(), nodeFieldType, new List()); - + fields.Add(fieldName, field); } } @@ -79,25 +78,25 @@ public static ObjectTypeDefinitionNode FromDatabaseObject( if (column.DefaultValue is not null) { - IValueNode arg = column.DefaultValue switch - { - byte value => new ObjectValueNode(new ObjectFieldNode(BYTE_TYPE, new IntValueNode(value))), - short value => new ObjectValueNode(new ObjectFieldNode(SHORT_TYPE, new IntValueNode(value))), - int value => new ObjectValueNode(new ObjectFieldNode(INT_TYPE, value)), - long value => new ObjectValueNode(new ObjectFieldNode(LONG_TYPE, new IntValueNode(value))), - string value => new ObjectValueNode(new ObjectFieldNode(STRING_TYPE, value)), - bool value => new ObjectValueNode(new ObjectFieldNode(BOOLEAN_TYPE, value)), - float value => new ObjectValueNode(new ObjectFieldNode(SINGLE_TYPE, new SingleType().ParseValue(value))), - double value => new ObjectValueNode(new ObjectFieldNode(FLOAT_TYPE, value)), - decimal value => new ObjectValueNode(new ObjectFieldNode(DECIMAL_TYPE, new FloatValueNode(value))), - DateTime value => new ObjectValueNode(new ObjectFieldNode(DATETIME_TYPE, new DateTimeType().ParseResult(value))), - DateTimeOffset value => new ObjectValueNode(new ObjectFieldNode(DATETIME_TYPE, new DateTimeType().ParseValue(value))), - byte[] value => new ObjectValueNode(new ObjectFieldNode(BYTEARRAY_TYPE, new ByteArrayType().ParseValue(value))), - _ => throw new DataApiBuilderException( - message: $"The type {column.DefaultValue.GetType()} is not supported as a GraphQL default value", - statusCode: HttpStatusCode.InternalServerError, - subStatusCode: DataApiBuilderException.SubStatusCodes.GraphQLMapping) - }; + IValueNode arg = column.DefaultValue switch + { + byte value => new ObjectValueNode(new ObjectFieldNode(BYTE_TYPE, new IntValueNode(value))), + short value => new ObjectValueNode(new ObjectFieldNode(SHORT_TYPE, new IntValueNode(value))), + int value => new ObjectValueNode(new ObjectFieldNode(INT_TYPE, value)), + long value => new ObjectValueNode(new ObjectFieldNode(LONG_TYPE, new IntValueNode(value))), + string value => new ObjectValueNode(new ObjectFieldNode(STRING_TYPE, value)), + bool value => new ObjectValueNode(new ObjectFieldNode(BOOLEAN_TYPE, value)), + float value => new ObjectValueNode(new ObjectFieldNode(SINGLE_TYPE, new SingleType().ParseValue(value))), + double value => new ObjectValueNode(new ObjectFieldNode(FLOAT_TYPE, value)), + decimal value => new ObjectValueNode(new ObjectFieldNode(DECIMAL_TYPE, new FloatValueNode(value))), + DateTime value => new ObjectValueNode(new ObjectFieldNode(DATETIME_TYPE, new DateTimeType().ParseResult(value))), + DateTimeOffset value => new ObjectValueNode(new ObjectFieldNode(DATETIME_TYPE, new DateTimeType().ParseValue(value))), + byte[] value => new ObjectValueNode(new ObjectFieldNode(BYTEARRAY_TYPE, new ByteArrayType().ParseValue(value))), + _ => throw new DataApiBuilderException( + message: $"The type {column.DefaultValue.GetType()} is not supported as a GraphQL default value", + statusCode: HttpStatusCode.InternalServerError, + subStatusCode: DataApiBuilderException.SubStatusCodes.GraphQLMapping) + }; directives.Add(new DirectiveNode(DefaultValueDirectiveType.DirectiveName, new ArgumentNode("value", arg))); } diff --git a/src/Service/Resolvers/SqlPaginationUtil.cs b/src/Service/Resolvers/SqlPaginationUtil.cs index 8a0ffad31d..6fa53f6f9b 100644 --- a/src/Service/Resolvers/SqlPaginationUtil.cs +++ b/src/Service/Resolvers/SqlPaginationUtil.cs @@ -122,8 +122,8 @@ public static List FormatStoredProcedureResultAsJsonList(JsonDocum } List resultJson = new(); - List> resultList = JsonSerializer.Deserialize>>(jsonDocument.RootElement.ToString())!; - foreach(Dictionary dict in resultList) + List> resultList = JsonSerializer.Deserialize>>(jsonDocument.RootElement.ToString())!; + foreach (Dictionary dict in resultList) { resultJson.Add(JsonDocument.Parse(JsonSerializer.Serialize(dict))); } diff --git a/src/Service/Resolvers/SqlQueryEngine.cs b/src/Service/Resolvers/SqlQueryEngine.cs index ded2c12a3f..488ff06089 100644 --- a/src/Service/Resolvers/SqlQueryEngine.cs +++ b/src/Service/Resolvers/SqlQueryEngine.cs @@ -5,8 +5,8 @@ using System.Text.Json; using System.Text.Json.Nodes; using System.Threading.Tasks; -using Azure.DataApiBuilder.Config; using Azure.DataApiBuilder.Auth; +using Azure.DataApiBuilder.Config; using Azure.DataApiBuilder.Service.Configurations; using Azure.DataApiBuilder.Service.Models; using Azure.DataApiBuilder.Service.Services; @@ -63,7 +63,7 @@ public SqlQueryEngine( public async Task> ExecuteAsync(IMiddlewareContext context, IDictionary parameters) { SqlQueryStructure structure = new(context, parameters, _sqlMetadataProvider, _authorizationResolver, _runtimeConfigProvider); - + if (structure.PaginationMetadata.IsPaginated) { return new Tuple( @@ -85,7 +85,7 @@ await ExecuteAsync(structure), public async Task, IMetadata>> ExecuteListAsync(IMiddlewareContext context, IDictionary parameters) { _sqlMetadataProvider.EntityToDatabaseObject.TryGetValue(context.Field.Name.Value, out DatabaseObject? databaseObject); - if(databaseObject is not null && databaseObject.SourceType is SourceType.StoredProcedure) + if (databaseObject is not null && databaseObject.SourceType is SourceType.StoredProcedure) { SqlExecuteStructure sqlExecuteStructure = new(context.FieldSelection.Name.Value, _sqlMetadataProvider, parameters); diff --git a/src/Service/Services/GraphQLSchemaCreator.cs b/src/Service/Services/GraphQLSchemaCreator.cs index de2b978e3d..8b407578ab 100644 --- a/src/Service/Services/GraphQLSchemaCreator.cs +++ b/src/Service/Services/GraphQLSchemaCreator.cs @@ -161,7 +161,6 @@ DatabaseType.postgresql or } } } - // The roles allowed for Fields are the roles allowed to READ the fields, so any role that has a read definition for the field. // Only add objectTypeDefinition for GraphQL if it has a role definition defined for access. @@ -180,7 +179,7 @@ DatabaseType.postgresql or { InputTypeBuilder.GenerateInputTypesForObjectType(node, inputObjects); } - + objectTypes.Add(entityName, node); } } diff --git a/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs index b40429bd0d..038dd65367 100644 --- a/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs @@ -7,6 +7,7 @@ using System.Net; using System.Text; using System.Text.Json; +using System.Text.Json.Nodes; using System.Threading.Tasks; using Azure.DataApiBuilder.Config; using Azure.DataApiBuilder.Service.Configurations; @@ -14,7 +15,6 @@ using Azure.DataApiBuilder.Service.Parsers; using Azure.DataApiBuilder.Service.Resolvers; using Microsoft.Extensions.Logging; -using System.Text.Json.Nodes; namespace Azure.DataApiBuilder.Service.Services { @@ -699,7 +699,7 @@ await FillSchemaForStoredProcedureAsync( GetSchemaName(entityName), GetDatabaseObjectName(entityName), GetStoredProcedureDefinition(entityName)); - + if (GetDatabaseType() == DatabaseType.mssql) { await PopulateResultSetDefinitionsForStoredProcedureAsync( From 58cca3e7b5473ea5bb15526a83a4dd6662f02cbb Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 15 Nov 2022 23:50:06 +0530 Subject: [PATCH 04/42] adding support for mutation --- .../Mutations/MutationBuilder.cs | 81 +++++++++++++++++++ .../Sql/SchemaConverter.cs | 25 ++++-- src/Service/Resolvers/SqlMutationEngine.cs | 52 ++++++++++++ 3 files changed, 153 insertions(+), 5 deletions(-) diff --git a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs index 6aafb2db26..e1f755f830 100644 --- a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs @@ -3,6 +3,7 @@ using HotChocolate.Language; using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLNaming; using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLUtils; +using static Azure.DataApiBuilder.Service.GraphQLBuilder.Queries.QueryBuilder; namespace Azure.DataApiBuilder.Service.GraphQLBuilder.Mutations { @@ -41,6 +42,11 @@ public static DocumentNode Build( if (entities[dbEntityName].ObjectType is SourceType.StoredProcedure) { + Operation storedProcedureOperation = GetOperationTypeForStoredProcedure(dbEntityName, entityPermissionsMap); + if (storedProcedureOperation is not Operation.Read) { + AddMutationsForStoredProcedure(dbEntityName, storedProcedureOperation, entityPermissionsMap, name, entities, mutationFields); + } + continue; } @@ -61,6 +67,21 @@ public static DocumentNode Build( return new(definitionNodes); } + private static Operation GetOperationTypeForStoredProcedure( + string dbEntityName, + Dictionary? entityPermissionsMap + ) + { + if (entityPermissionsMap![dbEntityName].OperationToRolesMap.Count == 1) { + return entityPermissionsMap[dbEntityName].OperationToRolesMap.First().Key; + } + else + { + throw new Exception("Stored Procedure cannot have more than one operation."); + } + + } + /// /// Helper function to create mutation definitions. /// @@ -108,6 +129,66 @@ List mutationFields } } + private static void AddMutationsForStoredProcedure( + string dbEntityName, + Operation operation, + Dictionary? entityPermissionsMap, + NameNode name, + IDictionary entities, + List mutationFields + ) + { + IEnumerable rolesAllowedForMutation = IAuthorizationResolver.GetRolesForOperation(dbEntityName, operation: operation, entityPermissionsMap); + if (rolesAllowedForMutation.Count() > 0) + { + mutationFields.Add(GenerateStoredProcedureMutation(name, entities[dbEntityName], rolesAllowedForMutation)); + } + } + + /// + /// Generates the StoredProcedure Query with input types, description, and return type. + /// + public static FieldDefinitionNode GenerateStoredProcedureMutation( + NameNode name, + Entity entity, + IEnumerable? rolesAllowedForMutation = null) + { + List inputValues = new(); + List fieldDefinitionNodeDirectives = new(); + + if (entity.Parameters is not null) + { + foreach (string param in entity.Parameters.Keys) + { + inputValues.Add( + new( + location: null, + new(param), + new StringValueNode($"parameters for {name.Value} stored-procedure"), + new NamedTypeNode("String"), + defaultValue: new StringValueNode($"{entity.Parameters[param]}"), + new List()) + ); + } + } + + if (CreateAuthorizationDirectiveIfNecessary( + rolesAllowedForMutation, + out DirectiveNode? authorizeDirective)) + { + fieldDefinitionNodeDirectives.Add(authorizeDirective!); + } + + return new( + location: null, + new NameNode(name.Value), + new StringValueNode($"Execute Stored-Procedure {name.Value}."), + inputValues, + new NonNullTypeNode(new NamedTypeNode(name)), + fieldDefinitionNodeDirectives + ); + } + public static Operation DetermineMutationOperationTypeBasedOnInputType(string inputTypeName) { return inputTypeName switch diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs index 93ea247d48..edd09de115 100644 --- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -44,18 +44,33 @@ public static ObjectTypeDefinitionNode FromDatabaseObject( nameNode = new(entityName); Dictionary resultSet = ((StoredProcedureDefinition)sourceDefinition).ResultSet; - foreach ((string fieldName, Type fieldType) in resultSet) + if (resultSet.Count == 0) { - NamedTypeNode nodeFieldType = new(GetGraphQLTypeForColumnType(fieldType)); FieldDefinitionNode field = new( location: null, - new(fieldName), + new("result"), description: null, new List(), - nodeFieldType, + new StringType().ToTypeNode(), new List()); - fields.Add(fieldName, field); + fields.Add("result", field); + } + else + { + foreach ((string fieldName, Type fieldType) in resultSet) + { + NamedTypeNode nodeFieldType = new(GetGraphQLTypeForColumnType(fieldType)); + FieldDefinitionNode field = new( + location: null, + new(fieldName), + description: null, + new List(), + nodeFieldType, + new List()); + + fields.Add(fieldName, field); + } } } else diff --git a/src/Service/Resolvers/SqlMutationEngine.cs b/src/Service/Resolvers/SqlMutationEngine.cs index 8a1fa1e2f0..6f427748a6 100644 --- a/src/Service/Resolvers/SqlMutationEngine.cs +++ b/src/Service/Resolvers/SqlMutationEngine.cs @@ -21,6 +21,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; +using System.Text.Json.Nodes; namespace Azure.DataApiBuilder.Service.Resolvers { @@ -67,6 +68,31 @@ public SqlMutationEngine( /// JSON object result and its related pagination metadata public async Task> ExecuteAsync(IMiddlewareContext context, IDictionary parameters) { + _sqlMetadataProvider.EntityToDatabaseObject.TryGetValue(context.Field.Name.Value, out DatabaseObject? databaseObject); + if (databaseObject is not null && databaseObject.SourceType is SourceType.StoredProcedure) + { + SqlExecuteStructure sqlExecuteStructure = new(context.FieldSelection.Name.Value, _sqlMetadataProvider, parameters); + string queryText = _queryBuilder.Build(sqlExecuteStructure); + _logger.LogInformation(queryText); + + Tuple?, Dictionary>? resultRowAndProperties = + await _queryExecutor.ExecuteQueryAsync( + queryText, + sqlExecuteStructure.Parameters, + _queryExecutor.ExtractRowFromDbDataReader); + + JsonDocument jsonResult = JsonDocument.Parse("{\"result\": {}}"); + if (resultRowAndProperties.Item1 is not null) + { + jsonResult = JsonSerializer.SerializeToDocument(resultRowAndProperties.Item1); + } + + + return new Tuple( + jsonResult, + PaginationMetadata.MakeEmptyPaginationMetadata()); + } + if (context.Selection.Type.IsListType()) { throw new NotSupportedException("Returning list types from mutations not supported"); @@ -320,6 +346,32 @@ resultRowAndProperties.Item1 is null || return null; } + private async Task ExecuteAsync(SqlExecuteStructure structure) + { + string queryString = _queryBuilder.Build(structure); + _logger.LogInformation(queryString); + + // JsonArray resultArray = + // await _queryExecutor.ExecuteQueryAsync( + // queryString, + // structure.Parameters, + // _queryExecutor.GetJsonArrayAsync); + + JsonDocument jsonDocument = null; + + // If result set is non-empty, parse rows from json array into JsonDocument + // if (resultArray is not null && resultArray.Count > 0) + // { + // jsonDocument = JsonDocument.Parse(resultArray.ToJsonString()); + // } + // else + // { + // _logger.LogInformation("Did not return enough rows."); + // } + + return jsonDocument; + } + /// /// Helper function returns an OkObjectResult with provided arguments in a /// form that complies with vNext Api guidelines. From f7015ce0773c4711d139f627b98f3962fbc06715 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 16 Nov 2022 10:13:16 +0530 Subject: [PATCH 05/42] fixing mutation queries --- .../Mutations/MutationBuilder.cs | 2 +- src/Service/Resolvers/SqlMutationEngine.cs | 24 ------------------- src/Service/Services/ResolverMiddleware.cs | 15 +++++++++--- 3 files changed, 13 insertions(+), 28 deletions(-) diff --git a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs index e1f755f830..eff6ad4e62 100644 --- a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs @@ -184,7 +184,7 @@ public static FieldDefinitionNode GenerateStoredProcedureMutation( new NameNode(name.Value), new StringValueNode($"Execute Stored-Procedure {name.Value}."), inputValues, - new NonNullTypeNode(new NamedTypeNode(name)), + new NonNullTypeNode(new ListTypeNode(new NonNullTypeNode(new NamedTypeNode(name)))), fieldDefinitionNodeDirectives ); } diff --git a/src/Service/Resolvers/SqlMutationEngine.cs b/src/Service/Resolvers/SqlMutationEngine.cs index 6f427748a6..6ae1f60d18 100644 --- a/src/Service/Resolvers/SqlMutationEngine.cs +++ b/src/Service/Resolvers/SqlMutationEngine.cs @@ -68,30 +68,6 @@ public SqlMutationEngine( /// JSON object result and its related pagination metadata public async Task> ExecuteAsync(IMiddlewareContext context, IDictionary parameters) { - _sqlMetadataProvider.EntityToDatabaseObject.TryGetValue(context.Field.Name.Value, out DatabaseObject? databaseObject); - if (databaseObject is not null && databaseObject.SourceType is SourceType.StoredProcedure) - { - SqlExecuteStructure sqlExecuteStructure = new(context.FieldSelection.Name.Value, _sqlMetadataProvider, parameters); - string queryText = _queryBuilder.Build(sqlExecuteStructure); - _logger.LogInformation(queryText); - - Tuple?, Dictionary>? resultRowAndProperties = - await _queryExecutor.ExecuteQueryAsync( - queryText, - sqlExecuteStructure.Parameters, - _queryExecutor.ExtractRowFromDbDataReader); - - JsonDocument jsonResult = JsonDocument.Parse("{\"result\": {}}"); - if (resultRowAndProperties.Item1 is not null) - { - jsonResult = JsonSerializer.SerializeToDocument(resultRowAndProperties.Item1); - } - - - return new Tuple( - jsonResult, - PaginationMetadata.MakeEmptyPaginationMetadata()); - } if (context.Selection.Type.IsListType()) { diff --git a/src/Service/Services/ResolverMiddleware.cs b/src/Service/Services/ResolverMiddleware.cs index 2b8b8b00cd..5ada5bc46b 100644 --- a/src/Service/Services/ResolverMiddleware.cs +++ b/src/Service/Services/ResolverMiddleware.cs @@ -62,9 +62,18 @@ public async Task InvokeAsync(IMiddlewareContext context) { IDictionary parameters = GetParametersFromContext(context); - Tuple result = await _mutationEngine.ExecuteAsync(context, parameters); - context.Result = result.Item1; - SetNewMetadata(context, result.Item2); + if (context.Selection.Type.IsListType()) + { + Tuple, IMetadata> result = await _queryEngine.ExecuteListAsync(context, parameters); + context.Result = result.Item1; + SetNewMetadata(context, result.Item2); + } + else + { + Tuple result = await _mutationEngine.ExecuteAsync(context, parameters); + context.Result = result.Item1; + SetNewMetadata(context, result.Item2); + } } else if (context.Selection.Field.Coordinate.TypeName.Value == "Query") { From c6cfd936e5366ebbc607719b3d078d896b6dc851 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 16 Nov 2022 10:14:29 +0530 Subject: [PATCH 06/42] fixing temp changes --- ConfigGenerators/dab-config.sql.reference.json | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/ConfigGenerators/dab-config.sql.reference.json b/ConfigGenerators/dab-config.sql.reference.json index b4b1d68697..4d82461c78 100644 --- a/ConfigGenerators/dab-config.sql.reference.json +++ b/ConfigGenerators/dab-config.sql.reference.json @@ -945,24 +945,6 @@ "actions": [ "*" ] } ] - }, - "CountTestBooks": { - "source": { - "type": "stored-procedure", - "object": "count_test_books" - }, - "rest": true, - "graphql": false, - "permissions": [ - { - "role": "anonymous", - "actions": [ "read" ] - }, - { - "role": "authenticated", - "actions": [ "*" ] - } - ] } } } From 3bb26705e5c0e31d452651cc63b2c6601fe3343d Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 16 Nov 2022 12:31:20 +0530 Subject: [PATCH 07/42] cleaning code --- .../Mutations/MutationBuilder.cs | 89 ++++++++++--------- .../Queries/QueryBuilder.cs | 82 ++++++++--------- .../Sql/SchemaConverter.cs | 2 + src/Service/Resolvers/SqlMutationEngine.cs | 28 ------ src/Service/Resolvers/SqlPaginationUtil.cs | 22 ----- src/Service/Resolvers/SqlQueryEngine.cs | 3 +- src/Service/Services/ResolverMiddleware.cs | 2 + 7 files changed, 92 insertions(+), 136 deletions(-) diff --git a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs index eff6ad4e62..d8916c3e6d 100644 --- a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs @@ -3,7 +3,6 @@ using HotChocolate.Language; using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLNaming; using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLUtils; -using static Azure.DataApiBuilder.Service.GraphQLBuilder.Queries.QueryBuilder; namespace Azure.DataApiBuilder.Service.GraphQLBuilder.Mutations { @@ -43,7 +42,8 @@ public static DocumentNode Build( if (entities[dbEntityName].ObjectType is SourceType.StoredProcedure) { Operation storedProcedureOperation = GetOperationTypeForStoredProcedure(dbEntityName, entityPermissionsMap); - if (storedProcedureOperation is not Operation.Read) { + if (storedProcedureOperation is not Operation.Read) + { AddMutationsForStoredProcedure(dbEntityName, storedProcedureOperation, entityPermissionsMap, name, entities, mutationFields); } @@ -72,7 +72,8 @@ private static Operation GetOperationTypeForStoredProcedure( Dictionary? entityPermissionsMap ) { - if (entityPermissionsMap![dbEntityName].OperationToRolesMap.Count == 1) { + if (entityPermissionsMap![dbEntityName].OperationToRolesMap.Count == 1) + { return entityPermissionsMap[dbEntityName].OperationToRolesMap.First().Key; } else @@ -141,53 +142,53 @@ List mutationFields IEnumerable rolesAllowedForMutation = IAuthorizationResolver.GetRolesForOperation(dbEntityName, operation: operation, entityPermissionsMap); if (rolesAllowedForMutation.Count() > 0) { - mutationFields.Add(GenerateStoredProcedureMutation(name, entities[dbEntityName], rolesAllowedForMutation)); + mutationFields.Add(GraphQLStoredProcedureBuilder.GenerateStoredProcedureSchema(name, entities[dbEntityName], rolesAllowedForMutation)); } } /// /// Generates the StoredProcedure Query with input types, description, and return type. /// - public static FieldDefinitionNode GenerateStoredProcedureMutation( - NameNode name, - Entity entity, - IEnumerable? rolesAllowedForMutation = null) - { - List inputValues = new(); - List fieldDefinitionNodeDirectives = new(); - - if (entity.Parameters is not null) - { - foreach (string param in entity.Parameters.Keys) - { - inputValues.Add( - new( - location: null, - new(param), - new StringValueNode($"parameters for {name.Value} stored-procedure"), - new NamedTypeNode("String"), - defaultValue: new StringValueNode($"{entity.Parameters[param]}"), - new List()) - ); - } - } - - if (CreateAuthorizationDirectiveIfNecessary( - rolesAllowedForMutation, - out DirectiveNode? authorizeDirective)) - { - fieldDefinitionNodeDirectives.Add(authorizeDirective!); - } - - return new( - location: null, - new NameNode(name.Value), - new StringValueNode($"Execute Stored-Procedure {name.Value}."), - inputValues, - new NonNullTypeNode(new ListTypeNode(new NonNullTypeNode(new NamedTypeNode(name)))), - fieldDefinitionNodeDirectives - ); - } + // public static FieldDefinitionNode GenerateStoredProcedureMutation( + // NameNode name, + // Entity entity, + // IEnumerable? rolesAllowedForMutation = null) + // { + // List inputValues = new(); + // List fieldDefinitionNodeDirectives = new(); + + // if (entity.Parameters is not null) + // { + // foreach (string param in entity.Parameters.Keys) + // { + // inputValues.Add( + // new( + // location: null, + // new(param), + // new StringValueNode($"parameters for {name.Value} stored-procedure"), + // new NamedTypeNode("String"), + // defaultValue: new StringValueNode($"{entity.Parameters[param]}"), + // new List()) + // ); + // } + // } + + // if (CreateAuthorizationDirectiveIfNecessary( + // rolesAllowedForMutation, + // out DirectiveNode? authorizeDirective)) + // { + // fieldDefinitionNodeDirectives.Add(authorizeDirective!); + // } + + // return new( + // location: null, + // new NameNode(name.Value), + // new StringValueNode($"Execute Stored-Procedure {name.Value}."), + // inputValues, + // new NonNullTypeNode(new ListTypeNode(new NonNullTypeNode(new NamedTypeNode(name)))), + // fieldDefinitionNodeDirectives + // ); + // } public static Operation DetermineMutationOperationTypeBasedOnInputType(string inputTypeName) { diff --git a/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs b/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs index 193146366b..fbb9943f50 100644 --- a/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs +++ b/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs @@ -57,7 +57,7 @@ public static DocumentNode Build( { if (entity.ObjectType is SourceType.StoredProcedure) { - queryFields.Add(GenerateStoredProcedureQuery(name, entity, rolesAllowedForRead)); + queryFields.Add(GraphQLStoredProcedureBuilder.GenerateStoredProcedureSchema(name, entity, rolesAllowedForRead)); } else { @@ -81,46 +81,46 @@ public static DocumentNode Build( /// /// Generates the StoredProcedure Query with input types, description, and return type. /// - public static FieldDefinitionNode GenerateStoredProcedureQuery( - NameNode name, - Entity entity, - IEnumerable? rolesAllowedForRead = null) - { - List inputValues = new(); - List fieldDefinitionNodeDirectives = new(); - - if (entity.Parameters is not null) - { - foreach (string param in entity.Parameters.Keys) - { - inputValues.Add( - new( - location: null, - new(param), - new StringValueNode($"parameters for {name.Value} stored-procedure"), - new NamedTypeNode("String"), - defaultValue: new StringValueNode($"{entity.Parameters[param]}"), - new List()) - ); - } - } - - if (CreateAuthorizationDirectiveIfNecessary( - rolesAllowedForRead, - out DirectiveNode? authorizeDirective)) - { - fieldDefinitionNodeDirectives.Add(authorizeDirective!); - } - - return new( - location: null, - new NameNode(name.Value), - new StringValueNode($"Execute Stored-Procedure {name.Value} and get results from the database"), - inputValues, - new NonNullTypeNode(new ListTypeNode(new NonNullTypeNode(new NamedTypeNode(name)))), - fieldDefinitionNodeDirectives - ); - } + // public static FieldDefinitionNode GenerateStoredProcedureQuery( + // NameNode name, + // Entity entity, + // IEnumerable? rolesAllowedForRead = null) + // { + // List inputValues = new(); + // List fieldDefinitionNodeDirectives = new(); + + // if (entity.Parameters is not null) + // { + // foreach (string param in entity.Parameters.Keys) + // { + // inputValues.Add( + // new( + // location: null, + // new(param), + // new StringValueNode($"parameters for {name.Value} stored-procedure"), + // new NamedTypeNode("String"), + // defaultValue: new StringValueNode($"{entity.Parameters[param]}"), + // new List()) + // ); + // } + // } + + // if (CreateAuthorizationDirectiveIfNecessary( + // rolesAllowedForRead, + // out DirectiveNode? authorizeDirective)) + // { + // fieldDefinitionNodeDirectives.Add(authorizeDirective!); + // } + + // return new( + // location: null, + // new NameNode(name.Value), + // new StringValueNode($"Execute Stored-Procedure {name.Value} and get results from the database"), + // inputValues, + // new NonNullTypeNode(new ListTypeNode(new NonNullTypeNode(new NamedTypeNode(name)))), + // fieldDefinitionNodeDirectives + // ); + // } public static FieldDefinitionNode GenerateByPKQuery( ObjectTypeDefinitionNode objectTypeDefinitionNode, diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs index edd09de115..ed9be1eea1 100644 --- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -44,6 +44,8 @@ public static ObjectTypeDefinitionNode FromDatabaseObject( nameNode = new(entityName); Dictionary resultSet = ((StoredProcedureDefinition)sourceDefinition).ResultSet; + // When the result set is not defined, it could be a mutation operation with no returning columns + // Here we create a field called result which will be an empty array. if (resultSet.Count == 0) { FieldDefinitionNode field = new( diff --git a/src/Service/Resolvers/SqlMutationEngine.cs b/src/Service/Resolvers/SqlMutationEngine.cs index 6ae1f60d18..8a1fa1e2f0 100644 --- a/src/Service/Resolvers/SqlMutationEngine.cs +++ b/src/Service/Resolvers/SqlMutationEngine.cs @@ -21,7 +21,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; -using System.Text.Json.Nodes; namespace Azure.DataApiBuilder.Service.Resolvers { @@ -68,7 +67,6 @@ public SqlMutationEngine( /// JSON object result and its related pagination metadata public async Task> ExecuteAsync(IMiddlewareContext context, IDictionary parameters) { - if (context.Selection.Type.IsListType()) { throw new NotSupportedException("Returning list types from mutations not supported"); @@ -322,32 +320,6 @@ resultRowAndProperties.Item1 is null || return null; } - private async Task ExecuteAsync(SqlExecuteStructure structure) - { - string queryString = _queryBuilder.Build(structure); - _logger.LogInformation(queryString); - - // JsonArray resultArray = - // await _queryExecutor.ExecuteQueryAsync( - // queryString, - // structure.Parameters, - // _queryExecutor.GetJsonArrayAsync); - - JsonDocument jsonDocument = null; - - // If result set is non-empty, parse rows from json array into JsonDocument - // if (resultArray is not null && resultArray.Count > 0) - // { - // jsonDocument = JsonDocument.Parse(resultArray.ToJsonString()); - // } - // else - // { - // _logger.LogInformation("Did not return enough rows."); - // } - - return jsonDocument; - } - /// /// Helper function returns an OkObjectResult with provided arguments in a /// form that complies with vNext Api guidelines. diff --git a/src/Service/Resolvers/SqlPaginationUtil.cs b/src/Service/Resolvers/SqlPaginationUtil.cs index 6fa53f6f9b..d597d3883b 100644 --- a/src/Service/Resolvers/SqlPaginationUtil.cs +++ b/src/Service/Resolvers/SqlPaginationUtil.cs @@ -109,28 +109,6 @@ public static JsonDocument CreatePaginationConnectionFromJsonDocument(JsonDocume return result; } - /// - /// Takes the result from DB as JsonDocument and formats it in a way that can be filtered by column - /// name. It parses the Json document into a list of Dictionary with key as result_column_name - /// with it's corresponding value. - /// - public static List FormatStoredProcedureResultAsJsonList(JsonDocument jsonDocument) - { - if (jsonDocument is null) - { - return new List(); - } - - List resultJson = new(); - List> resultList = JsonSerializer.Deserialize>>(jsonDocument.RootElement.ToString())!; - foreach (Dictionary dict in resultList) - { - resultJson.Add(JsonDocument.Parse(JsonSerializer.Serialize(dict))); - } - - return resultJson; - } - /// /// Extracts the columns from the json element needed for pagination, represents them as a string in json format and base64 encodes. /// The JSON is encoded in base64 for opaqueness. The cursor should function as a token that the user copies and pastes diff --git a/src/Service/Resolvers/SqlQueryEngine.cs b/src/Service/Resolvers/SqlQueryEngine.cs index 488ff06089..538c422191 100644 --- a/src/Service/Resolvers/SqlQueryEngine.cs +++ b/src/Service/Resolvers/SqlQueryEngine.cs @@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; +using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLStoredProcedureBuilder; namespace Azure.DataApiBuilder.Service.Resolvers { @@ -90,7 +91,7 @@ public async Task, IMetadata>> ExecuteListAsync( SqlExecuteStructure sqlExecuteStructure = new(context.FieldSelection.Name.Value, _sqlMetadataProvider, parameters); return new Tuple, IMetadata>( - SqlPaginationUtil.FormatStoredProcedureResultAsJsonList(await ExecuteAsync(sqlExecuteStructure)), + FormatStoredProcedureResultAsJsonList(await ExecuteAsync(sqlExecuteStructure)), PaginationMetadata.MakeEmptyPaginationMetadata()); } else diff --git a/src/Service/Services/ResolverMiddleware.cs b/src/Service/Services/ResolverMiddleware.cs index 5ada5bc46b..8f0e11eb3e 100644 --- a/src/Service/Services/ResolverMiddleware.cs +++ b/src/Service/Services/ResolverMiddleware.cs @@ -62,8 +62,10 @@ public async Task InvokeAsync(IMiddlewareContext context) { IDictionary parameters = GetParametersFromContext(context); + // Only Stored-Procedure has ListType as returnType for Mutation if (context.Selection.Type.IsListType()) { + // Both Query and Mutation execute the same SQL statement for Stored Procedure. Tuple, IMetadata> result = await _queryEngine.ExecuteListAsync(context, parameters); context.Result = result.Item1; SetNewMetadata(context, result.Item2); From 270a599e7e6e25f7987c2cf0ee5b58a8e63a4578 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 16 Nov 2022 12:31:43 +0530 Subject: [PATCH 08/42] fix formatting --- .../GraphQLStoredProcedureBuilder.cs | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 src/Service.GraphQLBuilder/GraphQLStoredProcedureBuilder.cs diff --git a/src/Service.GraphQLBuilder/GraphQLStoredProcedureBuilder.cs b/src/Service.GraphQLBuilder/GraphQLStoredProcedureBuilder.cs new file mode 100644 index 0000000000..80d94373d6 --- /dev/null +++ b/src/Service.GraphQLBuilder/GraphQLStoredProcedureBuilder.cs @@ -0,0 +1,73 @@ +using System.Text.Json; +using Azure.DataApiBuilder.Config; +using HotChocolate.Language; +using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLUtils; + +namespace Azure.DataApiBuilder.Service.GraphQLBuilder +{ + public static class GraphQLStoredProcedureBuilder + { + public static FieldDefinitionNode GenerateStoredProcedureSchema( + NameNode name, + Entity entity, + IEnumerable? rolesAllowed = null) + { + List inputValues = new(); + List fieldDefinitionNodeDirectives = new(); + + if (entity.Parameters is not null) + { + foreach (string param in entity.Parameters.Keys) + { + inputValues.Add( + new( + location: null, + new(param), + new StringValueNode($"parameters for {name.Value} stored-procedure"), + new NamedTypeNode("String"), + defaultValue: new StringValueNode($"{entity.Parameters[param]}"), + new List()) + ); + } + } + + if (CreateAuthorizationDirectiveIfNecessary( + rolesAllowed, + out DirectiveNode? authorizeDirective)) + { + fieldDefinitionNodeDirectives.Add(authorizeDirective!); + } + + return new( + location: null, + new NameNode(name.Value), + new StringValueNode($"Execute Stored-Procedure {name.Value} and get results from the database"), + inputValues, + new NonNullTypeNode(new ListTypeNode(new NonNullTypeNode(new NamedTypeNode(name)))), + fieldDefinitionNodeDirectives + ); + } + + /// + /// Takes the result from DB as JsonDocument and formats it in a way that can be filtered by column + /// name. It parses the Json document into a list of Dictionary with key as result_column_name + /// with it's corresponding value. + /// + public static List FormatStoredProcedureResultAsJsonList(JsonDocument jsonDocument) + { + if (jsonDocument is null) + { + return new List(); + } + + List resultJson = new(); + List> resultList = JsonSerializer.Deserialize>>(jsonDocument.RootElement.ToString())!; + foreach (Dictionary dict in resultList) + { + resultJson.Add(JsonDocument.Parse(JsonSerializer.Serialize(dict))); + } + + return resultJson; + } + } +} From 15781bddfef1c11d9a0cf6cb86efbcbc59e24246 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 16 Nov 2022 14:44:00 +0530 Subject: [PATCH 09/42] removing redundant --- .../GraphQLStoredProcedureBuilder.cs | 5 ++ .../Mutations/MutationBuilder.cs | 52 +++---------------- .../Queries/QueryBuilder.cs | 44 ---------------- 3 files changed, 13 insertions(+), 88 deletions(-) diff --git a/src/Service.GraphQLBuilder/GraphQLStoredProcedureBuilder.cs b/src/Service.GraphQLBuilder/GraphQLStoredProcedureBuilder.cs index 80d94373d6..8a302500cd 100644 --- a/src/Service.GraphQLBuilder/GraphQLStoredProcedureBuilder.cs +++ b/src/Service.GraphQLBuilder/GraphQLStoredProcedureBuilder.cs @@ -7,6 +7,11 @@ namespace Azure.DataApiBuilder.Service.GraphQLBuilder { public static class GraphQLStoredProcedureBuilder { + /// + /// Helper function to create StoredProcedure Schema for GraphQL. + /// It uses the parameters to build the arguments and returns a list + /// of the StoredProcedure GraphQL object. + /// public static FieldDefinitionNode GenerateStoredProcedureSchema( NameNode name, Entity entity, diff --git a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs index d8916c3e6d..0e63da415c 100644 --- a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs @@ -67,6 +67,10 @@ public static DocumentNode Build( return new(definitionNodes); } + /// + /// Tries to fetch the Operation Type for Stored Procedure. + /// Stored Procedure currently support only 1 CRUD operation at a time. + /// private static Operation GetOperationTypeForStoredProcedure( string dbEntityName, Dictionary? entityPermissionsMap @@ -130,6 +134,10 @@ List mutationFields } } + /// + /// Helper method to add the new StoredProcedure in the mutation fields + /// of GraphQL Schema + /// private static void AddMutationsForStoredProcedure( string dbEntityName, Operation operation, @@ -146,50 +154,6 @@ List mutationFields } } - /// - /// Generates the StoredProcedure Query with input types, description, and return type. - /// - // public static FieldDefinitionNode GenerateStoredProcedureMutation( - // NameNode name, - // Entity entity, - // IEnumerable? rolesAllowedForMutation = null) - // { - // List inputValues = new(); - // List fieldDefinitionNodeDirectives = new(); - - // if (entity.Parameters is not null) - // { - // foreach (string param in entity.Parameters.Keys) - // { - // inputValues.Add( - // new( - // location: null, - // new(param), - // new StringValueNode($"parameters for {name.Value} stored-procedure"), - // new NamedTypeNode("String"), - // defaultValue: new StringValueNode($"{entity.Parameters[param]}"), - // new List()) - // ); - // } - // } - - // if (CreateAuthorizationDirectiveIfNecessary( - // rolesAllowedForMutation, - // out DirectiveNode? authorizeDirective)) - // { - // fieldDefinitionNodeDirectives.Add(authorizeDirective!); - // } - - // return new( - // location: null, - // new NameNode(name.Value), - // new StringValueNode($"Execute Stored-Procedure {name.Value}."), - // inputValues, - // new NonNullTypeNode(new ListTypeNode(new NonNullTypeNode(new NamedTypeNode(name)))), - // fieldDefinitionNodeDirectives - // ); - // } - public static Operation DetermineMutationOperationTypeBasedOnInputType(string inputTypeName) { return inputTypeName switch diff --git a/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs b/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs index fbb9943f50..2616195a14 100644 --- a/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs +++ b/src/Service.GraphQLBuilder/Queries/QueryBuilder.cs @@ -78,50 +78,6 @@ public static DocumentNode Build( return new(definitionNodes); } - /// - /// Generates the StoredProcedure Query with input types, description, and return type. - /// - // public static FieldDefinitionNode GenerateStoredProcedureQuery( - // NameNode name, - // Entity entity, - // IEnumerable? rolesAllowedForRead = null) - // { - // List inputValues = new(); - // List fieldDefinitionNodeDirectives = new(); - - // if (entity.Parameters is not null) - // { - // foreach (string param in entity.Parameters.Keys) - // { - // inputValues.Add( - // new( - // location: null, - // new(param), - // new StringValueNode($"parameters for {name.Value} stored-procedure"), - // new NamedTypeNode("String"), - // defaultValue: new StringValueNode($"{entity.Parameters[param]}"), - // new List()) - // ); - // } - // } - - // if (CreateAuthorizationDirectiveIfNecessary( - // rolesAllowedForRead, - // out DirectiveNode? authorizeDirective)) - // { - // fieldDefinitionNodeDirectives.Add(authorizeDirective!); - // } - - // return new( - // location: null, - // new NameNode(name.Value), - // new StringValueNode($"Execute Stored-Procedure {name.Value} and get results from the database"), - // inputValues, - // new NonNullTypeNode(new ListTypeNode(new NonNullTypeNode(new NamedTypeNode(name)))), - // fieldDefinitionNodeDirectives - // ); - // } - public static FieldDefinitionNode GenerateByPKQuery( ObjectTypeDefinitionNode objectTypeDefinitionNode, NameNode name, From eb34b0dadbeccea03cec05734c74df11608cbdf8 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Thu, 17 Nov 2022 18:15:33 +0530 Subject: [PATCH 10/42] adding tests --- .../GraphQLMutationTestBase.cs | 63 +++++++++++++++++++ .../MsSqlGraphQLMutationTests.cs | 33 ++++++++++ .../GraphQLQueryTests/GraphQLQueryTestBase.cs | 61 ++++++++++++++++++ .../MsSqlGraphQLQueryTests.cs | 21 +++++++ src/Service/MsSqlBooks.sql | 12 ++++ .../MetadataProviders/SqlMetadataProvider.cs | 2 +- 6 files changed, 191 insertions(+), 1 deletion(-) diff --git a/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs b/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs index 9f58d7a3c0..0022a87fa3 100644 --- a/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs +++ b/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs @@ -82,6 +82,69 @@ public async Task InsertMutationForConstantdefaultValue(string dbQuery) SqlTestHelper.PerformTestEqualJsonStrings(expected, actual.ToString()); } + /// + /// Do: Inserts new review with default content for a Review and return its id and content + /// Check: If book with the given id is present in the database then + /// the mutation query will return the review Id with the content of the review added + /// + public async Task TestStoredProcedureMutationForInsertionWithNoReturns(string dbQuery) + { + string graphQLMutationName = "InsertBook"; + string graphQLMutation = @" + mutation { + InsertBook(title: ""Random Book"", publisher_id: ""1234"" ) { + result + } + } + "; + + string currentDbResponse = await GetDatabaseResultAsync(dbQuery); + JsonDocument currentResult = JsonDocument.Parse(currentDbResponse); + Assert.AreEqual(currentResult.RootElement.GetProperty("count").GetInt64(), 0); + JsonElement graphQLResponse = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true); + + // Stored Procedure didn't return anything + SqlTestHelper.PerformTestEqualJsonStrings("[]", graphQLResponse.ToString()); + + // check to verify new element is inserted + string updatedDbResponse = await GetDatabaseResultAsync(dbQuery); + JsonDocument updatedResult = JsonDocument.Parse(updatedDbResponse); + Assert.AreEqual(updatedResult.RootElement.GetProperty("count").GetInt64(), 1); + } + + /// + /// Do: Inserts new review with default content for a Review and return its id and content + /// Check: If book with the given id is present in the database then + /// the mutation query will return the review Id with the content of the review added + /// + public async Task TestStoredProcedureMutationForInsertionWithReturns(string dbQueryForResult, string dbQueryToVerifyInsertion) + { + string graphQLMutationName = "InsertAndDisplayAllBooks"; + string graphQLMutation = @" + mutation { + InsertAndDisplayAllBooks(title: ""Theory Of DAB"", publisher_id: ""1234"" ) { + id, + title, + publisher_id + } + } + "; + + string currentDbResponse = await GetDatabaseResultAsync(dbQueryToVerifyInsertion); + JsonDocument currentResult = JsonDocument.Parse(currentDbResponse); + Assert.AreEqual(currentResult.RootElement.GetProperty("count").GetInt64(), 0); + JsonElement graphQLResponse = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true); + string dbResponse = await GetDatabaseResultAsync(dbQueryForResult); + + // Stored Procedure didn't return anything + SqlTestHelper.PerformTestEqualJsonStrings(dbResponse, graphQLResponse.ToString()); + + // check to verify new element is inserted + string updatedDbResponse = await GetDatabaseResultAsync(dbQueryToVerifyInsertion); + JsonDocument updatedResult = JsonDocument.Parse(updatedDbResponse); + Assert.AreEqual(updatedResult.RootElement.GetProperty("count").GetInt64(), 1); + } + /// /// Do: Inserts new stock price with default current timestamp as the value of /// 'instant' column and returns the inserted row. diff --git a/src/Service.Tests/SqlTests/GraphQLMutationTests/MsSqlGraphQLMutationTests.cs b/src/Service.Tests/SqlTests/GraphQLMutationTests/MsSqlGraphQLMutationTests.cs index 12d3901afc..9b1d88457e 100644 --- a/src/Service.Tests/SqlTests/GraphQLMutationTests/MsSqlGraphQLMutationTests.cs +++ b/src/Service.Tests/SqlTests/GraphQLMutationTests/MsSqlGraphQLMutationTests.cs @@ -103,6 +103,39 @@ ORDER BY [id] asc await InsertMutationForConstantdefaultValue(msSqlQuery); } + [TestMethod] + public async Task TestStoredProcedureMutationForInsertionWithNoReturns() + { + string msSqlQuery = @" + SELECT COUNT(*) AS [count] + FROM [books] AS [table0] + WHERE [table0].[title] = 'Random Book' + AND [table0].[publisher_id] = 1234 + FOR JSON PATH, + INCLUDE_NULL_VALUES, + WITHOUT_ARRAY_WRAPPER + "; + + await TestStoredProcedureMutationForInsertionWithNoReturns(msSqlQuery); + } + + [TestMethod] + public async Task TestStoredProcedureMutationForInsertionWithReturns() + { + string dbQueryForResult = $"SELECT id, title, publisher_id FROM books ORDER BY id asc FOR JSON PATH, INCLUDE_NULL_VALUES"; + string dbQueryToVerifyInsertion = @" + SELECT COUNT(*) AS [count] + FROM [books] AS [table0] + WHERE [table0].[title] = 'Theory of DAB' + AND [table0].[publisher_id] = 1234 + FOR JSON PATH, + INCLUDE_NULL_VALUES, + WITHOUT_ARRAY_WRAPPER + "; + + await TestStoredProcedureMutationForInsertionWithReturns(dbQueryForResult, dbQueryToVerifyInsertion); + } + /// [TestMethod] public async Task InsertMutationForVariableNotNullDefault() diff --git a/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs b/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs index e900b4e25c..9485f27fb0 100644 --- a/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs +++ b/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs @@ -930,6 +930,67 @@ public virtual async Task TestQueryOnBasicView(string dbQuery) SqlTestHelper.PerformTestEqualJsonStrings(expected, actual.GetProperty("items").ToString()); } + /// + /// Query a simple view (contains columns from one table) + /// + [TestMethod] + public virtual async Task TestStoredProcedureQueryForGettingSingleRow(string dbQuery) + { + string graphQLQueryName = "GetBook"; + string graphQLQuery = @"{ + GetBook(id: ""3"") { + id + title + publisher_id + } + }"; + + JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLQuery, graphQLQueryName, isAuthenticated: false); + string expected = await GetDatabaseResultAsync(dbQuery, false); + + SqlTestHelper.PerformTestEqualJsonStrings(expected, actual.ToString()); + } + + /// + /// Query a simple view (contains columns from one table) + /// + [TestMethod] + public virtual async Task TestStoredProcedureQueryForGettingMultipleRows(string dbQuery) + { + string graphQLQueryName = "GetBooks"; + string graphQLQuery = @"{ + GetBooks { + id + title + publisher_id + } + }"; + + JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLQuery, graphQLQueryName, isAuthenticated: false); + string expected = await GetDatabaseResultAsync(dbQuery, false); + + SqlTestHelper.PerformTestEqualJsonStrings(expected, actual.ToString()); + } + + /// + /// Query a simple view (contains columns from one table) + /// + [TestMethod] + public virtual async Task TestStoredProcedureQueryForGettingTotalNumberOfRows(string dbQuery) + { + string graphQLQueryName = "CountBooks"; + string graphQLQuery = @"{ + CountBooks { + total_books + } + }"; + + JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLQuery, graphQLQueryName, isAuthenticated: false); + string expected = await GetDatabaseResultAsync(dbQuery, false); + + SqlTestHelper.PerformTestEqualJsonStrings(expected, actual.ToString()); + } + /// /// Query a composite view (contains columns from multiple tables) /// diff --git a/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLQueryTests.cs b/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLQueryTests.cs index 6d3f593dc6..510e10779d 100644 --- a/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLQueryTests.cs +++ b/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLQueryTests.cs @@ -253,6 +253,27 @@ public async Task TestQueryOnBasicView() await base.TestQueryOnBasicView(msSqlQuery); } + [TestMethod] + public async Task TestStoredProcedureQueryForGettingSingleRow() + { + string msSqlQuery = $"EXEC dbo.get_book_by_id @id=\"3\""; + await base.TestStoredProcedureQueryForGettingSingleRow(msSqlQuery); + } + + [TestMethod] + public async Task TestStoredProcedureQueryForGettingMultipleRows() + { + string msSqlQuery = $"EXEC dbo.get_books"; + await base.TestStoredProcedureQueryForGettingMultipleRows(msSqlQuery); + } + + [TestMethod] + public async Task TestStoredProcedureQueryForGettingTotalNumberOfRows() + { + string msSqlQuery = $"EXEC dbo.count_books"; + await base.TestStoredProcedureQueryForGettingTotalNumberOfRows(msSqlQuery); + } + [TestMethod] public async Task TestQueryOnCompositeView() { diff --git a/src/Service/MsSqlBooks.sql b/src/Service/MsSqlBooks.sql index 6269092052..290af50961 100644 --- a/src/Service/MsSqlBooks.sql +++ b/src/Service/MsSqlBooks.sql @@ -6,6 +6,9 @@ DROP VIEW IF EXISTS books_publishers_view_composite; DROP VIEW IF EXISTS books_publishers_view_composite_insertable; DROP PROCEDURE IF EXISTS get_books; DROP PROCEDURE IF EXISTS get_book_by_id; +DROP PROCEDURE IF EXISTS insert_book; +DROP PROCEDURE IF EXISTS count_books; +DROP PROCEDURE IF EXISTS insert_and_display_all_books; DROP TABLE IF EXISTS book_author_link; DROP TABLE IF EXISTS reviews; DROP TABLE IF EXISTS authors; @@ -299,3 +302,12 @@ EXEC('CREATE PROCEDURE get_book_by_id @id int AS WHERE id = @id'); EXEC('CREATE PROCEDURE get_books AS SELECT * FROM dbo.books'); +EXEC('CREATE PROCEDURE insert_book @title varchar(max), @publisher_id int AS + INSERT INTO dbo.books(title, publisher_id) VALUES (@title, @publisher_id)'); +EXEC('CREATE PROCEDURE count_books AS + SELECT COUNT(*) AS total_books FROM dbo.books'); +EXEC('CREATE PROCEDURE insert_and_display_all_books @title varchar(max), @publisher_id int AS + BEGIN + INSERT INTO dbo.books(title, publisher_id) VALUES (@title, @publisher_id) + SELECT * FROM dbo.books + END'); diff --git a/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs index 038dd65367..04905085e3 100644 --- a/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs @@ -763,7 +763,7 @@ private async Task PopulateResultSetDefinitionsForStoredProcedureAsync( Type resultFieldType = SqlToCLRType(element.GetProperty("system_type_name").ToString()); // Store the dictionary containing result set field with it's type - storedProcedureDefinition.ResultSet.Add(resultFieldName, resultFieldType); + storedProcedureDefinition.ResultSet.TryAdd(resultFieldName, resultFieldType); } } From 74decf441c8dff81ac1b3da56813dcb32ffbc83e Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Fri, 18 Nov 2022 10:45:51 +0530 Subject: [PATCH 11/42] fix argument type for graphQL --- .../GraphQLStoredProcedureBuilder.cs | 6 ++++-- src/Service.GraphQLBuilder/GraphQLUtils.cs | 21 +++++++++++++++++++ .../GraphQLMutationTestBase.cs | 4 ++-- .../GraphQLQueryTests/GraphQLQueryTestBase.cs | 18 +++++++++++++++- .../MsSqlGraphQLQueryTests.cs | 2 +- 5 files changed, 45 insertions(+), 6 deletions(-) diff --git a/src/Service.GraphQLBuilder/GraphQLStoredProcedureBuilder.cs b/src/Service.GraphQLBuilder/GraphQLStoredProcedureBuilder.cs index 8a302500cd..9bdbfaf2ea 100644 --- a/src/Service.GraphQLBuilder/GraphQLStoredProcedureBuilder.cs +++ b/src/Service.GraphQLBuilder/GraphQLStoredProcedureBuilder.cs @@ -2,6 +2,7 @@ using Azure.DataApiBuilder.Config; using HotChocolate.Language; using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLUtils; +using static Azure.DataApiBuilder.Service.GraphQLBuilder.Sql.SchemaConverter; namespace Azure.DataApiBuilder.Service.GraphQLBuilder { @@ -24,13 +25,14 @@ public static FieldDefinitionNode GenerateStoredProcedureSchema( { foreach (string param in entity.Parameters.Keys) { + Tuple defaultGraphQLValue = GetGraphQLTypeAndNodeTypeFromStringValue(entity.Parameters[param].ToString()!); inputValues.Add( new( location: null, new(param), new StringValueNode($"parameters for {name.Value} stored-procedure"), - new NamedTypeNode("String"), - defaultValue: new StringValueNode($"{entity.Parameters[param]}"), + new NamedTypeNode(defaultGraphQLValue.Item1), + defaultValue: defaultGraphQLValue.Item2, new List()) ); } diff --git a/src/Service.GraphQLBuilder/GraphQLUtils.cs b/src/Service.GraphQLBuilder/GraphQLUtils.cs index b42c19d8d3..4afebc300d 100644 --- a/src/Service.GraphQLBuilder/GraphQLUtils.cs +++ b/src/Service.GraphQLBuilder/GraphQLUtils.cs @@ -204,5 +204,26 @@ public static ObjectType UnderlyingGraphQLEntityType(IType type) return UnderlyingGraphQLEntityType(type.InnerType()); } + + /// + /// Parse a given string value to supported GraphQL Type and GraphQLValueNode + /// + public static Tuple GetGraphQLTypeAndNodeTypeFromStringValue(string stringValue) + { + if (int.TryParse(stringValue, out int integerValue)) + { + return new (LONG_TYPE, new IntValueNode(integerValue)); + } + else if (double.TryParse(stringValue, out double floatingValue)) + { + return new (FLOAT_TYPE, new FloatValueNode(floatingValue)); + } + else if (Boolean.TryParse(stringValue, out bool booleanValue)) + { + return new (BOOLEAN_TYPE, new BooleanValueNode(booleanValue)); + } + + return new (STRING_TYPE, new StringValueNode(stringValue)); + } } } diff --git a/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs b/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs index 0022a87fa3..5f4211bb44 100644 --- a/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs +++ b/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs @@ -92,7 +92,7 @@ public async Task TestStoredProcedureMutationForInsertionWithNoReturns(string db string graphQLMutationName = "InsertBook"; string graphQLMutation = @" mutation { - InsertBook(title: ""Random Book"", publisher_id: ""1234"" ) { + InsertBook(title: ""Random Book"", publisher_id: 1234 ) { result } } @@ -122,7 +122,7 @@ public async Task TestStoredProcedureMutationForInsertionWithReturns(string dbQu string graphQLMutationName = "InsertAndDisplayAllBooks"; string graphQLMutation = @" mutation { - InsertAndDisplayAllBooks(title: ""Theory Of DAB"", publisher_id: ""1234"" ) { + InsertAndDisplayAllBooks(title: ""Theory Of DAB"", publisher_id: 1234 ) { id, title, publisher_id diff --git a/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs b/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs index 9485f27fb0..9c603e38d5 100644 --- a/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs +++ b/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs @@ -938,7 +938,7 @@ public virtual async Task TestStoredProcedureQueryForGettingSingleRow(string dbQ { string graphQLQueryName = "GetBook"; string graphQLQuery = @"{ - GetBook(id: ""3"") { + GetBook(id: 3) { id title publisher_id @@ -1034,6 +1034,22 @@ public virtual async Task TestInvalidFirstParamQuery() SqlTestHelper.TestForErrorInGraphQLResponse(result.ToString(), statusCode: $"{DataApiBuilderException.SubStatusCodes.BadRequest}"); } + [TestMethod] + public virtual async Task TestStoredProcedureQueryWithInvalidArgumentType() + { + string graphQLQueryName = "GetBook"; + string graphQLQuery = @"{ + GetBook(id: ""3"") { + id + title + publisher_id + } + }"; + + JsonElement result = await ExecuteGraphQLRequestAsync(graphQLQuery, graphQLQueryName, isAuthenticated: false); + SqlTestHelper.TestForErrorInGraphQLResponse(result.ToString(), message: "The specified argument value does not match the argument type."); + } + [TestMethod] public virtual async Task TestInvalidFilterParamQuery() { diff --git a/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLQueryTests.cs b/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLQueryTests.cs index 510e10779d..a0474c2965 100644 --- a/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLQueryTests.cs +++ b/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLQueryTests.cs @@ -256,7 +256,7 @@ public async Task TestQueryOnBasicView() [TestMethod] public async Task TestStoredProcedureQueryForGettingSingleRow() { - string msSqlQuery = $"EXEC dbo.get_book_by_id @id=\"3\""; + string msSqlQuery = $"EXEC dbo.get_book_by_id @id=3"; await base.TestStoredProcedureQueryForGettingSingleRow(msSqlQuery); } From 615bb746670cde133ab6d05a8a80eefd23e139db Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Sun, 20 Nov 2022 20:13:09 +0530 Subject: [PATCH 12/42] fixing field level authorization --- src/Config/DatabaseObject.cs | 9 +- .../Mutations/MutationBuilder.cs | 33 +++- .../Sql/SchemaConverter.cs | 156 ++++++++---------- .../GraphQLQueryTests/GraphQLQueryTestBase.cs | 6 +- .../Authorization/AuthorizationResolver.cs | 4 + .../Configurations/RuntimeConfigValidator.cs | 7 + src/Service/Services/GraphQLSchemaCreator.cs | 24 +-- .../MetadataProviders/SqlMetadataProvider.cs | 11 +- 8 files changed, 140 insertions(+), 110 deletions(-) diff --git a/src/Config/DatabaseObject.cs b/src/Config/DatabaseObject.cs index 69e41e3061..bddf41c997 100644 --- a/src/Config/DatabaseObject.cs +++ b/src/Config/DatabaseObject.cs @@ -89,7 +89,7 @@ public class StoredProcedureDefinition : SourceDefinition /// The list of fields with their type in the Stored Procedure result /// Key: ResultSet field name, Value: ResultSet field Type /// - public Dictionary ResultSet { get; set; } = new(); + // public Dictionary ResultSet { get; set; } = new(); } public class ParameterDefinition @@ -182,6 +182,13 @@ public class ColumnDefinition public bool IsAutoGenerated { get; set; } public bool IsNullable { get; set; } public object? DefaultValue { get; set; } + + public ColumnDefinition() {} + + public ColumnDefinition(Type systemType) + { + this.SystemType = systemType; + } } public class ForeignKeyDefinition diff --git a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs index 0e63da415c..8e58d94917 100644 --- a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs @@ -3,6 +3,8 @@ using HotChocolate.Language; using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLNaming; using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLUtils; +using Azure.DataApiBuilder.Service.Exceptions; +using System.Net; namespace Azure.DataApiBuilder.Service.GraphQLBuilder.Mutations { @@ -76,13 +78,38 @@ private static Operation GetOperationTypeForStoredProcedure( Dictionary? entityPermissionsMap ) { - if (entityPermissionsMap![dbEntityName].OperationToRolesMap.Count == 1) + List operations = entityPermissionsMap![dbEntityName].OperationToRolesMap.Keys.ToList(); + operations.Remove(Operation.Read); + + // Only one of the mutation operation(CUD) is allowed at once + if (operations.Count == 0) + { + // If it only contained Read Operation + return Operation.Read; + } + else if (operations.Count == 1) { - return entityPermissionsMap[dbEntityName].OperationToRolesMap.First().Key; + if (entityPermissionsMap.TryGetValue(dbEntityName, out EntityMetadata entityMetadata)) + { + return entityMetadata!.OperationToRolesMap.First().Key; + } + else + { + throw new DataApiBuilderException( + message: $"Failed to obtain permissions for entity:{dbEntityName}", + statusCode: HttpStatusCode.PreconditionFailed, + subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization + ); + } + } else { - throw new Exception("Stored Procedure cannot have more than one operation."); + throw new DataApiBuilderException( + message: $"StoredProcedure can't have more than one CRUD operation.", + statusCode: HttpStatusCode.PreconditionFailed, + subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization + ); } } diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs index ed9be1eea1..a533a01d16 100644 --- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -42,108 +42,90 @@ public static ObjectTypeDefinitionNode FromDatabaseObject( if (databaseObject.SourceType is SourceType.StoredProcedure) { nameNode = new(entityName); - Dictionary resultSet = ((StoredProcedureDefinition)sourceDefinition).ResultSet; - - // When the result set is not defined, it could be a mutation operation with no returning columns - // Here we create a field called result which will be an empty array. - if (resultSet.Count == 0) - { - FieldDefinitionNode field = new( - location: null, - new("result"), - description: null, - new List(), - new StringType().ToTypeNode(), - new List()); - - fields.Add("result", field); - } - else - { - foreach ((string fieldName, Type fieldType) in resultSet) - { - NamedTypeNode nodeFieldType = new(GetGraphQLTypeForColumnType(fieldType)); - FieldDefinitionNode field = new( - location: null, - new(fieldName), - description: null, - new List(), - nodeFieldType, - new List()); - - fields.Add(fieldName, field); - } - } } else { nameNode = new(value: GetDefinedSingularName(entityName, configEntity)); + } + + // When the result set is not defined, it could be a mutation operation with no returning columns + // Here we create a field called result which will be an empty array. + if (databaseObject.SourceType is SourceType.StoredProcedure && ((StoredProcedureDefinition)sourceDefinition).Columns.Count == 0) + { + FieldDefinitionNode field = new( + location: null, + new("result"), + description: new StringValueNode("Contains output of stored-procedure execution"), + new List(), + new StringType().ToTypeNode(), + new List()); - foreach ((string columnName, ColumnDefinition column) in sourceDefinition.Columns) - { - List directives = new(); + fields.Add("result", field); + } - if (sourceDefinition.PrimaryKey.Contains(columnName)) - { - directives.Add(new DirectiveNode(PrimaryKeyDirectiveType.DirectiveName, new ArgumentNode("databaseType", column.SystemType.Name))); - } + foreach ((string columnName, ColumnDefinition column) in sourceDefinition.Columns) + { + List directives = new(); - if (column.IsAutoGenerated) - { - directives.Add(new DirectiveNode(AutoGeneratedDirectiveType.DirectiveName)); - } + if (databaseObject.SourceType is not SourceType.StoredProcedure && sourceDefinition.PrimaryKey.Contains(columnName)) + { + directives.Add(new DirectiveNode(PrimaryKeyDirectiveType.DirectiveName, new ArgumentNode("databaseType", column.SystemType.Name))); + } + + if (databaseObject.SourceType is not SourceType.StoredProcedure && column.IsAutoGenerated) + { + directives.Add(new DirectiveNode(AutoGeneratedDirectiveType.DirectiveName)); + } - if (column.DefaultValue is not null) + if (databaseObject.SourceType is not SourceType.StoredProcedure && column.DefaultValue is not null) + { + IValueNode arg = column.DefaultValue switch { - IValueNode arg = column.DefaultValue switch - { - byte value => new ObjectValueNode(new ObjectFieldNode(BYTE_TYPE, new IntValueNode(value))), - short value => new ObjectValueNode(new ObjectFieldNode(SHORT_TYPE, new IntValueNode(value))), - int value => new ObjectValueNode(new ObjectFieldNode(INT_TYPE, value)), - long value => new ObjectValueNode(new ObjectFieldNode(LONG_TYPE, new IntValueNode(value))), - string value => new ObjectValueNode(new ObjectFieldNode(STRING_TYPE, value)), - bool value => new ObjectValueNode(new ObjectFieldNode(BOOLEAN_TYPE, value)), - float value => new ObjectValueNode(new ObjectFieldNode(SINGLE_TYPE, new SingleType().ParseValue(value))), - double value => new ObjectValueNode(new ObjectFieldNode(FLOAT_TYPE, value)), - decimal value => new ObjectValueNode(new ObjectFieldNode(DECIMAL_TYPE, new FloatValueNode(value))), - DateTime value => new ObjectValueNode(new ObjectFieldNode(DATETIME_TYPE, new DateTimeType().ParseResult(value))), - DateTimeOffset value => new ObjectValueNode(new ObjectFieldNode(DATETIME_TYPE, new DateTimeType().ParseValue(value))), - byte[] value => new ObjectValueNode(new ObjectFieldNode(BYTEARRAY_TYPE, new ByteArrayType().ParseValue(value))), - _ => throw new DataApiBuilderException( - message: $"The type {column.DefaultValue.GetType()} is not supported as a GraphQL default value", - statusCode: HttpStatusCode.InternalServerError, - subStatusCode: DataApiBuilderException.SubStatusCodes.GraphQLMapping) - }; + byte value => new ObjectValueNode(new ObjectFieldNode(BYTE_TYPE, new IntValueNode(value))), + short value => new ObjectValueNode(new ObjectFieldNode(SHORT_TYPE, new IntValueNode(value))), + int value => new ObjectValueNode(new ObjectFieldNode(INT_TYPE, value)), + long value => new ObjectValueNode(new ObjectFieldNode(LONG_TYPE, new IntValueNode(value))), + string value => new ObjectValueNode(new ObjectFieldNode(STRING_TYPE, value)), + bool value => new ObjectValueNode(new ObjectFieldNode(BOOLEAN_TYPE, value)), + float value => new ObjectValueNode(new ObjectFieldNode(SINGLE_TYPE, new SingleType().ParseValue(value))), + double value => new ObjectValueNode(new ObjectFieldNode(FLOAT_TYPE, value)), + decimal value => new ObjectValueNode(new ObjectFieldNode(DECIMAL_TYPE, new FloatValueNode(value))), + DateTime value => new ObjectValueNode(new ObjectFieldNode(DATETIME_TYPE, new DateTimeType().ParseResult(value))), + DateTimeOffset value => new ObjectValueNode(new ObjectFieldNode(DATETIME_TYPE, new DateTimeType().ParseValue(value))), + byte[] value => new ObjectValueNode(new ObjectFieldNode(BYTEARRAY_TYPE, new ByteArrayType().ParseValue(value))), + _ => throw new DataApiBuilderException( + message: $"The type {column.DefaultValue.GetType()} is not supported as a GraphQL default value", + statusCode: HttpStatusCode.InternalServerError, + subStatusCode: DataApiBuilderException.SubStatusCodes.GraphQLMapping) + }; - directives.Add(new DirectiveNode(DefaultValueDirectiveType.DirectiveName, new ArgumentNode("value", arg))); - } + directives.Add(new DirectiveNode(DefaultValueDirectiveType.DirectiveName, new ArgumentNode("value", arg))); + } - // If no roles are allowed for the field, we should not include it in the schema. - // Consequently, the field is only added to schema if this conditional evaluates to TRUE. - if (rolesAllowedForFields.TryGetValue(key: columnName, out IEnumerable? roles)) + // If no roles are allowed for the field, we should not include it in the schema. + // Consequently, the field is only added to schema if this conditional evaluates to TRUE. + if (rolesAllowedForFields.TryGetValue(key: columnName, out IEnumerable? roles)) + { + // Roles will not be null here if TryGetValue evaluates to true, so here we check if there are any roles to process. + if (roles.Count() > 0) { - // Roles will not be null here if TryGetValue evaluates to true, so here we check if there are any roles to process. - if (roles.Count() > 0) + if (GraphQLUtils.CreateAuthorizationDirectiveIfNecessary( + roles, + out DirectiveNode? authZDirective)) { + directives.Add(authZDirective!); + } - if (GraphQLUtils.CreateAuthorizationDirectiveIfNecessary( - roles, - out DirectiveNode? authZDirective)) - { - directives.Add(authZDirective!); - } - - NamedTypeNode fieldType = new(GetGraphQLTypeForColumnType(column.SystemType)); - FieldDefinitionNode field = new( - location: null, - new(columnName), - description: null, - new List(), - column.IsNullable ? fieldType : new NonNullTypeNode(fieldType), - directives); + NamedTypeNode fieldType = new(GetGraphQLTypeForColumnType(column.SystemType)); + FieldDefinitionNode field = new( + location: null, + new(columnName), + description: null, + new List(), + column.IsNullable ? fieldType : new NonNullTypeNode(fieldType), + directives); - fields.Add(columnName, field); - } + fields.Add(columnName, field); } } } diff --git a/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs b/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs index 9c603e38d5..c231e23d48 100644 --- a/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs +++ b/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs @@ -931,7 +931,7 @@ public virtual async Task TestQueryOnBasicView(string dbQuery) } /// - /// Query a simple view (contains columns from one table) + /// Simple Stored Procedure to check SELECT query returning single row /// [TestMethod] public virtual async Task TestStoredProcedureQueryForGettingSingleRow(string dbQuery) @@ -952,7 +952,7 @@ public virtual async Task TestStoredProcedureQueryForGettingSingleRow(string dbQ } /// - /// Query a simple view (contains columns from one table) + /// Simple Stored Procedure to check SELECT query returning multiple rows /// [TestMethod] public virtual async Task TestStoredProcedureQueryForGettingMultipleRows(string dbQuery) @@ -973,7 +973,7 @@ public virtual async Task TestStoredProcedureQueryForGettingMultipleRows(string } /// - /// Query a simple view (contains columns from one table) + /// Simple Stored Procedure to check COUNT operation /// [TestMethod] public virtual async Task TestStoredProcedureQueryForGettingTotalNumberOfRows(string dbQuery) diff --git a/src/Service/Authorization/AuthorizationResolver.cs b/src/Service/Authorization/AuthorizationResolver.cs index 46c736c2bf..da8d88039d 100644 --- a/src/Service/Authorization/AuthorizationResolver.cs +++ b/src/Service/Authorization/AuthorizationResolver.cs @@ -292,6 +292,10 @@ public void SetEntityPermissionMap(RuntimeConfig? runtimeConfig) } } + if (entity.ObjectType is SourceType.StoredProcedure){ + Console.Write("asdasd"); + } + // Populate allowed exposed columns for each entity/role/operation combination during startup, // so that it doesn't need to be evaluated per request. PopulateAllowedExposedColumns(operationToColumn.AllowedExposedColumns, entityName, allowedColumns); diff --git a/src/Service/Configurations/RuntimeConfigValidator.cs b/src/Service/Configurations/RuntimeConfigValidator.cs index 5e2372c635..2e5c28b6bd 100644 --- a/src/Service/Configurations/RuntimeConfigValidator.cs +++ b/src/Service/Configurations/RuntimeConfigValidator.cs @@ -66,6 +66,8 @@ public void ValidateConfig() ValidateAuthenticationConfig(); + ValidateStoredProcedureInConfig(runtimeConfig); + // Running these graphQL validations only in development mode to ensure // fast startup of engine in production mode. if (runtimeConfig.GraphQLGlobalSettings.Enabled @@ -98,6 +100,11 @@ public static void ValidateDataSourceInConfig( ValidateDatabaseType(runtimeConfig, fileSystem, logger); } + public static void ValidateStoredProcedureInConfig(RuntimeConfig runtimeConfig) + { + // SetEntityPermissionMap(runtimeConfig); + } + /// /// Throws exception if database type is incorrectly configured /// in the config. diff --git a/src/Service/Services/GraphQLSchemaCreator.cs b/src/Service/Services/GraphQLSchemaCreator.cs index 8b407578ab..bda67e4d82 100644 --- a/src/Service/Services/GraphQLSchemaCreator.cs +++ b/src/Service/Services/GraphQLSchemaCreator.cs @@ -146,21 +146,23 @@ DatabaseType.postgresql or IEnumerable rolesAllowedForEntity = _authorizationResolver.GetRolesForEntity(entityName); Dictionary> rolesAllowedForFields = new(); SourceDefinition sourceDefinition = _sqlMetadataProvider.GetSourceDefinition(entityName); - if (databaseObject.SourceType is not SourceType.StoredProcedure) + if (databaseObject.SourceType is SourceType.StoredProcedure) { - foreach (string column in sourceDefinition.Columns.Keys) + Console.Write("asd"); + } + foreach (string column in sourceDefinition.Columns.Keys) + { + IEnumerable roles = _authorizationResolver.GetRolesForField(entityName, field: column, operation: Operation.Read); + if (!rolesAllowedForFields.TryAdd(key: column, value: roles)) { - IEnumerable roles = _authorizationResolver.GetRolesForField(entityName, field: column, operation: Operation.Read); - if (!rolesAllowedForFields.TryAdd(key: column, value: roles)) - { - throw new DataApiBuilderException( - message: "Column already processed for building ObjectTypeDefinition authorization definition.", - statusCode: System.Net.HttpStatusCode.InternalServerError, - subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization - ); - } + throw new DataApiBuilderException( + message: "Column already processed for building ObjectTypeDefinition authorization definition.", + statusCode: System.Net.HttpStatusCode.InternalServerError, + subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization + ); } } + // } // The roles allowed for Fields are the roles allowed to READ the fields, so any role that has a read definition for the field. // Only add objectTypeDefinition for GraphQL if it has a role definition defined for access. diff --git a/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs index 04905085e3..138b012f27 100644 --- a/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs @@ -762,8 +762,9 @@ private async Task PopulateResultSetDefinitionsForStoredProcedureAsync( string resultFieldName = element.GetProperty("result_field_name").ToString(); Type resultFieldType = SqlToCLRType(element.GetProperty("system_type_name").ToString()); - // Store the dictionary containing result set field with it's type - storedProcedureDefinition.ResultSet.TryAdd(resultFieldName, resultFieldType); + // Store the dictionary containing result set field with it's type as Columns + // storedProcedureDefinition.ResultSet.TryAdd(resultFieldName, resultFieldType); + storedProcedureDefinition.Columns.TryAdd(resultFieldName, new(resultFieldType)); } } @@ -803,8 +804,8 @@ private void GenerateExposedToBackingColumnMapsForEntities() { // Ensure we don't attempt for stored procedures, which have no // SourceDefinition, Columns, Keys, etc. - if (_entities[entityName].ObjectType is not SourceType.StoredProcedure) - { + // if (_entities[entityName].ObjectType is not SourceType.StoredProcedure) + // { Dictionary? mapping = GetMappingForEntity(entityName); EntityBackingColumnsToExposedNames[entityName] = mapping is not null ? mapping : new(); EntityExposedNamesToBackingColumnNames[entityName] = EntityBackingColumnsToExposedNames[entityName].ToDictionary(x => x.Value, x => x.Key); @@ -817,7 +818,7 @@ private void GenerateExposedToBackingColumnMapsForEntities() EntityExposedNamesToBackingColumnNames[entityName].Add(column, column); } } - } + // } } } From 61eff13151f464b39c17acf51c52fdb5699ec6b7 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Sun, 20 Nov 2022 21:16:28 +0530 Subject: [PATCH 13/42] fixing build --- src/Config/Entity.cs | 4 ++-- src/Service/Configurations/RuntimeConfigValidator.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Config/Entity.cs b/src/Config/Entity.cs index 74bfb62252..f7685c603c 100644 --- a/src/Config/Entity.cs +++ b/src/Config/Entity.cs @@ -140,14 +140,14 @@ public void TryPopulateSourceFields() throw new JsonException(message: "Must specify entity source."); } - JsonElement sourceJson = (JsonElement)Source; + JsonElement sourceJson = JsonSerializer.SerializeToElement(Source); // In the case of a simple, string source, we assume the source type is a table; parameters and key fields left null // Note: engine supports views backing entities labeled as Tables, as long as their primary key can be inferred if (sourceJson.ValueKind is JsonValueKind.String) { ObjectType = SourceType.Table; - SourceName = JsonSerializer.Deserialize((JsonElement)Source)!; + SourceName = JsonSerializer.Deserialize(sourceJson)!; Parameters = null; KeyFields = null; } diff --git a/src/Service/Configurations/RuntimeConfigValidator.cs b/src/Service/Configurations/RuntimeConfigValidator.cs index 2e5c28b6bd..0fd31586ac 100644 --- a/src/Service/Configurations/RuntimeConfigValidator.cs +++ b/src/Service/Configurations/RuntimeConfigValidator.cs @@ -219,7 +219,7 @@ public static void ValidateEntityNamesInConfig(Dictionary entity foreach (string entityName in entityCollection.Keys) { Entity entity = entityCollection[entityName]; - entity.TryPopulateSourceFields(); + // entity.TryPopulateSourceFields(); if (entity.GraphQL is null) { From a36f246e98c5925a13958ac7e695cf1bfbdd145d Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Mon, 21 Nov 2022 09:19:04 +0530 Subject: [PATCH 14/42] added new tests --- .../GraphQLMutationTestBase.cs | 48 +++++++++++++++++++ .../MsSqlGraphQLMutationTests.cs | 35 ++++++++++++++ src/Service/MsSqlBooks.sql | 13 ++++- 3 files changed, 95 insertions(+), 1 deletion(-) diff --git a/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs b/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs index 5f4211bb44..37918f0c9e 100644 --- a/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs +++ b/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs @@ -1,3 +1,4 @@ +using System; using System.Text.Json; using System.Threading.Tasks; using Azure.DataApiBuilder.Service.Exceptions; @@ -145,6 +146,53 @@ public async Task TestStoredProcedureMutationForInsertionWithReturns(string dbQu Assert.AreEqual(updatedResult.RootElement.GetProperty("count").GetInt64(), 1); } + public async Task TestStoredProcedureMutationForDeletion(string dbQueryToVerifyDeletion) + { + string graphQLMutationName = "DeleteBook"; + string graphQLMutation = @" + mutation { + DeleteBook(id: 13) { + result + } + } + "; + + string currentDbResponse = await GetDatabaseResultAsync(dbQueryToVerifyDeletion); + JsonDocument currentResult = JsonDocument.Parse(currentDbResponse); + Assert.AreEqual(currentResult.RootElement.GetProperty("count").GetInt64(), 1); + JsonElement graphQLResponse = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true); + + // Stored Procedure didn't return anything + SqlTestHelper.PerformTestEqualJsonStrings("[]", graphQLResponse.ToString()); + + // check to verify new element is inserted + string updatedDbResponse = await GetDatabaseResultAsync(dbQueryToVerifyDeletion); + JsonDocument updatedResult = JsonDocument.Parse(updatedDbResponse); + Assert.AreEqual(updatedResult.RootElement.GetProperty("count").GetInt64(), 0); + } + + public async Task TestStoredProcedureMutationForUpdate(string dbQuery) + { + string graphQLMutationName = "UpdateBookTitle"; + string graphQLMutation = @" + mutation { + UpdateBookTitle(id: 14, title: 'Before Midnight') { + id + title + publisher_id + } + } + "; + + string beforeUpdate = await GetDatabaseResultAsync(dbQuery); + Console.Write(beforeUpdate); + JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true); + string afterUpdate = await GetDatabaseResultAsync(dbQuery); + + SqlTestHelper.PerformTestEqualJsonStrings(afterUpdate, actual.ToString()); + } + + /// /// Do: Inserts new stock price with default current timestamp as the value of /// 'instant' column and returns the inserted row. diff --git a/src/Service.Tests/SqlTests/GraphQLMutationTests/MsSqlGraphQLMutationTests.cs b/src/Service.Tests/SqlTests/GraphQLMutationTests/MsSqlGraphQLMutationTests.cs index 9b1d88457e..05b9d317c6 100644 --- a/src/Service.Tests/SqlTests/GraphQLMutationTests/MsSqlGraphQLMutationTests.cs +++ b/src/Service.Tests/SqlTests/GraphQLMutationTests/MsSqlGraphQLMutationTests.cs @@ -136,6 +136,41 @@ FROM [books] AS [table0] await TestStoredProcedureMutationForInsertionWithReturns(dbQueryForResult, dbQueryToVerifyInsertion); } + [TestMethod] + public async Task TestStoredProcedureMutationForDeletion() + { + string dbQueryToVerifyDeletion = @" + SELECT COUNT(*) AS [count] + FROM [books] AS [table0] + WHERE + [table0].[id] = 13 + AND [table0].[title] = 'Before Sunrise' + AND [table0].[publisher_id] = 1234 + FOR JSON PATH, + INCLUDE_NULL_VALUES, + WITHOUT_ARRAY_WRAPPER + "; + + await TestStoredProcedureMutationForDeletion(dbQueryToVerifyDeletion); + } + + [TestMethod] + public async Task TestStoredProcedureMutationForUpdate() + { + string dbQuery = @" + SELECT id, title, publisher_id + FROM [books] AS [table0] + WHERE + [table0].[id] = 14 + AND [table0].[publisher_id] = 1234 + FOR JSON PATH, + INCLUDE_NULL_VALUES, + WITHOUT_ARRAY_WRAPPER + "; + + await TestStoredProcedureMutationForUpdate(dbQuery); + } + /// [TestMethod] public async Task InsertMutationForVariableNotNullDefault() diff --git a/src/Service/MsSqlBooks.sql b/src/Service/MsSqlBooks.sql index 290af50961..5b74c7d929 100644 --- a/src/Service/MsSqlBooks.sql +++ b/src/Service/MsSqlBooks.sql @@ -9,6 +9,8 @@ DROP PROCEDURE IF EXISTS get_book_by_id; DROP PROCEDURE IF EXISTS insert_book; DROP PROCEDURE IF EXISTS count_books; DROP PROCEDURE IF EXISTS insert_and_display_all_books; +DROP PROCEDURE IF EXISTS delete_book; +DROP PROCEDURE IF EXISTS update_book_title; DROP TABLE IF EXISTS book_author_link; DROP TABLE IF EXISTS reviews; DROP TABLE IF EXISTS authors; @@ -243,7 +245,9 @@ VALUES (1, 'Awesome book', 1234), (9, 'Policy-Test-01', 1940), (10, 'Policy-Test-02', 1940), (11, 'Policy-Test-04', 1941), -(12, 'Time to Eat 2', 1941); +(12, 'Time to Eat 2', 1941), +(13, 'Before Sunrise', 1234), +(14, 'Before Sunset', 1234); SET IDENTITY_INSERT books OFF SET IDENTITY_INSERT book_website_placements ON @@ -311,3 +315,10 @@ EXEC('CREATE PROCEDURE insert_and_display_all_books @title varchar(max), @publis INSERT INTO dbo.books(title, publisher_id) VALUES (@title, @publisher_id) SELECT * FROM dbo.books END'); +EXEC('CREATE PROCEDURE delete_book @id int AS + DELETE FROM dbo.books WHERE id=@id'); +EXEC('CREATE PROCEDURE update_book_title @id int, @title varchar(max) AS + BEGIN + UPDATE dbo.books SET title = @title WHERE id = @id + SELECT * from dbo.books WHERE id = @id + END'); From c1516780a4bd99b931c9a77ff7ce9d69ebf989ab Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Mon, 21 Nov 2022 11:58:48 +0530 Subject: [PATCH 15/42] fix commands file --- ConfigGenerators/MsSqlCommands.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ConfigGenerators/MsSqlCommands.txt b/ConfigGenerators/MsSqlCommands.txt index bc93f84c36..04bdfc5eb3 100644 --- a/ConfigGenerators/MsSqlCommands.txt +++ b/ConfigGenerators/MsSqlCommands.txt @@ -115,5 +115,5 @@ update GetBooks --config "dab-config.MsSql.json" --permissions "authenticated:*" update InsertBook --config "dab-config.MsSql.json" --permissions "authenticated:create" update CountBooks --config "dab-config.MsSql.json" --permissions "authenticated:read" update InsertAndDisplayAllBooks --config "dab-config.MsSql.json" --permissions "authenticated:create,read" -update DeleteBook --config "dab-config.MsSql.json"--permissions "authenticated:delete" +update DeleteBook --config "dab-config.MsSql.json" --permissions "authenticated:delete" update UpdateBookTitle --config "dab-config.MsSql.json" --permissions "authenticated:update,read" From f841fdc177c910fb9b823d1f98f4f4d8fd0485b7 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Mon, 21 Nov 2022 14:20:02 +0530 Subject: [PATCH 16/42] fixing tests --- .../SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs b/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs index c231e23d48..202d85ad34 100644 --- a/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs +++ b/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs @@ -934,7 +934,7 @@ public virtual async Task TestQueryOnBasicView(string dbQuery) /// Simple Stored Procedure to check SELECT query returning single row /// [TestMethod] - public virtual async Task TestStoredProcedureQueryForGettingSingleRow(string dbQuery) + public async Task TestStoredProcedureQueryForGettingSingleRow(string dbQuery) { string graphQLQueryName = "GetBook"; string graphQLQuery = @"{ @@ -955,7 +955,7 @@ public virtual async Task TestStoredProcedureQueryForGettingSingleRow(string dbQ /// Simple Stored Procedure to check SELECT query returning multiple rows /// [TestMethod] - public virtual async Task TestStoredProcedureQueryForGettingMultipleRows(string dbQuery) + public async Task TestStoredProcedureQueryForGettingMultipleRows(string dbQuery) { string graphQLQueryName = "GetBooks"; string graphQLQuery = @"{ @@ -976,7 +976,7 @@ public virtual async Task TestStoredProcedureQueryForGettingMultipleRows(string /// Simple Stored Procedure to check COUNT operation /// [TestMethod] - public virtual async Task TestStoredProcedureQueryForGettingTotalNumberOfRows(string dbQuery) + public async Task TestStoredProcedureQueryForGettingTotalNumberOfRows(string dbQuery) { string graphQLQueryName = "CountBooks"; string graphQLQuery = @"{ @@ -1035,7 +1035,7 @@ public virtual async Task TestInvalidFirstParamQuery() } [TestMethod] - public virtual async Task TestStoredProcedureQueryWithInvalidArgumentType() + public async Task TestStoredProcedureQueryWithInvalidArgumentType() { string graphQLQueryName = "GetBook"; string graphQLQuery = @"{ From d5016daf7ae10ec7b874bed1f798f8497ba35924 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Mon, 21 Nov 2022 15:25:36 +0530 Subject: [PATCH 17/42] fixing tests --- ConfigGenerators/MsSqlCommands.txt | 4 +-- .../Mutations/MutationBuilder.cs | 29 +++++++++++++------ .../GraphQLMutationTestBase.cs | 8 +++-- .../GraphQLQueryTests/GraphQLQueryTestBase.cs | 21 ++++++++++---- .../MsSqlGraphQLQueryTests.cs | 6 ++-- .../Configurations/RuntimeConfigValidator.cs | 11 +++---- src/Service/MySqlBooks.sql | 17 ++++++++++- src/Service/PostgreSqlBooks.sql | 17 ++++++++++- 8 files changed, 84 insertions(+), 29 deletions(-) diff --git a/ConfigGenerators/MsSqlCommands.txt b/ConfigGenerators/MsSqlCommands.txt index 04bdfc5eb3..ca904046c8 100644 --- a/ConfigGenerators/MsSqlCommands.txt +++ b/ConfigGenerators/MsSqlCommands.txt @@ -110,8 +110,8 @@ update Journal --config "dab-config.MsSql.json" --permissions "policy_tester_upd update Journal --config "dab-config.MsSql.json" --permissions "policy_tester_update_noread:delete" --fields.include "*" --policy-database "@item.id eq 1" update Journal --config "dab-config.MsSql.json" --permissions "authorizationHandlerTester:read" update ArtOfWar --config "dab-config.MsSql.json" --permissions "authenticated:*" --map "DetailAssessmentAndPlanning:始計,WagingWar:作戰,StrategicAttack:謀攻,NoteNum:┬─┬ノ( º _ ºノ)" -update GetBook --config "dab-config.MsSql.json" --permissions "authenticated:*" -update GetBooks --config "dab-config.MsSql.json" --permissions "authenticated:*" +update GetBook --config "dab-config.MsSql.json" --permissions "authenticated:read" +update GetBooks --config "dab-config.MsSql.json" --permissions "authenticated:read" update InsertBook --config "dab-config.MsSql.json" --permissions "authenticated:create" update CountBooks --config "dab-config.MsSql.json" --permissions "authenticated:read" update InsertAndDisplayAllBooks --config "dab-config.MsSql.json" --permissions "authenticated:create,read" diff --git a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs index f8b717f969..8e58d94917 100644 --- a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs @@ -1,10 +1,10 @@ -using System.Net; using Azure.DataApiBuilder.Auth; using Azure.DataApiBuilder.Config; -using Azure.DataApiBuilder.Service.Exceptions; using HotChocolate.Language; using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLNaming; using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLUtils; +using Azure.DataApiBuilder.Service.Exceptions; +using System.Net; namespace Azure.DataApiBuilder.Service.GraphQLBuilder.Mutations { @@ -87,18 +87,29 @@ private static Operation GetOperationTypeForStoredProcedure( // If it only contained Read Operation return Operation.Read; } - else if (entityPermissionsMap.TryGetValue(dbEntityName, out EntityMetadata entityMetadata)) + else if (operations.Count == 1) { - // If it contains any other operation apart from Read, then it's a Mutation - return entityMetadata!.OperationToRolesMap.First().Key; + if (entityPermissionsMap.TryGetValue(dbEntityName, out EntityMetadata entityMetadata)) + { + return entityMetadata!.OperationToRolesMap.First().Key; + } + else + { + throw new DataApiBuilderException( + message: $"Failed to obtain permissions for entity:{dbEntityName}", + statusCode: HttpStatusCode.PreconditionFailed, + subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization + ); + } + } else { throw new DataApiBuilderException( - message: $"Failed to obtain permissions for entity:{dbEntityName}", - statusCode: HttpStatusCode.PreconditionFailed, - subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization - ); + message: $"StoredProcedure can't have more than one CRUD operation.", + statusCode: HttpStatusCode.PreconditionFailed, + subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization + ); } } diff --git a/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs b/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs index 0f2d97d027..67a0708f28 100644 --- a/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs +++ b/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Text.Json; using System.Threading.Tasks; using Azure.DataApiBuilder.Service.Exceptions; @@ -176,7 +177,7 @@ public async Task TestStoredProcedureMutationForUpdate(string dbQuery) string graphQLMutationName = "UpdateBookTitle"; string graphQLMutation = @" mutation { - UpdateBookTitle(id: 14, title: 'Before Midnight') { + UpdateBookTitle(id: 14, title: ""Before Midnight"") { id title publisher_id @@ -188,8 +189,9 @@ public async Task TestStoredProcedureMutationForUpdate(string dbQuery) Console.Write(beforeUpdate); JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true); string afterUpdate = await GetDatabaseResultAsync(dbQuery); - - SqlTestHelper.PerformTestEqualJsonStrings(afterUpdate, actual.ToString()); + List jsonList = JsonSerializer.Deserialize>(actual.ToString()); + Assert.AreEqual(1,jsonList.Count); + SqlTestHelper.PerformTestEqualJsonStrings(afterUpdate, JsonSerializer.Serialize(jsonList[0])); } /// diff --git a/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs b/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs index 202d85ad34..241b02ae67 100644 --- a/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs +++ b/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs @@ -63,7 +63,7 @@ public virtual async Task MultipleResultJoinQuery() { string graphQLQueryName = "books"; string graphQLQuery = @"{ - books(first: 100) { + books(first: 12) { items { id title @@ -562,6 +562,9 @@ public virtual async Task TestFirstParamForListQueries() }, { ""title"": ""Also Awesome book"" + }, + { + ""title"": ""Before Sunrise"" } ] } @@ -604,6 +607,12 @@ public virtual async Task TestFilterParamForListQueries() ""items"": [ { ""id"": 1 + }, + { + ""id"": 13 + }, + { + ""id"": 14 } ] } @@ -616,6 +625,12 @@ public virtual async Task TestFilterParamForListQueries() ""items"": [ { ""id"": 1 + }, + { + ""id"": 13 + }, + { + ""id"": 14 } ] } @@ -933,7 +948,6 @@ public virtual async Task TestQueryOnBasicView(string dbQuery) /// /// Simple Stored Procedure to check SELECT query returning single row /// - [TestMethod] public async Task TestStoredProcedureQueryForGettingSingleRow(string dbQuery) { string graphQLQueryName = "GetBook"; @@ -954,7 +968,6 @@ public async Task TestStoredProcedureQueryForGettingSingleRow(string dbQuery) /// /// Simple Stored Procedure to check SELECT query returning multiple rows /// - [TestMethod] public async Task TestStoredProcedureQueryForGettingMultipleRows(string dbQuery) { string graphQLQueryName = "GetBooks"; @@ -975,7 +988,6 @@ public async Task TestStoredProcedureQueryForGettingMultipleRows(string dbQuery) /// /// Simple Stored Procedure to check COUNT operation /// - [TestMethod] public async Task TestStoredProcedureQueryForGettingTotalNumberOfRows(string dbQuery) { string graphQLQueryName = "CountBooks"; @@ -1034,7 +1046,6 @@ public virtual async Task TestInvalidFirstParamQuery() SqlTestHelper.TestForErrorInGraphQLResponse(result.ToString(), statusCode: $"{DataApiBuilderException.SubStatusCodes.BadRequest}"); } - [TestMethod] public async Task TestStoredProcedureQueryWithInvalidArgumentType() { string graphQLQueryName = "GetBook"; diff --git a/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLQueryTests.cs b/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLQueryTests.cs index a0474c2965..61a94588a3 100644 --- a/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLQueryTests.cs +++ b/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLQueryTests.cs @@ -257,21 +257,21 @@ public async Task TestQueryOnBasicView() public async Task TestStoredProcedureQueryForGettingSingleRow() { string msSqlQuery = $"EXEC dbo.get_book_by_id @id=3"; - await base.TestStoredProcedureQueryForGettingSingleRow(msSqlQuery); + await TestStoredProcedureQueryForGettingSingleRow(msSqlQuery); } [TestMethod] public async Task TestStoredProcedureQueryForGettingMultipleRows() { string msSqlQuery = $"EXEC dbo.get_books"; - await base.TestStoredProcedureQueryForGettingMultipleRows(msSqlQuery); + await TestStoredProcedureQueryForGettingMultipleRows(msSqlQuery); } [TestMethod] public async Task TestStoredProcedureQueryForGettingTotalNumberOfRows() { string msSqlQuery = $"EXEC dbo.count_books"; - await base.TestStoredProcedureQueryForGettingTotalNumberOfRows(msSqlQuery); + await TestStoredProcedureQueryForGettingTotalNumberOfRows(msSqlQuery); } [TestMethod] diff --git a/src/Service/Configurations/RuntimeConfigValidator.cs b/src/Service/Configurations/RuntimeConfigValidator.cs index 0fd31586ac..04563daaf6 100644 --- a/src/Service/Configurations/RuntimeConfigValidator.cs +++ b/src/Service/Configurations/RuntimeConfigValidator.cs @@ -66,7 +66,7 @@ public void ValidateConfig() ValidateAuthenticationConfig(); - ValidateStoredProcedureInConfig(runtimeConfig); + // ValidateStoredProcedureInConfig(runtimeConfig); // Running these graphQL validations only in development mode to ensure // fast startup of engine in production mode. @@ -100,10 +100,11 @@ public static void ValidateDataSourceInConfig( ValidateDatabaseType(runtimeConfig, fileSystem, logger); } - public static void ValidateStoredProcedureInConfig(RuntimeConfig runtimeConfig) - { - // SetEntityPermissionMap(runtimeConfig); - } + // public static void ValidateStoredProcedureInConfig(ISqlMetadataProvider sqlMetadataProvider) + // { + // AuthorizationResolver x = new AuthorizationResolver(_runtimeConfigProvider, sqlMetadataProvider, _logger); + // x.SetEntityPermissionMap(_runtimeConfigProvider.GetRuntimeConfiguration()); + // } /// /// Throws exception if database type is incorrectly configured diff --git a/src/Service/MySqlBooks.sql b/src/Service/MySqlBooks.sql index 4d606fc9cf..7611062087 100644 --- a/src/Service/MySqlBooks.sql +++ b/src/Service/MySqlBooks.sql @@ -212,7 +212,22 @@ ON DELETE CASCADE; INSERT INTO publishers(id, name) VALUES (1234, 'Big Company'), (2345, 'Small Town Publisher'), (2323, 'TBD Publishing One'), (2324, 'TBD Publishing Two Ltd'), (1940, 'Policy Publisher 01'), (1941, 'Policy Publisher 02'); INSERT INTO authors(id, name, birthdate) VALUES (123, 'Jelte', '2001-01-01'), (124, 'Aniruddh', '2002-02-02'), (125, 'Aniruddh', '2001-01-01'), (126, 'Aaron', '2001-01-01'); -INSERT INTO books(id, title, publisher_id) VALUES (1, 'Awesome book', 1234), (2, 'Also Awesome book', 1234), (3, 'Great wall of china explained', 2345), (4, 'US history in a nutshell', 2345), (5, 'Chernobyl Diaries', 2323), (6, 'The Palace Door', 2324), (7, 'The Groovy Bar', 2324), (8, 'Time to Eat', 2324), (9, 'Policy-Test-01', 1940), (10, 'Policy-Test-02', 1940), (11, 'Policy-Test-04', 1941), (12, 'Time to Eat 2', 1941); +INSERT INTO books(id, title, publisher_id) + VALUES + (1, 'Awesome book', 1234), + (2, 'Also Awesome book', 1234), + (3, 'Great wall of china explained', 2345), + (4, 'US history in a nutshell', 2345), + (5, 'Chernobyl Diaries', 2323), + (6, 'The Palace Door', 2324), + (7, 'The Groovy Bar', 2324), + (8, 'Time to Eat', 2324), + (9, 'Policy-Test-01', 1940), + (10, 'Policy-Test-02', 1940), + (11, 'Policy-Test-04', 1941), + (12, 'Time to Eat 2', 1941), + (13, 'Before Sunrise', 1234), + (14, 'Before Sunset', 1234); INSERT INTO book_website_placements(book_id, price) VALUES (1, 100), (2, 50), (3, 23), (5, 33); INSERT INTO website_users(id, username) VALUES (1, 'George'), (2, NULL), (3, ''), (4, 'book_lover_95'), (5, 'null'); INSERT INTO book_author_link(book_id, author_id) VALUES (1, 123), (2, 124), (3, 123), (3, 124), (4, 123), (4, 124); diff --git a/src/Service/PostgreSqlBooks.sql b/src/Service/PostgreSqlBooks.sql index 041033033a..8c0330e887 100644 --- a/src/Service/PostgreSqlBooks.sql +++ b/src/Service/PostgreSqlBooks.sql @@ -216,7 +216,22 @@ ON DELETE CASCADE; INSERT INTO publishers(id, name) VALUES (1234, 'Big Company'), (2345, 'Small Town Publisher'), (2323, 'TBD Publishing One'), (2324, 'TBD Publishing Two Ltd'), (1940, 'Policy Publisher 01'), (1941, 'Policy Publisher 02'); INSERT INTO authors(id, name, birthdate) VALUES (123, 'Jelte', '2001-01-01'), (124, 'Aniruddh', '2002-02-02'), (125, 'Aniruddh', '2001-01-01'), (126, 'Aaron', '2001-01-01'); -INSERT INTO books(id, title, publisher_id) VALUES (1, 'Awesome book', 1234), (2, 'Also Awesome book', 1234), (3, 'Great wall of china explained', 2345), (4, 'US history in a nutshell', 2345), (5, 'Chernobyl Diaries', 2323), (6, 'The Palace Door', 2324), (7, 'The Groovy Bar', 2324), (8, 'Time to Eat', 2324), (9, 'Policy-Test-01', 1940), (10, 'Policy-Test-02', 1940), (11, 'Policy-Test-04', 1941), (12, 'Time to Eat 2', 1941); +INSERT INTO books(id, title, publisher_id) + VALUES + (1, 'Awesome book', 1234), + (2, 'Also Awesome book', 1234), + (3, 'Great wall of china explained', 2345), + (4, 'US history in a nutshell', 2345), + (5, 'Chernobyl Diaries', 2323), + (6, 'The Palace Door', 2324), + (7, 'The Groovy Bar', 2324), + (8, 'Time to Eat', 2324), + (9, 'Policy-Test-01', 1940), + (10, 'Policy-Test-02', 1940), + (11, 'Policy-Test-04', 1941), + (12, 'Time to Eat 2', 1941), + (13, 'Before Sunrise', 1234), + (14, 'Before Sunset', 1234); INSERT INTO book_website_placements(book_id, price) VALUES (1, 100), (2, 50), (3, 23), (5, 33); INSERT INTO website_users(id, username) VALUES (1, 'George'), (2, NULL), (3, ''), (4, 'book_lover_95'), (5, 'null'); INSERT INTO book_author_link(book_id, author_id) VALUES (1, 123), (2, 124), (3, 123), (3, 124), (4, 123), (4, 124); From 1b45a78b568e0651e16d0c0e9633ba680297dbb3 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Mon, 21 Nov 2022 15:31:17 +0530 Subject: [PATCH 18/42] using GetDefinedSingularName for stored-procedure graphQL --- src/Service.GraphQLBuilder/Sql/SchemaConverter.cs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs index ef5161fa01..ad7bac725e 100644 --- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -37,16 +37,7 @@ public static ObjectTypeDefinitionNode FromDatabaseObject( Dictionary fields = new(); List objectTypeDirectives = new(); SourceDefinition sourceDefinition = SourceDefinition.GetSourceDefinitionForDatabaseObject(databaseObject); - NameNode nameNode; - - if (databaseObject.SourceType is SourceType.StoredProcedure) - { - nameNode = new(entityName); - } - else - { - nameNode = new(value: GetDefinedSingularName(entityName, configEntity)); - } + NameNode nameNode = new(value: GetDefinedSingularName(entityName, configEntity)); // When the result set is not defined, it could be a mutation operation with no returning columns // Here we create a field called result which will be an empty array. From bf62575c1f192cf3ea489fc9ea45b9ae5d1589d1 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Mon, 21 Nov 2022 15:36:29 +0530 Subject: [PATCH 19/42] fix formatting --- src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs | 6 +++--- .../GraphQLMutationTests/GraphQLMutationTestBase.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs index 8e58d94917..734f9637d6 100644 --- a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs @@ -1,10 +1,10 @@ +using System.Net; using Azure.DataApiBuilder.Auth; using Azure.DataApiBuilder.Config; +using Azure.DataApiBuilder.Service.Exceptions; using HotChocolate.Language; using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLNaming; using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLUtils; -using Azure.DataApiBuilder.Service.Exceptions; -using System.Net; namespace Azure.DataApiBuilder.Service.GraphQLBuilder.Mutations { @@ -101,7 +101,7 @@ private static Operation GetOperationTypeForStoredProcedure( subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization ); } - + } else { diff --git a/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs b/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs index 67a0708f28..4bfee0ffa9 100644 --- a/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs +++ b/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs @@ -190,7 +190,7 @@ public async Task TestStoredProcedureMutationForUpdate(string dbQuery) JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true); string afterUpdate = await GetDatabaseResultAsync(dbQuery); List jsonList = JsonSerializer.Deserialize>(actual.ToString()); - Assert.AreEqual(1,jsonList.Count); + Assert.AreEqual(1, jsonList.Count); SqlTestHelper.PerformTestEqualJsonStrings(afterUpdate, JsonSerializer.Serialize(jsonList[0])); } From 2017c74e996e8beaf91a96f574550ef10067b866 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Mon, 21 Nov 2022 17:23:13 +0530 Subject: [PATCH 20/42] fixing tests --- ConfigGenerators/MsSqlCommands.txt | 2 +- .../GraphQLPaginationTestBase.cs | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/ConfigGenerators/MsSqlCommands.txt b/ConfigGenerators/MsSqlCommands.txt index ca904046c8..e7bbfb0fc3 100644 --- a/ConfigGenerators/MsSqlCommands.txt +++ b/ConfigGenerators/MsSqlCommands.txt @@ -24,7 +24,7 @@ add Journal --config "dab-config.MsSql.json" --source "journals" --rest true --g add ArtOfWar --config "dab-config.MsSql.json" --source "aow" --rest true --permissions "anonymous:*" add series --config "dab-config.MsSql.json" --source "series" --permissions "anonymous:*" add GetBooks --config "dab-config.MsSql.json" --source "get_books" --source.type "stored-procedure" --permissions "anonymous:read" --rest true --graphql true -add GetBook --config "dab-config.MsSql.json" --source "get_book_by_id" --source.type "stored-procedure" --source.params "id:1" --permissions "anonymous:read" --rest true --graphql true +add GetBook --config "dab-config.MsSql.json" --source "get_book_by_id" --source.type "stored-procedure" --permissions "anonymous:read" --rest true --graphql true add InsertBook --config "dab-config.MsSql.json" --source "insert_book" --source.type "stored-procedure" --source.params "title:randomX,publisher_id:1234" --permissions "anonymous:create" --rest true --graphql true add CountBooks --config "dab-config.MsSql.json" --source "count_books" --source.type "stored-procedure" --permissions "anonymous:read" --rest true --graphql true add InsertAndDisplayAllBooks --config "dab-config.MsSql.json" --source "insert_and_display_all_books" --source.type "stored-procedure" --source.params "title:randomX,publisher_id:1234" --permissions "anonymous:create,read" --rest true --graphql true diff --git a/src/Service.Tests/SqlTests/GraphQLPaginationTests/GraphQLPaginationTestBase.cs b/src/Service.Tests/SqlTests/GraphQLPaginationTests/GraphQLPaginationTestBase.cs index 8de1e74779..3eca4e944e 100644 --- a/src/Service.Tests/SqlTests/GraphQLPaginationTests/GraphQLPaginationTestBase.cs +++ b/src/Service.Tests/SqlTests/GraphQLPaginationTests/GraphQLPaginationTestBase.cs @@ -129,6 +129,14 @@ public async Task RequestNoParamFullConnection() { ""id"": 12, ""title"": ""Time to Eat 2"" + }, + { + ""id"": 13, + ""title"": ""Before Sunrise"" + }, + { + ""id"": 14, + ""title"": ""Before Sunset"" } ], ""endCursor"": """ + SqlPaginationUtil.Base64Encode("[{\"Value\":12,\"Direction\":0,\"TableSchema\":\"\",\"TableName\":\"\",\"ColumnName\":\"id\"}]") + @""", @@ -287,6 +295,10 @@ public async Task RequestNestedPaginationQueries() { ""id"": 2, ""title"": ""Also Awesome book"" + }, + { + ""id"": 13, + ""title"": ""Before Sunrise"" } ], ""endCursor"": """ + SqlPaginationUtil.Base64Encode("[{\"Value\":2,\"Direction\":0,\"TableSchema\":\"\",\"TableName\":\"\",\"ColumnName\":\"id\"}]") + @""", @@ -356,8 +368,8 @@ public async Task RequestPaginatedQueryFromMutationResult() ""title"": ""Also Awesome book"" }, { - ""id"": 5001, - ""title"": ""Books, Pages, and Pagination. The Book"" + ""id"": 13, + ""title"": ""Before Sunrise"" } ], ""endCursor"": """ + SqlPaginationUtil.Base64Encode("[{\"Value\":5001,\"Direction\":0,\"TableSchema\":\"\",\"TableName\":\"\",\"ColumnName\":\"id\"}]") + @""", From 07e47bc173c362327483ead2d706c9901ccaa430 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Mon, 21 Nov 2022 17:39:44 +0530 Subject: [PATCH 21/42] fixing tests --- .../GraphQLPaginationTests/GraphQLPaginationTestBase.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Service.Tests/SqlTests/GraphQLPaginationTests/GraphQLPaginationTestBase.cs b/src/Service.Tests/SqlTests/GraphQLPaginationTests/GraphQLPaginationTestBase.cs index 3eca4e944e..51bb499097 100644 --- a/src/Service.Tests/SqlTests/GraphQLPaginationTests/GraphQLPaginationTestBase.cs +++ b/src/Service.Tests/SqlTests/GraphQLPaginationTests/GraphQLPaginationTestBase.cs @@ -139,7 +139,7 @@ public async Task RequestNoParamFullConnection() ""title"": ""Before Sunset"" } ], - ""endCursor"": """ + SqlPaginationUtil.Base64Encode("[{\"Value\":12,\"Direction\":0,\"TableSchema\":\"\",\"TableName\":\"\",\"ColumnName\":\"id\"}]") + @""", + ""endCursor"": """ + SqlPaginationUtil.Base64Encode("[{\"Value\":14,\"Direction\":0,\"TableSchema\":\"\",\"TableName\":\"\",\"ColumnName\":\"id\"}]") + @""", ""hasNextPage"": false }"; @@ -301,7 +301,7 @@ public async Task RequestNestedPaginationQueries() ""title"": ""Before Sunrise"" } ], - ""endCursor"": """ + SqlPaginationUtil.Base64Encode("[{\"Value\":2,\"Direction\":0,\"TableSchema\":\"\",\"TableName\":\"\",\"ColumnName\":\"id\"}]") + @""", + ""endCursor"": """ + SqlPaginationUtil.Base64Encode("[{\"Value\":13,\"Direction\":0,\"TableSchema\":\"\",\"TableName\":\"\",\"ColumnName\":\"id\"}]") + @""", ""hasNextPage"": false } } @@ -372,7 +372,7 @@ public async Task RequestPaginatedQueryFromMutationResult() ""title"": ""Before Sunrise"" } ], - ""endCursor"": """ + SqlPaginationUtil.Base64Encode("[{\"Value\":5001,\"Direction\":0,\"TableSchema\":\"\",\"TableName\":\"\",\"ColumnName\":\"id\"}]") + @""", + ""endCursor"": """ + SqlPaginationUtil.Base64Encode("[{\"Value\":14,\"Direction\":0,\"TableSchema\":\"\",\"TableName\":\"\",\"ColumnName\":\"id\"}]") + @""", ""hasNextPage"": false } } From ae84c772e91b7efd4ef2e42dc52187d3b88bbfb7 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Mon, 21 Nov 2022 17:58:30 +0530 Subject: [PATCH 22/42] fixing pagination tests --- .../GraphQLPaginationTests/GraphQLPaginationTestBase.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Service.Tests/SqlTests/GraphQLPaginationTests/GraphQLPaginationTestBase.cs b/src/Service.Tests/SqlTests/GraphQLPaginationTests/GraphQLPaginationTestBase.cs index 51bb499097..8889f5e715 100644 --- a/src/Service.Tests/SqlTests/GraphQLPaginationTests/GraphQLPaginationTestBase.cs +++ b/src/Service.Tests/SqlTests/GraphQLPaginationTests/GraphQLPaginationTestBase.cs @@ -302,7 +302,7 @@ public async Task RequestNestedPaginationQueries() } ], ""endCursor"": """ + SqlPaginationUtil.Base64Encode("[{\"Value\":13,\"Direction\":0,\"TableSchema\":\"\",\"TableName\":\"\",\"ColumnName\":\"id\"}]") + @""", - ""hasNextPage"": false + ""hasNextPage"": true } } }, @@ -373,7 +373,7 @@ public async Task RequestPaginatedQueryFromMutationResult() } ], ""endCursor"": """ + SqlPaginationUtil.Base64Encode("[{\"Value\":14,\"Direction\":0,\"TableSchema\":\"\",\"TableName\":\"\",\"ColumnName\":\"id\"}]") + @""", - ""hasNextPage"": false + ""hasNextPage"": true } } }"; From 117178605f99bb4426b2506bd42f0c1a612c3ad0 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 22 Nov 2022 08:18:01 +0530 Subject: [PATCH 23/42] fixing test --- .../GraphQLPaginationTests/GraphQLPaginationTestBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service.Tests/SqlTests/GraphQLPaginationTests/GraphQLPaginationTestBase.cs b/src/Service.Tests/SqlTests/GraphQLPaginationTests/GraphQLPaginationTestBase.cs index 8889f5e715..902adb1a0b 100644 --- a/src/Service.Tests/SqlTests/GraphQLPaginationTests/GraphQLPaginationTestBase.cs +++ b/src/Service.Tests/SqlTests/GraphQLPaginationTests/GraphQLPaginationTestBase.cs @@ -372,7 +372,7 @@ public async Task RequestPaginatedQueryFromMutationResult() ""title"": ""Before Sunrise"" } ], - ""endCursor"": """ + SqlPaginationUtil.Base64Encode("[{\"Value\":14,\"Direction\":0,\"TableSchema\":\"\",\"TableName\":\"\",\"ColumnName\":\"id\"}]") + @""", + ""endCursor"": """ + SqlPaginationUtil.Base64Encode("[{\"Value\":13,\"Direction\":0,\"TableSchema\":\"\",\"TableName\":\"\",\"ColumnName\":\"id\"}]") + @""", ""hasNextPage"": true } } From 293983b392cb46258a63c40055fe4b3179a699b3 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 22 Nov 2022 22:14:29 +0530 Subject: [PATCH 24/42] fix formatting --- ConfigGenerators/MsSqlCommands.txt | 2 + .../GraphQLStoredProcedureBuilder.cs | 21 +++++++- .../Mutations/MutationBuilder.cs | 1 - .../Sql/SchemaConverter.cs | 24 +++++---- .../GraphQLQueryTests/GraphQLQueryTestBase.cs | 5 +- .../MsSqlGraphQLQueryTests.cs | 2 +- .../Configurations/RuntimeConfigValidator.cs | 51 +++++++++++++++++++ src/Service/MsSqlBooks.sql | 4 ++ src/Service/Resolvers/SqlQueryEngine.cs | 6 ++- src/Service/Services/RequestValidator.cs | 3 +- src/Service/Startup.cs | 2 + 11 files changed, 103 insertions(+), 18 deletions(-) diff --git a/ConfigGenerators/MsSqlCommands.txt b/ConfigGenerators/MsSqlCommands.txt index e7bbfb0fc3..48b77a7548 100644 --- a/ConfigGenerators/MsSqlCommands.txt +++ b/ConfigGenerators/MsSqlCommands.txt @@ -25,6 +25,7 @@ add ArtOfWar --config "dab-config.MsSql.json" --source "aow" --rest true --permi add series --config "dab-config.MsSql.json" --source "series" --permissions "anonymous:*" add GetBooks --config "dab-config.MsSql.json" --source "get_books" --source.type "stored-procedure" --permissions "anonymous:read" --rest true --graphql true add GetBook --config "dab-config.MsSql.json" --source "get_book_by_id" --source.type "stored-procedure" --permissions "anonymous:read" --rest true --graphql true +add GetPublisher --config "dab-config.MsSql.json" --source "get_publisher_by_id" --source.type "stored-procedure" --source.params "id:1" --permissions "anonymous:read" --rest true --graphql true add InsertBook --config "dab-config.MsSql.json" --source "insert_book" --source.type "stored-procedure" --source.params "title:randomX,publisher_id:1234" --permissions "anonymous:create" --rest true --graphql true add CountBooks --config "dab-config.MsSql.json" --source "count_books" --source.type "stored-procedure" --permissions "anonymous:read" --rest true --graphql true add InsertAndDisplayAllBooks --config "dab-config.MsSql.json" --source "insert_and_display_all_books" --source.type "stored-procedure" --source.params "title:randomX,publisher_id:1234" --permissions "anonymous:create,read" --rest true --graphql true @@ -111,6 +112,7 @@ update Journal --config "dab-config.MsSql.json" --permissions "policy_tester_upd update Journal --config "dab-config.MsSql.json" --permissions "authorizationHandlerTester:read" update ArtOfWar --config "dab-config.MsSql.json" --permissions "authenticated:*" --map "DetailAssessmentAndPlanning:始計,WagingWar:作戰,StrategicAttack:謀攻,NoteNum:┬─┬ノ( º _ ºノ)" update GetBook --config "dab-config.MsSql.json" --permissions "authenticated:read" +update GetPublisher --config "dab-config.MsSql.json" --permissions "authenticated:read" update GetBooks --config "dab-config.MsSql.json" --permissions "authenticated:read" update InsertBook --config "dab-config.MsSql.json" --permissions "authenticated:create" update CountBooks --config "dab-config.MsSql.json" --permissions "authenticated:read" diff --git a/src/Service.GraphQLBuilder/GraphQLStoredProcedureBuilder.cs b/src/Service.GraphQLBuilder/GraphQLStoredProcedureBuilder.cs index 67ab5a39d3..0e956d5897 100644 --- a/src/Service.GraphQLBuilder/GraphQLStoredProcedureBuilder.cs +++ b/src/Service.GraphQLBuilder/GraphQLStoredProcedureBuilder.cs @@ -1,6 +1,7 @@ using System.Text.Json; using Azure.DataApiBuilder.Config; using HotChocolate.Language; +using HotChocolate.Types; using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLUtils; namespace Azure.DataApiBuilder.Service.GraphQLBuilder @@ -58,10 +59,11 @@ public static FieldDefinitionNode GenerateStoredProcedureSchema( /// Takes the result from DB as JsonDocument and formats it in a way that can be filtered by column /// name. It parses the Json document into a list of Dictionary with key as result_column_name /// with it's corresponding value. + /// returns an empty list in case of no result or if READ is not allowed /// - public static List FormatStoredProcedureResultAsJsonList(JsonDocument jsonDocument) + public static List FormatStoredProcedureResultAsJsonList(bool IsReadAllowed, JsonDocument jsonDocument) { - if (jsonDocument is null) + if (jsonDocument is null || !IsReadAllowed) { return new List(); } @@ -75,5 +77,20 @@ public static List FormatStoredProcedureResultAsJsonList(JsonDocum return resultJson; } + + /// + /// Helper method to create a default result field for stored-procedure which does not + /// return any row. + /// + public static FieldDefinitionNode GetDefaultResultFieldForStoredProcedure() + { + return new( + location: null, + new("result"), + description: new StringValueNode("Contains output of stored-procedure execution"), + new List(), + new StringType().ToTypeNode(), + new List()); + } } } diff --git a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs index 734f9637d6..1d8e6b1b84 100644 --- a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs @@ -111,7 +111,6 @@ private static Operation GetOperationTypeForStoredProcedure( subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization ); } - } /// diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs index ad7bac725e..1ec021772c 100644 --- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -9,6 +9,7 @@ using HotChocolate.Language; using HotChocolate.Types; using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLNaming; +using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLStoredProcedureBuilder; using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLTypes.SupportedTypes; namespace Azure.DataApiBuilder.Service.GraphQLBuilder.Sql @@ -43,15 +44,9 @@ public static ObjectTypeDefinitionNode FromDatabaseObject( // Here we create a field called result which will be an empty array. if (databaseObject.SourceType is SourceType.StoredProcedure && ((StoredProcedureDefinition)sourceDefinition).Columns.Count == 0) { - FieldDefinitionNode field = new( - location: null, - new("result"), - description: new StringValueNode("Contains output of stored-procedure execution"), - new List(), - new StringType().ToTypeNode(), - new List()); - - fields.Add("result", field); + FieldDefinitionNode field = GetDefaultResultFieldForStoredProcedure(); + + fields.TryAdd("result", field); } foreach ((string columnName, ColumnDefinition column) in sourceDefinition.Columns) @@ -118,6 +113,17 @@ public static ObjectTypeDefinitionNode FromDatabaseObject( fields.Add(columnName, field); } + else + { + // When no roles have permission to access the columns + // we create a default result set with no data. i.e, the stored-procedure + // executed but user can't see the results. + if (databaseObject.SourceType is SourceType.StoredProcedure) + { + FieldDefinitionNode field = GetDefaultResultFieldForStoredProcedure(); + fields.TryAdd("result", field); + } + } } } diff --git a/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs b/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs index 241b02ae67..958c2b7e41 100644 --- a/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs +++ b/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs @@ -952,10 +952,9 @@ public async Task TestStoredProcedureQueryForGettingSingleRow(string dbQuery) { string graphQLQueryName = "GetBook"; string graphQLQuery = @"{ - GetBook(id: 3) { + GetPublisher(id: 1234) { id - title - publisher_id + name } }"; diff --git a/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLQueryTests.cs b/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLQueryTests.cs index 61a94588a3..78ea7b5a6c 100644 --- a/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLQueryTests.cs +++ b/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLQueryTests.cs @@ -256,7 +256,7 @@ public async Task TestQueryOnBasicView() [TestMethod] public async Task TestStoredProcedureQueryForGettingSingleRow() { - string msSqlQuery = $"EXEC dbo.get_book_by_id @id=3"; + string msSqlQuery = $"EXEC dbo.get_publisher_by_id @id=1234"; await TestStoredProcedureQueryForGettingSingleRow(msSqlQuery); } diff --git a/src/Service/Configurations/RuntimeConfigValidator.cs b/src/Service/Configurations/RuntimeConfigValidator.cs index 5cf8e9d8b4..9f11a815db 100644 --- a/src/Service/Configurations/RuntimeConfigValidator.cs +++ b/src/Service/Configurations/RuntimeConfigValidator.cs @@ -9,6 +9,7 @@ using Azure.DataApiBuilder.Service.Authorization; using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.GraphQLBuilder; +using Azure.DataApiBuilder.Service.Models; using Azure.DataApiBuilder.Service.Services; using Microsoft.Extensions.Logging; using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLNaming; @@ -344,6 +345,7 @@ public void ValidatePermissionsInConfig(RuntimeConfig runtimeConfig) { string roleName = permissionSetting.Role; Object[] actions = permissionSetting.Operations; + List operationsList = new(); foreach (Object action in actions) { if (action is null) @@ -439,6 +441,24 @@ public void ValidatePermissionsInConfig(RuntimeConfig runtimeConfig) subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError); } } + + operationsList.Add(actionOp); + } + + // READ operation is allowed with other CRUD operations. + operationsList.Remove(Operation.Read); + + // Apart from READ one of the CUD operations is allowed for stored procedure. + if (entity.ObjectType is SourceType.StoredProcedure) + { + if ((operationsList.Count > 1) + || (operationsList.Count is 1 && operationsList[0] is Operation.All)) + { + throw new DataApiBuilderException( + message: $"StoredProcedure can process only one CUD (Create/Update/Delete) operation.", + statusCode: System.Net.HttpStatusCode.ServiceUnavailable, + subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError); + } } } } @@ -564,6 +584,37 @@ public void ValidateRelationshipsInConfig(RuntimeConfig runtimeConfig, ISqlMetad } } + public static void ValidateStoredProceduresInConfig(RuntimeConfig runtimeConfig, ISqlMetadataProvider sqlMetadataProvider) + { + foreach ((string entityName, Entity entity) in runtimeConfig.Entities) + { + // We are only doing this pre-check for GraphQL because for GraphQL we need the correct schema while making request + // so if the schema is not correct we will halt the engine + // but for rest we can do it when a request is made and only fail that particular request. + if (entity.ObjectType is SourceType.StoredProcedure && + entity.GraphQL is not null && !(entity.GraphQL is bool graphQLEnabled && !graphQLEnabled)) + { + DatabaseObject dbObject = sqlMetadataProvider.EntityToDatabaseObject[entityName]; + StoredProcedureRequestContext sqRequestContext = new( + entityName, + dbObject, + JsonSerializer.SerializeToElement(entity.Parameters), + Operation.All); + try + { + RequestValidator.ValidateStoredProcedureRequestContext(sqRequestContext, sqlMetadataProvider); + } + catch (DataApiBuilderException e) + { + throw new DataApiBuilderException( + message: e.Message, + statusCode: System.Net.HttpStatusCode.ServiceUnavailable, + subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError); + } + } + } + } + /// /// Pre-processes the permissions section of the runtime config object. /// For eg. removing the @item. directives, checking for invalid characters in claimTypes etc. diff --git a/src/Service/MsSqlBooks.sql b/src/Service/MsSqlBooks.sql index 5b74c7d929..51cdec48ff 100644 --- a/src/Service/MsSqlBooks.sql +++ b/src/Service/MsSqlBooks.sql @@ -6,6 +6,7 @@ DROP VIEW IF EXISTS books_publishers_view_composite; DROP VIEW IF EXISTS books_publishers_view_composite_insertable; DROP PROCEDURE IF EXISTS get_books; DROP PROCEDURE IF EXISTS get_book_by_id; +DROP PROCEDURE IF EXISTS get_publisher_by_id; DROP PROCEDURE IF EXISTS insert_book; DROP PROCEDURE IF EXISTS count_books; DROP PROCEDURE IF EXISTS insert_and_display_all_books; @@ -304,6 +305,9 @@ EXEC('CREATE VIEW books_publishers_view_composite_insertable as SELECT EXEC('CREATE PROCEDURE get_book_by_id @id int AS SELECT * FROM dbo.books WHERE id = @id'); +EXEC('CREATE PROCEDURE get_publisher_by_id @id int AS + SELECT * FROM dbo.publishers + WHERE id = @id'); EXEC('CREATE PROCEDURE get_books AS SELECT * FROM dbo.books'); EXEC('CREATE PROCEDURE insert_book @title varchar(max), @publisher_id int AS diff --git a/src/Service/Resolvers/SqlQueryEngine.cs b/src/Service/Resolvers/SqlQueryEngine.cs index 1151ff39fd..d385367df3 100644 --- a/src/Service/Resolvers/SqlQueryEngine.cs +++ b/src/Service/Resolvers/SqlQueryEngine.cs @@ -104,8 +104,12 @@ public async Task, IMetadata>> ExecuteListAsync( _gQLFilterParser, parameters); + // checking if user has read permission on the result + _authorizationResolver.EntityPermissionsMap.TryGetValue(context.Field.Name.Value, out EntityMetadata entityMetadata); + bool IsReadAllowed = entityMetadata.OperationToRolesMap.ContainsKey(Operation.Read); + return new Tuple, IMetadata>( - FormatStoredProcedureResultAsJsonList(await ExecuteAsync(sqlExecuteStructure)), + FormatStoredProcedureResultAsJsonList(IsReadAllowed, await ExecuteAsync(sqlExecuteStructure)), PaginationMetadata.MakeEmptyPaginationMetadata()); } else diff --git a/src/Service/Services/RequestValidator.cs b/src/Service/Services/RequestValidator.cs index dab606cbe0..2d4ce4fc76 100644 --- a/src/Service/Services/RequestValidator.cs +++ b/src/Service/Services/RequestValidator.cs @@ -164,7 +164,8 @@ public static void ValidateStoredProcedureRequestContext( if (missingFields.Count > 0) { throw new DataApiBuilderException( - message: $"Invalid request. Missing required procedure parameters: {string.Join(", ", missingFields)}", + message: $"Invalid request. Missing required procedure parameters: {string.Join(", ", missingFields)}" + + $" for entity: {spRequestCtx.EntityName}", statusCode: HttpStatusCode.BadRequest, subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } diff --git a/src/Service/Startup.cs b/src/Service/Startup.cs index b084252883..6eb618e2b5 100644 --- a/src/Service/Startup.cs +++ b/src/Service/Startup.cs @@ -501,6 +501,8 @@ private async Task PerformOnConfigChangeAsync(IApplicationBuilder app) runtimeConfigValidator.ValidateRelationshipsInConfig(runtimeConfig, sqlMetadataProvider!); } + RuntimeConfigValidator.ValidateStoredProceduresInConfig(runtimeConfig, sqlMetadataProvider); + _logger.LogInformation($"Successfully completed runtime initialization."); return true; } From 9c7f13d50db4694497ab128c2243ff1d9f7b2dbc Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 22 Nov 2022 22:51:02 +0530 Subject: [PATCH 25/42] updating MsSQL commands file --- ConfigGenerators/MsSqlCommands.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ConfigGenerators/MsSqlCommands.txt b/ConfigGenerators/MsSqlCommands.txt index 48b77a7548..fa67499e58 100644 --- a/ConfigGenerators/MsSqlCommands.txt +++ b/ConfigGenerators/MsSqlCommands.txt @@ -24,7 +24,7 @@ add Journal --config "dab-config.MsSql.json" --source "journals" --rest true --g add ArtOfWar --config "dab-config.MsSql.json" --source "aow" --rest true --permissions "anonymous:*" add series --config "dab-config.MsSql.json" --source "series" --permissions "anonymous:*" add GetBooks --config "dab-config.MsSql.json" --source "get_books" --source.type "stored-procedure" --permissions "anonymous:read" --rest true --graphql true -add GetBook --config "dab-config.MsSql.json" --source "get_book_by_id" --source.type "stored-procedure" --permissions "anonymous:read" --rest true --graphql true +add GetBook --config "dab-config.MsSql.json" --source "get_book_by_id" --source.type "stored-procedure" --permissions "anonymous:read" --rest true --graphql false add GetPublisher --config "dab-config.MsSql.json" --source "get_publisher_by_id" --source.type "stored-procedure" --source.params "id:1" --permissions "anonymous:read" --rest true --graphql true add InsertBook --config "dab-config.MsSql.json" --source "insert_book" --source.type "stored-procedure" --source.params "title:randomX,publisher_id:1234" --permissions "anonymous:create" --rest true --graphql true add CountBooks --config "dab-config.MsSql.json" --source "count_books" --source.type "stored-procedure" --permissions "anonymous:read" --rest true --graphql true From a9c2536113aa27a0999e9465f4b447668c161bae Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 23 Nov 2022 08:56:20 +0530 Subject: [PATCH 26/42] fixing rest tests --- .../SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs | 2 +- .../SqlTests/RestApiTests/Find/FindApiTestBase.cs | 6 +++--- src/Service/Services/RequestValidator.cs | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs b/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs index 958c2b7e41..3216f40110 100644 --- a/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs +++ b/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs @@ -950,7 +950,7 @@ public virtual async Task TestQueryOnBasicView(string dbQuery) /// public async Task TestStoredProcedureQueryForGettingSingleRow(string dbQuery) { - string graphQLQueryName = "GetBook"; + string graphQLQueryName = "GetPublisher"; string graphQLQuery = @"{ GetPublisher(id: 1234) { id diff --git a/src/Service.Tests/SqlTests/RestApiTests/Find/FindApiTestBase.cs b/src/Service.Tests/SqlTests/RestApiTests/Find/FindApiTestBase.cs index c0dd0ead2e..813640666b 100644 --- a/src/Service.Tests/SqlTests/RestApiTests/Find/FindApiTestBase.cs +++ b/src/Service.Tests/SqlTests/RestApiTests/Find/FindApiTestBase.cs @@ -1028,7 +1028,7 @@ await SetupAndRunRestApiTest( entityNameOrPath: _integrationProcedureFindOne_EntityName, sqlQuery: string.Empty, exceptionExpected: true, - expectedErrorMessage: "Invalid request. Missing required procedure parameters: id", + expectedErrorMessage: $"Invalid request. Missing required procedure parameters: id for entity:{_integrationProcedureFindOne_EntityName}", expectedStatusCode: HttpStatusCode.BadRequest ); } @@ -1047,7 +1047,7 @@ await SetupAndRunRestApiTest( entityNameOrPath: _integrationProcedureFindMany_EntityName, sqlQuery: string.Empty, exceptionExpected: true, - expectedErrorMessage: "Invalid request. Contained unexpected fields: param", + expectedErrorMessage: $"Invalid request. Contained unexpected fields: param for entity:{_integrationProcedureFindOne_EntityName}", expectedStatusCode: HttpStatusCode.BadRequest ); @@ -1058,7 +1058,7 @@ await SetupAndRunRestApiTest( entityNameOrPath: _integrationProcedureFindOne_EntityName, sqlQuery: string.Empty, exceptionExpected: true, - expectedErrorMessage: "Invalid request. Contained unexpected fields: param", + expectedErrorMessage: $"Invalid request. Contained unexpected fields: param for entity:{_integrationProcedureFindOne_EntityName}", expectedStatusCode: HttpStatusCode.BadRequest ); } diff --git a/src/Service/Services/RequestValidator.cs b/src/Service/Services/RequestValidator.cs index 2d4ce4fc76..c842421e5b 100644 --- a/src/Service/Services/RequestValidator.cs +++ b/src/Service/Services/RequestValidator.cs @@ -155,7 +155,8 @@ public static void ValidateStoredProcedureRequestContext( if (extraFields.Count > 0) { throw new DataApiBuilderException( - message: $"Invalid request. Contained unexpected fields: {string.Join(", ", extraFields)}", + message: $"Invalid request. Contained unexpected fields: {string.Join(", ", extraFields)}" + + $" for entity: {spRequestCtx.EntityName}", statusCode: HttpStatusCode.BadRequest, subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); } From 091bfbf5c7a9e5890b15e21078cb3f9b2562e9cf Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 23 Nov 2022 09:19:29 +0530 Subject: [PATCH 27/42] fixing tests --- .../dab-config.sql.reference.json | 159 +++++++++++++++++- .../RestApiTests/Find/FindApiTestBase.cs | 6 +- 2 files changed, 161 insertions(+), 4 deletions(-) diff --git a/ConfigGenerators/dab-config.sql.reference.json b/ConfigGenerators/dab-config.sql.reference.json index 4d82461c78..ac967a632a 100644 --- a/ConfigGenerators/dab-config.sql.reference.json +++ b/ConfigGenerators/dab-config.sql.reference.json @@ -916,7 +916,7 @@ "object": "get_books" }, "rest": true, - "graphql": false, + "graphql": true, "permissions": [ { "role": "anonymous", @@ -945,6 +945,163 @@ "actions": [ "*" ] } ] + }, + "InsertBook": { + "source": { + "type": "stored-procedure", + "object": "insert_book", + "parameters": { + "title": "randomX", + "publisher_id": 1234 + }, + "key-fields": [] + }, + "rest": true, + "permissions": [ + { + "role": "anonymous", + "actions": [ + "create" + ] + }, + { + "role": "authenticated", + "actions": [ + "create" + ] + } + ], + "graphql": true + }, + "CountBooks": { + "source": { + "type": "stored-procedure", + "object": "count_books", + "key-fields": [] + }, + "rest": true, + "permissions": [ + { + "role": "anonymous", + "actions": [ + "read" + ] + }, + { + "role": "authenticated", + "actions": [ + "read" + ] + } + ], + "graphql": true + }, + "InsertAndDisplayAllBooks": { + "source": { + "type": "stored-procedure", + "object": "insert_and_display_all_books", + "parameters": { + "title": "randomX", + "publisher_id": 1234 + }, + "key-fields": [] + }, + "rest": true, + "permissions": [ + { + "role": "anonymous", + "actions": [ + "create" + ] + }, + { + "role": "authenticated", + "actions": [ + "create" + ] + } + ], + "graphql": true + }, + "DeleteBook": { + "source": { + "type": "stored-procedure", + "object": "delete_book", + "parameters": { + "id": 1 + }, + "key-fields": [] + }, + "rest": true, + "permissions": [ + { + "role": "anonymous", + "actions": [ + "delete" + ] + }, + { + "role": "authenticated", + "actions": [ + "delete" + ] + } + ], + "graphql": true + }, + "UpdateBookTitle": { + "source": { + "type": "stored-procedure", + "object": "update_book_title", + "parameters": { + "id": 1, + "title": "Testing Tonight" + }, + "key-fields": [] + }, + "rest": true, + "permissions": [ + { + "role": "anonymous", + "actions": [ + "update", + "read" + ] + }, + { + "role": "authenticated", + "actions": [ + "update", + "read" + ] + } + ], + "graphql": true + }, + "GetPublisher": { + "source": { + "type": "stored-procedure", + "object": "get_publisher_by_id", + "parameters": { + "id": 1 + } + }, + "rest": true, + "permissions": [ + { + "role": "anonymous", + "actions": [ + "read" + ] + }, + { + "role": "authenticated", + "actions": [ + "read" + ] + } + ], + "graphql": true } } } diff --git a/src/Service.Tests/SqlTests/RestApiTests/Find/FindApiTestBase.cs b/src/Service.Tests/SqlTests/RestApiTests/Find/FindApiTestBase.cs index 813640666b..5d06a71504 100644 --- a/src/Service.Tests/SqlTests/RestApiTests/Find/FindApiTestBase.cs +++ b/src/Service.Tests/SqlTests/RestApiTests/Find/FindApiTestBase.cs @@ -1028,7 +1028,7 @@ await SetupAndRunRestApiTest( entityNameOrPath: _integrationProcedureFindOne_EntityName, sqlQuery: string.Empty, exceptionExpected: true, - expectedErrorMessage: $"Invalid request. Missing required procedure parameters: id for entity:{_integrationProcedureFindOne_EntityName}", + expectedErrorMessage: $"Invalid request. Missing required procedure parameters: id for entity: {_integrationProcedureFindOne_EntityName}", expectedStatusCode: HttpStatusCode.BadRequest ); } @@ -1047,7 +1047,7 @@ await SetupAndRunRestApiTest( entityNameOrPath: _integrationProcedureFindMany_EntityName, sqlQuery: string.Empty, exceptionExpected: true, - expectedErrorMessage: $"Invalid request. Contained unexpected fields: param for entity:{_integrationProcedureFindOne_EntityName}", + expectedErrorMessage: $"Invalid request. Contained unexpected fields: param for entity: {_integrationProcedureFindMany_EntityName}", expectedStatusCode: HttpStatusCode.BadRequest ); @@ -1058,7 +1058,7 @@ await SetupAndRunRestApiTest( entityNameOrPath: _integrationProcedureFindOne_EntityName, sqlQuery: string.Empty, exceptionExpected: true, - expectedErrorMessage: $"Invalid request. Contained unexpected fields: param for entity:{_integrationProcedureFindOne_EntityName}", + expectedErrorMessage: $"Invalid request. Contained unexpected fields: param for entity: {_integrationProcedureFindOne_EntityName}", expectedStatusCode: HttpStatusCode.BadRequest ); } From a8fc53a9f9baa5cc263bb0c984c918c8f7e53428 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 23 Nov 2022 21:19:52 +0530 Subject: [PATCH 28/42] adding new tests --- src/Config/DatabaseObject.cs | 6 --- src/Config/Entity.cs | 2 +- .../GraphQLMutationTestBase.cs | 29 ++++++++--- .../MsSqlGraphQLMutationTests.cs | 14 ++++-- .../Unittests/ConfigValidationUnitTests.cs | 50 +++++++++++++++++++ .../Authorization/AuthorizationResolver.cs | 5 -- .../Configurations/RuntimeConfigValidator.cs | 10 ++-- src/Service/Resolvers/SqlQueryEngine.cs | 6 ++- src/Service/Services/GraphQLSchemaCreator.cs | 4 -- src/Service/Startup.cs | 2 +- 10 files changed, 94 insertions(+), 34 deletions(-) diff --git a/src/Config/DatabaseObject.cs b/src/Config/DatabaseObject.cs index 3b6500d6b6..42d2b9fc23 100644 --- a/src/Config/DatabaseObject.cs +++ b/src/Config/DatabaseObject.cs @@ -84,12 +84,6 @@ public class StoredProcedureDefinition : SourceDefinition /// Key: parameter name, Value: ParameterDefinition object /// public Dictionary Parameters { get; set; } = new(); - - /// - /// The list of fields with their type in the Stored Procedure result - /// Key: ResultSet field name, Value: ResultSet field Type - /// - // public Dictionary ResultSet { get; set; } = new(); } public class ParameterDefinition diff --git a/src/Config/Entity.cs b/src/Config/Entity.cs index f7685c603c..7015dbc460 100644 --- a/src/Config/Entity.cs +++ b/src/Config/Entity.cs @@ -154,7 +154,7 @@ public void TryPopulateSourceFields() else if (sourceJson.ValueKind is JsonValueKind.Object) { DatabaseObjectSource? objectSource - = JsonSerializer.Deserialize((JsonElement)Source, + = JsonSerializer.Deserialize(sourceJson, options: RuntimeConfig.SerializerOptions); if (objectSource is null) diff --git a/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs b/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs index 4bfee0ffa9..db9bccd173 100644 --- a/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs +++ b/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs @@ -3,6 +3,7 @@ using System.Text.Json; using System.Threading.Tasks; using Azure.DataApiBuilder.Service.Exceptions; +using HotChocolate.Utilities; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Azure.DataApiBuilder.Service.Tests.SqlTests.GraphQLMutationTests @@ -119,32 +120,46 @@ public async Task TestStoredProcedureMutationForInsertionWithNoReturns(string db /// Check: If book with the given id is present in the database then /// the mutation query will return the review Id with the content of the review added /// - public async Task TestStoredProcedureMutationForInsertionWithReturns(string dbQueryForResult, string dbQueryToVerifyInsertion) + public async Task TestStoredProcedureMutationForInsertionReturnWithPermission( + string clientRole, + string bookName, + bool isAuthenticated, + string dbQueryToVerifyGraphQLResponse, + string dbQueryToVerifyInsertion) { string graphQLMutationName = "InsertAndDisplayAllBooks"; string graphQLMutation = @" mutation { - InsertAndDisplayAllBooks(title: ""Theory Of DAB"", publisher_id: 1234 ) { + InsertAndDisplayAllBooks(title: " + @$"""{bookName}""" + @", publisher_id: 1234 ) { id, title, publisher_id } } "; - + string currentDbResponse = await GetDatabaseResultAsync(dbQueryToVerifyInsertion); JsonDocument currentResult = JsonDocument.Parse(currentDbResponse); Assert.AreEqual(currentResult.RootElement.GetProperty("count").GetInt64(), 0); - JsonElement graphQLResponse = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true); - string dbResponse = await GetDatabaseResultAsync(dbQueryForResult); - // Stored Procedure didn't return anything - SqlTestHelper.PerformTestEqualJsonStrings(dbResponse, graphQLResponse.ToString()); + JsonElement graphQLResponse = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: isAuthenticated, clientRoleHeader: clientRole); // check to verify new element is inserted string updatedDbResponse = await GetDatabaseResultAsync(dbQueryToVerifyInsertion); JsonDocument updatedResult = JsonDocument.Parse(updatedDbResponse); Assert.AreEqual(updatedResult.RootElement.GetProperty("count").GetInt64(), 1); + + if (clientRole.Equals("Anonymous", StringComparison.OrdinalIgnoreCase)) + { + // Result not available without READ permissions + SqlTestHelper.PerformTestEqualJsonStrings("[]", graphQLResponse.ToString()); + } + else + { + // Result available with READ permissions + string DbResponse = await GetDatabaseResultAsync(dbQueryToVerifyGraphQLResponse); + SqlTestHelper.PerformTestEqualJsonStrings(DbResponse, graphQLResponse.ToString()); + } } public async Task TestStoredProcedureMutationForDeletion(string dbQueryToVerifyDeletion) diff --git a/src/Service.Tests/SqlTests/GraphQLMutationTests/MsSqlGraphQLMutationTests.cs b/src/Service.Tests/SqlTests/GraphQLMutationTests/MsSqlGraphQLMutationTests.cs index 05b9d317c6..41c6225c2c 100644 --- a/src/Service.Tests/SqlTests/GraphQLMutationTests/MsSqlGraphQLMutationTests.cs +++ b/src/Service.Tests/SqlTests/GraphQLMutationTests/MsSqlGraphQLMutationTests.cs @@ -1,3 +1,4 @@ +using System.Net; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -119,21 +120,24 @@ FROM [books] AS [table0] await TestStoredProcedureMutationForInsertionWithNoReturns(msSqlQuery); } - [TestMethod] - public async Task TestStoredProcedureMutationForInsertionWithReturns() + [DataTestMethod] + [DataRow("Anonymous", false, "NO Permission", DisplayName = "Simulator - Anonymous role does not have proper permissions.")] + [DataRow("Authenticated", true, "HAS Permission", DisplayName = "Simulator - Authenticated but Authenticated role does not have proper permissions.")] + public async Task TestStoredProcedureMutationForInsertionReturnWithPermission(string clientRole, bool isAuthenticated, string bookName) { - string dbQueryForResult = $"SELECT id, title, publisher_id FROM books ORDER BY id asc FOR JSON PATH, INCLUDE_NULL_VALUES"; string dbQueryToVerifyInsertion = @" SELECT COUNT(*) AS [count] FROM [books] AS [table0] - WHERE [table0].[title] = 'Theory of DAB' + WHERE [table0].[title] = " + $"'{bookName}'" + @" AND [table0].[publisher_id] = 1234 FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER "; - await TestStoredProcedureMutationForInsertionWithReturns(dbQueryForResult, dbQueryToVerifyInsertion); + string dbQueryToVerifyGraphQLResponse = $"SELECT id, title, publisher_id FROM books ORDER BY id asc FOR JSON PATH, INCLUDE_NULL_VALUES"; + + await TestStoredProcedureMutationForInsertionReturnWithPermission(clientRole, bookName, isAuthenticated, dbQueryToVerifyGraphQLResponse, dbQueryToVerifyInsertion); } [TestMethod] diff --git a/src/Service.Tests/Unittests/ConfigValidationUnitTests.cs b/src/Service.Tests/Unittests/ConfigValidationUnitTests.cs index ed2bf92fa4..fa5b7494c5 100644 --- a/src/Service.Tests/Unittests/ConfigValidationUnitTests.cs +++ b/src/Service.Tests/Unittests/ConfigValidationUnitTests.cs @@ -48,6 +48,56 @@ public void InaccessibleFieldRequestedByPolicy(string dbPolicy) Assert.AreEqual(DataApiBuilderException.SubStatusCodes.ConfigValidationError, ex.SubStatusCode); } + [DataTestMethod] + [DataRow(new object[]{"create", "read"}, true, DisplayName = "Field id is not accessible")] + [DataRow(new object[]{"update", "read"}, true, DisplayName = "Field id is not accessible")] + [DataRow(new object[]{"delete", "read"}, true, DisplayName = "Field id is not accessible")] + [DataRow(new object[]{"create"}, true, DisplayName = "Field id is not accessible")] + [DataRow(new object[]{"read"}, true, DisplayName = "Field id is not accessible")] + [DataRow(new object[]{"update"}, true, DisplayName = "Field id is not accessible")] + [DataRow(new object[]{"delete"}, true, DisplayName = "Field id is not accessible")] + [DataRow(new object[]{"update", "create"}, false, DisplayName = "Field id is not accessible")] + [DataRow(new object[]{"delete", "read", "update"}, false, DisplayName = "Field id is not accessible")] + public void InvalidCRUDForStoredProcedure(object[] operations, bool isValid) + { + RuntimeConfig runtimeConfig = AuthorizationHelpers.InitRuntimeConfig( + entityName: AuthorizationHelpers.TEST_ENTITY, + roleName: AuthorizationHelpers.TEST_ROLE + ); + + PermissionSetting permissionForEntity = new( + role: AuthorizationHelpers.TEST_ROLE, + operations: operations); + + object entitySource = new DatabaseObjectSource( + Type: SourceType.StoredProcedure, + Name: "sourceName", + Parameters: null, + KeyFields: null + ); + + Entity testEntity = new( + Source: entitySource, + Rest: true, + GraphQL: true, + Permissions: new PermissionSetting[] { permissionForEntity }, + Relationships: null, + Mappings: null + ); + runtimeConfig.Entities[AuthorizationHelpers.TEST_ENTITY] = testEntity; + RuntimeConfigValidator configValidator = AuthenticationConfigValidatorUnitTests.GetMockConfigValidator(ref runtimeConfig); + + if (!isValid) + { + DataApiBuilderException ex = Assert.ThrowsException(() => + configValidator.ValidatePermissionsInConfig(runtimeConfig)); + Assert.AreEqual("Invalid OPerations for Entity: SampleEntity. " + + $"StoredProcedure can process only one CUD (Create/Update/Delete) operation.", ex.Message); + Assert.AreEqual(HttpStatusCode.ServiceUnavailable, ex.StatusCode); + Assert.AreEqual(DataApiBuilderException.SubStatusCodes.ConfigValidationError, ex.SubStatusCode); + } + } + /// /// Test method to validate that an appropriate exception is thrown when there is an invalid action /// supplied in the runtimeconfig. diff --git a/src/Service/Authorization/AuthorizationResolver.cs b/src/Service/Authorization/AuthorizationResolver.cs index cca48cf561..46c736c2bf 100644 --- a/src/Service/Authorization/AuthorizationResolver.cs +++ b/src/Service/Authorization/AuthorizationResolver.cs @@ -292,11 +292,6 @@ public void SetEntityPermissionMap(RuntimeConfig? runtimeConfig) } } - if (entity.ObjectType is SourceType.StoredProcedure) - { - Console.Write("asdasd"); - } - // Populate allowed exposed columns for each entity/role/operation combination during startup, // so that it doesn't need to be evaluated per request. PopulateAllowedExposedColumns(operationToColumn.AllowedExposedColumns, entityName, allowedColumns); diff --git a/src/Service/Configurations/RuntimeConfigValidator.cs b/src/Service/Configurations/RuntimeConfigValidator.cs index 9f11a815db..eecb81e7b6 100644 --- a/src/Service/Configurations/RuntimeConfigValidator.cs +++ b/src/Service/Configurations/RuntimeConfigValidator.cs @@ -341,6 +341,7 @@ public void ValidatePermissionsInConfig(RuntimeConfig runtimeConfig) { foreach ((string entityName, Entity entity) in runtimeConfig.Entities) { + entity.TryPopulateSourceFields(); foreach (PermissionSetting permissionSetting in entity.Permissions) { string roleName = permissionSetting.Role; @@ -355,7 +356,8 @@ public void ValidatePermissionsInConfig(RuntimeConfig runtimeConfig) // Evaluate actionOp as the current operation to be validated. Operation actionOp; - if (((JsonElement)action!).ValueKind is JsonValueKind.String) + JsonElement actionJsonElement = JsonSerializer.SerializeToElement(action); + if ((actionJsonElement!).ValueKind is JsonValueKind.String) { string actionName = action.ToString()!; if (AuthorizationResolver.WILDCARD.Equals(actionName)) @@ -455,7 +457,8 @@ public void ValidatePermissionsInConfig(RuntimeConfig runtimeConfig) || (operationsList.Count is 1 && operationsList[0] is Operation.All)) { throw new DataApiBuilderException( - message: $"StoredProcedure can process only one CUD (Create/Update/Delete) operation.", + message: $"Invalid OPerations for Entity: {entityName}. " + + $"StoredProcedure can process only one CUD (Create/Update/Delete) operation.", statusCode: System.Net.HttpStatusCode.ServiceUnavailable, subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError); } @@ -584,13 +587,14 @@ public void ValidateRelationshipsInConfig(RuntimeConfig runtimeConfig, ISqlMetad } } - public static void ValidateStoredProceduresInConfig(RuntimeConfig runtimeConfig, ISqlMetadataProvider sqlMetadataProvider) + public void ValidateStoredProceduresInConfig(RuntimeConfig runtimeConfig, ISqlMetadataProvider sqlMetadataProvider) { foreach ((string entityName, Entity entity) in runtimeConfig.Entities) { // We are only doing this pre-check for GraphQL because for GraphQL we need the correct schema while making request // so if the schema is not correct we will halt the engine // but for rest we can do it when a request is made and only fail that particular request. + entity.TryPopulateSourceFields(); if (entity.ObjectType is SourceType.StoredProcedure && entity.GraphQL is not null && !(entity.GraphQL is bool graphQLEnabled && !graphQLEnabled)) { diff --git a/src/Service/Resolvers/SqlQueryEngine.cs b/src/Service/Resolvers/SqlQueryEngine.cs index d385367df3..83fb8bce56 100644 --- a/src/Service/Resolvers/SqlQueryEngine.cs +++ b/src/Service/Resolvers/SqlQueryEngine.cs @@ -17,6 +17,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLStoredProcedureBuilder; +using static Azure.DataApiBuilder.Service.Authorization.AuthorizationResolver; namespace Azure.DataApiBuilder.Service.Resolvers { @@ -104,9 +105,10 @@ public async Task, IMetadata>> ExecuteListAsync( _gQLFilterParser, parameters); - // checking if user has read permission on the result + // checking if role has read permission on the result _authorizationResolver.EntityPermissionsMap.TryGetValue(context.Field.Name.Value, out EntityMetadata entityMetadata); - bool IsReadAllowed = entityMetadata.OperationToRolesMap.ContainsKey(Operation.Read); + string role = context.ContextData[CLIENT_ROLE_HEADER].ToString(); + bool IsReadAllowed = entityMetadata.RoleToOperationMap[role].OperationToColumnMap.ContainsKey(Operation.Read); return new Tuple, IMetadata>( FormatStoredProcedureResultAsJsonList(IsReadAllowed, await ExecuteAsync(sqlExecuteStructure)), diff --git a/src/Service/Services/GraphQLSchemaCreator.cs b/src/Service/Services/GraphQLSchemaCreator.cs index fb33745682..249b2e4e4e 100644 --- a/src/Service/Services/GraphQLSchemaCreator.cs +++ b/src/Service/Services/GraphQLSchemaCreator.cs @@ -146,10 +146,6 @@ DatabaseType.postgresql or IEnumerable rolesAllowedForEntity = _authorizationResolver.GetRolesForEntity(entityName); Dictionary> rolesAllowedForFields = new(); SourceDefinition sourceDefinition = _sqlMetadataProvider.GetSourceDefinition(entityName); - if (databaseObject.SourceType is SourceType.StoredProcedure) - { - Console.Write("asd"); - } foreach (string column in sourceDefinition.Columns.Keys) { diff --git a/src/Service/Startup.cs b/src/Service/Startup.cs index 6eb618e2b5..5ea8f012f3 100644 --- a/src/Service/Startup.cs +++ b/src/Service/Startup.cs @@ -501,7 +501,7 @@ private async Task PerformOnConfigChangeAsync(IApplicationBuilder app) runtimeConfigValidator.ValidateRelationshipsInConfig(runtimeConfig, sqlMetadataProvider!); } - RuntimeConfigValidator.ValidateStoredProceduresInConfig(runtimeConfig, sqlMetadataProvider); + runtimeConfigValidator.ValidateStoredProceduresInConfig(runtimeConfig, sqlMetadataProvider!); _logger.LogInformation($"Successfully completed runtime initialization."); return true; From 3c35ae74d8eeaaaf9529a2427ac26020b209a644 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 23 Nov 2022 21:22:37 +0530 Subject: [PATCH 29/42] fix formatting --- .../GraphQLMutationTestBase.cs | 3 +-- .../MsSqlGraphQLMutationTests.cs | 1 - .../Unittests/ConfigValidationUnitTests.cs | 24 +++++++++---------- src/Service/Resolvers/SqlQueryEngine.cs | 2 +- 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs b/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs index db9bccd173..cc2d9d66ef 100644 --- a/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs +++ b/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs @@ -3,7 +3,6 @@ using System.Text.Json; using System.Threading.Tasks; using Azure.DataApiBuilder.Service.Exceptions; -using HotChocolate.Utilities; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Azure.DataApiBuilder.Service.Tests.SqlTests.GraphQLMutationTests @@ -137,7 +136,7 @@ public async Task TestStoredProcedureMutationForInsertionReturnWithPermission( } } "; - + string currentDbResponse = await GetDatabaseResultAsync(dbQueryToVerifyInsertion); JsonDocument currentResult = JsonDocument.Parse(currentDbResponse); Assert.AreEqual(currentResult.RootElement.GetProperty("count").GetInt64(), 0); diff --git a/src/Service.Tests/SqlTests/GraphQLMutationTests/MsSqlGraphQLMutationTests.cs b/src/Service.Tests/SqlTests/GraphQLMutationTests/MsSqlGraphQLMutationTests.cs index 41c6225c2c..99259c9683 100644 --- a/src/Service.Tests/SqlTests/GraphQLMutationTests/MsSqlGraphQLMutationTests.cs +++ b/src/Service.Tests/SqlTests/GraphQLMutationTests/MsSqlGraphQLMutationTests.cs @@ -1,4 +1,3 @@ -using System.Net; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; diff --git a/src/Service.Tests/Unittests/ConfigValidationUnitTests.cs b/src/Service.Tests/Unittests/ConfigValidationUnitTests.cs index fa5b7494c5..8589374d2c 100644 --- a/src/Service.Tests/Unittests/ConfigValidationUnitTests.cs +++ b/src/Service.Tests/Unittests/ConfigValidationUnitTests.cs @@ -49,33 +49,33 @@ public void InaccessibleFieldRequestedByPolicy(string dbPolicy) } [DataTestMethod] - [DataRow(new object[]{"create", "read"}, true, DisplayName = "Field id is not accessible")] - [DataRow(new object[]{"update", "read"}, true, DisplayName = "Field id is not accessible")] - [DataRow(new object[]{"delete", "read"}, true, DisplayName = "Field id is not accessible")] - [DataRow(new object[]{"create"}, true, DisplayName = "Field id is not accessible")] - [DataRow(new object[]{"read"}, true, DisplayName = "Field id is not accessible")] - [DataRow(new object[]{"update"}, true, DisplayName = "Field id is not accessible")] - [DataRow(new object[]{"delete"}, true, DisplayName = "Field id is not accessible")] - [DataRow(new object[]{"update", "create"}, false, DisplayName = "Field id is not accessible")] - [DataRow(new object[]{"delete", "read", "update"}, false, DisplayName = "Field id is not accessible")] + [DataRow(new object[] { "create", "read" }, true, DisplayName = "Field id is not accessible")] + [DataRow(new object[] { "update", "read" }, true, DisplayName = "Field id is not accessible")] + [DataRow(new object[] { "delete", "read" }, true, DisplayName = "Field id is not accessible")] + [DataRow(new object[] { "create" }, true, DisplayName = "Field id is not accessible")] + [DataRow(new object[] { "read" }, true, DisplayName = "Field id is not accessible")] + [DataRow(new object[] { "update" }, true, DisplayName = "Field id is not accessible")] + [DataRow(new object[] { "delete" }, true, DisplayName = "Field id is not accessible")] + [DataRow(new object[] { "update", "create" }, false, DisplayName = "Field id is not accessible")] + [DataRow(new object[] { "delete", "read", "update" }, false, DisplayName = "Field id is not accessible")] public void InvalidCRUDForStoredProcedure(object[] operations, bool isValid) { RuntimeConfig runtimeConfig = AuthorizationHelpers.InitRuntimeConfig( entityName: AuthorizationHelpers.TEST_ENTITY, roleName: AuthorizationHelpers.TEST_ROLE ); - + PermissionSetting permissionForEntity = new( role: AuthorizationHelpers.TEST_ROLE, operations: operations); - + object entitySource = new DatabaseObjectSource( Type: SourceType.StoredProcedure, Name: "sourceName", Parameters: null, KeyFields: null ); - + Entity testEntity = new( Source: entitySource, Rest: true, diff --git a/src/Service/Resolvers/SqlQueryEngine.cs b/src/Service/Resolvers/SqlQueryEngine.cs index 83fb8bce56..3ea020b35b 100644 --- a/src/Service/Resolvers/SqlQueryEngine.cs +++ b/src/Service/Resolvers/SqlQueryEngine.cs @@ -16,8 +16,8 @@ using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLStoredProcedureBuilder; using static Azure.DataApiBuilder.Service.Authorization.AuthorizationResolver; +using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLStoredProcedureBuilder; namespace Azure.DataApiBuilder.Service.Resolvers { From d6af9498698ecbdc043018396c6669ecf03bd09d Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 23 Nov 2022 21:50:10 +0530 Subject: [PATCH 30/42] fixing test --- ConfigGenerators/MsSqlCommands.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ConfigGenerators/MsSqlCommands.txt b/ConfigGenerators/MsSqlCommands.txt index fa67499e58..cbe53e5e2f 100644 --- a/ConfigGenerators/MsSqlCommands.txt +++ b/ConfigGenerators/MsSqlCommands.txt @@ -28,7 +28,7 @@ add GetBook --config "dab-config.MsSql.json" --source "get_book_by_id" --source. add GetPublisher --config "dab-config.MsSql.json" --source "get_publisher_by_id" --source.type "stored-procedure" --source.params "id:1" --permissions "anonymous:read" --rest true --graphql true add InsertBook --config "dab-config.MsSql.json" --source "insert_book" --source.type "stored-procedure" --source.params "title:randomX,publisher_id:1234" --permissions "anonymous:create" --rest true --graphql true add CountBooks --config "dab-config.MsSql.json" --source "count_books" --source.type "stored-procedure" --permissions "anonymous:read" --rest true --graphql true -add InsertAndDisplayAllBooks --config "dab-config.MsSql.json" --source "insert_and_display_all_books" --source.type "stored-procedure" --source.params "title:randomX,publisher_id:1234" --permissions "anonymous:create,read" --rest true --graphql true +add InsertAndDisplayAllBooks --config "dab-config.MsSql.json" --source "insert_and_display_all_books" --source.type "stored-procedure" --source.params "title:randomX,publisher_id:1234" --permissions "anonymous:create" --rest true --graphql true add DeleteBook --config "dab-config.MsSql.json" --source "delete_book" --source.type "stored-procedure" --source.params "id:1" --permissions "anonymous:delete" --rest true --graphql true add UpdateBookTitle --config "dab-config.MsSql.json" --source "update_book_title" --source.type "stored-procedure" --source.params "id:1,title:Testing Tonight" --permissions "anonymous:update,read" --rest true --graphql true update Publisher --config "dab-config.MsSql.json" --permissions "authenticated:create,read,update,delete" --rest true --graphql true --relationship books --target.entity Book --cardinality many From 58ade560eb14bb3128686b162c2132aa2f54c0a4 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 23 Nov 2022 23:18:01 +0530 Subject: [PATCH 31/42] fix formatting --- .../Mutations/MutationBuilder.cs | 13 +++++++---- .../GraphQLMutationTestBase.cs | 23 ++++++++++++++----- .../MsSqlGraphQLMutationTests.cs | 16 +++++++++++++ .../GraphQLQueryTests/GraphQLQueryTestBase.cs | 3 +++ .../MsSqlGraphQLQueryTests.cs | 9 ++++++++ .../Unittests/ConfigValidationUnitTests.cs | 21 +++++++++-------- .../Configurations/RuntimeConfigValidator.cs | 12 ++++------ .../MetadataProviders/SqlMetadataProvider.cs | 6 +---- 8 files changed, 70 insertions(+), 33 deletions(-) diff --git a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs index 1d8e6b1b84..35ccaf9a88 100644 --- a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs @@ -41,8 +41,11 @@ public static DocumentNode Build( NameNode name = objectTypeDefinitionNode.Name; string dbEntityName = ObjectTypeToEntityName(objectTypeDefinitionNode); + // We will only create a single mutation query for stored procedure + // unlike table/views where we create one for each CUD operation. if (entities[dbEntityName].ObjectType is SourceType.StoredProcedure) { + // If the role has actions other than READ, a schema for mutation will be generated. Operation storedProcedureOperation = GetOperationTypeForStoredProcedure(dbEntityName, entityPermissionsMap); if (storedProcedureOperation is not Operation.Read) { @@ -71,12 +74,12 @@ public static DocumentNode Build( /// /// Tries to fetch the Operation Type for Stored Procedure. - /// Stored Procedure currently support only 1 CRUD operation at a time. + /// Stored Procedure currently support at most 1 CUD operation at a time + /// apart from READ operation, an Exception is thrown otherwise. /// private static Operation GetOperationTypeForStoredProcedure( string dbEntityName, - Dictionary? entityPermissionsMap - ) + Dictionary? entityPermissionsMap) { List operations = entityPermissionsMap![dbEntityName].OperationToRolesMap.Keys.ToList(); operations.Remove(Operation.Read); @@ -89,9 +92,9 @@ private static Operation GetOperationTypeForStoredProcedure( } else if (operations.Count == 1) { - if (entityPermissionsMap.TryGetValue(dbEntityName, out EntityMetadata entityMetadata)) + if (entityPermissionsMap.TryGetValue(dbEntityName, out _)) { - return entityMetadata!.OperationToRolesMap.First().Key; + return operations.First(); } else { diff --git a/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs b/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs index cc2d9d66ef..a242ad9909 100644 --- a/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs +++ b/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs @@ -85,9 +85,9 @@ public async Task InsertMutationForConstantdefaultValue(string dbQuery) } /// - /// Do: Inserts new review with default content for a Review and return its id and content - /// Check: If book with the given id is present in the database then - /// the mutation query will return the review Id with the content of the review added + /// Do: Inserts new book in the books table with given publisher_id + /// Check: If the new book is inserted into the DB and + /// verifies the response. /// public async Task TestStoredProcedureMutationForInsertionWithNoReturns(string dbQuery) { @@ -115,9 +115,9 @@ public async Task TestStoredProcedureMutationForInsertionWithNoReturns(string db } /// - /// Do: Inserts new review with default content for a Review and return its id and content - /// Check: If book with the given id is present in the database then - /// the mutation query will return the review Id with the content of the review added + /// Do: Inserts new book in the books table with given publisher_id + /// Check: If the user has read permission it will display the result + /// else user won't see the result of the stored procedure. /// public async Task TestStoredProcedureMutationForInsertionReturnWithPermission( string clientRole, @@ -161,6 +161,11 @@ public async Task TestStoredProcedureMutationForInsertionReturnWithPermission( } } + /// + /// Do: Deletes book from the books table with given id + /// Check: If the intended book is deleted from the DB and + /// verifies the response. + /// public async Task TestStoredProcedureMutationForDeletion(string dbQueryToVerifyDeletion) { string graphQLMutationName = "DeleteBook"; @@ -186,6 +191,12 @@ public async Task TestStoredProcedureMutationForDeletion(string dbQueryToVerifyD Assert.AreEqual(updatedResult.RootElement.GetProperty("count").GetInt64(), 0); } + /// + /// Do: updates a book title from the books table with given id + /// and new title. + /// Check: The book title should be updated with the given id + /// and the response is verified which contains the updated row. + /// public async Task TestStoredProcedureMutationForUpdate(string dbQuery) { string graphQLMutationName = "UpdateBookTitle"; diff --git a/src/Service.Tests/SqlTests/GraphQLMutationTests/MsSqlGraphQLMutationTests.cs b/src/Service.Tests/SqlTests/GraphQLMutationTests/MsSqlGraphQLMutationTests.cs index 99259c9683..9490432654 100644 --- a/src/Service.Tests/SqlTests/GraphQLMutationTests/MsSqlGraphQLMutationTests.cs +++ b/src/Service.Tests/SqlTests/GraphQLMutationTests/MsSqlGraphQLMutationTests.cs @@ -103,6 +103,10 @@ ORDER BY [id] asc await InsertMutationForConstantdefaultValue(msSqlQuery); } + /// + /// Do: insert new Book and return nothing + /// Check: if the intended book is inserted in books table + /// [TestMethod] public async Task TestStoredProcedureMutationForInsertionWithNoReturns() { @@ -119,6 +123,10 @@ FROM [books] AS [table0] await TestStoredProcedureMutationForInsertionWithNoReturns(msSqlQuery); } + /// + /// Do: insert new Book and return all the rows from book table + /// Check: if the book is inserted and shows result to roles having read permission + /// [DataTestMethod] [DataRow("Anonymous", false, "NO Permission", DisplayName = "Simulator - Anonymous role does not have proper permissions.")] [DataRow("Authenticated", true, "HAS Permission", DisplayName = "Simulator - Authenticated but Authenticated role does not have proper permissions.")] @@ -139,6 +147,10 @@ FROM [books] AS [table0] await TestStoredProcedureMutationForInsertionReturnWithPermission(clientRole, bookName, isAuthenticated, dbQueryToVerifyGraphQLResponse, dbQueryToVerifyInsertion); } + /// + /// Do: deletes a Book and return nothing + /// Check: the intended book is deleted + /// [TestMethod] public async Task TestStoredProcedureMutationForDeletion() { @@ -157,6 +169,10 @@ FROM [books] AS [table0] await TestStoredProcedureMutationForDeletion(dbQueryToVerifyDeletion); } + /// + /// Do: Book title updation and return the updated row + /// Check: if the result returned from the mutation is correct + /// [TestMethod] public async Task TestStoredProcedureMutationForUpdate() { diff --git a/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs b/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs index 3216f40110..c6ebedd238 100644 --- a/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs +++ b/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs @@ -1045,6 +1045,9 @@ public virtual async Task TestInvalidFirstParamQuery() SqlTestHelper.TestForErrorInGraphQLResponse(result.ToString(), statusCode: $"{DataApiBuilderException.SubStatusCodes.BadRequest}"); } + /// + /// Checks failure on providing invalid arguments in graphQL Query + /// public async Task TestStoredProcedureQueryWithInvalidArgumentType() { string graphQLQueryName = "GetBook"; diff --git a/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLQueryTests.cs b/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLQueryTests.cs index 78ea7b5a6c..3a57c19aca 100644 --- a/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLQueryTests.cs +++ b/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLQueryTests.cs @@ -253,6 +253,9 @@ public async Task TestQueryOnBasicView() await base.TestQueryOnBasicView(msSqlQuery); } + /// + /// Test to execute stored-procedure in graphQL that returns a single row + /// [TestMethod] public async Task TestStoredProcedureQueryForGettingSingleRow() { @@ -260,6 +263,9 @@ public async Task TestStoredProcedureQueryForGettingSingleRow() await TestStoredProcedureQueryForGettingSingleRow(msSqlQuery); } + /// + /// Test to execute stored-procedure in graphQL that returns a list(multiple rows) + /// [TestMethod] public async Task TestStoredProcedureQueryForGettingMultipleRows() { @@ -267,6 +273,9 @@ public async Task TestStoredProcedureQueryForGettingMultipleRows() await TestStoredProcedureQueryForGettingMultipleRows(msSqlQuery); } + /// + /// Test to execute stored-procedure in graphQL that counts the total number of rows + /// [TestMethod] public async Task TestStoredProcedureQueryForGettingTotalNumberOfRows() { diff --git a/src/Service.Tests/Unittests/ConfigValidationUnitTests.cs b/src/Service.Tests/Unittests/ConfigValidationUnitTests.cs index 8589374d2c..d9ecc9a663 100644 --- a/src/Service.Tests/Unittests/ConfigValidationUnitTests.cs +++ b/src/Service.Tests/Unittests/ConfigValidationUnitTests.cs @@ -48,16 +48,19 @@ public void InaccessibleFieldRequestedByPolicy(string dbPolicy) Assert.AreEqual(DataApiBuilderException.SubStatusCodes.ConfigValidationError, ex.SubStatusCode); } + /// + /// Test method to validate that only 1 CUD operation along with READ is provided for a particular role. + /// [DataTestMethod] - [DataRow(new object[] { "create", "read" }, true, DisplayName = "Field id is not accessible")] - [DataRow(new object[] { "update", "read" }, true, DisplayName = "Field id is not accessible")] - [DataRow(new object[] { "delete", "read" }, true, DisplayName = "Field id is not accessible")] - [DataRow(new object[] { "create" }, true, DisplayName = "Field id is not accessible")] - [DataRow(new object[] { "read" }, true, DisplayName = "Field id is not accessible")] - [DataRow(new object[] { "update" }, true, DisplayName = "Field id is not accessible")] - [DataRow(new object[] { "delete" }, true, DisplayName = "Field id is not accessible")] - [DataRow(new object[] { "update", "create" }, false, DisplayName = "Field id is not accessible")] - [DataRow(new object[] { "delete", "read", "update" }, false, DisplayName = "Field id is not accessible")] + [DataRow(new object[] { "create", "read" }, true, DisplayName = "Stored-procedure with create-read permission")] + [DataRow(new object[] { "update", "read" }, true, DisplayName = "Stored-procedure with update-read permission")] + [DataRow(new object[] { "delete", "read" }, true, DisplayName = "Stored-procedure with delete-read permission")] + [DataRow(new object[] { "create" }, true, DisplayName = "Stored-procedure with only create permission")] + [DataRow(new object[] { "read" }, true, DisplayName = "Stored-procedure with only read permission")] + [DataRow(new object[] { "update" }, true, DisplayName = "Stored-procedure with only update permission")] + [DataRow(new object[] { "delete" }, true, DisplayName = "Stored-procedure with only delete permission")] + [DataRow(new object[] { "update", "create" }, false, DisplayName = "Stored-procedure with update-create permission")] + [DataRow(new object[] { "delete", "read", "update" }, false, DisplayName = "Stored-procedure with delete-read-update permission")] public void InvalidCRUDForStoredProcedure(object[] operations, bool isValid) { RuntimeConfig runtimeConfig = AuthorizationHelpers.InitRuntimeConfig( diff --git a/src/Service/Configurations/RuntimeConfigValidator.cs b/src/Service/Configurations/RuntimeConfigValidator.cs index eecb81e7b6..ac70409ab2 100644 --- a/src/Service/Configurations/RuntimeConfigValidator.cs +++ b/src/Service/Configurations/RuntimeConfigValidator.cs @@ -67,8 +67,6 @@ public void ValidateConfig() ValidateAuthenticationConfig(); - // ValidateStoredProcedureInConfig(runtimeConfig); - // Running these graphQL validations only in development mode to ensure // fast startup of engine in production mode. if (runtimeConfig.GraphQLGlobalSettings.Enabled @@ -101,12 +99,6 @@ public static void ValidateDataSourceInConfig( ValidateDatabaseType(runtimeConfig, fileSystem, logger); } - // public static void ValidateStoredProcedureInConfig(ISqlMetadataProvider sqlMetadataProvider) - // { - // AuthorizationResolver x = new AuthorizationResolver(_runtimeConfigProvider, sqlMetadataProvider, _logger); - // x.SetEntityPermissionMap(_runtimeConfigProvider.GetRuntimeConfiguration()); - // } - /// /// Throws exception if database type is incorrectly configured /// in the config. @@ -587,6 +579,10 @@ public void ValidateRelationshipsInConfig(RuntimeConfig runtimeConfig, ISqlMetad } } + /// + /// Validates the parameters given in the config are consistent with the DB i.e., config has all + /// the parameters that are specified for the stored procedure in DB. + /// public void ValidateStoredProceduresInConfig(RuntimeConfig runtimeConfig, ISqlMetadataProvider sqlMetadataProvider) { foreach ((string entityName, Entity entity) in runtimeConfig.Entities) diff --git a/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs index 593443d891..0ac2f09c7c 100644 --- a/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs @@ -802,10 +802,7 @@ private void GenerateExposedToBackingColumnMapsForEntities() { foreach (string entityName in _entities.Keys) { - // Ensure we don't attempt for stored procedures, which have no - // SourceDefinition, Columns, Keys, etc. - // if (_entities[entityName].ObjectType is not SourceType.StoredProcedure) - // { + // InCase of StoredProcedures, result set definitions becomes the column definition. Dictionary? mapping = GetMappingForEntity(entityName); EntityBackingColumnsToExposedNames[entityName] = mapping is not null ? mapping : new(); EntityExposedNamesToBackingColumnNames[entityName] = EntityBackingColumnsToExposedNames[entityName].ToDictionary(x => x.Value, x => x.Key); @@ -818,7 +815,6 @@ private void GenerateExposedToBackingColumnMapsForEntities() EntityExposedNamesToBackingColumnNames[entityName].Add(column, column); } } - // } } } From 53a0ebbd538147e42d4e543c107c52e945848fe2 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Fri, 25 Nov 2022 11:43:53 +0530 Subject: [PATCH 32/42] reomoving comented code --- .../Mutations/MutationBuilder.cs | 32 ++++--------------- .../Configurations/RuntimeConfigValidator.cs | 1 - src/Service/Services/GraphQLSchemaCreator.cs | 1 - .../MetadataProviders/SqlMetadataProvider.cs | 1 - 4 files changed, 7 insertions(+), 28 deletions(-) diff --git a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs index 35ccaf9a88..90eab1d1d9 100644 --- a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs @@ -75,7 +75,8 @@ public static DocumentNode Build( /// /// Tries to fetch the Operation Type for Stored Procedure. /// Stored Procedure currently support at most 1 CUD operation at a time - /// apart from READ operation, an Exception is thrown otherwise. + /// apart from READ operation, This check is done during intialization + /// as part of config validation. /// private static Operation GetOperationTypeForStoredProcedure( string dbEntityName, @@ -83,36 +84,17 @@ private static Operation GetOperationTypeForStoredProcedure( { List operations = entityPermissionsMap![dbEntityName].OperationToRolesMap.Keys.ToList(); operations.Remove(Operation.Read); - - // Only one of the mutation operation(CUD) is allowed at once + + // It can have maximum of two operation where one will be read and other can be one of CUD operations if (operations.Count == 0) { // If it only contained Read Operation return Operation.Read; } - else if (operations.Count == 1) - { - if (entityPermissionsMap.TryGetValue(dbEntityName, out _)) - { - return operations.First(); - } - else - { - throw new DataApiBuilderException( - message: $"Failed to obtain permissions for entity:{dbEntityName}", - statusCode: HttpStatusCode.PreconditionFailed, - subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization - ); - } - - } - else + else { - throw new DataApiBuilderException( - message: $"StoredProcedure can't have more than one CRUD operation.", - statusCode: HttpStatusCode.PreconditionFailed, - subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization - ); + // It will have only one element + return operations.First(); } } diff --git a/src/Service/Configurations/RuntimeConfigValidator.cs b/src/Service/Configurations/RuntimeConfigValidator.cs index ac70409ab2..be165422a4 100644 --- a/src/Service/Configurations/RuntimeConfigValidator.cs +++ b/src/Service/Configurations/RuntimeConfigValidator.cs @@ -215,7 +215,6 @@ public static void ValidateEntityNamesInConfig(Dictionary entity foreach (string entityName in entityCollection.Keys) { Entity entity = entityCollection[entityName]; - // entity.TryPopulateSourceFields(); if (entity.GraphQL is null) { diff --git a/src/Service/Services/GraphQLSchemaCreator.cs b/src/Service/Services/GraphQLSchemaCreator.cs index 249b2e4e4e..bed327e6ee 100644 --- a/src/Service/Services/GraphQLSchemaCreator.cs +++ b/src/Service/Services/GraphQLSchemaCreator.cs @@ -159,7 +159,6 @@ DatabaseType.postgresql or ); } } - // } // The roles allowed for Fields are the roles allowed to READ the fields, so any role that has a read definition for the field. // Only add objectTypeDefinition for GraphQL if it has a role definition defined for access. diff --git a/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs index 0ac2f09c7c..65135a094f 100644 --- a/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs @@ -763,7 +763,6 @@ private async Task PopulateResultSetDefinitionsForStoredProcedureAsync( Type resultFieldType = SqlToCLRType(element.GetProperty("system_type_name").ToString()); // Store the dictionary containing result set field with it's type as Columns - // storedProcedureDefinition.ResultSet.TryAdd(resultFieldName, resultFieldType); storedProcedureDefinition.Columns.TryAdd(resultFieldName, new(resultFieldType)); } } From 597c3ca9f71064ce277b37bdb1e33844cee1b354 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Fri, 25 Nov 2022 11:44:34 +0530 Subject: [PATCH 33/42] fix formatting --- src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs index 90eab1d1d9..bbd99ddd1e 100644 --- a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs @@ -1,7 +1,5 @@ -using System.Net; using Azure.DataApiBuilder.Auth; using Azure.DataApiBuilder.Config; -using Azure.DataApiBuilder.Service.Exceptions; using HotChocolate.Language; using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLNaming; using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLUtils; @@ -84,14 +82,14 @@ private static Operation GetOperationTypeForStoredProcedure( { List operations = entityPermissionsMap![dbEntityName].OperationToRolesMap.Keys.ToList(); operations.Remove(Operation.Read); - + // It can have maximum of two operation where one will be read and other can be one of CUD operations if (operations.Count == 0) { // If it only contained Read Operation return Operation.Read; } - else + else { // It will have only one element return operations.First(); From 3e3c74427e3b88c6a19c1288d2f705d8bf1d5599 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 29 Nov 2022 11:51:37 +0530 Subject: [PATCH 34/42] fixed action validation for stored procedure --- ConfigGenerators/MsSqlCommands.txt | 6 +- .../dab-config.sql.reference.json | 37 ++--------- .../GraphQLStoredProcedureBuilder.cs | 3 +- .../Mutations/MutationBuilder.cs | 19 ++---- .../Sql/SchemaConverter.cs | 6 +- .../GraphQLMutationTestBase.cs | 66 ++----------------- .../MsSqlGraphQLMutationTests.cs | 28 +------- .../Unittests/ConfigValidationUnitTests.cs | 22 ++++--- .../Configurations/RuntimeConfigValidator.cs | 9 +-- src/Service/MsSqlBooks.sql | 6 -- src/Service/Resolvers/SqlQueryEngine.cs | 13 ++-- 11 files changed, 47 insertions(+), 168 deletions(-) diff --git a/ConfigGenerators/MsSqlCommands.txt b/ConfigGenerators/MsSqlCommands.txt index cbe53e5e2f..25deba7584 100644 --- a/ConfigGenerators/MsSqlCommands.txt +++ b/ConfigGenerators/MsSqlCommands.txt @@ -28,9 +28,8 @@ add GetBook --config "dab-config.MsSql.json" --source "get_book_by_id" --source. add GetPublisher --config "dab-config.MsSql.json" --source "get_publisher_by_id" --source.type "stored-procedure" --source.params "id:1" --permissions "anonymous:read" --rest true --graphql true add InsertBook --config "dab-config.MsSql.json" --source "insert_book" --source.type "stored-procedure" --source.params "title:randomX,publisher_id:1234" --permissions "anonymous:create" --rest true --graphql true add CountBooks --config "dab-config.MsSql.json" --source "count_books" --source.type "stored-procedure" --permissions "anonymous:read" --rest true --graphql true -add InsertAndDisplayAllBooks --config "dab-config.MsSql.json" --source "insert_and_display_all_books" --source.type "stored-procedure" --source.params "title:randomX,publisher_id:1234" --permissions "anonymous:create" --rest true --graphql true add DeleteBook --config "dab-config.MsSql.json" --source "delete_book" --source.type "stored-procedure" --source.params "id:1" --permissions "anonymous:delete" --rest true --graphql true -add UpdateBookTitle --config "dab-config.MsSql.json" --source "update_book_title" --source.type "stored-procedure" --source.params "id:1,title:Testing Tonight" --permissions "anonymous:update,read" --rest true --graphql true +add UpdateBookTitle --config "dab-config.MsSql.json" --source "update_book_title" --source.type "stored-procedure" --source.params "id:1,title:Testing Tonight" --permissions "anonymous:update" --rest true --graphql true update Publisher --config "dab-config.MsSql.json" --permissions "authenticated:create,read,update,delete" --rest true --graphql true --relationship books --target.entity Book --cardinality many update Publisher --config "dab-config.MsSql.json" --permissions "policy_tester_01:create,delete" update Publisher --config "dab-config.MsSql.json" --permissions "policy_tester_01:update" --fields.include "*" @@ -116,6 +115,5 @@ update GetPublisher --config "dab-config.MsSql.json" --permissions "authenticate update GetBooks --config "dab-config.MsSql.json" --permissions "authenticated:read" update InsertBook --config "dab-config.MsSql.json" --permissions "authenticated:create" update CountBooks --config "dab-config.MsSql.json" --permissions "authenticated:read" -update InsertAndDisplayAllBooks --config "dab-config.MsSql.json" --permissions "authenticated:create,read" update DeleteBook --config "dab-config.MsSql.json" --permissions "authenticated:delete" -update UpdateBookTitle --config "dab-config.MsSql.json" --permissions "authenticated:update,read" +update UpdateBookTitle --config "dab-config.MsSql.json" --permissions "authenticated:update" diff --git a/ConfigGenerators/dab-config.sql.reference.json b/ConfigGenerators/dab-config.sql.reference.json index ac967a632a..a24c65c8c5 100644 --- a/ConfigGenerators/dab-config.sql.reference.json +++ b/ConfigGenerators/dab-config.sql.reference.json @@ -924,7 +924,7 @@ }, { "role": "authenticated", - "actions": [ "*" ] + "actions": [ "read" ] } ] }, @@ -942,7 +942,7 @@ }, { "role": "authenticated", - "actions": [ "*" ] + "actions": [ "read" ] } ] }, @@ -996,33 +996,6 @@ ], "graphql": true }, - "InsertAndDisplayAllBooks": { - "source": { - "type": "stored-procedure", - "object": "insert_and_display_all_books", - "parameters": { - "title": "randomX", - "publisher_id": 1234 - }, - "key-fields": [] - }, - "rest": true, - "permissions": [ - { - "role": "anonymous", - "actions": [ - "create" - ] - }, - { - "role": "authenticated", - "actions": [ - "create" - ] - } - ], - "graphql": true - }, "DeleteBook": { "source": { "type": "stored-procedure", @@ -1064,15 +1037,13 @@ { "role": "anonymous", "actions": [ - "update", - "read" + "update" ] }, { "role": "authenticated", "actions": [ - "update", - "read" + "update" ] } ], diff --git a/src/Service.GraphQLBuilder/GraphQLStoredProcedureBuilder.cs b/src/Service.GraphQLBuilder/GraphQLStoredProcedureBuilder.cs index 0e956d5897..7b7ad2be5b 100644 --- a/src/Service.GraphQLBuilder/GraphQLStoredProcedureBuilder.cs +++ b/src/Service.GraphQLBuilder/GraphQLStoredProcedureBuilder.cs @@ -59,7 +59,8 @@ public static FieldDefinitionNode GenerateStoredProcedureSchema( /// Takes the result from DB as JsonDocument and formats it in a way that can be filtered by column /// name. It parses the Json document into a list of Dictionary with key as result_column_name /// with it's corresponding value. - /// returns an empty list in case of no result or if READ is not allowed + /// returns an empty list in case of no result + /// or stored-procedure is trying to read from DB without READ permission. /// public static List FormatStoredProcedureResultAsJsonList(bool IsReadAllowed, JsonDocument jsonDocument) { diff --git a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs index bbd99ddd1e..beaf83fbf8 100644 --- a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs @@ -72,28 +72,17 @@ public static DocumentNode Build( /// /// Tries to fetch the Operation Type for Stored Procedure. - /// Stored Procedure currently support at most 1 CUD operation at a time - /// apart from READ operation, This check is done during intialization - /// as part of config validation. + /// Stored Procedure currently support at most 1 CRUD operation at a time. + /// This check is done during initialization as part of config validation. /// private static Operation GetOperationTypeForStoredProcedure( string dbEntityName, Dictionary? entityPermissionsMap) { List operations = entityPermissionsMap![dbEntityName].OperationToRolesMap.Keys.ToList(); - operations.Remove(Operation.Read); - // It can have maximum of two operation where one will be read and other can be one of CUD operations - if (operations.Count == 0) - { - // If it only contained Read Operation - return Operation.Read; - } - else - { - // It will have only one element - return operations.First(); - } + // Stored Procedure will have only CRUD action. + return operations.First(); } /// diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs index 1ec021772c..70dc13053b 100644 --- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -113,11 +113,11 @@ public static ObjectTypeDefinitionNode FromDatabaseObject( fields.Add(columnName, field); } - else + else if (databaseObject.SourceType is SourceType.StoredProcedure) { // When no roles have permission to access the columns - // we create a default result set with no data. i.e, the stored-procedure - // executed but user can't see the results. + // we create a default result set with no data. i.e, the stored-procedure is + // executed but user see empty results because it is trying to query the DB without read permission. if (databaseObject.SourceType is SourceType.StoredProcedure) { FieldDefinitionNode field = GetDefaultResultFieldForStoredProcedure(); diff --git a/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs b/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs index a242ad9909..32b0f7eac0 100644 --- a/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs +++ b/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using System.Text.Json; using System.Threading.Tasks; using Azure.DataApiBuilder.Service.Exceptions; @@ -89,7 +87,7 @@ public async Task InsertMutationForConstantdefaultValue(string dbQuery) /// Check: If the new book is inserted into the DB and /// verifies the response. /// - public async Task TestStoredProcedureMutationForInsertionWithNoReturns(string dbQuery) + public async Task TestStoredProcedureMutationForInsertion(string dbQuery) { string graphQLMutationName = "InsertBook"; string graphQLMutation = @" @@ -114,53 +112,6 @@ public async Task TestStoredProcedureMutationForInsertionWithNoReturns(string db Assert.AreEqual(updatedResult.RootElement.GetProperty("count").GetInt64(), 1); } - /// - /// Do: Inserts new book in the books table with given publisher_id - /// Check: If the user has read permission it will display the result - /// else user won't see the result of the stored procedure. - /// - public async Task TestStoredProcedureMutationForInsertionReturnWithPermission( - string clientRole, - string bookName, - bool isAuthenticated, - string dbQueryToVerifyGraphQLResponse, - string dbQueryToVerifyInsertion) - { - string graphQLMutationName = "InsertAndDisplayAllBooks"; - string graphQLMutation = @" - mutation { - InsertAndDisplayAllBooks(title: " + @$"""{bookName}""" + @", publisher_id: 1234 ) { - id, - title, - publisher_id - } - } - "; - - string currentDbResponse = await GetDatabaseResultAsync(dbQueryToVerifyInsertion); - JsonDocument currentResult = JsonDocument.Parse(currentDbResponse); - Assert.AreEqual(currentResult.RootElement.GetProperty("count").GetInt64(), 0); - - JsonElement graphQLResponse = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: isAuthenticated, clientRoleHeader: clientRole); - - // check to verify new element is inserted - string updatedDbResponse = await GetDatabaseResultAsync(dbQueryToVerifyInsertion); - JsonDocument updatedResult = JsonDocument.Parse(updatedDbResponse); - Assert.AreEqual(updatedResult.RootElement.GetProperty("count").GetInt64(), 1); - - if (clientRole.Equals("Anonymous", StringComparison.OrdinalIgnoreCase)) - { - // Result not available without READ permissions - SqlTestHelper.PerformTestEqualJsonStrings("[]", graphQLResponse.ToString()); - } - else - { - // Result available with READ permissions - string DbResponse = await GetDatabaseResultAsync(dbQueryToVerifyGraphQLResponse); - SqlTestHelper.PerformTestEqualJsonStrings(DbResponse, graphQLResponse.ToString()); - } - } - /// /// Do: Deletes book from the books table with given id /// Check: If the intended book is deleted from the DB and @@ -195,7 +146,7 @@ public async Task TestStoredProcedureMutationForDeletion(string dbQueryToVerifyD /// Do: updates a book title from the books table with given id /// and new title. /// Check: The book title should be updated with the given id - /// and the response is verified which contains the updated row. + /// DB is queried to verify the result. /// public async Task TestStoredProcedureMutationForUpdate(string dbQuery) { @@ -203,20 +154,17 @@ public async Task TestStoredProcedureMutationForUpdate(string dbQuery) string graphQLMutation = @" mutation { UpdateBookTitle(id: 14, title: ""Before Midnight"") { - id - title - publisher_id + result } } "; string beforeUpdate = await GetDatabaseResultAsync(dbQuery); - Console.Write(beforeUpdate); - JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true); + SqlTestHelper.PerformTestEqualJsonStrings("{\"id\":14,\"title\":\"Before Sunset\",\"publisher_id\":1234}", beforeUpdate); + JsonElement graphQLResponse = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true); + SqlTestHelper.PerformTestEqualJsonStrings("[]", graphQLResponse.ToString()); string afterUpdate = await GetDatabaseResultAsync(dbQuery); - List jsonList = JsonSerializer.Deserialize>(actual.ToString()); - Assert.AreEqual(1, jsonList.Count); - SqlTestHelper.PerformTestEqualJsonStrings(afterUpdate, JsonSerializer.Serialize(jsonList[0])); + SqlTestHelper.PerformTestEqualJsonStrings("{\"id\":14,\"title\":\"Before Midnight\",\"publisher_id\":1234}", afterUpdate); } /// diff --git a/src/Service.Tests/SqlTests/GraphQLMutationTests/MsSqlGraphQLMutationTests.cs b/src/Service.Tests/SqlTests/GraphQLMutationTests/MsSqlGraphQLMutationTests.cs index 9490432654..b1e90550dc 100644 --- a/src/Service.Tests/SqlTests/GraphQLMutationTests/MsSqlGraphQLMutationTests.cs +++ b/src/Service.Tests/SqlTests/GraphQLMutationTests/MsSqlGraphQLMutationTests.cs @@ -108,7 +108,7 @@ ORDER BY [id] asc /// Check: if the intended book is inserted in books table /// [TestMethod] - public async Task TestStoredProcedureMutationForInsertionWithNoReturns() + public async Task TestStoredProcedureMutationForInsertion() { string msSqlQuery = @" SELECT COUNT(*) AS [count] @@ -120,31 +120,7 @@ FROM [books] AS [table0] WITHOUT_ARRAY_WRAPPER "; - await TestStoredProcedureMutationForInsertionWithNoReturns(msSqlQuery); - } - - /// - /// Do: insert new Book and return all the rows from book table - /// Check: if the book is inserted and shows result to roles having read permission - /// - [DataTestMethod] - [DataRow("Anonymous", false, "NO Permission", DisplayName = "Simulator - Anonymous role does not have proper permissions.")] - [DataRow("Authenticated", true, "HAS Permission", DisplayName = "Simulator - Authenticated but Authenticated role does not have proper permissions.")] - public async Task TestStoredProcedureMutationForInsertionReturnWithPermission(string clientRole, bool isAuthenticated, string bookName) - { - string dbQueryToVerifyInsertion = @" - SELECT COUNT(*) AS [count] - FROM [books] AS [table0] - WHERE [table0].[title] = " + $"'{bookName}'" + @" - AND [table0].[publisher_id] = 1234 - FOR JSON PATH, - INCLUDE_NULL_VALUES, - WITHOUT_ARRAY_WRAPPER - "; - - string dbQueryToVerifyGraphQLResponse = $"SELECT id, title, publisher_id FROM books ORDER BY id asc FOR JSON PATH, INCLUDE_NULL_VALUES"; - - await TestStoredProcedureMutationForInsertionReturnWithPermission(clientRole, bookName, isAuthenticated, dbQueryToVerifyGraphQLResponse, dbQueryToVerifyInsertion); + await TestStoredProcedureMutationForInsertion(msSqlQuery); } /// diff --git a/src/Service.Tests/Unittests/ConfigValidationUnitTests.cs b/src/Service.Tests/Unittests/ConfigValidationUnitTests.cs index d9ecc9a663..854a46b00a 100644 --- a/src/Service.Tests/Unittests/ConfigValidationUnitTests.cs +++ b/src/Service.Tests/Unittests/ConfigValidationUnitTests.cs @@ -49,12 +49,12 @@ public void InaccessibleFieldRequestedByPolicy(string dbPolicy) } /// - /// Test method to validate that only 1 CUD operation along with READ is provided for a particular role. + /// Test method to validate that only 1 CRUD operation is supported for stored procedure. /// [DataTestMethod] - [DataRow(new object[] { "create", "read" }, true, DisplayName = "Stored-procedure with create-read permission")] - [DataRow(new object[] { "update", "read" }, true, DisplayName = "Stored-procedure with update-read permission")] - [DataRow(new object[] { "delete", "read" }, true, DisplayName = "Stored-procedure with delete-read permission")] + [DataRow(new object[] { "create", "read" }, false, DisplayName = "Stored-procedure with create-read permission")] + [DataRow(new object[] { "update", "read" }, false, DisplayName = "Stored-procedure with update-read permission")] + [DataRow(new object[] { "delete", "read" }, false, DisplayName = "Stored-procedure with delete-read permission")] [DataRow(new object[] { "create" }, true, DisplayName = "Stored-procedure with only create permission")] [DataRow(new object[] { "read" }, true, DisplayName = "Stored-procedure with only read permission")] [DataRow(new object[] { "update" }, true, DisplayName = "Stored-procedure with only update permission")] @@ -90,12 +90,16 @@ public void InvalidCRUDForStoredProcedure(object[] operations, bool isValid) runtimeConfig.Entities[AuthorizationHelpers.TEST_ENTITY] = testEntity; RuntimeConfigValidator configValidator = AuthenticationConfigValidatorUnitTests.GetMockConfigValidator(ref runtimeConfig); - if (!isValid) + try { - DataApiBuilderException ex = Assert.ThrowsException(() => - configValidator.ValidatePermissionsInConfig(runtimeConfig)); - Assert.AreEqual("Invalid OPerations for Entity: SampleEntity. " + - $"StoredProcedure can process only one CUD (Create/Update/Delete) operation.", ex.Message); + configValidator.ValidatePermissionsInConfig(runtimeConfig); + Assert.AreEqual(true, isValid); + } + catch (DataApiBuilderException ex) + { + Assert.AreEqual(false, isValid); + Assert.AreEqual("Invalid Operations for Entity: SampleEntity. " + + $"StoredProcedure can process only one CRUD (Create/Read/Update/Delete) operation.", ex.Message); Assert.AreEqual(HttpStatusCode.ServiceUnavailable, ex.StatusCode); Assert.AreEqual(DataApiBuilderException.SubStatusCodes.ConfigValidationError, ex.SubStatusCode); } diff --git a/src/Service/Configurations/RuntimeConfigValidator.cs b/src/Service/Configurations/RuntimeConfigValidator.cs index be165422a4..a41e70219f 100644 --- a/src/Service/Configurations/RuntimeConfigValidator.cs +++ b/src/Service/Configurations/RuntimeConfigValidator.cs @@ -438,18 +438,15 @@ public void ValidatePermissionsInConfig(RuntimeConfig runtimeConfig) operationsList.Add(actionOp); } - // READ operation is allowed with other CRUD operations. - operationsList.Remove(Operation.Read); - - // Apart from READ one of the CUD operations is allowed for stored procedure. + // Only one of the CRUD actions is allowed for stored procedure. if (entity.ObjectType is SourceType.StoredProcedure) { if ((operationsList.Count > 1) || (operationsList.Count is 1 && operationsList[0] is Operation.All)) { throw new DataApiBuilderException( - message: $"Invalid OPerations for Entity: {entityName}. " + - $"StoredProcedure can process only one CUD (Create/Update/Delete) operation.", + message: $"Invalid Operations for Entity: {entityName}. " + + $"StoredProcedure can process only one CRUD (Create/Read/Update/Delete) operation.", statusCode: System.Net.HttpStatusCode.ServiceUnavailable, subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError); } diff --git a/src/Service/MsSqlBooks.sql b/src/Service/MsSqlBooks.sql index 51cdec48ff..83d2bb1144 100644 --- a/src/Service/MsSqlBooks.sql +++ b/src/Service/MsSqlBooks.sql @@ -9,7 +9,6 @@ DROP PROCEDURE IF EXISTS get_book_by_id; DROP PROCEDURE IF EXISTS get_publisher_by_id; DROP PROCEDURE IF EXISTS insert_book; DROP PROCEDURE IF EXISTS count_books; -DROP PROCEDURE IF EXISTS insert_and_display_all_books; DROP PROCEDURE IF EXISTS delete_book; DROP PROCEDURE IF EXISTS update_book_title; DROP TABLE IF EXISTS book_author_link; @@ -314,11 +313,6 @@ EXEC('CREATE PROCEDURE insert_book @title varchar(max), @publisher_id int AS INSERT INTO dbo.books(title, publisher_id) VALUES (@title, @publisher_id)'); EXEC('CREATE PROCEDURE count_books AS SELECT COUNT(*) AS total_books FROM dbo.books'); -EXEC('CREATE PROCEDURE insert_and_display_all_books @title varchar(max), @publisher_id int AS - BEGIN - INSERT INTO dbo.books(title, publisher_id) VALUES (@title, @publisher_id) - SELECT * FROM dbo.books - END'); EXEC('CREATE PROCEDURE delete_book @id int AS DELETE FROM dbo.books WHERE id=@id'); EXEC('CREATE PROCEDURE update_book_title @id int, @title varchar(max) AS diff --git a/src/Service/Resolvers/SqlQueryEngine.cs b/src/Service/Resolvers/SqlQueryEngine.cs index 3ea020b35b..459187561c 100644 --- a/src/Service/Resolvers/SqlQueryEngine.cs +++ b/src/Service/Resolvers/SqlQueryEngine.cs @@ -117,12 +117,13 @@ public async Task, IMetadata>> ExecuteListAsync( else { SqlQueryStructure structure = new( - context, - parameters, - _sqlMetadataProvider, - _authorizationResolver, - _runtimeConfigProvider, - _gQLFilterParser); + context, + parameters, + _sqlMetadataProvider, + _authorizationResolver, + _runtimeConfigProvider, + _gQLFilterParser); + string queryString = _queryBuilder.Build(structure); _logger.LogInformation(queryString); List jsonListResult = From c364c36c4de2fd113229b6b3d36fb19069fdfe9f Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 29 Nov 2022 12:25:14 +0530 Subject: [PATCH 35/42] updating docs --- docs/views-and-stored-procedures.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/views-and-stored-procedures.md b/docs/views-and-stored-procedures.md index 2d6ebcb93d..525c17cdc8 100644 --- a/docs/views-and-stored-procedures.md +++ b/docs/views-and-stored-procedures.md @@ -71,7 +71,10 @@ the `dab-config.json` file will look like the following: The `parameters` object is optional, and is used to provide default values to be passed to the stored procedure parameters, if those are not provided in the HTTP request. -**ATTENTION**: Only the first result set returned by the stored procedure will be used by Data API Builder. +**ATTENTION**: +1. Only the first result set returned by the stored procedure will be used by Data API Builder. +2. Currently we only support simple Stored-Procedure,i.e. stored procedure that requires only 1 CRUD action to execute. +3. If more than one CRUD action is specified in the config, runtime initialization will fail due to config validation error. Please note that **you should configure the permission accordingly with the stored procedure behavior**. For example, if a Stored Procedure create a new item in the database, it is recommended to allow only the action `create` for such stored procedure. If, like in the sample, a stored procedure returns some data, it is recommended to allow only the action `read`. In general the recommendation is to align the allowed actions to what the stored procedure does, so to provide a consistent experience to the developer. From 44dabe8e1ffb0009137f080cc1b628f33373db6e Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 29 Nov 2022 14:28:31 +0530 Subject: [PATCH 36/42] updating stored-procedures --- .../GraphQLMutationTests/GraphQLMutationTestBase.cs | 8 ++++---- .../GraphQLMutationTests/MsSqlGraphQLMutationTests.cs | 6 +----- src/Service/MsSqlBooks.sql | 10 +++++++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs b/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs index 32b0f7eac0..9e0a4a5052 100644 --- a/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs +++ b/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs @@ -119,10 +119,10 @@ public async Task TestStoredProcedureMutationForInsertion(string dbQuery) /// public async Task TestStoredProcedureMutationForDeletion(string dbQueryToVerifyDeletion) { - string graphQLMutationName = "DeleteBook"; + string graphQLMutationName = "DeleteLastInsertedBook"; string graphQLMutation = @" mutation { - DeleteBook(id: 13) { + DeleteLastInsertedBook { result } } @@ -130,7 +130,7 @@ public async Task TestStoredProcedureMutationForDeletion(string dbQueryToVerifyD string currentDbResponse = await GetDatabaseResultAsync(dbQueryToVerifyDeletion); JsonDocument currentResult = JsonDocument.Parse(currentDbResponse); - Assert.AreEqual(currentResult.RootElement.GetProperty("count").GetInt64(), 1); + Assert.AreEqual(currentResult.RootElement.GetProperty("maxId").GetInt64(), 14); JsonElement graphQLResponse = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true); // Stored Procedure didn't return anything @@ -139,7 +139,7 @@ public async Task TestStoredProcedureMutationForDeletion(string dbQueryToVerifyD // check to verify new element is inserted string updatedDbResponse = await GetDatabaseResultAsync(dbQueryToVerifyDeletion); JsonDocument updatedResult = JsonDocument.Parse(updatedDbResponse); - Assert.AreEqual(updatedResult.RootElement.GetProperty("count").GetInt64(), 0); + Assert.AreEqual(updatedResult.RootElement.GetProperty("maxId").GetInt64(), 13); } /// diff --git a/src/Service.Tests/SqlTests/GraphQLMutationTests/MsSqlGraphQLMutationTests.cs b/src/Service.Tests/SqlTests/GraphQLMutationTests/MsSqlGraphQLMutationTests.cs index b1e90550dc..9699e3a6ee 100644 --- a/src/Service.Tests/SqlTests/GraphQLMutationTests/MsSqlGraphQLMutationTests.cs +++ b/src/Service.Tests/SqlTests/GraphQLMutationTests/MsSqlGraphQLMutationTests.cs @@ -131,12 +131,8 @@ FROM [books] AS [table0] public async Task TestStoredProcedureMutationForDeletion() { string dbQueryToVerifyDeletion = @" - SELECT COUNT(*) AS [count] + SELECT MAX(table0.id) AS [maxId] FROM [books] AS [table0] - WHERE - [table0].[id] = 13 - AND [table0].[title] = 'Before Sunrise' - AND [table0].[publisher_id] = 1234 FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER diff --git a/src/Service/MsSqlBooks.sql b/src/Service/MsSqlBooks.sql index 83d2bb1144..8d573bae25 100644 --- a/src/Service/MsSqlBooks.sql +++ b/src/Service/MsSqlBooks.sql @@ -9,7 +9,7 @@ DROP PROCEDURE IF EXISTS get_book_by_id; DROP PROCEDURE IF EXISTS get_publisher_by_id; DROP PROCEDURE IF EXISTS insert_book; DROP PROCEDURE IF EXISTS count_books; -DROP PROCEDURE IF EXISTS delete_book; +DROP PROCEDURE IF EXISTS delete_last_inserted_book; DROP PROCEDURE IF EXISTS update_book_title; DROP TABLE IF EXISTS book_author_link; DROP TABLE IF EXISTS reviews; @@ -313,8 +313,12 @@ EXEC('CREATE PROCEDURE insert_book @title varchar(max), @publisher_id int AS INSERT INTO dbo.books(title, publisher_id) VALUES (@title, @publisher_id)'); EXEC('CREATE PROCEDURE count_books AS SELECT COUNT(*) AS total_books FROM dbo.books'); -EXEC('CREATE PROCEDURE delete_book @id int AS - DELETE FROM dbo.books WHERE id=@id'); +EXEC('CREATE PROCEDURE delete_last_inserted_book AS + BEGIN + DELETE FROM dbo.books + WHERE + id = (select max(id) from dbo.books) + END'); EXEC('CREATE PROCEDURE update_book_title @id int, @title varchar(max) AS BEGIN UPDATE dbo.books SET title = @title WHERE id = @id From 62c80c02c95cf5628566fbd2d9473d2a0f413b8d Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 29 Nov 2022 14:33:12 +0530 Subject: [PATCH 37/42] fixing tests --- ConfigGenerators/MsSqlCommands.txt | 4 ++-- ConfigGenerators/dab-config.sql.reference.json | 7 ++----- docs/views-and-stored-procedures.md | 2 +- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/ConfigGenerators/MsSqlCommands.txt b/ConfigGenerators/MsSqlCommands.txt index 25deba7584..1cbd1b5763 100644 --- a/ConfigGenerators/MsSqlCommands.txt +++ b/ConfigGenerators/MsSqlCommands.txt @@ -28,7 +28,7 @@ add GetBook --config "dab-config.MsSql.json" --source "get_book_by_id" --source. add GetPublisher --config "dab-config.MsSql.json" --source "get_publisher_by_id" --source.type "stored-procedure" --source.params "id:1" --permissions "anonymous:read" --rest true --graphql true add InsertBook --config "dab-config.MsSql.json" --source "insert_book" --source.type "stored-procedure" --source.params "title:randomX,publisher_id:1234" --permissions "anonymous:create" --rest true --graphql true add CountBooks --config "dab-config.MsSql.json" --source "count_books" --source.type "stored-procedure" --permissions "anonymous:read" --rest true --graphql true -add DeleteBook --config "dab-config.MsSql.json" --source "delete_book" --source.type "stored-procedure" --source.params "id:1" --permissions "anonymous:delete" --rest true --graphql true +add DeleteLastInsertedBook --config "dab-config.MsSql.json" --source "delete_last_inserted_book" --source.type "stored-procedure" --permissions "anonymous:delete" --rest true --graphql true add UpdateBookTitle --config "dab-config.MsSql.json" --source "update_book_title" --source.type "stored-procedure" --source.params "id:1,title:Testing Tonight" --permissions "anonymous:update" --rest true --graphql true update Publisher --config "dab-config.MsSql.json" --permissions "authenticated:create,read,update,delete" --rest true --graphql true --relationship books --target.entity Book --cardinality many update Publisher --config "dab-config.MsSql.json" --permissions "policy_tester_01:create,delete" @@ -115,5 +115,5 @@ update GetPublisher --config "dab-config.MsSql.json" --permissions "authenticate update GetBooks --config "dab-config.MsSql.json" --permissions "authenticated:read" update InsertBook --config "dab-config.MsSql.json" --permissions "authenticated:create" update CountBooks --config "dab-config.MsSql.json" --permissions "authenticated:read" -update DeleteBook --config "dab-config.MsSql.json" --permissions "authenticated:delete" +update DeleteLastInsertedBook --config "dab-config.MsSql.json" --permissions "authenticated:delete" update UpdateBookTitle --config "dab-config.MsSql.json" --permissions "authenticated:update" diff --git a/ConfigGenerators/dab-config.sql.reference.json b/ConfigGenerators/dab-config.sql.reference.json index a24c65c8c5..e29e65babb 100644 --- a/ConfigGenerators/dab-config.sql.reference.json +++ b/ConfigGenerators/dab-config.sql.reference.json @@ -996,13 +996,10 @@ ], "graphql": true }, - "DeleteBook": { + "DeleteLastInsertedBook": { "source": { "type": "stored-procedure", - "object": "delete_book", - "parameters": { - "id": 1 - }, + "object": "delete_last_inserted_book", "key-fields": [] }, "rest": true, diff --git a/docs/views-and-stored-procedures.md b/docs/views-and-stored-procedures.md index 525c17cdc8..958b5038f2 100644 --- a/docs/views-and-stored-procedures.md +++ b/docs/views-and-stored-procedures.md @@ -73,7 +73,7 @@ The `parameters` object is optional, and is used to provide default values to be **ATTENTION**: 1. Only the first result set returned by the stored procedure will be used by Data API Builder. -2. Currently we only support simple Stored-Procedure,i.e. stored procedure that requires only 1 CRUD action to execute. +2. Currently we only support simple StoredProcedures,i.e. stored procedure that requires only 1 CRUD action to execute. 3. If more than one CRUD action is specified in the config, runtime initialization will fail due to config validation error. Please note that **you should configure the permission accordingly with the stored procedure behavior**. For example, if a Stored Procedure create a new item in the database, it is recommended to allow only the action `create` for such stored procedure. If, like in the sample, a stored procedure returns some data, it is recommended to allow only the action `read`. In general the recommendation is to align the allowed actions to what the stored procedure does, so to provide a consistent experience to the developer. From 6dbf26217c47e1ae8c72d4ef546c4d124cb3a1dc Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 29 Nov 2022 15:10:34 +0530 Subject: [PATCH 38/42] resolving merge conflicts --- ConfigGenerators/MsSqlCommands.txt | 2 + ConfigGenerators/MySqlCommands.txt | 2 + ConfigGenerators/PostgreSqlCommands.txt | 2 + .../dab-config.sql.reference.json | 23 +++++++ .../GraphQLMutationTestBase.cs | 51 +++++++++++++++ .../MsSqlGraphQLMutationTests.cs | 57 +++++++++++++++++ .../MySqlGraphQLMutationTests.cs | 63 +++++++++++++++++++ .../PostgreSqlGraphQLMutationTests.cs | 57 +++++++++++++++++ .../GraphQLQueryTests/GraphQLQueryTestBase.cs | 25 ++++++++ .../MsSqlGraphQLQueryTests.cs | 20 ++++++ .../MySqlGraphQLQueryTests.cs | 29 +++++++++ .../PostgreSqlGraphQLQueryTests.cs | 18 ++++++ src/Service/MsSqlBooks.sql | 15 +++++ src/Service/MySqlBooks.sql | 11 ++++ src/Service/PostgreSqlBooks.sql | 11 ++++ 15 files changed, 386 insertions(+) diff --git a/ConfigGenerators/MsSqlCommands.txt b/ConfigGenerators/MsSqlCommands.txt index 1cbd1b5763..6ad863ed0c 100644 --- a/ConfigGenerators/MsSqlCommands.txt +++ b/ConfigGenerators/MsSqlCommands.txt @@ -24,6 +24,7 @@ add Journal --config "dab-config.MsSql.json" --source "journals" --rest true --g add ArtOfWar --config "dab-config.MsSql.json" --source "aow" --rest true --permissions "anonymous:*" add series --config "dab-config.MsSql.json" --source "series" --permissions "anonymous:*" add GetBooks --config "dab-config.MsSql.json" --source "get_books" --source.type "stored-procedure" --permissions "anonymous:read" --rest true --graphql true +add Sales --config "dab-config.MsSql.json" --source "sales" --permissions "anonymous:*" --rest true --graphql true add GetBook --config "dab-config.MsSql.json" --source "get_book_by_id" --source.type "stored-procedure" --permissions "anonymous:read" --rest true --graphql false add GetPublisher --config "dab-config.MsSql.json" --source "get_publisher_by_id" --source.type "stored-procedure" --source.params "id:1" --permissions "anonymous:read" --rest true --graphql true add InsertBook --config "dab-config.MsSql.json" --source "insert_book" --source.type "stored-procedure" --source.params "title:randomX,publisher_id:1234" --permissions "anonymous:create" --rest true --graphql true @@ -117,3 +118,4 @@ update InsertBook --config "dab-config.MsSql.json" --permissions "authenticated: update CountBooks --config "dab-config.MsSql.json" --permissions "authenticated:read" update DeleteLastInsertedBook --config "dab-config.MsSql.json" --permissions "authenticated:delete" update UpdateBookTitle --config "dab-config.MsSql.json" --permissions "authenticated:update" +update Sales --config "dab-config.MsSql.json" --permissions "authenticated:*" diff --git a/ConfigGenerators/MySqlCommands.txt b/ConfigGenerators/MySqlCommands.txt index 02582ec25f..c7c4b3ea4e 100644 --- a/ConfigGenerators/MySqlCommands.txt +++ b/ConfigGenerators/MySqlCommands.txt @@ -23,6 +23,7 @@ add Notebook --config "dab-config.MySql.json" --source "notebooks" --permissions add Journal --config "dab-config.MySql.json" --source "journals" --rest true --graphql true --permissions "policy_tester_noupdate:create,delete" add ArtOfWar --config "dab-config.MySql.json" --source "aow" --rest true --permissions "anonymous:*" add series --config "dab-config.MySql.json" --source "series" --permissions "anonymous:*" +add Sales --config "dab-config.MySql.json" --source "sales" --permissions "anonymous:*" --rest true --graphql true update Publisher --config "dab-config.MySql.json" --permissions "authenticated:create,read,update,delete" --rest true --graphql true --relationship books --target.entity Book --cardinality many update Publisher --config "dab-config.MySql.json" --permissions "policy_tester_01:create,delete" update Publisher --config "dab-config.MySql.json" --permissions "policy_tester_01:update" --fields.include "*" @@ -103,3 +104,4 @@ update Journal --config "dab-config.MySql.json" --permissions "policy_tester_upd update Journal --config "dab-config.MySql.json" --permissions "policy_tester_update_noread:delete" --fields.include "*" --policy-database "@item.id eq 1" update Journal --config "dab-config.MySql.json" --permissions "authorizationHandlerTester:read" update ArtOfWar --config "dab-config.MySql.json" --permissions "authenticated:*" --map "DetailAssessmentAndPlanning:始計,WagingWar:作戰,StrategicAttack:謀攻,NoteNum:┬─┬ノ( º _ ºノ)" +update Sales --config "dab-config.MySql.json" --permissions "authenticated:*" diff --git a/ConfigGenerators/PostgreSqlCommands.txt b/ConfigGenerators/PostgreSqlCommands.txt index dc1d01367a..b79abf31a6 100644 --- a/ConfigGenerators/PostgreSqlCommands.txt +++ b/ConfigGenerators/PostgreSqlCommands.txt @@ -23,6 +23,7 @@ add Notebook --config "dab-config.PostgreSql.json" --source "notebooks" --permis add Journal --config "dab-config.PostgreSql.json" --source "journals" --rest true --graphql true --permissions "policy_tester_noupdate:create,delete" add ArtOfWar --config "dab-config.PostgreSql.json" --source "aow" --rest true --permissions "anonymous:*" add series --config "dab-config.PostgreSql.json" --source "series" --permissions "anonymous:*" +add Sales --config "dab-config.PostgreSql.json" --source "sales" --permissions "anonymous:*" --rest true --graphql true update Publisher --config "dab-config.PostgreSql.json" --permissions "authenticated:create,read,update,delete" --rest true --graphql true --relationship books --target.entity Book --cardinality many update Publisher --config "dab-config.PostgreSql.json" --permissions "policy_tester_01:create,delete" update Publisher --config "dab-config.PostgreSql.json" --permissions "policy_tester_01:update" --fields.include "*" @@ -100,3 +101,4 @@ update Journal --config "dab-config.PostgreSql.json" --permissions "policy_teste update Journal --config "dab-config.PostgreSql.json" --permissions "policy_tester_update_noread:delete" --fields.include "*" --policy-database "@item.id eq 1" update Journal --config "dab-config.PostgreSql.json" --permissions "authorizationHandlerTester:read" update ArtOfWar --config "dab-config.PostgreSql.json" --permissions "authenticated:*" --map "DetailAssessmentAndPlanning:始計,WagingWar:作戰,StrategicAttack:謀攻,NoteNum:┬─┬ノ( º _ ºノ)" +update Sales --config "dab-config.PostgreSql.json" --permissions "authenticated:*" diff --git a/ConfigGenerators/dab-config.sql.reference.json b/ConfigGenerators/dab-config.sql.reference.json index e29e65babb..04fed2efa5 100644 --- a/ConfigGenerators/dab-config.sql.reference.json +++ b/ConfigGenerators/dab-config.sql.reference.json @@ -946,6 +946,29 @@ } ] }, + "Sales": { + "source": { + "type": "table", + "object": "sales", + "key-fields": [] + }, + "rest": true, + "graphql": true, + "permissions": [ + { + "role": "anonymous", + "actions": [ + "*" + ] + }, + { + "role": "authenticated", + "actions": [ + "*" + ] + } + ] + }, "InsertBook": { "source": { "type": "stored-procedure", diff --git a/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs b/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs index 9e0a4a5052..00d64e1a33 100644 --- a/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs +++ b/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs @@ -59,6 +59,32 @@ public async Task InsertMutationWithVariables(string dbQuery) SqlTestHelper.PerformTestEqualJsonStrings(expected, actual.ToString()); } + /// + /// Do: Inserts new sale item into sales table that automatically calculates the total price + /// based on subtotal and tax. + /// Check: Calculated column is persisted successfully with correct calculated result. + /// + public async Task InsertMutationForComputedColumns(string dbQuery) + { + string graphQLMutationName = "createSales"; + string graphQLMutation = @" + mutation{ + createSales(item: {item_name: ""headphones"", subtotal: 195.00, tax: 10.33}) { + id + item_name + subtotal + tax + total + } + } + "; + + JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true); + string expected = await GetDatabaseResultAsync(dbQuery); + + SqlTestHelper.PerformTestEqualJsonStrings(expected, actual.ToString()); + } + /// /// Do: Inserts new review with default content for a Review and return its id and content /// Check: If book with the given id is present in the database then @@ -214,6 +240,31 @@ public async Task UpdateMutation(string dbQuery) SqlTestHelper.PerformTestEqualJsonStrings(expected, actual.ToString()); } + /// + /// Do: Update Sales in database and return its updated fields + /// Check: The calculated column has successfully been updated after updating the other fields + /// + public async Task UpdateMutationForComputedColumns(string dbQuery) + { + string graphQLMutationName = "updateSales"; + string graphQLMutation = @" + mutation{ + updateSales(id: 2, item: {item_name: ""phone"", subtotal: 495.00, tax: 30.33}) { + id + item_name + subtotal + tax + total + } + } + "; + + JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true); + string expected = await GetDatabaseResultAsync(dbQuery); + + SqlTestHelper.PerformTestEqualJsonStrings(expected, actual.ToString()); + } + /// /// Do: Delete book by id /// Check: if the mutation returned result is as expected and if book by that id has been deleted diff --git a/src/Service.Tests/SqlTests/GraphQLMutationTests/MsSqlGraphQLMutationTests.cs b/src/Service.Tests/SqlTests/GraphQLMutationTests/MsSqlGraphQLMutationTests.cs index 9699e3a6ee..330a3fcc6c 100644 --- a/src/Service.Tests/SqlTests/GraphQLMutationTests/MsSqlGraphQLMutationTests.cs +++ b/src/Service.Tests/SqlTests/GraphQLMutationTests/MsSqlGraphQLMutationTests.cs @@ -79,6 +79,35 @@ ORDER BY [id] asc await InsertMutationWithVariables(msSqlQuery); } + /// + /// Do: Inserts new sale item into sales table that automatically calculates the total price + /// based on subtotal and tax. + /// Check: Calculated column is persisted successfully with correct calculated result. + /// + [TestMethod] + public async Task InsertMutationForComputedColumns() + { + string msSqlQuery = @" + SELECT TOP 1 [table0].[id] AS [id], + [table0].[item_name] AS [item_name], + [table0].[subtotal] AS [subtotal], + [table0].[tax] AS [tax], + [table0].[total] AS [total] + FROM [sales] AS [table0] + WHERE [table0].[id] = 5001 + AND [table0].[item_name] = 'headphones' + AND [table0].[subtotal] = 195.00 + AND [table0].[tax] = 10.33 + AND [table0].[total] = 205.33 + ORDER BY [id] asc + FOR JSON PATH, + INCLUDE_NULL_VALUES, + WITHOUT_ARRAY_WRAPPER + "; + + await InsertMutationForComputedColumns(msSqlQuery); + } + /// /// Do: Inserts new review with default content for a Review and return its id and content /// Check: If book with the given id is present in the database then @@ -205,6 +234,34 @@ ORDER BY [books].[id] asc await UpdateMutation(msSqlQuery); } + /// + /// Do: Update Sales in database and return its updated fields + /// Check: The calculated column has successfully been updated after updating the other fields + /// + [TestMethod] + public async Task UpdateMutationForComputedColumns() + { + string msSqlQuery = @" + SELECT TOP 1 [table0].[id] AS [id], + [table0].[item_name] AS [item_name], + [table0].[subtotal] AS [subtotal], + [table0].[tax] AS [tax], + [table0].[total] AS [total] + FROM [sales] AS [table0] + WHERE [table0].[id] = 2 + AND [table0].[item_name] = 'phone' + AND [table0].[subtotal] = 495.00 + AND [table0].[tax] = 30.33 + AND [table0].[total] = 525.33 + ORDER BY [id] asc + FOR JSON PATH, + INCLUDE_NULL_VALUES, + WITHOUT_ARRAY_WRAPPER + "; + + await UpdateMutationForComputedColumns(msSqlQuery); + } + /// /// Do: Delete book by id /// Check: if the mutation returned result is as expected and if book by that id has been deleted diff --git a/src/Service.Tests/SqlTests/GraphQLMutationTests/MySqlGraphQLMutationTests.cs b/src/Service.Tests/SqlTests/GraphQLMutationTests/MySqlGraphQLMutationTests.cs index 1736e201ac..43e05ac392 100644 --- a/src/Service.Tests/SqlTests/GraphQLMutationTests/MySqlGraphQLMutationTests.cs +++ b/src/Service.Tests/SqlTests/GraphQLMutationTests/MySqlGraphQLMutationTests.cs @@ -60,6 +60,39 @@ ORDER BY `id` asc LIMIT 1 await InsertMutation(mySqlQuery); } + /// + /// Do: Inserts a new sale item into the sales table that automatically calculates the total price + /// based on subtotal and tax. + /// Check: Calculated column is persisted successfully with correct calculated result. + /// + [TestMethod] + public async Task InsertMutationForComputedColumns() + { + string mySqlQuery = @" + SELECT JSON_OBJECT( + 'id', `subq`.`id`, 'item_name', `subq`.`item_name`, + 'subtotal', `subq`.`subtotal`, 'tax', `subq`.`tax`, + 'total', `subq`.`total` + ) AS `data` + FROM ( + SELECT `table0`.`id` AS `id`, + `table0`.`item_name` AS `item_name`, + `table0`.`subtotal` AS `subtotal`, + `table0`.`tax` AS `tax`, + `table0`.`total` AS `total` + FROM `sales` AS `table0` + WHERE `id` = 5001 + AND `item_name` = 'headphones' + AND `subtotal` = 195.00 + AND `tax` = 10.33 + AND `total` = 205.33 + ORDER BY `id` asc LIMIT 1 + ) AS `subq` + "; + + await InsertMutationForComputedColumns(mySqlQuery); + } + /// /// Do: Inserts new book using variables to set its title and publisher_id /// Check: If book with the expected values of the new book is present in the database and @@ -149,6 +182,36 @@ ORDER BY `table0`.`id` asc LIMIT 1 await UpdateMutation(mySqlQuery); } + /// + /// Do: Update Sales in database and return its updated fields + /// Check: The calculated column has successfully been updated after updating the other fields + /// + [TestMethod] + // IGNORE FOR NOW, SEE: Issue #1001 + [Ignore] + public async Task UpdateMutationForComputedColumns() + { + string mySqlQuery = @" + SELECT JSON_OBJECT( + 'id', `subq2`.`id`, 'item_name', `subq2`.`item_name`, + 'subtotal', `subq2`.`subtotal`, 'tax', `subq2`.`tax`, + 'total', `subq2`.`total` + ) AS `data` + FROM ( + SELECT `table0`.`id` AS `id`, + `table0`.`item_name` AS `item_name`, + `table0`.`subtotal` AS `subtotal`, + `table0`.`tax` AS `tax`, + `table0`.`total` AS `total` + FROM `sales` AS `table0` + WHERE `id` = 2 + ORDER BY `id` asc LIMIT 1 + ) AS `subq2` + "; + + await UpdateMutationForComputedColumns(mySqlQuery); + } + /// /// Do: Delete book by id /// Check: if the mutation returned result is as expected and if book by that id has been deleted diff --git a/src/Service.Tests/SqlTests/GraphQLMutationTests/PostgreSqlGraphQLMutationTests.cs b/src/Service.Tests/SqlTests/GraphQLMutationTests/PostgreSqlGraphQLMutationTests.cs index f9a54367bf..740a1e0abc 100644 --- a/src/Service.Tests/SqlTests/GraphQLMutationTests/PostgreSqlGraphQLMutationTests.cs +++ b/src/Service.Tests/SqlTests/GraphQLMutationTests/PostgreSqlGraphQLMutationTests.cs @@ -59,6 +59,35 @@ ORDER BY id asc await InsertMutation(postgresQuery); } + /// + /// Do: Inserts new sale item into sales table that automatically calculates the total price + /// based on subtotal and tax. + /// Check: Calculated column is persisted successfully with correct calculated result. + /// + [TestMethod] + public async Task InsertMutationForComputedColumns() + { + string postgresQuery = @" + SELECT to_jsonb(subq) AS DATA + FROM + (SELECT table0.id AS id, + table0.item_name AS item_name, + table0.subtotal AS subtotal, + table0.tax AS tax, + table0.total AS total + FROM sales AS table0 + WHERE id = 5001 + AND item_name = 'headphones' + AND subtotal = 195.00 + AND tax = 10.33 + AND total = 205.33 + ORDER BY id asc + LIMIT 1) AS subq + "; + + await InsertMutationForComputedColumns(postgresQuery); + } + /// /// Do: Inserts new book using variables to set its title and publisher_id /// Check: If book with the expected values of the new book is present in the database and @@ -152,6 +181,34 @@ ORDER BY id asc await UpdateMutation(postgresQuery); } + /// + /// Do: Update Sales in database and return its updated fields + /// Check: The calculated column has successfully been updated after updating the other fields + /// + [TestMethod] + public async Task UpdateMutationForComputedColumns() + { + string postgresQuery = @" + SELECT to_jsonb(subq) AS DATA + FROM + (SELECT table0.id AS id, + table0.item_name AS item_name, + table0.subtotal AS subtotal, + table0.tax AS tax, + table0.total AS total + FROM sales AS table0 + WHERE id = 2 + AND item_name = 'phone' + AND subtotal = 495.00 + AND tax = 30.33 + AND total = 525.33 + ORDER BY id asc + LIMIT 1) AS subq + "; + + await UpdateMutationForComputedColumns(postgresQuery); + } + /// /// Do: Delete book by id /// Check: if the mutation returned result is as expected and if book by that id has been deleted diff --git a/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs b/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs index c6ebedd238..6d30970019 100644 --- a/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs +++ b/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs @@ -54,6 +54,31 @@ public async Task MultipleResultQueryWithVariables(string dbQuery) SqlTestHelper.PerformTestEqualJsonStrings(expected, actual.GetProperty("items").ToString()); } + /// + /// Gets array of results for querying a table containing computed columns. + /// + /// rows from sales table + public async Task MultipleResultQueryContainingComputedColumns(string dbQuery) + { + string graphQLQueryName = "sales"; + string graphQLQuery = @"{ + sales(first: 10) { + items { + id + item_name + subtotal + tax + total + } + } + }"; + + JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLQuery, graphQLQueryName, isAuthenticated: false); + string expected = await GetDatabaseResultAsync(dbQuery); + + SqlTestHelper.PerformTestEqualJsonStrings(expected, actual.GetProperty("items").ToString()); + } + /// /// Gets array of results for querying more than one item. /// diff --git a/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLQueryTests.cs b/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLQueryTests.cs index 3a57c19aca..d943315c5e 100644 --- a/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLQueryTests.cs +++ b/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLQueryTests.cs @@ -31,6 +31,26 @@ public async Task MultipleResultQuery() await MultipleResultQuery(msSqlQuery); } + /// + /// Gets array of results for querying a table containing computed columns. + /// + /// rows from sales table + [TestMethod] + public async Task MultipleResultQueryContainingComputedColumns() + { + string msSqlQuery = @" + SELECT + id, + item_name, + ROUND(subtotal,2) AS subtotal, + ROUND(tax,2) AS tax, + ROUND(total,2) AS total + FROM + sales + ORDER BY id asc FOR JSON PATH, INCLUDE_NULL_VALUES"; + await MultipleResultQueryContainingComputedColumns(msSqlQuery); + } + [TestMethod] public async Task MultipleResultQueryWithVariables() { diff --git a/src/Service.Tests/SqlTests/GraphQLQueryTests/MySqlGraphQLQueryTests.cs b/src/Service.Tests/SqlTests/GraphQLQueryTests/MySqlGraphQLQueryTests.cs index 2935396bd1..d23f8890fc 100644 --- a/src/Service.Tests/SqlTests/GraphQLQueryTests/MySqlGraphQLQueryTests.cs +++ b/src/Service.Tests/SqlTests/GraphQLQueryTests/MySqlGraphQLQueryTests.cs @@ -34,6 +34,35 @@ ORDER BY `table0`.`id` asc await MultipleResultQuery(mySqlQuery); } + /// + /// Gets array of results for querying a table containing computed columns. + /// + /// rows from sales table + [TestMethod] + public async Task MultipleResultQueryContainingComputedColumns() + { + string mySqlQuery = @" + SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT( + 'id', `subq1`.`id`, + 'item_name', `subq1`.`item_name`, + 'subtotal', `subq1`.`subtotal`, + 'tax', `subq1`.`tax`, + 'total', `subq1`.`total` + )), '[]') AS `data` + FROM + (SELECT `table0`.`id` AS `id`, + `table0`.`item_name` AS `item_name`, + `table0`.`subtotal` AS `subtotal`, + `table0`.`tax` AS `tax`, + `table0`.`total` AS `total` + FROM `sales` AS `table0` + WHERE 1 = 1 + ORDER BY `table0`.`id` asc + LIMIT 100) AS `subq1`"; + + await MultipleResultQueryContainingComputedColumns(mySqlQuery); + } + [TestMethod] public async Task MultipleResultQueryWithVariables() { diff --git a/src/Service.Tests/SqlTests/GraphQLQueryTests/PostgreSqlGraphQLQueryTests.cs b/src/Service.Tests/SqlTests/GraphQLQueryTests/PostgreSqlGraphQLQueryTests.cs index 95248b98aa..df3030379b 100644 --- a/src/Service.Tests/SqlTests/GraphQLQueryTests/PostgreSqlGraphQLQueryTests.cs +++ b/src/Service.Tests/SqlTests/GraphQLQueryTests/PostgreSqlGraphQLQueryTests.cs @@ -25,6 +25,24 @@ public async Task MultipleResultQuery() await MultipleResultQuery(postgresQuery); } + /// + /// Gets array of results for querying a table containing computed columns. + /// + /// rows from sales table + [TestMethod] + public async Task MultipleResultQueryContainingComputedColumns() + { + string postgresQuery = @"SELECT json_agg(to_jsonb(table0)) FROM + (SELECT + id, + item_name, + subtotal, + tax, + total + FROM sales ORDER BY id asc LIMIT 100) as table0"; + await MultipleResultQueryContainingComputedColumns(postgresQuery); + } + [TestMethod] public async Task MultipleResultQueryWithVariables() { diff --git a/src/Service/MsSqlBooks.sql b/src/Service/MsSqlBooks.sql index 8d573bae25..d964957f6f 100644 --- a/src/Service/MsSqlBooks.sql +++ b/src/Service/MsSqlBooks.sql @@ -31,6 +31,7 @@ DROP TABLE IF EXISTS notebooks; DROP TABLE IF EXISTS journals; DROP TABLE IF EXISTS aow; DROP TABLE IF EXISTS series; +DROP TABLE IF EXISTS sales; DROP SCHEMA IF EXISTS [foo]; COMMIT; @@ -176,6 +177,13 @@ CREATE TABLE series ( [name] nvarchar(1000) NOT NULL ); +CREATE TABLE sales ( + id int NOT NULL IDENTITY(5001, 1) PRIMARY KEY, + item_name varchar(max) NOT NULL, + subtotal decimal(18,2) NOT NULL, + tax decimal(18,2) NOT NULL +); + ALTER TABLE books ADD CONSTRAINT book_publisher_fk FOREIGN KEY (publisher_id) @@ -224,6 +232,9 @@ FOREIGN KEY (series_id) REFERENCES series(id) ON DELETE CASCADE; +ALTER TABLE sales +ADD total AS (subtotal + tax) PERSISTED; + SET IDENTITY_INSERT publishers ON INSERT INTO publishers(id, name) VALUES (1234, 'Big Company'), (2345, 'Small Town Publisher'), (2323, 'TBD Publishing One'), (2324, 'TBD Publishing Two Ltd'), (1940, 'Policy Publisher 01'), (1941, 'Policy Publisher 02'); SET IDENTITY_INSERT publishers OFF @@ -269,6 +280,10 @@ INSERT INTO type_table(id, byte_types, short_types, int_types, long_types, strin (5, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); SET IDENTITY_INSERT type_table OFF +SET IDENTITY_INSERT sales ON +INSERT INTO sales(id, item_name, subtotal, tax) VALUES (1, 'Watch', 249.00, 20.59), (2, 'Montior', 120.50, 11.12); +SET IDENTITY_INSERT sales OFF + INSERT INTO notebooks(id, notebookname, color, ownername) VALUES (1, 'Notebook1', 'red', 'Sean'), (2, 'Notebook2', 'green', 'Ani'), (3, 'Notebook3', 'blue', 'Jarupat'), (4, 'Notebook4', 'yellow', 'Aaron'); INSERT INTO journals(id, journalname, color, ownername) VALUES (1, 'Journal1', 'red', 'Sean'), (2, 'Journal2', 'green', 'Ani'), (3, 'Journal3', 'blue', 'Jarupat'), (4, 'Journal4', 'yellow', 'Aaron'); diff --git a/src/Service/MySqlBooks.sql b/src/Service/MySqlBooks.sql index 7611062087..27dc2d9e49 100644 --- a/src/Service/MySqlBooks.sql +++ b/src/Service/MySqlBooks.sql @@ -23,6 +23,7 @@ DROP TABLE IF EXISTS notebooks; DROP TABLE IF EXISTS journals; DROP TABLE IF EXISTS aow; DROP TABLE IF EXISTS series; +DROP TABLE IF EXISTS sales; CREATE TABLE publishers( @@ -162,6 +163,14 @@ CREATE TABLE series ( name text NOT NULL ); +CREATE TABLE sales ( + id int AUTO_INCREMENT PRIMARY KEY, + item_name text NOT NULL, + subtotal decimal(18,2) NOT NULL, + tax decimal(18,2) NOT NULL, + total decimal(18,2) generated always as (subtotal + tax) stored +); + ALTER TABLE books ADD CONSTRAINT book_publisher_fk FOREIGN KEY (publisher_id) @@ -251,6 +260,7 @@ INSERT INTO notebooks(id, notebookname, color, ownername) VALUES (1, 'Notebook1' INSERT INTO journals(id, journalname, color, ownername) VALUES (1, 'Journal1', 'red', 'Sean'), (2, 'Journal2', 'green', 'Ani'), (3, 'Journal3', 'blue', 'Jarupat'), (4, 'Journal4', 'yellow', 'Aaron'); INSERT INTO aow(NoteNum, DetailAssessmentAndPlanning, WagingWar, StrategicAttack) VALUES (1, 'chapter one notes: ', 'chapter two notes: ', 'chapter three notes: '); +INSERT INTO sales(id, item_name, subtotal, tax) VALUES (1, 'Watch', 249.00, 20.59), (2, 'Montior', 120.50, 11.12); -- Starting with id > 5000 is chosen arbitrarily so that the incremented id-s won't conflict with the manually inserted ids in this script -- AUTO_INCREMENT is set to 5001 so the next autogenerated id will be 5001 @@ -261,6 +271,7 @@ ALTER TABLE authors AUTO_INCREMENT = 5001; ALTER TABLE reviews AUTO_INCREMENT = 5001; ALTER TABLE comics AUTO_INCREMENT = 5001; ALTER TABLE type_table AUTO_INCREMENT = 5001; +ALTER TABLE sales AUTO_INCREMENT = 5001; prepare stmt1 from 'CREATE VIEW books_view_all AS SELECT * FROM books'; diff --git a/src/Service/PostgreSqlBooks.sql b/src/Service/PostgreSqlBooks.sql index 8c0330e887..88363e9d01 100644 --- a/src/Service/PostgreSqlBooks.sql +++ b/src/Service/PostgreSqlBooks.sql @@ -23,6 +23,7 @@ DROP TABLE IF EXISTS aow; DROP TABLE IF EXISTS notebooks; DROP TABLE IF EXISTS journals; DROP TABLE IF EXISTS series; +DROP TABLE IF EXISTS sales; DROP FUNCTION IF EXISTS insertCompositeView; DROP SCHEMA IF EXISTS foo; @@ -166,6 +167,14 @@ CREATE TABLE series ( name text NOT NULL ); +CREATE TABLE sales ( + id int GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + item_name text NOT NULL, + subtotal float NOT NULL, + tax float NOT NULL, + total float generated always as (subtotal + tax) stored +); + ALTER TABLE books ADD CONSTRAINT book_publisher_fk FOREIGN KEY (publisher_id) @@ -254,6 +263,7 @@ INSERT INTO notebooks(id, noteBookName, color, ownerName) VALUES (1, 'Notebook1' INSERT INTO journals(id, journalname, color, ownername) VALUES (1, 'Journal1', 'red', 'Sean'), (2, 'Journal2', 'green', 'Ani'), (3, 'Journal3', 'blue', 'Jarupat'), (4, 'Journal4', 'yellow', 'Aaron'); INSERT INTO aow("NoteNum", "DetailAssessmentAndPlanning", "WagingWar", "StrategicAttack") VALUES (1, 'chapter one notes: ', 'chapter two notes: ', 'chapter three notes: '); +INSERT INTO sales(id, item_name, subtotal, tax) VALUES (1, 'Watch', 249.00, 20.59), (2, 'Montior', 120.50, 11.12); --Starting with id > 5000 is chosen arbitrarily so that the incremented id-s won't conflict with the manually inserted ids in this script --Sequence counter is set to 5000 so the next autogenerated id will be 5001 @@ -263,6 +273,7 @@ SELECT setval('publishers_id_seq', 5000); SELECT setval('authors_id_seq', 5000); SELECT setval('reviews_id_seq', 5000); SELECT setval('type_table_id_seq', 5000); +SELECT setval('sales_id_seq', 5000); CREATE VIEW books_view_all AS SELECT * FROM books; CREATE VIEW books_view_with_mapping AS SELECT * FROM books; From fbb329b7326c078a9a3386452bf215b92835c6ae Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 29 Nov 2022 15:43:01 +0530 Subject: [PATCH 39/42] fix formatting --- src/Config/DatabaseObject.cs | 30 +++++++++---------- .../Sql/SchemaConverter.cs | 2 +- .../MetadataProviders/SqlMetadataProvider.cs | 2 +- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Config/DatabaseObject.cs b/src/Config/DatabaseObject.cs index 42d2b9fc23..6ce66bd06b 100644 --- a/src/Config/DatabaseObject.cs +++ b/src/Config/DatabaseObject.cs @@ -43,6 +43,21 @@ public override int GetHashCode() { return HashCode.Combine(SchemaName, Name); } + + /// + /// Get the underlying SourceDefinition based on database object source type + /// + public static SourceDefinition GetSourceDefinitionForDatabaseObject(DatabaseObject databaseObject) + { + return databaseObject.SourceType switch + { + SourceType.Table => ((DatabaseTable)databaseObject).TableDefinition, + SourceType.View => ((DatabaseView)databaseObject).ViewDefinition, + SourceType.StoredProcedure => ((DatabaseStoredProcedure)databaseObject).StoredProcedureDefinition, + _ => throw new Exception( + message: $"Unsupported SourceType. It can either be Table,View, or Stored Procedure.") + }; + } } /// @@ -132,21 +147,6 @@ public bool IsAnyColumnNullable(List columnsToCheck) .Where(isNullable => isNullable == true) .Any(); } - - /// - /// Get the underlying SourceDefinition based on database object source type - /// - public static SourceDefinition GetSourceDefinitionForDatabaseObject(DatabaseObject databaseObject) - { - return databaseObject.SourceType switch - { - SourceType.Table => ((DatabaseTable)databaseObject).TableDefinition, - SourceType.View => ((DatabaseView)databaseObject).ViewDefinition, - SourceType.StoredProcedure => ((DatabaseStoredProcedure)databaseObject).StoredProcedureDefinition, - _ => throw new Exception( - message: $"Unsupported SourceType. It can either be Table,View, or Stored Procedure.") - }; - } } /// diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs index 70dc13053b..882dcb2a92 100644 --- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -37,7 +37,7 @@ public static ObjectTypeDefinitionNode FromDatabaseObject( { Dictionary fields = new(); List objectTypeDirectives = new(); - SourceDefinition sourceDefinition = SourceDefinition.GetSourceDefinitionForDatabaseObject(databaseObject); + SourceDefinition sourceDefinition = DatabaseObject.GetSourceDefinitionForDatabaseObject(databaseObject); NameNode nameNode = new(value: GetDefinedSingularName(entityName, configEntity)); // When the result set is not defined, it could be a mutation operation with no returning columns diff --git a/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs index 65135a094f..b1594f8f7e 100644 --- a/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs @@ -150,7 +150,7 @@ public SourceDefinition GetSourceDefinition(string entityName) subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound); } - return SourceDefinition.GetSourceDefinitionForDatabaseObject(databaseObject); + return DatabaseObject.GetSourceDefinitionForDatabaseObject(databaseObject); } /// From eca1846b8db20b48cec0fb575e8954b02ac01f28 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 29 Nov 2022 15:52:22 +0530 Subject: [PATCH 40/42] resolving comments --- src/Cli/src/Utils.cs | 2 +- src/Service.GraphQLBuilder/GraphQLUtils.cs | 2 +- src/Service/Configurations/RuntimeConfigValidator.cs | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Cli/src/Utils.cs b/src/Cli/src/Utils.cs index 7a75a927c8..e9f105192b 100644 --- a/src/Cli/src/Utils.cs +++ b/src/Cli/src/Utils.cs @@ -645,7 +645,7 @@ private static object ParseStringValue(string stringValue) { return floatingValue; } - else if (Boolean.TryParse(stringValue, out bool booleanValue)) + else if (bool.TryParse(stringValue, out bool booleanValue)) { return booleanValue; } diff --git a/src/Service.GraphQLBuilder/GraphQLUtils.cs b/src/Service.GraphQLBuilder/GraphQLUtils.cs index 2141027c4a..48715f9293 100644 --- a/src/Service.GraphQLBuilder/GraphQLUtils.cs +++ b/src/Service.GraphQLBuilder/GraphQLUtils.cs @@ -218,7 +218,7 @@ public static Tuple GetGraphQLTypeAndNodeTypeFromStringValue { return new(FLOAT_TYPE, new FloatValueNode(floatingValue)); } - else if (Boolean.TryParse(stringValue, out bool booleanValue)) + else if (bool.TryParse(stringValue, out bool booleanValue)) { return new(BOOLEAN_TYPE, new BooleanValueNode(booleanValue)); } diff --git a/src/Service/Configurations/RuntimeConfigValidator.cs b/src/Service/Configurations/RuntimeConfigValidator.cs index a41e70219f..2483b27e6f 100644 --- a/src/Service/Configurations/RuntimeConfigValidator.cs +++ b/src/Service/Configurations/RuntimeConfigValidator.cs @@ -445,10 +445,10 @@ public void ValidatePermissionsInConfig(RuntimeConfig runtimeConfig) || (operationsList.Count is 1 && operationsList[0] is Operation.All)) { throw new DataApiBuilderException( - message: $"Invalid Operations for Entity: {entityName}. " + - $"StoredProcedure can process only one CRUD (Create/Read/Update/Delete) operation.", - statusCode: System.Net.HttpStatusCode.ServiceUnavailable, - subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError); + message: $"Invalid Operations for Entity: {entityName}. " + + $"StoredProcedure can process only one CRUD (Create/Read/Update/Delete) operation.", + statusCode: System.Net.HttpStatusCode.ServiceUnavailable, + subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError); } } } From 0a5e200e3e6ece7a09b776c153eeb543555cb2b7 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 29 Nov 2022 16:13:27 +0530 Subject: [PATCH 41/42] moved GetSourcedefinition to DatabaseObject class --- src/Config/DatabaseObject.cs | 19 +++++++++++-------- .../Sql/SchemaConverter.cs | 2 +- .../MetadataProviders/SqlMetadataProvider.cs | 2 +- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/Config/DatabaseObject.cs b/src/Config/DatabaseObject.cs index 6ce66bd06b..8724bd1f76 100644 --- a/src/Config/DatabaseObject.cs +++ b/src/Config/DatabaseObject.cs @@ -47,16 +47,19 @@ public override int GetHashCode() /// /// Get the underlying SourceDefinition based on database object source type /// - public static SourceDefinition GetSourceDefinitionForDatabaseObject(DatabaseObject databaseObject) + public SourceDefinition SourceDefinition { - return databaseObject.SourceType switch + get { - SourceType.Table => ((DatabaseTable)databaseObject).TableDefinition, - SourceType.View => ((DatabaseView)databaseObject).ViewDefinition, - SourceType.StoredProcedure => ((DatabaseStoredProcedure)databaseObject).StoredProcedureDefinition, - _ => throw new Exception( - message: $"Unsupported SourceType. It can either be Table,View, or Stored Procedure.") - }; + return SourceType switch + { + SourceType.Table => ((DatabaseTable)this).TableDefinition, + SourceType.View => ((DatabaseView)this).ViewDefinition, + SourceType.StoredProcedure => ((DatabaseStoredProcedure)this).StoredProcedureDefinition, + _ => throw new Exception( + message: $"Unsupported SourceType. It can either be Table,View, or Stored Procedure.") + }; + } } } diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs index 882dcb2a92..bb568eeda7 100644 --- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -37,7 +37,7 @@ public static ObjectTypeDefinitionNode FromDatabaseObject( { Dictionary fields = new(); List objectTypeDirectives = new(); - SourceDefinition sourceDefinition = DatabaseObject.GetSourceDefinitionForDatabaseObject(databaseObject); + SourceDefinition sourceDefinition = databaseObject.SourceDefinition; NameNode nameNode = new(value: GetDefinedSingularName(entityName, configEntity)); // When the result set is not defined, it could be a mutation operation with no returning columns diff --git a/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs index b1594f8f7e..8827d8aba3 100644 --- a/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs @@ -150,7 +150,7 @@ public SourceDefinition GetSourceDefinition(string entityName) subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound); } - return DatabaseObject.GetSourceDefinitionForDatabaseObject(databaseObject); + return databaseObject.SourceDefinition; } /// From 961885f3b1468e107fdd40d1a8c6933ef518525b Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 29 Nov 2022 17:45:22 +0530 Subject: [PATCH 42/42] resolved nits --- src/Service.GraphQLBuilder/GraphQLStoredProcedureBuilder.cs | 4 ++-- src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Service.GraphQLBuilder/GraphQLStoredProcedureBuilder.cs b/src/Service.GraphQLBuilder/GraphQLStoredProcedureBuilder.cs index 7b7ad2be5b..ecaf3cf5dd 100644 --- a/src/Service.GraphQLBuilder/GraphQLStoredProcedureBuilder.cs +++ b/src/Service.GraphQLBuilder/GraphQLStoredProcedureBuilder.cs @@ -71,9 +71,9 @@ public static List FormatStoredProcedureResultAsJsonList(bool IsRe List resultJson = new(); List> resultList = JsonSerializer.Deserialize>>(jsonDocument.RootElement.ToString())!; - foreach (Dictionary dict in resultList) + foreach (Dictionary result in resultList) { - resultJson.Add(JsonDocument.Parse(JsonSerializer.Serialize(dict))); + resultJson.Add(JsonDocument.Parse(JsonSerializer.Serialize(result))); } return resultJson; diff --git a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs index beaf83fbf8..d6358e6e53 100644 --- a/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs +++ b/src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs @@ -39,7 +39,7 @@ public static DocumentNode Build( NameNode name = objectTypeDefinitionNode.Name; string dbEntityName = ObjectTypeToEntityName(objectTypeDefinitionNode); - // We will only create a single mutation query for stored procedure + // For stored procedures, only one mutation is created in the schema // unlike table/views where we create one for each CUD operation. if (entities[dbEntityName].ObjectType is SourceType.StoredProcedure) { @@ -72,7 +72,7 @@ public static DocumentNode Build( /// /// Tries to fetch the Operation Type for Stored Procedure. - /// Stored Procedure currently support at most 1 CRUD operation at a time. + /// Stored Procedure currently supports exactly 1 CRUD operation at a time. /// This check is done during initialization as part of config validation. /// private static Operation GetOperationTypeForStoredProcedure(