diff options
author | crupest <crupest@outlook.com> | 2022-12-23 11:48:11 +0800 |
---|---|---|
committer | crupest <crupest@outlook.com> | 2022-12-23 11:48:11 +0800 |
commit | d1e2a504b0543f00a9c9d9602ba7f120bb0e8895 (patch) | |
tree | eff2982e0e8ea24f7952bba4678a18d7dcb250d5 /docker/crupest-api/CrupestApi | |
parent | 93924947fecca4ddad670f1f1768f32063cfec3a (diff) | |
download | crupest-d1e2a504b0543f00a9c9d9602ba7f120bb0e8895.tar.gz crupest-d1e2a504b0543f00a9c9d9602ba7f120bb0e8895.tar.bz2 crupest-d1e2a504b0543f00a9c9d9602ba7f120bb0e8895.zip |
Develop secret api. v60
Diffstat (limited to 'docker/crupest-api/CrupestApi')
7 files changed, 206 insertions, 78 deletions
diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/CrudIntegratedTest.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/CrudIntegratedTest.cs index ddda5f6..179048d 100644 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/CrudIntegratedTest.cs +++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/CrudIntegratedTest.cs @@ -15,7 +15,8 @@ public class CrudIntegratedTest : IAsyncLifetime public CrudIntegratedTest() { var builder = WebApplication.CreateBuilder(); - builder.Services.AddSingleton<SqliteMemoryConnectionFactory>(); + builder.Logging.ClearProviders(); + builder.Services.AddSingleton<IDbConnectionFactory, SqliteMemoryConnectionFactory>(); builder.Services.AddCrud<TestEntity>(); builder.WebHost.UseTestServer(); _app = builder.Build(); @@ -48,12 +49,94 @@ public class CrudIntegratedTest : IAsyncLifetime [Fact] - public async Task Test() + public async Task EmptyTest() { - var response = await _authorizedHttpClient.GetAsync($"/test?secret={_token}"); + using var response = await _authorizedHttpClient.GetAsync("/test"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadFromJsonAsync<List<TestEntity>>(); Assert.NotNull(body); Assert.Empty(body); } + + [Fact] + public async Task CrudTest() + { + { + using var response = await _authorizedHttpClient.PostAsJsonAsync("/test", new TestEntity + { + Name = "test", + Age = 22 + }); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadFromJsonAsync<TestEntity>(); + Assert.NotNull(body); + Assert.Equal("test", body.Name); + Assert.Equal(22, body.Age); + Assert.Null(body.Height); + Assert.NotEmpty(body.Secret); + } + + { + using var response = await _authorizedHttpClient.GetAsync("/test"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadFromJsonAsync<List<TestEntity>>(); + Assert.NotNull(body); + var entity = Assert.Single(body); + Assert.Equal("test", entity.Name); + Assert.Equal(22, entity.Age); + Assert.Null(entity.Height); + Assert.NotEmpty(entity.Secret); + } + + { + using var response = await _authorizedHttpClient.GetAsync("/test/test"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadFromJsonAsync<TestEntity>(); + Assert.NotNull(body); + Assert.Equal("test", body.Name); + Assert.Equal(22, body.Age); + Assert.Null(body.Height); + Assert.NotEmpty(body.Secret); + } + + { + using var response = await _authorizedHttpClient.PatchAsJsonAsync("/test/test", new TestEntity + { + Name = "test-2", + Age = 23, + Height = 188.0f + }); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadFromJsonAsync<TestEntity>(); + Assert.NotNull(body); + Assert.Equal("test-2", body.Name); + Assert.Equal(23, body.Age); + Assert.Equal(188.0f, body.Height); + Assert.NotEmpty(body.Secret); + } + + { + using var response = await _authorizedHttpClient.GetAsync("/test/test-2"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadFromJsonAsync<TestEntity>(); + Assert.NotNull(body); + Assert.Equal("test-2", body.Name); + Assert.Equal(23, body.Age); + Assert.Equal(188.0f, body.Height); + Assert.NotEmpty(body.Secret); + } + + { + using var response = await _authorizedHttpClient.DeleteAsync("/test/test-2"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + { + using var response = await _authorizedHttpClient.GetAsync("/test"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadFromJsonAsync<List<TestEntity>>(); + Assert.NotNull(body); + Assert.Empty(body); + } + } } diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudService.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudService.cs index dc32387..a56790a 100644 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudService.cs +++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudService.cs @@ -72,7 +72,7 @@ public class CrudService<TEntity> : IDisposable where TEntity : class var result = _table.Select<TEntity>(_dbConnection, null, WhereClause.Create().Eq(_table.KeyColumn.ColumnName, key)).SingleOrDefault(); if (result is null) { - throw new EntityNotExistException("Required entity not found."); + throw new EntityNotExistException($"Required entity for key {key} not found."); } return result; } @@ -91,7 +91,7 @@ public class CrudService<TEntity> : IDisposable where TEntity : class public object Create(TEntity entity) { var insertClause = ConvertEntityToInsertClauses(entity); - var key = _table.Insert(_dbConnection, insertClause); + _table.Insert(_dbConnection, insertClause, out var key); return key; } @@ -108,14 +108,16 @@ public class CrudService<TEntity> : IDisposable where TEntity : class return result; } - public void UpdateByKey(object key, TEntity entity, UpdateBehavior behavior = UpdateBehavior.None) + // Return new key. + public object UpdateByKey(object key, TEntity entity, UpdateBehavior behavior = UpdateBehavior.None) { var affectedCount = _table.Update(_dbConnection, WhereClause.Create().Eq(_table.KeyColumn.ColumnName, key), - ConvertEntityToUpdateClauses(entity, behavior)); + ConvertEntityToUpdateClauses(entity, behavior), out var newKey); if (affectedCount == 0) { - throw new EntityNotExistException("Required entity not found."); + throw new EntityNotExistException($"Required entity for key {key} not found."); } + return newKey ?? key; } public bool DeleteByKey(object key) diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudWebApplicationExtensions.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudWebApplicationExtensions.cs index dfb67d1..c9e43f2 100644 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudWebApplicationExtensions.cs +++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudWebApplicationExtensions.cs @@ -53,8 +53,8 @@ public static class CrudWebApplicationExtensions var jsonDocument = await context.Request.ReadJsonAsync(); var entity = entityJsonHelper.ConvertJsonToEntityForUpdate(jsonDocument.RootElement, out var updateBehavior); - crudService.UpdateByKey(key, entity, updateBehavior); - await context.ResponseJsonAsync(entityJsonHelper.ConvertEntityToDictionary(crudService.GetByKey(key))); + var newKey = crudService.UpdateByKey(key, entity, updateBehavior); + await context.ResponseJsonAsync(entityJsonHelper.ConvertEntityToDictionary(crudService.GetByKey(newKey))); }); app.MapDelete(path + "/{key}", async (context) => diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/EntityJsonHelper.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/EntityJsonHelper.cs index 406e214..cf3f178 100644 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/EntityJsonHelper.cs +++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/EntityJsonHelper.cs @@ -1,3 +1,4 @@ +using System.Globalization; using System.Text.Json; using Microsoft.Extensions.Options; @@ -46,34 +47,63 @@ public class EntityJsonHelper<TEntity> where TEntity : class return JsonSerializer.Serialize(dictionary, _jsonSerializerOptions.CurrentValue); } - private static Type MapJsonValueKindToType(JsonElement jsonElement, out object? value) + private object? ConvertJsonValue(JsonElement? optionalJsonElement, Type type, string propertyName) { - switch (jsonElement.ValueKind) + if (optionalJsonElement is null) { - case JsonValueKind.String: - { - value = jsonElement.GetString()!; - return typeof(string); - } - case JsonValueKind.Number: - { - value = jsonElement.GetDouble(); - return typeof(double); - } - case JsonValueKind.True: - case JsonValueKind.False: - { - value = jsonElement.GetBoolean(); - return typeof(bool); - } - case JsonValueKind.Null: - { - value = null; - return typeof(object); - } - default: - throw new UserException("Unsupported json value type."); + return null; + } + + var jsonElement = optionalJsonElement.Value; + + if (jsonElement.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined) + { + return null; + } + + if (jsonElement.ValueKind is JsonValueKind.String) + { + if (type != typeof(string)) + { + throw new UserException($"Property {propertyName} must be a string."); + } + return jsonElement.GetString()!; + } + + if (jsonElement.ValueKind is JsonValueKind.True or JsonValueKind.False) + { + if (type != typeof(bool)) + { + throw new UserException($"Property {propertyName} must be a boolean."); + } + return jsonElement.GetBoolean(); + } + + if (jsonElement.ValueKind is JsonValueKind.Number) + { + try + { + return Convert.ChangeType(jsonElement.GetRawText(), type, CultureInfo.InvariantCulture); + } + catch (Exception) + { + throw new UserException($"Property {propertyName} must be a valid number."); + } + } + + throw new UserException($"Property {propertyName} is of wrong type."); + } + + public Dictionary<string, JsonElement> ConvertJsonObjectToDictionary(JsonElement jsonElement) + { + var result = new Dictionary<string, JsonElement>(); + + foreach (var property in jsonElement.EnumerateObject()) + { + result[property.Name.ToLower()] = property.Value; } + + return result; } public TEntity ConvertJsonToEntityForInsert(JsonElement jsonElement) @@ -82,31 +112,23 @@ public class EntityJsonHelper<TEntity> where TEntity : class throw new ArgumentException("The jsonElement must be an object."); var result = Activator.CreateInstance<TEntity>(); + + Dictionary<string, JsonElement> jsonProperties = ConvertJsonObjectToDictionary(jsonElement); + foreach (var column in _table.PropertyColumns) { - if (jsonElement.TryGetProperty(column.ColumnName, out var jsonValue)) + var jsonPropertyValue = jsonProperties.GetValueOrDefault(column.ColumnName.ToLower()); + var value = ConvertJsonValue(jsonPropertyValue, column.ColumnType.DatabaseClrType, column.ColumnName); + if (column.IsOnlyGenerated && value is not null) { - if (column.IsOnlyGenerated) - { - throw new UserException($"Property {column.ColumnName} is auto generated, you cannot set it."); - } - - var valueType = MapJsonValueKindToType(jsonValue, out var value); - if (!valueType.IsAssignableTo(column.ColumnType.DatabaseClrType)) - { - throw new UserException($"Property {column.ColumnName} is of wrong type."); - } - - var realValue = column.ColumnType.ConvertFromDatabase(value); - column.PropertyInfo!.SetValue(result, realValue); + throw new UserException($"Property {column.ColumnName} is auto generated, you cannot set it."); } - else + if (!column.CanBeGenerated && value is null && column.IsNotNull) { - if (!column.CanBeGenerated) - { - throw new UserException($"Property {column.ColumnName} is not auto generated, you must set it."); - } + throw new UserException($"Property {column.ColumnName} can NOT be generated, you must set it."); } + var realValue = column.ColumnType.ConvertFromDatabase(value); + column.PropertyInfo!.SetValue(result, realValue); } return result; @@ -125,11 +147,15 @@ public class EntityJsonHelper<TEntity> where TEntity : class updateBehavior = UpdateBehavior.None; - if (jsonElement.TryGetProperty("$saveNull", out var saveNullValue)) + Dictionary<string, JsonElement> jsonProperties = ConvertJsonObjectToDictionary(jsonElement); + + bool saveNull = false; + if (jsonProperties.TryGetValue("$saveNull".ToLower(), out var saveNullValue)) { if (saveNullValue.ValueKind is JsonValueKind.True) { updateBehavior |= UpdateBehavior.SaveNull; + saveNull = true; } else if (saveNullValue.ValueKind is JsonValueKind.False) { @@ -144,26 +170,28 @@ public class EntityJsonHelper<TEntity> where TEntity : class var result = Activator.CreateInstance<TEntity>(); foreach (var column in _table.PropertyColumns) { - if (jsonElement.TryGetProperty(column.ColumnName, out var jsonValue)) + if (jsonProperties.TryGetValue(column.ColumnName.ToLower(), out var jsonPropertyValue)) { - if (column.IsOnlyGenerated) + if (jsonPropertyValue.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined) { - throw new UserException($"Property {column.ColumnName} is auto generated, you cannot set it."); - } + if ((column.IsOnlyGenerated || column.IsNoUpdate) && saveNull) + { + throw new UserException($"Property {column.ColumnName} is auto generated or not updatable, you cannot set it."); + } - if (column.IsNoUpdate) - { - throw new UserException($"Property {column.ColumnName} is not updatable, you cannot set it."); + column.PropertyInfo!.SetValue(result, null); } - - var valueType = MapJsonValueKindToType(jsonValue, out var value); - if (!valueType.IsAssignableTo(column.ColumnType.DatabaseClrType)) + else { - throw new UserException($"Property {column.ColumnName} is of wrong type."); + if (column.IsOnlyGenerated || column.IsNoUpdate) + { + throw new UserException($"Property {column.ColumnName} is auto generated or not updatable, you cannot set it."); + } + + var value = ConvertJsonValue(jsonPropertyValue, column.ColumnType.DatabaseClrType, column.ColumnName); + var realValue = column.ColumnType.ConvertFromDatabase(value); + column.PropertyInfo!.SetValue(result, realValue); } - - var realValue = column.ColumnType.ConvertFromDatabase(value); - column.PropertyInfo!.SetValue(result, realValue); } } diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/TableInfo.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/TableInfo.cs index bc580f2..41ef097 100644 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/TableInfo.cs +++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/TableInfo.cs @@ -492,9 +492,9 @@ CREATE TABLE {tableName}( /// Insert a entity and call hooks. /// </summary> /// <returns>The key of insert entity.</returns> - public object Insert(IDbConnection dbConnection, IInsertClause insert) + public int Insert(IDbConnection dbConnection, IInsertClause insert, out object key) { - object? key = null; + object? finalKey = null; var realInsert = InsertClause.Create(); @@ -546,23 +546,31 @@ CREATE TABLE {tableName}( if (realInsertItem.ColumnName == KeyColumn.ColumnName) { - key = realInsertItem.Value; + finalKey = realInsertItem.Value; } } + if (finalKey is null) throw new Exception("No key???"); + key = finalKey; + var (sql, parameters) = GenerateInsertSql(realInsert); - dbConnection.Execute(sql, ConvertParameters(parameters)); + var affectedRowCount = dbConnection.Execute(sql, ConvertParameters(parameters)); + + if (affectedRowCount != 1) + throw new Exception("Failed to insert."); - return key ?? throw new Exception("No key???"); + return affectedRowCount; } /// <summary> /// Upgrade a entity and call hooks. /// </summary> /// <returns>The key of insert entity.</returns> - public virtual int Update(IDbConnection dbConnection, IWhereClause? where, IUpdateClause update) + public virtual int Update(IDbConnection dbConnection, IWhereClause? where, IUpdateClause update, out object? newKey) { + newKey = null; + var realUpdate = UpdateClause.Create(); foreach (var column in Columns) @@ -580,6 +588,11 @@ CREATE TABLE {tableName}( column.InvokeValidator(value); realUpdate.Add(column.ColumnName, value); + + if (column.ColumnName == KeyColumn.ColumnName) + { + newKey = value; + } } } diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/HttpContextExtensions.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/HttpContextExtensions.cs index 2ad2c1f..a0b2d89 100644 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/HttpContextExtensions.cs +++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons/HttpContextExtensions.cs @@ -33,8 +33,10 @@ public static class CrupestApiJsonExtensions public static async Task<JsonDocument> ReadJsonAsync(this HttpRequest request) { + var jsonOptions = request.HttpContext.RequestServices.GetRequiredService<IOptionsSnapshot<JsonSerializerOptions>>(); using var stream = request.Body; - return await JsonDocument.ParseAsync(stream); + var body = await JsonSerializer.DeserializeAsync<JsonDocument>(stream, jsonOptions.Value); + return body!; } public static async Task WriteJsonAsync<T>(this HttpResponse response, T bodyObject, int statusCode = 200, HttpResponseAction? beforeWriteBody = null, CancellationToken cancellationToken = default) diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/SecretService.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/SecretService.cs index c2242ff..9a0ec95 100644 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/SecretService.cs +++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/SecretService.cs @@ -19,7 +19,7 @@ public class SecretService : CrudService<SecretInfo>, ISecretService .Add(nameof(SecretInfo.Key), SecretsConstants.SecretManagementKey) .Add(nameof(SecretInfo.Secret), "crupest") .Add(nameof(SecretInfo.Description), "This is the init key. Please revoke it immediately after creating a new one."); - _table.Insert(connection, insertClause); + _table.Insert(connection, insertClause, out var _); transaction.Commit(); } @@ -30,7 +30,7 @@ public class SecretService : CrudService<SecretInfo>, ISecretService .Add(nameof(SecretInfo.Key), key) .Add(nameof(SecretInfo.Secret), secret) .Add(nameof(SecretInfo.Description), "Test secret."); - _table.Insert(connection, insertClause); + _table.Insert(connection, insertClause, out var _); } public List<string> GetPermissions(string secret) |