diff --git a/.gitignore b/.gitignore
index 2c69ae7b..c7df4fee 100644
--- a/.gitignore
+++ b/.gitignore
@@ -375,3 +375,6 @@ MigrationBackup/
# Fody - auto-generated XML schema
FodyWeavers.xsd
src/EdgeDB.ExampleApp/dump.db
+
+# EdgeDB.Net CLI watcher info file
+edgeql.dotnet.watcher.process
\ No newline at end of file
diff --git a/EdgeDB.Net.sln b/EdgeDB.Net.sln
index 5e490f2b..459228e6 100644
--- a/EdgeDB.Net.sln
+++ b/EdgeDB.Net.sln
@@ -21,8 +21,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{67ED9EF0
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EdgeDB.QueryBuilder.OperatorGenerator", "tools\EdgeDB.QueryBuilder.OperatorGenerator\EdgeDB.QueryBuilder.OperatorGenerator.csproj", "{1557B745-EB4F-449A-9BE7-180C8990AD47}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EdgeDB.DotnetTool", "tools\EdgeDB.DotnetTool\EdgeDB.DotnetTool.csproj", "{74DB9D5E-9BA9-4282-90EA-2F7BDC4C4FBD}"
-EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EdgeDB.Tests.Integration", "tests\EdgeDB.Tests.Integration\EdgeDB.Tests.Integration.csproj", "{C189294A-4990-4A06-B120-A0AF03A798C6}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EdgeDB.Tests.Benchmarks", "tests\EdgeDB.Tests.Benchmarks\EdgeDB.Tests.Benchmarks.csproj", "{5FFD1E88-614D-409B-8420-F9571AC7CA60}"
@@ -35,6 +33,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EdgeDB.Serializer.Experimen
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EdgeDB.Examples.ExampleTODOApi", "examples\EdgeDB.Examples.ExampleTODOApi\EdgeDB.Examples.ExampleTODOApi.csproj", "{E38429C6-53A5-4311-8189-1F78238666DC}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EdgeDB.Net.CLI", "src\EdgeDB.Net.CLI\EdgeDB.Net.CLI.csproj", "{77D1980E-9835-4D16-B7B4-6CB5A4BD570C}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CLI Generation Example", "CLI Generation Example", "{46E94884-7A3D-4B48-9734-916ECCCB85C0}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EdgeDB.Examples.GenerationExample", "examples\EdgeDB.Examples.GenerationExample\EdgeDB.Examples.GenerationExample.csproj", "{4BCAA352-8488-46A9-B3C9-D2C50E937AFC}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EdgeDB.Generated", "examples\EdgeDB.Examples.GenerationExample\EdgeDB.Generated\EdgeDB.Generated.csproj", "{E03D7BD1-B093-44FF-B276-410345430523}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -61,10 +67,6 @@ Global
{1557B745-EB4F-449A-9BE7-180C8990AD47}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1557B745-EB4F-449A-9BE7-180C8990AD47}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1557B745-EB4F-449A-9BE7-180C8990AD47}.Release|Any CPU.Build.0 = Release|Any CPU
- {74DB9D5E-9BA9-4282-90EA-2F7BDC4C4FBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {74DB9D5E-9BA9-4282-90EA-2F7BDC4C4FBD}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {74DB9D5E-9BA9-4282-90EA-2F7BDC4C4FBD}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {74DB9D5E-9BA9-4282-90EA-2F7BDC4C4FBD}.Release|Any CPU.Build.0 = Release|Any CPU
{C189294A-4990-4A06-B120-A0AF03A798C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C189294A-4990-4A06-B120-A0AF03A798C6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C189294A-4990-4A06-B120-A0AF03A798C6}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -85,6 +87,18 @@ Global
{E38429C6-53A5-4311-8189-1F78238666DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E38429C6-53A5-4311-8189-1F78238666DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E38429C6-53A5-4311-8189-1F78238666DC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {77D1980E-9835-4D16-B7B4-6CB5A4BD570C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {77D1980E-9835-4D16-B7B4-6CB5A4BD570C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {77D1980E-9835-4D16-B7B4-6CB5A4BD570C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {77D1980E-9835-4D16-B7B4-6CB5A4BD570C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4BCAA352-8488-46A9-B3C9-D2C50E937AFC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4BCAA352-8488-46A9-B3C9-D2C50E937AFC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4BCAA352-8488-46A9-B3C9-D2C50E937AFC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4BCAA352-8488-46A9-B3C9-D2C50E937AFC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E03D7BD1-B093-44FF-B276-410345430523}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E03D7BD1-B093-44FF-B276-410345430523}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E03D7BD1-B093-44FF-B276-410345430523}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E03D7BD1-B093-44FF-B276-410345430523}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -95,12 +109,15 @@ Global
{CD4A4B5D-4B67-4E64-A13B-3F7BF770B056} = {E6B9FABC-241B-4561-9A94-E67B6BE380E2}
{4CC2101C-D0AB-4F08-A0FD-205F96958196} = {025AAADF-16AF-4367-9C3D-9E60EDED832F}
{1557B745-EB4F-449A-9BE7-180C8990AD47} = {67ED9EF0-7828-44C0-8CB0-DEBD69EC94CA}
- {74DB9D5E-9BA9-4282-90EA-2F7BDC4C4FBD} = {67ED9EF0-7828-44C0-8CB0-DEBD69EC94CA}
{C189294A-4990-4A06-B120-A0AF03A798C6} = {E6B9FABC-241B-4561-9A94-E67B6BE380E2}
{5FFD1E88-614D-409B-8420-F9571AC7CA60} = {E6B9FABC-241B-4561-9A94-E67B6BE380E2}
{3A4AAAA0-9948-43D3-B838-8EFAC130240C} = {67ED9EF0-7828-44C0-8CB0-DEBD69EC94CA}
{6FA68DEA-D398-4A5B-8025-5F15C728F04C} = {49B6FB80-A675-4ECA-802C-2337A4F37566}
{E38429C6-53A5-4311-8189-1F78238666DC} = {6FC214F5-C912-4D99-91B1-3E9F52A4E11B}
+ {77D1980E-9835-4D16-B7B4-6CB5A4BD570C} = {025AAADF-16AF-4367-9C3D-9E60EDED832F}
+ {46E94884-7A3D-4B48-9734-916ECCCB85C0} = {6FC214F5-C912-4D99-91B1-3E9F52A4E11B}
+ {4BCAA352-8488-46A9-B3C9-D2C50E937AFC} = {46E94884-7A3D-4B48-9734-916ECCCB85C0}
+ {E03D7BD1-B093-44FF-B276-410345430523} = {46E94884-7A3D-4B48-9734-916ECCCB85C0}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {4E90C94F-D693-4411-82F3-2051DE1BE052}
diff --git a/examples/EdgeDB.Examples.ExampleTODOApi/Properties/launchSettings.json b/examples/EdgeDB.Examples.ExampleTODOApi/Properties/launchSettings.json
index 9f782db0..0999bbcb 100644
--- a/examples/EdgeDB.Examples.ExampleTODOApi/Properties/launchSettings.json
+++ b/examples/EdgeDB.Examples.ExampleTODOApi/Properties/launchSettings.json
@@ -1,4 +1,4 @@
-{
+{
"$schema": "https://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
@@ -11,7 +11,6 @@
"profiles": {
"EdgeDB.Examples.ExampleTODOApi": {
"commandName": "Project",
- "dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7155;http://localhost:5155",
@@ -28,4 +27,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Examples.GenerationExample.csproj b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Examples.GenerationExample.csproj
new file mode 100644
index 00000000..7ca5e342
--- /dev/null
+++ b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Examples.GenerationExample.csproj
@@ -0,0 +1,20 @@
+
+
+
+ Exe
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/CreateUser.g.cs b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/CreateUser.g.cs
new file mode 100644
index 00000000..0cf9674d
--- /dev/null
+++ b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/CreateUser.g.cs
@@ -0,0 +1,34 @@
+// AUTOGENERATED: DO NOT MODIFY
+// edgeql:B9692A5CA0A9992246361197BEACBDE398A5A30C5DCCC83BCACD8C80D5842FEB
+// Generated on 2022-08-26T11:55:03.5378711Z
+#nullable enable
+using EdgeDB;
+
+namespace EdgeDB.Generated;
+
+#region Types
+[EdgeDBType]
+public sealed class CreateUserResult
+{
+ [EdgeDBProperty("id")]
+ public Guid Id { get; set; }
+}
+
+#endregion
+
+public static class CreateUser
+{
+ public static readonly string Query = @"INSERT Person {
+ name := $name,
+ email := $email
+}
+UNLESS CONFLICT ON .email
+ELSE (SELECT Person)";
+
+ public static Task ExecuteAsync(IEdgeDBQueryable client, String? name, String? email, CancellationToken token = default)
+ => client.QuerySingleAsync(Query, new Dictionary() { { "name", name }, { "email", email } }, capabilities: (Capabilities)1ul, token: token);
+
+ public static Task CreateUserAsync(this IEdgeDBQueryable client, String? name, String? email, CancellationToken token = default)
+ => ExecuteAsync(client, name, email, token: token);
+}
+#nullable restore
diff --git a/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/DeleteUser.g.cs b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/DeleteUser.g.cs
new file mode 100644
index 00000000..07e05e9c
--- /dev/null
+++ b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/DeleteUser.g.cs
@@ -0,0 +1,29 @@
+// AUTOGENERATED: DO NOT MODIFY
+// edgeql:28E22B161B70DB5DE661AC86709BEADD3F9DC475559DE90F194CBAF24A5A880C
+// Generated on 2022-08-28T12:51:13.6300794Z
+#nullable enable
+using EdgeDB;
+
+namespace EdgeDB.Generated;
+
+#region Types
+[EdgeDBType]
+public sealed class DeleteUserResult
+{
+ [EdgeDBProperty("id")]
+ public Guid Id { get; set; }
+}
+
+#endregion
+
+public static class DeleteUser
+{
+ public static readonly string Query = @"delete Person filter .email = $email";
+
+ public static Task ExecuteAsync(IEdgeDBQueryable client, String? email, CancellationToken token = default)
+ => client.QuerySingleAsync(Query, new Dictionary() { { "email", email } }, capabilities: (Capabilities)1ul, token: token);
+
+ public static Task DeleteUserAsync(this IEdgeDBQueryable client, String? email, CancellationToken token = default)
+ => ExecuteAsync(client, email, token: token);
+}
+#nullable restore
diff --git a/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/EdgeDB.Generated.csproj b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/EdgeDB.Generated.csproj
new file mode 100644
index 00000000..f4a98cb4
--- /dev/null
+++ b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/EdgeDB.Generated.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/GetUser.g.cs b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/GetUser.g.cs
new file mode 100644
index 00000000..7ba9436a
--- /dev/null
+++ b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/GetUser.g.cs
@@ -0,0 +1,38 @@
+// AUTOGENERATED: DO NOT MODIFY
+// edgeql:E116C5881529C70BFC6A9D0350075968AD967D391FEE0F74033FA870A1FE56DD
+// Generated on 2022-08-26T11:52:03.5962767Z
+#nullable enable
+using EdgeDB;
+
+namespace EdgeDB.Generated;
+
+#region Types
+[EdgeDBType]
+public sealed class GetUserResult
+{
+ [EdgeDBProperty("id")]
+ public Guid Id { get; set; }
+
+ [EdgeDBProperty("name")]
+ public String? Name { get; set; }
+
+ [EdgeDBProperty("email")]
+ public String? Email { get; set; }
+}
+
+#endregion
+
+public static class GetUser
+{
+ public static readonly string Query = @"select Person {
+ name, email
+}
+filter .email = $email";
+
+ public static Task ExecuteAsync(IEdgeDBQueryable client, String? email, CancellationToken token = default)
+ => client.QuerySingleAsync(Query, new Dictionary() { { "email", email } }, capabilities: (Capabilities)0ul, token: token);
+
+ public static Task GetUserAsync(this IEdgeDBQueryable client, String? email, CancellationToken token = default)
+ => ExecuteAsync(client, email, token: token);
+}
+#nullable restore
diff --git a/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/UpdateUser.g.cs b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/UpdateUser.g.cs
new file mode 100644
index 00000000..8023db81
--- /dev/null
+++ b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/UpdateUser.g.cs
@@ -0,0 +1,29 @@
+// AUTOGENERATED: DO NOT MODIFY
+// edgeql:A1C5568E24B561F8D8CE4109C9E8E604C801BF104AFA3A4C07A0F2EDB720AC27
+// Generated on 2022-08-26T15:33:42.9431602Z
+#nullable enable
+using EdgeDB;
+
+namespace EdgeDB.Generated;
+
+#region Types
+[EdgeDBType]
+public sealed class UpdateUserResult
+{
+ [EdgeDBProperty("id")]
+ public Guid Id { get; set; }
+}
+
+#endregion
+
+public static class UpdateUser
+{
+ public static readonly string Query = @"update Person filter .id = $id set { name := $name, email := $email }";
+
+ public static Task ExecuteAsync(IEdgeDBQueryable client, Guid id, String? name, String? email, CancellationToken token = default)
+ => client.QuerySingleAsync(Query, new Dictionary() { { "id", id }, { "name", name }, { "email", email } }, capabilities: (Capabilities)1ul, token: token);
+
+ public static Task UpdateUserAsync(this IEdgeDBQueryable client, Guid id, String? name, String? email, CancellationToken token = default)
+ => ExecuteAsync(client, id, name, email, token: token);
+}
+#nullable restore
diff --git a/examples/EdgeDB.Examples.GenerationExample/Program.cs b/examples/EdgeDB.Examples.GenerationExample/Program.cs
new file mode 100644
index 00000000..6a530b2a
--- /dev/null
+++ b/examples/EdgeDB.Examples.GenerationExample/Program.cs
@@ -0,0 +1,11 @@
+using EdgeDB;
+using EdgeDB.Generated;
+
+// create a client
+var client = new EdgeDBClient();
+
+// create a user
+await client.CreateUserAsync(name: "example", email: "example@example.com");
+
+// Get a user based on email
+var user = await client.GetUserAsync(email: "example@example.com");
diff --git a/examples/EdgeDB.Examples.GenerationExample/Scrips/CreateUser.edgeql b/examples/EdgeDB.Examples.GenerationExample/Scrips/CreateUser.edgeql
new file mode 100644
index 00000000..e65e9a37
--- /dev/null
+++ b/examples/EdgeDB.Examples.GenerationExample/Scrips/CreateUser.edgeql
@@ -0,0 +1,6 @@
+INSERT Person {
+ name := $name,
+ email := $email
+}
+UNLESS CONFLICT ON .email
+ELSE (SELECT Person)
\ No newline at end of file
diff --git a/examples/EdgeDB.Examples.GenerationExample/Scrips/DeleteUser.edgeql b/examples/EdgeDB.Examples.GenerationExample/Scrips/DeleteUser.edgeql
new file mode 100644
index 00000000..5a7b4883
--- /dev/null
+++ b/examples/EdgeDB.Examples.GenerationExample/Scrips/DeleteUser.edgeql
@@ -0,0 +1 @@
+delete Person filter .email = $email
\ No newline at end of file
diff --git a/examples/EdgeDB.Examples.GenerationExample/Scrips/GetUser.edgeql b/examples/EdgeDB.Examples.GenerationExample/Scrips/GetUser.edgeql
new file mode 100644
index 00000000..2d4f61b6
--- /dev/null
+++ b/examples/EdgeDB.Examples.GenerationExample/Scrips/GetUser.edgeql
@@ -0,0 +1,4 @@
+select Person {
+ name, email
+}
+filter .email = $email
\ No newline at end of file
diff --git a/examples/EdgeDB.Examples.GenerationExample/Scrips/UpdateUser.edgeql b/examples/EdgeDB.Examples.GenerationExample/Scrips/UpdateUser.edgeql
new file mode 100644
index 00000000..ded619fb
--- /dev/null
+++ b/examples/EdgeDB.Examples.GenerationExample/Scrips/UpdateUser.edgeql
@@ -0,0 +1 @@
+update Person filter .id = $id set { name := $name, email := $email }
\ No newline at end of file
diff --git a/src/EdgeDB.Net.CLI/Arguments/ConnectionArguments.cs b/src/EdgeDB.Net.CLI/Arguments/ConnectionArguments.cs
new file mode 100644
index 00000000..7bde6ef0
--- /dev/null
+++ b/src/EdgeDB.Net.CLI/Arguments/ConnectionArguments.cs
@@ -0,0 +1,94 @@
+using CommandLine;
+using EdgeDB.CLI.Utils;
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace EdgeDB.CLI.Arguments
+{
+ public class ConnectionArguments : LogArgs
+ {
+ [Option("dsn", HelpText = "DSN for EdgeDB to connect to (overrides all other options except password)")]
+ public string? DSN { get; set; }
+
+ [Option("credentials-file", HelpText = "Path to JSON file to read credentials from")]
+ public string? CredentialsFile { get; set; }
+
+ [Option('I', "instance", HelpText = "Local instance name created with edgedb instance create to connect to (overrides host and port)")]
+ public string? Instance { get; set; }
+
+ [Option('H', "host", HelpText = "Host of the EdgeDB instance")]
+ public string? Host { get; set; }
+
+ [Option('P', "port", HelpText = "Port to connect to EdgeDB")]
+ public int? Port { get; set; }
+
+ [Option('d', "database", HelpText = "Database name to connect to")]
+ public string? Database { get; set; }
+
+ [Option('u', "user", HelpText = "User name of the EdgeDB user")]
+ public string? User { get; set; }
+
+ [Option("password", HelpText = "Ask for password on the terminal (TTY)")]
+ public bool Password { get; set; }
+
+ [Option("password-from-stdin", HelpText = "Read the password from stdin rather than TTY (useful for scripts)")]
+ public bool PasswordFromSTDIN { get; set; }
+
+ [Option("tls-ca-file", HelpText = "Certificate to match server against\n\nThis might either be full self-signed server certificate or certificate authority (CA) certificate that server certificate is signed with.")]
+ public string? TLSCAFile { get; set; }
+
+ [Option("tls-security", HelpText = "Specify the client-side TLS security mode.")]
+ public TLSSecurityMode? TLSSecurity { get; set; }
+
+ public EdgeDBConnection GetConnection()
+ {
+ if (DSN is not null)
+ return EdgeDBConnection.FromDSN(DSN);
+
+ if (Instance is not null)
+ return EdgeDBConnection.FromInstanceName(Instance);
+
+ if (CredentialsFile is not null)
+ return JsonConvert.DeserializeObject(File.ReadAllText(CredentialsFile))
+ ?? throw new NullReferenceException($"The file '{CredentialsFile}' didn't contain a valid credential definition");
+
+ // create the resolved connection
+ var resolved = EdgeDBConnection.ResolveConnection();
+
+ if (Host is not null)
+ resolved.Hostname = Host;
+
+ if (Port.HasValue)
+ resolved.Port = Port.Value;
+
+ if (Database is not null)
+ resolved.Database = Database;
+
+ if (User is not null)
+ resolved.Username = User;
+
+ if (Password)
+ {
+ // read password from console
+ Console.Write($"Password for '{resolved.Database}': ");
+
+ resolved.Password = ConsoleUtils.ReadSecretInput();
+ }
+
+ if (PasswordFromSTDIN)
+ resolved.Password = Console.ReadLine();
+
+ if (TLSCAFile is not null)
+ resolved.TLSCertificateAuthority = TLSCAFile;
+
+ if (TLSSecurity.HasValue)
+ resolved.TLSSecurity = TLSSecurity.Value;
+
+ return resolved;
+ }
+ }
+}
diff --git a/src/EdgeDB.Net.CLI/Arguments/LogArgs.cs b/src/EdgeDB.Net.CLI/Arguments/LogArgs.cs
new file mode 100644
index 00000000..b72ed64a
--- /dev/null
+++ b/src/EdgeDB.Net.CLI/Arguments/LogArgs.cs
@@ -0,0 +1,23 @@
+using CommandLine;
+using Microsoft.Extensions.Logging;
+using Serilog.Events;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace EdgeDB.CLI.Arguments
+{
+ ///
+ /// A class containing logger arguments.
+ ///
+ public class LogArgs
+ {
+ ///
+ /// Gets or sets the log level for the default logger.
+ ///
+ [Option("loglevel", HelpText = "Configure the log level")]
+ public LogEventLevel LogLevel { get; set; } = LogEventLevel.Information;
+ }
+}
diff --git a/src/EdgeDB.Net.CLI/Commands/FileWatch.cs b/src/EdgeDB.Net.CLI/Commands/FileWatch.cs
new file mode 100644
index 00000000..c7ec55cf
--- /dev/null
+++ b/src/EdgeDB.Net.CLI/Commands/FileWatch.cs
@@ -0,0 +1,260 @@
+using CommandLine;
+using EdgeDB.CLI;
+using EdgeDB.CLI.Arguments;
+using EdgeDB.CLI.Utils;
+using Newtonsoft.Json;
+using Serilog;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace EdgeDB.CLI.Commands
+{
+ ///
+ /// A class representing the watch command.
+ ///
+ [Verb("watch", HelpText = "Configure the file watcher")]
+ public class FileWatch : ConnectionArguments, ICommand
+ {
+ ///
+ /// Gets or sets whether or not to kill a already running watcher.
+ ///
+ [Option('k', "kill", SetName = "functions", HelpText = "Kill the current running watcher for the project")]
+ public bool Kill { get; set; }
+
+ ///
+ /// Gets or sets whether or not to start a watcher.
+ ///
+ [Option('s', "start", SetName = "functions", HelpText = "Start a watcher for the current project")]
+ public bool Start { get; set; }
+
+ ///
+ /// Gets or sets the output directory to place the generated source files.
+ ///
+ [Option('o', "output", HelpText = "The output directory for the generated source to be placed.")]
+ public string? OutputDirectory { get; set; }
+
+ ///
+ /// Gets or sets the project name/namespace of generated files.
+ ///
+ [Option('n', "project-name", HelpText = "The name of the generated project and namespace of generated files.")]
+ public string GeneratedProjectName { get; set; } = "EdgeDB.Generated";
+
+ ///
+ public Task ExecuteAsync(ILogger logger)
+ {
+ // get project root
+ var root = ProjectUtils.GetProjectRoot();
+ var watcher = ProjectUtils.GetWatcherProcess(root);
+ logger.Debug("Watcher?: {@Watcher} Project root: {@Root}", watcher is not null, root);
+ try
+ {
+ if (!Kill && !Start)
+ {
+ // display information about the current watcher
+ if (watcher is null)
+ {
+ logger.Information("No file watcher is running for {@Dir}", root);
+ return Task.CompletedTask;
+ }
+
+ logger.Information("File watcher is watching {@Dir}", Path.Combine(root, "*.edgeql"));
+ logger.Information("Process ID: {@Id}", watcher.Id);
+
+ return Task.CompletedTask;
+ }
+
+ if (Kill)
+ {
+ if (watcher is null)
+ {
+ logger.Error("No watcher is running for {@Dir}", root);
+ return Task.CompletedTask;
+ }
+
+ watcher.Kill();
+ logger.Information("Watcher process {@PID} kiled", watcher.Id);
+
+ return Task.CompletedTask;
+ }
+
+ if (Start)
+ {
+ if (watcher is not null)
+ {
+ logger.Error("Watcher already running! Process ID: {@PID}", watcher.Id);
+ return Task.CompletedTask;
+ }
+
+ var connection = GetConnection();
+
+ OutputDirectory ??= Path.Combine(Environment.CurrentDirectory, GeneratedProjectName);
+
+ var pid = ProjectUtils.StartWatchProcess(connection, root, OutputDirectory, GeneratedProjectName);
+
+ logger.Information("Watcher process started, PID: {@PID}", pid);
+ }
+
+ return Task.CompletedTask;
+ }
+ catch(Exception x)
+ {
+ logger.Error(x, "Failed to run watcher command");
+ return Task.CompletedTask;
+ }
+ }
+ }
+
+ [Verb("file-watch-internal", Hidden = true)]
+ internal class FileWatchInternal : ICommand
+ {
+ [Option("connection")]
+ public string? Connection { get; set; }
+
+ [Option("dir")]
+ public string? Dir { get; set; }
+
+ [Option("output")]
+ public string? Output { get; set; }
+
+ [Option("namespace")]
+ public string? Namespace { get; set; }
+
+ private readonly FileSystemWatcher _watcher = new();
+ private EdgeDBTcpClient? _client;
+ private readonly SemaphoreSlim _mutex = new(1, 1);
+ private readonly ConcurrentStack _writeQueue = new();
+ private TaskCompletionSource _writeDispatcher = new();
+
+ public async Task ExecuteAsync(ILogger logger)
+ {
+ if (Connection is null)
+ throw new InvalidOperationException("Connection must be specified");
+
+ _client = new(JsonConvert.DeserializeObject(Connection)!, new());
+
+ _watcher.Path = Dir!;
+ _watcher.Filter = "*.edgeql";
+ _watcher.IncludeSubdirectories = true;
+
+ _watcher.Error += _watcher_Error;
+
+ _watcher.Changed += CreatedAndUpdated;
+ _watcher.Created += CreatedAndUpdated;
+ _watcher.Deleted += _watcher_Deleted;
+ _watcher.Renamed += _watcher_Renamed;
+
+ _watcher.NotifyFilter = NotifyFilters.Attributes
+ | NotifyFilters.CreationTime
+ | NotifyFilters.DirectoryName
+ | NotifyFilters.FileName
+ | NotifyFilters.LastAccess
+ | NotifyFilters.LastWrite
+ | NotifyFilters.Security
+ | NotifyFilters.Size;
+
+ _watcher.EnableRaisingEvents = true;
+
+ ProjectUtils.RegisterProcessAsWatcher(Dir!);
+
+ logger.Information("Watcher started for {@Dir}", Output);
+
+ while (true)
+ {
+ await _writeDispatcher.Task;
+
+ while(_writeQueue.TryPop(out var info))
+ {
+ await GenerateAsync(info);
+ }
+
+ _writeDispatcher = new();
+ }
+ }
+
+ private bool IsValidFile(string path)
+ => !path.StartsWith(Path.Combine(Dir!, "dbschema", "migrations"));
+
+ public async Task GenerateAsync(EdgeQLParser.GenerationTargetInfo info)
+ {
+ await _mutex.WaitAsync().ConfigureAwait(false);
+
+ try
+ {
+ // check if the file is a valid file
+ if (!IsValidFile(info.EdgeQLFilePath!))
+ return;
+
+ await _client!.ConnectAsync();
+
+ try
+ {
+ var result = await EdgeQLParser.ParseAndGenerateAsync(_client, Namespace!, info);
+ File.WriteAllText(info.TargetFilePath!, result.Code);
+ }
+ catch (EdgeDBErrorException err)
+ {
+ // error with file
+ Console.WriteLine(err.Message);
+ }
+ }
+ catch(Exception x)
+ {
+ Console.WriteLine(x);
+ }
+ finally
+ {
+ _mutex.Release();
+ }
+ }
+
+ private void CreatedAndUpdated(object sender, FileSystemEventArgs e)
+ {
+ if (!FileUtils.WaitForHotFile(e.FullPath))
+ return;
+
+ // wait an extra second to make sure the file is fully written
+ Thread.Sleep(1000);
+
+ var info = EdgeQLParser.GetTargetInfo(e.FullPath, Output!);
+
+ if (info.IsGeneratedTargetExistsAndIsUpToDate())
+ return;
+
+ _writeQueue.Push(info);
+ _writeDispatcher.TrySetResult();
+ }
+
+ private void _watcher_Deleted(object sender, FileSystemEventArgs e)
+ {
+ if (!IsValidFile(e.FullPath))
+ return;
+
+ // get the generated file name
+ var path = Path.Combine(Output!, $"{Path.GetFileNameWithoutExtension(e.FullPath)}.g.cs");
+
+ if (File.Exists(path))
+ File.Delete(path);
+ }
+
+ private void _watcher_Renamed(object sender, RenamedEventArgs e)
+ {
+ if (!IsValidFile(e.FullPath))
+ return;
+
+ var oldPath = Path.Combine(Output!, $"{Path.GetFileNameWithoutExtension(e.OldFullPath)}.g.cs");
+ var newPath = Path.Combine(Output!, $"{Path.GetFileNameWithoutExtension(e.FullPath)}.g.cs");
+
+ if (File.Exists(oldPath))
+ File.Move(oldPath, newPath);
+ }
+
+ private void _watcher_Error(object sender, ErrorEventArgs e)
+ {
+ Console.Error.WriteLine($"An error occored: {e.GetException()}");
+ }
+ }
+}
diff --git a/src/EdgeDB.Net.CLI/Commands/Generate.cs b/src/EdgeDB.Net.CLI/Commands/Generate.cs
new file mode 100644
index 00000000..d795437c
--- /dev/null
+++ b/src/EdgeDB.Net.CLI/Commands/Generate.cs
@@ -0,0 +1,139 @@
+using CommandLine;
+using EdgeDB.CLI.Arguments;
+using EdgeDB.CLI.Utils;
+using EdgeDB.Codecs;
+using Newtonsoft.Json;
+using Serilog;
+using System.Diagnostics;
+using System.Reflection;
+using System.Text.RegularExpressions;
+
+namespace EdgeDB.CLI;
+
+///
+/// A class representing the generate command.
+///
+[Verb("generate", HelpText = "Generate or updates csharp classes from .edgeql files.")]
+public class Generate : ConnectionArguments, ICommand
+{
+ ///
+ /// Gets or sets whether or not a class library should be generated.
+ ///
+ [Option('p', "project", HelpText = "Whether or not to create the default class library that will contain the generated source code. Enabled by default.")]
+ public bool GenerateProject { get; set; } = true;
+
+ ///
+ /// Gets or sets the output directory the generated source files will be placed.
+ ///
+ [Option('o', "output", HelpText = "The output directory for the generated source to be placed. When generating a project, source files will be placed in that projects directory. Default is the current directory")]
+ public string? OutputDirectory { get; set; }
+
+ ///
+ /// Gets or sets the project name/namespace.
+ ///
+ [Option('n', "project-name", HelpText = "The name of the generated project and namespace of generated files.")]
+ public string GeneratedProjectName { get; set; } = "EdgeDB.Generated";
+
+ ///
+ /// Gets or sets whether or not to force (re)generate source files.
+ ///
+ [Option('f', "force", HelpText = "Force regeneration of files")]
+ public bool Force { get; set; }
+
+ ///
+ /// Gets or sets whether or not to start a watch process post-generate.
+ ///
+ [Option("watch", HelpText = "Listens for any changes or new edgeql files and (re)generates them automatically")]
+ public bool Watch { get; set; }
+
+ ///
+ public async Task ExecuteAsync(ILogger logger)
+ {
+ // get connection info
+ var connection = GetConnection();
+
+ // create the client
+ var client = new EdgeDBTcpClient(connection, new());
+
+ logger.Information("Connecting to {@Host}:{@Port}...", connection.Hostname, connection.Port);
+ await client.ConnectAsync();
+
+ var projectRoot = ProjectUtils.GetProjectRoot();
+
+ OutputDirectory ??= Environment.CurrentDirectory;
+
+ Directory.CreateDirectory(OutputDirectory);
+
+ if (GenerateProject && !Directory.Exists(Path.Combine(OutputDirectory, GeneratedProjectName)))
+ {
+ logger.Information("Creating project {@ProjectName}...", GeneratedProjectName);
+ await ProjectUtils.CreateGeneratedProjectAsync(OutputDirectory, GeneratedProjectName);
+ }
+
+ if(GenerateProject)
+ OutputDirectory = Path.Combine(OutputDirectory, GeneratedProjectName);
+
+ // find edgeql files
+ var edgeqlFiles = ProjectUtils.GetTargetEdgeQLFiles(projectRoot).ToArray();
+
+ // error if any are the same name
+ var groupFileNames = edgeqlFiles.GroupBy(x => Path.GetFileNameWithoutExtension(x));
+ if(groupFileNames.Any(x => x.Count() > 1))
+ {
+ foreach(var conflict in groupFileNames.Where(x => x.Count() > 1))
+ {
+ logger.Fatal($"{{@Count}} files contain the same name ({string.Join(" - ", conflict.Select(x => x))})", conflict.Count());
+ }
+
+ return;
+ }
+
+ logger.Information("Generating {@FileCount} files...", edgeqlFiles.Length);
+
+ for(int i = 0; i != edgeqlFiles.Length; i++)
+ {
+ var file = edgeqlFiles[i];
+ var info = EdgeQLParser.GetTargetInfo(file, OutputDirectory);
+
+ if (!Force && info.IsGeneratedTargetExistsAndIsUpToDate())
+ {
+ logger.Warning("Skipping {@File}: File already generated and up-to-date.", file);
+ continue;
+ }
+
+ try
+ {
+ var result = await EdgeQLParser.ParseAndGenerateAsync(client, GeneratedProjectName, info);
+ File.WriteAllText(info.TargetFilePath!, result.Code);
+ }
+ catch (EdgeDBErrorException error)
+ {
+ logger.Error("Skipping {@File}: Failed to parse - {@Message} at line {@Line} column {@Column}",
+ file,
+ error.Message,
+ error.ErrorResponse.Attributes.FirstOrDefault(x => x.Code == 65523).ToString() ?? "??",
+ error.ErrorResponse.Attributes.FirstOrDefault(x => x.Code == 65524).ToString() ?? "??");
+ continue;
+ }
+
+ logger.Debug("{@EdgeQL} => {@CSharp}", file, info.TargetFilePath);
+ }
+
+ logger.Information("Generation complete!");
+
+ if(Watch)
+ {
+ var existing = ProjectUtils.GetWatcherProcess(projectRoot);
+
+ if(existing is not null)
+ {
+ logger.Warning("Watching already running");
+ return;
+ }
+
+ logger.Information("Starting file watcher...");
+ var pid = ProjectUtils.StartWatchProcess(connection, projectRoot, OutputDirectory, GeneratedProjectName);
+ logger.Information("File watcher process started, PID: {@PID}", pid);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EdgeDB.Net.CLI/EdgeDB.Net.CLI.csproj b/src/EdgeDB.Net.CLI/EdgeDB.Net.CLI.csproj
new file mode 100644
index 00000000..cfc8deef
--- /dev/null
+++ b/src/EdgeDB.Net.CLI/EdgeDB.Net.CLI.csproj
@@ -0,0 +1,26 @@
+
+
+
+ Exe
+ net6.0
+ enable
+ enable
+ EdgeDB.Net.CLI
+ EdgeDB
+ A CLI tool to generate C# files from edgeql files
+ edgeql
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/EdgeDB.Net.CLI/EdgeQLParser.cs b/src/EdgeDB.Net.CLI/EdgeQLParser.cs
new file mode 100644
index 00000000..72c761f5
--- /dev/null
+++ b/src/EdgeDB.Net.CLI/EdgeQLParser.cs
@@ -0,0 +1,557 @@
+using EdgeDB.CLI.Utils;
+using EdgeDB.Codecs;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+
+namespace EdgeDB.CLI
+{
+ ///
+ /// Represents a class responsible for parsing and transpiling edgeql to C#.
+ ///
+ internal class EdgeQLParser
+ {
+ ///
+ /// The file header regex for generate C# files.
+ ///
+ private static readonly Regex _headerHashRegex = new(@"\/\/ edgeql:([0-9a-fA-F]{64})");
+
+ ///
+ /// Parses and generates a from a given client and
+ /// .
+ ///
+ /// The client to preform the parse with.
+ /// The namespace for the .
+ ///
+ /// The information containimg the edgeql and related
+ /// content used to parse and generate.
+ ///
+ /// A containing the generated C# code, hash,
+ /// classname, and executer name.
+ ///
+ public static async Task ParseAndGenerateAsync(EdgeDBTcpClient client, string @namespace, GenerationTargetInfo targetInfo)
+ {
+ var parseResult = await client.ParseAsync(targetInfo.EdgeQL!, Cardinality.Many, IOFormat.Binary, Capabilities.All, default);
+
+ return GenerateCSharpFromEdgeQL(@namespace, targetInfo, parseResult);
+ }
+
+ ///
+ /// Checks whether an autogenerate header matches a hash.
+ ///
+ /// The header of the autogenerated file to check against.
+ /// The hash to check.
+ ///
+ /// if the header matches the hash; otherwise .
+ ///
+ public static bool TargetFileHashMatches(string header, string hash)
+ {
+ var match = _headerHashRegex.Match(header);
+ if (!match.Success)
+ return false;
+ return match.Groups[1].Value == hash;
+ }
+
+ ///
+ /// Gets the for the given file and
+ /// generation target directory.
+ ///
+ ///
+ /// This operation requires the file to be opened. This function will
+ /// throw if the file is being used by a different process.
+ ///
+ /// The path of the edgeql file.
+ /// The output target directory.
+ ///
+ /// The for the given file.
+ ///
+ public static GenerationTargetInfo GetTargetInfo(string edgeqlFilePath, string targetDir)
+ {
+ string fileContent = File.ReadAllText(edgeqlFilePath);
+ var hash = HashUtils.HashEdgeQL(fileContent);
+ var fileName = TextUtils.ToPascalCase(Path.GetFileName(edgeqlFilePath).Split('.')[0]);
+
+ return new GenerationTargetInfo
+ {
+ EdgeQLFileNameWithoutExtension = fileName,
+ EdgeQL = fileContent,
+ EdgeQLHash = hash,
+ EdgeQLFilePath = edgeqlFilePath,
+ TargetFilePath = Path.Combine(targetDir, $"{fileName}.g.cs")
+ };
+ }
+
+ ///
+ /// Represents a generation target, containing useful information for it.
+ ///
+ public class GenerationTargetInfo
+ {
+ ///
+ /// Gets or sets the edgeql file name without extension.
+ ///
+ public string? EdgeQLFileNameWithoutExtension { get; set; }
+
+ ///
+ /// Gets or sets the edgeql file path.
+ ///
+ public string? EdgeQLFilePath { get; set; }
+
+ ///
+ /// Gets or sets the output target file path.
+ ///
+ public string? TargetFilePath { get; set; }
+
+ ///
+ /// Gets or sets the edgeql.
+ ///
+ public string? EdgeQL { get; set; }
+
+ ///
+ /// Gets or sets the hash of the edgeql.
+ ///
+ public string? EdgeQLHash { get; set; }
+
+ ///
+ /// Checks if the target file exists and the header matches the hash of the edgeql.
+ ///
+ ///
+ public bool IsGeneratedTargetExistsAndIsUpToDate()
+ {
+ var lines = File.Exists(TargetFilePath) ? File.ReadAllLines(TargetFilePath) : Array.Empty();
+
+ return File.Exists(TargetFilePath) && lines.Length >= 2 && TargetFileHashMatches(lines[1], EdgeQLHash!);
+ }
+ }
+
+ ///
+ /// A class representing the result of a edgeql -> cs generation operation.
+ ///
+ public class GenerationResult
+ {
+ ///
+ /// Gets the code generated from edgeql.
+ ///
+ public string? Code { get; set; }
+
+ ///
+ /// Gets the hash of the edgeql and header of the generated code.
+ ///
+ public string? EdgeQLHash { get; set; }
+
+ ///
+ /// Gets the name of the class containing the execute method.
+ ///
+ public string? ExecuterClassName { get; set; }
+
+ ///
+ /// Gets the name of the return result that the executer method returns.
+ ///
+ public string? ReturnResult { get; set; }
+
+ ///
+ /// Gets a collection of parameters (edgeql arguments) for the executer function.
+ ///
+ public IEnumerable? Parameters { get; set; }
+ }
+
+ ///
+ /// Generates a from the given
+ /// and .
+ ///
+ /// The namepsace for the generated code to consume.
+ /// The used for generation.
+ /// The parse result from edgedb.
+ ///
+ ///
+ private static GenerationResult GenerateCSharpFromEdgeQL(string @namespace, GenerationTargetInfo targetInfo, EdgeDBBinaryClient.ParseResult parseResult)
+ {
+ var codecType = GetTypeInfoFromCodec(parseResult.OutCodec.Codec, $"{targetInfo.EdgeQLFileNameWithoutExtension} Result");
+
+ // create the class writer
+
+ var writer = new CodeWriter();
+ writer.AppendLine("// AUTOGENERATED: DO NOT MODIFY");
+ writer.AppendLine($"// edgeql:{targetInfo.EdgeQLHash}");
+ writer.AppendLine($"// Generated on {DateTime.UtcNow:O}");
+ writer.AppendLine("#nullable enable");
+ writer.AppendLine($"using EdgeDB;");
+ writer.AppendLine();
+ writer.AppendLine($"namespace {@namespace};");
+ writer.AppendLine();
+
+ var refTypes = new List();
+ var compliledTypes = new List<(CodecTypeInfo Info, string Reference)?>();
+
+ var mainResult = BuildTypes(codecType, out var typeName, refTypes);
+
+ compliledTypes.Add((codecType, mainResult));
+
+ if (refTypes.Any() || codecType.IsObject)
+ {
+ var seenTypes = new HashSet(refTypes) { codecType };
+ var refStack = new Stack(refTypes);
+
+ writer.AppendLine("#region Types");
+ writer.AppendLine(mainResult);
+
+ // circle dependency safe!
+ while (refStack.TryPop(out var typeInfo))
+ {
+ var complRef = compliledTypes.FirstOrDefault(x => x!.Value.Info.BodyEquals(typeInfo));
+
+ if (complRef is not null)
+ {
+ writer.AppendLine(complRef.Value.Reference);
+ continue;
+ }
+
+ var newTypes = new List();
+ var result = BuildTypes(typeInfo, out _, newTypes);
+
+ if (newTypes.Any())
+ foreach (var newType in newTypes.Where(x => !seenTypes.TryGetValue(x, out _)))
+ refStack.Push(newType);
+
+ writer.AppendLine(result);
+ compliledTypes.Add((typeInfo, result));
+ }
+
+ writer.AppendLine("#endregion");
+ writer.AppendLine();
+ }
+
+ // create the executor class
+ var classScope = writer.BeginScope($"public static class {targetInfo.EdgeQLFileNameWithoutExtension}");
+
+ writer.AppendLine($"public static readonly string Query = @\"{targetInfo.EdgeQL}\";");
+ writer.AppendLine();
+ var method = parseResult.Cardinality switch
+ {
+ Cardinality.AtMostOne => "QuerySingleAsync",
+ Cardinality.One => "QueryRequiredSingleAsync",
+ _ => "QueryAsync"
+ };
+
+ var resultType = parseResult.Cardinality switch
+ {
+ Cardinality.AtMostOne => $"{typeName ?? mainResult}?",
+ Cardinality.One => typeName ?? mainResult,
+ _ => $"IReadOnlyCollection<{typeName ?? mainResult}?>"
+ };
+
+ // build args
+ IEnumerable? argParameters;
+ IEnumerable? methodArgs;
+ if (parseResult.InCodec.Codec is NullCodec)
+ {
+ methodArgs = Array.Empty();
+ argParameters = Array.Empty();
+ }
+ else if (parseResult.InCodec.Codec is Codecs.Object argCodec)
+ {
+ argParameters = argParameters = argCodec.PropertyNames.Select((x, i) => BuildTypes(GetTypeInfoFromCodec(argCodec.InnerCodecs[i], x), out _, namesOnScalar: true, camelCase: true));
+ methodArgs = methodArgs = argCodec.PropertyNames.Select((x, i) =>
+ {
+ return $"{{ \"{x}\", {TextUtils.ToCamelCase(x)} }}";
+ });
+ }
+ else
+ throw new InvalidOperationException("Argument codec is malformed");
+
+ writer.AppendLine($"public static Task<{resultType}> ExecuteAsync(IEdgeDBQueryable client{(argParameters.Any() ? $", {string.Join(", ", argParameters)}" : "")}, CancellationToken token = default)");
+ writer.AppendLine($" => client.{method}<{typeName ?? mainResult}>(Query{(methodArgs.Any() ? $", new Dictionary() {{ {string.Join(", ", methodArgs)} }}" : "")}, capabilities: (Capabilities){(ulong)parseResult.Capabilities}ul, token: token);");
+
+ writer.AppendLine();
+ writer.AppendLine($"public static Task<{resultType}> {targetInfo.EdgeQLFileNameWithoutExtension}Async(this IEdgeDBQueryable client{(argParameters.Any() ? $", {string.Join(", ", argParameters)}" : "")}, CancellationToken token = default)");
+ writer.AppendLine($" => ExecuteAsync(client{(argParameters.Any() ? $", {string.Join(", ", argParameters.Select(x => x.Split(' ')[1]))}" : "")}, token: token);");
+
+ classScope.Dispose();
+
+ writer.AppendLine("#nullable restore");
+
+ return new()
+ {
+ ExecuterClassName = targetInfo.EdgeQLFileNameWithoutExtension,
+ EdgeQLHash = targetInfo.EdgeQLHash,
+ ReturnResult = resultType,
+ Parameters = argParameters,
+ Code = writer.ToString()
+ };
+ }
+
+ ///
+ /// Builds the C# equivalent type from the given .
+ ///
+ /// The codec info containing the type information to build.
+ /// The name of the type or scalar.
+ ///
+ /// The reference to a list of used sub-objects, use to keep track of generated
+ /// reference types.
+ ///
+ ///
+ /// Whether or not to include the on the generated
+ /// result.
+ ///
+ ///
+ /// Whether or not names will be camel case () or
+ /// pascal case ().
+ ///
+ ///
+ /// Whether or not to return the type name of objects without generating them. When
+ /// this is , the object info is added to the
+ /// .
+ ///
+ ///
+ /// The generated C# of the provided .
+ ///
+ /// The is unknown.
+ private static string BuildTypes(CodecTypeInfo info, out string? resultTypeName,
+ List? usedObjects = null, bool namesOnScalar = false, bool camelCase = false,
+ bool returnTypeName = false)
+ {
+ usedObjects ??= new();
+ var writer = new CodeWriter();
+ resultTypeName = null;
+
+ var fmtName = info.Name is not null
+ ? camelCase
+ ? TextUtils.ToCamelCase(info.Name)
+ : TextUtils.ToPascalCase(info.Name)
+ : null;
+
+ if (info.IsObject)
+ {
+ if (returnTypeName)
+ {
+ // add to used objects
+ usedObjects.Add(info);
+ if (namesOnScalar)
+ return $"{info.GetUniqueTypeName()}? {fmtName}";
+ return fmtName!;
+ }
+
+ // create the main class
+ writer.AppendLine("[EdgeDBType]");
+ writer.AppendLine($"public sealed class {info.GetUniqueTypeName()}");
+ using (_ = writer.BeginScope())
+ {
+ var properties = info.Children!.Select(x =>
+ {
+ var result = BuildTypes(x, out _, usedObjects, namesOnScalar: true, returnTypeName: true);
+ return $"[EdgeDBProperty(\"{x.Name}\")]{Environment.NewLine} public {result} {{ get; set; }}";
+ });
+
+ writer.AppendLine(string.Join($"{Environment.NewLine}{Environment.NewLine} ", properties));
+ }
+
+ resultTypeName = info.TypeName!;
+ return writer.ToString();
+ }
+
+ if (info.IsTuple)
+ {
+ var types = info.Children!.Select(x => BuildTypes(x, out _, usedObjects, true));
+ return $"({string.Join(", ", types)}){(namesOnScalar ? $" {fmtName}" : "")}";
+ }
+
+ if (info.IsArray)
+ {
+ var result = BuildTypes(info.Children!.Single(), out _, usedObjects, true);
+ return $"{result}[]{(namesOnScalar ? $" {fmtName}" : "")}";
+ }
+
+ if (info.IsSet)
+ {
+ var result = BuildTypes(info.Children!.Single(), out _, usedObjects, true);
+ return $"IEnumerable<{result}>{(namesOnScalar ? $" {fmtName}" : "")}";
+ }
+
+
+ if (info.TypeName is not null)
+ return $"{info.TypeName}{(namesOnScalar ? $" {fmtName}" : "")}";
+
+ throw new InvalidOperationException($"Unknown type def {info}");
+ }
+
+ ///
+ /// Creates a from the given .
+ ///
+ /// The codec to get the type info for.
+ /// The optional name of the codec.
+ /// The optional parent of the codec.
+ ///
+ /// A representing type information about the provided codec.
+ ///
+ ///
+ /// No could be created from the provided codec.
+ ///
+ private static CodecTypeInfo GetTypeInfoFromCodec(ICodec codec, string? name = null, CodecTypeInfo? parent = null)
+ {
+ CodecTypeInfo info;
+
+ switch (codec)
+ {
+ case Codecs.Object obj:
+ {
+ info = new CodecTypeInfo
+ {
+ IsObject = true,
+ TypeName = TextUtils.ToPascalCase(name!)
+ };
+ info.Children = obj.InnerCodecs
+ .Select((x, i) =>
+ obj.PropertyNames[i] is "__tname__" or "__tid__"
+ ? null
+ : GetTypeInfoFromCodec(x, obj.PropertyNames[i], info))
+ .Where(x => x is not null)!;
+ }
+ break;
+ case ICodec set when ReflectionUtils.IsSubclassOfRawGeneric(typeof(Set<>), set.GetType()):
+ {
+ info = new CodecTypeInfo
+ {
+ IsArray = true,
+ };
+ info.Children = new[]
+ {
+ GetTypeInfoFromCodec((ICodec)set.GetType().GetField("_innerCodec", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(set)!, parent: info)
+ };
+ }
+ break;
+ case ICodec array when ReflectionUtils.IsSubclassOfRawGeneric(typeof(Array<>), array.GetType()):
+ {
+ info = new CodecTypeInfo
+ {
+ IsSet = true,
+ };
+ info.Children = new[]
+ {
+ GetTypeInfoFromCodec((ICodec)array.GetType().GetField("_innerCodec", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(array)!, parent: info)
+ };
+ }
+ break;
+ case Codecs.Tuple tuple:
+ {
+ info = new CodecTypeInfo
+ {
+ IsTuple = true,
+ };
+ info.Children = tuple.InnerCodecs.Select(x => GetTypeInfoFromCodec(x, parent: info));
+ }
+ break;
+ case ICodec scalar when ReflectionUtils.IsSubclassOfInterfaceGeneric(typeof(IScalarCodec<>), codec!.GetType()):
+ {
+ info = new CodecTypeInfo
+ {
+ TypeName = $"{codec.GetType().GetInterface("IScalarCodec`1")!.GetGenericArguments()[0].Name}{(codec.GetType().GetInterface("IScalarCodec`1")!.GetGenericArguments()[0].IsValueType ? "" : "?")}",
+ };
+ }
+ break;
+ default:
+ throw new InvalidOperationException($"Unknown codec {codec}");
+ }
+
+ info.Name = name ?? info.Name;
+ info.Parent = parent;
+
+ return info;
+ }
+
+ ///
+ /// Represents an expanded form of a , containing
+ /// ease-to-parse information about a codec.
+ ///
+ private class CodecTypeInfo
+ {
+ ///
+ /// Gets whether or not the codec represents an array.
+ ///
+ public bool IsArray { get; init; }
+
+ ///
+ /// Gets whether or not the codec represents a set.
+ ///
+ public bool IsSet { get; init; }
+
+ ///
+ /// Gets whether or not the codec represents an object.
+ ///
+ public bool IsObject { get; init; }
+
+ ///
+ /// Gets whether or not the codec represents a tuple.
+ ///
+ public bool IsTuple { get; init; }
+
+ ///
+ /// Gets or sets the optional name of the codec.
+ ///
+ public string? Name { get; set; }
+
+ ///
+ /// Gets or sets the optional dotnet type name that represents what the
+ /// codec serializes/deserializes.
+ ///
+ public string? TypeName { get; set; }
+
+ ///
+ /// Gets or sets the child s for this parent .
+ ///
+ public IEnumerable? Children { get; set; }
+
+ ///
+ /// Gets the paret .
+ ///
+ public CodecTypeInfo? Parent { get; set; }
+
+ ///
+ /// Checks whether or not the current 's body is equal to
+ /// the given .
+ ///
+ ///
+ /// The to check against.
+ ///
+ ///
+ /// if the 's body matches the
+ /// current ; otherwise .
+ ///
+ public bool BodyEquals(CodecTypeInfo info)
+ {
+ return IsArray == info.IsArray &&
+ IsSet == info.IsSet &&
+ IsObject == info.IsObject &&
+ IsTuple == info.IsTuple &&
+ (info.Children?.SequenceEqual(Children ?? Array.Empty()) ?? false);
+ }
+
+ ///
+ /// Gets a unique name for the current .
+ ///
+ ///
+ /// A unique name representing the current .
+ ///
+ public string GetUniqueTypeName()
+ {
+ List path = new() { TypeName };
+ var p = Parent;
+ while (p is not null)
+ {
+ path.Add(p.TypeName);
+ p = p.Parent;
+ }
+ path.Reverse();
+ return string.Join("", path.Where(x => x is not null));
+ }
+
+ ///
+ public override string ToString()
+ {
+ return $"{Name} ({TypeName})";
+ }
+ }
+ }
+}
diff --git a/src/EdgeDB.Net.CLI/ICommand.cs b/src/EdgeDB.Net.CLI/ICommand.cs
new file mode 100644
index 00000000..3d1986ad
--- /dev/null
+++ b/src/EdgeDB.Net.CLI/ICommand.cs
@@ -0,0 +1,18 @@
+using Serilog;
+
+namespace EdgeDB.CLI;
+
+///
+/// Represents a generic command that can be executed.
+///
+interface ICommand
+{
+ ///
+ /// Executes the command, awaiting its completion.
+ ///
+ /// The logger for the command.
+ ///
+ /// A task that represents the execution flow of the command.
+ ///
+ Task ExecuteAsync(ILogger logger);
+}
\ No newline at end of file
diff --git a/src/EdgeDB.Net.CLI/Program.cs b/src/EdgeDB.Net.CLI/Program.cs
new file mode 100644
index 00000000..83d5ffa1
--- /dev/null
+++ b/src/EdgeDB.Net.CLI/Program.cs
@@ -0,0 +1,65 @@
+using CommandLine;
+using CommandLine.Text;
+using EdgeDB.CLI;
+using EdgeDB.CLI.Arguments;
+using Serilog;
+
+// intialize our logger
+Log.Logger = new LoggerConfiguration()
+ .MinimumLevel.Verbose()
+ .WriteTo.Console()
+ .CreateLogger();
+
+// find all types that extend the 'ICommand' interface.
+var commands = typeof(Program).Assembly.GetTypes().Where(x => x.GetInterfaces().Any(x => x == typeof(ICommand)));
+
+// create our command line arg parser with no default help writer.
+var parser = new Parser(x =>
+{
+ x.HelpWriter = null;
+});
+
+// parse the 'args'.
+var result = parser.ParseArguments(args, commands.ToArray());
+
+try
+{
+ // execute the parsed result if it is a command.
+ var commandResult = await result.WithParsedAsync(x =>
+ {
+ // if the command supports log args, change the log level for our logger.
+ if(x is LogArgs logArgs)
+ {
+ Log.Logger = new LoggerConfiguration()
+ .MinimumLevel.Is(logArgs.LogLevel)
+ .WriteTo.Console()
+ .CreateLogger();
+ }
+
+ // execute the command with the logger.
+ return x.ExecuteAsync(Log.Logger);
+ });
+
+ // if the result was not parsed to a valid command.
+ result.WithNotParsed(err =>
+ {
+ // build the help text.
+ var helpText = HelpText.AutoBuild(commandResult, h =>
+ {
+ h.AdditionalNewLineAfterOption = true;
+ h.Heading = "EdgeDB.Net CLI";
+ h.Copyright = "Copyright (c) 2022 EdgeDB";
+
+ return h;
+ }, e => e, verbsIndex: true);
+
+ // write out the help text.
+ Console.WriteLine(helpText);
+ });
+
+}
+catch (Exception x)
+{
+ // log the root exception.
+ Log.Logger.Fatal(x, "Critical error");
+}
\ No newline at end of file
diff --git a/src/EdgeDB.Net.CLI/Properties/launchSettings.json b/src/EdgeDB.Net.CLI/Properties/launchSettings.json
new file mode 100644
index 00000000..a30ce718
--- /dev/null
+++ b/src/EdgeDB.Net.CLI/Properties/launchSettings.json
@@ -0,0 +1,11 @@
+{
+ "profiles": {
+ "EdgeDB.Net.CLI": {
+ "commandName": "Project",
+ "commandLineArgs": "watch -k"
+ //"commandLineArgs": "watch -start -o C:\\Users\\lynch\\source\\repos\\EdgeDB\\examples\\EdgeDB.Examples.GenerationExample\\EdgeDB.Generated"
+ //"commandLineArgs": "file-watch-internal --dir C:\\Users\\lynch\\source\\repos\\EdgeDB --output C:\\Users\\lynch\\source\\repos\\EdgeDB\\examples\\EdgeDB.Examples.GenerationExample --namespace EdgeDB.Generated --connection \"{\\\"user\\\":\\\"edgedb\\\",\\\"password\\\":\\\"x3WnskHEbmXBvnT1Vf14ABuL\\\",\\\"Hostname\\\":\\\"127.0.0.1\\\",\\\"port\\\":10711,\\\"database\\\":\\\"edgedb\\\",\\\"tls_cert_data\\\":\\\"-----BEGIN CERTIFICATE-----\nMIIC0zCCAbugAwIBAgIRAPQegLaiakFsl0hPzNUeEaAwDQYJKoZIhvcNAQELBQAw\nGDEWMBQGA1UEAwwNRWRnZURCIFNlcnZlcjAeFw0yMjA3MjIxMzA1MzBaFw00MTA5\nMjExMzA1MzBaMBgxFjAUBgNVBAMMDUVkZ2VEQiBTZXJ2ZXIwggEiMA0GCSqGSIb3\nDQEBAQUAA4IBDwAwggEKAoIBAQDOSRrWTy9oC9J7CWvGKg9Rg7o/R1D4AQO++rML\npSBd1obrSlJB/p5kLWsMuSM29dgRubqqY7PrrjvcDedpDwyAp9JeIz6tIkkqo+/d\n8dRCaV/3wHa5BgfN8fEE6blnmdrQ3asuVafZ989+qoa1PjLc9iU5JJgp/gb/SS7Z\n02L+0y+vw/QjvQcRoiDfCcIfasp6Z2Jor9RCd+AchNG7m0teQ2iK9BXG33QJUXdA\nYwXEALQxJCi23v197xZQUJbXlxh4+YIFJiYA95gXzoB/lBUo/CQSNwxzkofwLtV/\nWT+CmAP25D/v6/rLWi+ps+vM5GFcIetMK0Bk8yzkr+EXB07/AgMBAAGjGDAWMBQG\nA1UdEQQNMAuCCWxvY2FsaG9zdDANBgkqhkiG9w0BAQsFAAOCAQEAUDNnRYoHCtW5\nRMBBf0YTr9zpLA69cK9J6OfY7wPW1ROd3/qmPz57fJ1DOq+5R4U8+rAvk6pO5p6l\nLG3RyQrC5bJuhyxxcBMkc43INC641gMrJlMm4SSRI0HO1nYiZLFdyUjTfrH//ThA\nFVDwAFR/Xw97R1qZQdZYLhxdnRjYIySWUs8yBljtuEB5iorDX9AaXQ25kZ4f8eTE\nsmxezVKQks+FUPdX6yhb++uWvqqzK2flWuiztZmQWLCQnydKuwyBx4izhsiSZ3wL\nbdO8MaTbq2o2dzvzQAWxsf8Wg1Xogep4PA0goquTJvgjzcHuEtyl8404Mb4U/Fpy\ngIFB7zA3bA==\n-----END CERTIFICATE-----\n\\\",\\\"tls_ca\\\":\\\"-----BEGIN CERTIFICATE-----\nMIIC0zCCAbugAwIBAgIRAPQegLaiakFsl0hPzNUeEaAwDQYJKoZIhvcNAQELBQAw\nGDEWMBQGA1UEAwwNRWRnZURCIFNlcnZlcjAeFw0yMjA3MjIxMzA1MzBaFw00MTA5\nMjExMzA1MzBaMBgxFjAUBgNVBAMMDUVkZ2VEQiBTZXJ2ZXIwggEiMA0GCSqGSIb3\nDQEBAQUAA4IBDwAwggEKAoIBAQDOSRrWTy9oC9J7CWvGKg9Rg7o/R1D4AQO++rML\npSBd1obrSlJB/p5kLWsMuSM29dgRubqqY7PrrjvcDedpDwyAp9JeIz6tIkkqo+/d\n8dRCaV/3wHa5BgfN8fEE6blnmdrQ3asuVafZ989+qoa1PjLc9iU5JJgp/gb/SS7Z\n02L+0y+vw/QjvQcRoiDfCcIfasp6Z2Jor9RCd+AchNG7m0teQ2iK9BXG33QJUXdA\nYwXEALQxJCi23v197xZQUJbXlxh4+YIFJiYA95gXzoB/lBUo/CQSNwxzkofwLtV/\nWT+CmAP25D/v6/rLWi+ps+vM5GFcIetMK0Bk8yzkr+EXB07/AgMBAAGjGDAWMBQG\nA1UdEQQNMAuCCWxvY2FsaG9zdDANBgkqhkiG9w0BAQsFAAOCAQEAUDNnRYoHCtW5\nRMBBf0YTr9zpLA69cK9J6OfY7wPW1ROd3/qmPz57fJ1DOq+5R4U8+rAvk6pO5p6l\nLG3RyQrC5bJuhyxxcBMkc43INC641gMrJlMm4SSRI0HO1nYiZLFdyUjTfrH//ThA\nFVDwAFR/Xw97R1qZQdZYLhxdnRjYIySWUs8yBljtuEB5iorDX9AaXQ25kZ4f8eTE\nsmxezVKQks+FUPdX6yhb++uWvqqzK2flWuiztZmQWLCQnydKuwyBx4izhsiSZ3wL\nbdO8MaTbq2o2dzvzQAWxsf8Wg1Xogep4PA0goquTJvgjzcHuEtyl8404Mb4U/Fpy\ngIFB7zA3bA==\n-----END CERTIFICATE-----\n\\\",\\\"tls_security\\\":0}\""
+ //"commandLineArgs": "generate -o C:\\Users\\lynch\\source\\repos\\EdgeDB\\examples\\EdgeDB.Examples.GenerationExample --watch"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EdgeDB.Net.CLI/Utils/CodeWriter.cs b/src/EdgeDB.Net.CLI/Utils/CodeWriter.cs
new file mode 100644
index 00000000..2bcd71e3
--- /dev/null
+++ b/src/EdgeDB.Net.CLI/Utils/CodeWriter.cs
@@ -0,0 +1,122 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace EdgeDB.CLI
+{
+ ///
+ /// A utility class for writing code.
+ ///
+ internal class CodeWriter
+ {
+ ///
+ /// The content of the code writer.
+ ///
+ public readonly StringBuilder Content = new();
+
+ ///
+ /// Gets the indentation level of the current code writer.
+ ///
+ public int IndentLevel { get; private set; }
+
+ ///
+ /// The scope tracker providing an implementation of IDisposable.
+ ///
+ private readonly ScopeTracker _scopeTracker; // We only need one. It can be reused.
+
+ ///
+ /// Creates a new .
+ ///
+ public CodeWriter()
+ {
+ _scopeTracker = new(this);
+ }
+
+ ///
+ /// Appends a string to the current code writer.
+ ///
+ /// The line to append
+ public void Append(string line)
+ => Content.Append(line);
+
+ ///
+ /// Appends a line to the current code writer, respecting
+ /// and ending in a line terminator.
+ ///
+ /// The line to append
+ public void AppendLine(string line)
+ => Content.Append(new string(' ', IndentLevel)).AppendLine(line);
+
+ ///
+ /// Appends an empty line to the current code writer, adding a line terminator to the end.
+ ///
+ public void AppendLine()
+ => Content.AppendLine();
+
+ ///
+ /// Begins a new scope with the specified line.
+ ///
+ /// The line to append.
+ /// An representing the scope returned.
+ public IDisposable BeginScope(string line)
+ {
+ AppendLine(line);
+ return BeginScope();
+ }
+
+ ///
+ /// Begins a new scope, incrementing the indent level until the scope is disposed.
+ ///
+ /// An representing the scope returned.
+ public IDisposable BeginScope()
+ {
+ Content.Append(new string(' ', IndentLevel)).AppendLine("{");
+ IndentLevel += 4;
+ return _scopeTracker;
+ }
+
+ ///
+ /// Ends a scope, decrementing the indent level.
+ ///
+ public void EndScope()
+ {
+ IndentLevel -= 4;
+ Content.Append(new string(' ', IndentLevel)).AppendLine("}");
+ }
+
+ ///
+ /// Converts the current code writer to a .
+ ///
+ /// A string representing the code written to the code writer.
+ public override string ToString()
+ => Content.ToString();
+
+ ///
+ /// An implementation of responsible for scope decrementing.
+ ///
+ class ScopeTracker : IDisposable
+ {
+ ///
+ /// Gets the That created this .
+ ///
+ public CodeWriter Parent { get; }
+
+ ///
+ /// Constructs a new .
+ ///
+ /// The parent that created the .
+ public ScopeTracker(CodeWriter parent)
+ {
+ Parent = parent;
+ }
+
+ ///
+ /// Disposes and ends the scope of this .
+ ///
+ public void Dispose()
+ {
+ Parent.EndScope();
+ }
+ }
+ }
+}
diff --git a/src/EdgeDB.Net.CLI/Utils/ConsoleUtils.cs b/src/EdgeDB.Net.CLI/Utils/ConsoleUtils.cs
new file mode 100644
index 00000000..8ed422cf
--- /dev/null
+++ b/src/EdgeDB.Net.CLI/Utils/ConsoleUtils.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace EdgeDB.CLI.Utils
+{
+ ///
+ /// A utility class containing methods related to the console.
+ ///
+ internal class ConsoleUtils
+ {
+ ///
+ /// Reads a secret input from STDIN.
+ ///
+ ///
+ /// The entered input received from STDIN.
+ ///
+ public static string ReadSecretInput()
+ {
+ string input = "";
+ ConsoleKey key;
+ do
+ {
+ var keyInfo = Console.ReadKey(true);
+ key = keyInfo.Key;
+
+ if (key == ConsoleKey.Backspace)
+ input = input.Length > 0 ? input[..^1] : "";
+ else if(!char.IsControl(keyInfo.KeyChar))
+ input += keyInfo.KeyChar;
+ }
+ while (key != ConsoleKey.Enter);
+
+ return input;
+ }
+ }
+}
diff --git a/src/EdgeDB.Net.CLI/Utils/FileUtils.cs b/src/EdgeDB.Net.CLI/Utils/FileUtils.cs
new file mode 100644
index 00000000..aef28823
--- /dev/null
+++ b/src/EdgeDB.Net.CLI/Utils/FileUtils.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace EdgeDB.CLI.Utils
+{
+ ///
+ /// A utility class containing methods related to file operations.
+ ///
+ internal static class FileUtils
+ {
+ ///
+ /// Waits synchronously for a file to be released.
+ ///
+ /// The file path.
+ /// The timeout.
+ /// if the file was released; otherwise .
+ public static bool WaitForHotFile(string path, int timeout = 5000)
+ {
+ var start = DateTime.UtcNow;
+ while (true)
+ {
+ try
+ {
+ using var fs = File.Open(path, FileMode.Open, FileAccess.ReadWrite, FileShare.None);
+ fs.Close();
+ return true;
+ }
+ catch
+ {
+ if ((DateTime.UtcNow - start).TotalMilliseconds >= timeout)
+ return false;
+
+ Thread.Sleep(200);
+ }
+ }
+ }
+ }
+}
diff --git a/src/EdgeDB.Net.CLI/Utils/HashUtils.cs b/src/EdgeDB.Net.CLI/Utils/HashUtils.cs
new file mode 100644
index 00000000..60716046
--- /dev/null
+++ b/src/EdgeDB.Net.CLI/Utils/HashUtils.cs
@@ -0,0 +1,27 @@
+using EdgeDB.Utils;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace EdgeDB.CLI.Utils
+{
+ ///
+ /// A utility class containing methods related to hashes.
+ ///
+ internal static class HashUtils
+ {
+ ///
+ /// Hashes edgeql for an autogenerated file header.
+ ///
+ /// The edgeql to hash.
+ /// The hashed edgeql as hex.
+ public static string HashEdgeQL(string edgeql)
+ {
+ using var algo = SHA256.Create();
+ return HexConverter.ToHex(algo.ComputeHash(Encoding.UTF8.GetBytes(edgeql)));
+ }
+ }
+}
diff --git a/src/EdgeDB.Net.CLI/Utils/ProjectUtils.cs b/src/EdgeDB.Net.CLI/Utils/ProjectUtils.cs
new file mode 100644
index 00000000..750e7bd0
--- /dev/null
+++ b/src/EdgeDB.Net.CLI/Utils/ProjectUtils.cs
@@ -0,0 +1,152 @@
+using CliWrap;
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace EdgeDB.CLI.Utils
+{
+ ///
+ /// A utility class containing methods realted to edgedb projects.
+ ///
+ internal static class ProjectUtils
+ {
+ ///
+ /// Gets the edgedb project root from the current directory.
+ ///
+ /// The project root directory.
+ /// The project could not be found.
+ public static string GetProjectRoot()
+ {
+ var directory = Environment.CurrentDirectory;
+ bool foundRoot = false;
+
+ while (!foundRoot)
+ {
+ if (
+ !(foundRoot = Directory.GetFiles(directory, "*", SearchOption.TopDirectoryOnly).Any(x => x.EndsWith($"{Path.DirectorySeparatorChar}edgedb.toml"))) &&
+ (directory = Directory.GetParent(directory!)?.FullName) is null)
+ throw new FileNotFoundException("Could not find edgedb.toml in the current and parent directories");
+ }
+
+ return directory;
+ }
+
+ ///
+ /// Starts a watcher process.
+ ///
+ /// The connection info for the watcher process.
+ /// The project root directory.
+ /// The output directory for files the watcher generates to place.
+ /// The namespace for generated files.
+ /// The started watcher process id.
+ public static int StartWatchProcess(EdgeDBConnection connection, string root, string outputDir, string @namespace)
+ {
+ var current = Process.GetCurrentProcess();
+ var connString = JsonConvert.SerializeObject(connection).Replace("\"", "\\\"");
+
+ return Process.Start(new ProcessStartInfo
+ {
+ FileName = current.MainModule!.FileName,
+ Arguments = $"file-watch-internal --connection \"{connString}\" --dir {root} --output \"{outputDir}\" --namespace \"{@namespace}\"",
+ UseShellExecute = true,
+ CreateNoWindow = true,
+ WindowStyle = ProcessWindowStyle.Hidden
+ })!.Id;
+ }
+
+ ///
+ /// Gets the watcher process for the provided root directory.
+ ///
+ /// The project root.
+ ///
+ /// The watcher or if not found.
+ ///
+ public static Process? GetWatcherProcess(string root)
+ {
+ var file = Path.Combine(root, "edgeql.dotnet.watcher.process");
+ if (File.Exists(file) && int.TryParse(File.ReadAllText(file), out var id))
+ {
+ try
+ {
+ return Process.GetProcesses().FirstOrDefault(x => x.Id == id);
+ }
+ catch { return null; }
+ }
+
+ return null;
+ }
+
+ ///
+ /// Registers the current process as the watcher project for the given project root.
+ ///
+ /// The project root.
+ public static void RegisterProcessAsWatcher(string root)
+ {
+ var id = Environment.ProcessId;
+
+ File.WriteAllText(Path.Combine(root, "edgeql.dotnet.watcher.process"), $"{id}");
+
+ // add to gitignore if its here
+ var gitignore = Path.Combine(root, ".gitignore");
+ if (File.Exists(gitignore))
+ {
+ var contents = File.ReadAllText(gitignore);
+
+ if(!contents.Contains("edgeql.dotnet.watcher.process"))
+ {
+ contents += $"{Environment.NewLine}# EdgeDB.Net CLI watcher info file{Environment.NewLine}edgeql.dotnet.watcher.process";
+ File.WriteAllText(gitignore, contents);
+ }
+ }
+ }
+
+ ///
+ /// Creates a dotnet project.
+ ///
+ /// The target directory.
+ /// The name of the project
+ /// The project failed to be created.
+ public static async Task CreateGeneratedProjectAsync(string root, string name)
+ {
+ var result = await Cli.Wrap("dotnet")
+ .WithArguments($"new classlib --framework \"net6.0\" -n {name}")
+ .WithWorkingDirectory(root)
+ .WithStandardErrorPipe(PipeTarget.ToStream(Console.OpenStandardError()))
+ .WithStandardOutputPipe(PipeTarget.ToStream(Console.OpenStandardOutput()))
+ .ExecuteAsync();
+
+ if (result.ExitCode != 0)
+ throw new IOException($"Failed to create new project");
+
+ result = await Cli.Wrap("dotnet")
+ .WithArguments("add package EdgeDB.Net.Driver")
+ .WithWorkingDirectory(Path.Combine(root, name))
+ .WithStandardErrorPipe(PipeTarget.ToStream(Console.OpenStandardError()))
+ .WithStandardOutputPipe(PipeTarget.ToStream(Console.OpenStandardOutput()))
+ .ExecuteAsync();
+
+ if (result.ExitCode != 0)
+ throw new IOException($"Failed to create new project");
+
+ // remove default file
+ File.Delete(Path.Combine(root, name, "Class1.cs"));
+ }
+
+ ///
+ /// Gets a list of edgeql file paths for the provided root directory.
+ ///
+ ///
+ /// migration files are ignored.
+ ///
+ /// The root directory to scan for edgeql files.
+ ///
+ /// An that enumerates a collection of files ending in .edgeql.
+ ///
+ public static IEnumerable GetTargetEdgeQLFiles(string root)
+ => Directory.GetFiles(root, "*.edgeql", SearchOption.AllDirectories).Where(x => !x.StartsWith(Path.Combine(root, "dbschema", "migrations")));
+ }
+}
diff --git a/src/EdgeDB.Net.CLI/Utils/TextUtils.cs b/src/EdgeDB.Net.CLI/Utils/TextUtils.cs
new file mode 100644
index 00000000..61e2eb69
--- /dev/null
+++ b/src/EdgeDB.Net.CLI/Utils/TextUtils.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+
+namespace EdgeDB.CLI.Utils
+{
+ ///
+ /// A utility class containing methods related to text operations.
+ ///
+ internal static class TextUtils
+ {
+ ///
+ /// The current culture info.
+ ///
+ private static CultureInfo? _cultureInfo;
+
+ ///
+ /// Converts the given string to pascal case.
+ ///
+ /// The string to convert to pascal case.
+ /// A pascal-cased version of the input string.
+ public static string ToPascalCase(string input)
+ {
+ _cultureInfo ??= CultureInfo.CurrentCulture;
+ var t = Regex.Replace(input, @"[^^]([A-Z])", m => $"{m.Value[0]} {m.Groups[1].Value}");
+
+ return _cultureInfo.TextInfo.ToTitleCase(t.Replace("_", " ")).Replace(" ", "");
+ }
+
+ ///
+ /// Converts the given string to camel case.
+ ///
+ /// The string to convert to pascal case.
+ /// A camel-cased version of the input string.
+ public static string ToCamelCase(string input)
+ {
+ var p = ToPascalCase(input);
+ return $"{p[0].ToString().ToLower()}{p[1..]}";
+ }
+ }
+}
diff --git a/src/EdgeDB.Net.Driver/AssemblyInfo.cs b/src/EdgeDB.Net.Driver/AssemblyInfo.cs
index 2c4eae45..c2b2a32a 100644
--- a/src/EdgeDB.Net.Driver/AssemblyInfo.cs
+++ b/src/EdgeDB.Net.Driver/AssemblyInfo.cs
@@ -1,6 +1,7 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("EdgeDB.Net.QueryBuilder")]
+[assembly: InternalsVisibleTo("EdgeDB.Net.CLI")]
[assembly: InternalsVisibleTo("EdgeDB.Runtime")]
[assembly: InternalsVisibleTo("EdgeDB.ExampleApp")]
[assembly: InternalsVisibleTo("EdgeDB.DotnetTool")]
diff --git a/src/EdgeDB.Net.Driver/Clients/EdgeDBBinaryClient.cs b/src/EdgeDB.Net.Driver/Clients/EdgeDBBinaryClient.cs
index 08526489..a0f87843 100644
--- a/src/EdgeDB.Net.Driver/Clients/EdgeDBBinaryClient.cs
+++ b/src/EdgeDB.Net.Driver/Clients/EdgeDBBinaryClient.cs
@@ -154,6 +154,25 @@ public RawExecuteResult(ICodec codec, List data)
}
}
+ internal readonly struct ParseResult
+ {
+ public readonly CodecInfo InCodec;
+ public readonly CodecInfo OutCodec;
+ public readonly IDictionary State;
+ public readonly Cardinality Cardinality;
+ public readonly Capabilities Capabilities;
+
+ public ParseResult(CodecInfo inCodec, CodecInfo outCodec, IDictionary state,
+ Cardinality cardinality, Capabilities capabilities)
+ {
+ InCodec = inCodec;
+ OutCodec = outCodec;
+ State = state;
+ Cardinality = cardinality;
+ Capabilities = capabilities;
+ }
+ }
+
/// A general error occored.
/// The client received an .
/// The client received an unexpected message.
@@ -180,73 +199,10 @@ internal async Task ExecuteInternalAsync(string query, IDictio
try
{
- var cacheKey = CodecBuilder.GetCacheHashKey(query, cardinality ?? Cardinality.Many, format);
-
- var serializedState = Session.Serialize();
+ var parsed = await ParseAsync(query, cardinality ?? Cardinality.Many, format, capabilities, token).ConfigureAwait(false);
- List p = new();
-
- if (!CodecBuilder.TryGetCodecs(cacheKey, out var inCodecInfo, out var outCodecInfo))
- {
- bool parseHandlerPredicate(IReceiveable? packet)
- {
- p.Add(packet);
- switch (packet)
- {
- case ErrorResponse err when err.ErrorCode is not ServerErrorCodes.StateMismatchError:
- throw new EdgeDBErrorException(err);
- case CommandDataDescription descriptor:
- {
- outCodecInfo = new(descriptor.OutputTypeDescriptorId,
- CodecBuilder.BuildCodec(descriptor.OutputTypeDescriptorId, descriptor.OutputTypeDescriptorBuffer));
-
- inCodecInfo = new(descriptor.InputTypeDescriptorId,
- CodecBuilder.BuildCodec(descriptor.InputTypeDescriptorId, descriptor.InputTypeDescriptorBuffer));
-
- CodecBuilder.UpdateKeyMap(cacheKey, descriptor.InputTypeDescriptorId, descriptor.OutputTypeDescriptorId);
- }
- break;
- case StateDataDescription stateDescriptor:
- {
- _stateCodec = CodecBuilder.BuildCodec(stateDescriptor.TypeDescriptorId, stateDescriptor.TypeDescriptorBuffer);
- _stateDescriptorId = stateDescriptor.TypeDescriptorId;
- }
- break;
- case ReadyForCommand ready:
- TransactionState = ready.TransactionState;
- return true;
- default:
- break;
- }
-
- return false;
- }
-
- var stateBuf = _stateCodec?.Serialize(serializedState)!;
-
- var result = await Duplexer.DuplexAndSyncAsync(new Parse
- {
- Capabilities = capabilities,
- Query = query,
- Format = format,
- ExpectedCardinality = cardinality ?? Cardinality.Many,
- ExplicitObjectIds = _config.ExplicitObjectIds,
- StateTypeDescriptorId = _stateDescriptorId,
- StateData = stateBuf,
- ImplicitLimit = _config.ImplicitLimit,
- ImplicitTypeNames = true, // used for type builder
- ImplicitTypeIds = true, // used for type builder
- }, parseHandlerPredicate, alwaysReturnError: false, token: token).ConfigureAwait(false);
-
- if (outCodecInfo is null)
- throw new MissingCodecException("Couldn't find a valid output codec");
-
- if (inCodecInfo is null)
- throw new MissingCodecException("Couldn't find a valid input codec");
- }
-
- if (inCodecInfo.Codec is not IArgumentCodec argumentCodec)
- throw new MissingCodecException($"Cannot encode arguments, {inCodecInfo.Codec} is not a registered argument codec");
+ if (parsed.InCodec.Codec is not IArgumentCodec argumentCodec)
+ throw new MissingCodecException($"Cannot encode arguments, {parsed.InCodec.Codec} is not a registered argument codec");
List receivedData = new();
@@ -275,20 +231,20 @@ bool handler(IReceiveable msg)
ExpectedCardinality = cardinality ?? Cardinality.Many,
ExplicitObjectIds = _config.ExplicitObjectIds,
StateTypeDescriptorId = _stateDescriptorId,
- StateData = _stateCodec?.Serialize(serializedState),
+ StateData = _stateCodec?.Serialize(parsed.State),
ImplicitTypeNames = true, // used for type builder
ImplicitTypeIds = true, // used for type builder
Arguments = argumentCodec?.SerializeArguments(args) ,
ImplicitLimit = _config.ImplicitLimit,
- InputTypeDescriptorId = inCodecInfo.Id,
- OutputTypeDescriptorId = outCodecInfo.Id,
+ InputTypeDescriptorId = parsed.InCodec.Id,
+ OutputTypeDescriptorId = parsed.OutCodec.Id,
}, handler, alwaysReturnError: false, token: linkedToken).ConfigureAwait(false);
executeResult.ThrowIfErrrorResponse();
execResult = new ExecuteResult(true, null, null, query);
- return new RawExecuteResult(outCodecInfo.Codec!, receivedData);
+ return new RawExecuteResult(parsed.OutCodec.Codec!, receivedData);
}
catch (OperationCanceledException)
{
@@ -330,6 +286,79 @@ bool handler(IReceiveable msg)
}
}
+ internal async Task ParseAsync(string query, Cardinality cardinality, IOFormat format, Capabilities? capabilities, CancellationToken token)
+ {
+ var cacheKey = CodecBuilder.GetCacheHashKey(query, cardinality, format);
+
+ var serializedState = Session.Serialize();
+
+ List p = new();
+
+ if (!CodecBuilder.TryGetCodecs(cacheKey, out var inCodecInfo, out var outCodecInfo))
+ {
+ bool parseHandlerPredicate(IReceiveable? packet)
+ {
+ p.Add(packet);
+ switch (packet)
+ {
+ case ErrorResponse err when err.ErrorCode is not ServerErrorCodes.StateMismatchError:
+ throw new EdgeDBErrorException(err);
+ case CommandDataDescription descriptor:
+ {
+ outCodecInfo = new(descriptor.OutputTypeDescriptorId,
+ CodecBuilder.BuildCodec(descriptor.OutputTypeDescriptorId, descriptor.OutputTypeDescriptorBuffer));
+
+ inCodecInfo = new(descriptor.InputTypeDescriptorId,
+ CodecBuilder.BuildCodec(descriptor.InputTypeDescriptorId, descriptor.InputTypeDescriptorBuffer));
+
+ CodecBuilder.UpdateKeyMap(cacheKey, descriptor.InputTypeDescriptorId, descriptor.OutputTypeDescriptorId);
+
+ cardinality = descriptor.Cardinality;
+ capabilities = descriptor.Capabilities;
+ }
+ break;
+ case StateDataDescription stateDescriptor:
+ {
+ _stateCodec = CodecBuilder.BuildCodec(stateDescriptor.TypeDescriptorId, stateDescriptor.TypeDescriptorBuffer);
+ _stateDescriptorId = stateDescriptor.TypeDescriptorId;
+ }
+ break;
+ case ReadyForCommand ready:
+ TransactionState = ready.TransactionState;
+ return true;
+ default:
+ break;
+ }
+
+ return false;
+ }
+
+ var stateBuf = _stateCodec?.Serialize(serializedState)!;
+
+ var result = await Duplexer.DuplexAndSyncAsync(new Parse
+ {
+ Capabilities = capabilities,
+ Query = query,
+ Format = format,
+ ExpectedCardinality = cardinality,
+ ExplicitObjectIds = _config.ExplicitObjectIds,
+ StateTypeDescriptorId = _stateDescriptorId,
+ StateData = stateBuf,
+ ImplicitLimit = _config.ImplicitLimit,
+ ImplicitTypeNames = true, // used for type builder
+ ImplicitTypeIds = true, // used for type builder
+ }, parseHandlerPredicate, alwaysReturnError: false, token: token).ConfigureAwait(false);
+
+ if (outCodecInfo is null)
+ throw new MissingCodecException("Couldn't find a valid output codec");
+
+ if (inCodecInfo is null)
+ throw new MissingCodecException("Couldn't find a valid input codec");
+ }
+
+ return new ParseResult(inCodecInfo, outCodecInfo, serializedState, cardinality, capabilities ?? Capabilities.ReadOnly);
+ }
+
///
/// A general error occored.
/// The client received an .
@@ -664,7 +693,10 @@ private void ParseServerSettings(ParameterStatus status)
///
public override async ValueTask ConnectAsync(CancellationToken token = default)
{
- await _connectSemaphone.WaitAsync();
+ if (IsConnected)
+ return;
+
+ await _connectSemaphone.WaitAsync(token);
try
{
diff --git a/src/EdgeDB.Net.Driver/Codecs/Object.cs b/src/EdgeDB.Net.Driver/Codecs/Object.cs
index 28b05ec0..6f32d170 100644
--- a/src/EdgeDB.Net.Driver/Codecs/Object.cs
+++ b/src/EdgeDB.Net.Driver/Codecs/Object.cs
@@ -4,22 +4,22 @@ namespace EdgeDB.Codecs
{
internal class Object : ICodec