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

Fix ID serialization on filter operations #7678

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#nullable enable
namespace HotChocolate.Types;

public interface IIdInputValueFormatter : IInputValueFormatter
{
object? FormatId(Type? namedType, object? originalValue);
}
55 changes: 47 additions & 8 deletions src/HotChocolate/Core/src/Types/Types/InputParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ public InputParser(ITypeConverter converter, InputParserOptions options)

var path = Path.Root.Append(field.Name);
var runtimeValue = ParseLiteralInternal(value, field.Type, path, 0, true, field);
runtimeValue = FormatValue(field, runtimeValue);

runtimeValue = FormatValue(field, targetType, runtimeValue);

// Caller doesn't care, but to ensure specificity, we set the field's runtime type
// to make sure it's at least converted to the right type.
Expand Down Expand Up @@ -258,7 +259,7 @@ private object ParseObject(
stack,
defaults,
field);
value = FormatValue(field, value);
value = FormatValue(field, null, value);
value = ConvertValue(field.RuntimeType, value);

if (field.IsOptional)
Expand Down Expand Up @@ -406,7 +407,7 @@ private object ParseDirective(
stack,
defaults,
field);
value = FormatValue(field, value);
value = FormatValue(field, null, value);
value = ConvertValue(field.RuntimeType, value);

if (field.IsOptional)
Expand Down Expand Up @@ -569,7 +570,7 @@ private object DeserializeObject(object resultValue, InputObjectType type, Path
}

var value = Deserialize(fieldValue, field.Type, fieldPath, field);
value = FormatValue(field, value);
value = FormatValue(field, null, value);
value = ConvertValue(field.RuntimeType, value);

if (field.IsOptional)
Expand Down Expand Up @@ -691,7 +692,7 @@ private object DeserializeObject(object resultValue, InputObjectType type, Path
throw new SerializationException(ex.Errors[0].WithPath(path), ex.Type, path);
}

value = FormatValue(field, value);
value = FormatValue(field, null, value);
value = ConvertValue(field.RuntimeType, value);

return field.IsOptional
Expand Down Expand Up @@ -739,18 +740,56 @@ private object DeserializeObject(object resultValue, InputObjectType type, Path
throw new SerializationException(ex.Errors[0].WithPath(path), ex.Type, path);
}

value = FormatValue(field, value);
value = FormatValue(field, null, value);
value = ConvertValue(field.RuntimeType, value);

return field.IsOptional
? new Optional(value, false)
: value;
}

private static object? FormatValue(IInputFieldInfo field, object? value)
private static object? FormatValue(IInputFieldInfo field, Type? targetType, object? value)
=> value is null || field.Formatter is null
? value
: field.Formatter.Format(value);
: FormatValue(field.Formatter, targetType, value);

private static object? FormatValue(IInputValueFormatter valueFormatter, Type? targetType, object? value)
{
if (valueFormatter is IIdInputValueFormatter idFormatter)
{
return idFormatter.FormatId(GetElementType(targetType), value);
}
return valueFormatter.Format(value);
}

private static Type? GetElementType(Type? targetType)
{
if (targetType is null)
{
return null;
}

if (targetType.IsArray)
{
return targetType.GetElementType();
}

if (targetType.IsGenericType && typeof(IEnumerable).IsAssignableFrom(targetType))
{
return targetType.GetGenericArguments()[0];
}

foreach (Type interfaceType in targetType.GetInterfaces())
{
if (interfaceType.IsGenericType && interfaceType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
{
return interfaceType.GetGenericArguments()[0];
}
}

return targetType;
}


private object? ConvertValue(Type requestedType, object? value)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,10 @@ public static IFilterOperationFieldDescriptor ID(
.Extend()
.OnBeforeCompletion((c, d) =>
{
var returnType = d.Member is null ? typeof(string) : d.Member.GetReturnType();
var returnTypeInfo = c.DescriptorContext.TypeInspector.CreateTypeInfo(returnType);
d.Formatters.Push(CreateSerializer(c, returnTypeInfo.NamedType));
d.Formatters.Add(new FilterGlobalIdInputValueFormatter(c.DescriptorContext.NodeIdSerializerAccessor));
});

return descriptor;
}

private static IInputValueFormatter CreateSerializer(
ITypeCompletionContext completionContext,
Type namedType)
=> new FilterGlobalIdInputValueFormatter(
completionContext.DescriptorContext.NodeIdSerializerAccessor,
namedType);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,20 @@
namespace HotChocolate.Data.Filters;

internal class FilterGlobalIdInputValueFormatter(
INodeIdSerializerAccessor serializerAccessor,
Type namedType)
: IInputValueFormatter
INodeIdSerializerAccessor serializerAccessor)
: IIdInputValueFormatter
{
private INodeIdSerializer? _serializer;

public object? Format(object? originalValue)

public object? FormatId(Type? namedType, object? originalValue)
{
if (originalValue is null)
if (originalValue is null)
{
return null;
}

if (namedType is null)
{
return null;
}
Expand Down Expand Up @@ -97,4 +102,9 @@ internal class FilterGlobalIdInputValueFormatter(

throw ThrowHelper.GlobalIdInputValueFormatter_SpecifiedValueIsNotAValidId();
}

public object? Format(object? originalValue)
{
throw new NotSupportedException();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using HotChocolate.Resolvers;
using HotChocolate.Types;
using HotChocolate.Types.Descriptors;
using HotChocolate.Types.Relay;
using Microsoft.Extensions.DependencyInjection;

namespace HotChocolate.Data.Filters;
Expand Down Expand Up @@ -115,6 +116,39 @@ await Snapshot
.MatchAsync();
}


[Fact]
public async Task RelayIds_Should_Work()
{
// arrange
var bars = new BarQuery().GetBars.First();
var id = new DefaultNodeIdSerializer().Format("Bar", bars.Id);
var executor = await new ServiceCollection()
.AddGraphQL()
.AddQueryType<BarQuery>()
.AddFiltering()
.BuildRequestExecutorAsync();

// act
// var query = $$"""
// { getBars(where: { id: { in: ["{{id}}"] } }) { id } }
// """;
var query = $$"""
{ getBars(where: { id: { in: ["{{id}}"] } }) { id } }
""";
var res1 = await executor.ExecuteAsync(
OperationRequestBuilder
.New()
.SetDocument(query)
.Build());

// assert
await Snapshot
.Create()
.AddResult(res1)
.MatchAsync();
}

public class Query
{
[UseFiltering]
Expand Down Expand Up @@ -151,6 +185,21 @@ public class Foo
public string? NotSettable { get; }
}

public class BarQuery
{
[UseFiltering]
public IQueryable<Bar> GetBars => new List<Bar>{
new() { Id = Guid.Parse("2a874c44944c463a9eaf47191813266d") },
new() { Id = Guid.Parse("6872d2a0e0954e2db96958ded680b3cc") }
}.AsQueryable();
}

public class Bar
{
[ID]
public Guid Id { get; set; }
}

public class AddTypeMismatchMiddlewareAttribute : ObjectFieldDescriptorAttribute
{
protected override void OnConfigure(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"data": {
"getBars": [
{
"id": "2a874c44944c463a9eaf47191813266d"
}
]
}
}
Loading