From 4fafbc290dc47293e5d62c9c097d8dcf4c1bb118 Mon Sep 17 00:00:00 2001 From: Jelte Fennema Date: Thu, 16 Sep 2021 15:03:01 +0200 Subject: [PATCH 1/2] Ignore more files in git --- .gitignore | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index bab11274cb..2770ea9410 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,6 @@ -Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/bin/* -Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/obj/* -Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/.idea/* -Cosmos.GraphQL.Service/Cosmos.GraphQL.Service.Tests/bin/* -Cosmos.GraphQL.Service/Cosmos.GraphQL.Service.Tests/obj/* -Cosmos.GraphQL.Service/Cosmos.GraphQL.Service.Tests/.idea/* +bin/ +obj/ +system-commandline-sentinel-files/ - -*/.idea/* +.idea/ +.vscode/ From f17007c8376fd274403f6de2065ba8438210fab3 Mon Sep 17 00:00:00 2001 From: Jelte Fennema Date: Thu, 16 Sep 2021 15:06:19 +0200 Subject: [PATCH 2/2] Add Demo project containing HotChocolate query generation --- Demo/Demo.csproj | 16 +++ Demo/Program.cs | 26 ++++ Demo/Properties/launchSettings.json | 28 ++++ Demo/Startup.cs | 215 ++++++++++++++++++++++++++++ Demo/appsettings.Development.json | 9 ++ Demo/appsettings.json | 10 ++ 6 files changed, 304 insertions(+) create mode 100644 Demo/Demo.csproj create mode 100644 Demo/Program.cs create mode 100644 Demo/Properties/launchSettings.json create mode 100644 Demo/Startup.cs create mode 100644 Demo/appsettings.Development.json create mode 100644 Demo/appsettings.json diff --git a/Demo/Demo.csproj b/Demo/Demo.csproj new file mode 100644 index 0000000000..644a23e799 --- /dev/null +++ b/Demo/Demo.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + + + + + + + + + + + + diff --git a/Demo/Program.cs b/Demo/Program.cs new file mode 100644 index 0000000000..443229a569 --- /dev/null +++ b/Demo/Program.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Demo +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/Demo/Properties/launchSettings.json b/Demo/Properties/launchSettings.json new file mode 100644 index 0000000000..792fd27f45 --- /dev/null +++ b/Demo/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:15115", + "sslPort": 44328 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Demo": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5002", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Demo/Startup.cs b/Demo/Startup.cs new file mode 100644 index 0000000000..1cbd5c0866 --- /dev/null +++ b/Demo/Startup.cs @@ -0,0 +1,215 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System.ComponentModel; +using HotChocolate.Language; +using HotChocolate.Resolvers; +using HotChocolate.Types; + +using Newtonsoft.Json.Linq; + +namespace Demo +{ + public class QueryMetadata + { + Dictionary columns; + public List conditions; + Dictionary joinQueries; + string tableName; + string tableAlias; + // public List conditions; + public QueryMetadata(string tableName, IResolverContext ctx) : this( + tableName, + "0", + ctx.Selection.SyntaxNode.SelectionSet.Selections + ) + { + // TODO: hopefully useful for later + ObjectField schema = ctx.Schema.QueryType.Fields.FirstOrDefault((field) => field.Name == "book"); + } + + public QueryMetadata(string tableName, string tableAlias, IReadOnlyList selections) + { + columns = new(); + joinQueries = new(); + conditions = new(); + + this.tableName = tableName; + this.tableAlias = tableAlias; + AddFields(selections); + } + static string Table(string name, string alias) + { + return $"{escapeIdentifier(name)} AS {escapeIdentifier(alias)}"; + } + + static string QualifiedColumn(string tableAlias, string columnName) + { + return $"{escapeIdentifier(tableAlias)}.{escapeIdentifier(columnName)}"; + } + + void AddFields(IReadOnlyList Selections) + { + int subselectionCount = 0; + foreach (var node in Selections) + { + FieldNode field = node as FieldNode; + string fieldName = field.Name.Value; + + if (field.SelectionSet == null) + { + // TODO: Get column name from JSON config + string columnName = field.Name.Value; + string column = QualifiedColumn(tableAlias, columnName); + columns.Add(fieldName, column); + } + else + { + string subtableName = "authors"; + string subtableAlias = $"{tableAlias}.{subselectionCount}"; + // TODO: Get find fieldtype in schema and use JSON config to determine join column(s) + string leftColumnName = "author_id"; + string rightColumnName = "id"; + string leftColumn = QualifiedColumn(tableAlias, leftColumnName); + string rightColumn = QualifiedColumn(subtableAlias, rightColumnName); + + QueryMetadata subquery = new(subtableName, subtableAlias, field.SelectionSet.Selections); + subquery.conditions.Add($"{leftColumn} = {rightColumn}"); + string subqueryAlias = $"q{subtableAlias}"; + joinQueries.Add(subqueryAlias, subquery); + columns.Add(fieldName, $"{escapeIdentifier(subqueryAlias)}.data"); + } + } + } + + static string escapeIdentifier(string ident) + { + // TODO: Make this safe by using SQL/PG library provided versions + return $"\"{ident}\""; + } + + static string escapeString(string str) + { + // TODO: Make this safe by using SQL/PG library provided versions + return $"'{str}'"; + } + + public override string ToString() + { + var buildObjectArgs = String.Join(", ", columns.Select(x => $"{escapeString(x.Key)}, ({x.Value})")); + string fromPart = Table(tableName, tableAlias); + fromPart += String.Join("", joinQueries.Select(x => $"LEFT OUTER JOIN LATERAL ({x.Value.ToString()}) AS {escapeIdentifier(x.Key)} ON TRUE")); + string query = $"SELECT json_build_object({buildObjectArgs}) AS data FROM {fromPart}"; + if (conditions.Count() > 0) + { + query += $" WHERE {String.Join(" AND ", conditions)}"; + } + return query; + } + + } + + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + services + .AddGraphQLServer() + .AddDocumentFromString(@" + type Query { + books: [Book] + } + + type Book { + id: ID + title: String + author: Author + author_id: ID + } + + type Author { + id: ID + name: String + birthdate: String + } + ") + + .AddResolver("Query", "books", (ctx) => + { + // Replace below with query + var metadata = new QueryMetadata("books", ctx); + JArray result = new(); + result.Add(JObject.FromObject(new + { + id = 1, + title = metadata.ToString(), + author_id = 123, + author = new + { + id = 123, + name = "Jelte", + birtdate = "2001-01-01" + } + })); + result.Add(JObject.FromObject(new + { + id = 2, + title = metadata.ToString(), + author_id = 124, + author = new + { + id = 124, + name = "Aniruddh", + birtdate = "2002-01-01" + } + })); + + return result; + }) + .AddResolver("Book", "id", (ctx) => + { + return (string)ctx.Parent()["id"]; + }) + .AddResolver("Book", "author", (ctx) => + { + return ctx.Parent()["author"]; + }) + .AddResolver("Book", "author_id", (ctx) => + { + return (int)ctx.Parent()["author_id"]; + }) + .AddResolver("Author", "id", (ctx) => + { + return (int)ctx.Parent()["id"]; + }) + .AddResolver("Author", "name", (ctx) => + { + return (string)ctx.Parent()["name"]; + }) + .AddResolver("Author", "birthdate", (ctx) => + { + return (string)ctx.Parent()["birtdate"]; + }) + ; + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app + .UseRouting() + .UseEndpoints(endpoints => + { + endpoints.MapGraphQL(); + }); + } + } +} diff --git a/Demo/appsettings.Development.json b/Demo/appsettings.Development.json new file mode 100644 index 0000000000..dba68eb124 --- /dev/null +++ b/Demo/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/Demo/appsettings.json b/Demo/appsettings.json new file mode 100644 index 0000000000..81ff877711 --- /dev/null +++ b/Demo/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +}