diff --git a/build.cake b/build.cake index 88f7f8138c6..a541a2328f5 100644 --- a/build.cake +++ b/build.cake @@ -27,18 +27,17 @@ Task("EnvironmentSetup") { if(string.IsNullOrEmpty(packageVersion)) { - packageVersion = EnvironmentVariable("CIRCLE_TAG") - ?? EnvironmentVariable("APPVEYOR_REPO_TAG_NAME") - ?? EnvironmentVariable("Version"); + packageVersion = EnvironmentVariable("Version"); } Environment.SetEnvironmentVariable("Version", packageVersion); if(string.IsNullOrEmpty(sonarPrKey)) { - sonarPrKey = EnvironmentVariable("APPVEYOR_PULL_REQUEST_NUMBER"); - sonarBranch = EnvironmentVariable("APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH"); - sonarBranchBase = EnvironmentVariable("APPVEYOR_REPO_BRANCH"); + sonarPrKey = EnvironmentVariable("PR_NUMBER"); + sonarBranch = EnvironmentVariable("PR_SOURCE_BRANCH"); + sonarBranchBase = EnvironmentVariable("PR_TARGET_BRANCH"); sonarBranchBase = "master"; + System.Console.WriteLine("PrKey" + sonarPrKey); } if(string.IsNullOrEmpty(sonarLogin)) @@ -47,44 +46,9 @@ Task("EnvironmentSetup") } }); -Task("Clean") - .IsDependentOn("EnvironmentSetup") - .Does(() => -{ - DotNetCoreClean("./src/DataLoader"); - DotNetCoreClean("./src/Core"); - DotNetCoreClean("./src/Server"); -}); - -Task("Restore") - .IsDependentOn("EnvironmentSetup") - .Does(() => -{ - DotNetCoreRestore("./tools/Build.sln"); -}); - -Task("RestoreCore") - .IsDependentOn("EnvironmentSetup") - .Does(() => -{ - DotNetCoreRestore("./tools/Build.Core.sln"); -}); - Task("Build") .IsDependentOn("EnvironmentSetup") .Does(() => -{ - var settings = new DotNetCoreBuildSettings - { - Configuration = configuration, - }; - - DotNetCoreBuild("./tools/Build.sln", settings); -}); - -Task("BuildDebug") - .IsDependentOn("EnvironmentSetup") - .Does(() => { var buildSettings = new DotNetCoreBuildSettings { @@ -94,57 +58,6 @@ Task("BuildDebug") DotNetCoreBuild("./tools/Build.sln", buildSettings); }); -Task("BuildCore") - .IsDependentOn("EnvironmentSetup") - .Does(() => -{ - var settings = new DotNetCoreBuildSettings - { - Configuration = configuration, - }; - - DotNetCoreBuild("./tools/Build.Core.sln", settings); -}); - -Task("Publish") - .IsDependentOn("EnvironmentSetup") - .Does(() => -{ - using(var process = StartAndReturnProcess("msbuild", - new ProcessSettings{ Arguments = "./tools/Build.sln /t:restore /p:configuration=" + configuration })) - { - process.WaitForExit(); - } - - using(var process = StartAndReturnProcess("msbuild", - new ProcessSettings{ Arguments = "./tools/Build.sln /t:build /p:configuration=" + configuration })) - { - process.WaitForExit(); - } - - using(var process = StartAndReturnProcess("msbuild", - new ProcessSettings{ Arguments = "./tools/Build.sln /t:pack /p:configuration=" + configuration + " /p:IncludeSource=true /p:IncludeSymbols=true" })) - { - process.WaitForExit(); - } -}); - -Task("PublishTemplates") - .IsDependentOn("EnvironmentSetup") - .Does(() => -{ - var nuGetPackSettings = new NuGetPackSettings - { - Version = packageVersion, - OutputDirectory = "src/Templates" - }; - - ReplaceTextInFiles("src/Templates/StarWars/content/StarWars/StarWars.csproj", "10.0.1", packageVersion); - ReplaceTextInFiles("src/Templates/Server/content/HotChocolate.Server.Template.csproj", "10.0.1", packageVersion); - NuGetPack("src/Templates/StarWars/HotChocolate.Templates.StarWars.nuspec", nuGetPackSettings); - NuGetPack("src/Templates/Server/HotChocolate.Templates.Server.nuspec", nuGetPackSettings); -}); - Task("Tests") .IsDependentOn("EnvironmentSetup") .Does(() => @@ -169,31 +82,18 @@ Task("Tests") .Append($"/p:CoverletOutput=\"../../{testOutputDir}/full_{i++}\" --blame") }; - DotNetCoreBuild("./tools/Build.sln", buildSettings); + DotNetCoreBuild("./tools/Build.Core.sln", buildSettings); foreach(var file in GetFiles("./src/**/*.Tests.csproj")) { - if(!file.FullPath.Contains("Redis") && !file.FullPath.Contains("Mongo")) + if(!file.FullPath.Contains("Classic") && !file.FullPath.Contains("Redis") && !file.FullPath.Contains("Mongo")) { DotNetCoreTest(file.FullPath, testSettings); } } }); -Task("TemplatesCompile") - .IsDependentOn("EnvironmentSetup") - .Does(() => -{ - var buildSettings = new DotNetCoreBuildSettings - { - Configuration = "Debug" - }; - - DotNetCoreBuild("./src/Templates/Server/content", buildSettings); - DotNetCoreBuild("./src/Templates/StarWars/content", buildSettings); -}); - -Task("CoreTests") +Task("AllTests") .IsDependentOn("EnvironmentSetup") .Does(() => { @@ -214,183 +114,14 @@ Task("CoreTests") .Append("/p:CollectCoverage=true") .Append("/p:Exclude=[xunit.*]*") .Append("/p:CoverletOutputFormat=opencover") - .Append($"/p:CoverletOutput=\"../../{testOutputDir}/core_{i++}\" --blame") + .Append($"/p:CoverletOutput=\"../../{testOutputDir}/full_{i++}\" --blame") }; DotNetCoreBuild("./tools/Build.Core.sln", buildSettings); foreach(var file in GetFiles("./src/**/*.Tests.csproj")) { - if(!file.FullPath.Contains("Redis") && !file.FullPath.Contains("Mongo")) - { - DotNetCoreTest(file.FullPath, testSettings); - } - } -}); - -Task("RedisTests") - .IsDependentOn("EnvironmentSetup") - .Does(() => -{ - int i = 0; - var testSettings = new DotNetCoreTestSettings - { - Configuration = "Debug", - ResultsDirectory = $"./{testOutputDir}", - Logger = "trx", - NoRestore = false, - NoBuild = false, - ArgumentCustomization = args => args - .Append("/p:CollectCoverage=true") - .Append("/p:Exclude=[xunit.*]*") - .Append("/p:CoverletOutputFormat=opencover") - .Append($"/p:CoverletOutput=\"../../{testOutputDir}/core_{i++}\" --blame") - }; - - foreach(var file in GetFiles("./src/**/*.Tests.csproj")) - { - if(file.FullPath.Contains("Redis")) - { - DotNetCoreTest(file.FullPath, testSettings); - } - } -}); - -Task("MongoTests") - .IsDependentOn("EnvironmentSetup") - .Does(() => -{ - int i = 0; - var testSettings = new DotNetCoreTestSettings - { - Configuration = "Debug", - ResultsDirectory = $"./{testOutputDir}", - Logger = "trx", - NoRestore = false, - NoBuild = false, - ArgumentCustomization = args => args - .Append("/p:CollectCoverage=true") - .Append("/p:Exclude=[xunit.*]*") - .Append("/p:CoverletOutputFormat=opencover") - .Append($"/p:CoverletOutput=\"../../{testOutputDir}/core_{i++}\" --blame") - }; - - foreach(var file in GetFiles("./src/**/*.Tests.csproj")) - { - if(file.FullPath.Contains("Mongo")) - { - DotNetCoreTest(file.FullPath, testSettings); - } - } -}); - -Task("HC_DataLoader_Tests") - .IsDependentOn("EnvironmentSetup") - .Does(() => -{ - int i = 0; - var testSettings = new DotNetCoreTestSettings - { - Configuration = "Debug", - ResultsDirectory = $"./{testOutputDir}", - Logger = "trx", - NoRestore = false, - NoBuild = false, - ArgumentCustomization = args => args - .Append("/p:CollectCoverage=true") - .Append("/p:Exclude=[xunit.*]*") - .Append("/p:CoverletOutputFormat=opencover") - .Append($"/p:CoverletOutput=\"../../{testOutputDir}/hc_dataloader_{i++}\" --blame") - }; - - foreach(var file in GetFiles("./src/DataLoader/**/*.Tests.csproj")) - { - if(!file.FullPath.Contains("Redis") && !file.FullPath.Contains("Mongo")) - { - DotNetCoreTest(file.FullPath, testSettings); - } - } -}); - - -Task("HC_Core_Tests") - .IsDependentOn("EnvironmentSetup") - .Does(() => -{ - int i = 0; - var testSettings = new DotNetCoreTestSettings - { - Configuration = "Debug", - ResultsDirectory = $"./{testOutputDir}", - Logger = "trx", - NoRestore = false, - NoBuild = false, - ArgumentCustomization = args => args - .Append("/p:CollectCoverage=true") - .Append("/p:Exclude=[xunit.*]*") - .Append("/p:CoverletOutputFormat=opencover") - .Append($"/p:CoverletOutput=\"../../{testOutputDir}/hc_core_{i++}\" --blame") - }; - - foreach(var file in GetFiles("./src/Core/**/*.Tests.csproj")) - { - if(!file.FullPath.Contains("Redis") && !file.FullPath.Contains("Mongo")) - { - DotNetCoreTest(file.FullPath, testSettings); - } - } -}); - -Task("HC_Server_Tests") - .IsDependentOn("EnvironmentSetup") - .Does(() => -{ - int i = 0; - var testSettings = new DotNetCoreTestSettings - { - Configuration = "Debug", - ResultsDirectory = $"./{testOutputDir}", - Logger = "trx", - NoRestore = false, - NoBuild = false, - ArgumentCustomization = args => args - .Append("/p:CollectCoverage=true") - .Append("/p:Exclude=[xunit.*]*") - .Append("/p:CoverletOutputFormat=opencover") - .Append($"/p:CoverletOutput=\"../../{testOutputDir}/hc_server_{i++}\" --blame") - }; - - foreach(var file in GetFiles("./src/Server/**/*.Tests.csproj")) - { - if(!file.FullPath.Contains("Redis") && !file.FullPath.Contains("Mongo")) - { - DotNetCoreTest(file.FullPath, testSettings); - } - } -}); - -Task("HC_Stitching_Tests") - .IsDependentOn("EnvironmentSetup") - .Does(() => -{ - int i = 0; - var testSettings = new DotNetCoreTestSettings - { - Configuration = "Debug", - ResultsDirectory = $"./{testOutputDir}", - Logger = "trx", - NoRestore = false, - NoBuild = false, - ArgumentCustomization = args => args - .Append("/p:CollectCoverage=true") - .Append("/p:Exclude=[xunit.*]*") - .Append("/p:CoverletOutputFormat=opencover") - .Append($"/p:CoverletOutput=\"../../{testOutputDir}/hc_stitching_{i++}\" --blame") - }; - - foreach(var file in GetFiles("./src/Stitching/**/*.Tests.csproj")) - { - if(!file.FullPath.Contains("Redis") && !file.FullPath.Contains("Mongo")) + if(!file.FullPath.Contains("Classic")) { DotNetCoreTest(file.FullPath, testSettings); } @@ -403,6 +134,7 @@ Task("SonarBegin") { SonarBegin(new SonarBeginSettings { + UseCoreClr = true, Url = "https://sonarcloud.io", Login = sonarLogin, Key = "HotChocolate", @@ -420,7 +152,6 @@ Task("SonarBegin") a = a.Append($"/d:sonar.pullrequest.base=\"{sonarBranchBase}\""); a = a.Append($"/d:sonar.pullrequest.provider=\"github\""); a = a.Append($"/d:sonar.pullrequest.github.repository=\"ChilliCream/hotchocolate\""); - // a = a.Append($"/d:sonar.pullrequest.github.endpoint=\"https://api.github.com/\""); } return a; } @@ -432,7 +163,8 @@ Task("SonarEnd") { SonarEnd(new SonarEndSettings { - Login = sonarLogin, + UseCoreClr = true, + Login = sonarLogin }); }); @@ -440,23 +172,13 @@ Task("SonarEnd") // TASK TARGETS ////////////////////////////////////////////////////////////////////// Task("Default") - .IsDependentOn("Tests"); + .IsDependentOn("Sonar"); Task("Sonar") .IsDependentOn("SonarBegin") - .IsDependentOn("Tests") - .IsDependentOn("SonarEnd"); - -Task("SonarSlim") - .IsDependentOn("SonarBegin") - .IsDependentOn("BuildDebug") + .IsDependentOn("AllTests") .IsDependentOn("SonarEnd"); -Task("Release") - .IsDependentOn("Sonar") - .IsDependentOn("Publish") - .IsDependentOn("PublishTemplates"); - ////////////////////////////////////////////////////////////////////// // EXECUTION ////////////////////////////////////////////////////////////////////// diff --git a/global.json b/global.json index 89b3b0f42de..79422f0cc1c 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "2.2.402" + "version": "3.0.100" } } diff --git a/run-benchmarks.ps1 b/run-benchmarks.ps1 deleted file mode 100644 index 40f63d2061c..00000000000 --- a/run-benchmarks.ps1 +++ /dev/null @@ -1,2 +0,0 @@ -dotnet build src -c release -dotnet "src/Benchmark.Tests/bin/Release/netcoreapp2.1/Benchmark.Tests.dll" diff --git a/run-benchmarks.sh b/run-benchmarks.sh deleted file mode 100755 index 5bb7661b9a6..00000000000 --- a/run-benchmarks.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash - -dotnet build src -c release -sudo dotnet "src/Benchmark.Tests/bin/Release/netcoreapp2.1/Benchmark.Tests.dll" diff --git a/run-tests.ps1 b/run-tests.ps1 deleted file mode 100644 index 3c564d2d63b..00000000000 --- a/run-tests.ps1 +++ /dev/null @@ -1 +0,0 @@ -./build.ps1 -Target Tests diff --git a/run-tests.sh b/run-tests.sh deleted file mode 100755 index 289aa63dfdf..00000000000 --- a/run-tests.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash - -dotnet cake --target=CoreTests diff --git a/src/Core/Core.Tests/Execution/ArgumentTests.cs b/src/Core/Core.Tests/Execution/ArgumentTests.cs index fe94d38c945..18c3c326261 100644 --- a/src/Core/Core.Tests/Execution/ArgumentTests.cs +++ b/src/Core/Core.Tests/Execution/ArgumentTests.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using HotChocolate.Language; +using HotChocolate.Resolvers; using HotChocolate.Types; using Snapshooter.Xunit; using Xunit; @@ -224,6 +225,173 @@ await executor.ExecuteAsync( result.MatchSnapshot(); } + [Fact] + public async Task Invalid_InputObject_Provided_As_Variable() + { + ISchema schema = SchemaBuilder.New() + .AddDocumentFromString( + @"input MyInput { + someField: String + } + + type Query { + x(arg: MyInput): String + }") + .Map(new FieldReference("Query", "x"), + next => ctx => + { + ctx.Result = "Foo"; + return Task.CompletedTask; + }) + .Create(); + + IReadOnlyQueryRequest request = QueryRequestBuilder.New() + .SetQuery( + @"query MyQuery($value: MyInput) { + x(arg: $value) + }") + .AddVariableValue("value", new Dictionary + { + { "clearlyNonsense", "bar" } + }) + .Create(); + + IQueryExecutor executor = schema.MakeExecutable(); + + IExecutionResult result = await executor.ExecuteAsync(request); + + result.MatchSnapshot(); + } + + [Fact] + public async Task Invalid_InputObject_SecondLevel_Provided_As_Variable() + { + ISchema schema = SchemaBuilder.New() + .AddDocumentFromString( + @"input MyInput { + someObj: SecondInput + } + + input SecondInput { + someField: String + } + + type Query { + x(arg: MyInput): String + }") + .Map(new FieldReference("Query", "x"), + next => ctx => + { + ctx.Result = "Foo"; + return Task.CompletedTask; + }) + .Create(); + + IReadOnlyQueryRequest request = QueryRequestBuilder.New() + .SetQuery( + @"query MyQuery($value: MyInput) { + x(arg: $value) + }") + .AddVariableValue("value", new Dictionary + { + { "someObj", new Dictionary + { + { "clearlyNonsense", "baz" } + } + } + }) + .Create(); + + IQueryExecutor executor = schema.MakeExecutable(); + + IExecutionResult result = await executor.ExecuteAsync(request); + + result.MatchSnapshot(); + } + + [Fact] + public async Task Valid_InputObject_Provided_As_Variable() + { + ISchema schema = SchemaBuilder.New() + .AddDocumentFromString( + @"input MyInput { + someField: String + } + + type Query { + x(arg: MyInput): String + }") + .Map(new FieldReference("Query", "x"), + next => ctx => + { + ctx.Result = "Foo"; + return Task.CompletedTask; + }) + .Create(); + + IReadOnlyQueryRequest request = QueryRequestBuilder.New() + .SetQuery( + @"query MyQuery($value: MyInput) { + x(arg: $value) + }") + .AddVariableValue("value", new Dictionary + { + { "someField", "bar" } + }) + .Create(); + + IQueryExecutor executor = schema.MakeExecutable(); + + IExecutionResult result = await executor.ExecuteAsync(request); + + result.MatchSnapshot(); + } + + [Fact] + public async Task Valid_InputObject_SecondLevel_Provided_As_Variable() + { + ISchema schema = SchemaBuilder.New() + .AddDocumentFromString( + @"input MyInput { + someObj: SecondInput + } + + input SecondInput { + someField: String + } + + type Query { + x(arg: MyInput): String + }") + .Map(new FieldReference("Query", "x"), + next => ctx => + { + ctx.Result = "Foo"; + return Task.CompletedTask; + }) + .Create(); + + IReadOnlyQueryRequest request = QueryRequestBuilder.New() + .SetQuery( + @"query MyQuery($value: MyInput) { + x(arg: $value) + }") + .AddVariableValue("value", new Dictionary + { + { "someObj", new Dictionary + { + { "someField", "baz" } + } + } + }) + .Create(); + + IQueryExecutor executor = schema.MakeExecutable(); + + IExecutionResult result = await executor.ExecuteAsync(request); + + result.MatchSnapshot(); + } public class Query { diff --git a/src/Core/Core.Tests/Execution/__snapshots__/ArgumentTests.Invalid_InputObject_Provided_As_Variable.snap b/src/Core/Core.Tests/Execution/__snapshots__/ArgumentTests.Invalid_InputObject_Provided_As_Variable.snap new file mode 100644 index 00000000000..aede5cf0ed3 --- /dev/null +++ b/src/Core/Core.Tests/Execution/__snapshots__/ArgumentTests.Invalid_InputObject_Provided_As_Variable.snap @@ -0,0 +1,22 @@ +{ + "Data": {}, + "Extensions": {}, + "Errors": [ + { + "Message": "The value of $value has a wrong structure.", + "Code": "EXEC_INVALID_TYPE", + "Path": null, + "Locations": [ + { + "Line": 1, + "Column": 15 + } + ], + "Exception": null, + "Extensions": { + "code": "EXEC_INVALID_TYPE" + } + } + ], + "ContextData": {} +} diff --git a/src/Core/Core.Tests/Execution/__snapshots__/ArgumentTests.Invalid_InputObject_SecondLevel_Provided_As_Variable.snap b/src/Core/Core.Tests/Execution/__snapshots__/ArgumentTests.Invalid_InputObject_SecondLevel_Provided_As_Variable.snap new file mode 100644 index 00000000000..aede5cf0ed3 --- /dev/null +++ b/src/Core/Core.Tests/Execution/__snapshots__/ArgumentTests.Invalid_InputObject_SecondLevel_Provided_As_Variable.snap @@ -0,0 +1,22 @@ +{ + "Data": {}, + "Extensions": {}, + "Errors": [ + { + "Message": "The value of $value has a wrong structure.", + "Code": "EXEC_INVALID_TYPE", + "Path": null, + "Locations": [ + { + "Line": 1, + "Column": 15 + } + ], + "Exception": null, + "Extensions": { + "code": "EXEC_INVALID_TYPE" + } + } + ], + "ContextData": {} +} diff --git a/src/Core/Core.Tests/Execution/__snapshots__/ArgumentTests.Valid_InputObject_Provided_As_Variable.snap b/src/Core/Core.Tests/Execution/__snapshots__/ArgumentTests.Valid_InputObject_Provided_As_Variable.snap new file mode 100644 index 00000000000..b86932f95bc --- /dev/null +++ b/src/Core/Core.Tests/Execution/__snapshots__/ArgumentTests.Valid_InputObject_Provided_As_Variable.snap @@ -0,0 +1,8 @@ +{ + "Data": { + "x": "Foo" + }, + "Extensions": {}, + "Errors": [], + "ContextData": {} +} diff --git a/src/Core/Core.Tests/Execution/__snapshots__/ArgumentTests.Valid_InputObject_SecondLevel_Provided_As_Variable.snap b/src/Core/Core.Tests/Execution/__snapshots__/ArgumentTests.Valid_InputObject_SecondLevel_Provided_As_Variable.snap new file mode 100644 index 00000000000..b86932f95bc --- /dev/null +++ b/src/Core/Core.Tests/Execution/__snapshots__/ArgumentTests.Valid_InputObject_SecondLevel_Provided_As_Variable.snap @@ -0,0 +1,8 @@ +{ + "Data": { + "x": "Foo" + }, + "Extensions": {}, + "Errors": [], + "ContextData": {} +} diff --git a/src/Core/Core/Execution/Utilities/DictionaryToObjectValueConverter.cs b/src/Core/Core/Execution/Utilities/DictionaryToObjectValueConverter.cs index 368da44be1a..8eb328feb3c 100644 --- a/src/Core/Core/Execution/Utilities/DictionaryToObjectValueConverter.cs +++ b/src/Core/Core/Execution/Utilities/DictionaryToObjectValueConverter.cs @@ -84,7 +84,7 @@ protected override void VisitField( KeyValuePair field, ConverterContext context) { - VisitValue(field.Value, context); + Visit(field.Value, context); } protected override void VisitList( diff --git a/src/Core/Types.Filters.Mongo.Tests/MongoFilterTests.cs b/src/Core/Types.Filters.Mongo.Tests/MongoFilterTests.cs index 620dc6fab7a..b29d12bb93d 100644 --- a/src/Core/Types.Filters.Mongo.Tests/MongoFilterTests.cs +++ b/src/Core/Types.Filters.Mongo.Tests/MongoFilterTests.cs @@ -8,6 +8,7 @@ using Snapshooter.Xunit; using HotChocolate.Types.Relay; using Squadron; +using System; namespace HotChocolate.Types.Filters { @@ -93,6 +94,78 @@ public async Task GetItems_EqualsFilter_FirstItems_Is_Returned() result.MatchSnapshot(); } + [Fact] + public async Task DateTimeType_GreaterThan_Filter() + { + // arrange + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton>(sp => + { + IMongoDatabase database = _mongoResource.CreateDatabase(); + + var collection = database.GetCollection("col"); + collection.InsertMany(new[] + { + new Model { Time = new DateTime(2000, 1, 1, 1, 1, 1, DateTimeKind.Utc) }, + new Model { Time = new DateTime(2016, 1, 1, 1, 1, 1, DateTimeKind.Utc) }, + }); + return collection; + }); + + ISchema schema = SchemaBuilder.New() + .AddQueryType() + .AddServices(serviceCollection.BuildServiceProvider()) + .Create(); + + IQueryExecutor executor = schema.MakeExecutable(); + + IReadOnlyQueryRequest request = QueryRequestBuilder.New() + .SetQuery("{ items(where: { time_gt: \"2001-01-01\" }) { time } }") + .Create(); + + // act + IExecutionResult result = await executor.ExecuteAsync(request); + + // assert + result.MatchSnapshot(); + } + + [Fact] + public async Task DateType_GreaterThan_Filter() + { + // arrange + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton>(sp => + { + IMongoDatabase database = _mongoResource.CreateDatabase(); + + var collection = database.GetCollection("col"); + collection.InsertMany(new[] + { + new Model { Date = new DateTime(2000, 1, 1, 1, 1, 1, DateTimeKind.Utc).Date }, + new Model { Date = new DateTime(2016, 1, 1, 1, 1, 1, DateTimeKind.Utc).Date }, + }); + return collection; + }); + + ISchema schema = SchemaBuilder.New() + .AddQueryType() + .AddServices(serviceCollection.BuildServiceProvider()) + .Create(); + + IQueryExecutor executor = schema.MakeExecutable(); + + IReadOnlyQueryRequest request = QueryRequestBuilder.New() + .SetQuery("{ items(where: { date_gt: \"2001-01-01\" }) { date } }") + .Create(); + + // act + IExecutionResult result = await executor.ExecuteAsync(request); + + // assert + result.MatchSnapshot(); + } + [Fact] public async Task GetItems_With_Paging_EqualsFilter_FirstItems_Is_Returned() { @@ -129,6 +202,78 @@ public async Task GetItems_With_Paging_EqualsFilter_FirstItems_Is_Returned() result.MatchSnapshot(); } + [Fact] + public async Task Boolean_Filter_Equals() + { + // arrange + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton>(sp => + { + IMongoDatabase database = _mongoResource.CreateDatabase(); + + var collection = database.GetCollection("col"); + collection.InsertMany(new[] + { + new Model { Foo = "abc", Bar = 1, Baz = true }, + new Model { Foo = "def", Bar = 2, Baz = false }, + }); + return collection; + }); + + ISchema schema = SchemaBuilder.New() + .AddQueryType() + .AddServices(serviceCollection.BuildServiceProvider()) + .Create(); + + IQueryExecutor executor = schema.MakeExecutable(); + + IReadOnlyQueryRequest request = QueryRequestBuilder.New() + .SetQuery("{ paging(where: { baz: true }) { nodes { foo } } }") + .Create(); + + // act + IExecutionResult result = await executor.ExecuteAsync(request); + + // assert + result.MatchSnapshot(); + } + + [Fact] + public async Task Boolean_Filter_Not_Equals() + { + // arrange + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton>(sp => + { + IMongoDatabase database = _mongoResource.CreateDatabase(); + + var collection = database.GetCollection("col"); + collection.InsertMany(new[] + { + new Model { Foo = "abc", Bar = 1, Baz = true }, + new Model { Foo = "def", Bar = 2, Baz = false }, + }); + return collection; + }); + + ISchema schema = SchemaBuilder.New() + .AddQueryType() + .AddServices(serviceCollection.BuildServiceProvider()) + .Create(); + + IQueryExecutor executor = schema.MakeExecutable(); + + IReadOnlyQueryRequest request = QueryRequestBuilder.New() + .SetQuery("{ paging(where: { baz_not: false }) { nodes { foo } } }") + .Create(); + + // act + IExecutionResult result = await executor.ExecuteAsync(request); + + // assert + result.MatchSnapshot(); + } + public class QueryType : ObjectType { protected override void Configure(IObjectTypeDescriptor descriptor) @@ -156,6 +301,12 @@ protected override void Configure( descriptor.Field(t => t.Id) .Type() .Resolver(c => c.Parent().Id); + + descriptor.Field(t => t.Time) + .Type>(); + + descriptor.Field(t => t.Date) + .Type>(); } } @@ -165,6 +316,8 @@ public class Model public string Foo { get; set; } public int Bar { get; set; } public bool Baz { get; set; } + public DateTime Time { get; set; } + public DateTime Date { get; set; } } } } diff --git a/src/Core/Types.Filters.Mongo.Tests/__snapshots__/MongoFilterTests.Boolean_Filter_Equals.snap b/src/Core/Types.Filters.Mongo.Tests/__snapshots__/MongoFilterTests.Boolean_Filter_Equals.snap new file mode 100644 index 00000000000..21eafe41473 --- /dev/null +++ b/src/Core/Types.Filters.Mongo.Tests/__snapshots__/MongoFilterTests.Boolean_Filter_Equals.snap @@ -0,0 +1,14 @@ +{ + "Data": { + "paging": { + "nodes": [ + { + "foo": "abc" + } + ] + } + }, + "Extensions": {}, + "Errors": [], + "ContextData": {} +} diff --git a/src/Core/Types.Filters.Mongo.Tests/__snapshots__/MongoFilterTests.Boolean_Filter_Not_Equals.snap b/src/Core/Types.Filters.Mongo.Tests/__snapshots__/MongoFilterTests.Boolean_Filter_Not_Equals.snap new file mode 100644 index 00000000000..21eafe41473 --- /dev/null +++ b/src/Core/Types.Filters.Mongo.Tests/__snapshots__/MongoFilterTests.Boolean_Filter_Not_Equals.snap @@ -0,0 +1,14 @@ +{ + "Data": { + "paging": { + "nodes": [ + { + "foo": "abc" + } + ] + } + }, + "Extensions": {}, + "Errors": [], + "ContextData": {} +} diff --git a/src/Core/Types.Filters.Mongo.Tests/__snapshots__/MongoFilterTests.DateTimeType_GreaterThan_Filter.snap b/src/Core/Types.Filters.Mongo.Tests/__snapshots__/MongoFilterTests.DateTimeType_GreaterThan_Filter.snap new file mode 100644 index 00000000000..ca8987b42ae --- /dev/null +++ b/src/Core/Types.Filters.Mongo.Tests/__snapshots__/MongoFilterTests.DateTimeType_GreaterThan_Filter.snap @@ -0,0 +1,12 @@ +{ + "Data": { + "items": [ + { + "time": "2016-01-01T01:01:01.000Z" + } + ] + }, + "Extensions": {}, + "Errors": [], + "ContextData": {} +} diff --git a/src/Core/Types.Filters.Mongo.Tests/__snapshots__/MongoFilterTests.DateType_GreaterThan_Filter.snap b/src/Core/Types.Filters.Mongo.Tests/__snapshots__/MongoFilterTests.DateType_GreaterThan_Filter.snap new file mode 100644 index 00000000000..5403be1d40f --- /dev/null +++ b/src/Core/Types.Filters.Mongo.Tests/__snapshots__/MongoFilterTests.DateType_GreaterThan_Filter.snap @@ -0,0 +1,12 @@ +{ + "Data": { + "items": [ + { + "date": "2016-01-01" + } + ] + }, + "Extensions": {}, + "Errors": [], + "ContextData": {} +} diff --git a/src/Core/Types.Filters.Tests/ComparableFilterInputTypeTests.cs b/src/Core/Types.Filters.Tests/ComparableFilterInputTypeTests.cs index 19bf07e3d4f..c136f8a0eeb 100644 --- a/src/Core/Types.Filters.Tests/ComparableFilterInputTypeTests.cs +++ b/src/Core/Types.Filters.Tests/ComparableFilterInputTypeTests.cs @@ -240,6 +240,18 @@ public void Model_With_Nullable_Properties() schema.ToString().MatchSnapshot(); } + [Fact] + public void Infer_Nullable_Fields() + { + // arrange + // act + var schema = CreateSchema( + new FilterInputType()); + + // assert + schema.ToString().MatchSnapshot(); + } + public enum FooBar { Foo, diff --git a/src/Core/Types.Filters.Tests/FilterInputTypeTest.cs b/src/Core/Types.Filters.Tests/FilterInputTypeTest.cs index 8e933c908bf..1aa9f5fa498 100644 --- a/src/Core/Types.Filters.Tests/FilterInputTypeTest.cs +++ b/src/Core/Types.Filters.Tests/FilterInputTypeTest.cs @@ -13,7 +13,6 @@ public class FilterInputTypeTest [Fact] public void FilterInputType_DynamicName() { - // arrange // act var schema = CreateSchema(s => s.AddType(new FilterInputType( @@ -36,7 +35,6 @@ public void FilterInputType_DynamicName() [Fact] public void FilterInputType_DynamicName_NonGeneric() { - // arrange // act var schema = CreateSchema(s => s.AddType(new FilterInputType( @@ -70,7 +68,6 @@ public void FilterInput_AddDirectives_NameArgs() ) ); - // assert schema.ToString().MatchSnapshot(); } @@ -120,15 +117,11 @@ public void FilterInput_AddDirectives_DirectiveClassInstance() // arrange // act var schema = CreateSchema(s => s.AddDirectiveType() - .AddType(new FilterInputType( - d => d - .Directive(new FooDirective()) - .Filter(x => x.Bar) - .BindFiltersExplicitly() - .AllowEquals() - ) - ) - ); + .AddType(new FilterInputType(d => d + .Directive(new FooDirective()) + .Filter(x => x.Bar) + .BindFiltersExplicitly() + .AllowEquals()))); // assert schema.ToString().MatchSnapshot(); @@ -140,15 +133,11 @@ public void FilterInput_AddDirectives_DirectiveType() // arrange // act var schema = CreateSchema(s => s.AddDirectiveType() - .AddType(new FilterInputType( - d => d - .Directive() - .Filter(x => x.Bar) - .BindFiltersExplicitly() - .AllowEquals() - ) - ) - ); + .AddType(new FilterInputType(d => d + .Directive() + .Filter(x => x.Bar) + .BindFiltersExplicitly() + .AllowEquals()))); // assert schema.ToString().MatchSnapshot(); @@ -191,7 +180,7 @@ public void FilterInput_AddName() } private class FooDirectiveType - : DirectiveType + : DirectiveType { protected override void Configure( IDirectiveTypeDescriptor descriptor) @@ -201,6 +190,7 @@ protected override void Configure( .Location(DirectiveLocation.InputFieldDefinition); } } + private class FooDirective { } private class Foo diff --git a/src/Core/Types.Filters.Tests/QueryableFilterTests.cs b/src/Core/Types.Filters.Tests/QueryableFilterTests.cs index 16aba125203..d9d034ba06b 100644 --- a/src/Core/Types.Filters.Tests/QueryableFilterTests.cs +++ b/src/Core/Types.Filters.Tests/QueryableFilterTests.cs @@ -204,6 +204,60 @@ public void Execute_Filter_In() result.MatchSnapshot(); } + [Fact] + public void Execute_Filter_Comparable_In() + { + // arrange + ISchema schema = SchemaBuilder.New() + .AddQueryType(d => d.Field(t => t.Foos).UseFiltering()) + .Create(); + + IQueryExecutor executor = schema.MakeExecutable(); + + // act + IExecutionResult result = executor.Execute( + "{ foos(where: { baz_in: [ 1 0 ] }) { bar } }"); + + // assert + result.MatchSnapshot(); + } + + [Fact] + public void Execute_Filter_Nullable_Equals_1() + { + // arrange + ISchema schema = SchemaBuilder.New() + .AddQueryType(d => d.Field(t => t.Foos).UseFiltering()) + .Create(); + + IQueryExecutor executor = schema.MakeExecutable(); + + // act + IExecutionResult result = executor.Execute( + "{ foos(where: { qux: 1 }) { bar qux } }"); + + // assert + result.MatchSnapshot(); + } + + [Fact] + public void Execute_Filter_Nullable_Equals_Null() + { + // arrange + ISchema schema = SchemaBuilder.New() + .AddQueryType(d => d.Field(t => t.Foos).UseFiltering()) + .Create(); + + IQueryExecutor executor = schema.MakeExecutable(); + + // act + IExecutionResult result = executor.Execute( + "{ foos(where: { qux: null }) { bar qux } }"); + + // assert + result.MatchSnapshot(); + } + [Fact] public void Execute_Filter_Equals_And() { @@ -256,19 +310,25 @@ public class Query { public IEnumerable Foos { get; } = new[] { - new Foo { Bar = "aa" }, - new Foo { Bar = "ba" }, - new Foo { Bar = "ca" }, - new Foo { Bar = "ab" }, - new Foo { Bar = "ac" }, - new Foo { Bar = "ad" }, - new Foo { Bar = null } + new Foo { Bar = "aa", Baz = 1, Qux = 1 }, + new Foo { Bar = "ba", Baz = 1 }, + new Foo { Bar = "ca", Baz = 2 }, + new Foo { Bar = "ab", Baz = 2 }, + new Foo { Bar = "ac", Baz = 2 }, + new Foo { Bar = "ad", Baz = 2 }, + new Foo { Bar = null, Baz = 0 } }; } public class Foo { public string Bar { get; set; } + + [GraphQLType(typeof(NonNullType))] + public long Baz { get; set; } + + [GraphQLType(typeof(IntType))] + public int? Qux { get; set; } } } } diff --git a/src/Core/Types.Filters.Tests/__snapshots__/ComparableFilterInputTypeTests.Infer_Nullable_Fields.snap b/src/Core/Types.Filters.Tests/__snapshots__/ComparableFilterInputTypeTests.Infer_Nullable_Fields.snap new file mode 100644 index 00000000000..85e4dabb7a7 --- /dev/null +++ b/src/Core/Types.Filters.Tests/__snapshots__/ComparableFilterInputTypeTests.Infer_Nullable_Fields.snap @@ -0,0 +1,30 @@ +schema { + query: Query +} + +type Query { + foo: String +} + +input FooNullableFilter { + AND: [FooNullableFilter!] + barShort: Short + barShort_gt: Short + barShort_gte: Short + barShort_in: [Short] + barShort_lt: Short + barShort_lte: Short + barShort_not: Short + barShort_not_gt: Short + barShort_not_gte: Short + barShort_not_in: [Short] + barShort_not_lt: Short + barShort_not_lte: Short + OR: [FooNullableFilter!] +} + +"The `Short` scalar type represents non-fractional signed whole 16-bit numeric values. Short can represent values between -(2^15) and 2^15 - 1." +scalar Short + +"The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text." +scalar String diff --git a/src/Core/Types.Filters.Tests/__snapshots__/QueryableFilterTests.Create_Schema_With_FilteType.snap b/src/Core/Types.Filters.Tests/__snapshots__/QueryableFilterTests.Create_Schema_With_FilteType.snap index 15b7706013f..a41cc052619 100644 --- a/src/Core/Types.Filters.Tests/__snapshots__/QueryableFilterTests.Create_Schema_With_FilteType.snap +++ b/src/Core/Types.Filters.Tests/__snapshots__/QueryableFilterTests.Create_Schema_With_FilteType.snap @@ -4,6 +4,8 @@ type Foo { bar: String + baz: Int! + qux: Int } type Query { @@ -22,8 +24,35 @@ input FooFilter { bar_not_in: [String] bar_not_starts_with: String bar_starts_with: String + baz: Int + baz_gt: Int + baz_gte: Int + baz_in: [Int!] + baz_lt: Int + baz_lte: Int + baz_not: Int + baz_not_gt: Int + baz_not_gte: Int + baz_not_in: [Int!] + baz_not_lt: Int + baz_not_lte: Int OR: [FooFilter!] + qux: Int + qux_gt: Int + qux_gte: Int + qux_in: [Int] + qux_lt: Int + qux_lte: Int + qux_not: Int + qux_not_gt: Int + qux_not_gte: Int + qux_not_in: [Int] + qux_not_lt: Int + qux_not_lte: Int } +"The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1." +scalar Int + "The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text." scalar String diff --git a/src/Core/Types.Filters.Tests/__snapshots__/QueryableFilterTests.Create_Schema_With_FilteType_With_Fluent_API.snap b/src/Core/Types.Filters.Tests/__snapshots__/QueryableFilterTests.Create_Schema_With_FilteType_With_Fluent_API.snap index f993de973d1..cd66b3c007b 100644 --- a/src/Core/Types.Filters.Tests/__snapshots__/QueryableFilterTests.Create_Schema_With_FilteType_With_Fluent_API.snap +++ b/src/Core/Types.Filters.Tests/__snapshots__/QueryableFilterTests.Create_Schema_With_FilteType_With_Fluent_API.snap @@ -4,6 +4,8 @@ type Foo { bar: String + baz: Int! + qux: Int } type Query { @@ -16,5 +18,8 @@ input FooFilter { OR: [FooFilter!] } +"The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1." +scalar Int + "The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text." scalar String diff --git a/src/Core/Types.Filters.Tests/__snapshots__/QueryableFilterTests.Execute_Filter_Comparable_In.snap b/src/Core/Types.Filters.Tests/__snapshots__/QueryableFilterTests.Execute_Filter_Comparable_In.snap new file mode 100644 index 00000000000..c5a8d505454 --- /dev/null +++ b/src/Core/Types.Filters.Tests/__snapshots__/QueryableFilterTests.Execute_Filter_Comparable_In.snap @@ -0,0 +1,18 @@ +{ + "Data": { + "foos": [ + { + "bar": "aa" + }, + { + "bar": "ba" + }, + { + "bar": null + } + ] + }, + "Extensions": {}, + "Errors": [], + "ContextData": {} +} diff --git a/src/Core/Types.Filters.Tests/__snapshots__/QueryableFilterTests.Execute_Filter_Nullable_Equals_1.snap b/src/Core/Types.Filters.Tests/__snapshots__/QueryableFilterTests.Execute_Filter_Nullable_Equals_1.snap new file mode 100644 index 00000000000..818c9c35d42 --- /dev/null +++ b/src/Core/Types.Filters.Tests/__snapshots__/QueryableFilterTests.Execute_Filter_Nullable_Equals_1.snap @@ -0,0 +1,13 @@ +{ + "Data": { + "foos": [ + { + "bar": "aa", + "qux": 1 + } + ] + }, + "Extensions": {}, + "Errors": [], + "ContextData": {} +} diff --git a/src/Core/Types.Filters.Tests/__snapshots__/QueryableFilterTests.Execute_Filter_Nullable_Equals_Null.snap b/src/Core/Types.Filters.Tests/__snapshots__/QueryableFilterTests.Execute_Filter_Nullable_Equals_Null.snap new file mode 100644 index 00000000000..b9b8300641b --- /dev/null +++ b/src/Core/Types.Filters.Tests/__snapshots__/QueryableFilterTests.Execute_Filter_Nullable_Equals_Null.snap @@ -0,0 +1,33 @@ +{ + "Data": { + "foos": [ + { + "bar": "ba", + "qux": null + }, + { + "bar": "ca", + "qux": null + }, + { + "bar": "ab", + "qux": null + }, + { + "bar": "ac", + "qux": null + }, + { + "bar": "ad", + "qux": null + }, + { + "bar": null, + "qux": null + } + ] + }, + "Extensions": {}, + "Errors": [], + "ContextData": {} +} diff --git a/src/Core/Types.Filters/Expressions/Boolean/BooleanEqualsOperationHandler.cs b/src/Core/Types.Filters/Expressions/Boolean/BooleanEqualsOperationHandler.cs index ac62f839faa..d41cc50086e 100644 --- a/src/Core/Types.Filters/Expressions/Boolean/BooleanEqualsOperationHandler.cs +++ b/src/Core/Types.Filters/Expressions/Boolean/BooleanEqualsOperationHandler.cs @@ -31,10 +31,8 @@ public bool TryHandle( return true; case FilterOperationKind.NotEquals: - expression = FilterExpressionBuilder.Not( - FilterExpressionBuilder.Equals( - property, parserValue) - ); + expression = FilterExpressionBuilder.NotEquals( + property, parserValue); return true; } diff --git a/src/Core/Types.Filters/Expressions/Comparable/ComparableEqualsOperationHandler.cs b/src/Core/Types.Filters/Expressions/Comparable/ComparableEqualsOperationHandler.cs index 48e9384e562..486fefb0c06 100644 --- a/src/Core/Types.Filters/Expressions/Comparable/ComparableEqualsOperationHandler.cs +++ b/src/Core/Types.Filters/Expressions/Comparable/ComparableEqualsOperationHandler.cs @@ -1,3 +1,4 @@ +using System; using System.Linq.Expressions; namespace HotChocolate.Types.Filters.Expressions @@ -8,20 +9,20 @@ public sealed class ComparableEqualsOperationHandler protected override bool TryCreateExpression( FilterOperation operation, MemberExpression property, - object parsedValue, + Func parseValue, out Expression expression) { switch (operation.Kind) { case FilterOperationKind.Equals: expression = FilterExpressionBuilder.Equals( - property, parsedValue); + property, parseValue()); return true; case FilterOperationKind.NotEquals: expression = FilterExpressionBuilder.Not( FilterExpressionBuilder.Equals( - property, parsedValue) + property, parseValue()) ); return true; diff --git a/src/Core/Types.Filters/Expressions/Comparable/ComparableGreaterThanOperationHandler.cs b/src/Core/Types.Filters/Expressions/Comparable/ComparableGreaterThanOperationHandler.cs index 4a5820dacce..6401bd215a2 100644 --- a/src/Core/Types.Filters/Expressions/Comparable/ComparableGreaterThanOperationHandler.cs +++ b/src/Core/Types.Filters/Expressions/Comparable/ComparableGreaterThanOperationHandler.cs @@ -1,3 +1,4 @@ +using System; using System.Linq.Expressions; namespace HotChocolate.Types.Filters.Expressions @@ -8,20 +9,20 @@ public sealed class ComparableGreaterThanOperationHandler protected override bool TryCreateExpression( FilterOperation operation, MemberExpression property, - object parsedValue, + Func parseValue, out Expression expression) { switch (operation.Kind) { case FilterOperationKind.GreaterThan: expression = FilterExpressionBuilder.GreaterThan( - property, parsedValue); + property, parseValue()); return true; case FilterOperationKind.NotGreaterThan: expression = FilterExpressionBuilder.Not( FilterExpressionBuilder.GreaterThan( - property, parsedValue) + property, parseValue()) ); return true; diff --git a/src/Core/Types.Filters/Expressions/Comparable/ComparableGreaterThanOrEqualsOperationHandler.cs b/src/Core/Types.Filters/Expressions/Comparable/ComparableGreaterThanOrEqualsOperationHandler.cs index 1544c9bd7b7..7c70873e7e7 100644 --- a/src/Core/Types.Filters/Expressions/Comparable/ComparableGreaterThanOrEqualsOperationHandler.cs +++ b/src/Core/Types.Filters/Expressions/Comparable/ComparableGreaterThanOrEqualsOperationHandler.cs @@ -1,3 +1,4 @@ +using System; using System.Linq.Expressions; namespace HotChocolate.Types.Filters.Expressions @@ -8,20 +9,20 @@ public sealed class ComparableGreaterThanOrEqualsOperationHandler protected override bool TryCreateExpression( FilterOperation operation, MemberExpression property, - object parsedValue, + Func parseValue, out Expression expression) { switch (operation.Kind) { case FilterOperationKind.GreaterThanOrEquals: expression = FilterExpressionBuilder.GreaterThanOrEqual( - property, parsedValue); + property, parseValue()); return true; case FilterOperationKind.NotGreaterThanOrEquals: expression = FilterExpressionBuilder.Not( FilterExpressionBuilder.GreaterThanOrEqual( - property, parsedValue) + property, parseValue()) ); return true; diff --git a/src/Core/Types.Filters/Expressions/Comparable/ComparableInOperationHandler.cs b/src/Core/Types.Filters/Expressions/Comparable/ComparableInOperationHandler.cs index 939522ed4fb..1ec6c9991c6 100644 --- a/src/Core/Types.Filters/Expressions/Comparable/ComparableInOperationHandler.cs +++ b/src/Core/Types.Filters/Expressions/Comparable/ComparableInOperationHandler.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq.Expressions; using HotChocolate.Language; using HotChocolate.Utilities; @@ -21,7 +22,6 @@ public bool TryHandle( { MemberExpression property = Expression.Property(instance, operation.Property); - var parsedValue = type.ParseLiteral(value); switch (operation.Kind) { @@ -29,7 +29,7 @@ public bool TryHandle( expression = FilterExpressionBuilder.In( property, operation.Property.PropertyType, - parsedValue); + ParseValue()); return true; case FilterOperationKind.NotIn: @@ -37,7 +37,7 @@ public bool TryHandle( FilterExpressionBuilder.In( property, operation.Property.PropertyType, - parsedValue) + ParseValue()) ); return true; } @@ -45,6 +45,25 @@ public bool TryHandle( expression = null; return false; + + object ParseValue() + { + var parsedValue = type.ParseLiteral(value); + Type elementType = type.ElementType().ToClrType(); + + if (operation.Property.PropertyType != elementType) + { + Type listType = typeof(List<>).MakeGenericType( + operation.Property.PropertyType); + + parsedValue = converter.Convert( + typeof(object), + listType, + parsedValue); + } + + return parsedValue; + } } } } diff --git a/src/Core/Types.Filters/Expressions/Comparable/ComparableLowerThanOperationHandler.cs b/src/Core/Types.Filters/Expressions/Comparable/ComparableLowerThanOperationHandler.cs index 944b24e1bc4..24a0acfee8c 100644 --- a/src/Core/Types.Filters/Expressions/Comparable/ComparableLowerThanOperationHandler.cs +++ b/src/Core/Types.Filters/Expressions/Comparable/ComparableLowerThanOperationHandler.cs @@ -1,3 +1,4 @@ +using System; using System.Linq.Expressions; namespace HotChocolate.Types.Filters.Expressions @@ -8,20 +9,20 @@ public sealed class ComparableLowerThanOperationHandler protected override bool TryCreateExpression( FilterOperation operation, MemberExpression property, - object parsedValue, + Func parseValue, out Expression expression) { switch (operation.Kind) { case FilterOperationKind.LowerThan: expression = FilterExpressionBuilder.LowerThan( - property, parsedValue); + property, parseValue()); return true; case FilterOperationKind.NotLowerThan: expression = FilterExpressionBuilder.Not( FilterExpressionBuilder.LowerThan( - property, parsedValue)); + property, parseValue())); return true; default: diff --git a/src/Core/Types.Filters/Expressions/Comparable/ComparableLowerThanOrEqualsOperationHandler.cs b/src/Core/Types.Filters/Expressions/Comparable/ComparableLowerThanOrEqualsOperationHandler.cs index 5a885be69b8..8356b50d4f5 100644 --- a/src/Core/Types.Filters/Expressions/Comparable/ComparableLowerThanOrEqualsOperationHandler.cs +++ b/src/Core/Types.Filters/Expressions/Comparable/ComparableLowerThanOrEqualsOperationHandler.cs @@ -1,3 +1,4 @@ +using System; using System.Linq.Expressions; namespace HotChocolate.Types.Filters.Expressions @@ -8,20 +9,20 @@ public sealed class ComparableLowerThanOrEqualsOperationHandler protected override bool TryCreateExpression( FilterOperation operation, MemberExpression property, - object parsedValue, + Func parseValue, out Expression expression) { switch (operation.Kind) { case FilterOperationKind.LowerThanOrEquals: expression = FilterExpressionBuilder.LowerThanOrEqual( - property, parsedValue); + property, parseValue()); return true; case FilterOperationKind.NotLowerThanOrEquals: expression = FilterExpressionBuilder.Not( FilterExpressionBuilder.LowerThanOrEqual( - property, parsedValue)); + property, parseValue())); return true; default: diff --git a/src/Core/Types.Filters/Expressions/Comparable/ComparableOperationHandlerBase.cs b/src/Core/Types.Filters/Expressions/Comparable/ComparableOperationHandlerBase.cs index b7d83294f0d..db7c049d83c 100644 --- a/src/Core/Types.Filters/Expressions/Comparable/ComparableOperationHandlerBase.cs +++ b/src/Core/Types.Filters/Expressions/Comparable/ComparableOperationHandlerBase.cs @@ -6,7 +6,7 @@ namespace HotChocolate.Types.Filters.Expressions { public abstract class ComparableOperationHandlerBase - : IExpressionOperationHandler + : IExpressionOperationHandler { public bool TryHandle( FilterOperation operation, @@ -19,12 +19,23 @@ public bool TryHandle( if (operation.Type == typeof(IComparable) && type.IsInstanceOfType(value)) { - MemberExpression property = - Expression.Property(instance, operation.Property); + MemberExpression property = Expression.Property(instance, operation.Property); + + return TryCreateExpression( + operation, + property, + ParseValue, + out expression); + } + + expression = null; + return false; + + object ParseValue() + { var parsedValue = type.ParseLiteral(value); - if (operation.Property.PropertyType - .IsInstanceOfType(parsedValue)) + if (!operation.Property.PropertyType.IsInstanceOfType(parsedValue)) { parsedValue = converter.Convert( typeof(object), @@ -32,21 +43,14 @@ public bool TryHandle( parsedValue); } - return TryCreateExpression( - operation, - property, - parsedValue, - out expression); + return parsedValue; } - - expression = null; - return false; } protected abstract bool TryCreateExpression( FilterOperation operation, MemberExpression property, - object parsedValue, + Func parseValue, out Expression expression); } } diff --git a/src/Core/Types.Filters/Expressions/FilterExpressionBuilder.cs b/src/Core/Types.Filters/Expressions/FilterExpressionBuilder.cs index 00bc8fb049e..a9fcec6a451 100644 --- a/src/Core/Types.Filters/Expressions/FilterExpressionBuilder.cs +++ b/src/Core/Types.Filters/Expressions/FilterExpressionBuilder.cs @@ -7,7 +7,6 @@ namespace HotChocolate.Types.Filters.Expressions { public static class FilterExpressionBuilder { - private static readonly MethodInfo _startsWith = typeof(string).GetMethods().Single(m => m.Name.Equals("StartsWith") @@ -43,14 +42,18 @@ public static Expression Equals( MemberExpression property, object value) { - return Expression.Equal(property, NullableSafeConstantExpression(value, property.Type)); + return Expression.Equal( + property, + NullableSafeConstantExpression(value, property.Type)); } public static Expression NotEquals( MemberExpression property, object value) { - return Expression.NotEqual(property, NullableSafeConstantExpression(value, property.Type)); + return Expression.NotEqual( + property, + NullableSafeConstantExpression(value, property.Type)); } public static Expression In( @@ -71,28 +74,36 @@ public static Expression GreaterThan( MemberExpression property, object value) { - return Expression.GreaterThan(property, NullableSafeConstantExpression(value, property.Type)); + return Expression.GreaterThan( + property, + NullableSafeConstantExpression(value, property.Type)); } public static Expression GreaterThanOrEqual( MemberExpression property, object value) { - return Expression.GreaterThanOrEqual(property, NullableSafeConstantExpression(value, property.Type)); + return Expression.GreaterThanOrEqual( + property, + NullableSafeConstantExpression(value, property.Type)); } public static Expression LowerThan( MemberExpression property, object value) { - return Expression.LessThan(property, NullableSafeConstantExpression(value, property.Type)); + return Expression.LessThan( + property, + NullableSafeConstantExpression(value, property.Type)); } public static Expression LowerThanOrEqual( MemberExpression property, object value) { - return Expression.LessThanOrEqual(property, NullableSafeConstantExpression(value, property.Type)); + return Expression.LessThanOrEqual( + property, + NullableSafeConstantExpression(value, property.Type)); } public static Expression StartsWith( @@ -127,7 +138,8 @@ public static Expression NotContains( { return Expression.OrElse( Expression.Equal(property, Expression.Constant(null)), - Expression.Not(Expression.Call(property, _contains, Expression.Constant(value)))); + Expression.Not(Expression.Call( + property, _contains, Expression.Constant(value)))); } } } diff --git a/src/Core/Types.Filters/FilterInputTypeDescriptor.cs b/src/Core/Types.Filters/FilterInputTypeDescriptor.cs index 5ee521d19d3..5e15c49baf2 100644 --- a/src/Core/Types.Filters/FilterInputTypeDescriptor.cs +++ b/src/Core/Types.Filters/FilterInputTypeDescriptor.cs @@ -154,7 +154,7 @@ private bool TryCreateImplicitFilter( return true; } - if (typeof(IComparable).IsAssignableFrom(property.PropertyType)) + if (IsComparable(property.PropertyType)) { var field = new ComparableFilterFieldDescriptor( Context, property); @@ -166,6 +166,24 @@ private bool TryCreateImplicitFilter( return false; } + private bool IsComparable(Type type) + { + if (typeof(IComparable).IsAssignableFrom(type)) + { + return true; + } + + if (type.IsValueType + && type.IsGenericType + && type.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + return typeof(IComparable).IsAssignableFrom( + Nullable.GetUnderlyingType(type)); + } + + return false; + } + public IFilterInputTypeDescriptor BindFields( BindingBehavior bindingBehavior) {