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 d2691c1..a7b0094 100644 --- a/Persistence.API/Controllers/TechMessagesController.cs +++ b/Persistence.API/Controllers/TechMessagesController.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Persistence.Models; +using Persistence.Models.Requests; using Persistence.Repositories; namespace Persistence.API.Controllers; @@ -36,7 +37,7 @@ public class TechMessagesController : ControllerBase /// /// [HttpGet] - public async Task>> GetPage([FromQuery] RequestDto request, CancellationToken token) + public async Task>> GetPage([FromQuery] PaginationRequest request, CancellationToken token) { var result = await techMessagesRepository.GetPage(request, token); diff --git a/Persistence.API/DependencyInjection.cs b/Persistence.API/DependencyInjection.cs index 19cedc9..bf297a6 100644 --- a/Persistence.API/DependencyInjection.cs +++ b/Persistence.API/DependencyInjection.cs @@ -1,5 +1,3 @@ -using System.Reflection; -using System.Text.Json.Nodes; using Mapster; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; @@ -9,17 +7,19 @@ using Persistence.Database.Entity; using Persistence.Models; using Persistence.Models.Configurations; using Swashbuckle.AspNetCore.SwaggerGen; +using System.Reflection; +using System.Text.Json.Nodes; namespace Persistence.API; public static class DependencyInjection { - public static void MapsterSetup() - { - TypeAdapterConfig.GlobalSettings.Default.Config - .ForType() - .Ignore(dest => dest.System, dest => dest.SystemId); - } + public static void MapsterSetup() + { + TypeAdapterConfig.GlobalSettings.Default.Config + .ForType() + .Ignore(dest => dest.System, dest => dest.SystemId); + } public static void AddSwagger(this IServiceCollection services, IConfiguration configuration) { services.AddSwaggerGen(c => @@ -43,162 +43,162 @@ public static class DependencyInjection c.SwaggerDoc("v1", new OpenApiInfo { Title = "Persistence web api", Version = "v1" }); - var needUseKeyCloak = configuration.GetSection("NeedUseKeyCloak").Get(); - if (needUseKeyCloak) - c.AddKeycloackSecurity(configuration); - else c.AddDefaultSecurity(configuration); + var needUseKeyCloak = configuration.GetSection("NeedUseKeyCloak").Get(); + if (needUseKeyCloak) + c.AddKeycloackSecurity(configuration); + else c.AddDefaultSecurity(configuration); - var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; - var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); - var includeControllerXmlComment = true; - c.IncludeXmlComments(xmlPath, includeControllerXmlComment); - }); + var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + var includeControllerXmlComment = true; + c.IncludeXmlComments(xmlPath, includeControllerXmlComment); + }); } - #region Authentication - public static void AddJWTAuthentication(this IServiceCollection services, IConfiguration configuration) + #region Authentication + public static void AddJWTAuthentication(this IServiceCollection services, IConfiguration configuration) { var needUseKeyCloak = configuration - .GetSection("NeedUseKeyCloak") - .Get(); - if (needUseKeyCloak) - services.AddKeyCloakAuthentication(configuration); - else services.AddDefaultAuthentication(configuration); - } + .GetSection("NeedUseKeyCloak") + .Get(); + if (needUseKeyCloak) + services.AddKeyCloakAuthentication(configuration); + else services.AddDefaultAuthentication(configuration); + } - private static void AddKeyCloakAuthentication(this IServiceCollection services, IConfiguration configuration) - { - services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) - .AddJwtBearer(options => - { - options.RequireHttpsMetadata = false; - options.Audience = configuration["Authentication:Audience"]; - options.MetadataAddress = configuration["Authentication:MetadataAddress"]!; - options.TokenValidationParameters = new TokenValidationParameters - { - ValidIssuer = configuration["Authentication:ValidIssuer"], - }; - }); - } + private static void AddKeyCloakAuthentication(this IServiceCollection services, IConfiguration configuration) + { + services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.RequireHttpsMetadata = false; + options.Audience = configuration["Authentication:Audience"]; + options.MetadataAddress = configuration["Authentication:MetadataAddress"]!; + options.TokenValidationParameters = new TokenValidationParameters + { + ValidIssuer = configuration["Authentication:ValidIssuer"], + }; + }); + } - private static void AddDefaultAuthentication(this IServiceCollection services, IConfiguration configuration) - { - services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) - .AddJwtBearer(options => - { - options.RequireHttpsMetadata = false; - options.TokenValidationParameters = new TokenValidationParameters - { - ValidateIssuer = true, - ValidIssuer = JwtParams.Issuer, - ValidateAudience = true, - ValidAudience = JwtParams.Audience, - ValidateLifetime = true, - IssuerSigningKey = JwtParams.SecurityKey, - ValidateIssuerSigningKey = false - }; - options.Events = new JwtBearerEvents - { - OnMessageReceived = context => - { - var accessToken = context.Request.Headers["Authorization"] - .ToString() - .Replace(JwtBearerDefaults.AuthenticationScheme, string.Empty) - .Trim(); + private static void AddDefaultAuthentication(this IServiceCollection services, IConfiguration configuration) + { + services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.RequireHttpsMetadata = false; + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = JwtParams.Issuer, + ValidateAudience = true, + ValidAudience = JwtParams.Audience, + ValidateLifetime = true, + IssuerSigningKey = JwtParams.SecurityKey, + ValidateIssuerSigningKey = false, + }; + options.Events = new JwtBearerEvents + { + OnMessageReceived = context => + { + var accessToken = context.Request.Headers["Authorization"] + .ToString() + .Replace(JwtBearerDefaults.AuthenticationScheme, string.Empty) + .Trim(); - context.Token = accessToken; + context.Token = accessToken; - return Task.CompletedTask; - }, - OnTokenValidated = context => - { - var username = context.Principal?.Claims - .FirstOrDefault(e => e.Type == "username")?.Value; + return Task.CompletedTask; + }, + OnTokenValidated = context => + { + var username = context.Principal?.Claims + .FirstOrDefault(e => e.Type == "username")?.Value; - var password = context.Principal?.Claims - .FirstOrDefault(e => e.Type == "password")?.Value; + var password = context.Principal?.Claims + .FirstOrDefault(e => e.Type == "password")?.Value; - var keyCloakUser = configuration - .GetSection(nameof(AuthUser)) - .Get()!; + var keyCloakUser = configuration + .GetSection(nameof(AuthUser)) + .Get()!; - if (username != keyCloakUser.Username || password != keyCloakUser.Password) - { - context.Fail("username or password did not match"); - } + if (username != keyCloakUser.Username || password != keyCloakUser.Password) + { + context.Fail("username or password did not match"); + } - return Task.CompletedTask; - } - }; - }); - } - #endregion + return Task.CompletedTask; + } + }; + }); + } + #endregion - #region Security (Swagger) - private static void AddKeycloackSecurity(this SwaggerGenOptions options, IConfiguration configuration) - { - 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", - In = ParameterLocation.Header, - Type = SecuritySchemeType.OAuth2, - Flows = new OpenApiOAuthFlows - { - Implicit = new OpenApiOAuthFlow - { - AuthorizationUrl = new Uri(configuration["Authentication:AuthorizationUrl"]), - } - } - }); + #region Security (Swagger) + private static void AddKeycloackSecurity(this SwaggerGenOptions options, IConfiguration configuration) + { + 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", + In = ParameterLocation.Header, + Type = SecuritySchemeType.OAuth2, + Flows = new OpenApiOAuthFlows + { + Implicit = new OpenApiOAuthFlow + { + AuthorizationUrl = new Uri(configuration["Authentication:AuthorizationUrl"]), + } + } + }); - options.AddSecurityRequirement(new OpenApiSecurityRequirement() - { - { - new OpenApiSecurityScheme - { - Reference = new OpenApiReference - { - Type = ReferenceType.SecurityScheme, - Id = "Keycloack" - }, - Scheme = "Bearer", - Name = "Bearer", - In = ParameterLocation.Header, - }, - new List() - } - }); - } + options.AddSecurityRequirement(new OpenApiSecurityRequirement() + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Keycloack" + }, + Scheme = "Bearer", + Name = "Bearer", + In = ParameterLocation.Header, + }, + new List() + } + }); + } - private static void AddDefaultSecurity(this SwaggerGenOptions options, IConfiguration configuration) - { - options.AddSecurityDefinition("Bearer", 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", - In = ParameterLocation.Header, - Type = SecuritySchemeType.ApiKey, - Scheme = "Bearer", - }); + private static void AddDefaultSecurity(this SwaggerGenOptions options, IConfiguration configuration) + { + options.AddSecurityDefinition("Bearer", 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", + In = ParameterLocation.Header, + Type = SecuritySchemeType.ApiKey, + Scheme = "Bearer", + }); - options.AddSecurityRequirement(new OpenApiSecurityRequirement() - { - { - new OpenApiSecurityScheme - { - Reference = new OpenApiReference - { - Type = ReferenceType.SecurityScheme, - Id = "Bearer" - }, - Scheme = "oauth2", - Name = "Bearer", - In = ParameterLocation.Header, - }, - new List() - } - }); - } - #endregion + options.AddSecurityRequirement(new OpenApiSecurityRequirement() + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + }, + Scheme = "oauth2", + Name = "Bearer", + In = ParameterLocation.Header, + }, + new List() + } + }); + } + #endregion } diff --git a/Persistence.API/Startup.cs b/Persistence.API/Startup.cs index 98ad4aa..8fe79ee 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 878c6cf..a9c80c1 100644 --- a/Persistence.Client/Clients/ITechMessagesClient.cs +++ b/Persistence.Client/Clients/ITechMessagesClient.cs @@ -1,4 +1,5 @@ using Persistence.Models; +using Persistence.Models.Requests; using Refit; namespace Persistence.Client.Clients @@ -11,7 +12,7 @@ namespace Persistence.Client.Clients private const string BaseRoute = "/api/techMessages"; [Get($"{BaseRoute}")] - Task>> GetPage([Query] RequestDto request, CancellationToken token); + Task>> GetPage([Query] PaginationRequest request, CancellationToken token); [Post($"{BaseRoute}")] Task> AddRange([Body] IEnumerable dtos, CancellationToken token); diff --git a/Persistence.Client/Helpers/ApiTokenHelper.cs b/Persistence.Client/Helpers/ApiTokenHelper.cs index 5eed66e..d829fcf 100644 --- a/Persistence.Client/Helpers/ApiTokenHelper.cs +++ b/Persistence.Client/Helpers/ApiTokenHelper.cs @@ -36,7 +36,8 @@ public static class ApiTokenHelper new("client_id", authUser.ClientId), new("username", authUser.Username), new("password", authUser.Password), - new("grant_type", authUser.GrantType) + new("grant_type", authUser.GrantType), + new(ClaimTypes.NameIdentifier.ToString(), Guid.NewGuid().ToString()) }; var tokenDescriptor = new SecurityTokenDescriptor 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 89b09db..80a9e25 100644 --- a/Persistence.Database.Postgres/PersistenceDbContext.cs +++ b/Persistence.Database.Postgres/PersistenceDbContext.cs @@ -1,23 +1,22 @@ using Microsoft.EntityFrameworkCore; -using Npgsql; using Persistence.Database.Entity; -using System.Data.Common; namespace Persistence.Database.Model; public partial class PersistenceDbContext : DbContext { public DbSet DataSaub => Set(); + public DbSet ChangeLog => Set(); - public DbSet Setpoint => Set(); + public DbSet Setpoint => Set(); - public DbSet TechMessage => Set(); + public DbSet TechMessage => Set(); - public DbSet TimestampedSets => Set(); + public DbSet TimestampedSets => Set(); public PersistenceDbContext() : base() { - + } public PersistenceDbContext(DbContextOptions options) @@ -49,7 +48,11 @@ public partial class PersistenceDbContext : DbContext .WithMany() .HasForeignKey(t => t.SystemId) .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - } + .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/Entity/ITimestampedData.cs b/Persistence.Database/Entity/ITimestampedData.cs index 9241ca5..ce21da5 100644 --- a/Persistence.Database/Entity/ITimestampedData.cs +++ b/Persistence.Database/Entity/ITimestampedData.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Persistence.Database.Model; +namespace Persistence.Database.Model; public interface ITimestampedData { /// 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 1f194ad..291991f 100644 --- a/Persistence.IntegrationTests/Controllers/TechMessagesControllerTest.cs +++ b/Persistence.IntegrationTests/Controllers/TechMessagesControllerTest.cs @@ -5,6 +5,7 @@ using Persistence.Client; using Persistence.Client.Clients; using Persistence.Database.Entity; using Persistence.Models; +using Persistence.Models.Requests; using Xunit; namespace Persistence.IntegrationTests.Controllers @@ -32,7 +33,7 @@ namespace Persistence.IntegrationTests.Controllers dbContext.CleanupDbSet(); dbContext.CleanupDbSet(); - var requestDto = new RequestDto() + var PaginationRequest = new PaginationRequest() { Skip = 1, Take = 2, @@ -40,14 +41,14 @@ namespace Persistence.IntegrationTests.Controllers }; //act - var response = await techMessagesClient.GetPage(requestDto, new CancellationToken()); + 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.Equal(PaginationRequest.Skip, response.Content.Skip); + Assert.Equal(PaginationRequest.Take, response.Content.Take); } [Fact] @@ -56,7 +57,7 @@ namespace Persistence.IntegrationTests.Controllers //arrange var dtos = await InsertRange(); var dtosCount = dtos.Count(); - var requestDto = new RequestDto() + var PaginationRequest = new PaginationRequest() { Skip = 0, Take = 2, @@ -64,7 +65,7 @@ namespace Persistence.IntegrationTests.Controllers }; //act - var response = await techMessagesClient.GetPage(requestDto, new CancellationToken()); + var response = await techMessagesClient.GetPage(PaginationRequest, new CancellationToken()); //assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); diff --git a/Persistence.Repository/DependencyInjection.cs b/Persistence.Repository/DependencyInjection.cs index e353d30..1749271 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 c838619..d2efbe4 100644 --- a/Persistence.Repository/Repositories/TechMessagesRepository.cs +++ b/Persistence.Repository/Repositories/TechMessagesRepository.cs @@ -3,6 +3,7 @@ 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; @@ -24,7 +25,7 @@ namespace Persistence.Repository.Repositories protected virtual IQueryable GetQueryReadOnly() => db.Set() .Include(e => e.System); - public async Task> GetPage(RequestDto request, CancellationToken token) + public async Task> GetPage(PaginationRequest request, CancellationToken token) { var query = GetQueryReadOnly(); var count = await query.CountAsync(token); 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 a310799..e5611a4 100644 --- a/Persistence/API/ITableDataApi.cs +++ b/Persistence/API/ITableDataApi.cs @@ -1,17 +1,13 @@ using Microsoft.AspNetCore.Mvc; using Persistence.Models; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +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 6b2a090..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/AbstractChangeLogRepository.cs b/Persistence/Repositories/AbstractChangeLogRepository.cs deleted file mode 100644 index d7b1cbc..0000000 --- a/Persistence/Repositories/AbstractChangeLogRepository.cs +++ /dev/null @@ -1,165 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Persistence.Models; -using System.Linq; - -namespace Persistence.Repositories; -//public abstract class AbstractChangeLogRepository : IChangeLogRepository -// where TDto : class, new() -// where TEntity : class, IChangeLogAbstract -// where TChangeLogDto : ChangeLogDto -//{ -// private readonly DbContext dbContext; - -// protected AbstractChangeLogRepository(DbContext dbContext) -// { -// this.dbContext = dbContext; -// } - -// public abstract TEntity Convert(TDto entity); -// public async Task Clear(int idUser,CancellationToken token) -// { -// throw new NotImplementedException(); - -// //var updateTime = DateTimeOffset.UtcNow; - -// ////todo -// //var query = BuildQuery(request); -// //query = query.Where(e => e.Obsolete == null); - -// //var entitiesToDelete = await query.ToArrayAsync(token); - -// //foreach (var entity in entitiesToDelete) -// //{ -// // entity.IdState = IChangeLogAbstract.IdCleared; -// // entity.Obsolete = updateTime; -// // entity.IdEditor = idUser; -// //} - -// //var result = await SaveChangesWithExceptionHandling(token); -// //return result; -// } - -// public async Task ClearAndInsertRange(int idUser, IEnumerable dtos, CancellationToken token) -// { -// var result = 0; -// using var transaction = await dbContext.Database.BeginTransactionAsync(token); -// try -// { -// result += await Clear(idUser, token); -// result += await InsertRangeWithoutTransaction(idUser, dtos, token); - -// await transaction.CommitAsync(token); -// return result; -// } -// catch -// { -// await transaction.RollbackAsync(token); -// throw; -// } -// } - -// public Task> GetCurrent(DateTimeOffset moment, CancellationToken token) -// { -// throw new NotImplementedException(); -// } - -// public Task> GetDatesChange(CancellationToken token) -// { -// throw new NotImplementedException(); -// } - -// public Task> GetGtDate(DateTimeOffset date, CancellationToken token) -// { -// throw new NotImplementedException(); -// } - -// public async Task AddRange(int idUser, IEnumerable dtos, CancellationToken token) -// { -// using var transaction = dbContext.Database.BeginTransaction(); -// try -// { -// var result = await InsertRangeWithoutTransaction(idUser, dtos, token); -// await transaction.CommitAsync(token); -// return result; -// } -// catch -// { -// await transaction.RollbackAsync(token); -// throw; -// } -// } - -// protected abstract DatabaseFacade GetDataBase(); - -// public Task MarkAsDeleted(int idUser, IEnumerable ids, CancellationToken token) -// { -// throw new NotImplementedException(); -// } - -// public Task UpdateOrInsertRange(int idUser, IEnumerable dtos, CancellationToken token) -// { -// throw new NotImplementedException(); -// } - -// public Task UpdateRange(int idUser, IEnumerable dtos, CancellationToken token) -// { -// throw new NotImplementedException(); -// } - -// public Task> GetChangeLogForDate(DateTimeOffset? updateFrom, CancellationToken token) -// { -// throw new NotImplementedException(); -// } - -// private async Task InsertRangeWithoutTransaction(int idUser, IEnumerable dtos, CancellationToken token) -// { -// var result = 0; -// if (dtos.Any()) -// { -// var entities = dtos.Select(Convert); -// var creation = DateTimeOffset.UtcNow; -// var dbSet = dbContext.Set(); -// foreach (var entity in entities) -// { -// entity.Id = default; -// entity.IdAuthor = idUser; -// entity.Creation = creation; -// entity.IdState = IChangeLogAbstract.IdStateActual; -// entity.IdEditor = null; -// entity.IdPrevious = null; -// entity.Obsolete = null; -// dbSet.Add(entity); -// } - -// result += await SaveChangesWithExceptionHandling(token); -// } - -// return result; -// } - -// private async Task SaveChangesWithExceptionHandling(CancellationToken token) -// { -// var result = await dbContext.SaveChangesAsync(token); -// return result; -// //try -// //{ -// // var result = await dbContext.SaveChangesAsync(token); -// // return result; -// //} -// //catch (DbUpdateException ex) -// //{ -// // if (ex.InnerException is PostgresException pgException) -// // TryConvertPostgresExceptionToValidateException(pgException); -// // throw; -// //} -// } - - - -// //private static void TryConvertPostgresExceptionToValidateException(PostgresException pgException) -// //{ -// // if (pgException.SqlState == PostgresErrorCodes.ForeignKeyViolation) -// // throw new ArgumentInvalidException("dtos", pgException.Message + "\r\n" + pgException.Detail); -// //} -//} 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 92e8f70..ae92912 100644 --- a/Persistence/Repositories/ITechMessagesRepository.cs +++ b/Persistence/Repositories/ITechMessagesRepository.cs @@ -1,59 +1,59 @@ -using System.Threading.Tasks; -using Persistence.Models; +using Persistence.Models; +using Persistence.Models.Requests; namespace Persistence.Repositories { - /// - /// Интерфейс по работе с технологическими сообщениями - /// - public interface ITechMessagesRepository - { - /// - /// Получить страницу списка объектов - /// - /// - /// - /// - Task> GetPage(RequestDto request, CancellationToken token); + /// + /// Интерфейс по работе с технологическими сообщениями + /// + public interface ITechMessagesRepository + { + /// + /// Получить страницу списка объектов + /// + /// + /// + /// + Task> GetPage(PaginationRequest request, CancellationToken token); - /// - /// Добавление новых сообщений - /// - /// - /// - /// - Task AddRange(IEnumerable dtos, Guid userId, CancellationToken token); + /// + /// Добавление новых сообщений + /// + /// + /// + /// + Task AddRange(IEnumerable dtos, Guid userId, CancellationToken token); - /// - /// Получение списка уникальных названий систем АБ - /// - /// - /// - Task> GetSystems(CancellationToken token); + /// + /// Получение списка уникальных названий систем АБ + /// + /// + /// + Task> GetSystems(CancellationToken token); - /// - /// Получение количества сообщений по категориям и системам автобурения - /// - /// Id Категории важности - /// Система автобурения - /// - /// - Task> GetStatistics(IEnumerable autoDrillingSystem, IEnumerable categoryIds, CancellationToken token); + /// + /// Получение количества сообщений по категориям и системам автобурения + /// + /// Id Категории важности + /// Система автобурения + /// + /// + Task> GetStatistics(IEnumerable autoDrillingSystem, IEnumerable categoryIds, CancellationToken token); - /// - /// Получить порцию записей, начиная с заданной даты - /// - /// - /// - /// - /// - Task> GetPart(DateTimeOffset dateBegin, int take, CancellationToken token); + /// + /// Получить порцию записей, начиная с заданной даты + /// + /// + /// + /// + /// + Task> GetPart(DateTimeOffset dateBegin, int take, CancellationToken token); - /// - /// Получить диапазон дат, для которых есть данные в репозитории - /// - /// - /// - Task GetDatesRangeAsync(CancellationToken token); - } + /// + /// Получить диапазон дат, для которых есть данные в репозитории + /// + /// + /// + Task GetDatesRangeAsync(CancellationToken token); + } } diff --git a/Persistence/Repositories/ITimeSeriesBaseRepository.cs b/Persistence/Repositories/ITimeSeriesBaseRepository.cs index 7102d97..26edf2f 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); }