diff --git a/Persistence.API/Controllers/ChangeLogController.cs b/Persistence.API/Controllers/ChangeLogController.cs new file mode 100644 index 0000000..cad59c4 --- /dev/null +++ b/Persistence.API/Controllers/ChangeLogController.cs @@ -0,0 +1,178 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Persistence.Models; +using Persistence.Models.Requests; +using Persistence.Repositories; +using System.Net; + +namespace Persistence.API.Controllers; + +[ApiController] +[Authorize] +[Route("api/[controller]")] +public class ChangeLogController : ControllerBase, IChangeLogApi +{ + private IChangeLogRepository repository; + + public ChangeLogController(IChangeLogRepository repository) + { + this.repository = repository; + } + + [HttpPost("{idDiscriminator}")] + [ProducesResponseType(typeof(int), (int)HttpStatusCode.Created)] + public async Task Add( + [FromRoute] Guid idDiscriminator, + [FromBody] DataWithWellDepthAndSectionDto dto, + CancellationToken token) + { + var userId = User.GetUserId(); + var result = await repository.AddRange(userId, idDiscriminator, [dto], token); + + return CreatedAtAction(nameof(Add), result); + } + + [HttpPost("range/{idDiscriminator}")] + [ProducesResponseType(typeof(int), (int)HttpStatusCode.Created)] + public async Task AddRange( + [FromRoute] Guid idDiscriminator, + [FromBody] IEnumerable dtos, + CancellationToken token) + { + var userId = User.GetUserId(); + var result = await repository.AddRange(userId, idDiscriminator, dtos, token); + + return CreatedAtAction(nameof(AddRange), result); + } + + [HttpDelete] + [ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)] + public async Task Delete(Guid id, CancellationToken token) + { + var userId = User.GetUserId(); + var result = await repository.MarkAsDeleted(userId, [id], token); + + return Ok(result); + } + + [HttpDelete("range")] + [ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)] + public async Task DeleteRange(IEnumerable ids, CancellationToken token) + { + var userId = User.GetUserId(); + var result = await repository.MarkAsDeleted(userId, ids, token); + + return Ok(result); + } + + [HttpPost("replace/{idDiscriminator}")] + [ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)] + public async Task ClearAndAddRange( + [FromRoute] Guid idDiscriminator, + [FromBody] IEnumerable dtos, + CancellationToken token) + { + var userId = User.GetUserId(); + var result = await repository.ClearAndAddRange(userId, idDiscriminator, dtos, token); + return Ok(result); + } + + [HttpPut] + [ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)] + public async Task Update( + DataWithWellDepthAndSectionDto dto, + CancellationToken token) + { + var userId = User.GetUserId(); + var result = await repository.UpdateRange(userId, [dto], token); + + return Ok(result); + } + + [HttpPut("range")] + [ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)] + public async Task UpdateRange( + IEnumerable dtos, + CancellationToken token) + { + var userId = User.GetUserId(); + var result = await repository.UpdateRange(userId, dtos, token); + + return Ok(result); + } + + [HttpGet("{idDiscriminator}")] + [ProducesResponseType(typeof(PaginationContainer), (int)HttpStatusCode.OK)] + public async Task GetCurrent( + [FromRoute] Guid idDiscriminator, + [FromQuery] SectionPartRequest filterRequest, + [FromQuery] PaginationRequest paginationRequest, + CancellationToken token) + { + var moment = new DateTimeOffset(3000, 1, 1, 0, 0, 0, TimeSpan.Zero); + var result = await repository.GetByDate(idDiscriminator, moment, filterRequest, paginationRequest, token); + + return Ok(result); + } + + [HttpGet("moment/{idDiscriminator}")] + [ProducesResponseType(typeof(PaginationContainer), (int)HttpStatusCode.OK)] + public async Task GetByDate( + [FromRoute] Guid idDiscriminator, + DateTimeOffset moment, + [FromQuery] SectionPartRequest filterRequest, + [FromQuery] PaginationRequest paginationRequest, + CancellationToken token) + { + var result = await repository.GetByDate(idDiscriminator, moment, filterRequest, paginationRequest, token); + + return Ok(result); + } + + [HttpGet("history/{idDiscriminator}")] + [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.NoContent)] + public async Task GetChangeLogForDate( + [FromRoute] Guid idDiscriminator, + DateTimeOffset dateBegin, + DateTimeOffset dateEnd, + CancellationToken token) + { + var result = await repository.GetChangeLogForInterval(idDiscriminator, dateBegin, dateEnd, token); + + return Ok(result); + } + + [HttpGet("datesChange/{idDiscriminator}")] + [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.NoContent)] + public async Task GetDatesChange([FromRoute] Guid idDiscriminator, CancellationToken token) + { + var result = await repository.GetDatesChange(idDiscriminator, token); + + return Ok(result); + } + + [HttpGet("part/{idDiscriminator}")] + [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.NoContent)] + public async Task GetPart([FromRoute] Guid idDiscriminator, DateTimeOffset dateBegin, int take = 86400, CancellationToken token = default) + { + var result = await repository.GetGtDate(idDiscriminator, dateBegin, token); + + return Ok(result); + } + + [HttpGet("datesRange/{idDiscriminator}")] + [ProducesResponseType(typeof(DatesRangeDto), (int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.NoContent)] + public async Task GetDatesRangeAsync([FromRoute] Guid idDiscriminator, CancellationToken token) + { + var result = await repository.GetDatesRange(idDiscriminator, token); + + if (result is null) + return NoContent(); + + return Ok(result); + } +} diff --git a/Persistence.API/Controllers/TechMessagesController.cs b/Persistence.API/Controllers/TechMessagesController.cs index f19f428..a7b0094 100644 --- a/Persistence.API/Controllers/TechMessagesController.cs +++ b/Persistence.API/Controllers/TechMessagesController.cs @@ -1,8 +1,9 @@ -using Microsoft.AspNetCore.Authorization; +using System.Net; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Persistence.Models; +using Persistence.Models.Requests; using Persistence.Repositories; -using System.Net; namespace Persistence.API.Controllers; @@ -14,115 +15,115 @@ namespace Persistence.API.Controllers; [Route("api/[controller]")] public class TechMessagesController : ControllerBase { - private readonly ITechMessagesRepository techMessagesRepository; - private static readonly Dictionary categories = new Dictionary() - { - { 0, "System" }, - { 1, "Авария" }, - { 2, "Предупреждение" }, - { 3, "Инфо" }, - { 4, "Прочее" } - }; + private readonly ITechMessagesRepository techMessagesRepository; + private static readonly Dictionary categories = new Dictionary() + { + { 0, "System" }, + { 1, "Авария" }, + { 2, "Предупреждение" }, + { 3, "Инфо" }, + { 4, "Прочее" } + }; - public TechMessagesController(ITechMessagesRepository techMessagesRepository) - { - this.techMessagesRepository = techMessagesRepository; - } + public TechMessagesController(ITechMessagesRepository techMessagesRepository) + { + this.techMessagesRepository = techMessagesRepository; + } - /// - /// Получить список технологических сообщений в виде страницы - /// - /// - /// - /// - [HttpGet] - public async Task>> GetPage([FromQuery] RequestDto request, CancellationToken token) - { - var result = await techMessagesRepository.GetPage(request, token); + /// + /// Получить список технологических сообщений в виде страницы + /// + /// + /// + /// + [HttpGet] + public async Task>> GetPage([FromQuery] PaginationRequest request, CancellationToken token) + { + var result = await techMessagesRepository.GetPage(request, token); - return Ok(result); - } + return Ok(result); + } - /// - /// Получить статистику по системам - /// - /// - /// - /// - /// - [HttpGet("statistics")] - public async Task>> GetStatistics([FromQuery] IEnumerable autoDrillingSystem, [FromQuery] IEnumerable categoryIds, CancellationToken token) - { - var result = await techMessagesRepository.GetStatistics(autoDrillingSystem, categoryIds, token); + /// + /// Получить статистику по системам + /// + /// + /// + /// + /// + [HttpGet("statistics")] + public async Task>> GetStatistics([FromQuery] IEnumerable autoDrillingSystem, [FromQuery] IEnumerable categoryIds, CancellationToken token) + { + var result = await techMessagesRepository.GetStatistics(autoDrillingSystem, categoryIds, token); - return Ok(result); - } + return Ok(result); + } - /// - /// Получить список всех систем - /// - /// - /// - [HttpGet("systems")] - public async Task>> GetSystems(CancellationToken token) - { - var result = await techMessagesRepository.GetSystems(token); + /// + /// Получить список всех систем + /// + /// + /// + [HttpGet("systems")] + public async Task>> GetSystems(CancellationToken token) + { + var result = await techMessagesRepository.GetSystems(token); - return Ok(result); - } + return Ok(result); + } - /// - /// Получить диапазон дат, для которых есть данные в репозитории - /// - /// - /// - [HttpGet("range")] - public async Task> GetDatesRangeAsync(CancellationToken token) - { - var result = await techMessagesRepository.GetDatesRangeAsync(token); + /// + /// Получить диапазон дат, для которых есть данные в репозитории + /// + /// + /// + [HttpGet("range")] + public async Task> GetDatesRangeAsync(CancellationToken token) + { + var result = await techMessagesRepository.GetDatesRangeAsync(token); - return Ok(result); - } + return Ok(result); + } - /// - /// Получить порцию записей, начиная с заданной даты - /// - /// - /// - /// - /// - [HttpGet("part")] - public async Task>> GetPart(DateTimeOffset dateBegin, int take, CancellationToken token) - { - var result = await techMessagesRepository.GetPart(dateBegin, take, token); + /// + /// Получить порцию записей, начиная с заданной даты + /// + /// + /// + /// + /// + [HttpGet("part")] + public async Task>> GetPart(DateTimeOffset dateBegin, int take, CancellationToken token) + { + var result = await techMessagesRepository.GetPart(dateBegin, take, token); - return Ok(result); - } + return Ok(result); + } - /// - /// Добавить новые технологические сообщения - /// - /// - /// - /// - [HttpPost] - [ProducesResponseType(typeof(int), (int)HttpStatusCode.Created)] - public async Task AddRange([FromBody] IEnumerable dtos, CancellationToken token) - { - var userId = User.GetUserId(); + /// + /// Добавить новые технологические сообщения + /// + /// + /// + /// + [HttpPost] + [ProducesResponseType(typeof(int), (int)HttpStatusCode.Created)] + public async Task AddRange([FromBody] IEnumerable dtos, CancellationToken token) + { + var userId = User.GetUserId(); var result = await techMessagesRepository.AddRange(dtos, userId, token); - return CreatedAtAction(nameof(AddRange), result); - } + return CreatedAtAction(nameof(AddRange), result); + } - /// - /// Получить словарь категорий - /// - /// - [HttpGet("categories")] - public ActionResult> GetImportantCategories() - { - return Ok(categories); - } + /// + /// Получить словарь категорий + /// + /// + [HttpGet("categories")] + public ActionResult> GetImportantCategories() + { + return Ok(categories); + } } \ No newline at end of file diff --git a/Persistence.API/DependencyInjection.cs b/Persistence.API/DependencyInjection.cs index 953565e..bf297a6 100644 --- a/Persistence.API/DependencyInjection.cs +++ b/Persistence.API/DependencyInjection.cs @@ -28,12 +28,12 @@ public static class DependencyInjection c.MapType(() => new OpenApiSchema { Type = "string", Format = "date" }); c.MapType(() => new OpenApiSchema { - AnyOf = - [ - new() {Type = "string", Format = "string" }, - new() {Type = "number", Format = "int32" }, - new() {Type = "number", Format = "float" }, - ] + AnyOf = new OpenApiSchema[] + { + new OpenApiSchema {Type = "string", Format = "string" }, + new OpenApiSchema {Type = "number", Format = "int32" }, + new OpenApiSchema {Type = "number", Format = "float" }, + } }); c.CustomOperationIds(e => @@ -45,8 +45,8 @@ public static class DependencyInjection var needUseKeyCloak = configuration.GetSection("NeedUseKeyCloak").Get(); if (needUseKeyCloak) - c.AddKeycloakSecurity(configuration); - else c.AddDefaultSecurity(); + c.AddKeycloackSecurity(configuration); + else c.AddDefaultSecurity(configuration); var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); @@ -95,7 +95,7 @@ public static class DependencyInjection ValidAudience = JwtParams.Audience, ValidateLifetime = true, IssuerSigningKey = JwtParams.SecurityKey, - ValidateIssuerSigningKey = false + ValidateIssuerSigningKey = false, }; options.Events = new JwtBearerEvents { @@ -135,9 +135,9 @@ public static class DependencyInjection #endregion #region Security (Swagger) - private static void AddKeycloakSecurity(this SwaggerGenOptions options, IConfiguration configuration) + private static void AddKeycloackSecurity(this SwaggerGenOptions options, IConfiguration configuration) { - options.AddSecurityDefinition("Keycloak", new OpenApiSecurityScheme + options.AddSecurityDefinition("Keycloack", new OpenApiSecurityScheme { Description = @"JWT Authorization header using the Bearer scheme. Enter 'Bearer' [space] and then your token in the text input below. Example: 'Bearer 12345abcdef'", Name = "Authorization", @@ -147,8 +147,7 @@ public static class DependencyInjection { Implicit = new OpenApiOAuthFlow { - - AuthorizationUrl = new Uri(configuration["Authentication:AuthorizationUrl"]!), + AuthorizationUrl = new Uri(configuration["Authentication:AuthorizationUrl"]), } } }); @@ -161,7 +160,7 @@ public static class DependencyInjection Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, - Id = "Keycloak" + Id = "Keycloack" }, Scheme = "Bearer", Name = "Bearer", @@ -172,7 +171,7 @@ public static class DependencyInjection }); } - private static void AddDefaultSecurity(this SwaggerGenOptions options) + private static void AddDefaultSecurity(this SwaggerGenOptions options, IConfiguration configuration) { options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { diff --git a/Persistence.API/Startup.cs b/Persistence.API/Startup.cs index 2255664..a8d272e 100644 --- a/Persistence.API/Startup.cs +++ b/Persistence.API/Startup.cs @@ -22,6 +22,7 @@ public class Startup services.AddSwagger(Configuration); services.AddInfrastructure(); services.AddPersistenceDbContext(Configuration); + services.AddAuthorization(); services.AddJWTAuthentication(Configuration); services.AddMemoryCache(); @@ -41,6 +42,9 @@ public class Startup app.UseDeveloperExceptionPage(); } + + app.UseHttpsRedirection(); + app.UseAuthentication(); app.UseAuthorization(); diff --git a/Persistence.API/appsettings.json b/Persistence.API/appsettings.json index 3e1eea7..d4248fb 100644 --- a/Persistence.API/appsettings.json +++ b/Persistence.API/appsettings.json @@ -13,6 +13,14 @@ "MetadataAddress": "http://192.168.0.10:8321/realms/Persistence/.well-known/openid-configuration", "Audience": "account", "ValidIssuer": "http://192.168.0.10:8321/realms/Persistence", - "AuthorizationUrl": "http://192.168.0.10:8321/realms/Persistence/protocol/openid-connect/auth" + "AuthorizationUrl": "http://192.168.0.10:8321/realms/Persistence/protocol/openid-connect/auth" + }, + "NeedUseKeyCloak": false, + "AuthUser": { + "username": "myuser", + "password": 12345, + "clientId": "webapi", + "grantType": "password", + "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "7d9f3574-6574-4ca3-845a-0276eb4aa8f6" } } diff --git a/Persistence.Client/Clients/IChangeLogClient.cs b/Persistence.Client/Clients/IChangeLogClient.cs new file mode 100644 index 0000000..06bbc4d --- /dev/null +++ b/Persistence.Client/Clients/IChangeLogClient.cs @@ -0,0 +1,106 @@ +using Persistence.Models; +using Persistence.Models.Requests; +using Refit; + +namespace Persistence.Client.Clients; + +/// +/// Интерфейс для тестирования API, предназначенного для работы с записями ChangeLod +/// +public interface IChangeLogClient +{ + private const string BaseRoute = "/api/ChangeLog"; + + /// + /// Импорт с заменой: удаление старых строк и добавление новых + /// + /// + /// + /// + [Post($"{BaseRoute}/replace/{{idDiscriminator}}")] + Task> ClearAndAddRange(Guid idDiscriminator, IEnumerable dtos); + + /// + /// Получение актуальных данных на определенную дату (с пагинацией) + /// + /// + /// + /// параметры запроса фильтрации + /// параметры запроса пагинации + /// + [Get($"{BaseRoute}/moment/{{idDiscriminator}}")] + Task>> GetByDate( + Guid idDiscriminator, + DateTimeOffset moment, + [Query] SectionPartRequest filterRequest, + [Query] PaginationRequest paginationRequest); + + /// + /// Получение исторических данных за определенный период времени + /// + /// + /// + /// + /// + [Get($"{BaseRoute}/history/{{idDiscriminator}}")] + Task>> GetChangeLogForInterval(Guid idDiscriminator, DateTimeOffset dateBegin, DateTimeOffset dateEnd); + + /// + /// Добавить одну запись + /// + /// + /// + /// + [Post($"{BaseRoute}/{{idDiscriminator}}")] + Task> Add(Guid idDiscriminator, DataWithWellDepthAndSectionDto dto); + + /// + /// Добавить несколько записей + /// + /// + /// + /// + [Post($"{BaseRoute}/range/{{idDiscriminator}}")] + Task> AddRange(Guid idDiscriminator, IEnumerable dtos); + + /// + /// Обновить одну запись + /// + /// + /// + [Put($"{BaseRoute}")] + Task> Update(DataWithWellDepthAndSectionDto dto); + + /// + /// Обновить несколько записей + /// + /// + /// + [Put($"{BaseRoute}/range")] + Task> UpdateRange(IEnumerable dtos); + + /// + /// Удалить одну запись + /// + /// + /// + [Delete($"{BaseRoute}")] + Task> Delete(Guid id); + + /// + /// Удалить несколько записей + /// + /// + /// + [Delete($"{BaseRoute}/range")] + Task> DeleteRange([Body] IEnumerable ids); + + /// + /// Получение списка дат, в которые происходили изменения (день, месяц, год, без времени) + /// + /// + /// + [Get($"{BaseRoute}/datesRange/{{idDiscriminator}}")] + Task> GetDatesRange(Guid idDiscriminator); + +} diff --git a/Persistence.Client/Clients/ITechMessagesClient.cs b/Persistence.Client/Clients/ITechMessagesClient.cs index f3f37fe..a9c80c1 100644 --- a/Persistence.Client/Clients/ITechMessagesClient.cs +++ b/Persistence.Client/Clients/ITechMessagesClient.cs @@ -1,31 +1,32 @@ using Persistence.Models; +using Persistence.Models.Requests; using Refit; namespace Persistence.Client.Clients { - /// - /// Интерфейс клиента для хранения технологических сообщений - /// - public interface ITechMessagesClient - { - private const string BaseRoute = "/api/techMessages"; + /// + /// Интерфейс клиента для хранения технологических сообщений + /// + public interface ITechMessagesClient + { + private const string BaseRoute = "/api/techMessages"; - [Get($"{BaseRoute}")] - Task>> GetPage([Query] RequestDto request, CancellationToken token); + [Get($"{BaseRoute}")] + Task>> GetPage([Query] PaginationRequest request, CancellationToken token); - [Post($"{BaseRoute}")] - Task> AddRange([Body] IEnumerable dtos, CancellationToken token); + [Post($"{BaseRoute}")] + Task> AddRange([Body] IEnumerable dtos, CancellationToken token); - [Get($"{BaseRoute}/systems")] - Task>> GetSystems(CancellationToken token); + [Get($"{BaseRoute}/systems")] + Task>> GetSystems(CancellationToken token); - [Get($"{BaseRoute}/range")] - Task> GetDatesRangeAsync(CancellationToken token); + [Get($"{BaseRoute}/range")] + Task> GetDatesRangeAsync(CancellationToken token); - [Get($"{BaseRoute}/part")] - Task>> GetPart(DateTimeOffset dateBegin, int take, CancellationToken token); + [Get($"{BaseRoute}/part")] + Task>> GetPart(DateTimeOffset dateBegin, int take, CancellationToken token); - [Get($"{BaseRoute}/statistics")] - Task>> GetStatistics([Query] string autoDrillingSystem, [Query] int categoryId, CancellationToken token); - } + [Get($"{BaseRoute}/statistics")] + Task>> GetStatistics([Query] string autoDrillingSystem, [Query] int categoryId, CancellationToken token); + } } diff --git a/Persistence.Client/Helpers/ApiTokenHelper.cs b/Persistence.Client/Helpers/ApiTokenHelper.cs index f0249d0..d829fcf 100644 --- a/Persistence.Client/Helpers/ApiTokenHelper.cs +++ b/Persistence.Client/Helpers/ApiTokenHelper.cs @@ -1,74 +1,75 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.IdentityModel.Tokens; -using Persistence.Models.Configurations; -using RestSharp; -using System.IdentityModel.Tokens.Jwt; +using System.IdentityModel.Tokens.Jwt; using System.Net.Http.Headers; using System.Security.Claims; using System.Text.Json; +using Microsoft.Extensions.Configuration; +using Microsoft.IdentityModel.Tokens; +using Persistence.Models.Configurations; +using RestSharp; namespace Persistence.Client.Helpers; public static class ApiTokenHelper { - public static void Authorize(this HttpClient httpClient, IConfiguration configuration) - { - var authUser = configuration - .GetSection(nameof(AuthUser)) - .Get()!; - var needUseKeyCloak = configuration - .GetSection("NeedUseKeyCloak") - .Get()!; - var keycloakGetTokenUrl = configuration.GetSection("KeycloakGetTokenUrl").Get() ?? string.Empty; + public static void Authorize(this HttpClient httpClient, IConfiguration configuration) + { + var authUser = configuration + .GetSection(nameof(AuthUser)) + .Get()!; + var needUseKeyCloak = configuration + .GetSection("NeedUseKeyCloak") + .Get()!; + var keycloakGetTokenUrl = configuration.GetSection("KeycloakGetTokenUrl").Get() ?? string.Empty; - var jwtToken = needUseKeyCloak - ? authUser.CreateKeyCloakJwtToken(keycloakGetTokenUrl) - : authUser.CreateDefaultJwtToken(); + var jwtToken = needUseKeyCloak + ? authUser.CreateKeyCloakJwtToken(keycloakGetTokenUrl) + : authUser.CreateDefaultJwtToken(); - httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", jwtToken); - } + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", jwtToken); + } - private static string CreateDefaultJwtToken(this AuthUser authUser) - { - var nameIdentifier = Guid.NewGuid().ToString(); - var claims = new List() - { - new(ClaimTypes.NameIdentifier, nameIdentifier), - new("client_id", authUser.ClientId), - new("username", authUser.Username), - new("password", authUser.Password), - new("grant_type", authUser.GrantType) - }; + private static string CreateDefaultJwtToken(this AuthUser authUser) + { + var nameIdetifier = Guid.NewGuid().ToString(); + var claims = new List() + { + new(ClaimTypes.NameIdentifier, nameIdetifier), + new("client_id", authUser.ClientId), + new("username", authUser.Username), + new("password", authUser.Password), + new("grant_type", authUser.GrantType), + new(ClaimTypes.NameIdentifier.ToString(), Guid.NewGuid().ToString()) + }; - var tokenDescriptor = new SecurityTokenDescriptor - { - Issuer = JwtParams.Issuer, - Audience = JwtParams.Audience, - Subject = new ClaimsIdentity(claims), - Expires = DateTime.UtcNow.AddHours(1), - SigningCredentials = new SigningCredentials(JwtParams.SecurityKey, SecurityAlgorithms.HmacSha256Signature) - }; - var tokenHandler = new JwtSecurityTokenHandler(); - var token = tokenHandler.CreateToken(tokenDescriptor); - return tokenHandler.WriteToken(token); - } + var tokenDescriptor = new SecurityTokenDescriptor + { + Issuer = JwtParams.Issuer, + Audience = JwtParams.Audience, + Subject = new ClaimsIdentity(claims), + Expires = DateTime.UtcNow.AddHours(1), + SigningCredentials = new SigningCredentials(JwtParams.SecurityKey, SecurityAlgorithms.HmacSha256Signature) + }; + var tokenHandler = new JwtSecurityTokenHandler(); + var token = tokenHandler.CreateToken(tokenDescriptor); + return tokenHandler.WriteToken(token); + } - private static string CreateKeyCloakJwtToken(this AuthUser authUser, string keycloakGetTokenUrl) - { - var restClient = new RestClient(); + private static string CreateKeyCloakJwtToken(this AuthUser authUser, string keycloakGetTokenUrl) + { + var restClient = new RestClient(); - var request = new RestRequest(keycloakGetTokenUrl, Method.Post); - request.AddParameter("username", authUser.Username); - request.AddParameter("password", authUser.Password); - request.AddParameter("client_id", authUser.ClientId); - request.AddParameter("grant_type", authUser.GrantType); + var request = new RestRequest(keycloakGetTokenUrl, Method.Post); + request.AddParameter("username", authUser.Username); + request.AddParameter("password", authUser.Password); + request.AddParameter("client_id", authUser.ClientId); + request.AddParameter("grant_type", authUser.GrantType); - var keycloakResponse = restClient.Post(request); - if (keycloakResponse.IsSuccessful && !String.IsNullOrEmpty(keycloakResponse.Content)) - { - var token = JsonSerializer.Deserialize(keycloakResponse.Content)!; - return token.AccessToken; - } + var keyCloackResponse = restClient.Post(request); + if (keyCloackResponse.IsSuccessful && !String.IsNullOrEmpty(keyCloackResponse.Content)) + { + var token = JsonSerializer.Deserialize(keyCloackResponse.Content)!; + return token.AccessToken; + } - return String.Empty; - } + return String.Empty; + } } diff --git a/Persistence.Database.Postgres/DependencyInjection.cs b/Persistence.Database.Postgres/DependencyInjection.cs index 3acd55c..bbd936b 100644 --- a/Persistence.Database.Postgres/DependencyInjection.cs +++ b/Persistence.Database.Postgres/DependencyInjection.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Npgsql; namespace Persistence.Database.Model; diff --git a/Persistence.Database.Postgres/Migrations/20241126071115_Add_ChangeLog.Designer.cs b/Persistence.Database.Postgres/Migrations/20241126071115_Add_ChangeLog.Designer.cs new file mode 100644 index 0000000..c17da2b --- /dev/null +++ b/Persistence.Database.Postgres/Migrations/20241126071115_Add_ChangeLog.Designer.cs @@ -0,0 +1,169 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Persistence.Database.Model; + +#nullable disable + +namespace Persistence.Database.Postgres.Migrations +{ + [DbContext(typeof(PersistenceDbContext))] + [Migration("20241126071115_Add_ChangeLog")] + partial class Add_ChangeLog + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .UseCollation("Russian_Russia.1251") + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "adminpack"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Persistence.Database.Model.ChangeLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("Id"); + + b.Property("Creation") + .HasColumnType("timestamp with time zone") + .HasColumnName("Creation"); + + b.Property("DepthEnd") + .HasColumnType("double precision") + .HasColumnName("DepthEnd"); + + b.Property("DepthStart") + .HasColumnType("double precision") + .HasColumnName("DepthStart"); + + b.Property("IdAuthor") + .HasColumnType("uuid") + .HasColumnName("IdAuthor"); + + b.Property("IdDiscriminator") + .HasColumnType("uuid") + .HasColumnName("IdDiscriminator"); + + b.Property("IdEditor") + .HasColumnType("uuid") + .HasColumnName("IdEditor"); + + b.Property("IdNext") + .HasColumnType("uuid") + .HasColumnName("IdNext"); + + b.Property("IdSection") + .HasColumnType("uuid") + .HasColumnName("IdSection"); + + b.Property("Obsolete") + .HasColumnType("timestamp with time zone") + .HasColumnName("Obsolete"); + + b.Property>("Value") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("Value"); + + b.HasKey("Id"); + + b.ToTable("ChangeLog"); + }); + + modelBuilder.Entity("Persistence.Database.Model.DataSaub", b => + { + b.Property("Date") + .HasColumnType("timestamp with time zone") + .HasColumnName("date"); + + b.Property("AxialLoad") + .HasColumnType("double precision") + .HasColumnName("axialLoad"); + + b.Property("BitDepth") + .HasColumnType("double precision") + .HasColumnName("bitDepth"); + + b.Property("BlockPosition") + .HasColumnType("double precision") + .HasColumnName("blockPosition"); + + b.Property("BlockSpeed") + .HasColumnType("double precision") + .HasColumnName("blockSpeed"); + + b.Property("Flow") + .HasColumnType("double precision") + .HasColumnName("flow"); + + b.Property("HookWeight") + .HasColumnType("double precision") + .HasColumnName("hookWeight"); + + b.Property("IdFeedRegulator") + .HasColumnType("integer") + .HasColumnName("idFeedRegulator"); + + b.Property("Mode") + .HasColumnType("integer") + .HasColumnName("mode"); + + b.Property("Mse") + .HasColumnType("double precision") + .HasColumnName("mse"); + + b.Property("MseState") + .HasColumnType("smallint") + .HasColumnName("mseState"); + + b.Property("Pressure") + .HasColumnType("double precision") + .HasColumnName("pressure"); + + b.Property("Pump0Flow") + .HasColumnType("double precision") + .HasColumnName("pump0Flow"); + + b.Property("Pump1Flow") + .HasColumnType("double precision") + .HasColumnName("pump1Flow"); + + b.Property("Pump2Flow") + .HasColumnType("double precision") + .HasColumnName("pump2Flow"); + + b.Property("RotorSpeed") + .HasColumnType("double precision") + .HasColumnName("rotorSpeed"); + + b.Property("RotorTorque") + .HasColumnType("double precision") + .HasColumnName("rotorTorque"); + + b.Property("User") + .HasColumnType("text") + .HasColumnName("user"); + + b.Property("WellDepth") + .HasColumnType("double precision") + .HasColumnName("wellDepth"); + + b.HasKey("Date"); + + b.ToTable("DataSaub"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Persistence.Database.Postgres/Migrations/20241126071115_Add_ChangeLog.cs b/Persistence.Database.Postgres/Migrations/20241126071115_Add_ChangeLog.cs new file mode 100644 index 0000000..0802b00 --- /dev/null +++ b/Persistence.Database.Postgres/Migrations/20241126071115_Add_ChangeLog.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.Database.Postgres.Migrations +{ + /// + public partial class Add_ChangeLog : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ChangeLog", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + IdDiscriminator = table.Column(type: "uuid", nullable: false), + IdAuthor = table.Column(type: "uuid", nullable: false), + IdEditor = table.Column(type: "uuid", nullable: true), + Creation = table.Column(type: "timestamp with time zone", nullable: false), + Obsolete = table.Column(type: "timestamp with time zone", nullable: true), + IdNext = table.Column(type: "uuid", nullable: true), + DepthStart = table.Column(type: "double precision", nullable: false), + DepthEnd = table.Column(type: "double precision", nullable: false), + IdSection = table.Column(type: "uuid", nullable: false), + Value = table.Column>(type: "jsonb", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ChangeLog", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ChangeLog"); + } + } +} diff --git a/Persistence.Database.Postgres/Migrations/PersistenceDbContextModelSnapshot.cs b/Persistence.Database.Postgres/Migrations/PersistenceDbContextModelSnapshot.cs index e53c81a..8d3335c 100644 --- a/Persistence.Database.Postgres/Migrations/PersistenceDbContextModelSnapshot.cs +++ b/Persistence.Database.Postgres/Migrations/PersistenceDbContextModelSnapshot.cs @@ -1,5 +1,6 @@ // using System; +using System.Collections.Generic; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; @@ -107,6 +108,59 @@ namespace Persistence.Database.Postgres.Migrations }); }); + modelBuilder.Entity("Persistence.Database.Model.ChangeLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("Id"); + + b.Property("Creation") + .HasColumnType("timestamp with time zone") + .HasColumnName("Creation"); + + b.Property("DepthEnd") + .HasColumnType("double precision") + .HasColumnName("DepthEnd"); + + b.Property("DepthStart") + .HasColumnType("double precision") + .HasColumnName("DepthStart"); + + b.Property("IdAuthor") + .HasColumnType("uuid") + .HasColumnName("IdAuthor"); + + b.Property("IdDiscriminator") + .HasColumnType("uuid") + .HasColumnName("IdDiscriminator"); + + b.Property("IdEditor") + .HasColumnType("uuid") + .HasColumnName("IdEditor"); + + b.Property("IdNext") + .HasColumnType("uuid") + .HasColumnName("IdNext"); + + b.Property("IdSection") + .HasColumnType("uuid") + .HasColumnName("IdSection"); + + b.Property("Obsolete") + .HasColumnType("timestamp with time zone") + .HasColumnName("Obsolete"); + + b.Property>("Value") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("Value"); + + b.HasKey("Id"); + + b.ToTable("ChangeLog"); + }); + modelBuilder.Entity("Persistence.Database.Model.DataSaub", b => { b.Property("Date") diff --git a/Persistence.Database.Postgres/PersistenceDbContext.cs b/Persistence.Database.Postgres/PersistenceDbContext.cs index 4d0218b..80a9e25 100644 --- a/Persistence.Database.Postgres/PersistenceDbContext.cs +++ b/Persistence.Database.Postgres/PersistenceDbContext.cs @@ -5,6 +5,7 @@ namespace Persistence.Database.Model; public partial class PersistenceDbContext : DbContext { public DbSet DataSaub => Set(); + public DbSet ChangeLog => Set(); public DbSet Setpoint => Set(); @@ -49,5 +50,9 @@ public partial class PersistenceDbContext : DbContext .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); + + modelBuilder.Entity() + .Property(e => e.Value) + .HasJsonConversion(); } } diff --git a/Persistence.Database.Postgres/Readme.md b/Persistence.Database.Postgres/Readme.md index 756a8f5..7172774 100644 --- a/Persistence.Database.Postgres/Readme.md +++ b/Persistence.Database.Postgres/Readme.md @@ -2,4 +2,10 @@ ``` dotnet ef migrations add --project Persistence.Database.Postgres -``` \ No newline at end of file +``` + +## Откатить миграцию +``` +dotnet ef migrations remove --project Persistence.Database.Postgres +``` +Удаляется последняя созданная миграция. \ No newline at end of file diff --git a/Persistence.Database/Entity/ChangeLog.cs b/Persistence.Database/Entity/ChangeLog.cs new file mode 100644 index 0000000..9a8001b --- /dev/null +++ b/Persistence.Database/Entity/ChangeLog.cs @@ -0,0 +1,46 @@ + +using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations; +using Microsoft.EntityFrameworkCore; +using Persistence.Models; + +namespace Persistence.Database.Model; + +/// +/// Часть записи, описывающая изменение +/// +public class ChangeLog : IChangeLog, IWithSectionPart +{ + [Key, Comment("Ключ записи")] + public Guid Id { get; set; } + + [Comment("Дискриминатор таблицы")] + public Guid IdDiscriminator { get; set; } + + [Comment("Автор изменения")] + public Guid IdAuthor { get; set; } + + [Comment("Редактор")] + public Guid? IdEditor { get; set; } + + [Comment("Дата создания записи")] + public DateTimeOffset Creation { get; set; } + + [Comment("Дата устаревания (например при удалении)")] + public DateTimeOffset? Obsolete { get; set; } + + [Comment("Id заменяющей записи")] + public Guid? IdNext { get; set; } + + [Comment("Глубина забоя на дату начала интервала")] + public double DepthStart { get; set; } + + [Comment("Глубина забоя на дату окончания интервала")] + public double DepthEnd { get; set; } + + [Comment("Ключ секции")] + public Guid IdSection { get; set; } + + [Column(TypeName = "jsonb"), Comment("Значение")] + public required IDictionary Value { get; set; } +} diff --git a/Persistence.Database/Entity/IChangeLog.cs b/Persistence.Database/Entity/IChangeLog.cs new file mode 100644 index 0000000..c4dc962 --- /dev/null +++ b/Persistence.Database/Entity/IChangeLog.cs @@ -0,0 +1,48 @@ + +namespace Persistence.Database.Model; + +/// +/// Часть записи, описывающая изменение +/// +public interface IChangeLog +{ + /// + /// Ключ записи + /// + public Guid Id { get; set; } + + /// + /// Автор изменения + /// + public Guid IdAuthor { get; set; } + + /// + /// Редактор + /// + public Guid? IdEditor { get; set; } + + /// + /// Дата создания записи + /// + public DateTimeOffset Creation { get; set; } + + /// + /// Дата устаревания (например при удалении) + /// + public DateTimeOffset? Obsolete { get; set; } + + /// + /// Id заменяющей записи + /// + public Guid? IdNext { get; set; } + + /// + /// Дискриминатор таблицы + /// + public Guid IdDiscriminator { get; set; } + + /// + /// Значение + /// + public IDictionary Value { get; set; } +} diff --git a/Persistence.Database/Persistence.Database.csproj b/Persistence.Database/Persistence.Database.csproj index 8154daf..3019e82 100644 --- a/Persistence.Database/Persistence.Database.csproj +++ b/Persistence.Database/Persistence.Database.csproj @@ -14,4 +14,8 @@ + + + + diff --git a/Persistence.IntegrationTests/Controllers/ChangeLogControllerTest.cs b/Persistence.IntegrationTests/Controllers/ChangeLogControllerTest.cs new file mode 100644 index 0000000..8c3b422 --- /dev/null +++ b/Persistence.IntegrationTests/Controllers/ChangeLogControllerTest.cs @@ -0,0 +1,351 @@ +using Mapster; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Persistence.Client; +using Persistence.Client.Clients; +using Persistence.Database.Model; +using Persistence.Models; +using Persistence.Models.Requests; +using System.Net; +using Xunit; + +namespace Persistence.IntegrationTests.Controllers; +public class ChangeLogControllerTest : BaseIntegrationTest +{ + private readonly IChangeLogClient client; + private static Random generatorRandomDigits = new Random(); + + public ChangeLogControllerTest(WebAppFactoryFixture factory) : base(factory) + { + var persistenceClientFactory = scope.ServiceProvider + .GetRequiredService(); + + client = persistenceClientFactory.GetClient(); + } + + [Fact] + public async Task ClearAndInsertRange_InEmptyDb() + { + // arrange + var idDiscriminator = Guid.NewGuid(); + var dtos = Generate(2, DateTimeOffset.UtcNow); + + // act + var result = await client.ClearAndAddRange(idDiscriminator, dtos); + + // assert + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + Assert.Equal(2, result.Content); + } + + [Fact] + public async Task ClearAndInsertRange_InNotEmptyDb() + { + // arrange + var insertedCount = 10; + var createdResult = CreateChangeLogItems(insertedCount, (-15, 15)); + var idDiscriminator = createdResult.Item1; + var dtos = createdResult.Item2.Select(e => e.Adapt()); + + // act + var result = await client.ClearAndAddRange(idDiscriminator, dtos); + + // assert + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + Assert.Equal(insertedCount*2, result.Content); + } + + [Fact] + public async Task Add_returns_success() + { + // arrange + var count = 1; + var idDiscriminator = Guid.NewGuid(); + var dtos = Generate(count, DateTimeOffset.UtcNow); + var dto = dtos.FirstOrDefault()!; + + // act + var result = await client.Add(idDiscriminator, dto); + + // assert + Assert.Equal(HttpStatusCode.Created, result.StatusCode); + Assert.Equal(count, result.Content); + } + + [Fact] + public async Task AddRange_returns_success() + { + // arrange + var count = 3; + var idDiscriminator = Guid.NewGuid(); + var dtos = Generate(count, DateTimeOffset.UtcNow); + + // act + var result = await client.AddRange(idDiscriminator, dtos); + + // assert + Assert.Equal(HttpStatusCode.Created, result.StatusCode); + Assert.Equal(count, result.Content); + } + + [Fact] + public async Task Update_returns_success() + { + // arrange + var idDiscriminator = Guid.NewGuid(); + var dtos = Generate(1, DateTimeOffset.UtcNow); + var dto = dtos.FirstOrDefault()!; + var result = await client.Add(idDiscriminator, dto); + Assert.Equal(HttpStatusCode.Created, result.StatusCode); + + var entity = dbContext.ChangeLog + .Where(x => x.IdDiscriminator == idDiscriminator) + .FirstOrDefault(); + dto = entity.Adapt(); + dto.DepthEnd = dto.DepthEnd + 10; + + // act + result = await client.Update(dto); + + // assert + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + Assert.Equal(2, result.Content); + + var dateBegin = DateTimeOffset.UtcNow.AddDays(-1); + var dateEnd = DateTimeOffset.UtcNow.AddDays(1); + + var changeLogResult = await client.GetChangeLogForInterval(idDiscriminator, dateBegin, dateEnd); + Assert.Equal(HttpStatusCode.OK, changeLogResult.StatusCode); + Assert.NotNull(changeLogResult.Content); + + var changeLogDtos = changeLogResult.Content; + + var obsoleteDto = changeLogDtos + .Where(e => e.Obsolete.HasValue) + .FirstOrDefault(); + + var activeDto = changeLogDtos + .Where(e => !e.Obsolete.HasValue) + .FirstOrDefault(); + + if (obsoleteDto == null || activeDto == null) + { + Assert.Fail(); + return; + } + + Assert.Equal(activeDto.Id, obsoleteDto.IdNext); + + } + + [Fact] + public async Task UpdateRange_returns_success() + { + // arrange + var count = 2; + var dtos = Generate(count, DateTimeOffset.UtcNow); + var entities = dtos.Select(d => d.Adapt()).ToArray(); + dbContext.ChangeLog.AddRange(entities); + dbContext.SaveChanges(); + + dtos = entities.Select(c => new DataWithWellDepthAndSectionDto() + { + DepthEnd = c.DepthEnd + 10, + DepthStart = c.DepthStart + 10, + Id = c.Id, + IdSection = c.IdSection, + Value = c.Value + }).ToArray(); + + // act + var result = await client.UpdateRange(dtos); + + // assert + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + Assert.Equal(count * 2, result.Content); + } + + [Fact] + public async Task Delete_returns_success() + { + // arrange + var dtos = Generate(1, DateTimeOffset.UtcNow); + var dto = dtos.FirstOrDefault()!; + var entity = dto.Adapt(); + dbContext.ChangeLog.Add(entity); + dbContext.SaveChanges(); + + // act + var result = await client.Delete(entity.Id); + + // assert + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + Assert.Equal(1, result.Content); + } + + [Fact] + public async Task DeleteRange_returns_success() + { + // arrange + var count = 10; + var dtos = Generate(count, DateTimeOffset.UtcNow); + var entities = dtos.Select(d => d.Adapt()).ToArray(); + dbContext.ChangeLog.AddRange(entities); + dbContext.SaveChanges(); + + // act + var ids = entities.Select(e => e.Id); + var result = await client.DeleteRange(ids); + + // assert + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + Assert.Equal(count, result.Content); + } + + [Fact] + public async Task GetDatesRange_returns_success() + { + // arrange + var changeLogItems = CreateChangeLogItems(3, (-15, 15)); + var idDiscriminator = changeLogItems.Item1; + var entities = changeLogItems.Item2.OrderBy(e => e.Creation); + + // act + var result = await client.GetDatesRange(idDiscriminator); + + // assert + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + Assert.NotNull(result.Content); + + var minDate = entities.First().Creation; + var maxDate = entities.Last().Creation; + + var expectedMinDate = minDate.ToUniversalTime().ToString(); + var actualMinDate = result.Content.From.ToUniversalTime().ToString(); + Assert.Equal(expectedMinDate, actualMinDate); + + var expectedMaxDate = maxDate.ToUniversalTime().ToString(); + var actualMaxDate = result.Content.To.ToUniversalTime().ToString(); + Assert.Equal(expectedMaxDate, actualMaxDate); + } + + [Fact] + public async Task GetByDate_returns_success() + { + // arrange + //создаем записи + var count = 5; + var changeLogItems = CreateChangeLogItems(count, (-15, 15)); + var idDiscriminator = changeLogItems.Item1; + var entities = changeLogItems.Item2; + + //удаляем все созданные записи за исключением первой и второй + //даты 2-х оставшихся записей должны вернуться в методе GetByDate + var ids = entities.Select(e => e.Id); + var idsToDelete = ids.Skip(2); + + var deletedCount = await client.DeleteRange(idsToDelete); + + var filterRequest = new SectionPartRequest() + { + DepthStart = 0, + DepthEnd = 1000, + }; + + var paginationRequest = new PaginationRequest() + { + Skip = 0, + Take = 10, + SortSettings = String.Empty, + }; + + var moment = DateTimeOffset.UtcNow.AddDays(16); + var result = await client.GetByDate(idDiscriminator, moment, filterRequest, paginationRequest); + + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + Assert.NotNull(result.Content); + + var restEntities = entities.Where(e => !idsToDelete.Contains(e.Id)); + Assert.Equal(restEntities.Count(), result.Content.Count); + + var actualIds = restEntities.Select(e => e.Id); + var expectedIds = result.Content.Items.Select(e => e.Id); + Assert.Equivalent(expectedIds, actualIds); + } + + [Theory] + [InlineData(5, -15, 15, -20, 20, 10)] + [InlineData(5, -15, -10, -16, -9, 5)] + public async Task GetChangeLogForInterval_returns_success( + int insertedCount, + int daysBeforeNowChangeLog, + int daysAfterNowChangeLog, + int daysBeforeNowFilter, + int daysAfterNowFilter, + int changeLogCount) + { + // arrange + //создаем записи + var count = insertedCount; + var daysRange = (daysBeforeNowChangeLog, daysAfterNowChangeLog); + var changeLogItems = CreateChangeLogItems(count, daysRange); + var idDiscriminator = changeLogItems.Item1; + var entities = changeLogItems.Item2; + + foreach (var entity in entities) + { + entity.DepthEnd = entity.DepthEnd + 10; + } + var dtos = entities.Select(e => e.Adapt()).ToArray(); + await client.UpdateRange(dtos); + + //act + var dateBegin = DateTimeOffset.UtcNow.AddDays(daysBeforeNowFilter); + var dateEnd = DateTimeOffset.UtcNow.AddDays(daysAfterNowFilter); + var result = await client.GetChangeLogForInterval(idDiscriminator, dateBegin, dateEnd); + + //assert + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + Assert.NotNull(result.Content); + Assert.Equal(changeLogCount, result.Content.Count()); + } + + + private static IEnumerable Generate(int count, DateTimeOffset from) + { + for (int i = 0; i < count; i++) + yield return new DataWithWellDepthAndSectionDto() + { + Value = new Dictionary() + { + { "Key", 1 } + }, + DepthStart = generatorRandomDigits.Next(1, 5), + DepthEnd = generatorRandomDigits.Next(5, 15), + Id = Guid.NewGuid(), + IdSection = Guid.NewGuid() + }; + + } + + private (Guid, ChangeLog[]) CreateChangeLogItems(int count, (int, int) daysRange) + { + var minDayCount = daysRange.Item1; + var maxDayCount = daysRange.Item2; + + Guid idDiscriminator = Guid.NewGuid(); + var dtos = Generate(count, DateTimeOffset.UtcNow); + var entities = dtos.Select(d => + { + var entity = d.Adapt(); + entity.IdDiscriminator = idDiscriminator; + entity.Creation = DateTimeOffset.UtcNow.AddDays(generatorRandomDigits.Next(minDayCount, maxDayCount)); + + return entity; + }).ToArray(); + dbContext.ChangeLog.AddRange(entities); + dbContext.SaveChanges(); + + return (idDiscriminator, entities); + } +} diff --git a/Persistence.IntegrationTests/Controllers/TechMessagesControllerTest.cs b/Persistence.IntegrationTests/Controllers/TechMessagesControllerTest.cs index 63f5171..291991f 100644 --- a/Persistence.IntegrationTests/Controllers/TechMessagesControllerTest.cs +++ b/Persistence.IntegrationTests/Controllers/TechMessagesControllerTest.cs @@ -1,288 +1,289 @@ -using Microsoft.Extensions.Caching.Memory; +using System.Net; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using Persistence.Client; using Persistence.Client.Clients; using Persistence.Database.Entity; using Persistence.Models; -using System.Net; +using Persistence.Models.Requests; using Xunit; namespace Persistence.IntegrationTests.Controllers { - public class TechMessagesControllerTest : BaseIntegrationTest - { - private static readonly string SystemCacheKey = $"{typeof(Database.Entity.DrillingSystem).FullName}CacheKey"; - private readonly ITechMessagesClient techMessagesClient; - private readonly IMemoryCache memoryCache; - public TechMessagesControllerTest(WebAppFactoryFixture factory) : base(factory) - { - var scope = factory.Services.CreateScope(); - var persistenceClientFactory = scope.ServiceProvider - .GetRequiredService(); + public class TechMessagesControllerTest : BaseIntegrationTest + { + private static readonly string SystemCacheKey = $"{typeof(Database.Entity.DrillingSystem).FullName}CacheKey"; + private readonly ITechMessagesClient techMessagesClient; + private readonly IMemoryCache memoryCache; + public TechMessagesControllerTest(WebAppFactoryFixture factory) : base(factory) + { + var scope = factory.Services.CreateScope(); + var persistenceClientFactory = scope.ServiceProvider + .GetRequiredService(); - techMessagesClient = persistenceClientFactory.GetClient(); - memoryCache = scope.ServiceProvider.GetRequiredService(); - } + techMessagesClient = persistenceClientFactory.GetClient(); + memoryCache = scope.ServiceProvider.GetRequiredService(); + } - [Fact] - public async Task GetPage_returns_success() - { - //arrange - memoryCache.Remove(SystemCacheKey); - dbContext.CleanupDbSet(); - dbContext.CleanupDbSet(); + [Fact] + public async Task GetPage_returns_success() + { + //arrange + memoryCache.Remove(SystemCacheKey); + dbContext.CleanupDbSet(); + dbContext.CleanupDbSet(); - var requestDto = new RequestDto() - { - Skip = 1, - Take = 2, - SortSettings = nameof(TechMessage.CategoryId) - }; + var PaginationRequest = new PaginationRequest() + { + Skip = 1, + Take = 2, + SortSettings = nameof(TechMessage.CategoryId) + }; - //act - var response = await techMessagesClient.GetPage(requestDto, new CancellationToken()); + //act + var response = await techMessagesClient.GetPage(PaginationRequest, new CancellationToken()); - //assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - Assert.Empty(response.Content.Items); - Assert.Equal(requestDto.Skip, response.Content.Skip); - Assert.Equal(requestDto.Take, response.Content.Take); - } + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + Assert.Empty(response.Content.Items); + Assert.Equal(PaginationRequest.Skip, response.Content.Skip); + Assert.Equal(PaginationRequest.Take, response.Content.Take); + } - [Fact] - public async Task GetPage_AfterSave_returns_success() - { - //arrange - var dtos = await InsertRange(); - var dtosCount = dtos.Count(); - var requestDto = new RequestDto() - { - Skip = 0, - Take = 2, - SortSettings = nameof(TechMessage.CategoryId) - }; + [Fact] + public async Task GetPage_AfterSave_returns_success() + { + //arrange + var dtos = await InsertRange(); + var dtosCount = dtos.Count(); + var PaginationRequest = new PaginationRequest() + { + Skip = 0, + Take = 2, + SortSettings = nameof(TechMessage.CategoryId) + }; - //act - var response = await techMessagesClient.GetPage(requestDto, new CancellationToken()); + //act + var response = await techMessagesClient.GetPage(PaginationRequest, new CancellationToken()); - //assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - Assert.Equal(dtosCount, response.Content.Count); - } + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + Assert.Equal(dtosCount, response.Content.Count); + } - [Fact] - public async Task InsertRange_returns_success() - { - await InsertRange(); - } + [Fact] + public async Task InsertRange_returns_success() + { + await InsertRange(); + } - [Fact] - public async Task InsertRange_returns_BadRequest() - { - //arrange - var dtos = new List() - { - new() - { - EventId = Guid.NewGuid(), - CategoryId = -1, // < 0 + [Fact] + public async Task InsertRange_returns_BadRequest() + { + //arrange + var dtos = new List() + { + new TechMessageDto() + { + EventId = Guid.NewGuid(), + CategoryId = -1, // < 0 Timestamp = DateTimeOffset.UtcNow, - Depth = -1, // < 0 + Depth = -1, // < 0 MessageText = string.Empty, // length < 0 System = string.Concat(Enumerable.Repeat(nameof(TechMessageDto.System), 100)), // length > 256 UserId = Guid.NewGuid() - } - }; + } + }; - //act - var response = await techMessagesClient.AddRange(dtos, new CancellationToken()); + //act + var response = await techMessagesClient.AddRange(dtos, new CancellationToken()); - //assert - Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); - } + //assert + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } - [Fact] - public async Task GetSystems_returns_success() - { - //arrange - memoryCache.Remove(SystemCacheKey); - dbContext.CleanupDbSet(); - dbContext.CleanupDbSet(); + [Fact] + public async Task GetSystems_returns_success() + { + //arrange + memoryCache.Remove(SystemCacheKey); + dbContext.CleanupDbSet(); + dbContext.CleanupDbSet(); - //act - var response = await techMessagesClient.GetSystems(new CancellationToken()); + //act + var response = await techMessagesClient.GetSystems(new CancellationToken()); - //assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - Assert.Empty(response.Content); - } + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + Assert.Empty(response.Content); + } - [Fact] - public async Task GetSystems_AfterSave_returns_success() - { - //arrange - var dtos = await InsertRange(); - var systems = dtos - .Select(e => e.System) - .Distinct() - .ToArray(); + [Fact] + public async Task GetSystems_AfterSave_returns_success() + { + //arrange + var dtos = await InsertRange(); + var systems = dtos + .Select(e => e.System) + .Distinct() + .ToArray(); - //act - var response = await techMessagesClient.GetSystems(new CancellationToken()); + //act + var response = await techMessagesClient.GetSystems(new CancellationToken()); - //assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - string?[]? content = response.Content?.ToArray(); - Assert.Equal(systems, content); - } + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + string?[]? content = response.Content?.ToArray(); + Assert.Equal(systems, content); + } - [Fact] - public async Task GetStatistics_returns_success() - { - //arrange - memoryCache.Remove(SystemCacheKey); - dbContext.CleanupDbSet(); - dbContext.CleanupDbSet(); + [Fact] + public async Task GetStatistics_returns_success() + { + //arrange + memoryCache.Remove(SystemCacheKey); + dbContext.CleanupDbSet(); + dbContext.CleanupDbSet(); - var imortantId = 1; - var autoDrillingSystem = nameof(TechMessageDto.System); + var imortantId = 1; + var autoDrillingSystem = nameof(TechMessageDto.System); - //act - var response = await techMessagesClient.GetStatistics(autoDrillingSystem, imortantId, new CancellationToken()); + //act + var response = await techMessagesClient.GetStatistics(autoDrillingSystem, imortantId, new CancellationToken()); - //assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - Assert.Empty(response.Content); - } + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + Assert.Empty(response.Content); + } - [Fact] - public async Task GetStatistics_AfterSave_returns_success() - { - //arrange - var imortantId = 0; - var autoDrillingSystem = nameof(TechMessageDto.System); - var dtos = await InsertRange(); - var filteredDtos = dtos.Where(e => e.CategoryId == imortantId && e.System == autoDrillingSystem); + [Fact] + public async Task GetStatistics_AfterSave_returns_success() + { + //arrange + var imortantId = 0; + var autoDrillingSystem = nameof(TechMessageDto.System); + var dtos = await InsertRange(); + var filteredDtos = dtos.Where(e => e.CategoryId == imortantId && e.System == autoDrillingSystem); - //act - var response = await techMessagesClient.GetStatistics(autoDrillingSystem, imortantId, new CancellationToken()); + //act + var response = await techMessagesClient.GetStatistics(autoDrillingSystem, imortantId, new CancellationToken()); - //assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - var categories = response.Content - .FirstOrDefault()?.Categories - .FirstOrDefault(e => e.Key == 0).Value; - Assert.Equal(filteredDtos.Count(), categories); - } + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + var categories = response.Content + .FirstOrDefault()?.Categories + .FirstOrDefault(e => e.Key == 0).Value; + Assert.Equal(filteredDtos.Count(), categories); + } - [Fact] - public async Task GetDatesRange_returns_success() - { - //act - var response = await techMessagesClient.GetDatesRangeAsync(new CancellationToken()); + [Fact] + public async Task GetDatesRange_returns_success() + { + //act + var response = await techMessagesClient.GetDatesRangeAsync(new CancellationToken()); - //assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - //Assert.Equal(DateTimeOffset.MinValue, response.Content?.From); - //Assert.Equal(DateTimeOffset.MaxValue, response.Content?.To); - } + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + //Assert.Equal(DateTimeOffset.MinValue, response.Content?.From); + //Assert.Equal(DateTimeOffset.MaxValue, response.Content?.To); + } - [Fact] - public async Task GetDatesRange_AfterSave_returns_success() - { - //arrange - await InsertRange(); + [Fact] + public async Task GetDatesRange_AfterSave_returns_success() + { + //arrange + await InsertRange(); - //act - var response = await techMessagesClient.GetDatesRangeAsync(new CancellationToken()); + //act + var response = await techMessagesClient.GetDatesRangeAsync(new CancellationToken()); - //assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - Assert.NotNull(response.Content?.From); - Assert.NotNull(response.Content?.To); - } + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + Assert.NotNull(response.Content?.From); + Assert.NotNull(response.Content?.To); + } - [Fact] - public async Task GetPart_returns_success() - { - //arrange - var dateBegin = DateTimeOffset.UtcNow; - var take = 2; + [Fact] + public async Task GetPart_returns_success() + { + //arrange + var dateBegin = DateTimeOffset.UtcNow; + var take = 2; - //act - var response = await techMessagesClient.GetPart(dateBegin, take, new CancellationToken()); + //act + var response = await techMessagesClient.GetPart(dateBegin, take, new CancellationToken()); - //assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - Assert.Empty(response.Content); - } + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + Assert.Empty(response.Content); + } - [Fact] - public async Task GetPart_AfterSave_returns_success() - { - //arrange - var dateBegin = DateTimeOffset.UtcNow; - var take = 1; - await InsertRange(); + [Fact] + public async Task GetPart_AfterSave_returns_success() + { + //arrange + var dateBegin = DateTimeOffset.UtcNow; + var take = 1; + await InsertRange(); - //act - var response = await techMessagesClient.GetPart(dateBegin, take, new CancellationToken()); + //act + var response = await techMessagesClient.GetPart(dateBegin, take, new CancellationToken()); - //assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - Assert.NotEmpty(response.Content); - } + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + Assert.NotEmpty(response.Content); + } - private async Task> InsertRange() - { - //arrange - memoryCache.Remove(SystemCacheKey); - dbContext.CleanupDbSet(); - dbContext.CleanupDbSet(); + private async Task> InsertRange() + { + //arrange + memoryCache.Remove(SystemCacheKey); + dbContext.CleanupDbSet(); + dbContext.CleanupDbSet(); - var dtos = new List() - { - new() - { - EventId = Guid.NewGuid(), - CategoryId = 1, - Timestamp = DateTimeOffset.UtcNow, - Depth = 1.11, - MessageText = nameof(TechMessageDto.MessageText), - System = nameof(TechMessageDto.System).ToLower(), - UserId = Guid.NewGuid() - }, - new() - { - EventId = Guid.NewGuid(), - CategoryId = 2, - Timestamp = DateTimeOffset.UtcNow, - Depth = 2.22, - MessageText = nameof(TechMessageDto.MessageText), - System = nameof(TechMessageDto.System).ToLower(), - UserId = Guid.NewGuid() - } - }; + var dtos = new List() + { + new TechMessageDto() + { + EventId = Guid.NewGuid(), + CategoryId = 1, + Timestamp = DateTimeOffset.UtcNow, + Depth = 1.11, + MessageText = nameof(TechMessageDto.MessageText), + System = nameof(TechMessageDto.System).ToLower(), + UserId = Guid.NewGuid() + }, + new TechMessageDto() + { + EventId = Guid.NewGuid(), + CategoryId = 2, + Timestamp = DateTimeOffset.UtcNow, + Depth = 2.22, + MessageText = nameof(TechMessageDto.MessageText), + System = nameof(TechMessageDto.System).ToLower(), + UserId = Guid.NewGuid() + } + }; - //act - var response = await techMessagesClient.AddRange(dtos, new CancellationToken()); + //act + var response = await techMessagesClient.AddRange(dtos, new CancellationToken()); - //assert - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - Assert.Equal(dtos.Count, response.Content); + //assert + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + Assert.Equal(dtos.Count, response.Content); - return dtos; - } - } + return dtos; + } + } } diff --git a/Persistence.Repository/DependencyInjection.cs b/Persistence.Repository/DependencyInjection.cs index e61d126..c433615 100644 --- a/Persistence.Repository/DependencyInjection.cs +++ b/Persistence.Repository/DependencyInjection.cs @@ -1,5 +1,7 @@ +using Mapster; using Microsoft.Extensions.DependencyInjection; using Persistence.Database.Model; +using Persistence.Models; using Persistence.Repositories; using Persistence.Repository.Data; using Persistence.Repository.Repositories; @@ -9,6 +11,17 @@ public static class DependencyInjection { public static void MapsterSetup() { + TypeAdapterConfig.GlobalSettings.Default.Config + .ForType() + .Map(dest => dest.Value, src => new DataWithWellDepthAndSectionDto() + { + DepthEnd = src.DepthEnd, + DepthStart = src.DepthStart, + IdSection = src.IdSection, + Value = src.Value, + Id = src.Id + }); + } public static IServiceCollection AddInfrastructure(this IServiceCollection services) @@ -18,6 +31,7 @@ public static class DependencyInjection services.AddTransient, TimeSeriesDataRepository>(); services.AddTransient(); services.AddTransient, TimeSeriesDataCachedRepository>(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/Persistence.Repository/Persistence.Repository.csproj b/Persistence.Repository/Persistence.Repository.csproj index 55bd8ea..833fc6f 100644 --- a/Persistence.Repository/Persistence.Repository.csproj +++ b/Persistence.Repository/Persistence.Repository.csproj @@ -8,6 +8,7 @@ + diff --git a/Persistence.Repository/QueryBuilders.cs b/Persistence.Repository/QueryBuilders.cs new file mode 100644 index 0000000..6070a8a --- /dev/null +++ b/Persistence.Repository/QueryBuilders.cs @@ -0,0 +1,82 @@ +using Microsoft.EntityFrameworkCore; +using Persistence.Database.Model; +using Persistence.Models; +using Persistence.Models.Requests; + +namespace Persistence.Repository; + +/// +/// класс с набором методов, необходимых для фильтрации записей +/// +public static class QueryBuilders +{ + public static IQueryable Apply(this IQueryable query, SectionPartRequest request) + where TEntity : class, IWithSectionPart + { + if (request.IdSection.HasValue) + { + query = query.Where(e => e.IdSection == request.IdSection); + } + if (request.DepthStart.HasValue) + { + query = query.Where(e => e.DepthStart >= request.DepthStart); + } + if (request.DepthEnd.HasValue) + { + query = query.Where(e => e.DepthEnd <= request.DepthEnd); + } + + return query; + } + + public static IQueryable Apply(this IQueryable query,DateTimeOffset momentUtc) + where TEntity : class, IChangeLog + { + momentUtc = momentUtc.ToUniversalTime(); + + query = query + .Where(e => e.Creation <= momentUtc) + .Where(e => e.Obsolete == null || e.Obsolete >= momentUtc); + + return query; + } + + + public static async Task> ApplyPagination( + this IQueryable query, + PaginationRequest request, + Func Convert, + CancellationToken token) + where TEntity : class, IWithSectionPart + where TDto : class + { + if (String.IsNullOrEmpty(request.SortSettings)) + { + query = query + .OrderBy(e => e.IdSection) + .ThenBy(e => e.DepthStart) + .ThenBy(e => e.DepthEnd); + } + else + { + query = query.SortBy(request.SortSettings); + } + + var entities = await query + .Skip(request.Skip) + .Take(request.Take) + .ToArrayAsync(token); + + var count = await query.CountAsync(token); + var items = entities.Select(Convert); + var result = new PaginationContainer + { + Skip = request.Skip, + Take = request.Take, + Items = items, + Count = count + }; + + return result; + } +} diff --git a/Persistence.Repository/Repositories/ChangeLogRepository.cs b/Persistence.Repository/Repositories/ChangeLogRepository.cs new file mode 100644 index 0000000..ac00e05 --- /dev/null +++ b/Persistence.Repository/Repositories/ChangeLogRepository.cs @@ -0,0 +1,259 @@ +using Mapster; +using Microsoft.EntityFrameworkCore; +using Persistence.Database.Model; +using Persistence.Models; +using Persistence.Models.Requests; +using Persistence.Repositories; +using UuidExtensions; + +namespace Persistence.Repository.Repositories; +public class ChangeLogRepository : IChangeLogRepository +{ + private DbContext db; + + public ChangeLogRepository(DbContext db) + { + this.db = db; + } + + public async Task AddRange(Guid idAuthor, Guid idDiscriminator, IEnumerable dtos, CancellationToken token) + { + var entities = new List(); + foreach (var dto in dtos) + { + var entity = CreateEntityFromDto(idAuthor, idDiscriminator, dto); + entities.Add(entity); + } + db.Set().AddRange(entities); + + var result = await db.SaveChangesAsync(token); + + return result; + } + + public async Task MarkAsDeleted(Guid idEditor, IEnumerable ids, CancellationToken token) + { + var query = db.Set() + .Where(s => ids.Contains(s.Id)) + .Where(s => s.Obsolete == null); + + if (query.Count() != ids.Count()) + { + throw new ArgumentException("Count of active items not equal count of ids", nameof(ids)); + } + + var entities = await query.ToArrayAsync(token); + + var result = await MarkAsObsolete(idEditor, entities, token); + + return result; + } + + public async Task MarkAsDeleted(Guid idEditor, Guid idDiscriminator, CancellationToken token) + { + var query = db.Set() + .Where(s => s.IdDiscriminator == idDiscriminator) + .Where(e => e.Obsolete == null); + + var entities = await query.ToArrayAsync(token); + + var result = await MarkAsObsolete(idEditor, entities, token); + + return result; + } + + private async Task MarkAsObsolete(Guid idEditor, IEnumerable entities, CancellationToken token) + { + var updateTime = DateTimeOffset.UtcNow; + + foreach (var entity in entities) + { + entity.Obsolete = updateTime; + entity.IdEditor = idEditor; + } + + return await db.SaveChangesAsync(token); + } + + public async Task ClearAndAddRange(Guid idAuthor, Guid idDiscriminator, IEnumerable dtos, CancellationToken token) + { + var result = 0; + + using var transaction = await db.Database.BeginTransactionAsync(token); + + result += await MarkAsDeleted(idAuthor, idDiscriminator, token); + result += await AddRange(idAuthor, idDiscriminator, dtos, token); + + await transaction.CommitAsync(token); + + + return result; + } + + public async Task UpdateRange(Guid idEditor, IEnumerable dtos, CancellationToken token) + { + var dbSet = db.Set(); + + var updatedIds = dtos.Select(d => d.Id); + var updatedEntities = dbSet + .Where(s => updatedIds.Contains(s.Id)) + .ToDictionary(s => s.Id); + + var result = 0; + using var transaction = await db.Database.BeginTransactionAsync(token); + + foreach (var dto in dtos) + { + var updatedEntity = updatedEntities.GetValueOrDefault(dto.Id); + if (updatedEntity is null) + { + throw new ArgumentException($"Entity with id = {dto.Id} doesn't exist in Db", nameof(dto)); + } + + var newEntity = CreateEntityFromDto(idEditor, updatedEntity.IdDiscriminator, dto); + dbSet.Add(newEntity); + + updatedEntity.IdNext = newEntity.Id; + updatedEntity.Obsolete = DateTimeOffset.UtcNow; + updatedEntity.IdEditor = idEditor; + } + + result = await db.SaveChangesAsync(token); + await transaction.CommitAsync(token); + + return result; + + + } + + public async Task> GetByDate( + Guid idDiscriminator, + DateTimeOffset momentUtc, + SectionPartRequest filterRequest, + PaginationRequest paginationRequest, + CancellationToken token) + { + var query = CreateQuery(idDiscriminator); + query = query.Apply(momentUtc); + query = query.Apply(filterRequest); + + var result = await query.ApplyPagination(paginationRequest, Convert, token); + + return result; + } + + private IQueryable CreateQuery(Guid idDiscriminator) + { + var query = db.Set().Where(e => e.IdDiscriminator == idDiscriminator); + + return query; + } + + public async Task> GetChangeLogForInterval(Guid idDiscriminator, DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token) + { + var query = db.Set().Where(s => s.IdDiscriminator == idDiscriminator); + + var min = new DateTimeOffset(dateBegin.ToUniversalTime().Date, TimeSpan.Zero); + var max = new DateTimeOffset(dateEnd.ToUniversalTime().Date, TimeSpan.Zero); + + var createdQuery = query.Where(e => e.Creation >= min && e.Creation <= max); + var editedQuery = query.Where(e => e.Obsolete != null && e.Obsolete >= min && e.Obsolete <= max); + + query = createdQuery.Union(editedQuery); + var entities = await query.ToArrayAsync(token); + + var dtos = entities.Select(e => e.Adapt()); + + return dtos; + } + + + + public async Task> GetDatesChange(Guid idDiscriminator, CancellationToken token) + { + var query = db.Set().Where(e => e.IdDiscriminator == idDiscriminator); + + var datesCreateQuery = query + .Select(e => e.Creation) + .Distinct(); + + var datesCreate = await datesCreateQuery.ToArrayAsync(token); + + var datesUpdateQuery = query + .Where(e => e.Obsolete != null) + .Select(e => e.Obsolete!.Value) + .Distinct(); + + var datesUpdate = await datesUpdateQuery.ToArrayAsync(token); + + var dates = Enumerable.Concat(datesCreate, datesUpdate); + var datesOnly = dates + .Select(d => new DateOnly(d.Year, d.Month, d.Day)) + .Distinct() + .OrderBy(d => d); + + return datesOnly; + } + + private ChangeLog CreateEntityFromDto(Guid idAuthor, Guid idDiscriminator, DataWithWellDepthAndSectionDto dto) + { + var entity = new ChangeLog() + { + Id = Uuid7.Guid(), + Creation = DateTimeOffset.UtcNow, + IdAuthor = idAuthor, + IdDiscriminator = idDiscriminator, + IdEditor = idAuthor, + + Value = dto.Value, + IdSection = dto.IdSection, + DepthStart = dto.DepthStart, + DepthEnd = dto.DepthEnd, + }; + + return entity; + } + + public async Task> GetGtDate(Guid idDiscriminator, DateTimeOffset dateBegin, CancellationToken token) + { + var date = dateBegin.ToUniversalTime(); + var query = this.db.Set() + .Where(e => e.IdDiscriminator == idDiscriminator) + .Where(e => e.Creation >= date || e.Obsolete >= date); + + var entities = await query.ToArrayAsync(token); + + var dtos = entities.Select(Convert); + + return dtos; + } + + public async Task GetDatesRange(Guid idDiscriminator, CancellationToken token) + { + var query = db.Set() + .Where(e => e.IdDiscriminator == idDiscriminator) + .GroupBy(e => 1) + .Select(group => new + { + Min = group.Min(e => e.Creation), + Max = group.Max(e => (e.Obsolete.HasValue && e.Obsolete > e.Creation) + ? e.Obsolete.Value + : e.Creation), + }); + + var values = await query.FirstOrDefaultAsync(token); + + if (values is null) + { + return null; + } + + return new DatesRangeDto + { + From = values.Min, + To = values.Max, + }; + } + + private DataWithWellDepthAndSectionDto Convert(ChangeLog entity) => entity.Adapt(); +} diff --git a/Persistence.Repository/Repositories/TechMessagesRepository.cs b/Persistence.Repository/Repositories/TechMessagesRepository.cs index f9e1e27..d2efbe4 100644 --- a/Persistence.Repository/Repositories/TechMessagesRepository.cs +++ b/Persistence.Repository/Repositories/TechMessagesRepository.cs @@ -3,175 +3,176 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; using Persistence.Database.Entity; using Persistence.Models; +using Persistence.Models.Requests; using Persistence.Repositories; using Persistence.Repository.Extensions; namespace Persistence.Repository.Repositories { - public class TechMessagesRepository : ITechMessagesRepository - { - private static readonly string SystemCacheKey = $"{typeof(Database.Entity.DrillingSystem).FullName}CacheKey"; - private const int CacheExpirationInMinutes = 60; - private readonly IMemoryCache memoryCache; - private DbContext db; + public class TechMessagesRepository : ITechMessagesRepository + { + private static readonly string SystemCacheKey = $"{typeof(Database.Entity.DrillingSystem).FullName}CacheKey"; + private const int CacheExpirationInMinutes = 60; + private readonly IMemoryCache memoryCache; + private DbContext db; - public TechMessagesRepository(DbContext db, IMemoryCache memoryCache) - { - this.memoryCache = memoryCache; - this.db = db; - } + public TechMessagesRepository(DbContext db, IMemoryCache memoryCache) + { + this.memoryCache = memoryCache; + this.db = db; + } - protected virtual IQueryable GetQueryReadOnly() => db.Set() - .Include(e => e.System); + protected virtual IQueryable GetQueryReadOnly() => db.Set() + .Include(e => e.System); - public async Task> GetPage(RequestDto request, CancellationToken token) - { - var query = GetQueryReadOnly(); - var count = await query.CountAsync(token); + public async Task> GetPage(PaginationRequest request, CancellationToken token) + { + var query = GetQueryReadOnly(); + var count = await query.CountAsync(token); - var sort = request.SortSettings != string.Empty - ? request.SortSettings - : nameof(TechMessage.Timestamp); - var entities = await query - .SortBy(request.SortSettings) - .Skip(request.Skip) - .Take(request.Take) - .ToArrayAsync(token); + var sort = request.SortSettings != string.Empty + ? request.SortSettings + : nameof(TechMessage.Timestamp); + var entities = await query + .SortBy(request.SortSettings) + .Skip(request.Skip) + .Take(request.Take) + .ToArrayAsync(token); - var dto = new PaginationContainer() - { - Skip = request.Skip, - Take = request.Take, - Count = count, - Items = entities.Select(e => e.Adapt()) - }; + var dto = new PaginationContainer() + { + Skip = request.Skip, + Take = request.Take, + Count = count, + Items = entities.Select(e => e.Adapt()) + }; - return dto; - } + return dto; + } - public async Task> GetStatistics(IEnumerable autoDrillingSystem, IEnumerable categoryIds, CancellationToken token) - { - var query = GetQueryReadOnly(); - var systems = autoDrillingSystem.Select(s => s.ToLower().Trim()); - var result = await query - .Where(e => systems.Count() == 0 || systems.Contains(e.System.Name.ToLower().Trim())) - .GroupBy(e => e.System.Name, (key, group) => new - { - System = key, - Categories = group - .Where(g => categoryIds.Count() == 0 || categoryIds.Contains(g.CategoryId)) - }) - .ToArrayAsync(token); + public async Task> GetStatistics(IEnumerable autoDrillingSystem, IEnumerable categoryIds, CancellationToken token) + { + var query = GetQueryReadOnly(); + var systems = autoDrillingSystem.Select(s => s.ToLower().Trim()); + var result = await query + .Where(e => systems.Count() == 0 || systems.Contains(e.System.Name.ToLower().Trim())) + .GroupBy(e => e.System.Name, (key, group) => new + { + System = key, + Categories = group + .Where(g => categoryIds.Count() == 0 || categoryIds.Contains(g.CategoryId)) + }) + .ToArrayAsync(token); - var entities = new List(); - foreach (var e in result) - { - var categories = e.Categories - .GroupBy(g => g.CategoryId) - .ToDictionary(c => c.Key, v => v.Count()); - var entity = new MessagesStatisticDto() - { - System = e.System, - Categories = categories - }; - entities.Add(entity); - } + var entities = new List(); + foreach (var e in result) + { + var categories = e.Categories + .GroupBy(g => g.CategoryId) + .ToDictionary(c => c.Key, v => v.Count()); + var entity = new MessagesStatisticDto() + { + System = e.System, + Categories = categories + }; + entities.Add(entity); + } - return entities; - } + return entities; + } - public async Task> GetSystems(CancellationToken token) - { - var entities = await GetDrillingSystems(token); - var result = entities.Select(e => e.Name); + public async Task> GetSystems(CancellationToken token) + { + var entities = await GetDrillingSystems(token); + var result = entities.Select(e => e.Name); - return result; - } + return result; + } - public async Task AddRange(IEnumerable dtos, Guid userId, CancellationToken token) - { + public async Task AddRange(IEnumerable dtos, Guid userId, CancellationToken token) + { - var entities = new List(); - foreach (var dto in dtos) - { - var entity = dto.Adapt(); - var systems = await GetDrillingSystems(token); - var systemId = systems.FirstOrDefault(e => e.Name.ToLower().Trim() == dto.System.ToLower().Trim())?.SystemId - ?? await CreateDrillingSystem(dto.System, token); + var entities = new List(); + foreach (var dto in dtos) + { + var entity = dto.Adapt(); + var systems = await GetDrillingSystems(token); + var systemId = systems.FirstOrDefault(e => e.Name.ToLower().Trim() == dto.System.ToLower().Trim())?.SystemId + ?? await CreateDrillingSystem(dto.System, token); - entity.SystemId = systemId; - entity.UserId = userId; + entity.SystemId = systemId; + entity.UserId = userId; - entities.Add(entity); - } + entities.Add(entity); + } - await db.Set().AddRangeAsync(entities, token); - var result = await db.SaveChangesAsync(token); + await db.Set().AddRangeAsync(entities, token); + var result = await db.SaveChangesAsync(token); - return result; - } + return result; + } - public async Task> GetPart(DateTimeOffset dateBegin, int take, CancellationToken token) - { - var query = GetQueryReadOnly(); - var entities = await query - .Where(e => e.Timestamp >= dateBegin) - .Take(take) - .ToArrayAsync(token); - var dtos = entities - .Select(e => e.Adapt()); + public async Task> GetPart(DateTimeOffset dateBegin, int take, CancellationToken token) + { + var query = GetQueryReadOnly(); + var entities = await query + .Where(e => e.Timestamp >= dateBegin) + .Take(take) + .ToArrayAsync(token); + var dtos = entities + .Select(e => e.Adapt()); - return dtos; - } + return dtos; + } - public async Task GetDatesRangeAsync(CancellationToken token) - { - var query = GetQueryReadOnly() - .GroupBy(e => 1) - .Select(group => new - { - Min = group.Min(e => e.Timestamp), - Max = group.Max(e => e.Timestamp), - }); - var values = await query.FirstOrDefaultAsync(token); - var result = new DatesRangeDto() - { - From = values?.Min ?? DateTimeOffset.MinValue, - To = values?.Max ?? DateTimeOffset.MaxValue - }; + public async Task GetDatesRangeAsync(CancellationToken token) + { + var query = GetQueryReadOnly() + .GroupBy(e => 1) + .Select(group => new + { + Min = group.Min(e => e.Timestamp), + Max = group.Max(e => e.Timestamp), + }); + var values = await query.FirstOrDefaultAsync(token); + var result = new DatesRangeDto() + { + From = values?.Min ?? DateTimeOffset.MinValue, + To = values?.Max ?? DateTimeOffset.MaxValue + }; - return result; - } + return result; + } - private async Task> GetDrillingSystems(CancellationToken token) - { - var systems = await memoryCache.GetOrCreateAsync(SystemCacheKey, async f => - { - f.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(CacheExpirationInMinutes); + private async Task> GetDrillingSystems(CancellationToken token) + { + var systems = await memoryCache.GetOrCreateAsync(SystemCacheKey, async f => + { + f.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(CacheExpirationInMinutes); - var query = db.Set(); - var entities = await query.ToListAsync(token); - var dtos = entities.Select(e => e.Adapt()); + var query = db.Set(); + var entities = await query.ToListAsync(token); + var dtos = entities.Select(e => e.Adapt()); - return dtos; - }); + return dtos; + }); - return systems!; - } - private async Task CreateDrillingSystem(string name, CancellationToken token) - { - memoryCache.Remove(SystemCacheKey); + return systems!; + } + private async Task CreateDrillingSystem(string name, CancellationToken token) + { + memoryCache.Remove(SystemCacheKey); - var entity = new Database.Entity.DrillingSystem() - { - SystemId = default, - Name = name.ToLower().Trim() - }; + var entity = new Database.Entity.DrillingSystem() + { + SystemId = default, + Name = name.ToLower().Trim() + }; - await db.Set().AddAsync(entity); - await db.SaveChangesAsync(token); + await db.Set().AddAsync(entity); + await db.SaveChangesAsync(token); - return entity.SystemId; - } - } + return entity.SystemId; + } + } } diff --git a/Persistence/API/IChangeLogApi.cs b/Persistence/API/IChangeLogApi.cs index bb4a3c3..25b5158 100644 --- a/Persistence/API/IChangeLogApi.cs +++ b/Persistence/API/IChangeLogApi.cs @@ -1,45 +1,71 @@ using Microsoft.AspNetCore.Mvc; using Persistence.Models; +using Persistence.Models.Requests; namespace Persistence.API; /// /// Интерфейс для работы с API журнала изменений /// -public interface IChangeLogApi - where TDto : class, new() - where TChangeLogDto : ChangeLogDto +public interface IChangeLogApi : ISyncWithDiscriminatorApi { /// - /// Получение исторических данных на текущую дату + /// Импорт с заменой: удаление старых строк и добавление новых /// + /// + /// /// /// - Task>> GetChangeLogCurrent(CancellationToken token); + Task ClearAndAddRange(Guid idDiscriminator, IEnumerable dtos, CancellationToken token); /// - /// Получение исторических данных на определенную дату + /// Получение данных на текущую дату (с пагинацией) /// - /// + /// + /// параметры запроса фильтрации + /// параметры запроса пагинации /// /// - Task>> GetChangeLogForDate(DateTimeOffset historyMoment, CancellationToken token); + Task GetCurrent(Guid idDiscriminator, SectionPartRequest filterRequest, PaginationRequest paginationRequest, CancellationToken token); + + /// + /// Получение данных на определенную дату (с пагинацией) + /// + /// + /// + /// параметры запроса фильтрации + /// параметры запроса пагинации + /// + /// + Task GetByDate(Guid idDiscriminator, DateTimeOffset moment, SectionPartRequest filterRequest, PaginationRequest paginationRequest, CancellationToken token); + + /// + /// Получение исторических данных за определенный период времени + /// + /// + /// + /// + /// + /// + Task GetChangeLogForDate(Guid idDiscriminator, DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token); /// /// Добавить одну запись /// + /// /// /// /// - Task> Add(TDto dto, CancellationToken token); + Task Add(Guid idDiscriminator, DataWithWellDepthAndSectionDto dto, CancellationToken token); /// /// Добавить несколько записей /// + /// /// /// /// - Task> AddRange(IEnumerable dtos, CancellationToken token); + Task AddRange(Guid idDiscriminator, IEnumerable dtos, CancellationToken token); /// /// Обновить одну запись @@ -47,7 +73,7 @@ public interface IChangeLogApi /// /// /// - Task> Update(TDto dto, CancellationToken token); + Task Update(DataWithWellDepthAndSectionDto dto, CancellationToken token); /// /// Обновить несколько записей @@ -55,7 +81,7 @@ public interface IChangeLogApi /// /// /// - Task> UpdateRange(IEnumerable dtos, CancellationToken token); + Task UpdateRange(IEnumerable dtos, CancellationToken token); /// /// Удалить одну запись @@ -63,7 +89,7 @@ public interface IChangeLogApi /// /// /// - Task> Delete(int id, CancellationToken token); + Task Delete(Guid id, CancellationToken token); /// /// Удалить несколько записей @@ -71,5 +97,13 @@ public interface IChangeLogApi /// /// /// - Task> DeleteRange(IEnumerable ids, CancellationToken token); + Task DeleteRange(IEnumerable ids, CancellationToken token); + + /// + /// Получение списка дат, в которые происходили изменения (день, месяц, год, без времени) + /// + /// + /// + /// + Task GetDatesChange(Guid idDiscriminator, CancellationToken token); } diff --git a/Persistence/API/ISyncWithDiscriminatorApi.cs b/Persistence/API/ISyncWithDiscriminatorApi.cs new file mode 100644 index 0000000..c0c72f0 --- /dev/null +++ b/Persistence/API/ISyncWithDiscriminatorApi.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Mvc; +using Persistence.Models; + +namespace Persistence.API; + +/// +/// Интерфейс для API, предназначенного для синхронизации данных, у которых есть дискриминатор +/// +public interface ISyncWithDiscriminatorApi +{ + /// + /// Получить порцию записей, начиная с заданной даты + /// + /// + /// + /// количество записей + /// + /// + Task GetPart(Guid idDiscriminator, DateTimeOffset dateBegin, int take = 24 * 60 * 60, CancellationToken token = default); + + /// + /// Получить диапазон дат, для которых есть данные в репозитории + /// + /// + /// + /// + Task GetDatesRangeAsync(Guid idDiscriminator, CancellationToken token); +} diff --git a/Persistence/API/ITableDataApi.cs b/Persistence/API/ITableDataApi.cs index 3707ab5..e5611a4 100644 --- a/Persistence/API/ITableDataApi.cs +++ b/Persistence/API/ITableDataApi.cs @@ -1,12 +1,13 @@ using Microsoft.AspNetCore.Mvc; using Persistence.Models; +using Persistence.Models.Requests; namespace Persistence.API; /// Интерфейс для API, предназначенного для работы с табличными данными public interface ITableDataApi where TDto : class, new() - where TRequest : RequestDto + where TRequest : PaginationRequest { /// /// Получить страницу списка объектов diff --git a/Persistence/EFExtensions.cs b/Persistence/EFExtensions.cs new file mode 100644 index 0000000..00474cc --- /dev/null +++ b/Persistence/EFExtensions.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace Persistence; +public static class EFExtensions +{ + struct TypeAcessor + { + public LambdaExpression KeySelector { get; set; } + public MethodInfo OrderBy { get; set; } + public MethodInfo OrderByDescending { get; set; } + public MethodInfo ThenBy { get; set; } + public MethodInfo ThenByDescending { get; set; } + } + private static readonly MethodInfo methodOrderBy = GetExtOrderMethod("OrderBy"); + + private static readonly MethodInfo methodOrderByDescending = GetExtOrderMethod("OrderByDescending"); + + private static readonly MethodInfo methodThenBy = GetExtOrderMethod("ThenBy"); + + private static readonly MethodInfo methodThenByDescending = GetExtOrderMethod("ThenByDescending"); + private static ConcurrentDictionary> TypePropSelectors { get; set; } = new(); + + private static MethodInfo GetExtOrderMethod(string methodName) + => typeof(System.Linq.Queryable) + .GetMethods() + .Where(m => m.Name == methodName && + m.IsGenericMethodDefinition && + m.GetParameters().Length == 2 && + m.GetParameters()[1].ParameterType.IsAssignableTo(typeof(LambdaExpression))) + .Single(); + private static Dictionary MakeTypeAcessors(Type type) + { + var propContainer = new Dictionary(); + var properties = type.GetProperties(); + foreach (var propertyInfo in properties) + { + var name = propertyInfo.Name.ToLower(); + ParameterExpression arg = Expression.Parameter(type, "x"); + MemberExpression property = Expression.Property(arg, propertyInfo.Name); + var selector = Expression.Lambda(property, new ParameterExpression[] { arg }); + var typeAccessor = new TypeAcessor + { + KeySelector = selector, + OrderBy = methodOrderBy.MakeGenericMethod(type, propertyInfo.PropertyType), + OrderByDescending = methodOrderByDescending.MakeGenericMethod(type, propertyInfo.PropertyType), + ThenBy = methodThenBy.MakeGenericMethod(type, propertyInfo.PropertyType), + ThenByDescending = methodThenByDescending.MakeGenericMethod(type, propertyInfo.PropertyType), + }; + + propContainer.Add(name, typeAccessor); + } + return propContainer; + } + + /// + /// Добавить в запрос сортировку по возрастанию или убыванию. + /// Этот метод сбросит ранее наложенные сортировки. + /// + /// + /// + /// + /// Свойство сортировки. + /// Состоит из названия свойства (в любом регистре) + /// и опционально указания направления сортировки "asc" или "desc" + /// + /// + /// var query = query("Date desc"); + /// + /// Запрос с примененной сортировкой + public static IOrderedQueryable SortBy( + this IQueryable query, + string propertySort) + { + var parts = propertySort.Split(" ", 2, StringSplitOptions.RemoveEmptyEntries); + var isDesc = parts.Length >= 2 && parts[1].ToLower().Trim() == "desc"; + var propertyName = parts[0]; + + var newQuery = query.SortBy(propertyName, isDesc); + return newQuery; + } + + /// + /// Добавить в запрос сортировку по возрастанию или убыванию + /// + /// + /// + /// Название свойства (в любом регистре) + /// Сортировать по убыванию + /// Запрос с примененной сортировкой + public static IOrderedQueryable SortBy( + this IQueryable query, + string propertyName, + bool isDesc) + { + var typePropSelector = TypePropSelectors.GetOrAdd(typeof(TSource), MakeTypeAcessors); + var propertyNamelower = propertyName.ToLower(); + var typeAccessor = typePropSelector[propertyNamelower]; + + var genericMethod = isDesc + ? typeAccessor.OrderByDescending + : typeAccessor.OrderBy; + + var newQuery = (IOrderedQueryable)genericMethod + .Invoke(genericMethod, new object[] { query, typeAccessor.KeySelector })!; + return newQuery; + } +} diff --git a/Persistence/Models/ChangeLogDto.cs b/Persistence/Models/ChangeLogDto.cs index 2e5bfff..d42d659 100644 --- a/Persistence/Models/ChangeLogDto.cs +++ b/Persistence/Models/ChangeLogDto.cs @@ -3,40 +3,40 @@ namespace Persistence.Models; /// /// Часть записи описывающая изменение /// -public class ChangeLogDto where T : class +public class ChangeLogDto { /// - /// Запись + /// Ключ записи /// - public required T Item { get; set; } + public Guid Id { get; set; } /// - /// Автор + /// Создатель записи /// - public UserDto? Author { get; set; } + public Guid IdAuthor { get; set; } /// - /// Автор + /// Пользователь, изменивший запись /// - public UserDto? Editor { get; set; } + public Guid? IdEditor { get; set; } /// - /// Дата создания записи + /// Дата создания /// public DateTimeOffset Creation { get; set; } /// - /// Дата устаревания (например, при удалении) + /// Дата устаревания /// public DateTimeOffset? Obsolete { get; set; } /// - /// Id состояния + /// Ключ заменившей записи /// - public int IdState { get; set; } + public Guid? IdNext { get; set; } /// - /// Id заменяемой записи + /// Объект записи /// - public int? IdPrevious { get; set; } + public DataWithWellDepthAndSectionDto Value { get; set; } = default!; } diff --git a/Persistence/Models/DataWithWellDepthAndSectionDto.cs b/Persistence/Models/DataWithWellDepthAndSectionDto.cs new file mode 100644 index 0000000..7939dd0 --- /dev/null +++ b/Persistence/Models/DataWithWellDepthAndSectionDto.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Persistence.Models; + +/// +/// Dto для хранения записей, содержащих начальную и конечную глубину забоя, а также секцию +/// +public class DataWithWellDepthAndSectionDto +{ + /// + /// Ключ записи + /// + public Guid Id { get; set; } + + /// + /// Глубина забоя на дату начала интервала + /// + public double DepthStart { get; set; } + + /// + /// Глубина забоя на дату окончания интервала + /// + public double DepthEnd { get; set; } + + /// + /// Ключ секции + /// + public Guid IdSection { get; set; } + + /// + /// Объект записи + /// + public required IDictionary Value { get; set; } +} diff --git a/Persistence/Models/IChangeLogAbstract.cs b/Persistence/Models/IChangeLogAbstract.cs deleted file mode 100644 index a23ab1b..0000000 --- a/Persistence/Models/IChangeLogAbstract.cs +++ /dev/null @@ -1,62 +0,0 @@ -namespace Persistence.Models; - -/// -/// Часть записи описывающая изменение -/// -public interface IChangeLogAbstract -{ - /// - /// Актуальная - /// - public const int IdStateActual = 0; - - /// - /// Замененная - /// - public const int IdStateReplaced = 1; - - /// - /// Удаленная - /// - public const int IdStateDeleted = 2; - - /// - /// Очищено при импорте - /// - public const int IdCleared = 3; - - /// - /// Ид записи - /// - public int Id { get; set; } - - /// - /// Автор изменения - /// - public int IdAuthor { get; set; } - - /// - /// Редактор - /// - public int? IdEditor { get; set; } - - /// - /// Дата создания записи - /// - public DateTimeOffset Creation { get; set; } - - /// - /// Дата устаревания (например при удалении) - /// - public DateTimeOffset? Obsolete { get; set; } - - /// - /// "ИД состояния записи: \n0 - актуальная\n1 - замененная\n2 - удаленная - /// - public int IdState { get; set; } - - /// - /// Id заменяемой записи - /// - public int? IdPrevious { get; set; } -} diff --git a/Persistence/Models/IWithSectionPart.cs b/Persistence/Models/IWithSectionPart.cs new file mode 100644 index 0000000..adb5fdd --- /dev/null +++ b/Persistence/Models/IWithSectionPart.cs @@ -0,0 +1,9 @@ +namespace Persistence.Models; +public interface IWithSectionPart +{ + public double DepthStart { get; set; } + + public double DepthEnd { get; set; } + + public Guid IdSection { get; set; } +} diff --git a/Persistence/Models/RequestDto.cs b/Persistence/Models/Requests/PaginationRequest.cs similarity index 51% rename from Persistence/Models/RequestDto.cs rename to Persistence/Models/Requests/PaginationRequest.cs index c61ac79..d9974cd 100644 --- a/Persistence/Models/RequestDto.cs +++ b/Persistence/Models/Requests/PaginationRequest.cs @@ -1,23 +1,25 @@ -namespace Persistence.Models; +namespace Persistence.Models.Requests; /// /// Контейнер для поддержки постраничного просмотра таблиц /// /// -public class RequestDto +public class PaginationRequest { /// /// Кол-во записей пропущенных с начала таблицы в запросе от api /// - public int Skip { get; set; } + public int Skip { get; set; } = 0; /// /// Кол-во записей в запросе от api /// - public int Take { get; set; } + public int Take { get; set; } = 32; /// - /// Настройки сортировки + /// Сортировки: + /// Содержат список названий полей сортировки + /// Указать направление сортировки можно через пробел "asc" или "desc" /// - public string SortSettings { get; set; } = string.Empty; + public string? SortSettings { get; set; } } diff --git a/Persistence/Models/Requests/SectionPartRequest.cs b/Persistence/Models/Requests/SectionPartRequest.cs new file mode 100644 index 0000000..3eca015 --- /dev/null +++ b/Persistence/Models/Requests/SectionPartRequest.cs @@ -0,0 +1,22 @@ +namespace Persistence.Models.Requests; + +/// +/// Запрос для фильтрации данных по секции и глубине +/// +public class SectionPartRequest +{ + /// + /// Глубина забоя на дату начала интервала + /// + public double? DepthStart { get; set; } + + /// + /// Глубина забоя на дату окончания интервала + /// + public double? DepthEnd { get; set; } + + /// + /// Ключ секции + /// + public Guid? IdSection { get; set; } +} diff --git a/Persistence/Repositories/IChangeLogRepository.cs b/Persistence/Repositories/IChangeLogRepository.cs index c12c61b..e197090 100644 --- a/Persistence/Repositories/IChangeLogRepository.cs +++ b/Persistence/Repositories/IChangeLogRepository.cs @@ -1,4 +1,5 @@ using Persistence.Models; +using Persistence.Models.Requests; namespace Persistence.Repositories; @@ -6,84 +7,81 @@ namespace Persistence.Repositories; /// Интерфейс для работы с историческими данными /// /// -public interface IChangeLogRepository : ISyncRepository - where TDto : class, ITimeSeriesAbstractDto, new() - where TChangeLogDto : ChangeLogDto +public interface IChangeLogRepository : ISyncWithDiscriminatorRepository { /// /// Добавление записей /// - /// пользователь, который добавляет + /// пользователь, который добавляет + /// ключ справочника /// /// /// - Task InsertRange(int idUser, IEnumerable dtos, CancellationToken token); - - /// - /// Редактирование записей - /// - /// пользователь, который редактирует - /// - /// - /// - Task UpdateRange(int idUser, IEnumerable dtos, CancellationToken token); - - /// - /// Добавляет Dto у которых id == 0, изменяет dto у которых id != 0 - /// - /// пользователь, который редактирует или добавляет - /// - /// - /// - Task UpdateOrInsertRange(int idUser, IEnumerable dtos, CancellationToken token); - - /// - /// Помечает записи как удаленные - /// - /// пользователь, который чистит - /// - /// - Task Clear(int idUser, CancellationToken token); - - /// - /// Очистить и добавить новые - /// - /// - /// - /// - /// - Task ClearAndInsertRange(int idUser, IEnumerable dtos, CancellationToken token); + Task AddRange(Guid idAuthor, Guid idDiscriminator, IEnumerable dtos, CancellationToken token); /// /// Пометить записи как удаленные /// - /// - /// + /// + /// ключи записей /// /// - Task MarkAsDeleted(int idUser, IEnumerable ids, CancellationToken token); + Task MarkAsDeleted(Guid idEditor, IEnumerable ids, CancellationToken token); /// - /// Получение дат изменений записей + /// Пометить записи как удаленные /// - /// + /// + /// дискриминатор таблицы /// /// - Task> GetDatesChange(CancellationToken token); + Task MarkAsDeleted(Guid idEditor, Guid idDiscriminator, CancellationToken token); /// - /// Получение измененных записей за определенную дату + /// Очистить и добавить новые /// - /// + /// + /// + /// /// /// - Task> GetChangeLogForDate(DateTimeOffset? updateFrom, CancellationToken token); + Task ClearAndAddRange(Guid idAuthor, Guid idDiscriminator, IEnumerable dtos, CancellationToken token); /// - /// Получение текущих сейчас записей по параметрам + /// Редактирование записей /// - /// + /// пользователь, который редактирует + /// /// /// - Task> GetCurrent(DateTimeOffset moment, CancellationToken token); + Task UpdateRange(Guid idEditor, IEnumerable dtos, CancellationToken token); + + /// + /// Получение актуальных записей на определенный момент времени (с пагинацией) + /// + /// + /// текущий момент времени + /// параметры запроса фильтрации + /// параметры запроса пагинации + /// + /// + Task> GetByDate(Guid idDiscriminator, DateTimeOffset moment, SectionPartRequest filterRequest, PaginationRequest paginationRequest, CancellationToken token); + + /// + /// Получение измененных записей за период времени + /// + /// + /// + /// + /// + /// + Task> GetChangeLogForInterval(Guid idDiscriminator, DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token); + + /// + /// Получение списка дат, в которые происходили изменения (день, месяц, год, без времени) + /// + /// + /// + /// + Task> GetDatesChange(Guid idDiscriminator, CancellationToken token); } diff --git a/Persistence/Repositories/ISyncRepository.cs b/Persistence/Repositories/ISyncRepository.cs index e18c5f5..5a5c9be 100644 --- a/Persistence/Repositories/ISyncRepository.cs +++ b/Persistence/Repositories/ISyncRepository.cs @@ -1,17 +1,25 @@ -namespace Persistence.Repositories; +using Persistence.Models; + +namespace Persistence.Repositories; /// /// Интерфейс по работе с данными /// /// public interface ISyncRepository - where TDto : class, new() { /// /// Получить данные, начиная с определенной даты /// /// дата начала + /// /// + Task> GetGtDate(DateTimeOffset dateBegin, CancellationToken token); + + + /// + /// Получить диапазон дат, для которых есть данные в репозитории + /// /// /// - Task> GetGtDate(DateTimeOffset dateBegin, CancellationToken token); + Task GetDatesRange(CancellationToken token); } diff --git a/Persistence/Repositories/ISyncWithDiscriminatorRepository.cs b/Persistence/Repositories/ISyncWithDiscriminatorRepository.cs new file mode 100644 index 0000000..7e0bd62 --- /dev/null +++ b/Persistence/Repositories/ISyncWithDiscriminatorRepository.cs @@ -0,0 +1,28 @@ +using Persistence.Models; + +namespace Persistence.Repositories; + +/// +/// Интерфейс по работе с данными, у которых есть дискриминатор +/// +/// +public interface ISyncWithDiscriminatorRepository +{ + /// + /// Получить данные, начиная с определенной даты + /// + /// дискриминатор таблицы + /// дата начала + /// + /// /// + Task> GetGtDate(Guid idDiscriminator, DateTimeOffset dateBegin, CancellationToken token); + + + /// + /// Получить диапазон дат, для которых есть данные в репозитории + /// + /// дискриминатор таблицы + /// + /// + Task GetDatesRange(Guid idDiscriminator, CancellationToken token); +} diff --git a/Persistence/Repositories/ITableDataRepository.cs b/Persistence/Repositories/ITableDataRepository.cs index 73d332d..0ca1715 100644 --- a/Persistence/Repositories/ITableDataRepository.cs +++ b/Persistence/Repositories/ITableDataRepository.cs @@ -1,4 +1,4 @@ -using Persistence.Models; +using Persistence.Models.Requests; namespace Persistence.Repositories; @@ -7,7 +7,7 @@ namespace Persistence.Repositories; /// public interface ITableDataRepository where TDto : class, new() - where TRequest : RequestDto + where TRequest : PaginationRequest { /// /// Получить страницу списка объектов diff --git a/Persistence/Repositories/ITechMessagesRepository.cs b/Persistence/Repositories/ITechMessagesRepository.cs index a6cb7d3..ae92912 100644 --- a/Persistence/Repositories/ITechMessagesRepository.cs +++ b/Persistence/Repositories/ITechMessagesRepository.cs @@ -1,4 +1,5 @@ using Persistence.Models; +using Persistence.Models.Requests; namespace Persistence.Repositories { @@ -13,7 +14,7 @@ namespace Persistence.Repositories /// /// /// - Task> GetPage(RequestDto request, CancellationToken token); + Task> GetPage(PaginationRequest request, CancellationToken token); /// /// Добавление новых сообщений diff --git a/Persistence/Repositories/ITimeSeriesBaseRepository.cs b/Persistence/Repositories/ITimeSeriesBaseRepository.cs index 11203c8..74cc545 100644 --- a/Persistence/Repositories/ITimeSeriesBaseRepository.cs +++ b/Persistence/Repositories/ITimeSeriesBaseRepository.cs @@ -19,11 +19,4 @@ public interface ITimeSeriesBaseRepository double intervalSec = 600d, int approxPointsCount = 1024, CancellationToken token = default); - - /// - /// Получить диапазон дат, для которых есть данные в репозитории - /// - /// - /// - Task GetDatesRange(CancellationToken token); }