Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved stored procedure parameter value type handling during GraphQL schema creation #1383

Merged
merged 16 commits into from
Apr 4, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 31 additions & 13 deletions src/Service.GraphQLBuilder/GraphQLStoredProcedureBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,29 +15,47 @@ public static class GraphQLStoredProcedureBuilder
/// <summary>
/// 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.
/// of the StoredProcedure GraphQL object.
/// </summary>
/// <param name="name">Name used for InputValueDefinition name.</param>
/// <param name="entity">Entity's runtime config metadata.</param>
/// <param name="dbObject">Stored procedure database schema metadata.</param>
/// <param name="rolesAllowed">Role authorization metadata.</param>
/// <returns>Stored procedure mutation field.</returns>
public static FieldDefinitionNode GenerateStoredProcedureSchema(
NameNode name,
Entity entity,
IEnumerable<string>? rolesAllowed = null)
DatabaseObject dbObject,
IEnumerable<string>? rolesAllowed = null
)
{
List<InputValueDefinitionNode> inputValues = new();
List<DirectiveNode> fieldDefinitionNodeDirectives = new();

// StoredProcedureDefinition contains both output result set column and input parameter metadata
// which are needed because parameter and column names can differ.
StoredProcedureDefinition spdef = (StoredProcedureDefinition)dbObject.SourceDefinition;

// Create input value definitions from parameters defined in runtime config.
if (entity.Parameters is not null)
{
foreach (string param in entity.Parameters.Keys)
{
Tuple<string, IValueNode> defaultGraphQLValue = GetGraphQLTypeAndNodeTypeFromStringValue(entity.Parameters[param].ToString()!);
// Input parameters defined in the runtime config may denote values that may not cast
// to the exact value type defined in the database schema.
// e.g. Runtime config parameter value set as 1, while database schema denotes value type decimal.
// Without database metadata, there is no way to know to cast 1 to a decimal versus an integer.
string defaultValueFromConfig = ((JsonElement)entity.Parameters[param]).ToString();
Tuple<string, IValueNode> defaultGraphQLValue = ConvertValueToGraphQLType(defaultValueFromConfig, parameterDefinition: spdef.Parameters[param]);

inputValues.Add(
new(
location: null,
new(param),
new StringValueNode($"parameters for {name.Value} stored-procedure"),
new NamedTypeNode(defaultGraphQLValue.Item1),
name: new(param),
description: new StringValueNode($"parameters for {name.Value} stored-procedure"),
type: new NamedTypeNode(defaultGraphQLValue.Item1),
defaultValue: defaultGraphQLValue.Item2,
new List<DirectiveNode>())
directives: new List<DirectiveNode>())
);
}
}
Expand Down Expand Up @@ -84,18 +102,18 @@ public static List<JsonDocument> FormatStoredProcedureResultAsJsonList(JsonDocum
}

/// <summary>
/// Helper method to create a default result field for stored-procedure which does not
/// return any row.
/// Create and return a default GraphQL result field for a stored-procedure which doesn't
/// define a result set and doesn't return any rows.
/// </summary>
public static FieldDefinitionNode GetDefaultResultFieldForStoredProcedure()
{
return new(
location: null,
new("result"),
name: new("result"),
description: new StringValueNode("Contains output of stored-procedure execution"),
new List<InputValueDefinitionNode>(),
new StringType().ToTypeNode(),
new List<DirectiveNode>());
arguments: new List<InputValueDefinitionNode>(),
type: new StringType().ToTypeNode(),
directives: new List<DirectiveNode>());
}
}
}
47 changes: 32 additions & 15 deletions src/Service.GraphQLBuilder/GraphQLUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
// Licensed under the MIT License.

using System.Diagnostics.CodeAnalysis;
using System.Net;
using Azure.DataApiBuilder.Config;
using Azure.DataApiBuilder.Service.Exceptions;
using Azure.DataApiBuilder.Service.GraphQLBuilder.CustomScalars;
using Azure.DataApiBuilder.Service.GraphQLBuilder.Directives;
using Azure.DataApiBuilder.Service.GraphQLBuilder.Sql;
using HotChocolate.Language;
using HotChocolate.Types;
using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLTypes.SupportedTypes;
Expand Down Expand Up @@ -170,7 +173,7 @@ public static bool CreateAuthorizationDirectiveIfNecessary(
/// </summary>
/// <param name="fieldDirectives">Collection of directives on GraphQL field.</param>
/// <param name="modelName">Value of @model directive, if present.</param>
/// <returns></returns>
/// <returns>True when name resolution succeeded, false otherwise.</returns>
public static bool TryExtractGraphQLFieldModelName(IDirectiveCollection fieldDirectives, [NotNullWhen(true)] out string? modelName)
{
foreach (Directive dir in fieldDirectives)
Expand Down Expand Up @@ -209,24 +212,38 @@ public static ObjectType UnderlyingGraphQLEntityType(IType type)
}

/// <summary>
/// Parse a given string value to supported GraphQL Type and GraphQLValueNode
/// Translates a JSON string or number value defined in the runtime configuration to a GraphQL {Type}ValueNode which represents
/// the associated GraphQL type. The target value type is referenced from the passed in parameterDefinition which
/// holds database schema metadata.
/// </summary>
public static Tuple<string, IValueNode> GetGraphQLTypeAndNodeTypeFromStringValue(string stringValue)
/// <param name="defaultValueFromConfig">String representation of default value defined in runtime config.</param>
/// <param name="parameterDefinition">Database schema metadata for stored procedure parameter which include value and value type.</param>
/// <returns>Tuple where first item is the string representation of a GraphQLType (e.g. "Byte", "Int", "Decimal")
/// and the second item is the GraphQL *type*ValueNode </returns>
seantleonard marked this conversation as resolved.
Show resolved Hide resolved
/// <exception cref="DataApiBuilderException">Raised when parameter casting fails due to unsupported type.</exception>
public static Tuple<string, IValueNode> ConvertValueToGraphQLType(string defaultValueFromConfig, ParameterDefinition parameterDefinition)
seantleonard marked this conversation as resolved.
Show resolved Hide resolved
{
if (int.TryParse(stringValue, out int integerValue))
string paramValueType = SchemaConverter.GetGraphQLTypeFromSystemType(type: parameterDefinition.SystemType);
Tuple<string, IValueNode> valueNode = paramValueType switch
{
return new(LONG_TYPE, new IntValueNode(integerValue));
}
else if (double.TryParse(stringValue, out double floatingValue))
{
return new(FLOAT_TYPE, new FloatValueNode(floatingValue));
}
else if (bool.TryParse(stringValue, out bool booleanValue))
{
return new(BOOLEAN_TYPE, new BooleanValueNode(booleanValue));
}
BYTE_TYPE => new(BYTE_TYPE, new IntValueNode(byte.Parse(defaultValueFromConfig))),
SHORT_TYPE => new(SHORT_TYPE, new IntValueNode(short.Parse(defaultValueFromConfig))),
INT_TYPE => new(INT_TYPE, new IntValueNode(int.Parse(defaultValueFromConfig))),
LONG_TYPE => new(LONG_TYPE, new IntValueNode(long.Parse(defaultValueFromConfig))),
seantleonard marked this conversation as resolved.
Show resolved Hide resolved
STRING_TYPE => new(STRING_TYPE, new StringValueNode(defaultValueFromConfig)),
BOOLEAN_TYPE => new(BOOLEAN_TYPE, new BooleanValueNode(bool.Parse(defaultValueFromConfig))),
SINGLE_TYPE => new(SINGLE_TYPE, new SingleType().ParseValue(float.Parse(defaultValueFromConfig))),
FLOAT_TYPE => new(FLOAT_TYPE, new FloatValueNode(double.Parse(defaultValueFromConfig))),
DECIMAL_TYPE => new(DECIMAL_TYPE, new FloatValueNode(decimal.Parse(defaultValueFromConfig))),
DATETIME_TYPE => new(DATETIME_TYPE, new DateTimeType().ParseResult(DateTime.Parse(defaultValueFromConfig))),
BYTEARRAY_TYPE => new(BYTEARRAY_TYPE, new ByteArrayType().ParseValue(Convert.FromBase64String(defaultValueFromConfig))),
_ => throw new DataApiBuilderException(
message: $"The parameter value {defaultValueFromConfig} provided in configuration cannot be converted to the type {paramValueType}",
seantleonard marked this conversation as resolved.
Show resolved Hide resolved
statusCode: HttpStatusCode.InternalServerError,
subStatusCode: DataApiBuilderException.SubStatusCodes.GraphQLMapping)
};

return new(STRING_TYPE, new StringValueNode(stringValue));
return valueNode;
}
}
}
28 changes: 22 additions & 6 deletions src/Service.GraphQLBuilder/Mutations/MutationBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

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;
Expand All @@ -25,12 +27,15 @@ public static class MutationBuilder
/// <param name="root">Root of GraphQL schema</param>
/// <param name="databaseType">i.e. MSSQL, MySQL, Postgres, Cosmos</param>
/// <param name="entities">Map of entityName -> EntityMetadata</param>
/// <returns></returns>
/// <param name="entityPermissionsMap">Permissions metadata defined in runtime config.</param>
/// <param name="dbObjects">Database object metadata</param>
/// <returns>Mutations DocumentNode</returns>
public static DocumentNode Build(
DocumentNode root,
DatabaseType databaseType,
IDictionary<string, Entity> entities,
Dictionary<string, EntityMetadata>? entityPermissionsMap = null)
Dictionary<string, EntityMetadata>? entityPermissionsMap = null,
Dictionary<string, DatabaseObject>? dbObjects = null)
{
List<FieldDefinitionNode> mutationFields = new();
Dictionary<NameNode, InputObjectTypeDefinitionNode> inputs = new();
Expand All @@ -43,7 +48,7 @@ public static DocumentNode Build(
string dbEntityName = ObjectTypeToEntityName(objectTypeDefinitionNode);

// For stored procedures, only one mutation is created in the schema
// unlike table/views where we create one for each CUD operation.
// unlike table/views where we create one for each create, update, and/or delete operation.
if (entities[dbEntityName].ObjectType is SourceType.StoredProcedure)
{
// check graphql sp config
Expand All @@ -53,7 +58,17 @@ public static DocumentNode Build(

if (isSPDefinedAsMutation)
{
AddMutationsForStoredProcedure(dbEntityName, entityPermissionsMap, name, entities, mutationFields);
if (dbObjects is not null && dbObjects.TryGetValue(entityName, out DatabaseObject? dbObject) && dbObject is not null)
{
AddMutationsForStoredProcedure(dbEntityName, entityPermissionsMap, name, entities, mutationFields, dbObject);
}
else
{
throw new DataApiBuilderException(
message: "GraphQL Schema Creation for Stored Procedures requires DatabaseObject defined. Most likely error in startup.",
seantleonard marked this conversation as resolved.
Show resolved Hide resolved
statusCode: HttpStatusCode.InternalServerError,
subStatusCode: DataApiBuilderException.SubStatusCodes.GraphQLMapping);
}
}
}
else
Expand Down Expand Up @@ -135,13 +150,14 @@ private static void AddMutationsForStoredProcedure(
Dictionary<string, EntityMetadata>? entityPermissionsMap,
NameNode name,
IDictionary<string, Entity> entities,
List<FieldDefinitionNode> mutationFields
List<FieldDefinitionNode> mutationFields,
DatabaseObject dbObject
)
{
IEnumerable<string> rolesAllowedForMutation = IAuthorizationResolver.GetRolesForOperation(dbEntityName, operation: Operation.Execute, entityPermissionsMap);
if (rolesAllowedForMutation.Count() > 0)
{
mutationFields.Add(GraphQLStoredProcedureBuilder.GenerateStoredProcedureSchema(name, entities[dbEntityName], rolesAllowedForMutation));
mutationFields.Add(GraphQLStoredProcedureBuilder.GenerateStoredProcedureSchema(name, entities[dbEntityName], dbObject, rolesAllowedForMutation));
}
}

Expand Down
21 changes: 13 additions & 8 deletions src/Service.GraphQLBuilder/Queries/QueryBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,20 @@ public static class QueryBuilder
/// Creates a DocumentNode containing FieldDefinitionNodes representing the FindByPK and FindAll queries
/// Also populates the DocumentNode with return types.
/// </summary>
/// <param name="root"></param>
/// <param name="databaseType"></param>
/// <param name="entities"></param>
/// <param name="inputTypes"></param>
/// <param name="entityPermissionsMap">Collection of permissions defined in runtime config.</param>
/// <returns></returns>
/// <param name="root">Root of GraphQL schema</param>
/// <param name="databaseType">i.e. MSSQL, MySQL, Postgres, Cosmos</param>
/// <param name="entities">Map of entityName -> EntityMetadata</param>
/// <param name="entityPermissionsMap">Permissions metadata defined in runtime config.</param>
/// <param name="dbObjects">Database object metadata</param>
/// <returns>Queries DocumentNode</returns>
public static DocumentNode Build(
DocumentNode root,
DatabaseType databaseType,
IDictionary<string, Entity> entities,
Dictionary<string, InputObjectTypeDefinitionNode> inputTypes,
Dictionary<string, EntityMetadata>? entityPermissionsMap = null)
Dictionary<string, EntityMetadata>? entityPermissionsMap = null,
Dictionary<string, DatabaseObject>? dbObjects = null
)
{
List<FieldDefinitionNode> queryFields = new();
List<ObjectTypeDefinitionNode> returnTypes = new();
Expand All @@ -61,7 +63,10 @@ public static DocumentNode Build(

if (isSPDefinedAsQuery && rolesAllowedForExecute.Any())
{
queryFields.Add(GraphQLStoredProcedureBuilder.GenerateStoredProcedureSchema(name, entity, rolesAllowedForExecute));
if (dbObjects is not null && dbObjects.TryGetValue(entityName, out DatabaseObject? dbObject) && dbObject is not null)
{
queryFields.Add(GraphQLStoredProcedureBuilder.GenerateStoredProcedureSchema(name, entity, dbObject, rolesAllowedForExecute));
}
}
}
else
Expand Down
Loading