From c93ce4578187b36afa6876298eb6041b86b7156a Mon Sep 17 00:00:00 2001 From: Jelte Fennema Date: Mon, 4 Oct 2021 17:17:09 +0200 Subject: [PATCH 1/4] Log exceptions and stacktrace to stdout HotChocolate catches all exceptions and turns them into nice user facing errors. That's great, but also we want to see the errors while developing. This prints any exception to stdout and then re-throws it, allowing HotChococate to convert it to nice user facing error. --- .../Services/ResolverMiddleware.cs | 96 ++++++++++--------- 1 file changed, 53 insertions(+), 43 deletions(-) diff --git a/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Services/ResolverMiddleware.cs b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Services/ResolverMiddleware.cs index 283e0f58b3..95f7065a93 100644 --- a/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Services/ResolverMiddleware.cs +++ b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Services/ResolverMiddleware.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Text.Json; using System.Threading.Tasks; @@ -32,62 +33,71 @@ public ResolverMiddleware(FieldDelegate next) public async Task InvokeAsync(IMiddlewareContext context) { - if (context.Selection.Field.Coordinate.TypeName.Value == "Mutation") + try { - IDictionary parameters = GetParametersFromContext(context); - - context.Result = await _mutationEngine.ExecuteAsync(context.Selection.Field.Name.Value, parameters); - } - - if (context.Selection.Field.Coordinate.TypeName.Value == "Query") - { - IDictionary parameters = GetParametersFromContext(context); - - if (context.Selection.Type.IsListType()) + if (context.Selection.Field.Coordinate.TypeName.Value == "Mutation") { - context.Result = await _queryEngine.ExecuteListAsync(context.Selection.Field.Name.Value, parameters); + IDictionary parameters = GetParametersFromContext(context); + + context.Result = await _mutationEngine.ExecuteAsync(context.Selection.Field.Name.Value, parameters); } - else + + if (context.Selection.Field.Coordinate.TypeName.Value == "Query") { - context.Result = await _queryEngine.ExecuteAsync(context.Selection.Field.Name.Value, parameters); + IDictionary parameters = GetParametersFromContext(context); + + if (context.Selection.Type.IsListType()) + { + context.Result = await _queryEngine.ExecuteListAsync(context.Selection.Field.Name.Value, parameters); + } + else + { + context.Result = await _queryEngine.ExecuteAsync(context.Selection.Field.Name.Value, parameters); + } } - } - - if (isInnerObject(context)) - { - JsonDocument result = context.Parent(); - JsonElement jsonElement; - bool hasProperty = - result.RootElement.TryGetProperty(context.Selection.Field.Name.Value, out jsonElement); - if (result != null && hasProperty) + if (isInnerObject(context)) { - //TODO: Try to avoid additional deserialization/serialization here. - context.Result = JsonDocument.Parse(jsonElement.ToString()); + JsonDocument result = context.Parent(); + + JsonElement jsonElement; + bool hasProperty = + result.RootElement.TryGetProperty(context.Selection.Field.Name.Value, out jsonElement); + if (result != null && hasProperty) + { + //TODO: Try to avoid additional deserialization/serialization here. + context.Result = JsonDocument.Parse(jsonElement.ToString()); + } + else + { + context.Result = null; + } } - else + + if (context.Selection.Field.Type.IsLeafType()) { - context.Result = null; + JsonDocument result = context.Parent(); + JsonElement jsonElement; + bool hasProperty = + result.RootElement.TryGetProperty(context.Selection.Field.Name.Value, out jsonElement); + if (result != null && hasProperty) + { + context.Result = jsonElement.ToString(); + } + else + { + context.Result = null; + } } - } - if (context.Selection.Field.Type.IsLeafType()) + await _next(context); + } + catch (Exception ex) { - JsonDocument result = context.Parent(); - JsonElement jsonElement; - bool hasProperty = - result.RootElement.TryGetProperty(context.Selection.Field.Name.Value, out jsonElement); - if (result != null && hasProperty) - { - context.Result = jsonElement.ToString(); - } - else - { - context.Result = null; - } + Console.WriteLine(ex.Message); + Console.WriteLine(ex.StackTrace); + throw; } - - await _next(context); } private bool isInnerObject(IMiddlewareContext context) From 7e76e6c61bc060ac900f77c76c68519473ffa4ec Mon Sep 17 00:00:00 2001 From: Jelte Fennema Date: Tue, 5 Oct 2021 12:15:55 +0200 Subject: [PATCH 2/4] Make dotnet watch run restart on json and gql changes When running with `dotnet watch run` the binary gets rebuilt and restarted whenever a `.cs` file changes. For this project a lot of behaviour is defined in the config files though. This makes sure the `watch run` restarts the program whenever a json or gql file is modified in the project. --- .../Cosmos.GraphQL.Service/Cosmos.GraphQL.Service.csproj | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service.csproj b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service.csproj index 14feaeba1b..68610208ae 100644 --- a/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service.csproj +++ b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service.csproj @@ -14,6 +14,11 @@ + + + + + From 629594b0acf85e147e7dcdaeadd95c79b6bbc7d1 Mon Sep 17 00:00:00 2001 From: Jelte Fennema Date: Tue, 5 Oct 2021 16:17:53 +0200 Subject: [PATCH 3/4] Generate sql queries --- .../Models/TypeMetadata.cs | 16 ++ .../Resolvers/CosmosQueryEngine.cs | 7 +- .../Resolvers/IQueryBuilder.cs | 4 +- .../Resolvers/IQueryEngine.cs | 5 +- .../Resolvers/MsSqlQueryBuilder.cs | 39 ++++- .../Resolvers/PostgresQueryBuilder.cs | 40 ++++- .../Resolvers/QueryExecutor.cs | 2 +- .../Resolvers/SqlQueryEngine.cs | 63 ++++---- .../Resolvers/SqlQueryStructure.cs | 146 ++++++++++++++++++ .../Services/FileMetadataStoreProvider.cs | 18 ++- .../Services/IMetadataStoreProvider.cs | 1 + .../Services/ResolverMiddleware.cs | 32 +++- .../appsettings.MsSql.json | 2 +- .../appsettings.PostgreSql.json | 2 +- .../Cosmos.GraphQL.Service/books.gql | 23 +++ .../postgresql-config.json | 21 --- .../Cosmos.GraphQL.Service/sql-config.json | 42 +++++ 17 files changed, 385 insertions(+), 78 deletions(-) create mode 100644 Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Models/TypeMetadata.cs create mode 100644 Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Resolvers/SqlQueryStructure.cs create mode 100644 Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/books.gql delete mode 100644 Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/postgresql-config.json create mode 100644 Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/sql-config.json diff --git a/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Models/TypeMetadata.cs b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Models/TypeMetadata.cs new file mode 100644 index 0000000000..1543c9b73e --- /dev/null +++ b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Models/TypeMetadata.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace Cosmos.GraphQL.Service.Models +{ + public class TypeMetadata + { + public string Table { get; set; } + public Dictionary JoinMappings { get; set; } = new(); + } + + public class JoinMapping + { + public string LeftColumn { get; set; } + public string RightColumn { get; set; } + } +} diff --git a/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Resolvers/CosmosQueryEngine.cs b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Resolvers/CosmosQueryEngine.cs index 2e60b2efa2..53708f30eb 100644 --- a/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Resolvers/CosmosQueryEngine.cs +++ b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Resolvers/CosmosQueryEngine.cs @@ -5,6 +5,7 @@ using Cosmos.GraphQL.Service.Resolvers; using Microsoft.Azure.Cosmos; using Newtonsoft.Json.Linq; +using HotChocolate.Resolvers; namespace Cosmos.GraphQL.Services { @@ -36,13 +37,14 @@ public void RegisterResolver(GraphQLQueryResolver resolver) // // ExecuteAsync the given named graphql query on the backend. // - public async Task ExecuteAsync(string graphQLQueryName, IDictionary parameters) + public async Task ExecuteAsync(IMiddlewareContext context, IDictionary parameters) { // TODO: fixme we have multiple rounds of serialization/deserialization JsomDocument/JObject // TODO: add support for nesting // TODO: add support for join query against another container // TODO: add support for TOP and Order-by push-down + string graphQLQueryName = context.Selection.Field.Name.Value; var resolver = this._metadataStoreProvider.GetQueryResolver(graphQLQueryName); var container = this._clientProvider.GetClient().GetDatabase(resolver.databaseName).GetContainer(resolver.containerName); var querySpec = new QueryDefinition(resolver.parametrizedQuery); @@ -70,13 +72,14 @@ public async Task ExecuteAsync(string graphQLQueryName, IDictionar return jsonDocument; } - public async Task> ExecuteListAsync(string graphQLQueryName, IDictionary parameters) + public async Task> ExecuteListAsync(IMiddlewareContext context, IDictionary parameters) { // TODO: fixme we have multiple rounds of serialization/deserialization JsomDocument/JObject // TODO: add support for nesting // TODO: add support for join query against another container // TODO: add support for TOP and Order-by push-down + string graphQLQueryName = context.Selection.Field.Name.Value; var resolver = this._metadataStoreProvider.GetQueryResolver(graphQLQueryName); var container = this._clientProvider.GetClient().GetDatabase(resolver.databaseName).GetContainer(resolver.containerName); var querySpec = new QueryDefinition(resolver.parametrizedQuery); diff --git a/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Resolvers/IQueryBuilder.cs b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Resolvers/IQueryBuilder.cs index 4884e18030..72953dcae6 100644 --- a/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Resolvers/IQueryBuilder.cs +++ b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Resolvers/IQueryBuilder.cs @@ -12,6 +12,8 @@ public interface IQueryBuilder // Modifies the inputQuery in such a way that it returns the results as // a JSON string. // - public string Build(string inputQuery, bool isList); + public string QuoteIdentifier(string ident); + public string WrapSubqueryColumn(string column, SqlQueryStructure subquery); + public string Build(SqlQueryStructure structure); } } diff --git a/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Resolvers/IQueryEngine.cs b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Resolvers/IQueryEngine.cs index 571339b597..4455564ec5 100644 --- a/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Resolvers/IQueryEngine.cs +++ b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Resolvers/IQueryEngine.cs @@ -2,6 +2,7 @@ using System.Text.Json; using System.Threading.Tasks; using Cosmos.GraphQL.Service.Models; +using HotChocolate.Resolvers; namespace Cosmos.GraphQL.Services { @@ -18,11 +19,11 @@ public interface IQueryEngine // // Executes the given named graphql query on the backend and expecting a single Json back. // - public Task ExecuteAsync(string graphQLQueryName, IDictionary parameters); + public Task ExecuteAsync(IMiddlewareContext context, IDictionary parameters); // // Executes the given named graphql query on the backend and expecting a list of Jsons back. // - public Task> ExecuteListAsync(string graphQLQueryName, IDictionary parameters); + public Task> ExecuteListAsync(IMiddlewareContext context, IDictionary parameters); } } diff --git a/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Resolvers/MsSqlQueryBuilder.cs b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Resolvers/MsSqlQueryBuilder.cs index a03651b55b..42fe00059e 100644 --- a/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Resolvers/MsSqlQueryBuilder.cs +++ b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Resolvers/MsSqlQueryBuilder.cs @@ -1,3 +1,7 @@ +using System; +using System.Data.Common; +using Microsoft.Data.SqlClient; +using System.Linq; namespace Cosmos.GraphQL.Service.Resolvers { @@ -9,15 +13,40 @@ public class MsSqlQueryBuilder : IQueryBuilder private const string x_ForJsonSuffix = " FOR JSON PATH, INCLUDE_NULL_VALUES"; private const string x_WithoutArrayWrapperSuffix = "WITHOUT_ARRAY_WRAPPER"; - public string Build(string inputQuery, bool isList) + private static DbCommandBuilder Builder = new SqlCommandBuilder(); + public string QuoteIdentifier(string ident) { - string queryText = inputQuery + x_ForJsonSuffix; - if (!isList) + return Builder.QuoteIdentifier(ident); + } + + public string WrapSubqueryColumn(string column, SqlQueryStructure subquery) + { + if (subquery.IsList()) { - queryText += "," + x_WithoutArrayWrapperSuffix; + return $"JSON_QUERY (COALESCE({column}, '[]'))"; } - return queryText; + return $"JSON_QUERY ({column})"; } + public string Build(SqlQueryStructure structure) + { + var selectedColumns = String.Join(", ", structure.Columns.Select(x => $"{x.Value} AS {QuoteIdentifier(x.Key)}")); + string fromPart = structure.Table(structure.TableName, structure.TableAlias); + fromPart += String.Join( + "", + structure.JoinQueries.Select( + x => $" OUTER APPLY ({Build(x.Value)}) AS {QuoteIdentifier(x.Key)}({structure.DataIdent})")); + string query = $"SELECT {selectedColumns} FROM {fromPart}"; + if (structure.Conditions.Count() > 0) + { + query += $" WHERE {String.Join(" AND ", structure.Conditions)}"; + } + query += x_ForJsonSuffix; + if (!structure.IsList()) + { + query += "," + x_WithoutArrayWrapperSuffix; + } + return query; + } } } diff --git a/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Resolvers/PostgresQueryBuilder.cs b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Resolvers/PostgresQueryBuilder.cs index f0f7d3ab26..09f07cadc0 100644 --- a/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Resolvers/PostgresQueryBuilder.cs +++ b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Resolvers/PostgresQueryBuilder.cs @@ -1,3 +1,7 @@ +using System; +using System.Linq; +using System.Data.Common; +using Npgsql; namespace Cosmos.GraphQL.Service.Resolvers { @@ -6,14 +10,40 @@ namespace Cosmos.GraphQL.Service.Resolvers /// public class PostgresQueryBuilder : IQueryBuilder { - public string Build(string inputQuery, bool isList) + private static DbCommandBuilder Builder = new NpgsqlCommandBuilder(); + public string QuoteIdentifier(string ident) { - if (!isList) + return Builder.QuoteIdentifier(ident); + } + + public string WrapSubqueryColumn(string column, SqlQueryStructure subquery) + { + return column; + } + + public string Build(SqlQueryStructure structure) + { + var selectedColumns = String.Join(", ", structure.Columns.Select(x => $"{x.Value} AS {QuoteIdentifier(x.Key)}")); + string fromPart = structure.Table(structure.TableName, structure.TableAlias); + fromPart += String.Join("", structure.JoinQueries.Select(x => $" LEFT OUTER JOIN LATERAL ({Build(x.Value)}) AS {QuoteIdentifier(x.Key)} ON TRUE")); + string query = $"SELECT {selectedColumns} FROM {fromPart}"; + if (structure.Conditions.Count() > 0) { - return $"SELECT row_to_json(q) FROM ({inputQuery}) q"; + query += $" WHERE {String.Join(" AND ", structure.Conditions)}"; } - return $"SELECT jsonb_agg(row_to_json(q)) FROM ({inputQuery}) q"; + var subqName = QuoteIdentifier($"subq{structure.Counter.Next()}"); + string start; + if (structure.IsList()) + { + start = $"SELECT COALESCE(jsonb_agg(to_jsonb({subqName})), '[]') AS {structure.DataIdent} FROM ("; + } + else + { + start = $"SELECT to_jsonb({subqName}) AS {structure.DataIdent} FROM ("; + } + var end = $") AS {subqName}"; + query = $"{start} {query} {end}"; + return query; } - } } diff --git a/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Resolvers/QueryExecutor.cs b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Resolvers/QueryExecutor.cs index 2501ac8d99..e67dbb6ed2 100644 --- a/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Resolvers/QueryExecutor.cs +++ b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Resolvers/QueryExecutor.cs @@ -46,7 +46,7 @@ public async Task ExecuteQueryAsync(string sqltext, IDictionary GetJsonStringFromDbReader(DbDataReader dbDataReader) + { + var jsonString = new StringBuilder(); + // Even though we only return a single cell, we need this loop for + // MS SQL. Sadly it splits FOR JSON PATH output across multiple + // cells if the JSON consists of more than 2033 bytes: + // Sources: + // 1. https://docs.microsoft.com/en-us/sql/relational-databases/json/format-query-results-as-json-with-for-json-sql-server?view=sql-server-2017#output-of-the-for-json-clause + // 2. https://stackoverflow.com/questions/54973536/for-json-path-results-in-ssms-truncated-to-2033-characters/54973676 + // 3. https://docs.microsoft.com/en-us/sql/relational-databases/json/use-for-json-output-in-sql-server-and-in-client-apps-sql-server?view=sql-server-2017#use-for-json-output-in-a-c-client-app + if (await dbDataReader.ReadAsync()) + { + jsonString.Append(dbDataReader.GetString(0)); + } + return jsonString.ToString(); + } + // // ExecuteAsync the given named graphql query on the backend. // - public async Task ExecuteAsync(string graphQLQueryName, IDictionary parameters) + public async Task ExecuteAsync(IMiddlewareContext context, IDictionary parameters) { // TODO: add support for nesting // TODO: add support for join query against another table // TODO: add support for TOP and Order-by push-down + string graphQLQueryName = context.Selection.Field.Name.Value; GraphQLQueryResolver resolver = _metadataStoreProvider.GetQueryResolver(graphQLQueryName); - JsonDocument jsonDocument = JsonDocument.Parse("{ }"); - - string queryText = _queryBuilder.Build(resolver.parametrizedQuery, false); - + SqlQueryStructure structure = new(context, _metadataStoreProvider, _queryBuilder); + Console.WriteLine(structure.ToString()); // Open connection and execute query using _queryExecutor // - DbDataReader dbDataReader = await _queryExecutor.ExecuteQueryAsync(queryText, parameters); + DbDataReader dbDataReader = await _queryExecutor.ExecuteQueryAsync(structure.ToString(), parameters); // Parse Results into Json and return - // - if (await dbDataReader.ReadAsync()) - { - jsonDocument = JsonDocument.Parse(dbDataReader.GetString(0)); - } - else + if (!dbDataReader.HasRows) { - Console.WriteLine("Did not return enough rows in the JSON result."); + return null; } - return jsonDocument; + return JsonDocument.Parse(await GetJsonStringFromDbReader(dbDataReader)); } // // Executes the given named graphql query on the backend and expecting a list of Jsons back. // - public async Task> ExecuteListAsync(string graphQLQueryName, IDictionary parameters) + public async Task> ExecuteListAsync(IMiddlewareContext context, IDictionary parameters) { // TODO: add support for nesting // TODO: add support for join query against another container // TODO: add support for TOP and Order-by push-down + string graphQLQueryName = context.Selection.Field.Name.Value; GraphQLQueryResolver resolver = _metadataStoreProvider.GetQueryResolver(graphQLQueryName); - List resultsAsList = new List(); - string queryText = _queryBuilder.Build(resolver.parametrizedQuery, true); - DbDataReader dbDataReader = await _queryExecutor.ExecuteQueryAsync(queryText, parameters); - // Deserialize results into list of JsonDocuments and return + SqlQueryStructure structure = new(context, _metadataStoreProvider, _queryBuilder); + Console.WriteLine(structure.ToString()); + DbDataReader dbDataReader = await _queryExecutor.ExecuteQueryAsync(structure.ToString(), parameters); + + // Parse Results into Json and return // - if (await dbDataReader.ReadAsync()) - { - resultsAsList = JsonSerializer.Deserialize>(dbDataReader.GetString(0)); - } - else + if (!dbDataReader.HasRows) { - Console.WriteLine("Did not return enough rows in the JSON result."); + return new List(); } - return resultsAsList; + return JsonSerializer.Deserialize>(await GetJsonStringFromDbReader(dbDataReader)); } } } diff --git a/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Resolvers/SqlQueryStructure.cs b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Resolvers/SqlQueryStructure.cs new file mode 100644 index 0000000000..9516284085 --- /dev/null +++ b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Resolvers/SqlQueryStructure.cs @@ -0,0 +1,146 @@ +using System.Collections.Generic; +using HotChocolate.Language; +using HotChocolate.Resolvers; +using HotChocolate.Types; +using Cosmos.GraphQL.Services; + +namespace Cosmos.GraphQL.Service.Resolvers +{ + + public class IncrementingInteger + { + private int integer; + public IncrementingInteger() + { + integer = 1; + } + + public int Next() + { + return integer++; + } + + } + + public class SqlQueryStructure + { + public Dictionary Columns { get; } + public List Conditions; + public Dictionary JoinQueries { get; } + public string TableName { get; } + public string TableAlias { get; } + public IncrementingInteger Counter { get; } + public string DataIdent { get; } + IResolverContext ctx; + IObjectField schemaField; + ObjectType coreFieldType; + private readonly IMetadataStoreProvider metadataStoreProvider; + private readonly IQueryBuilder queryBuilder; + // public List conditions; + public SqlQueryStructure(IResolverContext ctx, IMetadataStoreProvider metadataStoreProvider, IQueryBuilder queryBuilder) : this( + ctx, + metadataStoreProvider, + queryBuilder, + ctx.Selection.Field, + "table0", + ctx.Selection.SyntaxNode, + new IncrementingInteger() + ) + { } + + public SqlQueryStructure( + IResolverContext ctx, + IMetadataStoreProvider metadataStoreProvider, + IQueryBuilder queryBuilder, + IObjectField schemaField, + string tableAlias, + FieldNode queryField, + IncrementingInteger counter + ) + { + Columns = new(); + JoinQueries = new(); + Conditions = new(); + Counter = counter; + this.ctx = ctx; + this.schemaField = schemaField; + this.metadataStoreProvider = metadataStoreProvider; + this.queryBuilder = queryBuilder; + if (IsList()) + { + // TODO: Do checking of the Kind here + coreFieldType = (ObjectType)schemaField.Type.InnerType(); + } + else + { + // TODO: Do checking of the Kind here + coreFieldType = (ObjectType)schemaField.Type; + } + DataIdent = QuoteIdentifier("data"); + + // TODO: Allow specifying a different table name in the config + TableName = $"{coreFieldType.Name.Value.ToLower()}s"; + TableAlias = tableAlias; + AddFields(queryField.SelectionSet.Selections); + } + public string Table(string name, string alias) + { + return $"{QuoteIdentifier(name)} AS {QuoteIdentifier(alias)}"; + } + + public string QualifiedColumn(string tableAlias, string columnName) + { + return $"{QuoteIdentifier(tableAlias)}.{QuoteIdentifier(columnName)}"; + } + + void AddFields(IReadOnlyList Selections) + { + foreach (var node in Selections) + { + FieldNode field = node as FieldNode; + string fieldName = field.Name.Value; + + if (field.SelectionSet == null) + { + // TODO: Get allow configuring a different column name in + // the JSON config + string columnName = field.Name.Value; + string column = QualifiedColumn(TableAlias, columnName); + Columns.Add(fieldName, column); + } + else + { + string subtableAlias = $"table{Counter.Next()}"; + var metadata = metadataStoreProvider.GetTypeMetadata(coreFieldType.Name); + var joinMapping = metadata.JoinMappings[fieldName]; + string leftColumn = QualifiedColumn(TableAlias, joinMapping.LeftColumn); + string rightColumn = QualifiedColumn(subtableAlias, joinMapping.RightColumn); + + var subSchemaField = coreFieldType.Fields[fieldName]; + + SqlQueryStructure subquery = new(ctx, metadataStoreProvider, queryBuilder, subSchemaField, subtableAlias, field, Counter); + subquery.Conditions.Add($"{leftColumn} = {rightColumn}"); + string subqueryAlias = $"{subtableAlias}_subq"; + JoinQueries.Add(subqueryAlias, subquery); + var column = queryBuilder.WrapSubqueryColumn($"{QuoteIdentifier(subqueryAlias)}.{DataIdent}", subquery); + Columns.Add(fieldName, column); + } + } + } + + public string QuoteIdentifier(string ident) + { + return queryBuilder.QuoteIdentifier(ident); + } + + public bool IsList() + { + return schemaField.Type.Kind == TypeKind.List; + } + + public override string ToString() + { + return queryBuilder.Build(this); + } + } +} diff --git a/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Services/FileMetadataStoreProvider.cs b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Services/FileMetadataStoreProvider.cs index 10d99436b7..5fe3b3c6ef 100644 --- a/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Services/FileMetadataStoreProvider.cs +++ b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Services/FileMetadataStoreProvider.cs @@ -21,8 +21,9 @@ public class ResolverConfig /// Location of the graphQL schema file /// public string GraphQLSchemaFile { get; set; } - public List QueryResolvers { get; set; } - public List MutationResolvers { get; set; } + public List QueryResolvers { get; set; } = new(); + public List MutationResolvers { get; set; } = new(); + public Dictionary TypeMetadata { get; set; } = new(); } /// @@ -61,9 +62,6 @@ private void init() _config.GraphQLSchema = File.ReadAllText(_config.GraphQLSchemaFile ?? "schema.gql"); } - _config.QueryResolvers ??= new(); - _config.MutationResolvers ??= new(); - _queryResolvers = new(); foreach (var resolver in _config.QueryResolvers) { @@ -106,6 +104,16 @@ public GraphQLQueryResolver GetQueryResolver(string name) return resolver; } + public TypeMetadata GetTypeMetadata(string name) + { + if (!_config.TypeMetadata.TryGetValue(name, out TypeMetadata metadata)) + { + throw new KeyNotFoundException($"TypeMetadata for {name} does not exist."); + } + + return metadata; + } + public void StoreGraphQLSchema(string schema) { // no op diff --git a/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Services/IMetadataStoreProvider.cs b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Services/IMetadataStoreProvider.cs index 97535a7e19..ffabd2b228 100644 --- a/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Services/IMetadataStoreProvider.cs +++ b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Services/IMetadataStoreProvider.cs @@ -9,6 +9,7 @@ public interface IMetadataStoreProvider string GetGraphQLSchema(); MutationResolver GetMutationResolver(string name); GraphQLQueryResolver GetQueryResolver(string name); + TypeMetadata GetTypeMetadata(string name); void StoreMutationResolver(MutationResolver mutationResolver); void StoreQueryResolver(GraphQLQueryResolver mutationResolver); } diff --git a/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Services/ResolverMiddleware.cs b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Services/ResolverMiddleware.cs index 95f7065a93..b596bf0646 100644 --- a/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Services/ResolverMiddleware.cs +++ b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Services/ResolverMiddleware.cs @@ -41,22 +41,36 @@ public async Task InvokeAsync(IMiddlewareContext context) context.Result = await _mutationEngine.ExecuteAsync(context.Selection.Field.Name.Value, parameters); } - - if (context.Selection.Field.Coordinate.TypeName.Value == "Query") + else if (context.Selection.Field.Coordinate.TypeName.Value == "Query") { IDictionary parameters = GetParametersFromContext(context); if (context.Selection.Type.IsListType()) { - context.Result = await _queryEngine.ExecuteListAsync(context.Selection.Field.Name.Value, parameters); + context.Result = await _queryEngine.ExecuteListAsync(context, parameters); } else { - context.Result = await _queryEngine.ExecuteAsync(context.Selection.Field.Name.Value, parameters); + context.Result = await _queryEngine.ExecuteAsync(context, parameters); } } - if (isInnerObject(context)) + else if (context.Selection.Field.Type.IsLeafType()) + { + JsonDocument result = context.Parent(); + JsonElement jsonElement; + bool hasProperty = + result.RootElement.TryGetProperty(context.Selection.Field.Name.Value, out jsonElement); + if (result != null && hasProperty) + { + context.Result = jsonElement.ToString(); + } + else + { + context.Result = null; + } + } + else if (isInnerObject(context)) { JsonDocument result = context.Parent(); @@ -73,16 +87,17 @@ public async Task InvokeAsync(IMiddlewareContext context) context.Result = null; } } - - if (context.Selection.Field.Type.IsLeafType()) + else if (context.Selection.Type.IsListType()) { JsonDocument result = context.Parent(); + JsonElement jsonElement; bool hasProperty = result.RootElement.TryGetProperty(context.Selection.Field.Name.Value, out jsonElement); if (result != null && hasProperty) { - context.Result = jsonElement.ToString(); + //TODO: Try to avoid additional deserialization/serialization here. + context.Result = JsonSerializer.Deserialize>(jsonElement.ToString()); } else { @@ -90,6 +105,7 @@ public async Task InvokeAsync(IMiddlewareContext context) } } + await _next(context); } catch (Exception ex) diff --git a/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/appsettings.MsSql.json b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/appsettings.MsSql.json index 106739b33d..2dd5d6c224 100644 --- a/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/appsettings.MsSql.json +++ b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/appsettings.MsSql.json @@ -1,7 +1,7 @@ { "DatabaseConnection": { "DatabaseType": "MsSql", - "ResolverConfigFile": "mssql-config.json", + "ResolverConfigFile": "sql-config.json", "Credentials": { "Server": "localhost", "DatabaseName": "graphql", diff --git a/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/appsettings.PostgreSql.json b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/appsettings.PostgreSql.json index fd132cc7e2..927fb1dc8d 100644 --- a/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/appsettings.PostgreSql.json +++ b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/appsettings.PostgreSql.json @@ -1,7 +1,7 @@ { "DatabaseConnection": { "DatabaseType": "PostgreSql", - "ResolverConfigFile": "postgresql-config.json", + "ResolverConfigFile": "sql-config.json", "Credentials": { "ConnectionString": "Host=localhost;Database=graphql" } diff --git a/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/books.gql b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/books.gql new file mode 100644 index 0000000000..886ef2bd4c --- /dev/null +++ b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/books.gql @@ -0,0 +1,23 @@ +type Query { + getBooks(first: Int = 3): [Book] +} + +type Author { + id: Int + name: String + books(first: Int = 2): [Book] +} + +type Book { + id: Int + title: String + author_id: Int + author: Author + reviews(first: Int = 2): [Review] +} + +type Review { + id: Int + content: String + book: Book +} diff --git a/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/postgresql-config.json b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/postgresql-config.json deleted file mode 100644 index ec9892ff3d..0000000000 --- a/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/postgresql-config.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "GraphQLSchema": "", - "QueryResolvers": [ - { - "id": "characterById", - "parametrizedQuery": "SELECT id, name, type, homePlanet, primaryFunction FROM character WHERE id = @id::integer" - }, - { - "id": "characterList", - "parametrizedQuery": "SELECT id, name, type, homePlanet, primaryFunction FROM character" - }, - { - "id": "planetById", - "parametrizedQuery": "SELECT id, name FROM planet WHERE id = @id::integer" - }, - { - "id": "planetList", - "parametrizedQuery": "SELECT id, name FROM planet" - } - ] -} diff --git a/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/sql-config.json b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/sql-config.json new file mode 100644 index 0000000000..0ba0cc6e69 --- /dev/null +++ b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/sql-config.json @@ -0,0 +1,42 @@ +{ + "GraphQLSchema": "", + "GraphQLSchemaFile": "books.gql", + "QueryResolvers": [ + { + "id": "getBooks" + } + ], + "TypeMetadata": { + "Author": { + "Table": "authors", + "JoinMappings": { + "books": { + "LeftColumn": "id", + "RightColumn": "author_id" + } + } + }, + "Book": { + "Table": "books", + "JoinMappings": { + "author": { + "LeftColumn": "author_id", + "RightColumn": "id" + }, + "reviews": { + "LeftColumn": "id", + "RightColumn": "book_id" + } + } + }, + "Review": { + "Table": "reviews", + "JoinMappings": { + "reviews": { + "LeftColumn": "book_id", + "RightColumn": "id" + } + } + } + } +} From e7b2b73e92cab818c4c768451dcfd489eb135588 Mon Sep 17 00:00:00 2001 From: Aniruddh Munde Date: Mon, 8 Nov 2021 22:44:48 -0800 Subject: [PATCH 4/4] Get successful build and fix IDE issues after merge --- .../Resolvers/MsSqlQueryBuilder.cs | 52 ----------------- DataGateway.Service/Models/TypeMetadata.cs | 2 +- .../Resolvers/MsSqlQueryBuilder.cs | 45 +++++++++++--- .../Resolvers/SqlQueryEngine.cs | 8 +-- .../Resolvers/SqlQueryStructure.cs | 58 +++++++++---------- .../Services/ResolverMiddleware.cs | 4 +- DataGateway.Service/appsettings.MsSql.json | 2 +- .../appsettings.PostgreSql.json | 2 +- DataGateway.Service/mssql-config.json | 21 ------- DataGateway.Service/postgresql-config.json | 21 ------- 10 files changed, 74 insertions(+), 141 deletions(-) delete mode 100644 Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Resolvers/MsSqlQueryBuilder.cs delete mode 100644 DataGateway.Service/mssql-config.json delete mode 100644 DataGateway.Service/postgresql-config.json diff --git a/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Resolvers/MsSqlQueryBuilder.cs b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Resolvers/MsSqlQueryBuilder.cs deleted file mode 100644 index 42fe00059e..0000000000 --- a/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Resolvers/MsSqlQueryBuilder.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Data.Common; -using Microsoft.Data.SqlClient; -using System.Linq; - -namespace Cosmos.GraphQL.Service.Resolvers -{ - /// - /// Modifies a query that returns regular rows to return JSON for MSSQL. - /// - public class MsSqlQueryBuilder : IQueryBuilder - { - private const string x_ForJsonSuffix = " FOR JSON PATH, INCLUDE_NULL_VALUES"; - private const string x_WithoutArrayWrapperSuffix = "WITHOUT_ARRAY_WRAPPER"; - - private static DbCommandBuilder Builder = new SqlCommandBuilder(); - public string QuoteIdentifier(string ident) - { - return Builder.QuoteIdentifier(ident); - } - - public string WrapSubqueryColumn(string column, SqlQueryStructure subquery) - { - if (subquery.IsList()) - { - return $"JSON_QUERY (COALESCE({column}, '[]'))"; - } - return $"JSON_QUERY ({column})"; - } - - public string Build(SqlQueryStructure structure) - { - var selectedColumns = String.Join(", ", structure.Columns.Select(x => $"{x.Value} AS {QuoteIdentifier(x.Key)}")); - string fromPart = structure.Table(structure.TableName, structure.TableAlias); - fromPart += String.Join( - "", - structure.JoinQueries.Select( - x => $" OUTER APPLY ({Build(x.Value)}) AS {QuoteIdentifier(x.Key)}({structure.DataIdent})")); - string query = $"SELECT {selectedColumns} FROM {fromPart}"; - if (structure.Conditions.Count() > 0) - { - query += $" WHERE {String.Join(" AND ", structure.Conditions)}"; - } - query += x_ForJsonSuffix; - if (!structure.IsList()) - { - query += "," + x_WithoutArrayWrapperSuffix; - } - return query; - } - } -} diff --git a/DataGateway.Service/Models/TypeMetadata.cs b/DataGateway.Service/Models/TypeMetadata.cs index 1543c9b73e..d1e12d394d 100644 --- a/DataGateway.Service/Models/TypeMetadata.cs +++ b/DataGateway.Service/Models/TypeMetadata.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace Cosmos.GraphQL.Service.Models +namespace Azure.DataGateway.Service.Models { public class TypeMetadata { diff --git a/DataGateway.Service/Resolvers/MsSqlQueryBuilder.cs b/DataGateway.Service/Resolvers/MsSqlQueryBuilder.cs index 8e8badabf3..9c8d9a2acd 100644 --- a/DataGateway.Service/Resolvers/MsSqlQueryBuilder.cs +++ b/DataGateway.Service/Resolvers/MsSqlQueryBuilder.cs @@ -1,3 +1,7 @@ +using System; +using System.Data.Common; +using Microsoft.Data.SqlClient; +using System.Linq; namespace Azure.DataGateway.Service.Resolvers { @@ -6,19 +10,46 @@ namespace Azure.DataGateway.Service.Resolvers /// public class MsSqlQueryBuilder : IQueryBuilder { - private const string X_FOR_JSON_SUFFIX = " FOR JSON PATH, INCLUDE_NULL_VALUES"; - private const string X_WITHOUT_ARRAY_WRAPPER_SUFFIX = "WITHOUT_ARRAY_WRAPPER"; + private const string X_FORJSONSUFFIX = " FOR JSON PATH, INCLUDE_NULL_VALUES"; + private const string X_WITHOUTARRAYWRAPPERSUFFIX = "WITHOUT_ARRAY_WRAPPER"; - public string Build(string inputQuery, bool isList) + private static DbCommandBuilder _builder = new SqlCommandBuilder(); + public string QuoteIdentifier(string ident) { - string queryText = inputQuery + X_FOR_JSON_SUFFIX; - if (!isList) + return _builder.QuoteIdentifier(ident); + } + + public string WrapSubqueryColumn(string column, SqlQueryStructure subquery) + { + if (subquery.IsList()) { - queryText += "," + X_WITHOUT_ARRAY_WRAPPER_SUFFIX; + return $"JSON_QUERY (COALESCE({column}, '[]'))"; } - return queryText; + return $"JSON_QUERY ({column})"; } + public string Build(SqlQueryStructure structure) + { + string selectedColumns = String.Join(", ", structure.Columns.Select(x => $"{x.Value} AS {QuoteIdentifier(x.Key)}")); + string fromPart = structure.Table(structure.TableName, structure.TableAlias); + fromPart += String.Join( + "", + structure.JoinQueries.Select( + x => $" OUTER APPLY ({Build(x.Value)}) AS {QuoteIdentifier(x.Key)}({structure.DataIdent})")); + string query = $"SELECT {selectedColumns} FROM {fromPart}"; + if (structure.Conditions.Count() > 0) + { + query += $" WHERE {String.Join(" AND ", structure.Conditions)}"; + } + + query += X_FORJSONSUFFIX; + if (!structure.IsList()) + { + query += "," + X_WITHOUTARRAYWRAPPERSUFFIX; + } + + return query; + } } } diff --git a/DataGateway.Service/Resolvers/SqlQueryEngine.cs b/DataGateway.Service/Resolvers/SqlQueryEngine.cs index 8491444472..5f38620695 100644 --- a/DataGateway.Service/Resolvers/SqlQueryEngine.cs +++ b/DataGateway.Service/Resolvers/SqlQueryEngine.cs @@ -38,7 +38,7 @@ public void RegisterResolver(GraphQLQueryResolver resolver) // no-op } - private async Task GetJsonStringFromDbReader(DbDataReader dbDataReader) + private static async Task GetJsonStringFromDbReader(DbDataReader dbDataReader) { var jsonString = new StringBuilder(); // Even though we only return a single cell, we need this loop for @@ -52,6 +52,7 @@ private async Task GetJsonStringFromDbReader(DbDataReader dbDataReader) { jsonString.Append(dbDataReader.GetString(0)); } + return jsonString.ToString(); } @@ -64,8 +65,6 @@ public async Task ExecuteAsync(IMiddlewareContext context, IDictio // TODO: add support for join query against another table // TODO: add support for TOP and Order-by push-down - string graphQLQueryName = context.Selection.Field.Name.Value; - GraphQLQueryResolver resolver = _metadataStoreProvider.GetQueryResolver(graphQLQueryName); SqlQueryStructure structure = new(context, _metadataStoreProvider, _queryBuilder); Console.WriteLine(structure.ToString()); // Open connection and execute query using _queryExecutor @@ -90,9 +89,6 @@ public async Task> ExecuteListAsync(IMiddlewareContext // TODO: add support for join query against another container // TODO: add support for TOP and Order-by push-down - string graphQLQueryName = context.Selection.Field.Name.Value; - GraphQLQueryResolver resolver = _metadataStoreProvider.GetQueryResolver(graphQLQueryName); - SqlQueryStructure structure = new(context, _metadataStoreProvider, _queryBuilder); Console.WriteLine(structure.ToString()); DbDataReader dbDataReader = await _queryExecutor.ExecuteQueryAsync(structure.ToString(), parameters); diff --git a/DataGateway.Service/Resolvers/SqlQueryStructure.cs b/DataGateway.Service/Resolvers/SqlQueryStructure.cs index 9516284085..a96fe070fa 100644 --- a/DataGateway.Service/Resolvers/SqlQueryStructure.cs +++ b/DataGateway.Service/Resolvers/SqlQueryStructure.cs @@ -1,23 +1,22 @@ -using System.Collections.Generic; +using Azure.DataGateway.Services; using HotChocolate.Language; using HotChocolate.Resolvers; using HotChocolate.Types; -using Cosmos.GraphQL.Services; +using System.Collections.Generic; -namespace Cosmos.GraphQL.Service.Resolvers +namespace Azure.DataGateway.Service.Resolvers { - public class IncrementingInteger { - private int integer; + private int _integer; public IncrementingInteger() { - integer = 1; + _integer = 1; } public int Next() { - return integer++; + return _integer++; } } @@ -31,11 +30,11 @@ public class SqlQueryStructure public string TableAlias { get; } public IncrementingInteger Counter { get; } public string DataIdent { get; } - IResolverContext ctx; - IObjectField schemaField; - ObjectType coreFieldType; - private readonly IMetadataStoreProvider metadataStoreProvider; - private readonly IQueryBuilder queryBuilder; + IResolverContext _ctx; + IObjectField _schemaField; + ObjectType _coreFieldType; + private readonly IMetadataStoreProvider _metadataStoreProvider; + private readonly IQueryBuilder _queryBuilder; // public List conditions; public SqlQueryStructure(IResolverContext ctx, IMetadataStoreProvider metadataStoreProvider, IQueryBuilder queryBuilder) : this( ctx, @@ -62,24 +61,25 @@ IncrementingInteger counter JoinQueries = new(); Conditions = new(); Counter = counter; - this.ctx = ctx; - this.schemaField = schemaField; - this.metadataStoreProvider = metadataStoreProvider; - this.queryBuilder = queryBuilder; + this._ctx = ctx; + this._schemaField = schemaField; + this._metadataStoreProvider = metadataStoreProvider; + this._queryBuilder = queryBuilder; if (IsList()) { // TODO: Do checking of the Kind here - coreFieldType = (ObjectType)schemaField.Type.InnerType(); + _coreFieldType = (ObjectType)schemaField.Type.InnerType(); } else { // TODO: Do checking of the Kind here - coreFieldType = (ObjectType)schemaField.Type; + _coreFieldType = (ObjectType)schemaField.Type; } + DataIdent = QuoteIdentifier("data"); // TODO: Allow specifying a different table name in the config - TableName = $"{coreFieldType.Name.Value.ToLower()}s"; + TableName = $"{_coreFieldType.Name.Value.ToLower()}s"; TableAlias = tableAlias; AddFields(queryField.SelectionSet.Selections); } @@ -95,9 +95,9 @@ public string QualifiedColumn(string tableAlias, string columnName) void AddFields(IReadOnlyList Selections) { - foreach (var node in Selections) + foreach (ISelectionNode node in Selections) { - FieldNode field = node as FieldNode; + var field = node as FieldNode; string fieldName = field.Name.Value; if (field.SelectionSet == null) @@ -111,18 +111,18 @@ void AddFields(IReadOnlyList Selections) else { string subtableAlias = $"table{Counter.Next()}"; - var metadata = metadataStoreProvider.GetTypeMetadata(coreFieldType.Name); - var joinMapping = metadata.JoinMappings[fieldName]; + Models.TypeMetadata metadata = _metadataStoreProvider.GetTypeMetadata(_coreFieldType.Name); + Models.JoinMapping joinMapping = metadata.JoinMappings[fieldName]; string leftColumn = QualifiedColumn(TableAlias, joinMapping.LeftColumn); string rightColumn = QualifiedColumn(subtableAlias, joinMapping.RightColumn); - var subSchemaField = coreFieldType.Fields[fieldName]; + ObjectField subSchemaField = _coreFieldType.Fields[fieldName]; - SqlQueryStructure subquery = new(ctx, metadataStoreProvider, queryBuilder, subSchemaField, subtableAlias, field, Counter); + SqlQueryStructure subquery = new(_ctx, _metadataStoreProvider, _queryBuilder, subSchemaField, subtableAlias, field, Counter); subquery.Conditions.Add($"{leftColumn} = {rightColumn}"); string subqueryAlias = $"{subtableAlias}_subq"; JoinQueries.Add(subqueryAlias, subquery); - var column = queryBuilder.WrapSubqueryColumn($"{QuoteIdentifier(subqueryAlias)}.{DataIdent}", subquery); + string column = _queryBuilder.WrapSubqueryColumn($"{QuoteIdentifier(subqueryAlias)}.{DataIdent}", subquery); Columns.Add(fieldName, column); } } @@ -130,17 +130,17 @@ void AddFields(IReadOnlyList Selections) public string QuoteIdentifier(string ident) { - return queryBuilder.QuoteIdentifier(ident); + return _queryBuilder.QuoteIdentifier(ident); } public bool IsList() { - return schemaField.Type.Kind == TypeKind.List; + return _schemaField.Type.Kind == TypeKind.List; } public override string ToString() { - return queryBuilder.Build(this); + return _queryBuilder.Build(this); } } } diff --git a/DataGateway.Service/Services/ResolverMiddleware.cs b/DataGateway.Service/Services/ResolverMiddleware.cs index 136fc49a69..e7486bfde6 100644 --- a/DataGateway.Service/Services/ResolverMiddleware.cs +++ b/DataGateway.Service/Services/ResolverMiddleware.cs @@ -2,6 +2,7 @@ using HotChocolate.Language; using HotChocolate.Resolvers; using HotChocolate.Types; +using System; using System.Collections.Generic; using System.Text.Json; using System.Threading.Tasks; @@ -69,7 +70,7 @@ public async Task InvokeAsync(IMiddlewareContext context) context.Result = null; } } - else if (isInnerObject(context)) + else if (IsInnerObject(context)) { JsonDocument result = context.Parent(); @@ -104,7 +105,6 @@ public async Task InvokeAsync(IMiddlewareContext context) } } - await _next(context); } catch (Exception ex) diff --git a/DataGateway.Service/appsettings.MsSql.json b/DataGateway.Service/appsettings.MsSql.json index 441c856d53..85a3f3d00a 100644 --- a/DataGateway.Service/appsettings.MsSql.json +++ b/DataGateway.Service/appsettings.MsSql.json @@ -1,7 +1,7 @@ { "DataGatewayConfig": { "DatabaseType": "MsSql", - "ResolverConfigFile": "mssql-config.json", + "ResolverConfigFile": "sql-config.json", "DatabaseConnection": { "ConnectionString": "Server=tcp:127.0.0.1,1433;Persist Security Info=False;User ID=sa;Password=REPLACEME;MultipleActiveResultSets=False;Connection Timeout=5;" } diff --git a/DataGateway.Service/appsettings.PostgreSql.json b/DataGateway.Service/appsettings.PostgreSql.json index a2d4396d90..60c037dfc4 100644 --- a/DataGateway.Service/appsettings.PostgreSql.json +++ b/DataGateway.Service/appsettings.PostgreSql.json @@ -1,7 +1,7 @@ { "DataGatewayConfig": { "DatabaseType": "PostgreSql", - "ResolverConfigFile": "postgresql-config.json", + "ResolverConfigFile": "sql-config.json", "DatabaseConnection": { "ConnectionString": "Host=localhost;Database=graphql" } diff --git a/DataGateway.Service/mssql-config.json b/DataGateway.Service/mssql-config.json deleted file mode 100644 index 353fd25c76..0000000000 --- a/DataGateway.Service/mssql-config.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "GraphQLSchema": "", - "QueryResolvers": [ - { - "id": "characterById", - "parametrizedQuery": "SELECT id, name, type, homePlanet, primaryFunction FROM character WHERE id = @id" - }, - { - "id": "characterList", - "parametrizedQuery": "SELECT id, name, type, homePlanet, primaryFunction FROM character" - }, - { - "id": "planetById", - "parametrizedQuery": "SELECT id, name FROM planet WHERE id = @id" - }, - { - "id": "planetList", - "parametrizedQuery": "SELECT id, name FROM planet" - } - ] -} diff --git a/DataGateway.Service/postgresql-config.json b/DataGateway.Service/postgresql-config.json deleted file mode 100644 index ec9892ff3d..0000000000 --- a/DataGateway.Service/postgresql-config.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "GraphQLSchema": "", - "QueryResolvers": [ - { - "id": "characterById", - "parametrizedQuery": "SELECT id, name, type, homePlanet, primaryFunction FROM character WHERE id = @id::integer" - }, - { - "id": "characterList", - "parametrizedQuery": "SELECT id, name, type, homePlanet, primaryFunction FROM character" - }, - { - "id": "planetById", - "parametrizedQuery": "SELECT id, name FROM planet WHERE id = @id::integer" - }, - { - "id": "planetList", - "parametrizedQuery": "SELECT id, name FROM planet" - } - ] -}