From a30f0d7420e859afb6d16b82b3554fe3aad0cf51 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Thu, 10 Mar 2022 17:34:30 +1100 Subject: [PATCH 01/12] migrating cosmos tests to use variables --- .../CosmosTests/MutationTests.cs | 28 ++++++++-------- .../CosmosTests/QueryTests.cs | 33 ++++++++++++------- .../CosmosTests/TestBase.cs | 7 ++-- 3 files changed, 39 insertions(+), 29 deletions(-) diff --git a/DataGateway.Service.Tests/CosmosTests/MutationTests.cs b/DataGateway.Service.Tests/CosmosTests/MutationTests.cs index 703839846c..5806f9f1ff 100644 --- a/DataGateway.Service.Tests/CosmosTests/MutationTests.cs +++ b/DataGateway.Service.Tests/CosmosTests/MutationTests.cs @@ -10,23 +10,23 @@ public class MutationTests : TestBase { private static readonly string _containerName = Guid.NewGuid().ToString(); private static readonly string _mutationStringFormat = @" - mutation - {{ - addPlanet (id: ""{0}"", name: ""{1}"") - {{ + mutation ($id: ID!, $name: String!) + { + addPlanet (id: $id, name: $name) + { id name - }} - }}"; + } + }"; private static readonly string _mutationDeleteItemStringFormat = @" - mutation - {{ - deletePlanet (id: ""{0}"") - {{ + mutation ($id: ID!) + { + deletePlanet (id: $id) + { id name - }} - }}"; + } + }"; /// /// Executes once for the test class. @@ -49,14 +49,14 @@ public async Task TestMutationRun() // Run mutation Add planet; String id = Guid.NewGuid().ToString(); string mutation = String.Format(_mutationStringFormat, id, "test_name"); - JsonElement response = await ExecuteGraphQLRequestAsync("addPlanet", mutation); + JsonElement response = await ExecuteGraphQLRequestAsync("addPlanet", mutation, new() { { "$id", id }, { "$name", "test_name" } }); // Validate results Assert.IsFalse(response.ToString().Contains("Error")); // Run mutation delete item; mutation = String.Format(_mutationDeleteItemStringFormat, id); - response = await ExecuteGraphQLRequestAsync("deletePlanet", mutation); + response = await ExecuteGraphQLRequestAsync("deletePlanet", mutation, new() { { "$id", id } }); // Validate results Assert.IsFalse(response.ToString().Contains("Error")); diff --git a/DataGateway.Service.Tests/CosmosTests/QueryTests.cs b/DataGateway.Service.Tests/CosmosTests/QueryTests.cs index ec6b2caae8..1e436eb8f5 100644 --- a/DataGateway.Service.Tests/CosmosTests/QueryTests.cs +++ b/DataGateway.Service.Tests/CosmosTests/QueryTests.cs @@ -11,15 +11,25 @@ public class QueryTests : TestBase { private static readonly string _containerName = Guid.NewGuid().ToString(); - public static readonly string PlanetByIdQueryFormat = @"{{planetById (id: {0}){{ id, name}} }}"; + public static readonly string PlanetByIdQueryFormat = @" +query ($id: ID) { + planetById (id: $id) { + id + name + } +}"; public static readonly string PlanetListQuery = @"{planetList{ id, name}}"; public static readonly string PlanetConnectionQueryStringFormat = @" - {{planets (first: {0}, after: {1}){{ - items{{ id name }} - endCursor - hasNextPage - }} - }}"; +query ($first: Int!, $after: String) { + planets (first: $first, after: $after) { + items { + id + name + } + endCursor + hasNextPage + } +}"; private static List _idList; @@ -42,8 +52,8 @@ public static void TestFixtureSetup(TestContext context) public async Task TestSimpleQuery() { // Run query - string query = string.Format(PlanetByIdQueryFormat, arg0: "\"" + _idList[0] + "\""); - JsonElement response = await ExecuteGraphQLRequestAsync("planetById", query); + string id = _idList[0]; + JsonElement response = await ExecuteGraphQLRequestAsync("planetById", PlanetByIdQueryFormat, new() { { "$id", id } }); // Validate results Assert.IsFalse(response.ToString().Contains("Error")); @@ -57,7 +67,7 @@ public async Task TestSimpleQuery() public async Task TestPaginatedQuery() { // Run query - JsonElement response = await ExecuteGraphQLRequestAsync("planetList", PlanetListQuery); + JsonElement response = await ExecuteGraphQLRequestAsync("planetList", PlanetListQuery, new()); int actualElements = response.GetArrayLength(); // Run paginated query int totalElementsFromPaginatedQuery = 0; @@ -73,8 +83,7 @@ public async Task TestPaginatedQuery() continuationToken = "\"" + continuationToken + "\""; } - string paginatedQuery = string.Format(PlanetConnectionQueryStringFormat, arg0: pagesize, arg1: continuationToken); - JsonElement page = await ExecuteGraphQLRequestAsync("planets", paginatedQuery); + JsonElement page = await ExecuteGraphQLRequestAsync("planets", PlanetConnectionQueryStringFormat, new() { { "$first", pagesize }, { "$after", continuationToken } }); JsonElement continuation = page.GetProperty("endCursor"); continuationToken = continuation.ToString(); totalElementsFromPaginatedQuery += page.GetProperty("items").GetArrayLength(); diff --git a/DataGateway.Service.Tests/CosmosTests/TestBase.cs b/DataGateway.Service.Tests/CosmosTests/TestBase.cs index 60ea2fc06e..494e815ebb 100644 --- a/DataGateway.Service.Tests/CosmosTests/TestBase.cs +++ b/DataGateway.Service.Tests/CosmosTests/TestBase.cs @@ -125,13 +125,14 @@ internal static void RegisterGraphQLType(string id, /// Executes the GraphQL request and returns the results /// /// Name of the GraphQL query/mutation - /// The GraphQL query/mutation + /// The GraphQL query/mutation /// - internal static async Task ExecuteGraphQLRequestAsync(string queryName, string graphQLQuery) + internal static async Task ExecuteGraphQLRequestAsync(string queryName, string query, Dictionary variables) { string queryJson = JObject.FromObject(new { - query = graphQLQuery + query, + variables }).ToString(); _controller.ControllerContext.HttpContext = GetHttpContextWithBody(queryJson); JsonElement graphQLResult = await _controller.PostAsync(); From 72c8c9ec45bb527683295383c3af08bdfe2c918c Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Thu, 10 Mar 2022 21:50:03 +1100 Subject: [PATCH 02/12] supporting variables for cosmos graphql tests --- .../CosmosTests/MutationTests.cs | 10 ++++---- .../CosmosTests/QueryTests.cs | 15 ++++-------- .../Resolvers/CosmosQueryStructure.cs | 4 ++-- .../Sql Query Structures/SqlQueryStructure.cs | 2 +- .../Services/GraphQLService.cs | 4 +++- .../Services/ResolverMiddleware.cs | 24 +++++++++---------- 6 files changed, 25 insertions(+), 34 deletions(-) diff --git a/DataGateway.Service.Tests/CosmosTests/MutationTests.cs b/DataGateway.Service.Tests/CosmosTests/MutationTests.cs index 5806f9f1ff..64fc9f4c41 100644 --- a/DataGateway.Service.Tests/CosmosTests/MutationTests.cs +++ b/DataGateway.Service.Tests/CosmosTests/MutationTests.cs @@ -10,7 +10,7 @@ public class MutationTests : TestBase { private static readonly string _containerName = Guid.NewGuid().ToString(); private static readonly string _mutationStringFormat = @" - mutation ($id: ID!, $name: String!) + mutation ($id: String, $name: String) { addPlanet (id: $id, name: $name) { @@ -19,7 +19,7 @@ public class MutationTests : TestBase } }"; private static readonly string _mutationDeleteItemStringFormat = @" - mutation ($id: ID!) + mutation ($id: String) { deletePlanet (id: $id) { @@ -48,15 +48,13 @@ public async Task TestMutationRun() { // Run mutation Add planet; String id = Guid.NewGuid().ToString(); - string mutation = String.Format(_mutationStringFormat, id, "test_name"); - JsonElement response = await ExecuteGraphQLRequestAsync("addPlanet", mutation, new() { { "$id", id }, { "$name", "test_name" } }); + JsonElement response = await ExecuteGraphQLRequestAsync("addPlanet", _mutationStringFormat, new() { { "id", id }, { "name", "test_name" } }); // Validate results Assert.IsFalse(response.ToString().Contains("Error")); // Run mutation delete item; - mutation = String.Format(_mutationDeleteItemStringFormat, id); - response = await ExecuteGraphQLRequestAsync("deletePlanet", mutation, new() { { "$id", id } }); + response = await ExecuteGraphQLRequestAsync("deletePlanet", _mutationDeleteItemStringFormat, new() { { "id", id } }); // Validate results Assert.IsFalse(response.ToString().Contains("Error")); diff --git a/DataGateway.Service.Tests/CosmosTests/QueryTests.cs b/DataGateway.Service.Tests/CosmosTests/QueryTests.cs index 1e436eb8f5..f4e1c10b5c 100644 --- a/DataGateway.Service.Tests/CosmosTests/QueryTests.cs +++ b/DataGateway.Service.Tests/CosmosTests/QueryTests.cs @@ -20,7 +20,7 @@ public class QueryTests : TestBase }"; public static readonly string PlanetListQuery = @"{planetList{ id, name}}"; public static readonly string PlanetConnectionQueryStringFormat = @" -query ($first: Int!, $after: String) { +query ($first: Int, $after: String) { planets (first: $first, after: $after) { items { id @@ -53,7 +53,7 @@ public async Task TestSimpleQuery() { // Run query string id = _idList[0]; - JsonElement response = await ExecuteGraphQLRequestAsync("planetById", PlanetByIdQueryFormat, new() { { "$id", id } }); + JsonElement response = await ExecuteGraphQLRequestAsync("planetById", PlanetByIdQueryFormat, new() { { "id", id } }); // Validate results Assert.IsFalse(response.ToString().Contains("Error")); @@ -71,19 +71,12 @@ public async Task TestPaginatedQuery() int actualElements = response.GetArrayLength(); // Run paginated query int totalElementsFromPaginatedQuery = 0; - string continuationToken = "null"; + string continuationToken = null; const int pagesize = 5; do { - if (continuationToken != "null") - { - // We need to append an escape quote to continuation token because of the way we are using string.format - // for generating the graphql paginated query stringformat for this test. - continuationToken = "\"" + continuationToken + "\""; - } - - JsonElement page = await ExecuteGraphQLRequestAsync("planets", PlanetConnectionQueryStringFormat, new() { { "$first", pagesize }, { "$after", continuationToken } }); + JsonElement page = await ExecuteGraphQLRequestAsync("planets", PlanetConnectionQueryStringFormat, new() { { "first", pagesize }, { "after", continuationToken } }); JsonElement continuation = page.GetProperty("endCursor"); continuationToken = continuation.ToString(); totalElementsFromPaginatedQuery += page.GetProperty("items").GetArrayLength(); diff --git a/DataGateway.Service/Resolvers/CosmosQueryStructure.cs b/DataGateway.Service/Resolvers/CosmosQueryStructure.cs index e341abb7d6..a54a301a59 100644 --- a/DataGateway.Service/Resolvers/CosmosQueryStructure.cs +++ b/DataGateway.Service/Resolvers/CosmosQueryStructure.cs @@ -17,7 +17,7 @@ public class CosmosQueryStructure : BaseSqlQueryStructure public string Container { get; internal set; } public string Database { get; internal set; } public string? Continuation { get; internal set; } - public long MaxItemCount { get; internal set; } + public int MaxItemCount { get; internal set; } public CosmosQueryStructure(IMiddlewareContext context, IDictionary parameters, @@ -59,7 +59,7 @@ private void Init(IDictionary queryParams) // TODO: Revisit 'first' while adding support for TOP queries if (parameter.Key == "first") { - MaxItemCount = (long)parameter.Value; + MaxItemCount = (int)parameter.Value; continue; } diff --git a/DataGateway.Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs b/DataGateway.Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs index 7b1c2dc7fb..ff10d6517a 100644 --- a/DataGateway.Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs +++ b/DataGateway.Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs @@ -458,7 +458,7 @@ void AddGraphQLFields(IReadOnlyList Selections) { IObjectField? subschemaField = _underlyingFieldType.Fields[fieldName]; - IDictionary subqueryParams = ResolverMiddleware.GetParametersFromSchemaAndQueryFields(subschemaField, field); + IDictionary subqueryParams = ResolverMiddleware.GetParametersFromSchemaAndQueryFields(subschemaField, field, null); if (_ctx == null) { throw new InvalidOperationException("No GraphQL context exists"); diff --git a/DataGateway.Service/Services/GraphQLService.cs b/DataGateway.Service/Services/GraphQLService.cs index 329417eaba..b81233794d 100644 --- a/DataGateway.Service/Services/GraphQLService.cs +++ b/DataGateway.Service/Services/GraphQLService.cs @@ -9,6 +9,7 @@ using HotChocolate.Execution.Configuration; using HotChocolate.Types; using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; namespace Azure.DataGateway.Service.Services { @@ -129,7 +130,8 @@ private static IQueryRequest CompileRequest(string requestBody, Dictionary>(requestBodyJson.RootElement.GetProperty("variables").ToString()!)); // Individually adds each property to requestBuilder if they are provided. // Avoids using SetProperties() as it detrimentally overwrites diff --git a/DataGateway.Service/Services/ResolverMiddleware.cs b/DataGateway.Service/Services/ResolverMiddleware.cs index eb5ddba4a6..597ed0d523 100644 --- a/DataGateway.Service/Services/ResolverMiddleware.cs +++ b/DataGateway.Service/Services/ResolverMiddleware.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Azure.DataGateway.Service.Models; using Azure.DataGateway.Service.Resolvers; +using HotChocolate.Execution; using HotChocolate.Language; using HotChocolate.Resolvers; using HotChocolate.Types; @@ -119,17 +120,14 @@ protected static bool IsInnerObject(IMiddlewareContext context) return context.Selection.Field.Type.IsObjectType() && context.Parent() != default; } - static private object ArgumentValue(IValueNode value) + static private object ArgumentValue(IValueNode value, IVariableValueCollection variables) { - if (value.Kind == SyntaxKind.IntValue) + return value.Kind switch { - IntValueNode intValue = (IntValueNode)value; - return intValue.ToInt64(); - } - else - { - return value.Value; - } + SyntaxKind.IntValue => ((IntValueNode)value).ToInt32(), + SyntaxKind.Variable => variables.GetVariable(((VariableNode)value).Value), + _ => value.Value + }; } /// @@ -137,7 +135,7 @@ static private object ArgumentValue(IValueNode value) /// Extracts defualt parameter values from the schema or null if no default /// Overrides default values with actual values of parameters provided /// - public static IDictionary GetParametersFromSchemaAndQueryFields(IObjectField schema, FieldNode query) + public static IDictionary GetParametersFromSchemaAndQueryFields(IObjectField schema, FieldNode query, IVariableValueCollection variables) { IDictionary parameters = new Dictionary(); @@ -151,7 +149,7 @@ public static IDictionary GetParametersFromSchemaAndQueryFields( } else { - parameters.Add(argument.Name.Value, ArgumentValue(argument.DefaultValue)); + parameters.Add(argument.Name.Value, ArgumentValue(argument.DefaultValue, variables)); } } @@ -159,7 +157,7 @@ public static IDictionary GetParametersFromSchemaAndQueryFields( IReadOnlyList passedArguments = query.Arguments; foreach (ArgumentNode argument in passedArguments) { - parameters[argument.Name.Value] = ArgumentValue(argument.Value); + parameters[argument.Name.Value] = ArgumentValue(argument.Value, variables); } return parameters; @@ -167,7 +165,7 @@ public static IDictionary GetParametersFromSchemaAndQueryFields( protected static IDictionary GetParametersFromContext(IMiddlewareContext context) { - return GetParametersFromSchemaAndQueryFields(context.Selection.Field, context.Selection.SyntaxNode); + return GetParametersFromSchemaAndQueryFields(context.Selection.Field, context.Selection.SyntaxNode, context.Variables); } /// From 7b8ac7f5f4875894a2a1aa4579436ca816b21e6e Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Thu, 10 Mar 2022 22:07:44 +1100 Subject: [PATCH 03/12] handling no variables in request --- DataGateway.Service/Services/GraphQLService.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/DataGateway.Service/Services/GraphQLService.cs b/DataGateway.Service/Services/GraphQLService.cs index b81233794d..fecd8ae27b 100644 --- a/DataGateway.Service/Services/GraphQLService.cs +++ b/DataGateway.Service/Services/GraphQLService.cs @@ -130,8 +130,16 @@ private static IQueryRequest CompileRequest(string requestBody, Dictionary>(requestBodyJson.RootElement.GetProperty("variables").ToString()!)); + .SetQuery(requestBodyJson.RootElement.GetProperty("query").GetString()!); + + JsonElement variables; + if (requestBodyJson.RootElement.TryGetProperty("variables", out variables)) + { + requestBuilder = + requestBuilder.SetVariableValues( + JsonConvert.DeserializeObject>(variables.ToString()!) + ); + } // Individually adds each property to requestBuilder if they are provided. // Avoids using SetProperties() as it detrimentally overwrites From cd1837dccf1adbd64c9af66023c23800c78617c8 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Fri, 11 Mar 2022 09:51:53 +1100 Subject: [PATCH 04/12] Adding test cases for not using variables in Cosmos queries --- .../CosmosTests/QueryTests.cs | 61 +++++++++++++++++-- .../CosmosTests/TestBase.cs | 34 ++++++++++- 2 files changed, 90 insertions(+), 5 deletions(-) diff --git a/DataGateway.Service.Tests/CosmosTests/QueryTests.cs b/DataGateway.Service.Tests/CosmosTests/QueryTests.cs index f4e1c10b5c..af29835d3e 100644 --- a/DataGateway.Service.Tests/CosmosTests/QueryTests.cs +++ b/DataGateway.Service.Tests/CosmosTests/QueryTests.cs @@ -20,7 +20,7 @@ public class QueryTests : TestBase }"; public static readonly string PlanetListQuery = @"{planetList{ id, name}}"; public static readonly string PlanetConnectionQueryStringFormat = @" -query ($first: Int, $after: String) { +query ($first: Int!, $after: String) { planets (first: $first, after: $after) { items { id @@ -49,14 +49,14 @@ public static void TestFixtureSetup(TestContext context) } [TestMethod] - public async Task TestSimpleQuery() + public async Task GetByPrimaryKeyWithVariables() { // Run query string id = _idList[0]; JsonElement response = await ExecuteGraphQLRequestAsync("planetById", PlanetByIdQueryFormat, new() { { "id", id } }); // Validate results - Assert.IsFalse(response.ToString().Contains("Error")); + Assert.AreEqual(id, response.GetProperty("id").GetString()); } /// @@ -64,7 +64,7 @@ public async Task TestSimpleQuery() /// running a paginated query that gets n items per page. We then make sure the number of documents match /// [TestMethod] - public async Task TestPaginatedQuery() + public async Task GetPaginatedWithVariables() { // Run query JsonElement response = await ExecuteGraphQLRequestAsync("planetList", PlanetListQuery, new()); @@ -86,6 +86,59 @@ public async Task TestPaginatedQuery() Assert.AreEqual(actualElements, totalElementsFromPaginatedQuery); } + [TestMethod] + public async Task GetByPrimaryKeyWithoutVariables() + { + // Run query + string id = _idList[0]; + string query = @$" +query {{ + planetById (id: ""{id}"") {{ + id + name + }} +}}"; + JsonElement response = await ExecuteGraphQLRequestAsync("planetById", query, new()); + + // Validate results + Assert.AreEqual(id, response.GetProperty("id").GetString()); + } + + [TestMethod] + public async Task GetPaginatedWithoutVariables() + { + // Run query + JsonElement response = await ExecuteGraphQLRequestAsync("planetList", PlanetListQuery, new()); + int actualElements = response.GetArrayLength(); + // Run paginated query + int totalElementsFromPaginatedQuery = 0; + string continuationToken = null; + const int pagesize = 5; + + do + { + string planetConnectionQueryStringFormat = @$" +query {{ + planets (first: {pagesize}, after: {(continuationToken == null ? "null" : "\"" + continuationToken + "\"")}) {{ + items {{ + id + name + }} + endCursor + hasNextPage + }} +}}"; + + JsonElement page = await ExecuteGraphQLRequestAsync("planets", planetConnectionQueryStringFormat, new()); + JsonElement continuation = page.GetProperty("endCursor"); + continuationToken = continuation.ToString(); + totalElementsFromPaginatedQuery += page.GetProperty("items").GetArrayLength(); + } while (!string.IsNullOrEmpty(continuationToken)); + + // Validate results + Assert.AreEqual(actualElements, totalElementsFromPaginatedQuery); + } + /// /// Runs once after all tests in this class are executed /// diff --git a/DataGateway.Service.Tests/CosmosTests/TestBase.cs b/DataGateway.Service.Tests/CosmosTests/TestBase.cs index 494e815ebb..aab54746a3 100644 --- a/DataGateway.Service.Tests/CosmosTests/TestBase.cs +++ b/DataGateway.Service.Tests/CosmosTests/TestBase.cs @@ -34,7 +34,39 @@ public static void Init(TestContext context) { _clientProvider = new CosmosClientProvider(TestHelper.DataGatewayConfig); _metadataStoreProvider = new MetadataStoreProviderForTest(); - string jsonString = File.ReadAllText("schema.gql"); + string jsonString = @" +type Query { + characterList: [Character] + characterById (id : ID!): Character + planetById (id: ID! = 1): Planet + getPlanet(id: ID, name: String): Planet + planetList: [Planet] + planets(first: Int, after: String): PlanetConnection +} + +type Mutation { + addPlanet(id: String, name: String): Planet + deletePlanet(id: String): Planet +} + +type PlanetConnection { + items: [Planet] + endCursor: String + hasNextPage: Boolean +} + +type Character { + id : ID, + name : String, + type: String, + homePlanet: Int, + primaryFunction: String +} + +type Planet { + id : ID, + name : String +}"; _metadataStoreProvider.GraphQLSchema = jsonString; _queryEngine = new CosmosQueryEngine(_clientProvider, _metadataStoreProvider); _mutationEngine = new CosmosMutationEngine(_clientProvider, _metadataStoreProvider); From db5cf5e5992120e7273a819a97601154d9c56dc4 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Fri, 11 Mar 2022 10:04:53 +1100 Subject: [PATCH 05/12] getting Cosmos mutation tests working with and without variables --- .../CosmosTests/MutationTests.cs | 66 +++++++++++++++++-- 1 file changed, 61 insertions(+), 5 deletions(-) diff --git a/DataGateway.Service.Tests/CosmosTests/MutationTests.cs b/DataGateway.Service.Tests/CosmosTests/MutationTests.cs index 64fc9f4c41..9621df58e9 100644 --- a/DataGateway.Service.Tests/CosmosTests/MutationTests.cs +++ b/DataGateway.Service.Tests/CosmosTests/MutationTests.cs @@ -44,20 +44,76 @@ public static void TestFixtureSetup(TestContext context) } [TestMethod] - public async Task TestMutationRun() + public async Task CanCreateItemWithVariables() { // Run mutation Add planet; - String id = Guid.NewGuid().ToString(); + string id = Guid.NewGuid().ToString(); JsonElement response = await ExecuteGraphQLRequestAsync("addPlanet", _mutationStringFormat, new() { { "id", id }, { "name", "test_name" } }); // Validate results - Assert.IsFalse(response.ToString().Contains("Error")); + Assert.AreEqual(id, response.GetProperty("id").GetString()); + } + + [TestMethod] + public async Task CanDeleteItemWithVariables() + { + // Pop an item in to delete + string id = Guid.NewGuid().ToString(); + _ = await ExecuteGraphQLRequestAsync("addPlanet", _mutationStringFormat, new() { { "id", id }, { "name", "test_name" } }); + + // Run mutation delete item; + JsonElement response = await ExecuteGraphQLRequestAsync("deletePlanet", _mutationDeleteItemStringFormat, new() { { "id", id } }); + + // Validate results + Assert.IsNull(response.GetProperty("id").GetString()); + } + + [TestMethod] + public async Task CanCreateItemWithoutVariables() + { + // Run mutation Add planet; + string id = Guid.NewGuid().ToString(); + const string name = "test_name"; + string mutation = $@" +mutation {{ + addPlanet (id: ""{id}"", name: ""{name}"") {{ + id + name + }} +}}"; + JsonElement response = await ExecuteGraphQLRequestAsync("addPlanet", mutation, new()); + + // Validate results + Assert.AreEqual(id, response.GetProperty("id").GetString()); + } + + [TestMethod] + public async Task CanDeleteItemWithoutVariables() + { + // Pop an item in to delete + string id = Guid.NewGuid().ToString(); + const string name = "test_name"; + string addMutation = $@" +mutation {{ + addPlanet (id: ""{id}"", name: ""{name}"") {{ + id + name + }} +}}"; + _ = await ExecuteGraphQLRequestAsync("addPlanet", addMutation, new()); // Run mutation delete item; - response = await ExecuteGraphQLRequestAsync("deletePlanet", _mutationDeleteItemStringFormat, new() { { "id", id } }); + string deleteMutation = $@" +mutation {{ + deletePlanet (id: ""{id}"") {{ + id + name + }} +}}"; + JsonElement response = await ExecuteGraphQLRequestAsync("deletePlanet", deleteMutation, new()); // Validate results - Assert.IsFalse(response.ToString().Contains("Error")); + Assert.IsNull(response.GetProperty("id").GetString()); } /// From 30ff83fb69c31b1d03782d25fc42c60a8319cc6f Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Fri, 11 Mar 2022 10:16:57 +1100 Subject: [PATCH 06/12] adding test coverage for MsSQL with variable queries --- .../SqlTests/MsSqlGraphQLQueryTests.cs | 19 +++++++++++++++++++ .../SqlTests/SqlTestBase.cs | 11 ++++++----- .../Sql Query Structures/SqlQueryStructure.cs | 2 +- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/DataGateway.Service.Tests/SqlTests/MsSqlGraphQLQueryTests.cs b/DataGateway.Service.Tests/SqlTests/MsSqlGraphQLQueryTests.cs index eaacfadda1..9f79942154 100644 --- a/DataGateway.Service.Tests/SqlTests/MsSqlGraphQLQueryTests.cs +++ b/DataGateway.Service.Tests/SqlTests/MsSqlGraphQLQueryTests.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Text.Json; using System.Threading.Tasks; using Azure.DataGateway.Service.Controllers; @@ -58,6 +59,24 @@ public async Task MultipleResultQuery() SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); } + [TestMethod] + public async Task MultipleResultQueryWithVariables() + { + string graphQLQueryName = "getBooks"; + string graphQLQuery = @"query ($first: Int!) { + getBooks(first: $first) { + id + title + } + }"; + string msSqlQuery = $"SELECT id, title FROM books ORDER BY id FOR JSON PATH, INCLUDE_NULL_VALUES"; + + string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController, new() { { "first", 100 } }); + string expected = await GetDatabaseResultAsync(msSqlQuery); + + SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + } + /// /// Gets array of results for querying more than one item. /// diff --git a/DataGateway.Service.Tests/SqlTests/SqlTestBase.cs b/DataGateway.Service.Tests/SqlTests/SqlTestBase.cs index 1b9da8340f..28996d827c 100644 --- a/DataGateway.Service.Tests/SqlTests/SqlTestBase.cs +++ b/DataGateway.Service.Tests/SqlTests/SqlTestBase.cs @@ -270,9 +270,9 @@ protected static void ConfigureRestController( /// /// /// string in JSON format - protected static async Task GetGraphQLResultAsync(string graphQLQuery, string graphQLQueryName, GraphQLController graphQLController) + protected static async Task GetGraphQLResultAsync(string graphQLQuery, string graphQLQueryName, GraphQLController graphQLController, Dictionary variables = null) { - JsonElement graphQLResult = await GetGraphQLControllerResultAsync(graphQLQuery, graphQLQueryName, graphQLController); + JsonElement graphQLResult = await GetGraphQLControllerResultAsync(graphQLQuery, graphQLQueryName, graphQLController, variables); Console.WriteLine(graphQLResult.ToString()); JsonElement graphQLResultData = graphQLResult.GetProperty("data").GetProperty(graphQLQueryName); @@ -284,15 +284,16 @@ protected static async Task GetGraphQLResultAsync(string graphQLQuery, s /// Sends graphQL query through graphQL service, consisting of gql engine processing (resolvers, object serialization) /// returning the result as a JsonDocument /// - /// + /// /// /// /// JsonDocument - protected static async Task GetGraphQLControllerResultAsync(string graphQLQuery, string graphQLQueryName, GraphQLController graphQLController) + protected static async Task GetGraphQLControllerResultAsync(string query, string graphQLQueryName, GraphQLController graphQLController, Dictionary variables = null) { string graphqlQueryJson = JObject.FromObject(new { - query = graphQLQuery + query, + variables }).ToString(); Console.WriteLine(graphqlQueryJson); diff --git a/DataGateway.Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs b/DataGateway.Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs index ff10d6517a..b8d4acab2f 100644 --- a/DataGateway.Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs +++ b/DataGateway.Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs @@ -224,7 +224,7 @@ IncrementingInteger counter if (firstObject != null) { // due to the way parameters get resolved, - long first = (long)firstObject; + int first = (int)firstObject; if (first <= 0) { From 3eed8ad1a40079c49e2653ff8ed95e11bde1c925 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Fri, 11 Mar 2022 10:31:54 +1100 Subject: [PATCH 07/12] unneeded using --- DataGateway.Service.Tests/SqlTests/MsSqlGraphQLQueryTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/DataGateway.Service.Tests/SqlTests/MsSqlGraphQLQueryTests.cs b/DataGateway.Service.Tests/SqlTests/MsSqlGraphQLQueryTests.cs index 9f79942154..91927e185f 100644 --- a/DataGateway.Service.Tests/SqlTests/MsSqlGraphQLQueryTests.cs +++ b/DataGateway.Service.Tests/SqlTests/MsSqlGraphQLQueryTests.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using System.Text.Json; using System.Threading.Tasks; using Azure.DataGateway.Service.Controllers; From 6c29896400f22723a0774f2d4cbdc41007aacac2 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Fri, 11 Mar 2022 10:43:07 +1100 Subject: [PATCH 08/12] variable coverage in MySql and PostgreSQL --- .../SqlTests/MySqlGraphQLQueryTests.cs | 26 +++++++++++++++++++ .../SqlTests/PostgreSqlGraphQLQueryTests.cs | 18 +++++++++++++ 2 files changed, 44 insertions(+) diff --git a/DataGateway.Service.Tests/SqlTests/MySqlGraphQLQueryTests.cs b/DataGateway.Service.Tests/SqlTests/MySqlGraphQLQueryTests.cs index dcee965abd..ae3a4560dd 100644 --- a/DataGateway.Service.Tests/SqlTests/MySqlGraphQLQueryTests.cs +++ b/DataGateway.Service.Tests/SqlTests/MySqlGraphQLQueryTests.cs @@ -60,6 +60,32 @@ ORDER BY `table0`.`id` SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); } + [TestMethod] + public async Task MultipleResultQueryWithVariables() + { + string graphQLQueryName = "getBooks"; + string graphQLQuery = @"query ($first: Int!) { + getBooks(first: $first) { + id + title + } + }"; + string mySqlQuery = @" + SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT('id', `subq1`.`id`, 'title', `subq1`.`title`)), '[]') AS `data` + FROM + (SELECT `table0`.`id` AS `id`, + `table0`.`title` AS `title` + FROM `books` AS `table0` + WHERE 1 = 1 + ORDER BY `table0`.`id` + LIMIT 100) AS `subq1`"; + + string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController, new() { { "first", 100 } }); + string expected = await GetDatabaseResultAsync(mySqlQuery); + + SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + } + [TestMethod] public async Task MultipleResultJoinQuery() { diff --git a/DataGateway.Service.Tests/SqlTests/PostgreSqlGraphQLQueryTests.cs b/DataGateway.Service.Tests/SqlTests/PostgreSqlGraphQLQueryTests.cs index 38c9943e77..7c2abfc035 100644 --- a/DataGateway.Service.Tests/SqlTests/PostgreSqlGraphQLQueryTests.cs +++ b/DataGateway.Service.Tests/SqlTests/PostgreSqlGraphQLQueryTests.cs @@ -52,6 +52,24 @@ public async Task MultipleResultQuery() SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); } + [TestMethod] + public async Task MultipleResultQueryWithVariables() + { + string graphQLQueryName = "getBooks"; + string graphQLQuery = @"query ($first: Int!) { + getBooks(first: $first) { + id + title + } + }"; + string postgresQuery = $"SELECT json_agg(to_jsonb(table0)) FROM (SELECT id, title FROM books ORDER BY id) as table0 LIMIT 100"; + + string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); + string expected = await GetDatabaseResultAsync(postgresQuery); + + SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + } + [TestMethod] public async Task MultipleResultJoinQuery() { From 8b324cd8c300c40b87e80a898e4a8da0e838b86c Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Fri, 11 Mar 2022 10:58:12 +1100 Subject: [PATCH 09/12] forgot to add the variables to PostgreSQL test --- .../SqlTests/PostgreSqlGraphQLQueryTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DataGateway.Service.Tests/SqlTests/PostgreSqlGraphQLQueryTests.cs b/DataGateway.Service.Tests/SqlTests/PostgreSqlGraphQLQueryTests.cs index 7c2abfc035..d5f4924013 100644 --- a/DataGateway.Service.Tests/SqlTests/PostgreSqlGraphQLQueryTests.cs +++ b/DataGateway.Service.Tests/SqlTests/PostgreSqlGraphQLQueryTests.cs @@ -64,7 +64,7 @@ public async Task MultipleResultQueryWithVariables() }"; string postgresQuery = $"SELECT json_agg(to_jsonb(table0)) FROM (SELECT id, title FROM books ORDER BY id) as table0 LIMIT 100"; - string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController); + string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController, new() { { "first", 100 } }); string expected = await GetDatabaseResultAsync(postgresQuery); SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); From 6c581b566287ff96fa3e6d99794823bac18d82ac Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Tue, 15 Mar 2022 12:01:22 +1100 Subject: [PATCH 10/12] handling nested query variables --- .../CosmosTests/QueryTests.cs | 6 +- .../CosmosTests/TestBase.cs | 21 ++++-- .../SqlTests/MsSqlGraphQLQueryTests.cs | 69 +++++++++++++++++++ .../SqlTests/SqlTestBase.cs | 12 ++-- .../Sql Query Structures/SqlQueryStructure.cs | 5 +- 5 files changed, 96 insertions(+), 17 deletions(-) diff --git a/DataGateway.Service.Tests/CosmosTests/QueryTests.cs b/DataGateway.Service.Tests/CosmosTests/QueryTests.cs index af29835d3e..8d111d7c1f 100644 --- a/DataGateway.Service.Tests/CosmosTests/QueryTests.cs +++ b/DataGateway.Service.Tests/CosmosTests/QueryTests.cs @@ -67,7 +67,7 @@ public async Task GetByPrimaryKeyWithVariables() public async Task GetPaginatedWithVariables() { // Run query - JsonElement response = await ExecuteGraphQLRequestAsync("planetList", PlanetListQuery, new()); + JsonElement response = await ExecuteGraphQLRequestAsync("planetList", PlanetListQuery); int actualElements = response.GetArrayLength(); // Run paginated query int totalElementsFromPaginatedQuery = 0; @@ -98,7 +98,7 @@ public async Task GetByPrimaryKeyWithoutVariables() name }} }}"; - JsonElement response = await ExecuteGraphQLRequestAsync("planetById", query, new()); + JsonElement response = await ExecuteGraphQLRequestAsync("planetById", query); // Validate results Assert.AreEqual(id, response.GetProperty("id").GetString()); @@ -108,7 +108,7 @@ public async Task GetByPrimaryKeyWithoutVariables() public async Task GetPaginatedWithoutVariables() { // Run query - JsonElement response = await ExecuteGraphQLRequestAsync("planetList", PlanetListQuery, new()); + JsonElement response = await ExecuteGraphQLRequestAsync("planetList", PlanetListQuery); int actualElements = response.GetArrayLength(); // Run paginated query int totalElementsFromPaginatedQuery = 0; diff --git a/DataGateway.Service.Tests/CosmosTests/TestBase.cs b/DataGateway.Service.Tests/CosmosTests/TestBase.cs index aab54746a3..28d5b76ad4 100644 --- a/DataGateway.Service.Tests/CosmosTests/TestBase.cs +++ b/DataGateway.Service.Tests/CosmosTests/TestBase.cs @@ -159,17 +159,24 @@ internal static void RegisterGraphQLType(string id, /// Name of the GraphQL query/mutation /// The GraphQL query/mutation /// - internal static async Task ExecuteGraphQLRequestAsync(string queryName, string query, Dictionary variables) + internal static async Task ExecuteGraphQLRequestAsync(string queryName, string query, Dictionary variables = null) { - string queryJson = JObject.FromObject(new - { - query, - variables - }).ToString(); + string queryJson = variables == null ? + JObject.FromObject(new { query }).ToString() : + JObject.FromObject(new + { + query, + variables + }).ToString(); _controller.ControllerContext.HttpContext = GetHttpContextWithBody(queryJson); JsonElement graphQLResult = await _controller.PostAsync(); + + if(graphQLResult.TryGetProperty("errors", out JsonElement errors)) + { + Assert.Fail(errors.GetRawText()); + } + return graphQLResult.GetProperty("data").GetProperty(queryName); } - } } diff --git a/DataGateway.Service.Tests/SqlTests/MsSqlGraphQLQueryTests.cs b/DataGateway.Service.Tests/SqlTests/MsSqlGraphQLQueryTests.cs index e97e5e3ab9..53c84ed6ea 100644 --- a/DataGateway.Service.Tests/SqlTests/MsSqlGraphQLQueryTests.cs +++ b/DataGateway.Service.Tests/SqlTests/MsSqlGraphQLQueryTests.cs @@ -313,6 +313,75 @@ ORDER BY [id] SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); } + /// + /// This deeply nests a many-to-many join multiple times to show that + /// it still results in a valid query. + /// + /// + [TestMethod] + public async Task DeeplyNestedManyToManyJoinQueryWithVariables() + { + string graphQLQueryName = "getBooks"; + string graphQLQuery = @"query ($first: Int) { + getBooks(first: $first) { + title + authors(first: $first) { + name + books(first: $first) { + title + authors(first: $first) { + name + } + } + } + } + }"; + + string msSqlQuery = @" + SELECT TOP 100 [table0].[title] AS [title], + JSON_QUERY(COALESCE([table6_subq].[data], '[]')) AS [authors] + FROM [books] AS [table0] + OUTER APPLY ( + SELECT TOP 100 [table6].[name] AS [name], + JSON_QUERY(COALESCE([table7_subq].[data], '[]')) AS [books] + FROM [authors] AS [table6] + INNER JOIN [book_author_link] AS [table11] ON [table11].[author_id] = [table6].[id] + OUTER APPLY ( + SELECT TOP 100 [table7].[title] AS [title], + JSON_QUERY(COALESCE([table8_subq].[data], '[]')) AS [authors] + FROM [books] AS [table7] + INNER JOIN [book_author_link] AS [table10] ON [table10].[book_id] = [table7].[id] + OUTER APPLY ( + SELECT TOP 100 [table8].[name] AS [name] + FROM [authors] AS [table8] + INNER JOIN [book_author_link] AS [table9] ON [table9].[author_id] = [table8].[id] + WHERE [table7].[id] = [table9].[book_id] + ORDER BY [id] + FOR JSON PATH, + INCLUDE_NULL_VALUES + ) AS [table8_subq]([data]) + WHERE [table6].[id] = [table10].[author_id] + ORDER BY [id] + FOR JSON PATH, + INCLUDE_NULL_VALUES + ) AS [table7_subq]([data]) + WHERE [table0].[id] = [table11].[book_id] + ORDER BY [id] + FOR JSON PATH, + INCLUDE_NULL_VALUES + ) AS [table6_subq]([data]) + WHERE 1 = 1 + ORDER BY [id] + FOR JSON PATH, + INCLUDE_NULL_VALUES + "; + + string actual = await GetGraphQLResultAsync(graphQLQuery, graphQLQueryName, _graphQLController, new() { { "first", 100 } }); + string expected = await GetDatabaseResultAsync(msSqlQuery); + + SqlTestHelper.PerformTestEqualJsonStrings(expected, actual); + } + [TestMethod] public async Task QueryWithSingleColumnPrimaryKey() { diff --git a/DataGateway.Service.Tests/SqlTests/SqlTestBase.cs b/DataGateway.Service.Tests/SqlTests/SqlTestBase.cs index 3bd33f85e5..d26790b6ec 100644 --- a/DataGateway.Service.Tests/SqlTests/SqlTestBase.cs +++ b/DataGateway.Service.Tests/SqlTests/SqlTestBase.cs @@ -320,11 +320,13 @@ protected static async Task GetGraphQLResultAsync(string graphQLQuery, s /// JsonDocument protected static async Task GetGraphQLControllerResultAsync(string query, string graphQLQueryName, GraphQLController graphQLController, Dictionary variables = null) { - string graphqlQueryJson = JObject.FromObject(new - { - query, - variables - }).ToString(); + string graphqlQueryJson = variables == null ? + JObject.FromObject(new { query }).ToString() : + JObject.FromObject(new + { + query, + variables + }).ToString(); Console.WriteLine(graphqlQueryJson); diff --git a/DataGateway.Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs b/DataGateway.Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs index aeacde669d..a08a52c9a0 100644 --- a/DataGateway.Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs +++ b/DataGateway.Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs @@ -474,12 +474,13 @@ void AddGraphQLFields(IReadOnlyList Selections) { IObjectField? subschemaField = _underlyingFieldType.Fields[fieldName]; - IDictionary subqueryParams = ResolverMiddleware.GetParametersFromSchemaAndQueryFields(subschemaField, field, null); if (_ctx == null) { - throw new InvalidOperationException("No GraphQL context exists"); + throw new DataGatewayException("No GraphQL context exists", HttpStatusCode.InternalServerError, DataGatewayException.SubStatusCodes.UnexpectedError); } + IDictionary subqueryParams = ResolverMiddleware.GetParametersFromSchemaAndQueryFields(subschemaField, field, _ctx.Variables); + SqlQueryStructure subquery = new(_ctx, subqueryParams, MetadataStoreProvider, subschemaField, field, Counter); if (PaginationMetadata.IsPaginated) From 110dba6c27fbb32b25f189049e94647ef5d33e96 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Tue, 15 Mar 2022 12:07:42 +1100 Subject: [PATCH 11/12] fixing formatting issue --- DataGateway.Service.Tests/CosmosTests/TestBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DataGateway.Service.Tests/CosmosTests/TestBase.cs b/DataGateway.Service.Tests/CosmosTests/TestBase.cs index 28d5b76ad4..c42156f91d 100644 --- a/DataGateway.Service.Tests/CosmosTests/TestBase.cs +++ b/DataGateway.Service.Tests/CosmosTests/TestBase.cs @@ -171,7 +171,7 @@ internal static async Task ExecuteGraphQLRequestAsync(string queryN _controller.ControllerContext.HttpContext = GetHttpContextWithBody(queryJson); JsonElement graphQLResult = await _controller.PostAsync(); - if(graphQLResult.TryGetProperty("errors", out JsonElement errors)) + if (graphQLResult.TryGetProperty("errors", out JsonElement errors)) { Assert.Fail(errors.GetRawText()); } From 38f347f9b213156f9e5d143bba00c8702b7f9c28 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Wed, 16 Mar 2022 10:29:54 +1100 Subject: [PATCH 12/12] adding doc tips to method --- DataGateway.Service.Tests/CosmosTests/TestBase.cs | 1 + DataGateway.Service.Tests/SqlTests/SqlTestBase.cs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/DataGateway.Service.Tests/CosmosTests/TestBase.cs b/DataGateway.Service.Tests/CosmosTests/TestBase.cs index c42156f91d..e793714477 100644 --- a/DataGateway.Service.Tests/CosmosTests/TestBase.cs +++ b/DataGateway.Service.Tests/CosmosTests/TestBase.cs @@ -158,6 +158,7 @@ internal static void RegisterGraphQLType(string id, /// /// Name of the GraphQL query/mutation /// The GraphQL query/mutation + /// Variables to be included in the GraphQL request. If null, no variables property is included in the request, to pass an empty object provide an empty dictionary /// internal static async Task ExecuteGraphQLRequestAsync(string queryName, string query, Dictionary variables = null) { diff --git a/DataGateway.Service.Tests/SqlTests/SqlTestBase.cs b/DataGateway.Service.Tests/SqlTests/SqlTestBase.cs index 3cef188ed2..ed271ae8d1 100644 --- a/DataGateway.Service.Tests/SqlTests/SqlTestBase.cs +++ b/DataGateway.Service.Tests/SqlTests/SqlTestBase.cs @@ -299,6 +299,7 @@ protected static void ConfigureRestController( /// /// /// + /// Variables to be included in the GraphQL request. If null, no variables property is included in the request, to pass an empty object provide an empty dictionary /// string in JSON format protected static async Task GetGraphQLResultAsync(string graphQLQuery, string graphQLQueryName, GraphQLController graphQLController, Dictionary variables = null) { @@ -317,6 +318,7 @@ protected static async Task GetGraphQLResultAsync(string graphQLQuery, s /// /// /// + /// Variables to be included in the GraphQL request. If null, no variables property is included in the request, to pass an empty object provide an empty dictionary /// JsonDocument protected static async Task GetGraphQLControllerResultAsync(string query, string graphQLQueryName, GraphQLController graphQLController, Dictionary variables = null) {