Skip to content

Commit

Permalink
[Cherry pick] Resolve uniqueidentifier database type from `dab-conf…
Browse files Browse the repository at this point in the history
…ig.json` (#2075)

Full Title: Resolve `uniqueidentifier` database type from
`dab-config.json` defined stored proc parameter default values. (#2042)

## Why make this change?

- Closes #2027. DAB would not start up correctly when runtime config
took the following form:
```json
  "entities": {
    "SpUuidParam": {
      "source": {
        "object": "sp_uuid_param",
        "type": "stored-procedure",
        "parameters": {
          "param1": "f58b7b58-62c9-4b97-ab60-75de70793f66"
        }
      },
      "graphql": {
        "enabled": true,
        "operation": "Query",
        "type": {
          "singular": "SpUuidParam",
          "plural": "SpUuidParams"
        }
      },
      "rest": {
        "enabled": true
      },
      "permissions": [
        {
          "role": "anonymous",
          "actions": [ "*" ]
        },
        {
          "role": "authenticated",
          "actions": [ "*" ]
        }
      ]
    }
```

And stored proc in tsql
```tsql
CREATE PROCEDURE [dbo].[sp_uuid_param] @param1 uniqueidentifier AS
      SELECT @param1 AS [ReturnPayload]
```

DAB was lacking the ability to handle the stored procedure having an
input parameter with value type `uniqueidentifier`. When a default value
was defined in the config, that value would fail conversion to the UUID
type during GraphQL schema creation.

The call stack flows through:

```csharp
                    if (entity.Source.Parameters is not null && entity.Source.Parameters.TryGetValue(param, out object? value))
                    {
                        Tuple<string, IValueNode> defaultGraphQLValue = ConvertValueToGraphQLType(value.ToString()!, parameterDefinition: spdef.Parameters[param]);
                        defaultValueNode = defaultGraphQLValue.Item2;
                    }
```

where `ConvertValueToGraphQLType(...)` would attempt to convert the
config defined value to a GraphQL type based on the type inferred from
the parameter's SystemType (System.Guid). The parameter object has the
following properties when the parameter is passed to that function:

- SystemType -> `System.Guid`
- DbType -> `Guid`
- ConfigDefaultValue -> `f58b7b58-62c9-4b97-ab60-75de70793f66`
- HasConfigDefault -> `true`

`ConvertValueToGraphQLType(...)` did not have the conversion needed to
create a UUID type.

## What is this change?

- In the function called to process config defined default values for
stored procedure parameters, `ConvertValueToGraphQLType(...)` add the
conversion:

```csharp
UUID_TYPE => new(UUID_TYPE, new UuidType().ParseValue(Guid.Parse(defaultValueFromConfig))),
```

- Moved `ConvertValueToGraphQLType()` from `GraphQLUtils.cs` to
`GraphQLStoredProcedureBuilder.cs` to align with usage and purpose of
function. Reduces size of MEGA UTIL class. Also adds comment for
`GraphQLUtils.BuiltInTypes` hashset entry 'ID' to inform that it enables
CosmosDB functionality as only cosmos tests failed when that entry was
commented out.
- Reorganizes the ConvertValueToGraphQLType() order of types in switch
statements to align with GraphQLUtils.BuiltInTypes hashset. That way it
is easier to notice discrepancies in the two lists.

## How was this tested?

- [x] Integration Tests

## Sample Request(s)

- Use the db schema and entity config provided above. Startup will
succeed without conversion errors when attempting to create the GraphQL
schema.

Co-authored-by: Aniruddh Munde <anmunde@microsoft.com>
  • Loading branch information
seantleonard and Aniruddh25 committed Mar 4, 2024
1 parent 36c3791 commit 8590aef
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 59 deletions.
55 changes: 55 additions & 0 deletions src/Service.GraphQLBuilder/GraphQLStoredProcedureBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Globalization;
using System.Net;
using System.Text.Json;
using Azure.DataApiBuilder.Config.DatabasePrimitives;
using Azure.DataApiBuilder.Config.ObjectModel;
using Azure.DataApiBuilder.Service.Exceptions;
using Azure.DataApiBuilder.Service.GraphQLBuilder.CustomScalars;
using Azure.DataApiBuilder.Service.GraphQLBuilder.Sql;
using HotChocolate.Language;
using HotChocolate.Types;
using HotChocolate.Types.NodaTime;
using NodaTime.Text;
using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLNaming;
using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLTypes.SupportedHotChocolateTypes;
using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLUtils;

namespace Azure.DataApiBuilder.Service.GraphQLBuilder
Expand Down Expand Up @@ -122,5 +129,53 @@ public static FieldDefinitionNode GetDefaultResultFieldForStoredProcedure()
type: new StringType().ToTypeNode(),
directives: new List<DirectiveNode>());
}

/// <summary>
/// Translates a JSON string or number value defined as a stored procedure's default value
/// within 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>
/// <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>
/// <exception cref="DataApiBuilderException">Raised when parameter casting fails due to unsupported type.</exception>
private static Tuple<string, IValueNode> ConvertValueToGraphQLType(string defaultValueFromConfig, ParameterDefinition parameterDefinition)
{
string paramValueType = SchemaConverter.GetGraphQLTypeFromSystemType(type: parameterDefinition.SystemType);

try
{
Tuple<string, IValueNode> valueNode = paramValueType switch
{
UUID_TYPE => new(UUID_TYPE, new UuidType().ParseValue(Guid.Parse(defaultValueFromConfig))),
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))),
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))),
STRING_TYPE => new(STRING_TYPE, new StringValueNode(defaultValueFromConfig)),
BOOLEAN_TYPE => new(BOOLEAN_TYPE, new BooleanValueNode(bool.Parse(defaultValueFromConfig))),
DATETIME_TYPE => new(DATETIME_TYPE, new DateTimeType().ParseResult(
DateTime.Parse(defaultValueFromConfig, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AssumeUniversal))),
BYTEARRAY_TYPE => new(BYTEARRAY_TYPE, new ByteArrayType().ParseValue(Convert.FromBase64String(defaultValueFromConfig))),
LOCALTIME_TYPE => new(LOCALTIME_TYPE, new LocalTimeType().ParseResult(LocalTimePattern.ExtendedIso.Parse(defaultValueFromConfig).Value)),
_ => throw new NotSupportedException(message: $"The {defaultValueFromConfig} parameter's value type [{paramValueType}] is not supported.")
};

return valueNode;
}
catch (Exception error)
{
throw new DataApiBuilderException(
message: $"The parameter value {defaultValueFromConfig} provided in configuration cannot be converted to the type {paramValueType}",
statusCode: HttpStatusCode.InternalServerError,
subStatusCode: DataApiBuilderException.SubStatusCodes.GraphQLMapping,
innerException: error);
}
}
}
}
62 changes: 3 additions & 59 deletions src/Service.GraphQLBuilder/GraphQLUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,14 @@
// Licensed under the MIT License.

using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Net;
using Azure.DataApiBuilder.Config.DatabasePrimitives;
using Azure.DataApiBuilder.Config.ObjectModel;
using Azure.DataApiBuilder.Service.Exceptions;
using Azure.DataApiBuilder.Service.GraphQLBuilder.CustomScalars;
using Azure.DataApiBuilder.Service.GraphQLBuilder.Directives;
using Azure.DataApiBuilder.Service.GraphQLBuilder.Queries;
using Azure.DataApiBuilder.Service.GraphQLBuilder.Sql;
using HotChocolate.Language;
using HotChocolate.Resolvers;
using HotChocolate.Types;
using HotChocolate.Types.NodaTime;
using NodaTime.Text;
using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLTypes.SupportedHotChocolateTypes;

namespace Azure.DataApiBuilder.Service.GraphQLBuilder
Expand Down Expand Up @@ -45,9 +39,9 @@ public static bool IsModelType(ObjectType objectType)

public static bool IsBuiltInType(ITypeNode typeNode)
{
HashSet<string> inBuiltTypes = new()
HashSet<string> builtInTypes = new()
{
"ID",
"ID", // Required for CosmosDB
UUID_TYPE,
BYTE_TYPE,
SHORT_TYPE,
Expand All @@ -63,7 +57,7 @@ public static bool IsBuiltInType(ITypeNode typeNode)
LOCALTIME_TYPE
};
string name = typeNode.NamedType().Name.Value;
return inBuiltTypes.Contains(name);
return builtInTypes.Contains(name);
}

/// <summary>
Expand Down Expand Up @@ -224,56 +218,6 @@ public static ObjectType UnderlyingGraphQLEntityType(IType type)
return UnderlyingGraphQLEntityType(type.InnerType());
}

/// <summary>
/// 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>
/// <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>
/// <exception cref="DataApiBuilderException">Raised when parameter casting fails due to unsupported type.</exception>
public static Tuple<string, IValueNode> ConvertValueToGraphQLType(string defaultValueFromConfig, ParameterDefinition parameterDefinition)
{
string paramValueType = SchemaConverter.GetGraphQLTypeFromSystemType(type: parameterDefinition.SystemType);

try
{
Tuple<string, IValueNode> valueNode = paramValueType switch
{
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))),
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, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AssumeUniversal))),
BYTEARRAY_TYPE => new(BYTEARRAY_TYPE, new ByteArrayType().ParseValue(Convert.FromBase64String(defaultValueFromConfig))),
LOCALTIME_TYPE => new(LOCALTIME_TYPE, new LocalTimeType().ParseResult(LocalTimePattern.ExtendedIso.Parse(defaultValueFromConfig).Value)),
_ => throw new NotSupportedException(message: $"The {defaultValueFromConfig} parameter's value type [{paramValueType}] is not supported.")
};

return valueNode;
}
catch (Exception error) when (
error is FormatException ||
error is OverflowException ||
error is ArgumentException ||
error is NotSupportedException)
{
throw new DataApiBuilderException(
message: $"The parameter value {defaultValueFromConfig} provided in configuration cannot be converted to the type {paramValueType}",
statusCode: HttpStatusCode.InternalServerError,
subStatusCode: DataApiBuilderException.SubStatusCodes.GraphQLMapping,
innerException: error);
}
}

/// <summary>
/// Generates the datasource name from the GraphQL context.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ public class StoredProcedureBuilderTests
[DataRow(typeof(DateTimeOffset), DATETIME_TYPE, "11/19/2012 10:57:11 AM -08:00", false, DisplayName = "DateTimeOffset")]
[DataRow(typeof(TimeOnly), LOCALTIME_TYPE, "10:57:11.0000", false, DisplayName = "LocalTime")]
[DataRow(typeof(byte[]), BYTEARRAY_TYPE, "AgQGCAoMDhASFA==", false, DisplayName = "Byte[]")]
[DataRow(typeof(Guid), UUID_TYPE, "f58b7b58-62c9-4b97-ab60-75de70793f66", false, DisplayName = "GraphQL UUID/ SystemType GUID")]
[DataRow(typeof(string), STRING_TYPE, "f58b7b58-62c9-4b97-ab60-75de70793f66", false, DisplayName = "DB/SystemType String -> GUID value -> Resolve as GraphQL string")]
public void StoredProcedure_ParameterValueTypeResolution(
Type systemType,
string expectedGraphQLType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,8 @@ public async Task TestTimeTypePrecisionCheck(string gqlValue, int count)
[DataRow(INT_TYPE, "0")]
[DataRow(INT_TYPE, "-9999")]
[DataRow(INT_TYPE, "null")]
[DataRow(UUID_TYPE, "3a1483a5-9ac2-4998-bcf3-78a28078c6ac")]
[DataRow(UUID_TYPE, "null")]
[DataRow(LONG_TYPE, "0")]
[DataRow(LONG_TYPE, "9000000000000000000")]
[DataRow(LONG_TYPE, "-9000000000000000000")]
Expand Down Expand Up @@ -419,6 +421,7 @@ public async Task InsertInvalidTimeIntoTimeTypeColumn(string type, string value)
[DataRow(TIME_TYPE, "\"23:59:59.9999999\"")]
[DataRow(TIME_TYPE, "null")]
[DataRow(BYTEARRAY_TYPE, "V2hhdGNodSBkb2luZyBkZWNvZGluZyBvdXIgdGVzdCBiYXNlNjQgc3RyaW5ncz8=")]
[DataRow(UUID_TYPE, "3a1483a5-9ac2-4998-bcf3-78a28078c6ac")]
public async Task InsertIntoTypeColumnWithArgument(string type, object value)
{
if (!IsSupportedType(type))
Expand Down

0 comments on commit 8590aef

Please sign in to comment.