diff --git a/RelationalAI/Client.cs b/RelationalAI/Client.cs index 7fd1320..6fd9429 100644 --- a/RelationalAI/Client.cs +++ b/RelationalAI/Client.cs @@ -55,7 +55,7 @@ public HttpClient HttpClient public async Task CreateDatabaseAsync(string database, string engine = null, bool overwrite = false, string source = null) { - _logger.LogInformation($"CreateDatabase: {database}"); + _logger.LogInformation($"CreateDatabase: database {database}, source database {source}"); if (engine != null) { return await CreateDatabaseV1Async(database, engine, overwrite); @@ -81,26 +81,22 @@ public async Task CloneDatabaseAsync( string source, bool overwrite = false) { + _logger.LogInformation($"CloneDatabase: database {database}, engine {engine}, source {source}, overwrite {overwrite}"); var mode = CreateMode(source, overwrite); var tx = new Transaction(_context.Region, database, engine, mode, false, source); await _rest.PostAsync(MakeUrl(PathTransaction), tx.Payload(null), null, tx.QueryParams()); - return await GetDatabaseAsync(database); + return await GetDatabaseAsyncInternal(database); } public async Task GetDatabaseAsync(string database) { - var parameters = new Dictionary - { - { "name", database } - }; - - var resp = await GetResourceAsync(PathDatabase, null, parameters); - var dbs = Json.Deserialize(resp).Databases; - return dbs.Count > 0 ? dbs[0] : throw new HttpError(404, $"Database with name `{database}` not found"); + _logger.LogInformation($"GetDatabase {database}"); + return await GetDatabaseAsyncInternal(database); } public async Task> ListDatabasesAsync(DatabaseState? state = null) { + _logger.LogInformation("ListDatabases" + (state == null ? string.Empty : $"state {state}")); var parameters = new Dictionary(); if (state != null) { @@ -113,6 +109,7 @@ public async Task> ListDatabasesAsync(DatabaseState? state = null public async Task DeleteDatabaseAsync(string database) { + _logger.LogInformation($"DeleteDatabase {database}"); var data = new Dictionary { { "name", database } @@ -124,14 +121,7 @@ public async Task DeleteDatabaseAsync(string database) public async Task CreateEngineAsync(string engine, string size = "XS") { _logger.LogInformation($"CreateEngine: {engine}, size: {size}"); - var data = new Dictionary - { - { "region", _context.Region }, - { "name", engine }, - { "size", size.ToString() } - }; - var resp = await _rest.PutAsync(MakeUrl(PathEngine), data) as string; - return Json.Deserialize(resp).Engine; + return await CreateEngineAsyncInternal(engine, size); } [Obsolete("This method is deprecated, please use the exposed http client instead")] @@ -154,35 +144,19 @@ public async Task CreateEngineWithVersionAsync(string engine, string ver public async Task CreateEngineWaitAsync(string engine, string size = "XS") { - var startTime = DateTimeOffset.Now.ToUnixTimeMilliseconds(); - await CreateEngineAsync(engine, size); - var resp = await Policy - .HandleResult(e => !EngineStates.IsTerminalState(e.State, EngineStates.Provisioned)) - .RetryWithTimeout(startTime, 0.1, 120, 10 * 60) - .ExecuteAsync(() => GetEngineAsync(engine)); - - if (resp.State != EngineStates.Provisioned) - { - throw new EngineProvisionFailedException(resp); - } - - return resp; + _logger.LogInformation($"CreateEngine {engine}, size: {size}"); + return await CreateEngineWaitAsyncInternal(engine, size); } public async Task GetEngineAsync(string engine) { - var parameters = new Dictionary - { - { "name", engine }, - { "deleted_on", string.Empty } - }; - var resp = await GetResourceAsync(PathEngine, null, parameters); - var engines = Json.Deserialize(resp).Engines; - return engines.Count > 0 ? engines[0] : throw new HttpError(404, $"Engine with name `{engine}` not found"); + _logger.LogInformation($"GetEngine {engine}"); + return await GetEngineAsyncInternal(engine); } public async Task> ListEnginesAsync(string state = null) { + _logger.LogInformation($"ListEngines state {state}"); var parameters = new Dictionary(); if (state != null) { @@ -195,28 +169,26 @@ public async Task> ListEnginesAsync(string state = null) public async Task DeleteEngineAsync(string engine) { - var data = new Dictionary - { - { "name", engine } - }; - var resp = await _rest.DeleteAsync(MakeUrl(PathEngine), data) as string; - return Json.Deserialize(resp); + _logger.LogInformation($"DeleteEngine {engine}"); + return await DeleteEngineAsyncInternal(engine); } public async Task DeleteEngineWaitAsync(string engine) { + _logger.LogInformation($"DeleteEngineWait {engine}"); var startTime = DateTimeOffset.Now.ToUnixTimeMilliseconds(); - var resp = await DeleteEngineAsync(engine); + var resp = await DeleteEngineAsyncInternal(engine); var engineResponse = await Policy .HandleResult(e => !EngineStates.IsFinalState(e.State)) .RetryWithTimeout(startTime, 0.1, 120, 10 * 60) - .ExecuteAsync(() => GetEngineAsync(engine)); + .ExecuteAsync(() => GetEngineAsyncInternal(engine)); resp.Status.State = engineResponse.State; return resp; } public async Task CreateOAuthClientAsync(string name, List permissions = null) { + _logger.LogInformation($"CreateOAuthClient {name}" + (permissions == null ? string.Empty : $", permissions {JsonConvert.SerializeObject(permissions)}")); var uniquePermissions = new HashSet(); permissions?.ForEach(p => uniquePermissions.Add(p.Value())); var data = new Dictionary @@ -230,7 +202,8 @@ public async Task CreateOAuthClientAsync(string name, List FindOAuthClientAsync(string name) { - var clients = await ListOAuthClientsAsync(); + _logger.LogInformation($"FindOAuthClient {name}"); + var clients = await ListOAuthClientsAsyncInternal(); return clients.FirstOrDefault(client => client.Name == name) ?? throw new HttpError(404, $"OAuth Client with name `{name}` not found"); @@ -245,19 +218,20 @@ public async Task GetOAuthClientAsync(string id) public async Task> ListOAuthClientsAsync() { - var resp = await ListCollectionsAsync(PathOAuthClients); - return Json.Deserialize(resp).Clients; + _logger.LogInformation($"ListOAuthClients"); + return await ListOAuthClientsAsyncInternal(); } public async Task DeleteOAuthClientAsync(string id) { + _logger.LogInformation($"DeleteOAuthClient id: {id}"); var resp = await _rest.DeleteAsync(MakeUrl($"{PathOAuthClients}/{id}")) as string; return Json.Deserialize(resp); } public async Task CreateUserAsync(string email, List roles = null) { - _logger.LogInformation($"Create user email: {email}"); + _logger.LogInformation($"CreateUser: email {email}" + (roles == null ? string.Empty : $", roles {JsonConvert.SerializeObject(roles)}")); var uniqueRoles = new HashSet(); roles?.ForEach(r => uniqueRoles.Add(r.Value())); var data = new Dictionary @@ -271,26 +245,14 @@ public async Task CreateUserAsync(string email, List roles = null) public async Task UpdateUserAsync(string id, UserStatus status = UserStatus.None, List roles = null) { - var data = new Dictionary(); - if (roles != null) - { - var uniqueRoles = new HashSet(); - roles.ForEach(r => uniqueRoles.Add(r.Value())); - data.Add("roles", uniqueRoles); - } - - if (status != UserStatus.None) - { - data.Add("status", status.Value()); - } - - var resp = await _rest.PatchAsync(MakeUrl($"{PathUsers}/{id}"), data) as string; - return Json.Deserialize(resp).User; + _logger.LogInformation($"UpdateUserAsync: id {id}, user status {status}" + (roles == null ? string.Empty : $", roles {JsonConvert.SerializeObject(roles)}")); + return await UpdateUserAsyncInternal(id, status, roles); } public async Task FindUserAsync(string email) { - var users = await ListUsersAsync(); + _logger.LogInformation($"FindUser email {email}"); + var users = await ListUsersAsyncInternal(); return users.FirstOrDefault(user => user.Email == email) ?? throw new HttpError(404, $"User with email `{email}` not found"); @@ -298,74 +260,77 @@ public async Task FindUserAsync(string email) public async Task GetUserAsync(string userId) { + _logger.LogInformation($"GetUser id {userId}"); var resp = await GetResourceAsync($"{PathUsers}/{userId}"); return Json.Deserialize(resp).User; } public async Task> ListUsersAsync() { - var resp = await ListCollectionsAsync(PathUsers); - return Json.Deserialize(resp).Users; + _logger.LogInformation($"ListUsers"); + return await ListUsersAsyncInternal(); } public async Task DeleteUserAsync(string id) { + _logger.LogInformation($"DeleteUser id {id}"); var resp = await _rest.DeleteAsync(MakeUrl($"{PathUsers}/{id}")) as string; return Json.Deserialize(resp); } public Task DisableUserAsync(string id) { - return UpdateUserAsync(id, UserStatus.InActive); + _logger.LogInformation($"DisableUser {id}"); + return UpdateUserAsyncInternal(id, UserStatus.InActive); } public Task EnableUserAsync(string id) { - return UpdateUserAsync(id, UserStatus.Active); + _logger.LogInformation($"EnableUser id {id}"); + return UpdateUserAsyncInternal(id, UserStatus.Active); } public async Task GetTransactionsAsync() { + _logger.LogInformation($"GetTransactions"); var rsp = await _rest.GetAsync(MakeUrl(PathTransactions)) as string; return Json.Deserialize(rsp); } public async Task GetTransactionAsync(string id) { - var rsp = await _rest.GetAsync(MakeUrl($"{PathTransactions}/{id}")) as string; - return Json.Deserialize(rsp); + _logger.LogInformation($"GetTransaction id {id}"); + return await GetTransactionAsyncInternal(id); } public async Task> GetTransactionResultsAsync(string id) { - var files = await _rest.GetAsync(MakeUrl($"{PathTransactions}/{id}/results")) as List; - return _rest.ReadArrowFiles(files); + _logger.LogInformation($"GetTransactionResults id {id}"); + return await GetTransactionResultsAsyncInternal(id); } public async Task GetTransactionMetadataAsync(string id) { - var headers = new Dictionary - { - { "accept", "application/x-protobuf" }, - }; - - return await _rest.GetAsync(MakeUrl($"{PathTransactions}/{id}/metadata"), headers: headers) as MetadataInfo; + _logger.LogInformation($"GetTransactionMetadata id {id}"); + return await GetTransactionMetadataAsyncInternal(id); } public async Task> GetTransactionProblemsAsync(string id) { - var rsp = await _rest.GetAsync(MakeUrl($"{PathTransactions}/{id}/problems")) as string; - return ParseProblemsResult(rsp); + _logger.LogInformation($"GetTransactionProblems id {id}"); + return await GetTransactionProblemsAsyncInternal(id); } public async Task CancelTransactionAsync(string id) { + _logger.LogInformation($"CancelTransaction id {id}"); var rsp = await _rest.PostAsync(MakeUrl($"{PathTransactions}/{id}/cancel"), new Dictionary()) as string; return Json.Deserialize(rsp); } public async Task DeleteTransactionAsync(string id) { + _logger.LogInformation($"DeleteTransaction id {id}"); var resp = await _rest.DeleteAsync(MakeUrl($"{PathTransactions}/{id}")) as string; return FormatResponse(resp); } @@ -385,6 +350,7 @@ public async Task LoadModelsAsync( string engine, Dictionary models) { + _logger.LogInformation($"LoadModels: database {database}, engine {engine}, models count {models.Count}"); var queries = new List(); var queriesInputs = new Dictionary(); var randInt = new Random().Next(int.MaxValue); @@ -400,7 +366,7 @@ public async Task LoadModelsAsync( index++; } - return await ExecuteAsync(database, engine, string.Join('\n', queries), false, queriesInputs); + return await ExecuteAsyncInternal(database, engine, string.Join('\n', queries), false, queriesInputs); } public async Task LoadModelsWaitAsync( @@ -408,6 +374,7 @@ public async Task LoadModelsWaitAsync( string engine, Dictionary models) { + _logger.LogInformation($"LoadModelsWait: database {database}, engine {engine}, models count {models.Count}"); var queries = new List(); var queriesInputs = new Dictionary(); var randInt = new Random().Next(int.MaxValue); @@ -423,16 +390,17 @@ public async Task LoadModelsWaitAsync( index++; } - return await ExecuteWaitAsync(database, engine, string.Join('\n', queries), false, queriesInputs); + return await ExecuteWaitAsyncInternal(database, engine, string.Join('\n', queries), false, queriesInputs); } public async Task> ListModelsAsync(string database, string engine) { + _logger.LogInformation($"ListModels: database {database}, engine {engine}"); var outName = $"models_{new Random().Next(int.MaxValue)}"; var query = $"def output:{outName}[name] = rel:catalog:model(name, _)"; var models = new List(); - var resp = await ExecuteWaitAsync(database, engine, query); + var resp = await ExecuteWaitAsyncInternal(database, engine, query); var result = resp.Results.Find(r => r.RelationId.Equals($"/:output/:{outName}/String")); if (result != null) @@ -448,10 +416,11 @@ public async Task> ListModelsAsync(string database, string engine) public async Task GetModelAsync(string database, string engine, string name) { + _logger.LogInformation($"GetModel: database {database}, engine {engine}, name {name}"); var outName = $"model_{new Random().Next(int.MaxValue)}"; var query = $"def output:{outName} = rel:catalog:model[\"{name}\"]"; - var resp = await ExecuteWaitAsync(database, engine, query); + var resp = await ExecuteWaitAsyncInternal(database, engine, query); var model = new Model(name, null); var result = resp.Results.Find(r => r.RelationId.Equals($"/:output/:{outName}/String")); @@ -466,13 +435,14 @@ public async Task GetModelAsync(string database, string engine, string na public async Task DeleteModelsAsync(string database, string engine, List models) { + _logger.LogInformation($"DeleteModels: database {database}, engine {engine}, models {JsonConvert.SerializeObject(models)}"); var queries = new List(); foreach (var model in models) { queries.Add($"def delete:rel:catalog:model[\"{model}\"] = rel:catalog:model[\"{model}\"]"); } - return await ExecuteWaitAsync(database, engine, string.Join('\n', queries), false); + return await ExecuteWaitAsyncInternal(database, engine, string.Join('\n', queries), false); } // Query @@ -483,11 +453,8 @@ public async Task ExecuteV1Async( bool readOnly = false, Dictionary inputs = null) { - var tx = new Transaction(_context.Region, database, engine, TransactionMode.Open, readOnly); - var actions = new List { DbAction.MakeQueryAction(source, inputs) }; - var body = tx.Payload(actions); - var resp = await _rest.PostAsync(MakeUrl(PathTransaction), body, null, tx.QueryParams()) as string; - return Json.Deserialize(resp); + _logger.LogInformation($"ExecuteV1Async: engine {engine}, database {database}, readonly {readOnly}"); + return await ExecuteV1AsyncInternal(database, engine, source, readOnly, inputs); } public async Task ExecuteWaitAsync( @@ -497,40 +464,10 @@ public async Task ExecuteWaitAsync( bool readOnly = false, Dictionary inputs = null) { - var startTime = DateTimeOffset.Now.ToUnixTimeMilliseconds(); - var rsp = await ExecuteAsync(database, engine, source, readOnly, inputs); - var id = rsp.Transaction.Id; - - // fast-path - if (rsp.GotCompleteResult) - { - return rsp; - } - - // slow-path - var transactionResponse = await Policy - .HandleResult(r => !r.Transaction.State.IsFinalState()) - .RetryForeverWithBoundedDelay(startTime, 0.2) // wait for 20% of the total runtime - .ExecuteAsync(() => GetTransactionAsync(id)); - - var transaction = transactionResponse.Transaction; - List results = null; - MetadataInfo metadata = null; - List problems = null; - - if (transaction.State == TransactionAsyncState.Completed || TransactionAsyncAbortReason.IntegrityConstraintViolation.Equals(transaction.AbortReason)) - { - results = await GetTransactionResultsAsync(id); - metadata = await GetTransactionMetadataAsync(id); - problems = await GetTransactionProblemsAsync(id); - } - - return new TransactionAsyncResult( - transaction, - results, - metadata, - problems, - true); + _logger.LogInformation($"ExecuteWait: database {database}, engine {engine}, readonly {readOnly}"); + var rsp = await ExecuteWaitAsyncInternal(database, engine, source, readOnly, inputs); + _logger.LogInformation($"TransactionID: {rsp.Transaction.Id}, state: {rsp.Transaction.State}"); + return rsp; } public async Task ExecuteAsync( @@ -540,18 +477,10 @@ public async Task ExecuteAsync( bool readOnly = false, Dictionary inputs = null) { - var tx = new TransactionAsync(database, engine, readOnly, source, inputs); - var body = tx.Payload(); - var rsp = await _rest.PostAsync(MakeUrl(PathTransactions), body, null, tx.QueryParams()); - - if (rsp is string s) - { - var txn = Json.Deserialize(s); - _logger.LogInformation($"Transaction id: {txn.Id}, state: {txn.State}"); - return new TransactionAsyncResult(txn, new List(), null, new List()); - } - - return ReadTransactionAsyncResults(rsp as List); + _logger.LogInformation($"Execute: database {database}, engine {engine}, readonly {readOnly}"); + var rsp = await ExecuteAsyncInternal(database, engine, source, readOnly); + _logger.LogInformation($"TransactionID: {rsp.Transaction.Id}, state: {rsp.Transaction.State}"); + return rsp; } public Task LoadJsonAsync( @@ -560,12 +489,13 @@ public Task LoadJsonAsync( string relation, string data) { + _logger.LogInformation($"LoadJson: database {database}, engine {engine}, relation {relation}"); var inputs = new Dictionary { { "data", data } }; var source = GenLoadJson(relation); - return ExecuteV1Async(database, engine, source, false, inputs); + return ExecuteV1AsyncInternal(database, engine, source, false, inputs); } public Task LoadCsvAsync( @@ -575,20 +505,13 @@ public Task LoadCsvAsync( string data, CsvOptions options = null) { + _logger.LogInformation($"LoadCsv: database {engine}, engine {engine}, relation {relation}"); var source = GenLoadCsv(relation, options); var inputs = new Dictionary { { "data", data } }; - return ExecuteV1Async(database, engine, source, false, inputs); - } - - private async Task CreateDatabaseV1Async(string database, string engine, bool overwrite = false) - { - var mode = CreateMode(null, overwrite); - var transaction = new Transaction(_context.Region, database, engine, mode); - await _rest.PostAsync(MakeUrl(PathTransaction), transaction.Payload(null), null, transaction.QueryParams()); - return await GetDatabaseAsync(database); + return ExecuteV1AsyncInternal(database, engine, source, false, inputs); } private static TransactionMode CreateMode(string source, bool overwrite) @@ -748,6 +671,213 @@ private static List ParseProblemsResult(string rsp) return output; } + private async Task GetEngineAsyncInternal(string engine) + { + var parameters = new Dictionary + { + { "name", engine }, + { "deleted_on", string.Empty } + }; + var resp = await GetResourceAsync(PathEngine, null, parameters); + var engines = Json.Deserialize(resp).Engines; + return engines.Count > 0 ? engines[0] : throw new HttpError(404, $"Engine with name `{engine}` not found"); + } + + private async Task CreateEngineWaitAsyncInternal(string engine, string size = "XS") + { + var startTime = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + await CreateEngineAsyncInternal(engine, size); + var resp = await Policy + .HandleResult(e => !EngineStates.IsTerminalState(e.State, EngineStates.Provisioned)) + .RetryWithTimeout(startTime, 0.1, 120, 10 * 60) + .ExecuteAsync(() => GetEngineAsyncInternal(engine)); + + if (resp.State != EngineStates.Provisioned) + { + throw new EngineProvisionFailedException(resp); + } + + return resp; + } + + private async Task CreateEngineAsyncInternal(string engine, string size = "XS") + { + var data = new Dictionary + { + { "region", _context.Region }, + { "name", engine }, + { "size", size.ToString() } + }; + var resp = await _rest.PutAsync(MakeUrl(PathEngine), data) as string; + return Json.Deserialize(resp).Engine; + } + + private async Task> ListOAuthClientsAsyncInternal() + { + var resp = await ListCollectionsAsync(PathOAuthClients); + return Json.Deserialize(resp).Clients; + } + + private async Task DeleteEngineAsyncInternal(string engine) + { + var data = new Dictionary + { + { "name", engine } + }; + var resp = await _rest.DeleteAsync(MakeUrl(PathEngine), data) as string; + return Json.Deserialize(resp); + } + + private async Task> ListUsersAsyncInternal() + { + var resp = await ListCollectionsAsync(PathUsers); + return Json.Deserialize(resp).Users; + } + + private async Task GetTransactionAsyncInternal(string id) + { + var rsp = await _rest.GetAsync(MakeUrl($"{PathTransactions}/{id}")) as string; + return Json.Deserialize(rsp); + } + + private async Task> GetTransactionResultsAsyncInternal(string id) + { + var files = await _rest.GetAsync(MakeUrl($"{PathTransactions}/{id}/results")) as List; + return _rest.ReadArrowFiles(files); + } + + private async Task GetTransactionMetadataAsyncInternal(string id) + { + var headers = new Dictionary + { + { "accept", "application/x-protobuf" }, + }; + + return await _rest.GetAsync(MakeUrl($"{PathTransactions}/{id}/metadata"), headers: headers) as MetadataInfo; + } + + private async Task> GetTransactionProblemsAsyncInternal(string id) + { + var rsp = await _rest.GetAsync(MakeUrl($"{PathTransactions}/{id}/problems")) as string; + return ParseProblemsResult(rsp); + } + + private async Task ExecuteWaitAsyncInternal( + string database, + string engine, + string source, + bool readOnly = false, + Dictionary inputs = null) + { + var startTime = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + var rsp = await ExecuteAsyncInternal(database, engine, source, readOnly, inputs); + var id = rsp.Transaction.Id; + + // fast-path + if (rsp.GotCompleteResult) + { + return rsp; + } + + // slow-path + var transactionResponse = await Policy + .HandleResult(r => !r.Transaction.State.IsFinalState()) + .RetryForeverWithBoundedDelay(startTime, 0.2) // wait for 20% of the total runtime + .ExecuteAsync(() => GetTransactionAsyncInternal(id)); + + var transaction = transactionResponse.Transaction; + List results = null; + MetadataInfo metadata = null; + List problems = null; + + if (transaction.State == TransactionAsyncState.Completed || TransactionAsyncAbortReason.IntegrityConstraintViolation.Equals(transaction.AbortReason)) + { + results = await GetTransactionResultsAsyncInternal(id); + metadata = await GetTransactionMetadataAsyncInternal(id); + problems = await GetTransactionProblemsAsyncInternal(id); + } + + return new TransactionAsyncResult( + transaction, + results, + metadata, + problems, + true); + } + + private async Task ExecuteV1AsyncInternal( + string database, + string engine, + string source, + bool readOnly = false, + Dictionary inputs = null) + { + var tx = new Transaction(_context.Region, database, engine, TransactionMode.Open, readOnly); + var actions = new List { DbAction.MakeQueryAction(source, inputs) }; + var body = tx.Payload(actions); + var resp = await _rest.PostAsync(MakeUrl(PathTransaction), body, null, tx.QueryParams()) as string; + return Json.Deserialize(resp); + } + + private async Task ExecuteAsyncInternal( + string database, + string engine, + string source, + bool readOnly = false, + Dictionary inputs = null) + { + var tx = new TransactionAsync(database, engine, readOnly, source, inputs); + var body = tx.Payload(); + var rsp = await _rest.PostAsync(MakeUrl(PathTransactions), body, null, tx.QueryParams()); + + if (rsp is string s) + { + var txn = Json.Deserialize(s); + return new TransactionAsyncResult(txn, new List(), null, new List()); + } + + return ReadTransactionAsyncResults(rsp as List); + } + + private async Task CreateDatabaseV1Async(string database, string engine, bool overwrite = false) + { + var mode = CreateMode(null, overwrite); + var transaction = new Transaction(_context.Region, database, engine, mode); + await _rest.PostAsync(MakeUrl(PathTransaction), transaction.Payload(null), null, transaction.QueryParams()); + return await GetDatabaseAsyncInternal(database); + } + + private async Task GetDatabaseAsyncInternal(string database) + { + var parameters = new Dictionary + { + { "name", database } + }; + + var resp = await GetResourceAsync(PathDatabase, null, parameters); + var dbs = Json.Deserialize(resp).Databases; + return dbs.Count > 0 ? dbs[0] : throw new HttpError(404, $"Database with name `{database}` not found"); + } + + private async Task UpdateUserAsyncInternal(string id, UserStatus status = UserStatus.None, List roles = null) + { + var data = new Dictionary(); + if (roles != null) + { + var uniqueRoles = new HashSet(); + roles.ForEach(r => uniqueRoles.Add(r.Value())); + data.Add("roles", uniqueRoles); + } + + if (status != UserStatus.None) + { + data.Add("status", status.Value()); + } + + var resp = await _rest.PatchAsync(MakeUrl($"{PathUsers}/{id}"), data) as string; + return Json.Deserialize(resp).User; + } + private TransactionAsyncResult ReadTransactionAsyncResults(List files) { var transaction = files.Find(f => f.Name == "transaction"); @@ -765,7 +895,6 @@ private TransactionAsyncResult ReadTransactionAsyncResults(List.Deserialize(_rest.ReadString(transaction.Data)); - _logger.LogInformation($"Transaction id: {transactionResult.Id}, state: {transactionResult.State}"); var metadataProto = _rest.ReadMetadataProtobuf(metadata.Data); List problemsResult = null; diff --git a/RelationalAI/Rest.cs b/RelationalAI/Rest.cs index 8b75d59..ad96409 100644 --- a/RelationalAI/Rest.cs +++ b/RelationalAI/Rest.cs @@ -327,21 +327,21 @@ private async Task RequestHelperAsync( // Get the result back or throws an exception. var response = await HttpClient.SendAsync(request); + + string contentType = response.Content.Headers.ContentType.MediaType; + var userAgent = response.RequestMessage.Headers.TryGetValues("User-Agent", out var values) ? values.FirstOrDefault() : string.Empty; + var requestId = response.Headers.TryGetValues("X-Request-ID", out values) ? values.FirstOrDefault() : string.Empty; + + _logger.LogDebug($"{response.RequestMessage.Method} HTTP/{response.Version} {contentType}" + + $" {response.RequestMessage.RequestUri} {(int)response.StatusCode} {userAgent} {requestId}"); + var content = await response.Content.ReadAsByteArrayAsync(); if ((int)response.StatusCode >= 400) { - string requestId = string.Empty; - if (response.Headers.TryGetValues("X-Request-ID", out IEnumerable values)) - { - requestId = values.First(); - } - throw new HttpError((int)response.StatusCode, $"(request id: {requestId})\n{ReadString(content)}"); } - var contentType = response.Content.Headers.ContentType.MediaType; - return contentType.ToLower() switch { "application/json" => ReadString(content),