diff --git a/Persistence.API/Controllers/SetpointController.cs b/Persistence.API/Controllers/SetpointController.cs new file mode 100644 index 0000000..14f966c --- /dev/null +++ b/Persistence.API/Controllers/SetpointController.cs @@ -0,0 +1,51 @@ +using Microsoft.AspNetCore.Mvc; +using Persistence.Models; +using Persistence.Repositories; + +namespace Persistence.API.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class SetpointController : ControllerBase, ISetpointApi + { + private readonly ISetpointRepository setpointRepository; + + public SetpointController(ISetpointRepository setpointRepository) + { + this.setpointRepository = setpointRepository; + } + + [HttpGet("current")] + public async Task>> GetCurrent([FromQuery] IEnumerable setpointKeys, CancellationToken token) + { + var result = await setpointRepository.GetCurrent(setpointKeys, token); + + return Ok(result); + } + + [HttpGet("history")] + public async Task>> GetHistory([FromQuery] IEnumerable setpointKeys, [FromQuery] DateTimeOffset historyMoment, CancellationToken token) + { + var result = await setpointRepository.GetHistory(setpointKeys, historyMoment, token); + + return Ok(result); + } + + [HttpGet("log")] + public async Task>>> GetLog([FromQuery] IEnumerable setpointKeys, CancellationToken token) + { + var result = await setpointRepository.GetLog(setpointKeys, token); + + return Ok(result); + } + + [HttpPost] + public async Task> Save(Guid setpointKey, object newValue, CancellationToken token) + { + // ToDo: вычитка idUser + await setpointRepository.Save(setpointKey, newValue, 0, token); + + return Ok(); + } + } +} diff --git a/Persistence.Database.Postgres/Migrations/20241118052225_SetpointMigration.Designer.cs b/Persistence.Database.Postgres/Migrations/20241118052225_SetpointMigration.Designer.cs new file mode 100644 index 0000000..9399dd4 --- /dev/null +++ b/Persistence.Database.Postgres/Migrations/20241118052225_SetpointMigration.Designer.cs @@ -0,0 +1,146 @@ +// +using System; +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("20241118052225_SetpointMigration")] + partial class SetpointMigration + { + /// + 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.DataSaub", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + 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("TimeStamp") + .HasColumnType("integer") + .HasColumnName("timestamp"); + + b.Property("User") + .HasColumnType("text") + .HasColumnName("user"); + + b.Property("WellDepth") + .HasColumnType("double precision") + .HasColumnName("wellDepth"); + + b.HasKey("Id"); + + b.ToTable("DataSaub"); + }); + + modelBuilder.Entity("Persistence.Database.Model.Setpoint", b => + { + b.Property("Key") + .HasColumnType("uuid") + .HasComment("Ключ"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasComment("Дата изменения уставки"); + + b.Property("IdUser") + .HasColumnType("integer") + .HasComment("Id автора последнего изменения"); + + b.Property("Value") + .IsRequired() + .HasColumnType("jsonb") + .HasComment("Значение уставки"); + + b.HasKey("Key", "Created"); + + b.ToTable("Setpoint"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Persistence.Database.Postgres/Migrations/20241118052225_SetpointMigration.cs b/Persistence.Database.Postgres/Migrations/20241118052225_SetpointMigration.cs new file mode 100644 index 0000000..49e438a --- /dev/null +++ b/Persistence.Database.Postgres/Migrations/20241118052225_SetpointMigration.cs @@ -0,0 +1,36 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.Database.Postgres.Migrations +{ + /// + public partial class SetpointMigration : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Setpoint", + columns: table => new + { + Key = table.Column(type: "uuid", nullable: false, comment: "Ключ"), + Created = table.Column(type: "timestamp with time zone", nullable: false, comment: "Дата изменения уставки"), + Value = table.Column(type: "jsonb", nullable: false, comment: "Значение уставки"), + IdUser = table.Column(type: "integer", nullable: false, comment: "Id автора последнего изменения") + }, + constraints: table => + { + table.PrimaryKey("PK_Setpoint", x => new { x.Key, x.Created }); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Setpoint"); + } + } +} diff --git a/Persistence.Database.Postgres/Migrations/PersistenceDbContextModelSnapshot.cs b/Persistence.Database.Postgres/Migrations/PersistenceDbContextModelSnapshot.cs index 394c112..f41f669 100644 --- a/Persistence.Database.Postgres/Migrations/PersistenceDbContextModelSnapshot.cs +++ b/Persistence.Database.Postgres/Migrations/PersistenceDbContextModelSnapshot.cs @@ -106,6 +106,30 @@ namespace Persistence.Database.Postgres.Migrations b.ToTable("DataSaub"); }); + + modelBuilder.Entity("Persistence.Database.Model.Setpoint", b => + { + b.Property("Key") + .HasColumnType("uuid") + .HasComment("Ключ"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasComment("Дата изменения уставки"); + + b.Property("IdUser") + .HasColumnType("integer") + .HasComment("Id автора последнего изменения"); + + b.Property("Value") + .IsRequired() + .HasColumnType("jsonb") + .HasComment("Значение уставки"); + + b.HasKey("Key", "Created"); + + b.ToTable("Setpoint"); + }); #pragma warning restore 612, 618 } } diff --git a/Persistence.Database.Postgres/PersistenceDbContext.cs b/Persistence.Database.Postgres/PersistenceDbContext.cs index f68be73..58d201c 100644 --- a/Persistence.Database.Postgres/PersistenceDbContext.cs +++ b/Persistence.Database.Postgres/PersistenceDbContext.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using System.Data.Common; namespace Persistence.Database.Model; @@ -6,6 +6,8 @@ public partial class PersistenceDbContext : DbContext, IPersistenceDbContext { public DbSet DataSaub => Set(); + public DbSet Setpoint => Set(); + public PersistenceDbContext() : base() { diff --git a/Persistence.Database/Entity/Setpoint.cs b/Persistence.Database/Entity/Setpoint.cs new file mode 100644 index 0000000..ef6b5dc --- /dev/null +++ b/Persistence.Database/Entity/Setpoint.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore; + +namespace Persistence.Database.Model +{ + [PrimaryKey(nameof(Key), nameof(Created))] + public class Setpoint + { + [Comment("Ключ")] + public Guid Key { get; set; } + + [Column(TypeName = "jsonb"), Comment("Значение уставки")] + public required object Value { get; set; } + + [Comment("Дата создания уставки")] + public DateTimeOffset Created { get; set; } + + [Comment("Id автора последнего изменения")] + public int IdUser { get; set; } + } +} diff --git a/Persistence.Database/Model/IPersistenceDbContext.cs b/Persistence.Database/Model/IPersistenceDbContext.cs new file mode 100644 index 0000000..2c1aebb --- /dev/null +++ b/Persistence.Database/Model/IPersistenceDbContext.cs @@ -0,0 +1,17 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using System; +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Persistence.Database.Model; +public interface IPersistenceDbContext : IDisposable +{ + DbSet DataSaub { get; } + DbSet Setpoint { get; } + DatabaseFacade Database { get; } + Task SaveChangesAsync(CancellationToken cancellationToken); +} diff --git a/Persistence.IntegrationTests/Clients/ISetpointClient.cs b/Persistence.IntegrationTests/Clients/ISetpointClient.cs new file mode 100644 index 0000000..a4f79b5 --- /dev/null +++ b/Persistence.IntegrationTests/Clients/ISetpointClient.cs @@ -0,0 +1,25 @@ +using Persistence.Models; +using Refit; + +namespace Persistence.IntegrationTests.Clients +{ + /// + /// Интерфейс для тестирования API, предназначенного для работы с уставками + /// + public interface ISetpointClient + { + private const string BaseRoute = "/api/setpoint"; + + [Get($"{BaseRoute}/current")] + Task>> GetCurrent([Query(CollectionFormat.Multi)] IEnumerable setpointKeys); + + [Get($"{BaseRoute}/history")] + Task>> GetHistory([Query(CollectionFormat.Multi)] IEnumerable setpointKeys, [Query] DateTimeOffset historyMoment); + + [Get($"{BaseRoute}/log")] + Task>>> GetLog([Query(CollectionFormat.Multi)] IEnumerable setpointKeys); + + [Post($"{BaseRoute}/")] + Task Save(Guid setpointKey, object newValue); + } +} diff --git a/Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs b/Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs new file mode 100644 index 0000000..a33c15d --- /dev/null +++ b/Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs @@ -0,0 +1,153 @@ +using System.Net; +using Persistence.IntegrationTests.Clients; +using Xunit; + +namespace Persistence.IntegrationTests.Controllers +{ + public class SetpointControllerTest : BaseIntegrationTest + { + private ISetpointClient client; + private class TestObject + { + public string? value1 { get; set; } + public int? value2 { get; set; } + } + public SetpointControllerTest(WebAppFactoryFixture factory) : base(factory) + { + client = factory.GetHttpClient(string.Empty); + } + + [Fact] + public async Task GetCurrent_returns_success() + { + //arrange + var setpointKeys = new List() + { + Guid.NewGuid(), + Guid.NewGuid() + }; + + //act + var response = await client.GetCurrent(setpointKeys); + + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + Assert.Empty(response.Content); + } + + [Fact] + public async Task GetCurrent_AfterSave_returns_success() + { + //arrange + var setpointKey = await Save(); + + //act + var response = await client.GetCurrent([setpointKey]); + + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + Assert.NotEmpty(response.Content); + Assert.Equal(setpointKey, response.Content.FirstOrDefault()?.Key); + } + + [Fact] + public async Task GetHistory_returns_success() + { + //arrange + var setpointKeys = new List() + { + Guid.NewGuid(), + Guid.NewGuid() + }; + var historyMoment = DateTimeOffset.UtcNow; + + //act + var response = await client.GetHistory(setpointKeys, historyMoment); + + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + Assert.Empty(response.Content); + } + + [Fact] + public async Task GetHistory_AfterSave_returns_success() + { + //arrange + var setpointKey = await Save(); + var historyMoment = DateTimeOffset.UtcNow; + historyMoment = historyMoment.AddDays(1); + + //act + var response = await client.GetHistory([setpointKey], historyMoment); + + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + Assert.NotEmpty(response.Content); + Assert.Equal(setpointKey, response.Content.FirstOrDefault()?.Key); + } + + [Fact] + public async Task GetLog_returns_success() + { + //arrange + var setpointKeys = new List() + { + Guid.NewGuid(), + Guid.NewGuid() + }; + + //act + var response = await client.GetLog(setpointKeys); + + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + Assert.Empty(response.Content); + } + + [Fact] + public async Task GetLog_AfterSave_returns_success() + { + //arrange + var setpointKey = await Save(); + + //act + var response = await client.GetLog([setpointKey]); + + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + Assert.NotEmpty(response.Content); + Assert.Equal(setpointKey, response.Content.FirstOrDefault()?.Key); + } + + [Fact] + public async Task Save_returns_success() + { + await Save(); + } + + private async Task Save() + { + //arrange + var setpointKey = Guid.NewGuid(); + var setpointValue = new TestObject() + { + value1 = "1", + value2 = 2 + }; + + //act + var response = await client.Save(setpointKey, setpointValue); + + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + return setpointKey; + } + } +} diff --git a/Persistence.IntegrationTests/WebAppFactoryFixture.cs b/Persistence.IntegrationTests/WebAppFactoryFixture.cs index 8ac7bf2..57e0dfc 100644 --- a/Persistence.IntegrationTests/WebAppFactoryFixture.cs +++ b/Persistence.IntegrationTests/WebAppFactoryFixture.cs @@ -10,6 +10,7 @@ using Refit; using RestSharp; using System.Net.Http.Headers; using System.Text.Json; +using Persistence.Database.Postgres; namespace Persistence.IntegrationTests; public class WebAppFactoryFixture : WebApplicationFactory diff --git a/Persistence.Repository/Data/SetpointDto.cs b/Persistence.Repository/Data/SetpointDto.cs new file mode 100644 index 0000000..4a20aa4 --- /dev/null +++ b/Persistence.Repository/Data/SetpointDto.cs @@ -0,0 +1,28 @@ +namespace Persistence.Repository.Data +{ + /// + /// Модель для работы с уставкой + /// + public class SetpointDto + { + /// + /// Идентификатор уставки + /// + public int Id { get; set; } + + /// + /// Значение уставки + /// + public required object Value { get; set; } + + /// + /// Дата сохранения уставки + /// + public DateTimeOffset Edit { get; set; } + + /// + /// Ключ пользователя + /// + public int IdUser { get; set; } + } +} diff --git a/Persistence.Repository/DependencyInjection.cs b/Persistence.Repository/DependencyInjection.cs index 8e2f759..27063cb 100644 --- a/Persistence.Repository/DependencyInjection.cs +++ b/Persistence.Repository/DependencyInjection.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Persistence.Database.Model; using Persistence.Repositories; using Persistence.Repository.Data; @@ -15,7 +15,8 @@ public static class DependencyInjection { MapsterSetup(); - services.AddTransient, TimeSeriesDataCachedRepository>(); + services.AddTransient, TimeSeriesDataRepository>(); + services.AddTransient(); return services; } diff --git a/Persistence.Repository/Repositories/SetpointRepository.cs b/Persistence.Repository/Repositories/SetpointRepository.cs new file mode 100644 index 0000000..7f81c29 --- /dev/null +++ b/Persistence.Repository/Repositories/SetpointRepository.cs @@ -0,0 +1,73 @@ +using Mapster; +using Microsoft.EntityFrameworkCore; +using Persistence.Database.Model; +using Persistence.Models; +using Persistence.Repositories; + +namespace Persistence.Repository.Repositories +{ + public class SetpointRepository : ISetpointRepository + { + private DbContext db; + public SetpointRepository(DbContext db) + { + this.db = db; + } + + protected virtual IQueryable GetQueryReadOnly() => db.Set(); + + public async Task> GetCurrent(IEnumerable setpointKeys, CancellationToken token) + { + var query = GetQueryReadOnly(); + var entities = await query + .Where(e => setpointKeys.Contains(e.Key)) + .ToArrayAsync(token); + var dtos = entities.Select(e => e.Adapt()); + + return dtos; + } + + public async Task> GetHistory(IEnumerable setpointKeys, DateTimeOffset historyMoment, CancellationToken token) + { + var query = GetQueryReadOnly(); + var entities = await query + .Where(e => setpointKeys.Contains(e.Key)) + .ToArrayAsync(token); + var filteredEntities = entities + .GroupBy(e => e.Key) + .Select(e => e.OrderBy(o => o.Created)) + .Select(e => e.Where(e => e.Created <= historyMoment).Last()); + var dtos = filteredEntities + .Select(e => e.Adapt()); + + return dtos; + } + + public async Task>> GetLog(IEnumerable setpointKeys, CancellationToken token) + { + var query = GetQueryReadOnly(); + var entities = await query + .Where(e => setpointKeys.Contains(e.Key)) + .ToArrayAsync(token); + var dtos = entities + .GroupBy(e => e.Key) + .ToDictionary(e => e.Key, v => v.Select(z => z.Adapt())); + + return dtos; + } + + public async Task Save(Guid setpointKey, object newValue, int idUser, CancellationToken token) + { + var entity = new Setpoint() + { + Key = setpointKey, + Value = newValue, + IdUser = idUser, + Created = DateTimeOffset.UtcNow + }; + + await db.Set().AddAsync(entity, token); + await db.SaveChangesAsync(token); + } + } +} diff --git a/Persistence/Models/SetpointLogDto.cs b/Persistence/Models/SetpointLogDto.cs index 34b74f5..484be7a 100644 --- a/Persistence/Models/SetpointLogDto.cs +++ b/Persistence/Models/SetpointLogDto.cs @@ -1,4 +1,4 @@ -namespace Persistence.Models; +namespace Persistence.Models; /// /// Модель для описания лога уставки @@ -8,8 +8,8 @@ public class SetpointLogDto : SetpointValueDto /// /// Дата сохранения уставки /// - public DateTimeOffset DateEdit { get; set; } - + public DateTimeOffset Created { get; set; } + /// /// Ключ пользователя /// diff --git a/Persistence/Models/SetpointValueDto.cs b/Persistence/Models/SetpointValueDto.cs index 5c6d337..06a2b3e 100644 --- a/Persistence/Models/SetpointValueDto.cs +++ b/Persistence/Models/SetpointValueDto.cs @@ -1,18 +1,18 @@ -namespace Persistence.Models; +namespace Persistence.Models; /// /// Модель для хранения значения уставки /// public class SetpointValueDto { - /// /// Идентификатор уставки + /// /// - public int Id { get; set; } - + public Guid Key { get; set; } + /// /// Значение уставки /// - public object Value { get; set; } + public required object Value { get; set; } } diff --git a/Persistence/Repositories/ISetpointRepository.cs b/Persistence/Repositories/ISetpointRepository.cs index 1e1e2c3..1d82b16 100644 --- a/Persistence/Repositories/ISetpointRepository.cs +++ b/Persistence/Repositories/ISetpointRepository.cs @@ -1,4 +1,4 @@ -using Persistence.Models; +using Persistence.Models; namespace Persistence.Repositories; @@ -7,23 +7,30 @@ namespace Persistence.Repositories; /// public interface ISetpointRepository { + /// + /// Получить значения уставок по набору ключей + /// + /// + /// + /// + Task> GetCurrent(IEnumerable setpointKeys, CancellationToken token); - /// - /// Получить значения уставок за определенный момент времени - /// - /// - /// дата, на которую получаем данные - /// - /// - Task> GetHistory(IEnumerable setpoitKeys, DateTimeOffset historyMoment, CancellationToken token); + /// + /// Получить значения уставок за определенный момент времени + /// + /// + /// дата, на которую получаем данные + /// + /// + Task> GetHistory(IEnumerable setpointKeys, DateTimeOffset historyMoment, CancellationToken token); /// /// Получить историю изменений значений уставок /// - /// + /// /// /// - Task>> GetLog(IEnumerable setpoitKeys, CancellationToken token); + Task>> GetLog(IEnumerable setpointKeys, CancellationToken token); /// /// Метод сохранения уставки @@ -35,5 +42,5 @@ public interface ISetpointRepository /// /// to do /// id User учесть в соответствующем методе репозитория - Task Save(Guid setpointKey, int idUser, object newValue, CancellationToken token); + Task Save(Guid setpointKey, object newValue, int idUser, CancellationToken token); }