diff --git a/DataGateway.Service/Models/TypeMetadata.cs b/DataGateway.Service/Models/TypeMetadata.cs new file mode 100644 index 0000000000..d1e12d394d --- /dev/null +++ b/DataGateway.Service/Models/TypeMetadata.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace Azure.DataGateway.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/DataGateway.Service/Resolvers/CosmosQueryEngine.cs b/DataGateway.Service/Resolvers/CosmosQueryEngine.cs index c2d3948134..3009712fe3 100644 --- a/DataGateway.Service/Resolvers/CosmosQueryEngine.cs +++ b/DataGateway.Service/Resolvers/CosmosQueryEngine.cs @@ -1,5 +1,6 @@ using Azure.DataGateway.Service.Models; using Azure.DataGateway.Service.Resolvers; +using HotChocolate.Resolvers; using Microsoft.Azure.Cosmos; using Newtonsoft.Json.Linq; using System.Collections.Generic; @@ -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; GraphQLQueryResolver resolver = this._metadataStoreProvider.GetQueryResolver(graphQLQueryName); Container container = this._clientProvider.Client.GetDatabase(resolver.DatabaseName).GetContainer(resolver.ContainerName); var querySpec = new QueryDefinition(resolver.ParametrizedQuery); @@ -71,13 +73,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; GraphQLQueryResolver resolver = this._metadataStoreProvider.GetQueryResolver(graphQLQueryName); Container container = this._clientProvider.Client.GetDatabase(resolver.DatabaseName).GetContainer(resolver.ContainerName); var querySpec = new QueryDefinition(resolver.ParametrizedQuery); diff --git a/DataGateway.Service/Resolvers/IQueryBuilder.cs b/DataGateway.Service/Resolvers/IQueryBuilder.cs index 1ea97f3a4e..18805965bc 100644 --- a/DataGateway.Service/Resolvers/IQueryBuilder.cs +++ b/DataGateway.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/DataGateway.Service/Resolvers/IQueryEngine.cs b/DataGateway.Service/Resolvers/IQueryEngine.cs index ea626bc462..28d5dca282 100644 --- a/DataGateway.Service/Resolvers/IQueryEngine.cs +++ b/DataGateway.Service/Resolvers/IQueryEngine.cs @@ -1,4 +1,5 @@ using Azure.DataGateway.Service.Models; +using HotChocolate.Resolvers; using System.Collections.Generic; using System.Text.Json; using System.Threading.Tasks; @@ -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/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/PostgresQueryBuilder.cs b/DataGateway.Service/Resolvers/PostgresQueryBuilder.cs index f47f115b42..d3f2122340 100644 --- a/DataGateway.Service/Resolvers/PostgresQueryBuilder.cs +++ b/DataGateway.Service/Resolvers/PostgresQueryBuilder.cs @@ -1,3 +1,7 @@ +using System; +using System.Linq; +using System.Data.Common; +using Npgsql; namespace Azure.DataGateway.Service.Resolvers { @@ -6,15 +10,40 @@ namespace Azure.DataGateway.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 $"SELECT row_to_json(q) FROM ({inputQuery}) q"; - } + return Builder.QuoteIdentifier(ident); + } - return $"SELECT jsonb_agg(row_to_json(q)) FROM ({inputQuery}) q"; + 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) + { + query += $" WHERE {String.Join(" AND ", structure.Conditions)}"; + } + 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/DataGateway.Service/Resolvers/QueryExecutor.cs b/DataGateway.Service/Resolvers/QueryExecutor.cs index 3799ac9c02..a7072dc167 100644 --- a/DataGateway.Service/Resolvers/QueryExecutor.cs +++ b/DataGateway.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 - GraphQLQueryResolver resolver = _metadataStoreProvider.GetQueryResolver(graphQLQueryName); - var 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 - GraphQLQueryResolver resolver = _metadataStoreProvider.GetQueryResolver(graphQLQueryName); - var resultsAsList = new List(); - string queryText = _queryBuilder.Build(resolver.ParametrizedQuery, true); - DbDataReader dbDataReader = await _queryExecutor.ExecuteQueryAsync(queryText, parameters); + SqlQueryStructure structure = new(context, _metadataStoreProvider, _queryBuilder); + Console.WriteLine(structure.ToString()); + DbDataReader dbDataReader = await _queryExecutor.ExecuteQueryAsync(structure.ToString(), parameters); - // Deserialize results into list of JsonDocuments and return + // 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/DataGateway.Service/Resolvers/SqlQueryStructure.cs b/DataGateway.Service/Resolvers/SqlQueryStructure.cs new file mode 100644 index 0000000000..a96fe070fa --- /dev/null +++ b/DataGateway.Service/Resolvers/SqlQueryStructure.cs @@ -0,0 +1,146 @@ +using Azure.DataGateway.Services; +using HotChocolate.Language; +using HotChocolate.Resolvers; +using HotChocolate.Types; +using System.Collections.Generic; + +namespace Azure.DataGateway.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 (ISelectionNode node in Selections) + { + var 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()}"; + 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); + + ObjectField 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); + string 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/DataGateway.Service/Services/FileMetadataStoreProvider.cs b/DataGateway.Service/Services/FileMetadataStoreProvider.cs index 64a53f7930..7a639fbd8e 100644 --- a/DataGateway.Service/Services/FileMetadataStoreProvider.cs +++ b/DataGateway.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 (GraphQLQueryResolver resolver in _config.QueryResolvers) { @@ -105,6 +103,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/DataGateway.Service/Services/IMetadataStoreProvider.cs b/DataGateway.Service/Services/IMetadataStoreProvider.cs index 6006ea7188..77755198da 100644 --- a/DataGateway.Service/Services/IMetadataStoreProvider.cs +++ b/DataGateway.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/DataGateway.Service/Services/ResolverMiddleware.cs b/DataGateway.Service/Services/ResolverMiddleware.cs index ae458f892b..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; @@ -32,62 +33,86 @@ 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 + 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, parameters); + } + else + { + context.Result = await _queryEngine.ExecuteAsync(context, 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) + else if (context.Selection.Field.Type.IsLeafType()) { - //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) + { + context.Result = jsonElement.ToString(); + } + else + { + context.Result = null; + } } - else + else if (IsInnerObject(context)) { - 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) + { + //TODO: Try to avoid additional deserialization/serialization here. + context.Result = JsonDocument.Parse(jsonElement.ToString()); + } + else + { + context.Result = null; + } } - } - - 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 + else if (context.Selection.Type.IsListType()) { - 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) + { + //TODO: Try to avoid additional deserialization/serialization here. + context.Result = JsonSerializer.Deserialize>(jsonElement.ToString()); + } + else + { + context.Result = null; + } } - } - await _next(context); + await _next(context); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + Console.WriteLine(ex.StackTrace); + throw; + } } private static bool IsInnerObject(IMiddlewareContext context) 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/books.gql b/DataGateway.Service/books.gql new file mode 100644 index 0000000000..886ef2bd4c --- /dev/null +++ b/DataGateway.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/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" - } - ] -} diff --git a/DataGateway.Service/sql-config.json b/DataGateway.Service/sql-config.json new file mode 100644 index 0000000000..0ba0cc6e69 --- /dev/null +++ b/DataGateway.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" + } + } + } + } +}