Compare commits

..

1 Commits

Author SHA1 Message Date
c844131bbe Переименование базы данных
All checks were successful
Unit tests / test (push) Successful in 56s
2025-01-24 17:31:58 +05:00
122 changed files with 1262 additions and 3304 deletions

View File

@ -1,25 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"DefaultConnection": "Host=db:5432;Database=persistence;Username=postgres;Password=postgres;Persist Security Info=True"
},
"AllowedHosts": "*",
"NeedUseKeyCloak": false,
"KeyCloakAuthentication": {
"Audience": "account",
"Host": "http://192.168.0.10:8321/realms/Persistence"
},
"AuthUser": {
"username": "myuser",
"password": 12345,
"clientId": "webapi",
"grantType": "password",
"http://schemas.xmlsoap.org/ws/2005/05/identity /claims/nameidentifier": "7d9f3574-6574-4ca3-845a-0276eb4aa8f6"
},
"ClientUrl": "http://localhost:5000/"
}

View File

@ -1,31 +0,0 @@
networks:
persistence:
external: false
services:
db:
image: timescale/timescaledb:latest-pg16
container_name: some-timescaledb-16
restart: always
environment:
- POSTGRES_PASSWORD=postgres
networks:
- persistence
ports:
- "5462:5432"
volumes:
- ./db:/var/lib/postgresql/data
persistence:
image: git.ddrilling.ru/ddrilling/persistence:latest
container_name: persistence
restart: always
depends_on:
- db
networks:
- persistence
ports:
- "1111:8080"
volumes:
- ./appsettings.json:/app/appsettings.json

View File

@ -0,0 +1,38 @@
name: Unit tests
run-name: ${{ gitea.actor }} is testing
on: push
jobs:
test:
runs-on: ubuntu-latest
container: node
# Service containers to run with `runner-job`
services:
# Label used to access the service container
postgres:
# Docker Hub image
image: postgres
# Provide the password for postgres
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
# Set health checks to wait until postgres has started
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
# Maps tcp port 5432 on service container to the host
- 5442:5432
steps:
- name: Setup dotnet
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.x
- name: Check out repository code
uses: actions/checkout@v4
- name: Run integration tests
run: dotnet test DD.Persistence.IntegrationTests

View File

@ -1,80 +0,0 @@
name: Unit tests
run-name: ${{ gitea.actor }} is testing
on:
push:
branches:
- master
jobs:
tests-and-publication:
runs-on: ubuntu-latest
# container: node
# Service containers to run with `runner-job`
services:
# Label used to access the service container
postgres:
# Docker Hub image
image: postgres
# Provide the password for postgres
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
# Set health checks to wait until postgres has started
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
# Maps tcp port 5432 on service container to the host
- 5442:5432
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup .NET
if: ${{ steps.cache-dotnet.outputs.cache-hit != 'true' }}
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.x
- name: Add gitea as nuget source
run: dotnet nuget add source --name gitea --username publisher --password ${{ secrets.PUBLISHER_PASSWORD }} --store-password-in-clear-text https://git.ddrilling.ru/api/packages/DDrilling/nuget/index.json
- name: Restore dependencies
run: dotnet restore
- name: Build solution
run: dotnet build --no-restore
- name: Run integration tests
run: dotnet test DD.Persistence.IntegrationTests
- name: Run unit tests
run: dotnet test DD.Persistence.Test --no-build
- name: Pack Persistence Client
run: dotnet pack DD.Persistence.Client/DD.Persistence.Client.csproj -c Debug -o ./artifacts
- name: Pack Persistence Models
run: dotnet pack DD.Persistence.Models/DD.Persistence.Models.csproj -c Debug -o ./artifacts
- name: Publish Persistence Client Package
run: dotnet nuget push ./artifacts/DD.Persistence.Client.*.nupkg --source gitea --skip-duplicate
- name: Publish Persistence Models Package
run: dotnet nuget push ./artifacts/DD.Persistence.Models.*.nupkg --source gitea --skip-duplicate
- name: Login to Gitea Docker Registry
run: docker login -u publisher -p ${{ secrets.PUBLISHER_PASSWORD }} https://git.ddrilling.ru
- name: Docker Build
run: docker build -t git.ddrilling.ru/ddrilling/persistence:latest --network=host --build-arg PUBLISHERPASSWORD="${{ secrets.PUBLISHER_PASSWORD }}" -f DD.Persistence.App/Dockerfile .
- name: Push Docker image to Gitea
run: docker push git.ddrilling.ru/ddrilling/persistence:latest

View File

@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using DD.Persistence.Models;
using DD.Persistence.Models.Requests;
@ -24,7 +24,7 @@ public class ChangeLogController : ControllerBase, IChangeLogApi
[ProducesResponseType(typeof(int), (int)HttpStatusCode.Created)]
public async Task<IActionResult> Add(
[FromRoute] Guid idDiscriminator,
[FromBody] ChangeLogValuesDto dto,
[FromBody] DataWithWellDepthAndSectionDto dto,
CancellationToken token)
{
var userId = User.GetUserId<Guid>();
@ -37,7 +37,7 @@ public class ChangeLogController : ControllerBase, IChangeLogApi
[ProducesResponseType(typeof(int), (int)HttpStatusCode.Created)]
public async Task<IActionResult> AddRange(
[FromRoute] Guid idDiscriminator,
[FromBody] IEnumerable<ChangeLogValuesDto> dtos,
[FromBody] IEnumerable<DataWithWellDepthAndSectionDto> dtos,
CancellationToken token)
{
var userId = User.GetUserId<Guid>();
@ -70,7 +70,7 @@ public class ChangeLogController : ControllerBase, IChangeLogApi
[ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)]
public async Task<IActionResult> ClearAndAddRange(
[FromRoute] Guid idDiscriminator,
[FromBody] IEnumerable<ChangeLogValuesDto> dtos,
[FromBody] IEnumerable<DataWithWellDepthAndSectionDto> dtos,
CancellationToken token)
{
var userId = User.GetUserId<Guid>();
@ -81,7 +81,7 @@ public class ChangeLogController : ControllerBase, IChangeLogApi
[HttpPut]
[ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)]
public async Task<IActionResult> Update(
ChangeLogValuesDto dto,
DataWithWellDepthAndSectionDto dto,
CancellationToken token)
{
var userId = User.GetUserId<Guid>();
@ -93,7 +93,7 @@ public class ChangeLogController : ControllerBase, IChangeLogApi
[HttpPut("range")]
[ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)]
public async Task<IActionResult> UpdateRange(
IEnumerable<ChangeLogValuesDto> dtos,
IEnumerable<DataWithWellDepthAndSectionDto> dtos,
CancellationToken token)
{
var userId = User.GetUserId<Guid>();
@ -103,27 +103,29 @@ public class ChangeLogController : ControllerBase, IChangeLogApi
}
[HttpGet("{idDiscriminator}")]
[ProducesResponseType(typeof(PaginationContainer<ChangeLogValuesDto>), (int)HttpStatusCode.OK)]
[ProducesResponseType(typeof(PaginationContainer<DataWithWellDepthAndSectionDto>), (int)HttpStatusCode.OK)]
public async Task<IActionResult> 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, paginationRequest, token);
var result = await repository.GetByDate(idDiscriminator, moment, filterRequest, paginationRequest, token);
return Ok(result);
}
[HttpGet("moment/{idDiscriminator}")]
[ProducesResponseType(typeof(PaginationContainer<ChangeLogValuesDto>), (int)HttpStatusCode.OK)]
[ProducesResponseType(typeof(PaginationContainer<DataWithWellDepthAndSectionDto>), (int)HttpStatusCode.OK)]
public async Task<IActionResult> GetByDate(
[FromRoute] Guid idDiscriminator,
DateTimeOffset moment,
[FromQuery] SectionPartRequest filterRequest,
[FromQuery] PaginationRequest paginationRequest,
CancellationToken token)
{
var result = await repository.GetByDate(idDiscriminator, moment, paginationRequest, token);
var result = await repository.GetByDate(idDiscriminator, moment, filterRequest, paginationRequest, token);
return Ok(result);
}
@ -153,9 +155,9 @@ public class ChangeLogController : ControllerBase, IChangeLogApi
}
[HttpGet("part/{idDiscriminator}")]
[ProducesResponseType(typeof(IEnumerable<ChangeLogValuesDto>), (int)HttpStatusCode.OK)]
[ProducesResponseType(typeof(IEnumerable<DataWithWellDepthAndSectionDto>), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
public async Task<ActionResult<IEnumerable<ChangeLogValuesDto>>> GetPart([FromRoute] Guid idDiscriminator, DateTimeOffset dateBegin, int take = 86400, CancellationToken token = default)
public async Task<ActionResult<IEnumerable<DataWithWellDepthAndSectionDto>>> GetPart([FromRoute] Guid idDiscriminator, DateTimeOffset dateBegin, int take = 86400, CancellationToken token = default)
{
var result = await repository.GetGtDate(idDiscriminator, dateBegin, token);
@ -174,87 +176,4 @@ public class ChangeLogController : ControllerBase, IChangeLogApi
return Ok(result);
}
/// <summary>
/// Метод, который возвращает статистику по количеству изменений в разрезе дней
/// </summary>
/// <param name="request"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpGet("statistics")]
public async Task<ActionResult<IEnumerable<StatisticsChangeLogDto>>> GetStatisticsCountAsync([FromQuery] ChangeLogRequest request, CancellationToken token)
{
var result = new List<StatisticsChangeLogDto>() {
new() { DateTime = DateTimeOffset.UtcNow.AddDays(-60), ChangesCount = 10},
new() { DateTime = DateTimeOffset.UtcNow.AddDays(-50), ChangesCount = 2},
new() { DateTime = DateTimeOffset.UtcNow.AddDays(-25), ChangesCount = 560},
new() { DateTime = DateTimeOffset.UtcNow.AddDays(-2), ChangesCount = 78},
new() { DateTime = DateTimeOffset.UtcNow.AddDays(-1), ChangesCount = 39},
};
return Ok(result);
}
/// <summary>
/// Метод, который возвращает историю изменений в разрезе дней
/// </summary>
/// <param name="request"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpGet("history")]
public async Task<ActionResult<IEnumerable<HistoryChangeLogDto>>> HistoryChangeLogAsync([FromQuery] ChangeLogRequest request, CancellationToken token)
{
var userId = Guid.CreateVersion7();
var changeLogItemCurrentId = Guid.CreateVersion7();
var changeLogItemCreation = DateTimeOffset.UtcNow;
var changeLogItems = new List<ChangeLogDto>()
{
new ChangeLogDto()
{
Id = changeLogItemCurrentId,
Creation = changeLogItemCreation,
IdAuthor = userId,
IdEditor = userId,
Obsolete = null,
Value = new ChangeLogValuesDto(){
Id = Guid.CreateVersion7(),
Value = new Dictionary<string, object>() {
["1"] = new { id = 1, caption = "Изменение 1 (c правкой)" },
["2"] = new { id = 2, caption = "Изменение 2 (с правкой)" },
}
}
},
new ChangeLogDto()
{
Id = Guid.CreateVersion7(),
Creation = DateTimeOffset.UtcNow.AddDays(-10),
IdAuthor = userId,
IdEditor = userId,
IdNext = changeLogItemCurrentId,
Obsolete = DateTimeOffset.UtcNow.AddDays(-5),
Value = new ChangeLogValuesDto(){
Id = Guid.CreateVersion7(),
Value = new Dictionary<string, object>() {
["1"] = new { id = 1, caption = "Изменение 1" },
["2"] = new { id = 2, caption = "Изменение 2" },
}
}
}
};
var result = new List<HistoryChangeLogDto>() {
new() {
Comment = "Петров И. Ю. попросил внести изменения",
DateTime = changeLogItemCreation,
DiscriminatorId = Guid.CreateVersion7(),
User = new UserDto()
{
Id = userId,
DisplayName = "Иванов И. И"
},
ChangeLogItems = changeLogItems
},
};
return Ok(result);
}
}

View File

@ -1,7 +1,5 @@
using DD.Persistence.Filter.Models.Abstractions;
using DD.Persistence.Models;
using DD.Persistence.Models;
using DD.Persistence.Models.Common;
using DD.Persistence.Repositories;
using DD.Persistence.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@ -14,15 +12,14 @@ namespace DD.Persistence.API.Controllers;
/// </summary>
[ApiController]
[Authorize]
[Route("api/[controller]")]
[Route("api/[controller]/{discriminatorId}")]
public class TimestampedValuesController : ControllerBase
{
private readonly ITimestampedValuesService timestampedValuesService;
private readonly ITimestampedValuesRepository timestampedValuesRepository;
public TimestampedValuesController(ITimestampedValuesService repository, ITimestampedValuesRepository timestampedValuesRepository)
private readonly ITimestampedValuesService timestampedValuesRepository;
public TimestampedValuesController(ITimestampedValuesService repository)
{
this.timestampedValuesService = repository;
this.timestampedValuesRepository = timestampedValuesRepository;
this.timestampedValuesRepository = repository;
}
/// <summary>
@ -32,11 +29,11 @@ public class TimestampedValuesController : ControllerBase
/// <param name="discriminatorId">Дискриминатор (идентификатор) набора</param>
/// <param name="dtos"></param>
/// <param name="token"></param>
[HttpPost("{discriminatorId}")]
[HttpPost]
[ProducesResponseType(typeof(int), (int)HttpStatusCode.Created)]
public async Task<IActionResult> AddRange([FromRoute] Guid discriminatorId, [FromBody] IEnumerable<TimestampedValuesDto> dtos, CancellationToken token)
{
var result = await timestampedValuesService.AddRange(discriminatorId, dtos, token);
var result = await timestampedValuesRepository.AddRange(discriminatorId, dtos, token);
return CreatedAtAction(nameof(AddRange), result);
}
@ -44,9 +41,8 @@ public class TimestampedValuesController : ControllerBase
/// <summary>
/// Получение данных с фильтрацией. Значение фильтра null - отключен
/// </summary>
/// <param name="discriminatorIds">Набор дискриминаторов</param>
/// <param name="discriminatorId">Дискриминатор (идентификатор) набора</param>
/// <param name="timestampBegin">Фильтр позднее даты</param>
/// <param name="filterTree">Кастомный фильтр по набору значений</param>
/// <param name="columnNames">Фильтр свойств набора</param>
/// <param name="skip"></param>
/// <param name="take"></param>
@ -54,14 +50,9 @@ public class TimestampedValuesController : ControllerBase
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<TimestampedValuesDto>), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
public async Task<ActionResult<IEnumerable<TimestampedValuesDto>>> Get([FromQuery] IEnumerable<Guid> discriminatorIds,
DateTimeOffset? timestampBegin,
[FromQuery] TNode? filterTree,
[FromQuery] string[]? columnNames,
int skip, int take,
CancellationToken token)
public async Task<ActionResult<IEnumerable<TimestampedValuesDto>>> Get([FromRoute] Guid discriminatorId, DateTimeOffset? timestampBegin, [FromQuery] string[]? columnNames, int skip, int take, CancellationToken token)
{
var result = await timestampedValuesService.Get(discriminatorIds, timestampBegin, filterTree, columnNames, skip, take, token);
var result = await timestampedValuesRepository.Get(discriminatorId, timestampBegin, columnNames, skip, take, token);
return result.Any() ? Ok(result) : NoContent();
}
@ -72,12 +63,12 @@ public class TimestampedValuesController : ControllerBase
/// <param name="discriminatorId">Дискриминатор (идентификатор) набора</param>
/// <param name="timestampBegin">Фильтр позднее даты</param>
/// <param name="token"></param>
[HttpGet("{discriminatorId}/gtdate")]
[HttpGet("gtdate")]
[ProducesResponseType(typeof(IEnumerable<TimestampedValuesDto>), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
public async Task<ActionResult<IEnumerable<TimestampedValuesDto>>> GetGtDate([FromRoute] Guid discriminatorId, DateTimeOffset timestampBegin, CancellationToken token)
{
var result = await timestampedValuesService.GetGtDate(discriminatorId, timestampBegin, token);
var result = await timestampedValuesRepository.GetGtDate(discriminatorId, timestampBegin, token);
return result.Any() ? Ok(result) : NoContent();
}
@ -88,12 +79,12 @@ public class TimestampedValuesController : ControllerBase
/// <param name="discriminatorId">Дискриминатор (идентификатор) набора</param>
/// <param name="take"></param>
/// <param name="token"></param>
[HttpGet("{discriminatorId}/first")]
[HttpGet("first")]
[ProducesResponseType(typeof(IEnumerable<TimestampedValuesDto>), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
public async Task<ActionResult<IEnumerable<TimestampedValuesDto>>> GetFirst([FromRoute] Guid discriminatorId, int take, CancellationToken token)
{
var result = await timestampedValuesService.GetFirst(discriminatorId, take, token);
var result = await timestampedValuesRepository.GetFirst(discriminatorId, take, token);
return result.Any() ? Ok(result) : NoContent();
}
@ -104,12 +95,12 @@ public class TimestampedValuesController : ControllerBase
/// <param name="discriminatorId">Дискриминатор (идентификатор) набора</param>
/// <param name="take"></param>
/// <param name="token"></param>
[HttpGet("{discriminatorId}/last")]
[HttpGet("last")]
[ProducesResponseType(typeof(IEnumerable<TimestampedValuesDto>), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
public async Task<ActionResult<IEnumerable<TimestampedValuesDto>>> GetLast([FromRoute] Guid discriminatorId, int take, CancellationToken token)
{
var result = await timestampedValuesService.GetLast(discriminatorId, take, token);
var result = await timestampedValuesRepository.GetLast(discriminatorId, take, token);
return result.Any() ? Ok(result) : NoContent();
}
@ -122,12 +113,12 @@ public class TimestampedValuesController : ControllerBase
/// <param name="intervalSec"></param>
/// <param name="approxPointsCount"></param>
/// <param name="token"></param>
[HttpGet("{discriminatorId}/resampled")]
[HttpGet("resampled")]
[ProducesResponseType(typeof(IEnumerable<TimestampedValuesDto>), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
public async Task<ActionResult<IEnumerable<TimestampedValuesDto>>> GetResampledData([FromRoute] Guid discriminatorId, DateTimeOffset timestampBegin, double intervalSec = 600d, int approxPointsCount = 1024, CancellationToken token = default)
{
var result = await timestampedValuesService.GetResampledData(discriminatorId, timestampBegin, intervalSec, approxPointsCount, token);
var result = await timestampedValuesRepository.GetResampledData(discriminatorId, timestampBegin, intervalSec, approxPointsCount, token);
return result.Any() ? Ok(result) : NoContent();
}
@ -137,7 +128,7 @@ public class TimestampedValuesController : ControllerBase
/// </summary>
/// <param name="discriminatorId">Дискриминатор (идентификатор) набора</param>
/// <param name="token"></param>
[HttpGet("{discriminatorId}/count")]
[HttpGet("count")]
public async Task<ActionResult<int>> Count([FromRoute] Guid discriminatorId, CancellationToken token)
{
var result = await timestampedValuesRepository.Count(discriminatorId, token);
@ -150,7 +141,7 @@ public class TimestampedValuesController : ControllerBase
/// </summary>
/// <param name="discriminatorId"></param>
/// <param name="token"></param>
[HttpGet("{discriminatorId}/datesRange")]
[HttpGet("datesRange")]
public async Task<ActionResult<DatesRangeDto>> GetDatesRange([FromRoute] Guid discriminatorId, CancellationToken token)
{
var result = await timestampedValuesRepository.GetDatesRange(discriminatorId, token);

View File

@ -25,11 +25,8 @@
<ItemGroup>
<ProjectReference Include="..\DD.Persistence.Database.Postgres\DD.Persistence.Database.Postgres.csproj" />
<ProjectReference Include="..\DD.Persistence.Database\DD.Persistence.Database.csproj" />
<ProjectReference Include="..\DD.Persistence.Repository\DD.Persistence.Repository.csproj" />
<ProjectReference Include="..\DD.Persistence\DD.Persistence.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Docs\" />
</ItemGroup>
</Project>

View File

@ -1,4 +1,4 @@
using DD.Persistence.Filter.Models.Abstractions;
using DD.Persistence.Database.Postgres.Services;
using DD.Persistence.Models.Configurations;
using DD.Persistence.Services;
using DD.Persistence.Services.Interfaces;
@ -28,7 +28,6 @@ public static class DependencyInjection
new OpenApiSchema {Type = "number", Format = "float" }
]
});
c.MapType<TNode>(() => new OpenApiSchema { Type = "string" });
c.CustomOperationIds(e =>
{
@ -53,6 +52,7 @@ public static class DependencyInjection
{
services.AddTransient<IWitsDataService, WitsDataService>();
services.AddTransient<ITimestampedValuesService, TimestampedValuesService>();
services.AddTransient<IArchiveService, PostgresArchiveService>();
}
#region Authentication

View File

@ -1,359 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<mxfile host="app.diagrams.net" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36" version="24.8.3">
<diagram name="Страница — 1" id="7k5Wemfp-yc9piGHxsiE">
<mxGraphModel dx="2049" dy="1054" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1300" pageHeight="1050" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="_dnhcZFeje3u91oS2JwL-1" value="" style="endArrow=none;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="355" y="930" as="sourcePoint" />
<mxPoint x="355" y="210" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-2" value="" style="endArrow=none;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="555" y="930" as="sourcePoint" />
<mxPoint x="555" y="210" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-4" value="&lt;b&gt;FRONT&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fillColor=#76608a;strokeColor=#432D57;fontColor=#ffffff;" vertex="1" parent="1">
<mxGeometry x="325" y="180" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-5" value="&lt;b&gt;BACK&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fillColor=#76608a;fontColor=#ffffff;strokeColor=#432D57;" vertex="1" parent="1">
<mxGeometry x="525" y="180" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-7" value="&lt;b&gt;CACHE&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fillColor=#76608a;fontColor=#ffffff;strokeColor=#432D57;" vertex="1" parent="1">
<mxGeometry x="725" y="180" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-8" value="" style="endArrow=none;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="955" y="930" as="sourcePoint" />
<mxPoint x="955" y="210" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-9" value="&lt;b&gt;COMMIT REPOSITORY&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fillColor=#76608a;fontColor=#ffffff;strokeColor=#432D57;" vertex="1" parent="1">
<mxGeometry x="880" y="180" width="155" height="30" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-32" value="" style="endArrow=none;html=1;rounded=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;align=center;" edge="1" parent="1" target="_dnhcZFeje3u91oS2JwL-7">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="755" y="930" as="sourcePoint" />
<mxPoint x="756.5000000000002" y="300" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-52" value="" style="endArrow=classic;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="555" y="300" as="sourcePoint" />
<mxPoint x="755" y="300" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-53" value="&lt;span style=&quot;font-size: 12px; text-wrap-mode: wrap;&quot;&gt;GetOrCreate(userId, message)&lt;/span&gt;" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;labelBackgroundColor=none;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-52">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-67" value="" style="endArrow=classic;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="755" y="310" as="sourcePoint" />
<mxPoint x="955" y="310.83" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-68" value="CreateCommit(userId, message)" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;labelBackgroundColor=none;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-67">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-69" value="" style="endArrow=classic;html=1;rounded=0;dashed=1;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="955" y="330" as="sourcePoint" />
<mxPoint x="755" y="330" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-70" value="CommitId" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-69">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-88" value="" style="endArrow=none;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1155" y="930" as="sourcePoint" />
<mxPoint x="1155" y="210" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-89" value="&lt;b&gt;CHANGE LOG REPOSITORY&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fillColor=#76608a;fontColor=#ffffff;strokeColor=#432D57;" vertex="1" parent="1">
<mxGeometry x="1060" y="180" width="185" height="30" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-90" value="" style="endArrow=classic;html=1;rounded=0;dashed=1;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="755" y="340" as="sourcePoint" />
<mxPoint x="555" y="340" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-91" value="CommitId" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-90">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-93" value="" style="endArrow=classic;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="555" y="380" as="sourcePoint" />
<mxPoint x="1155" y="380" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-94" value="AddRange(items, commitId)" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;labelBackgroundColor=none;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-93">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-96" value="" style="endArrow=classic;html=1;rounded=0;dashed=1;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="1155" y="410" as="sourcePoint" />
<mxPoint x="555" y="410" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-97" value="CREATED" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-96">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-100" value="" style="endArrow=classic;html=1;rounded=0;dashed=1;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="555" y="420" as="sourcePoint" />
<mxPoint x="355" y="420" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-101" value="CREATED" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-100">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-102" value="" style="endArrow=none;html=1;rounded=0;strokeWidth=9;curved=1;targetPerimeterSpacing=3;fillColor=#f8cecc;gradientColor=#ea6b66;strokeColor=#b85450;opacity=50;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="555" y="420" as="sourcePoint" />
<mxPoint x="555" y="290" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-104" value="" style="endArrow=classic;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="355" y="290" as="sourcePoint" />
<mxPoint x="555" y="290" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-105" value="Insert(items, message) [POST]" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;labelBackgroundColor=none;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-104">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-107" value="" style="endArrow=none;html=1;rounded=0;strokeWidth=9;curved=1;targetPerimeterSpacing=3;fillColor=#f8cecc;gradientColor=#ea6b66;strokeColor=#b85450;opacity=50;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="354.13" y="420" as="sourcePoint" />
<mxPoint x="354.13" y="290" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-108" value="" style="endArrow=classic;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="355" y="520" as="sourcePoint" />
<mxPoint x="555" y="520" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-109" value="Update(items, message) [PUT]" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-108">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-110" value="" style="endArrow=classic;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="555" y="530" as="sourcePoint" />
<mxPoint x="755" y="530" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-111" value="&lt;span style=&quot;font-size: 12px; text-wrap-mode: wrap;&quot;&gt;GetOrCreate(userId, message)&lt;/span&gt;" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;labelBackgroundColor=none;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-110">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-112" value="" style="endArrow=classic;html=1;rounded=0;dashed=1;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="755" y="560" as="sourcePoint" />
<mxPoint x="555" y="560" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-113" value="CommitId" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-112">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-114" value="" style="endArrow=none;html=1;rounded=0;strokeWidth=9;curved=1;targetPerimeterSpacing=3;fillColor=#f8cecc;gradientColor=#ea6b66;strokeColor=#b85450;opacity=50;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="554.5699999999999" y="650" as="sourcePoint" />
<mxPoint x="554.5699999999999" y="520" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-115" value="" style="endArrow=classic;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="555" y="610" as="sourcePoint" />
<mxPoint x="1155" y="610" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-116" value="UpdateRange(items, commitId)" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-115">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-117" value="" style="endArrow=classic;html=1;rounded=0;dashed=1;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="1155" y="640" as="sourcePoint" />
<mxPoint x="555" y="640" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-118" value="OK" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-117">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-119" value="" style="endArrow=classic;html=1;rounded=0;dashed=1;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="555" y="650" as="sourcePoint" />
<mxPoint x="355" y="650" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-120" value="OK" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-119">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-121" value="" style="edgeStyle=elbowEdgeStyle;elbow=horizontal;endArrow=classic;html=1;curved=0;rounded=0;endSize=8;startSize=8;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="755" y="530" as="sourcePoint" />
<mxPoint x="755" y="560" as="targetPoint" />
<Array as="points">
<mxPoint x="775" y="550" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-122" value="" style="endArrow=classic;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="355" y="760" as="sourcePoint" />
<mxPoint x="555" y="760" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-123" value="Delete(items, message) [DELETE]" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-122">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-124" value="" style="endArrow=classic;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="555" y="770" as="sourcePoint" />
<mxPoint x="755" y="770" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-125" value="&lt;span style=&quot;font-size: 12px; text-wrap-mode: wrap;&quot;&gt;GetOrCreate(userId, message)&lt;/span&gt;" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;labelBackgroundColor=none;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-124">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-126" value="" style="endArrow=classic;html=1;rounded=0;dashed=1;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="755" y="800" as="sourcePoint" />
<mxPoint x="555" y="800" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-127" value="CommitId" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-126">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-128" value="" style="endArrow=none;html=1;rounded=0;strokeWidth=9;curved=1;targetPerimeterSpacing=3;fillColor=#f8cecc;gradientColor=#ea6b66;strokeColor=#b85450;opacity=50;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="554.5699999999999" y="890" as="sourcePoint" />
<mxPoint x="554.5699999999999" y="760" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-129" value="" style="endArrow=classic;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="555" y="850" as="sourcePoint" />
<mxPoint x="1155" y="850" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-130" value="UpdateRange(items, commitId)" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-129">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-131" value="" style="endArrow=classic;html=1;rounded=0;dashed=1;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="1155" y="880" as="sourcePoint" />
<mxPoint x="555" y="880" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-132" value="OK" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-131">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-133" value="" style="endArrow=classic;html=1;rounded=0;dashed=1;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="555" y="890" as="sourcePoint" />
<mxPoint x="355" y="890" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-134" value="OK" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-133">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-135" value="" style="edgeStyle=elbowEdgeStyle;elbow=horizontal;endArrow=classic;html=1;curved=0;rounded=0;endSize=8;startSize=8;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="755" y="770" as="sourcePoint" />
<mxPoint x="755" y="800" as="targetPoint" />
<Array as="points">
<mxPoint x="775" y="790" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-136" value="" style="endArrow=none;html=1;rounded=0;strokeWidth=9;curved=1;targetPerimeterSpacing=3;fillColor=#f8cecc;gradientColor=#ea6b66;strokeColor=#b85450;opacity=50;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="354.57" y="650" as="sourcePoint" />
<mxPoint x="354.57" y="520" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-137" value="" style="endArrow=none;html=1;rounded=0;strokeWidth=9;curved=1;targetPerimeterSpacing=3;fillColor=#f8cecc;gradientColor=#ea6b66;strokeColor=#b85450;opacity=50;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="354.57" y="890" as="sourcePoint" />
<mxPoint x="354.57" y="760" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-138" value="" style="endArrow=none;html=1;rounded=0;strokeWidth=10;curved=1;targetPerimeterSpacing=3;fillColor=#f8cecc;gradientColor=#ea6b66;strokeColor=#b85450;opacity=50;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1155" y="411" as="sourcePoint" />
<mxPoint x="1155" y="381" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-139" value="" style="endArrow=none;html=1;rounded=0;strokeWidth=10;curved=1;targetPerimeterSpacing=3;fillColor=#f8cecc;gradientColor=#ea6b66;strokeColor=#b85450;opacity=50;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1155" y="641" as="sourcePoint" />
<mxPoint x="1155" y="611" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-140" value="" style="endArrow=none;html=1;rounded=0;strokeWidth=10;curved=1;targetPerimeterSpacing=3;fillColor=#f8cecc;gradientColor=#ea6b66;strokeColor=#b85450;opacity=50;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1154.75" y="881" as="sourcePoint" />
<mxPoint x="1154.75" y="851" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-141" value="" style="endArrow=none;html=1;rounded=0;strokeWidth=10;curved=1;targetPerimeterSpacing=3;fillColor=#dae8fc;gradientColor=#7ea6e0;strokeColor=#6c8ebf;opacity=50;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="755" y="830" as="sourcePoint" />
<mxPoint x="754.93" y="312" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-142" value="" style="endArrow=none;html=1;rounded=0;fillColor=#dae8fc;strokeColor=#6c8ebf;strokeWidth=10;opacity=50;gradientColor=#7ea6e0;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="165" y="882" as="sourcePoint" />
<mxPoint x="165.22" y="292" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-143" value="&lt;font color=&quot;#330066&quot;&gt;User&lt;/font&gt;" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;fillColor=#76608a;strokeColor=#432D57;fontColor=#ffffff;align=center;" vertex="1" parent="1">
<mxGeometry x="155" y="180" width="21.5" height="43" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-146" value="" style="endArrow=classic;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="170" y="280" as="sourcePoint" />
<mxPoint x="360" y="280" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-147" value="SAVE" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;labelBackgroundColor=none;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-146">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-148" value="" style="endArrow=none;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="165.22" y="930" as="sourcePoint" />
<mxPoint x="165.22" y="210" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-149" value="" style="endArrow=classic;html=1;rounded=0;dashed=1;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="355" y="892" as="sourcePoint" />
<mxPoint x="165" y="892" as="targetPoint" />
<Array as="points">
<mxPoint x="265" y="892" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-150" value="OK" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-149">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-152" value="&lt;h1 style=&quot;margin-top: 0px;&quot;&gt;UML-диаграмма процесса пакетного редактирования&lt;/h1&gt;&lt;p&gt;.&lt;/p&gt;" style="text;html=1;whiteSpace=wrap;overflow=hidden;rounded=0;align=center;" vertex="1" parent="1">
<mxGeometry x="75" y="50" width="1150" height="70" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-154" value="frame" style="shape=umlFrame;whiteSpace=wrap;html=1;pointerEvents=0;" vertex="1" parent="1">
<mxGeometry x="120" y="250" width="1120" height="680" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-156" value="&lt;p style=&quot;line-height: 120%;&quot;&gt;Время жизни кеша задается внутри проекта&lt;/p&gt;" style="shape=note;size=20;whiteSpace=wrap;html=1;labelBackgroundColor=none;fillColor=#d0cee2;strokeColor=#56517e;opacity=50;" vertex="1" parent="1">
<mxGeometry x="760" y="660" width="120" height="100" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

View File

@ -48,7 +48,4 @@ Password: 12345
}
```
## Пакетное редактирование (на примере ChangeLog)
UML-диаграмма процесса редактирования находится по [ссылке](https://git.ddrilling.ru/on.nemtina/persistence/src/branch/master/DD.Persistence.API/Docs/ChangeLog_actions.drawio.xml)

View File

@ -1,6 +1,6 @@
using DD.Persistence.Database;
using DD.Persistence.Database.Model;
using DD.Persistence.Database.Model;
using DD.Persistence.Database.Postgres.Extensions;
using DD.Persistence.Repository;
namespace DD.Persistence.API;
@ -14,7 +14,7 @@ public class Startup
public void ConfigureServices(IServiceCollection services)
{
// AddRange services to the container.
// Add services to the container.
services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle

View File

@ -28,6 +28,7 @@ COPY ["DD.Persistence/DD.Persistence.csproj", "DD.Persistence/"]
COPY ["DD.Persistence.Database/DD.Persistence.Database.csproj", "DD.Persistence.Database/"]
COPY ["DD.Persistence.Database.Postgres/DD.Persistence.Database.Postgres.csproj", "DD.Persistence.Database.Postgres/"]
COPY ["DD.Persistence.Models/DD.Persistence.Models.csproj", "DD.Persistence.Models/"]
COPY ["DD.Persistence.Repository/DD.Persistence.Repository.csproj", "DD.Persistence.Repository/"]
RUN dotnet restore "./DD.Persistence.App/DD.Persistence.App.csproj"

View File

@ -6,7 +6,8 @@
}
},
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Database=persistence;Username=postgres;Password=postgres;Persist Security Info=True"
"DefaultConnection": "Host=localhost;Database=persistence;Username=postgres;Password=q;Persist Security Info=True",
"SystemBaseConnection": "Host=localhost;Database=postgres;Username=postgres;Password=q;Persist Security Info=True"
},
"AllowedHosts": "*",
"NeedUseKeyCloak": false,

View File

@ -7,19 +7,16 @@ using DD.Persistence.Client.Clients.Interfaces.Refit;
using DD.Persistence.Models.Common;
namespace DD.Persistence.Client.Clients;
/// <inheritdoc/>
public class ChangeLogClient : BaseClient, IChangeLogClient
{
private readonly IRefitChangeLogClient refitChangeLogClient;
/// <inheritdoc/>
public ChangeLogClient(IRefitClientFactory<IRefitChangeLogClient> refitClientFactory, ILogger<ChangeLogClient> logger) : base(logger)
{
this.refitChangeLogClient = refitClientFactory.Create();
}
/// <inheritdoc/>
public async Task<int> ClearAndAddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token)
public async Task<int> ClearAndAddRange(Guid idDiscriminator, IEnumerable<DataWithWellDepthAndSectionDto> dtos, CancellationToken token)
{
var result = await ExecuteGetResponse(
async () => await refitChangeLogClient.ClearAndAddRange(idDiscriminator, dtos, token), token);
@ -27,17 +24,15 @@ public class ChangeLogClient : BaseClient, IChangeLogClient
return result;
}
/// <inheritdoc/>
public async Task<PaginationContainer<ChangeLogValuesDto>> GetByDate(Guid idDiscriminator, DateTimeOffset moment,
PaginationRequest paginationRequest, CancellationToken token)
public async Task<PaginationContainer<DataWithWellDepthAndSectionDto>> GetByDate(Guid idDiscriminator, DateTimeOffset moment,
SectionPartRequest filterRequest, PaginationRequest paginationRequest, CancellationToken token)
{
var result = await ExecuteGetResponse(
async () => await refitChangeLogClient.GetByDate(idDiscriminator, moment, paginationRequest, token), token);
async () => await refitChangeLogClient.GetByDate(idDiscriminator, moment, filterRequest, paginationRequest, token), token);
return result!;
return result;
}
/// <inheritdoc/>
public async Task<IEnumerable<ChangeLogDto>> GetChangeLogForInterval(Guid idDiscriminator, DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token)
{
var result = await ExecuteGetResponse(
@ -46,8 +41,7 @@ public class ChangeLogClient : BaseClient, IChangeLogClient
return result!;
}
/// <inheritdoc/>
public async Task<int> Add(Guid idDiscriminator, ChangeLogValuesDto dto, CancellationToken token)
public async Task<int> Add(Guid idDiscriminator, DataWithWellDepthAndSectionDto dto, CancellationToken token)
{
var result = await ExecutePostResponse(
async () => await refitChangeLogClient.Add(idDiscriminator, dto, token), token);
@ -55,8 +49,7 @@ public class ChangeLogClient : BaseClient, IChangeLogClient
return result;
}
/// <inheritdoc/>
public async Task<int> AddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token)
public async Task<int> AddRange(Guid idDiscriminator, IEnumerable<DataWithWellDepthAndSectionDto> dtos, CancellationToken token)
{
var result = await ExecutePostResponse(
async () => await refitChangeLogClient.AddRange(idDiscriminator, dtos, token), token);
@ -64,8 +57,7 @@ public class ChangeLogClient : BaseClient, IChangeLogClient
return result;
}
/// <inheritdoc/>
public async Task<int> Update(ChangeLogValuesDto dto, CancellationToken token)
public async Task<int> Update(DataWithWellDepthAndSectionDto dto, CancellationToken token)
{
var result = await ExecutePostResponse(
async () => await refitChangeLogClient.Update(dto, token), token);
@ -73,8 +65,7 @@ public class ChangeLogClient : BaseClient, IChangeLogClient
return result;
}
/// <inheritdoc/>
public async Task<int> UpdateRange(IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token)
public async Task<int> UpdateRange(IEnumerable<DataWithWellDepthAndSectionDto> dtos, CancellationToken token)
{
var result = await ExecutePostResponse(
async () => await refitChangeLogClient.UpdateRange(dtos, token), token);
@ -82,7 +73,6 @@ public class ChangeLogClient : BaseClient, IChangeLogClient
return result;
}
/// <inheritdoc/>
public async Task<int> Delete(Guid id, CancellationToken token)
{
var result = await ExecutePostResponse(
@ -91,7 +81,6 @@ public class ChangeLogClient : BaseClient, IChangeLogClient
return result;
}
/// <inheritdoc/>
public async Task<int> DeleteRange(IEnumerable<Guid> ids, CancellationToken token)
{
var result = await ExecutePostResponse(
@ -100,7 +89,6 @@ public class ChangeLogClient : BaseClient, IChangeLogClient
return result;
}
/// <inheritdoc/>
public async Task<DatesRangeDto?> GetDatesRange(Guid idDiscriminator, CancellationToken token)
{
var result = await ExecuteGetResponse(
@ -109,7 +97,6 @@ public class ChangeLogClient : BaseClient, IChangeLogClient
return result;
}
/// <inheritdoc/>
public void Dispose()
{
refitChangeLogClient.Dispose();

View File

@ -16,7 +16,7 @@ public interface IChangeLogClient : IDisposable
/// <param name="dto"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> Add(Guid idDiscriminator, ChangeLogValuesDto dto, CancellationToken token);
Task<int> Add(Guid idDiscriminator, DataWithWellDepthAndSectionDto dto, CancellationToken token);
/// <summary>
/// Добавить несколько записей
@ -25,7 +25,7 @@ public interface IChangeLogClient : IDisposable
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> AddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token);
Task<int> AddRange(Guid idDiscriminator, IEnumerable<DataWithWellDepthAndSectionDto> dtos, CancellationToken token);
/// <summary>
/// Импорт с заменой: удаление старых строк и добавление новых
@ -34,7 +34,7 @@ public interface IChangeLogClient : IDisposable
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> ClearAndAddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token);
Task<int> ClearAndAddRange(Guid idDiscriminator, IEnumerable<DataWithWellDepthAndSectionDto> dtos, CancellationToken token);
/// <summary>
/// Удалить одну запись
@ -57,10 +57,11 @@ public interface IChangeLogClient : IDisposable
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="moment"></param>
/// <param name="filterRequest"></param>
/// <param name="paginationRequest"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<PaginationContainer<ChangeLogValuesDto>> GetByDate(Guid idDiscriminator, DateTimeOffset moment, PaginationRequest paginationRequest, CancellationToken token);
Task<PaginationContainer<DataWithWellDepthAndSectionDto>> GetByDate(Guid idDiscriminator, DateTimeOffset moment, SectionPartRequest filterRequest, PaginationRequest paginationRequest, CancellationToken token);
/// <summary>
/// Получение исторических данных за определенный период времени
@ -86,7 +87,7 @@ public interface IChangeLogClient : IDisposable
/// <param name="dto"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> Update(ChangeLogValuesDto dto, CancellationToken token);
Task<int> Update(DataWithWellDepthAndSectionDto dto, CancellationToken token);
/// <summary>
/// Обновить несколько записей
@ -94,5 +95,5 @@ public interface IChangeLogClient : IDisposable
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> UpdateRange(IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token);
Task<int> UpdateRange(IEnumerable<DataWithWellDepthAndSectionDto> dtos, CancellationToken token);
}

View File

@ -20,34 +20,15 @@ public interface ITimestampedValuesClient : IDisposable
Task<int> AddRange(Guid discriminatorId, IEnumerable<TimestampedValuesDto> dtos, CancellationToken token);
/// <summary>
/// Получить данные с фильтрацией для нескольких систем. Значение фильтра null - отключен
/// Получить данные с фильтрацией. Значение фильтра null - отключен
/// </summary>
/// <param name="discriminatorIds">Набор дискриминаторов (идентификаторов)</param>
/// <param name="discriminatorId">Дискриминатор (идентификатор) набора</param>
/// <param name="timestampBegin">Фильтр позднее даты</param>
/// <param name="filterTree">Кастомный фильтр по набору значений</param>
/// <param name="columnNames">Фильтр свойств набора</param>
/// <param name="skip"></param>
/// <param name="take"></param>
/// <param name="token"></param>
Task<IEnumerable<TimestampedValuesDto>> Get(IEnumerable<Guid> discriminatorIds,
DateTimeOffset? timestampBegin,
string? filterTree,
IEnumerable<string>? columnNames,
int skip,
int take,
CancellationToken token);
/// <summary>
/// Получить данные с фильтрацией для нескольких систем. Значение фильтра null - отключен
/// </summary>
/// <param name="discriminatorId"></param>
/// <param name="geTimestamp"></param>
/// <param name="filterTree"></param>
/// <param name="columnNames">Фильтр свойств набора</param>
/// <param name="skip"></param>
/// <param name="take"></param>
/// <param name="token"></param>
Task<IEnumerable<T>> Get<T>(Guid discriminatorId, DateTimeOffset? geTimestamp, string? filterTree, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token);
Task<IEnumerable<TimestampedValuesDto>> Get(Guid discriminatorId, DateTimeOffset? timestampBegin, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token);
/// <summary>
/// Получить данные, начиная с заданной отметки времени
@ -97,6 +78,19 @@ public interface ITimestampedValuesClient : IDisposable
/// <param name="token"></param>
Task<DatesRangeDto?> GetDatesRange(Guid discriminatorId, CancellationToken token);
/// <summary>
///
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="idDiscriminator"></param>
/// <param name="geTimestamp"></param>
/// <param name="columnNames"></param>
/// <param name="skip"></param>
/// <param name="take"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<T>> Get<T>(Guid idDiscriminator, DateTimeOffset? geTimestamp, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token);
/// <summary>
///
/// </summary>

View File

@ -5,74 +5,42 @@ using Refit;
namespace DD.Persistence.Client.Clients.Interfaces.Refit;
/// <summary>
/// Refit интерфейс для IRefitChangeLogClient
/// </summary>
public interface IRefitChangeLogClient : IRefitClient, IDisposable
{
private const string BaseRoute = "/api/ChangeLog";
/// <summary>
/// Импорт с заменой: удаление старых строк и добавление новых
/// </summary>
[Post($"{BaseRoute}/replace/{{idDiscriminator}}")]
Task<IApiResponse<int>> ClearAndAddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token);
Task<IApiResponse<int>> ClearAndAddRange(Guid idDiscriminator, IEnumerable<DataWithWellDepthAndSectionDto> dtos, CancellationToken token);
/// <summary>
/// Получение актуальных данных на определенную дату (с пагинацией)
/// </summary>
[Get($"{BaseRoute}/moment/{{idDiscriminator}}")]
Task<IApiResponse<PaginationContainer<ChangeLogValuesDto>>> GetByDate(
Task<IApiResponse<PaginationContainer<DataWithWellDepthAndSectionDto>>> GetByDate(
Guid idDiscriminator,
DateTimeOffset moment,
[Query] SectionPartRequest filterRequest,
[Query] PaginationRequest paginationRequest,
CancellationToken token);
/// <summary>
/// Получение исторических данных за определенный период времени
/// </summary>
[Get($"{BaseRoute}/history/{{idDiscriminator}}")]
Task<IApiResponse<IEnumerable<ChangeLogDto>>> GetChangeLogForInterval(Guid idDiscriminator, DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token);
/// <summary>
/// Добавить одну запись
/// </summary>
[Post($"{BaseRoute}/{{idDiscriminator}}")]
Task<IApiResponse<int>> Add(Guid idDiscriminator, ChangeLogValuesDto dto, CancellationToken token);
Task<IApiResponse<int>> Add(Guid idDiscriminator, DataWithWellDepthAndSectionDto dto, CancellationToken token);
/// <summary>
/// Добавить несколько записей
/// </summary>
[Post($"{BaseRoute}/range/{{idDiscriminator}}")]
Task<IApiResponse<int>> AddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token);
Task<IApiResponse<int>> AddRange(Guid idDiscriminator, IEnumerable<DataWithWellDepthAndSectionDto> dtos, CancellationToken token);
/// <summary>
/// Обновить одну запись
/// </summary>
[Put($"{BaseRoute}")]
Task<IApiResponse<int>> Update(ChangeLogValuesDto dto, CancellationToken token);
Task<IApiResponse<int>> Update(DataWithWellDepthAndSectionDto dto, CancellationToken token);
/// <summary>
/// Обновить несколько записей
/// </summary>
[Put($"{BaseRoute}/range")]
Task<IApiResponse<int>> UpdateRange(IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token);
Task<IApiResponse<int>> UpdateRange(IEnumerable<DataWithWellDepthAndSectionDto> dtos, CancellationToken token);
/// <summary>
/// Удалить одну запись
/// </summary>
[Delete($"{BaseRoute}")]
Task<IApiResponse<int>> Delete(Guid id, CancellationToken token);
/// <summary>
/// Удалить несколько записей
/// </summary>
[Delete($"{BaseRoute}/range")]
Task<IApiResponse<int>> DeleteRange([Body] IEnumerable<Guid> ids, CancellationToken token);
/// <summary>
/// Получение списка дат, в которые происходили изменения (день, месяц, год, без времени)
/// </summary>
[Get($"{BaseRoute}/datesRange/{{idDiscriminator}}")]
Task<IApiResponse<DatesRangeDto?>> GetDatesRange(Guid idDiscriminator, CancellationToken token);

View File

@ -9,59 +9,53 @@ namespace DD.Persistence.Client.Clients.Interfaces.Refit;
/// </summary>
public interface IRefitTimestampedValuesClient : IRefitClient, IDisposable
{
private const string baseUrl = "/api/TimestampedValues";
private const string baseUrl = "/api/TimestampedValues/{discriminatorId}";
/// <summary>
/// Записать новые данные
/// </summary>
[Post($"{baseUrl}/{{discriminatorId}}")]
[Post(baseUrl)]
Task<IApiResponse<int>> AddRange(Guid discriminatorId, IEnumerable<TimestampedValuesDto> dtos, CancellationToken token);
/// <summary>
/// Получение данных с фильтрацией
/// </summary>
[Get($"{baseUrl}")]
Task<IApiResponse<IEnumerable<TimestampedValuesDto>>> Get([Query(CollectionFormat.Multi)] IEnumerable<Guid> discriminatorIds,
DateTimeOffset? timestampBegin,
[Query] string? filterTree,
[Query(CollectionFormat.Multi)] IEnumerable<string>? columnNames,
int skip,
int take,
CancellationToken token);
[Get(baseUrl)]
Task<IApiResponse<IEnumerable<TimestampedValuesDto>>> Get(Guid discriminatorId, DateTimeOffset? timestampBegin, [Query(CollectionFormat.Multi)] IEnumerable<string>? columnNames, int skip, int take, CancellationToken token);
/// <summary>
/// Получить данные, начиная с заданной отметки времени
/// </summary>
[Get($"{baseUrl}/{{discriminatorId}}/gtdate")]
[Get($"{baseUrl}/gtdate")]
Task<IApiResponse<IEnumerable<TimestampedValuesDto>>> GetGtDate(Guid discriminatorId, DateTimeOffset timestampBegin, CancellationToken token);
/// <summary>
/// Получить данные c начала
/// </summary>
[Get($"{baseUrl}/{{discriminatorId}}/first")]
[Get($"{baseUrl}/first")]
Task<IApiResponse<IEnumerable<TimestampedValuesDto>>> GetFirst(Guid discriminatorId, int take, CancellationToken token);
/// <summary>
/// Получить данные c конца
/// </summary>
[Get($"{baseUrl}/{{discriminatorId}}/last")]
[Get($"{baseUrl}/last")]
Task<IApiResponse<IEnumerable<TimestampedValuesDto>>> GetLast(Guid discriminatorId, int take, CancellationToken token);
/// <summary>
/// Получить список объектов с прореживанием, удовлетворяющий диапазону временных отметок
/// </summary>
[Get($"{baseUrl}/{{discriminatorId}}/resampled")]
[Get($"{baseUrl}/resampled")]
Task<IApiResponse<IEnumerable<TimestampedValuesDto>>> GetResampledData(Guid discriminatorId, DateTimeOffset timestampBegin, double intervalSec = 600d, int approxPointsCount = 1024, CancellationToken token = default);
/// <summary>
/// Получить количество записей по указанному набору в БД. Для пагинации
/// </summary>
[Get($"{baseUrl}/{{discriminatorId}}/count")]
[Get($"{baseUrl}/count")]
Task<IApiResponse<int>> Count(Guid discriminatorId, CancellationToken token);
/// <summary>
/// Получить диапазон дат, в пределах которых хранятся даные
/// </summary>
[Get($"{baseUrl}/{{discriminatorId}}/datesRange")]
[Get($"{baseUrl}/datesRange")]
Task<IApiResponse<DatesRangeDto?>> GetDatesRange(Guid discriminatorId, CancellationToken token);
}

View File

@ -32,20 +32,11 @@ public class TimestampedValuesClient : BaseClient, ITimestampedValuesClient
}
/// <inheritdoc/>
public async Task<IEnumerable<TimestampedValuesDto>> Get(IEnumerable<Guid> discriminatorIds, DateTimeOffset? geTimestamp, string? filterTree, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token)
public async Task<IEnumerable<TimestampedValuesDto>> Get(Guid discriminatorId, DateTimeOffset? geTimestamp, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token)
{
var result = await ExecuteGetResponse(
async () => await refitTimestampedSetClient.Get(discriminatorIds, geTimestamp, filterTree, columnNames, skip, take, token), token);
return result!;
}
/// <inheritdoc/>
public async Task<IEnumerable<T>> Get<T>(Guid discriminatorId, DateTimeOffset? geTimestamp, string? filterTree, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token)
{
var data = await Get([discriminatorId], geTimestamp, filterTree, columnNames, skip, take, token);
var mapper = GetMapper<T>(discriminatorId);
return data.Select(mapper.DeserializeTimeStampedData);
async () => await refitTimestampedSetClient.Get(discriminatorId, geTimestamp, columnNames, skip, take, token), token);
return result;
}
/// <inheritdoc/>
@ -54,7 +45,7 @@ public class TimestampedValuesClient : BaseClient, ITimestampedValuesClient
var result = await ExecuteGetResponse(
async () => await refitTimestampedSetClient.GetGtDate(discriminatorId, timestampBegin, token), token);
return result!;
return result;
}
/// <inheritdoc/>
@ -63,7 +54,7 @@ public class TimestampedValuesClient : BaseClient, ITimestampedValuesClient
var result = await ExecuteGetResponse(
async () => await refitTimestampedSetClient.GetFirst(discriminatorId, take, token), token);
return result!;
return result;
}
/// <inheritdoc/>
@ -72,7 +63,7 @@ public class TimestampedValuesClient : BaseClient, ITimestampedValuesClient
var result = await ExecuteGetResponse(
async () => await refitTimestampedSetClient.GetLast(discriminatorId, take, token), token);
return result!;
return result;
}
/// <inheritdoc/>
@ -81,7 +72,7 @@ public class TimestampedValuesClient : BaseClient, ITimestampedValuesClient
var result = await ExecuteGetResponse(
async () => await refitTimestampedSetClient.GetResampledData(discriminatorId, dateBegin, intervalSec, approxPointsCount, token), token);
return result!;
return result;
}
/// <inheritdoc/>
@ -102,6 +93,15 @@ public class TimestampedValuesClient : BaseClient, ITimestampedValuesClient
return result;
}
/// <inheritdoc/>
public async Task<IEnumerable<T>> Get<T>(Guid idDiscriminator, DateTimeOffset? geTimestamp, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token)
{
var data = await Get(idDiscriminator, geTimestamp, columnNames, skip, take, token);
var mapper = GetMapper<T>(idDiscriminator);
return data.Select(mapper.DeserializeTimeStampedData);
}
/// <inheritdoc/>
public async Task<IEnumerable<T>> GetLast<T>(Guid idDiscriminator, int take, CancellationToken token)
{

View File

@ -11,9 +11,9 @@
<!--Наименование-->
<Title>DD.Persistence.Client</Title>
<!--Версия пакета-->
<VersionPrefix>1.6.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH)).1</VersionPrefix>
<VersionPrefix>1.5.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH)).1</VersionPrefix>
<!--Версия сборки-->
<AssemblyVersion>1.6.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH)).1</AssemblyVersion>
<AssemblyVersion>1.5.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH)).1</AssemblyVersion>
<!--Id пакета-->
<PackageId>DD.Persistence.Client</PackageId>
@ -40,8 +40,8 @@
</PropertyGroup>
<PropertyGroup>
<VersionPrefix>1.6.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH))</VersionPrefix>
<AssemblyVersion>1.6.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH))</AssemblyVersion>
<VersionPrefix>1.5.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH))</VersionPrefix>
<AssemblyVersion>1.5.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH))</AssemblyVersion>
</PropertyGroup>
<ItemGroup>
@ -52,6 +52,7 @@
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.3.0" />
<PackageReference Include="Refit" Version="8.0.0" />
<PackageReference Include="Refit.HttpClientFactory" Version="8.0.0" />
<PackageReference Include="RestSharp" Version="112.1.0" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="9.0.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.3.0" />
</ItemGroup>

View File

@ -0,0 +1,80 @@
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using DD.Persistence.Models.Configurations;
using RestSharp;
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text.Json;
namespace DD.Persistence.Client.Helpers;
public static class ApiTokenHelper
{
public static void Authorize(this HttpClient httpClient, IConfiguration configuration)
{
var authUser = configuration
.GetSection(nameof(AuthUser))
.Get<AuthUser>()!;
var needUseKeyCloak = configuration
.GetSection("NeedUseKeyCloak")
.Get<bool>()!;
var keycloakGetTokenUrl = configuration.GetSection("KeycloakGetTokenUrl").Get<string>() ?? string.Empty;
var jwtToken = needUseKeyCloak
? authUser.CreateKeyCloakJwtToken(keycloakGetTokenUrl)
: authUser.CreateDefaultJwtToken();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", jwtToken);
}
public static void Authorize(this HttpClient httpClient, string jwtToken)
{
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", jwtToken);
}
private static string CreateDefaultJwtToken(this AuthUser authUser)
{
var nameIdetifier = Guid.NewGuid().ToString();
var claims = new List<Claim>()
{
new(ClaimTypes.NameIdentifier, nameIdetifier),
new("client_id", authUser.ClientId),
new("username", authUser.Username),
new("password", authUser.Password),
new("grant_type", authUser.GrantType),
new(ClaimTypes.NameIdentifier.ToString(), Guid.NewGuid().ToString())
};
var tokenDescriptor = new SecurityTokenDescriptor
{
Issuer = JwtParams.Issuer,
Audience = JwtParams.Audience,
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.AddHours(1),
SigningCredentials = new SigningCredentials(JwtParams.SecurityKey, SecurityAlgorithms.HmacSha256Signature)
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
private static string CreateKeyCloakJwtToken(this AuthUser authUser, string keycloakGetTokenUrl)
{
var restClient = new RestClient();
var request = new RestRequest(keycloakGetTokenUrl, Method.Post);
request.AddParameter("username", authUser.Username);
request.AddParameter("password", authUser.Password);
request.AddParameter("client_id", authUser.ClientId);
request.AddParameter("grant_type", authUser.GrantType);
var keycloakResponse = restClient.Post(request);
if (keycloakResponse.IsSuccessful && !String.IsNullOrEmpty(keycloakResponse.Content))
{
var token = JsonSerializer.Deserialize<JwtToken>(keycloakResponse.Content)!;
return token.AccessToken;
}
return String.Empty;
}
}

View File

@ -1,27 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2" />
<PackageReference Include="Mapster" Version="7.4.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DD.Persistence.Database.Postgres\DD.Persistence.Database.Postgres.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>

View File

@ -1,40 +0,0 @@
using DD.Persistence.Database.Model;
using DD.Persistence.Database.Postgres.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace DD.Persistence.Database.Postgres.Test;
public class DbFixture : IDisposable
{
private string connectionString { get; }
public ServiceProvider serviceProvider { get; private set; }
public DbFixture()
{
connectionString = $"Host=localhost;Port=5462;Username=postgres;Password=postgres;Database={Guid.CreateVersion7()}";
var serviceCollection = new ServiceCollection();
serviceCollection
.AddDbContext<PersistenceDbContext>(options => options.UseNpgsql(connectionString),
ServiceLifetime.Transient);
serviceProvider = serviceCollection.BuildServiceProvider();
var context = serviceProvider.GetRequiredService<PersistenceDbContext>();
context.Database.EnsureCreated();
context.Database.AddPartitioning();
context.SaveChanges();
}
public void Dispose()
{
var dbContext = new PersistencePostgresContext(
new DbContextOptionsBuilder<PersistencePostgresContext>()
.UseNpgsql(connectionString)
.Options);
dbContext.Database.EnsureDeleted();
GC.SuppressFinalize(this);
}
}

View File

@ -1,65 +0,0 @@
using DD.Persistence.Database.Entity;
using Mapster;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Npgsql;
namespace DD.Persistence.Database.Postgres.Test;
public class UnitTestCheckHyperTables : IClassFixture<DbFixture>
{
private ServiceProvider _serviceProvider;
public UnitTestCheckHyperTables(DbFixture fixture)
{
_serviceProvider = fixture.serviceProvider;
}
[Fact]
public void CreateHyperTable_For_ParameterData_Return_Success()
{
var chunksCount = 0;
var entity = new ParameterData()
{
DiscriminatorId = Guid.NewGuid(),
ParameterId = 1,
Timestamp = DateTime.UtcNow,
Value = "123"
};
using (var context = _serviceProvider.GetService<PersistenceDbContext>()!)
{
context.ParameterData.Add(entity);
var entity2 = entity.Adapt<ParameterData>();
entity2.ParameterId = 2;
context.ParameterData.Add(entity2);
var entity3 = entity2.Adapt<ParameterData>();
entity3.ParameterId = 3;
context.ParameterData.Add(entity3);
var entity4 = entity3.Adapt<ParameterData>();
entity4.ParameterId = 4;
context.ParameterData.Add(entity4);
var entity5 = entity3.Adapt<ParameterData>();
entity5.Timestamp = DateTime.UtcNow.AddDays(1).AddHours(1);
context.ParameterData.Add(entity5);
var entity6 = entity3.Adapt<ParameterData>();
entity6.DiscriminatorId = Guid.CreateVersion7();
context.ParameterData.Add(entity6);
context.SaveChanges();
string sql = "select count(*) from (select show_chunks('parameter_data'));";
var queryRow = context.Database.SqlQueryRaw<int>(sql);
chunksCount = queryRow.AsEnumerable().FirstOrDefault();
}
Assert.Equal(5, chunksCount);
}
}

View File

@ -1,13 +1,6 @@
using DD.Persistence.Database.Entity;
using DD.Persistence.Database.Postgres.Repositories;
using DD.Persistence.Database.Postgres.RepositoriesCached;
using DD.Persistence.Models;
using DD.Persistence.Repositories;
using Mapster;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System.Reflection;
namespace DD.Persistence.Database.Model;

View File

@ -25,8 +25,6 @@ public static class EFExtensionsPartitioning
/// <param name="db"></param>
private static void AddParameterDataPartitioning(this DatabaseFacade db)
{
var dayCount = 1;
var sectionCount = 128;
var type = typeof(ParameterData);
var tableAttribute = type.GetCustomAttribute<TableAttribute>();
if (tableAttribute is null)
@ -34,16 +32,13 @@ public static class EFExtensionsPartitioning
return;
}
var sqlCreateHypertableString = $"SELECT create_hypertable('{tableAttribute.Name}'," +
$"by_range('{nameof(ParameterData.Timestamp)}', INTERVAL '{dayCount} day'), if_not_exists => {true});";
db.ExecuteSqlRaw(sqlCreateHypertableString);
var sqlCreateDimensionParameterId = $"SELECT add_dimension('{tableAttribute.Name}'," +
$"by_hash('{nameof(ParameterData.ParameterId)}', {sectionCount}), if_not_exists => {true});";
db.ExecuteSqlRaw(sqlCreateDimensionParameterId);
var sqlCreateDimensionDiscriminatorId = $"SELECT add_dimension('{tableAttribute.Name}'," +
$"by_hash('{nameof(ParameterData.DiscriminatorId)}', {sectionCount}), if_not_exists => {true});";
db.ExecuteSqlRaw(sqlCreateDimensionDiscriminatorId);
const int sectionsNumber = 2;
const int chunkTimeInterval = 5;
var sqlString = $"SELECT create_hypertable({tableAttribute.Name}," +
$"'{nameof(ParameterData.Timestamp)}'," +
$"'{nameof(ParameterData.ParameterId)}'," +
$"{sectionsNumber}," +
$"chunk_time_interval => INTERVAL '{chunkTimeInterval} day');";
db.ExecuteSqlRaw(sqlString);
}
}

View File

@ -13,7 +13,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace DD.Persistence.Database.Postgres.Migrations
{
[DbContext(typeof(PersistencePostgresContext))]
[Migration("20250210055116_Init")]
[Migration("20250122120353_Init")]
partial class Init
{
/// <inheritdoc />
@ -26,45 +26,21 @@ namespace DD.Persistence.Database.Postgres.Migrations
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("DD.Persistence.Database.Entity.ChangeLog", b =>
modelBuilder.Entity("DD.Persistence.Database.Entity.DataScheme", b =>
{
b.Property<Guid>("Id")
b.Property<Guid>("DiscriminatorId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasComment("Ключ записи");
.HasComment("Идентификатор схемы данных");
b.Property<DateTimeOffset>("Creation")
.HasColumnType("timestamp with time zone")
.HasComment("Дата создания записи");
b.Property<Guid>("DiscriminatorId")
.HasColumnType("uuid")
.HasComment("Дискриминатор таблицы");
b.Property<Guid>("IdAuthor")
.HasColumnType("uuid")
.HasComment("Автор изменения");
b.Property<Guid?>("IdEditor")
.HasColumnType("uuid")
.HasComment("Редактор");
b.Property<Guid?>("IdNext")
.HasColumnType("uuid")
.HasComment("Id заменяющей записи");
b.Property<DateTimeOffset?>("Obsolete")
.HasColumnType("timestamp with time zone")
.HasComment("Дата устаревания (например при удалении)");
b.Property<string>("Value")
b.Property<string>("PropNames")
.IsRequired()
.HasColumnType("jsonb")
.HasComment("Значение");
.HasComment("Наименования полей в порядке индексации");
b.HasKey("Id");
b.HasKey("DiscriminatorId");
b.ToTable("change_log");
b.ToTable("data_scheme");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.DataSourceSystem", b =>
@ -112,53 +88,6 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.ToTable("parameter_data");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.SchemeProperty", b =>
{
b.Property<Guid>("DiscriminatorId")
.HasColumnType("uuid")
.HasComment("Идентификатор схемы данных");
b.Property<int>("Index")
.HasColumnType("integer")
.HasComment("Индекс поля");
b.Property<byte>("PropertyKind")
.HasColumnType("smallint")
.HasComment("Тип индексируемого поля");
b.Property<string>("PropertyName")
.IsRequired()
.HasColumnType("text")
.HasComment("Наименования индексируемого поля");
b.HasKey("DiscriminatorId", "Index");
b.ToTable("scheme_property");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.Setpoint", b =>
{
b.Property<Guid>("Key")
.HasColumnType("uuid")
.HasComment("Ключ");
b.Property<DateTimeOffset>("Timestamp")
.HasColumnType("timestamp with time zone")
.HasComment("Дата создания уставки");
b.Property<Guid>("IdUser")
.HasColumnType("uuid")
.HasComment("Id автора последнего изменения");
b.Property<JsonElement>("Value")
.HasColumnType("jsonb")
.HasComment("Значение уставки");
b.HasKey("Key", "Timestamp");
b.ToTable("setpoint");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.TechMessage", b =>
{
b.Property<Guid>("EventId")
@ -214,6 +143,82 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.ToTable("timestamped_values");
});
modelBuilder.Entity("DD.Persistence.Database.Model.ChangeLog", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasComment("Ключ записи");
b.Property<DateTimeOffset>("Creation")
.HasColumnType("timestamp with time zone")
.HasComment("Дата создания записи");
b.Property<double>("DepthEnd")
.HasColumnType("double precision")
.HasComment("Глубина забоя на дату окончания интервала");
b.Property<double>("DepthStart")
.HasColumnType("double precision")
.HasComment("Глубина забоя на дату начала интервала");
b.Property<Guid>("IdAuthor")
.HasColumnType("uuid")
.HasComment("Автор изменения");
b.Property<Guid>("IdDiscriminator")
.HasColumnType("uuid")
.HasComment("Дискриминатор таблицы");
b.Property<Guid?>("IdEditor")
.HasColumnType("uuid")
.HasComment("Редактор");
b.Property<Guid?>("IdNext")
.HasColumnType("uuid")
.HasComment("Id заменяющей записи");
b.Property<Guid>("IdSection")
.HasColumnType("uuid")
.HasComment("Ключ секции");
b.Property<DateTimeOffset?>("Obsolete")
.HasColumnType("timestamp with time zone")
.HasComment("Дата устаревания (например при удалении)");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("jsonb")
.HasComment("Значение");
b.HasKey("Id");
b.ToTable("change_log");
});
modelBuilder.Entity("DD.Persistence.Database.Model.Setpoint", b =>
{
b.Property<Guid>("Key")
.HasColumnType("uuid")
.HasComment("Ключ");
b.Property<DateTimeOffset>("Timestamp")
.HasColumnType("timestamp with time zone")
.HasComment("Дата создания уставки");
b.Property<Guid>("IdUser")
.HasColumnType("uuid")
.HasComment("Id автора последнего изменения");
b.Property<JsonElement>("Value")
.HasColumnType("jsonb")
.HasComment("Значение уставки");
b.HasKey("Key", "Timestamp");
b.ToTable("setpoint");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.TechMessage", b =>
{
b.HasOne("DD.Persistence.Database.Entity.DataSourceSystem", "System")
@ -224,6 +229,17 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.Navigation("System");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.TimestampedValues", b =>
{
b.HasOne("DD.Persistence.Database.Entity.DataScheme", "DataScheme")
.WithMany()
.HasForeignKey("DiscriminatorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("DataScheme");
});
#pragma warning restore 612, 618
}
}

View File

@ -17,12 +17,15 @@ namespace DD.Persistence.Database.Postgres.Migrations
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false, comment: "Ключ записи"),
DiscriminatorId = table.Column<Guid>(type: "uuid", nullable: false, comment: "Дискриминатор таблицы"),
IdDiscriminator = table.Column<Guid>(type: "uuid", nullable: false, comment: "Дискриминатор таблицы"),
IdAuthor = table.Column<Guid>(type: "uuid", nullable: false, comment: "Автор изменения"),
IdEditor = table.Column<Guid>(type: "uuid", nullable: true, comment: "Редактор"),
Creation = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, comment: "Дата создания записи"),
Obsolete = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true, comment: "Дата устаревания (например при удалении)"),
IdNext = table.Column<Guid>(type: "uuid", nullable: true, comment: "Id заменяющей записи"),
DepthStart = table.Column<double>(type: "double precision", nullable: false, comment: "Глубина забоя на дату начала интервала"),
DepthEnd = table.Column<double>(type: "double precision", nullable: false, comment: "Глубина забоя на дату окончания интервала"),
IdSection = table.Column<Guid>(type: "uuid", nullable: false, comment: "Ключ секции"),
Value = table.Column<string>(type: "jsonb", nullable: false, comment: "Значение")
},
constraints: table =>
@ -30,6 +33,18 @@ namespace DD.Persistence.Database.Postgres.Migrations
table.PrimaryKey("PK_change_log", x => x.Id);
});
migrationBuilder.CreateTable(
name: "data_scheme",
columns: table => new
{
DiscriminatorId = table.Column<Guid>(type: "uuid", nullable: false, comment: "Идентификатор схемы данных"),
PropNames = table.Column<string>(type: "jsonb", nullable: false, comment: "Наименования полей в порядке индексации")
},
constraints: table =>
{
table.PrimaryKey("PK_data_scheme", x => x.DiscriminatorId);
});
migrationBuilder.CreateTable(
name: "data_source_system",
columns: table => new
@ -57,20 +72,6 @@ namespace DD.Persistence.Database.Postgres.Migrations
table.PrimaryKey("PK_parameter_data", x => new { x.DiscriminatorId, x.ParameterId, x.Timestamp });
});
migrationBuilder.CreateTable(
name: "scheme_property",
columns: table => new
{
DiscriminatorId = table.Column<Guid>(type: "uuid", nullable: false, comment: "Идентификатор схемы данных"),
Index = table.Column<int>(type: "integer", nullable: false, comment: "Индекс поля"),
PropertyName = table.Column<string>(type: "text", nullable: false, comment: "Наименования индексируемого поля"),
PropertyKind = table.Column<byte>(type: "smallint", nullable: false, comment: "Тип индексируемого поля")
},
constraints: table =>
{
table.PrimaryKey("PK_scheme_property", x => new { x.DiscriminatorId, x.Index });
});
migrationBuilder.CreateTable(
name: "setpoint",
columns: table => new
@ -96,6 +97,12 @@ namespace DD.Persistence.Database.Postgres.Migrations
constraints: table =>
{
table.PrimaryKey("PK_timestamped_values", x => new { x.DiscriminatorId, x.Timestamp });
table.ForeignKey(
name: "FK_timestamped_values_data_scheme_DiscriminatorId",
column: x => x.DiscriminatorId,
principalTable: "data_scheme",
principalColumn: "DiscriminatorId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
@ -135,9 +142,6 @@ namespace DD.Persistence.Database.Postgres.Migrations
migrationBuilder.DropTable(
name: "parameter_data");
migrationBuilder.DropTable(
name: "scheme_property");
migrationBuilder.DropTable(
name: "setpoint");
@ -149,6 +153,9 @@ namespace DD.Persistence.Database.Postgres.Migrations
migrationBuilder.DropTable(
name: "data_source_system");
migrationBuilder.DropTable(
name: "data_scheme");
}
}
}

View File

@ -23,45 +23,21 @@ namespace DD.Persistence.Database.Postgres.Migrations
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("DD.Persistence.Database.Entity.ChangeLog", b =>
modelBuilder.Entity("DD.Persistence.Database.Entity.DataScheme", b =>
{
b.Property<Guid>("Id")
b.Property<Guid>("DiscriminatorId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasComment("Ключ записи");
.HasComment("Идентификатор схемы данных");
b.Property<DateTimeOffset>("Creation")
.HasColumnType("timestamp with time zone")
.HasComment("Дата создания записи");
b.Property<Guid>("DiscriminatorId")
.HasColumnType("uuid")
.HasComment("Дискриминатор таблицы");
b.Property<Guid>("IdAuthor")
.HasColumnType("uuid")
.HasComment("Автор изменения");
b.Property<Guid?>("IdEditor")
.HasColumnType("uuid")
.HasComment("Редактор");
b.Property<Guid?>("IdNext")
.HasColumnType("uuid")
.HasComment("Id заменяющей записи");
b.Property<DateTimeOffset?>("Obsolete")
.HasColumnType("timestamp with time zone")
.HasComment("Дата устаревания (например при удалении)");
b.Property<string>("Value")
b.Property<string>("PropNames")
.IsRequired()
.HasColumnType("jsonb")
.HasComment("Значение");
.HasComment("Наименования полей в порядке индексации");
b.HasKey("Id");
b.HasKey("DiscriminatorId");
b.ToTable("change_log");
b.ToTable("data_scheme");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.DataSourceSystem", b =>
@ -109,53 +85,6 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.ToTable("parameter_data");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.SchemeProperty", b =>
{
b.Property<Guid>("DiscriminatorId")
.HasColumnType("uuid")
.HasComment("Идентификатор схемы данных");
b.Property<int>("Index")
.HasColumnType("integer")
.HasComment("Индекс поля");
b.Property<byte>("PropertyKind")
.HasColumnType("smallint")
.HasComment("Тип индексируемого поля");
b.Property<string>("PropertyName")
.IsRequired()
.HasColumnType("text")
.HasComment("Наименования индексируемого поля");
b.HasKey("DiscriminatorId", "Index");
b.ToTable("scheme_property");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.Setpoint", b =>
{
b.Property<Guid>("Key")
.HasColumnType("uuid")
.HasComment("Ключ");
b.Property<DateTimeOffset>("Timestamp")
.HasColumnType("timestamp with time zone")
.HasComment("Дата создания уставки");
b.Property<Guid>("IdUser")
.HasColumnType("uuid")
.HasComment("Id автора последнего изменения");
b.Property<JsonElement>("Value")
.HasColumnType("jsonb")
.HasComment("Значение уставки");
b.HasKey("Key", "Timestamp");
b.ToTable("setpoint");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.TechMessage", b =>
{
b.Property<Guid>("EventId")
@ -211,6 +140,82 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.ToTable("timestamped_values");
});
modelBuilder.Entity("DD.Persistence.Database.Model.ChangeLog", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasComment("Ключ записи");
b.Property<DateTimeOffset>("Creation")
.HasColumnType("timestamp with time zone")
.HasComment("Дата создания записи");
b.Property<double>("DepthEnd")
.HasColumnType("double precision")
.HasComment("Глубина забоя на дату окончания интервала");
b.Property<double>("DepthStart")
.HasColumnType("double precision")
.HasComment("Глубина забоя на дату начала интервала");
b.Property<Guid>("IdAuthor")
.HasColumnType("uuid")
.HasComment("Автор изменения");
b.Property<Guid>("IdDiscriminator")
.HasColumnType("uuid")
.HasComment("Дискриминатор таблицы");
b.Property<Guid?>("IdEditor")
.HasColumnType("uuid")
.HasComment("Редактор");
b.Property<Guid?>("IdNext")
.HasColumnType("uuid")
.HasComment("Id заменяющей записи");
b.Property<Guid>("IdSection")
.HasColumnType("uuid")
.HasComment("Ключ секции");
b.Property<DateTimeOffset?>("Obsolete")
.HasColumnType("timestamp with time zone")
.HasComment("Дата устаревания (например при удалении)");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("jsonb")
.HasComment("Значение");
b.HasKey("Id");
b.ToTable("change_log");
});
modelBuilder.Entity("DD.Persistence.Database.Model.Setpoint", b =>
{
b.Property<Guid>("Key")
.HasColumnType("uuid")
.HasComment("Ключ");
b.Property<DateTimeOffset>("Timestamp")
.HasColumnType("timestamp with time zone")
.HasComment("Дата создания уставки");
b.Property<Guid>("IdUser")
.HasColumnType("uuid")
.HasComment("Id автора последнего изменения");
b.Property<JsonElement>("Value")
.HasColumnType("jsonb")
.HasComment("Значение уставки");
b.HasKey("Key", "Timestamp");
b.ToTable("setpoint");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.TechMessage", b =>
{
b.HasOne("DD.Persistence.Database.Entity.DataSourceSystem", "System")
@ -221,6 +226,17 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.Navigation("System");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.TimestampedValues", b =>
{
b.HasOne("DD.Persistence.Database.Entity.DataScheme", "DataScheme")
.WithMany()
.HasForeignKey("DiscriminatorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("DataScheme");
});
#pragma warning restore 612, 618
}
}

View File

@ -1,9 +1,9 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace DD.Persistence.Database.Model;
/// <summary>
/// EF контекст для базы данных Postgres
/// EF êîíòåêñò äëÿ ÁÄ Postgres
/// </summary>
public partial class PersistencePostgresContext : PersistenceDbContext
{

View File

@ -0,0 +1,56 @@
using DD.Persistence.Services.Interfaces;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Npgsql;
namespace DD.Persistence.Database.Postgres.Services;
/// <summary>
/// Сервис по работе с источником данных - базой postgres
/// </summary>
public class PostgresArchiveService : IArchiveService
{
private string connectionString;
private ILogger<PostgresArchiveService> logger;
/// <summary>
/// Сервис по работе с источником данных - базой postgres
/// </summary>
/// <param name="configuration"></param>
public PostgresArchiveService(IConfiguration configuration, ILogger<PostgresArchiveService> logger)
{
this.connectionString = configuration.GetConnectionString("SystemBaseConnection")!;
this.logger = logger;
}
public Task Create(string newName, CancellationToken token)
{
throw new NotImplementedException();
}
public async Task Rename(string newName, CancellationToken token)
{
await using var connection = new NpgsqlConnection(connectionString);
try
{
await connection.OpenAsync();
var sqlCloseConnect =
$"SELECT pg_terminate_backend(pg_stat_activity.pid) " +
$"FROM pg_stat_activity " +
$"WHERE pg_stat_activity.datname = 'persistence' AND pid <> pg_backend_pid();";
await using var command = new NpgsqlCommand(sqlCloseConnect, connection);
await command.ExecuteNonQueryAsync();
var rename = $"ALTER DATABASE persistence RENAME TO {newName};";
await using var sqlRenameDatabase = new NpgsqlCommand(rename, connection);
await sqlRenameDatabase.ExecuteNonQueryAsync();
}
catch (NpgsqlException exception)
{
this.logger.LogError(exception.Message);
throw;
}
}
}

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
@ -7,14 +7,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Ardalis.Specification.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Mapster" Version="7.4.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="UuidExtensions" Version="1.2.0" />
</ItemGroup>
<ItemGroup>

View File

@ -1,4 +1,4 @@

using DD.Persistence.Database.EntityAbstractions;
using DD.Persistence.ModelsAbstractions;
using Microsoft.EntityFrameworkCore;
@ -11,13 +11,13 @@ namespace DD.Persistence.Database.Entity;
/// Часть записи, описывающая изменение
/// </summary>
[Table("change_log")]
public class ChangeLog : IDiscriminatorItem, IChangeLog
public class ChangeLog : IChangeLog, IWithSectionPart
{
[Key, Comment("Ключ записи")]
public Guid Id { get; set; }
[Comment("Дискриминатор таблицы")]
public Guid DiscriminatorId { get; set; }
public Guid IdDiscriminator { get; set; }
[Comment("Автор изменения")]
public Guid IdAuthor { get; set; }
@ -34,6 +34,15 @@ public class ChangeLog : IDiscriminatorItem, IChangeLog
[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<string, object> Value { get; set; }
}

View File

@ -0,0 +1,15 @@
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace DD.Persistence.Database.Entity;
[Table("data_scheme")]
public class DataScheme
{
[Key, Comment("Идентификатор схемы данных"),]
public Guid DiscriminatorId { get; set; }
[Comment("Наименования полей в порядке индексации"), Column(TypeName = "jsonb")]
public string[] PropNames { get; set; } = [];
}

View File

@ -7,7 +7,7 @@ namespace DD.Persistence.Database.Entity;
[Table("parameter_data")]
[PrimaryKey(nameof(DiscriminatorId), nameof(ParameterId), nameof(Timestamp))]
public class ParameterData : IDiscriminatorItem, ITimestampedItem
public class ParameterData : ITimestampedItem
{
[Required, Comment("Дискриминатор системы")]
public Guid DiscriminatorId { get; set; }

View File

@ -1,24 +0,0 @@
using DD.Persistence.Database.EntityAbstractions;
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json;
namespace DD.Persistence.Database.Entity;
[Table("scheme_property")]
[PrimaryKey(nameof(DiscriminatorId), nameof(Index))]
public class SchemeProperty : IDiscriminatorItem
{
[Comment("Идентификатор схемы данных")]
public Guid DiscriminatorId { get; set; }
[Comment("Индекс поля")]
public int Index { get; set; }
[Comment("Наименования индексируемого поля")]
public required string PropertyName { get; set; }
[Comment("Тип индексируемого поля")]
public required JsonValueKind PropertyKind { get; set; }
}

View File

@ -7,7 +7,7 @@ namespace DD.Persistence.Database.Entity;
[Table("timestamped_values")]
[PrimaryKey(nameof(DiscriminatorId), nameof(Timestamp))]
public class TimestampedValues : IDiscriminatorItem, ITimestampedItem, IValuesItem
public class TimestampedValues : ITimestampedItem
{
[Comment("Временная отметка"), Key]
public DateTimeOffset Timestamp { get; set; }
@ -17,4 +17,7 @@ public class TimestampedValues : IDiscriminatorItem, ITimestampedItem, IValuesIt
[Comment("Данные"), Column(TypeName = "jsonb")]
public required object[] Values { get; set; }
[Required, ForeignKey(nameof(DiscriminatorId)), Comment("Идентификаторы")]
public virtual DataScheme? DataScheme { get; set; }
}

View File

@ -38,7 +38,7 @@ public interface IChangeLog
/// <summary>
/// Дискриминатор таблицы
/// </summary>
public Guid DiscriminatorId { get; set; }
public Guid IdDiscriminator { get; set; }
/// <summary>
/// Значение

View File

@ -1,8 +0,0 @@
namespace DD.Persistence.Database.EntityAbstractions;
public interface IDiscriminatorItem
{
/// <summary>
/// Дискриминатор
/// </summary>
Guid DiscriminatorId { get; set; }
}

View File

@ -1,14 +0,0 @@
using DD.Persistence.Database.Entity;
namespace DD.Persistence.Database.EntityAbstractions;
/// <summary>
/// Сущность с данными, принадлежащими к определенной схеме
/// </summary>
public interface IValuesItem
{
/// <summary>
/// Значения
/// </summary>
object[] Values { get; set; }
}

View File

@ -1,38 +0,0 @@
using Ardalis.Specification;
using DD.Persistence.Database.Helpers;
using System.Linq.Expressions;
namespace DD.Persistence.Database.Postgres.Extensions;
public static class SpecificationExtensions
{
public static Expression<Func<T, bool>>? Or<T>(this ISpecification<T> spec, ISpecification<T> otherSpec)
{
var parameter = Expression.Parameter(typeof(T), "x");
var exprSpec1 = CombineWhereExpressions(spec.WhereExpressions, parameter);
var exprSpec2 = CombineWhereExpressions(otherSpec.WhereExpressions, parameter);
Expression? orExpression = exprSpec1 is not null && exprSpec2 is not null
? Expression.OrElse(exprSpec1, exprSpec2)
: exprSpec1 ?? exprSpec2;
if (orExpression is null)
return null;
var lambdaExpr = Expression.Lambda<Func<T, bool>>(orExpression, parameter);
return lambdaExpr;
}
public static Expression? CombineWhereExpressions<T>(IEnumerable<WhereExpressionInfo<T>> whereExpressions, ParameterExpression parameter)
{
Expression? newExpr = null;
foreach (var where in whereExpressions)
{
var expr = ParameterReplacerVisitor.Replace(where.Filter.Body, where.Filter.Parameters[0], parameter);
newExpr = newExpr is null ? expr : Expression.AndAlso(newExpr, expr);
}
return newExpr;
}
}

View File

@ -1,116 +0,0 @@
using Ardalis.Specification;
using Ardalis.Specification.EntityFrameworkCore;
using DD.Persistence.Database.Entity;
using DD.Persistence.Database.EntityAbstractions;
using DD.Persistence.Database.Specifications.Operation;
using DD.Persistence.Database.Specifications.ValuesItem;
using DD.Persistence.Filter.Models;
using DD.Persistence.Filter.Models.Abstractions;
using DD.Persistence.Filter.Models.Enumerations;
using DD.Persistence.Filter.Visitors;
using DD.Persistence.Models;
using System.Text.Json;
namespace DD.Persistence.Database.Postgres.Helpers;
public static class FilterBuilder
{
public static IQueryable<TEntity> ApplyFilter<TEntity>(this IQueryable<TEntity> query, DataSchemeDto dataSchemeDto, TNode root)
where TEntity : class, IValuesItem
{
var filterSpec = dataSchemeDto.BuildFilter<TEntity>(root);
if (filterSpec != null)
return query.WithSpecification(filterSpec);
return query;
}
private static ISpecification<TEntity>? BuildFilter<TEntity>(this DataSchemeDto dataSchemeDto, TNode root)
where TEntity : IValuesItem
{
var result = dataSchemeDto.BuildSpecificationByNextNode<TEntity>(root);
return result;
}
private static ISpecification<TEntity>? BuildSpecificationByNextNode<TEntity>(this DataSchemeDto dataSchemeDto, TNode node)
where TEntity : IValuesItem
{
var visitor = new NodeVisitor<ISpecification<TEntity>?>(
dataSchemeDto.VertexProcessing<TEntity>,
dataSchemeDto.LeafProcessing<TEntity>
);
var result = node.AcceptVisitor(visitor);
return result;
}
private static ISpecification<TEntity>? VertexProcessing<TEntity>(this DataSchemeDto dataSchemeDto, TVertex vertex)
where TEntity : IValuesItem
{
var leftSpecification = dataSchemeDto.BuildSpecificationByNextNode<TEntity>(vertex.Left);
var rigthSpecification = dataSchemeDto.BuildSpecificationByNextNode<TEntity>(vertex.Rigth);
if (leftSpecification is null)
return rigthSpecification;
if (rigthSpecification is null)
return leftSpecification;
ISpecification<TEntity>? result = null;
switch (vertex.Operation)
{
case OperationEnum.And:
result = new AndSpec<TEntity>(leftSpecification, rigthSpecification);
break;
case OperationEnum.Or:
result = new OrSpec<TEntity>(leftSpecification, rigthSpecification);
break;
}
return result;
}
private static ISpecification<TEntity>? LeafProcessing<TEntity>(this DataSchemeDto dataSchemeDto, TLeaf leaf)
where TEntity : IValuesItem
{
var schemeProperty = dataSchemeDto.FirstOrDefault(e => e.PropertyName.Equals(leaf.PropName));
if (schemeProperty is null)
throw new ArgumentException($"Свойство {leaf.PropName} не найдено в схеме данных");
ISpecification<TEntity>? result = null;
switch (schemeProperty.PropertyKind)
{
case JsonValueKind.String:
var stringValue = Convert.ToString(leaf.Value);
var stringSpecifications = StringSpecifications<TEntity>();
result = stringSpecifications[leaf.Operation](schemeProperty.Index, stringValue);
break;
case JsonValueKind.Number:
var doubleValue = Convert.ToDouble(leaf.Value);
var doubleSpecifications = DoubleSpecifications<TEntity>();
result = doubleSpecifications[leaf.Operation](schemeProperty.Index, doubleValue);
break;
}
return result;
}
private static Dictionary<OperationEnum, Func<int, string?, ISpecification<TEntity>>> StringSpecifications<TEntity>()
where TEntity : IValuesItem => new()
{
{ OperationEnum.Equal, (int index, string? value) => new ValueEqualSpec<TEntity>(index, value) },
{ OperationEnum.NotEqual, (int index, string? value) => new ValueNotEqualSpec<TEntity>(index, value) },
{ OperationEnum.Greate, (int index, string? value) => new ValueGreateSpec<TEntity>(index, value) },
{ OperationEnum.GreateOrEqual, (int index, string? value) => new ValueGreateOrEqualSpec<TEntity>(index, value) },
{ OperationEnum.Less, (int index, string? value) => new ValueLessSpec<TEntity>(index, value) },
{ OperationEnum.LessOrEqual, (int index, string? value) => new ValueLessOrEqualSpec<TEntity>(index, value) }
};
private static Dictionary<OperationEnum, Func<int, double?, ISpecification<TEntity>>> DoubleSpecifications<TEntity>()
where TEntity : IValuesItem => new()
{
{ OperationEnum.Equal, (int index, double? value) => new ValueEqualSpec<TEntity>(index, value) },
{ OperationEnum.NotEqual, (int index, double? value) => new ValueNotEqualSpec<TEntity>(index, value) },
{ OperationEnum.Greate, (int index, double? value) => new ValueGreateSpec<TEntity>(index, value) },
{ OperationEnum.GreateOrEqual, (int index, double? value) => new ValueGreateOrEqualSpec<TEntity>(index, value) },
{ OperationEnum.Less, (int index, double? value) => new ValueLessSpec<TEntity>(index, value) },
{ OperationEnum.LessOrEqual, (int index, double? value) => new ValueLessOrEqualSpec<TEntity>(index, value) }
};
}

View File

@ -1,20 +0,0 @@
using System.Linq.Expressions;
namespace DD.Persistence.Database.Helpers;
public class ParameterReplacerVisitor : ExpressionVisitor
{
private readonly Expression _newExpression;
private readonly ParameterExpression _oldParameter;
private ParameterReplacerVisitor(ParameterExpression oldParameter, Expression newExpression)
{
_oldParameter = oldParameter;
_newExpression = newExpression;
}
internal static Expression Replace(Expression expression, ParameterExpression oldParameter, Expression newExpression)
=> new ParameterReplacerVisitor(oldParameter, newExpression).Visit(expression);
protected override Expression VisitParameter(ParameterExpression p)
=> p == _oldParameter ? _newExpression : p;
}

View File

@ -10,7 +10,7 @@ public class PersistenceDbContext : DbContext
{
public DbSet<Setpoint> Setpoint => Set<Setpoint>();
public DbSet<SchemeProperty> SchemeProperty => Set<SchemeProperty>();
public DbSet<DataScheme> DataSchemes => Set<DataScheme>();
public DbSet<TimestampedValues> TimestampedValues => Set<TimestampedValues>();
@ -30,6 +30,10 @@ public class PersistenceDbContext : DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<DataScheme>()
.Property(e => e.PropNames)
.HasJsonConversion();
modelBuilder.Entity<TimestampedValues>()
.Property(e => e.Values)
.HasJsonConversion();

View File

@ -1,43 +0,0 @@
using DD.Persistence.Database.Entity;
using DD.Persistence.Models;
using DD.Persistence.Repositories;
using Mapster;
using Microsoft.EntityFrameworkCore;
namespace DD.Persistence.Database.Repositories;
public class SchemePropertyRepository : ISchemePropertyRepository
{
protected DbContext db;
public SchemePropertyRepository(DbContext db)
{
this.db = db;
}
protected IQueryable<SchemeProperty> GetQueryReadOnly() => db.Set<SchemeProperty>();
public virtual async Task AddRange(DataSchemeDto dataSchemeDto, CancellationToken token)
{
var entities = dataSchemeDto.Select(e =>
KeyValuePair.Create(dataSchemeDto.DiscriminatorId, e)
.Adapt<SchemeProperty>()
);
await db.Set<SchemeProperty>().AddRangeAsync(entities, token);
await db.SaveChangesAsync(token);
}
public virtual async Task<DataSchemeDto?> Get(Guid dataSchemeId, CancellationToken token)
{
var query = GetQueryReadOnly()
.Where(e => e.DiscriminatorId == dataSchemeId);
var entities = await query.ToArrayAsync(token);
DataSchemeDto? result = null;
if (entities.Length != 0)
{
var properties = entities.Select(e => e.Adapt<SchemePropertyDto>()).ToArray();
result = new DataSchemeDto(dataSchemeId, properties);
}
return result;
}
}

View File

@ -1,180 +0,0 @@
using DD.Persistence.Database.Entity;
using DD.Persistence.Database.Postgres.Helpers;
using DD.Persistence.Filter.Models.Abstractions;
using DD.Persistence.Models;
using DD.Persistence.Models.Common;
using DD.Persistence.Repositories;
using Microsoft.EntityFrameworkCore;
namespace DD.Persistence.Database.Postgres.Repositories;
public class TimestampedValuesRepository : ITimestampedValuesRepository
{
private readonly DbContext db;
private readonly ISchemePropertyRepository schemePropertyRepository;
public TimestampedValuesRepository(DbContext db, ISchemePropertyRepository schemePropertyRepository)
{
this.db = db;
this.schemePropertyRepository = schemePropertyRepository;
}
protected IQueryable<TimestampedValues> GetQueryReadOnly() => db.Set<TimestampedValues>();
public async Task<int> AddRange(Guid discriminatorId, IEnumerable<TimestampedValuesDto> dtos, CancellationToken token)
{
var timestampedValuesEntities = dtos.Select(dto => new TimestampedValues()
{
DiscriminatorId = discriminatorId,
Timestamp = dto.Timestamp.ToUniversalTime(),
Values = dto.Values.Values.ToArray()
});
await db.AddRangeAsync(timestampedValuesEntities, token);
var result = await db.SaveChangesAsync(token);
return result;
}
public async Task<IDictionary<Guid, IEnumerable<(DateTimeOffset Timestamp, object[] Values)>>> Get(IEnumerable<Guid> discriminatorIds,
DateTimeOffset? geTimestamp,
TNode? filterTree,
IEnumerable<string>? columnNames,
int skip,
int take,
CancellationToken token)
{
var resultQuery = Array.Empty<TimestampedValues>().AsQueryable();
foreach (var discriminatorId in discriminatorIds)
{
var scheme = await schemePropertyRepository.Get(discriminatorId, token);
if (scheme == null)
throw new NotSupportedException($"Для переданного дискриминатора {discriminatorId} не была обнаружена схема данных");
var geTimestampUtc = geTimestamp!.Value.ToUniversalTime();
var query = GetQueryReadOnly()
.Where(e => e.DiscriminatorId == discriminatorId)
.Where(entity => entity.Timestamp >= geTimestampUtc);
if (filterTree != null)
query = query.ApplyFilter(scheme, filterTree);
resultQuery = resultQuery.Any() ? resultQuery.Union(query) : query;
}
var groupedQuery = resultQuery!
.GroupBy(e => e.DiscriminatorId)
.Select(g => KeyValuePair.Create(
g.Key,
g.OrderBy(i => i.Timestamp).Skip(skip).Take(take))
);
var entities = await groupedQuery.ToArrayAsync(token);
var result = entities.ToDictionary(k => k.Key, v => v.Value.Select(e => (
e.Timestamp,
e.Values
)));
return result;
}
public async Task<IEnumerable<(DateTimeOffset Timestamp, object[] Values)>> GetFirst(Guid discriminatorId, int takeCount, CancellationToken token)
{
var query = GetQueryReadOnly()
.OrderBy(e => e.Timestamp)
.Take(takeCount);
var entities = await query.ToArrayAsync(token);
var result = entities.Select(e => (
e.Timestamp,
e.Values
));
return result;
}
public async Task<IEnumerable<(DateTimeOffset Timestamp, object[] Values)>> GetLast(Guid discriminatorId, int takeCount, CancellationToken token)
{
var query = GetQueryReadOnly()
.OrderByDescending(e => e.Timestamp)
.Take(takeCount);
var entities = await query.ToArrayAsync(token);
var result = entities.Select(e => (
e.Timestamp,
e.Values
));
return result;
}
// ToDo: прореживание должно осуществляться до материализации
public async Task<IEnumerable<(DateTimeOffset Timestamp, object[] Values)>> GetResampledData(
Guid discriminatorId,
DateTimeOffset dateBegin,
double intervalSec = 600d,
int approxPointsCount = 1024,
CancellationToken token = default)
{
var result = await GetGtDate(discriminatorId, dateBegin, token);
var dateEnd = dateBegin.AddSeconds(intervalSec);
result = result
.Where(i => i.Item1 <= dateEnd);
var ratio = result.Count() / approxPointsCount;
if (ratio > 1)
result = result
.Where((_, index) => index % ratio == 0);
return result;
}
public async Task<IEnumerable<(DateTimeOffset Timestamp, object[] Values)>> GetGtDate(Guid discriminatorId, DateTimeOffset gtTimestamp, CancellationToken token)
{
var gtTimestampUtc = gtTimestamp.ToUniversalTime();
var query = GetQueryReadOnly()
.Where(entity => entity.Timestamp > gtTimestampUtc);
var entities = await query.ToArrayAsync(token);
var result = entities.Select(e => (
e.Timestamp,
e.Values
));
return result;
}
public async Task<DatesRangeDto?> GetDatesRange(Guid discriminatorId, CancellationToken token)
{
var query = GetQueryReadOnly()
.GroupBy(entity => entity.DiscriminatorId)
.Select(group => new
{
Min = group.Min(entity => entity.Timestamp),
Max = group.Max(entity => entity.Timestamp),
});
var item = await query.FirstOrDefaultAsync(token);
if (item is null)
{
return null;
}
var dto = new DatesRangeDto
{
From = item.Min,
To = item.Max,
};
return dto;
}
public async Task<int> Count(Guid discriminatorId, CancellationToken token)
{
var query = GetQueryReadOnly()
.Where(e => e.DiscriminatorId == discriminatorId);
var result = await query.CountAsync(token);
return result;
}
}

View File

@ -1,22 +0,0 @@
using Ardalis.Specification;
namespace DD.Persistence.Database.Specifications.Operation;
public class AndSpec<TEntity> : Specification<TEntity>
{
public AndSpec(ISpecification<TEntity> first, ISpecification<TEntity> second)
{
if (first is null || second is null)
return;
ApplyCriteria(first);
ApplyCriteria(second);
}
private void ApplyCriteria(ISpecification<TEntity> specification)
{
foreach (var criteria in specification.WhereExpressions)
{
Query.Where(criteria.Filter);
}
}
}

View File

@ -1,15 +0,0 @@
using Ardalis.Specification;
using DD.Persistence.Database.Postgres.Extensions;
namespace DD.Persistence.Database.Specifications.Operation;
public class OrSpec<TEntity> : Specification<TEntity>
{
public OrSpec(ISpecification<TEntity> first, ISpecification<TEntity> second)
{
var orExpression = first.Or(second);
if (orExpression == null)
return;
Query.Where(orExpression);
}
}

View File

@ -1,22 +0,0 @@
using Ardalis.Specification;
using DD.Persistence.Database.EntityAbstractions;
namespace DD.Persistence.Database.Specifications.ValuesItem;
/// <summary>
/// Спецификация эквивалентности значений IValuesItem в соответствии с индексацией
/// </summary>
/// <typeparam name="TEntity"></typeparam>
public class ValueEqualSpec<TEntity> : Specification<TEntity>
where TEntity : IValuesItem
{
public ValueEqualSpec(int index, string? value)
{
Query.Where(e => Convert.ToString(e.Values[index]) == value);
}
public ValueEqualSpec(int index, double? value)
{
Query.Where(e => Convert.ToDouble(e.Values[index]) == value);
}
}

View File

@ -1,22 +0,0 @@
using Ardalis.Specification;
using DD.Persistence.Database.EntityAbstractions;
namespace DD.Persistence.Database.Specifications.ValuesItem;
/// <summary>
/// Спецификация "больше либо равно" для значений IValuesItem в соответствии с индексацией
/// </summary>
/// <typeparam name="TEntity"></typeparam>
public class ValueGreateOrEqualSpec<TEntity> : Specification<TEntity>
where TEntity : IValuesItem
{
public ValueGreateOrEqualSpec(int index, string? value)
{
Query.Where(e => string.Compare(Convert.ToString(e.Values[index]), value) >= 0);
}
public ValueGreateOrEqualSpec(int index, double? value)
{
Query.Where(e => Convert.ToDouble(e.Values[index]) >= value);
}
}

View File

@ -1,22 +0,0 @@
using Ardalis.Specification;
using DD.Persistence.Database.EntityAbstractions;
namespace DD.Persistence.Database.Specifications.ValuesItem;
/// <summary>
/// Спецификация "больше" для значений IValuesItem в соответствии с индексацией
/// </summary>
/// <typeparam name="TEntity"></typeparam>
public class ValueGreateSpec<TEntity> : Specification<TEntity>
where TEntity : IValuesItem
{
public ValueGreateSpec(int index, string? value)
{
Query.Where(e => string.Compare(Convert.ToString(e.Values[index]), value) > 0);
}
public ValueGreateSpec(int index, double? value)
{
Query.Where(e => Convert.ToDouble(e.Values[index]) > value);
}
}

View File

@ -1,22 +0,0 @@
using Ardalis.Specification;
using DD.Persistence.Database.EntityAbstractions;
namespace DD.Persistence.Database.Specifications.ValuesItem;
/// <summary>
/// Спецификация "меньше либо равно" для значений IValuesItem в соответствии с индексацией
/// </summary>
/// <typeparam name="TEntity"></typeparam>
public class ValueLessOrEqualSpec<TEntity> : Specification<TEntity>
where TEntity : IValuesItem
{
public ValueLessOrEqualSpec(int index, string? value)
{
Query.Where(e => string.Compare(Convert.ToString(e.Values[index]), value) <= 0);
}
public ValueLessOrEqualSpec(int index, double? value)
{
Query.Where(e => Convert.ToDouble(e.Values[index]) <= value);
}
}

View File

@ -1,22 +0,0 @@
using Ardalis.Specification;
using DD.Persistence.Database.EntityAbstractions;
namespace DD.Persistence.Database.Specifications.ValuesItem;
/// <summary>
/// Спецификация "меньше" для значений IValuesItem в соответствии с индексацией
/// </summary>
/// <typeparam name="TEntity"></typeparam>
public class ValueLessSpec<TEntity> : Specification<TEntity>
where TEntity : IValuesItem
{
public ValueLessSpec(int index, string? value)
{
Query.Where(e => string.Compare(Convert.ToString(e.Values[index]), value) < 0);
}
public ValueLessSpec(int index, double? value)
{
Query.Where(e => Convert.ToDouble(e.Values[index]) < value);
}
}

View File

@ -1,22 +0,0 @@
using Ardalis.Specification;
using DD.Persistence.Database.EntityAbstractions;
namespace DD.Persistence.Database.Specifications.ValuesItem;
/// <summary>
/// Спецификация неравенства значений IValuesItem в соответствии с индексацией
/// </summary>
/// <typeparam name="TEntity"></typeparam>
public class ValueNotEqualSpec<TEntity> : Specification<TEntity>
where TEntity : IValuesItem
{
public ValueNotEqualSpec(int index, string? value)
{
Query.Where(e => Convert.ToString(e.Values[index]) != value);
}
public ValueNotEqualSpec(int index, double? value)
{
Query.Where(e => Convert.ToDouble(e.Values[index]) != value);
}
}

View File

@ -1,63 +0,0 @@
using DD.Persistence.Models.Configurations;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text.Json;
namespace DD.Persistence.IntegrationTests;
/// <summary>
///  Класс, позволяющий генерировать api-token
/// </summary>
public static class ApiTokenHelper
{
/// <summary>
/// Метод авторизации
/// </summary>
/// <param name="httpClient"></param>
/// <param name="configuration"></param>
/// <returns></returns>
public static void Authorize(this HttpClient httpClient, IConfiguration configuration)
{
var authUser = configuration
.GetSection(nameof(AuthUser))
.Get<AuthUser>()!;
var jwtToken = authUser.CreateDefaultJwtToken();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", jwtToken);
}
/// <summary>
/// Авторизация через собственный jwt-токен
/// </summary>
/// <param name="authUser"></param>
/// <returns></returns>
private static string CreateDefaultJwtToken(this AuthUser authUser)
{
var nameIdetifier = Guid.NewGuid().ToString();
var claims = new List<Claim>()
{
new(ClaimTypes.NameIdentifier, nameIdetifier),
new("client_id", authUser.ClientId),
new("username", authUser.Username),
new("password", authUser.Password),
new("grant_type", authUser.GrantType),
new(ClaimTypes.NameIdentifier.ToString(), Guid.NewGuid().ToString())
};
var tokenDescriptor = new SecurityTokenDescriptor
{
Issuer = JwtParams.Issuer,
Audience = JwtParams.Audience,
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.AddHours(1),
SigningCredentials = new SigningCredentials(JwtParams.SecurityKey, SecurityAlgorithms.HmacSha256Signature)
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
}

View File

@ -50,7 +50,7 @@ public class ChangeLogControllerTest : BaseIntegrationTest
var insertedCount = 10;
var createdResult = CreateChangeLogItems(insertedCount, (-15, 15));
var idDiscriminator = createdResult.Item1;
var dtos = createdResult.Item2.Select(e => e.Adapt<ChangeLogValuesDto>());
var dtos = createdResult.Item2.Select(e => e.Adapt<DataWithWellDepthAndSectionDto>());
// act
var result = await client.ClearAndAddRange(idDiscriminator, dtos, new CancellationToken());
@ -102,9 +102,10 @@ public class ChangeLogControllerTest : BaseIntegrationTest
var result = await client.Add(idDiscriminator, dto, new CancellationToken());
var entity = dbContext.ChangeLog
.Where(x => x.DiscriminatorId == idDiscriminator)
.Where(x => x.IdDiscriminator == idDiscriminator)
.FirstOrDefault();
dto = entity.Adapt<ChangeLogValuesDto>();
dto = entity.Adapt<DataWithWellDepthAndSectionDto>();
dto.DepthEnd += 10;
// act
result = await client.Update(dto, new CancellationToken());
@ -146,9 +147,12 @@ public class ChangeLogControllerTest : BaseIntegrationTest
dbContext.ChangeLog.AddRange(entities);
dbContext.SaveChanges();
dtos = entities.Select(c => new ChangeLogValuesDto()
dtos = entities.Select(c => new DataWithWellDepthAndSectionDto()
{
DepthEnd = c.DepthEnd + 10,
DepthStart = c.DepthStart + 10,
Id = c.Id,
IdSection = c.IdSection,
Value = c.Value
}).ToArray();
@ -239,6 +243,12 @@ public class ChangeLogControllerTest : BaseIntegrationTest
var deletedCount = await client.DeleteRange(idsToDelete, new CancellationToken());
var filterRequest = new SectionPartRequest()
{
DepthStart = 0,
DepthEnd = 1000,
};
var paginationRequest = new PaginationRequest()
{
Skip = 0,
@ -247,7 +257,7 @@ public class ChangeLogControllerTest : BaseIntegrationTest
};
var moment = DateTimeOffset.UtcNow.AddDays(16);
var result = await client.GetByDate(idDiscriminator, moment, paginationRequest, new CancellationToken());
var result = await client.GetByDate(idDiscriminator, moment, filterRequest, paginationRequest, new CancellationToken());
Assert.NotNull(result);
@ -280,7 +290,11 @@ public class ChangeLogControllerTest : BaseIntegrationTest
var idDiscriminator = changeLogItems.Item1;
var entities = changeLogItems.Item2;
var dtos = entities.Select(e => e.Adapt<ChangeLogValuesDto>()).ToArray();
foreach (var entity in entities)
{
entity.DepthEnd += 10;
}
var dtos = entities.Select(e => e.Adapt<DataWithWellDepthAndSectionDto>()).ToArray();
await client.UpdateRange(dtos, new CancellationToken());
//act
@ -294,16 +308,19 @@ public class ChangeLogControllerTest : BaseIntegrationTest
}
private static IEnumerable<ChangeLogValuesDto> Generate(int count)
private static IEnumerable<DataWithWellDepthAndSectionDto> Generate(int count)
{
for (int i = 0; i < count; i++)
yield return new ChangeLogValuesDto()
yield return new DataWithWellDepthAndSectionDto()
{
Value = new Dictionary<string, object>()
{
{ "Key", 1 }
},
Id = Guid.NewGuid()
DepthStart = generatorRandomDigits.Next(1, 5),
DepthEnd = generatorRandomDigits.Next(5, 15),
Id = Guid.NewGuid(),
IdSection = Guid.NewGuid()
};
}
@ -318,7 +335,7 @@ public class ChangeLogControllerTest : BaseIntegrationTest
var entities = dtos.Select(d =>
{
var entity = d.Adapt<ChangeLog>();
entity.DiscriminatorId = idDiscriminator;
entity.IdDiscriminator = idDiscriminator;
entity.Creation = DateTimeOffset.UtcNow.AddDays(generatorRandomDigits.Next(minDayCount, maxDayCount));
return entity;

View File

@ -38,64 +38,47 @@ public class TimestampedValuesControllerTest : BaseIntegrationTest
}
[Fact]
public async Task Get_returns_BadRequest()
public async Task Get_returns_success()
{
//arrange
Cleanup();
var firstDiscriminatorId = Guid.NewGuid();
discriminatorIds.Append(firstDiscriminatorId);
var discriminatorId = Guid.NewGuid();
discriminatorIds.Append(discriminatorId);
var secondDiscriminatorId = Guid.NewGuid();
discriminatorIds.Append(secondDiscriminatorId);
try
{
//act
var response = await timestampedValuesClient.Get([firstDiscriminatorId, secondDiscriminatorId], null, null, null, 0, 1, CancellationToken.None);
}
catch (Exception ex)
{
var expectedMessage = $"На сервере произошла ошибка, в результате которой он не может успешно обработать запрос";
var response = await timestampedValuesClient.Get(discriminatorId, null, null, 0, 1, CancellationToken.None);
//assert
Assert.Equal(expectedMessage, ex.Message);
}
Assert.Null(response);
}
[Fact]
public async Task Get_AfterSave_returns_success()
{
//arrange
Cleanup();
var firstDiscriminatorId = Guid.NewGuid();
discriminatorIds.Append(firstDiscriminatorId);
var secondDiscriminatorId = Guid.NewGuid();
discriminatorIds.Append(secondDiscriminatorId);
var discriminatorId = Guid.NewGuid();
discriminatorIds.Append(discriminatorId);
var timestampBegin = DateTimeOffset.UtcNow.AddDays(-1);
var columnNames = new List<string>() { "A", "C" };
var skip = 0;
var take = 6; // Ровно столько значений будет удовлетворять фильтру (\"A\">3) (для одного дискриминатора)
var skip = 5;
var take = 5;
var customFilter = "(\"A\">3)";
var dtos = (await AddRange(firstDiscriminatorId)).ToList();
dtos.AddRange(await AddRange(secondDiscriminatorId));
var dtos = await AddRange(discriminatorId);
//act
var response = await timestampedValuesClient.Get([firstDiscriminatorId, secondDiscriminatorId],
timestampBegin, customFilter, columnNames, skip, take, CancellationToken.None);
var response = await timestampedValuesClient.Get(discriminatorId, timestampBegin, columnNames, skip, take, CancellationToken.None);
//assert
Assert.NotNull(response);
Assert.NotEmpty(response);
var expectedCount = take * 2;
var actualCount = response.Count();
Assert.Equal(expectedCount, actualCount);
Assert.Equal(take, actualCount);
var actualColumnNames = response.SelectMany(e => e.Values.Keys).Distinct().ToList();
Assert.Equal(columnNames, actualColumnNames);
@ -109,7 +92,6 @@ public class TimestampedValuesControllerTest : BaseIntegrationTest
Assert.Equal(expectedValueKind, actualValueKind);
}
[Fact]
public async Task GetGtDate_returns_success()
{
@ -388,7 +370,7 @@ public class TimestampedValuesControllerTest : BaseIntegrationTest
var response = await timestampedValuesClient.AddRange(discriminatorId, generatedDtos, CancellationToken.None);
// assert
//Assert.Equal(generatedDtos.Count(), response);
Assert.Equal(generatedDtos.Count(), response);
return generatedDtos;
}
@ -416,12 +398,8 @@ public class TimestampedValuesControllerTest : BaseIntegrationTest
private void Cleanup()
{
foreach (var item in discriminatorIds)
{
memoryCache.Remove(item);
}
discriminatorIds = [];
dbContext.CleanupDbSet<TimestampedValues>();
dbContext.CleanupDbSet<SchemeProperty>();
dbContext.CleanupDbSet<DataScheme>();
}
}

View File

@ -8,6 +8,7 @@ using Microsoft.Extensions.Logging;
using DD.Persistence.API;
using DD.Persistence.Client;
using DD.Persistence.Database.Model;
using RestSharp;
using DD.Persistence.App;
using DD.Persistence.Client.Helpers;
using DD.Persistence.Factories;

View File

@ -38,5 +38,5 @@ public class ChangeLogDto
/// <summary>
/// Объект записи
/// </summary>
public ChangeLogValuesDto Value { get; set; } = default!;
public DataWithWellDepthAndSectionDto Value { get; set; } = default!;
}

View File

@ -1,17 +0,0 @@
namespace DD.Persistence.Models;
/// <summary>
/// Dto для хранения записей, содержащих начальную и конечную глубину забоя, а также секцию
/// </summary>
public class ChangeLogValuesDto
{
/// <summary>
/// Ключ записи
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// Объект записи
/// </summary>
public required IDictionary<string, object> Value { get; set; }
}

View File

@ -1,11 +1,9 @@
using System.Collections;
namespace DD.Persistence.Models;
namespace DD.Persistence.Models;
/// <summary>
/// Схема для набора данных
/// </summary>
public class DataSchemeDto : IEnumerable<SchemePropertyDto>, IEquatable<IEnumerable<SchemePropertyDto>>
public class DataSchemeDto
{
/// <summary>
/// Дискриминатор
@ -13,30 +11,7 @@ public class DataSchemeDto : IEnumerable<SchemePropertyDto>, IEquatable<IEnumera
public Guid DiscriminatorId { get; set; }
/// <summary>
/// Поля
/// Наименования полей
/// </summary>
private IEnumerable<SchemePropertyDto> Properties { get; } = [];
/// <inheritdoc/>
public DataSchemeDto(Guid discriminatorId, IEnumerable<SchemePropertyDto> Properties)
{
DiscriminatorId = discriminatorId;
this.Properties = Properties;
}
/// <inheritdoc/>
public IEnumerator<SchemePropertyDto> GetEnumerator()
=> Properties.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
/// <inheritdoc/>
public bool Equals(IEnumerable<SchemePropertyDto>? otherProperties)
{
if (otherProperties is null)
return false;
return Properties.SequenceEqual(otherProperties);
}
public string[] PropNames { get; set; } = [];
}

View File

@ -0,0 +1,32 @@
namespace DD.Persistence.Models;
/// <summary>
/// Dto для хранения записей, содержащих начальную и конечную глубину забоя, а также секцию
/// </summary>
public class DataWithWellDepthAndSectionDto
{
/// <summary>
/// Ключ записи
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// Глубина забоя на дату начала интервала
/// </summary>
public double DepthStart { get; set; }
/// <summary>
/// Глубина забоя на дату окончания интервала
/// </summary>
public double DepthEnd { get; set; }
/// <summary>
/// Ключ секции
/// </summary>
public Guid IdSection { get; set; }
/// <summary>
/// Объект записи
/// </summary>
public required IDictionary<string, object> Value { get; set; }
}

View File

@ -1,32 +0,0 @@
namespace DD.Persistence.Models;
/// <summary>
/// Модель, необходимая для отображения истории по журналу изменений
/// </summary>
public class HistoryChangeLogDto
{
/// <summary>
/// Дата и время изменений
/// </summary>
public DateTimeOffset DateTime { get; set; }
/// <summary>
/// Пользователь, совершивший изменение данных
/// </summary>
public required UserDto User { get; set; }
/// <summary>
/// Проект, с которым связаны изменения
/// </summary>
public Guid DiscriminatorId { get; set; }
/// <summary>
/// Список изменений
/// </summary>
public required IEnumerable<ChangeLogDto> ChangeLogItems { get; set; }
/// <summary>
/// Комментарий к изменению
/// </summary>
public required string Comment { get; set; }
}

View File

@ -0,0 +1,9 @@
namespace DD.Persistence.ModelsAbstractions;
public interface IWithSectionPart
{
public double DepthStart { get; set; }
public double DepthEnd { get; set; }
public Guid IdSection { get; set; }
}

View File

@ -1,17 +0,0 @@
namespace DD.Persistence.Models.Requests;
/// <summary>
/// Запрос, используемый для получения данных по журналу операций
/// </summary>
public class ChangeLogRequest
{
/// <summary>
/// Дискриминатор задачи
/// </summary>
public Guid DiscriminatorId { get; set; }
/// <summary>
/// Пользователь
/// </summary>
public Guid UserId { get; set; }
}

View File

@ -0,0 +1,22 @@
namespace DD.Persistence.Models.Requests;
/// <summary>
/// Запрос для фильтрации данных по секции и глубине
/// </summary>
public class SectionPartRequest
{
/// <summary>
/// Глубина забоя на дату начала интервала
/// </summary>
public double? DepthStart { get; set; }
/// <summary>
/// Глубина забоя на дату окончания интервала
/// </summary>
public double? DepthEnd { get; set; }
/// <summary>
/// Ключ секции
/// </summary>
public Guid? IdSection { get; set; }
}

View File

@ -1,30 +0,0 @@
using System.Text.Json;
namespace DD.Persistence.Models;
/// <summary>
/// Индексируемого поле из схемы для набора данных
/// </summary>
public class SchemePropertyDto : IEquatable<SchemePropertyDto>
{
/// <summary>
/// Индекс поля
/// </summary>
public required int Index { get; set; }
/// <summary>
/// Наименование индексируемого поля
/// </summary>
public required string PropertyName { get; set; }
/// <summary>
/// Тип индексируемого поля
/// </summary>
public required JsonValueKind PropertyKind { get; set; }
/// <inheritdoc/>
public bool Equals(SchemePropertyDto? other)
{
return Index == other?.Index && PropertyName == other?.PropertyName && PropertyKind == other?.PropertyKind;
}
}

View File

@ -1,23 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DD.Persistence.Models;
/// <summary>
/// Модель, необходимая для отображения статистики по журналу изменений
/// </summary>
public class StatisticsChangeLogDto
{
/// <summary>
/// Дата и время изменений
/// </summary>
public DateTimeOffset DateTime { get; set; }
/// <summary>
/// Количество изменений
/// </summary>
public int ChangesCount { get; set; }
}

View File

@ -1,23 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DD.Persistence.Models;
/// <summary>
/// Класс, описывающий пользователя
/// </summary>
public class UserDto
{
/// <summary>
/// Идентификатор пользователя
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// Имя пользователя для отображения
/// </summary>
public required string DisplayName { get; set; }
}

View File

@ -20,6 +20,7 @@
<ItemGroup>
<ProjectReference Include="..\DD.Persistence.Database.Postgres\DD.Persistence.Database.Postgres.csproj" />
<ProjectReference Include="..\DD.Persistence.Repository\DD.Persistence.Repository.csproj" />
</ItemGroup>
<ItemGroup>

View File

@ -1,7 +1,12 @@
using DD.Persistence.Database.Model;
using DD.Persistence.Database.Postgres.Repositories;
using DD.Persistence.Repository.Repositories;
using Shouldly;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace DD.Persistence.Repository.Test;
public class SetpointRepositoryShould : IClassFixture<RepositoryTestFixture>
@ -24,6 +29,7 @@ public class SetpointRepositoryShould : IClassFixture<RepositoryTestFixture>
var value = GetJsonFromObject(22);
await sut.Add(id, value, Guid.NewGuid(), CancellationToken.None);
var t = fixture.dbContainer.GetConnectionString();
//act
var result = await sut.GetCurrent([id], CancellationToken.None);

View File

@ -1,6 +1,6 @@
using System.Collections;
namespace DD.Persistence.Database.Postgres.Helpers;
namespace DD.Persistence.Repository;
/// <summary>
/// Цикличный массив
/// </summary>

View File

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Mapster" Version="7.4.0" />
<PackageReference Include="UuidExtensions" Version="1.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DD.Persistence.Database\DD.Persistence.Database.csproj" />
<ProjectReference Include="..\DD.Persistence\DD.Persistence.csproj" />
</ItemGroup>
</Project>

View File

@ -1,19 +1,15 @@
using DD.Persistence.Database.Entity;
using DD.Persistence.Database.Postgres.Repositories;
using DD.Persistence.Database.Postgres.RepositoriesCached;
using DD.Persistence.Database.Repositories;
using DD.Persistence.Database.RepositoriesCached;
using DD.Persistence.Database.Entity;
using DD.Persistence.Models;
using DD.Persistence.Repositories;
using DD.Persistence.Repository.Repositories;
using DD.Persistence.Repository.RepositoriesCached;
using Mapster;
using Microsoft.Extensions.DependencyInjection;
using System.Reflection;
namespace DD.Persistence.Database;
namespace DD.Persistence.Repository;
public static class DependencyInjection
{
// ToDo: перенести в другой файл
public static void MapsterSetup()
{
TypeAdapterConfig.GlobalSettings.Default.Config
@ -21,17 +17,14 @@ public static class DependencyInjection
.Ignore(dest => dest.System, dest => dest.SystemId);
TypeAdapterConfig<ChangeLog, ChangeLogDto>.NewConfig()
.Map(dest => dest.Value, src => new ChangeLogValuesDto()
.Map(dest => dest.Value, src => new DataWithWellDepthAndSectionDto()
{
DepthEnd = src.DepthEnd,
DepthStart = src.DepthStart,
IdSection = src.IdSection,
Value = src.Value,
Id = src.Id
});
TypeAdapterConfig<KeyValuePair<Guid, SchemePropertyDto>, SchemeProperty>.NewConfig()
.Map(dest => dest.DiscriminatorId, src => src.Key)
.Map(dest => dest.Index, src => src.Value.Index)
.Map(dest => dest.PropertyKind, src => src.Value.PropertyKind)
.Map(dest => dest.PropertyName, src => src.Value.PropertyName);
}
public static IServiceCollection AddInfrastructure(this IServiceCollection services)
@ -42,15 +35,13 @@ public static class DependencyInjection
MapsterSetup();
//services.AddTransient(typeof(PersistenceRepository<TimestampedValues>));
services.AddTransient<ISetpointRepository, SetpointRepository>();
services.AddTransient<IChangeLogRepository, ChangeLogRepository>();
services.AddTransient<ITimestampedValuesRepository, TimestampedValuesRepository>();
services.AddTransient<ITechMessagesRepository, TechMessagesRepository>();
services.AddTransient<IParameterRepository, ParameterRepository>();
services.AddTransient<IDataSourceSystemRepository, DataSourceSystemCachedRepository>();
services.AddTransient<ISchemePropertyRepository, SchemePropertyCachedRepository>();
services.AddTransient<IDataSchemeRepository, DataSchemeCachedRepository>();
return services;
}

View File

@ -2,7 +2,7 @@ using System.Collections.Concurrent;
using System.Linq.Expressions;
using System.Reflection;
namespace DD.Persistence.Database.Extensions;
namespace DD.Persistence.Repository.Extensions;
public static class EFExtensionsSortBy
{

View File

@ -1,16 +1,36 @@
using DD.Persistence.Database.EntityAbstractions;
using DD.Persistence.Extensions;
using DD.Persistence.Models.Common;
using Microsoft.EntityFrameworkCore;
using DD.Persistence.Models.Requests;
using Microsoft.EntityFrameworkCore;
using DD.Persistence.Models.Common;
using DD.Persistence.ModelsAbstractions;
using DD.Persistence.Database.EntityAbstractions;
using DD.Persistence.Extensions;
namespace DD.Persistence.Database.Postgres.Helpers;
namespace DD.Persistence.Repository;
/// <summary>
/// класс с набором методов, необходимых для фильтрации записей
/// </summary>
public static class QueryBuilders
{
public static IQueryable<TEntity> Apply<TEntity>(this IQueryable<TEntity> 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<TEntity> Apply<TEntity>(this IQueryable<TEntity> query, DateTimeOffset momentUtc)
where TEntity : class, IChangeLog
{
@ -23,15 +43,23 @@ public static class QueryBuilders
return query;
}
public static async Task<PaginationContainer<TDto>> ApplyPagination<TEntity, TDto>(
this IQueryable<TEntity> query,
PaginationRequest request,
Func<TEntity, TDto> Convert,
CancellationToken token)
where TEntity : class
where TEntity : class, IWithSectionPart
where TDto : class
{
if (!String.IsNullOrEmpty(request.SortSettings))
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);
}

View File

@ -1,5 +1,4 @@
using DD.Persistence.Database.Entity;
using DD.Persistence.Database.Postgres.Helpers;
using DD.Persistence.Models;
using DD.Persistence.Models.Common;
using DD.Persistence.Models.Requests;
@ -8,7 +7,7 @@ using Mapster;
using Microsoft.EntityFrameworkCore;
using UuidExtensions;
namespace DD.Persistence.Database.Repositories;
namespace DD.Persistence.Repository.Repositories;
public class ChangeLogRepository : IChangeLogRepository
{
private readonly DbContext db;
@ -18,7 +17,7 @@ public class ChangeLogRepository : IChangeLogRepository
this.db = db;
}
public async Task<int> AddRange(Guid idAuthor, Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token)
public async Task<int> AddRange(Guid idAuthor, Guid idDiscriminator, IEnumerable<DataWithWellDepthAndSectionDto> dtos, CancellationToken token)
{
var entities = new List<ChangeLog>();
foreach (var dto in dtos)
@ -54,7 +53,7 @@ public class ChangeLogRepository : IChangeLogRepository
public async Task<int> MarkAsDeleted(Guid idEditor, Guid idDiscriminator, CancellationToken token)
{
var query = db.Set<ChangeLog>()
.Where(s => s.DiscriminatorId == idDiscriminator)
.Where(s => s.IdDiscriminator == idDiscriminator)
.Where(e => e.Obsolete == null);
var entities = await query.ToArrayAsync(token);
@ -77,7 +76,7 @@ public class ChangeLogRepository : IChangeLogRepository
return await db.SaveChangesAsync(token);
}
public async Task<int> ClearAndAddRange(Guid idAuthor, Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token)
public async Task<int> ClearAndAddRange(Guid idAuthor, Guid idDiscriminator, IEnumerable<DataWithWellDepthAndSectionDto> dtos, CancellationToken token)
{
var result = 0;
@ -92,7 +91,7 @@ public class ChangeLogRepository : IChangeLogRepository
return result;
}
public async Task<int> UpdateRange(Guid idEditor, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token)
public async Task<int> UpdateRange(Guid idEditor, IEnumerable<DataWithWellDepthAndSectionDto> dtos, CancellationToken token)
{
var dbSet = db.Set<ChangeLog>();
@ -112,7 +111,7 @@ public class ChangeLogRepository : IChangeLogRepository
throw new ArgumentException($"Entity with id = {dto.Id} doesn't exist in Db", nameof(dto));
}
var newEntity = CreateEntityFromDto(idEditor, updatedEntity.DiscriminatorId, dto);
var newEntity = CreateEntityFromDto(idEditor, updatedEntity.IdDiscriminator, dto);
dbSet.Add(newEntity);
updatedEntity.IdNext = newEntity.Id;
@ -128,14 +127,16 @@ public class ChangeLogRepository : IChangeLogRepository
}
public async Task<PaginationContainer<ChangeLogValuesDto>> GetByDate(
public async Task<PaginationContainer<DataWithWellDepthAndSectionDto>> 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);
@ -144,14 +145,14 @@ public class ChangeLogRepository : IChangeLogRepository
private IQueryable<ChangeLog> CreateQuery(Guid idDiscriminator)
{
var query = db.Set<ChangeLog>().Where(e => e.DiscriminatorId == idDiscriminator);
var query = db.Set<ChangeLog>().Where(e => e.IdDiscriminator == idDiscriminator);
return query;
}
public async Task<IEnumerable<ChangeLogDto>> GetChangeLogForInterval(Guid idDiscriminator, DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token)
{
var query = db.Set<ChangeLog>().Where(s => s.DiscriminatorId == idDiscriminator);
var query = db.Set<ChangeLog>().Where(s => s.IdDiscriminator == idDiscriminator);
var min = new DateTimeOffset(dateBegin.ToUniversalTime().Date, TimeSpan.Zero);
var max = new DateTimeOffset(dateEnd.ToUniversalTime().Date, TimeSpan.Zero);
@ -171,7 +172,7 @@ public class ChangeLogRepository : IChangeLogRepository
public async Task<IEnumerable<DateOnly>> GetDatesChange(Guid idDiscriminator, CancellationToken token)
{
var query = db.Set<ChangeLog>().Where(e => e.DiscriminatorId == idDiscriminator);
var query = db.Set<ChangeLog>().Where(e => e.IdDiscriminator == idDiscriminator);
var datesCreateQuery = query
.Select(e => e.Creation)
@ -186,7 +187,7 @@ public class ChangeLogRepository : IChangeLogRepository
var datesUpdate = await datesUpdateQuery.ToArrayAsync(token);
var dates = datesCreate.Concat(datesUpdate);
var dates = Enumerable.Concat(datesCreate, datesUpdate);
var datesOnly = dates
.Select(d => new DateOnly(d.Year, d.Month, d.Day))
.Distinct()
@ -195,27 +196,30 @@ public class ChangeLogRepository : IChangeLogRepository
return datesOnly;
}
private static ChangeLog CreateEntityFromDto(Guid idAuthor, Guid idDiscriminator, ChangeLogValuesDto dto)
private static ChangeLog CreateEntityFromDto(Guid idAuthor, Guid idDiscriminator, DataWithWellDepthAndSectionDto dto)
{
var entity = new ChangeLog()
{
Id = Uuid7.Guid(),
Creation = DateTimeOffset.UtcNow,
IdAuthor = idAuthor,
DiscriminatorId = idDiscriminator,
IdDiscriminator = idDiscriminator,
IdEditor = idAuthor,
Value = dto.Value
Value = dto.Value,
IdSection = dto.IdSection,
DepthStart = dto.DepthStart,
DepthEnd = dto.DepthEnd,
};
return entity;
}
public async Task<IEnumerable<ChangeLogValuesDto>> GetGtDate(Guid idDiscriminator, DateTimeOffset dateBegin, CancellationToken token)
public async Task<IEnumerable<DataWithWellDepthAndSectionDto>> GetGtDate(Guid idDiscriminator, DateTimeOffset dateBegin, CancellationToken token)
{
var date = dateBegin.ToUniversalTime();
var query = db.Set<ChangeLog>()
.Where(e => e.DiscriminatorId == idDiscriminator)
var query = this.db.Set<ChangeLog>()
.Where(e => e.IdDiscriminator == idDiscriminator)
.Where(e => e.Creation >= date || e.Obsolete >= date);
var entities = await query.ToArrayAsync(token);
@ -228,12 +232,12 @@ public class ChangeLogRepository : IChangeLogRepository
public async Task<DatesRangeDto?> GetDatesRange(Guid idDiscriminator, CancellationToken token)
{
var query = db.Set<ChangeLog>()
.Where(e => e.DiscriminatorId == idDiscriminator)
.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
Max = group.Max(e => (e.Obsolete.HasValue && e.Obsolete > e.Creation)
? e.Obsolete.Value
: e.Creation),
});
@ -252,5 +256,5 @@ public class ChangeLogRepository : IChangeLogRepository
};
}
private ChangeLogValuesDto Convert(ChangeLog entity) => entity.Adapt<ChangeLogValuesDto>();
private DataWithWellDepthAndSectionDto Convert(ChangeLog entity) => entity.Adapt<DataWithWellDepthAndSectionDto>();
}

View File

@ -0,0 +1,34 @@
using DD.Persistence.Database.Entity;
using DD.Persistence.Models;
using DD.Persistence.Repositories;
using Mapster;
using Microsoft.EntityFrameworkCore;
namespace DD.Persistence.Repository.Repositories;
public class DataSchemeRepository : IDataSchemeRepository
{
protected DbContext db;
public DataSchemeRepository(DbContext db)
{
this.db = db;
}
protected virtual IQueryable<DataScheme> GetQueryReadOnly() => db.Set<DataScheme>();
public virtual async Task Add(DataSchemeDto dataSourceSystemDto, CancellationToken token)
{
var entity = dataSourceSystemDto.Adapt<DataScheme>();
await db.Set<DataScheme>().AddAsync(entity, token);
await db.SaveChangesAsync(token);
}
public virtual async Task<DataSchemeDto?> Get(Guid dataSchemeId, CancellationToken token)
{
var query = GetQueryReadOnly()
.Where(e => e.DiscriminatorId == dataSchemeId);
var entity = await query.ToArrayAsync();
var dto = entity.Select(e => e.Adapt<DataSchemeDto>()).FirstOrDefault();
return dto;
}
}

View File

@ -4,7 +4,7 @@ using DD.Persistence.Repositories;
using Mapster;
using Microsoft.EntityFrameworkCore;
namespace DD.Persistence.Database.Postgres.Repositories;
namespace DD.Persistence.Repository.Repositories;
public class DataSourceSystemRepository : IDataSourceSystemRepository
{
protected DbContext db;
@ -12,7 +12,7 @@ public class DataSourceSystemRepository : IDataSourceSystemRepository
{
this.db = db;
}
protected IQueryable<DataSourceSystem> GetQueryReadOnly() => db.Set<DataSourceSystem>();
protected virtual IQueryable<DataSourceSystem> GetQueryReadOnly() => db.Set<DataSourceSystem>();
public virtual async Task Add(DataSourceSystemDto dataSourceSystemDto, CancellationToken token)
{

View File

@ -5,7 +5,7 @@ using DD.Persistence.Models;
using DD.Persistence.Repositories;
using DD.Persistence.Models.Common;
namespace DD.Persistence.Database.Postgres.Repositories;
namespace DD.Persistence.Repository.Repositories;
public class ParameterRepository : IParameterRepository
{
private DbContext db;
@ -15,7 +15,7 @@ public class ParameterRepository : IParameterRepository
this.db = db;
}
protected IQueryable<ParameterData> GetQueryReadOnly() => db.Set<ParameterData>();
protected virtual IQueryable<ParameterData> GetQueryReadOnly() => db.Set<ParameterData>();
public async Task<DatesRangeDto> GetDatesRangeAsync(Guid idDiscriminator, CancellationToken token)
{

View File

@ -6,7 +6,7 @@ using Mapster;
using Microsoft.EntityFrameworkCore;
using System.Text.Json;
namespace DD.Persistence.Database.Postgres.Repositories
namespace DD.Persistence.Repository.Repositories
{
public class SetpointRepository : ISetpointRepository
{
@ -16,7 +16,7 @@ namespace DD.Persistence.Database.Postgres.Repositories
this.db = db;
}
protected IQueryable<Setpoint> GetQueryReadOnly() => db.Set<Setpoint>();
protected virtual IQueryable<Setpoint> GetQueryReadOnly() => db.Set<Setpoint>();
public async Task<IEnumerable<SetpointValueDto>> GetCurrent(
IEnumerable<Guid> setpointKeys,

View File

@ -7,7 +7,7 @@ using DD.Persistence.Repositories;
using Mapster;
using Microsoft.EntityFrameworkCore;
namespace DD.Persistence.Database.Postgres.Repositories
namespace DD.Persistence.Repository.Repositories
{
public class TechMessagesRepository : ITechMessagesRepository
{
@ -20,7 +20,7 @@ namespace DD.Persistence.Database.Postgres.Repositories
this.sourceSystemRepository = sourceSystemRepository;
}
protected IQueryable<TechMessage> GetQueryReadOnly() => db.Set<TechMessage>()
protected virtual IQueryable<TechMessage> GetQueryReadOnly() => db.Set<TechMessage>()
.Include(e => e.System);
public async Task<PaginationContainer<TechMessageDto>> GetPage(PaginationRequest request, CancellationToken token)

View File

@ -0,0 +1,179 @@
using DD.Persistence.Database.Entity;
using DD.Persistence.Models;
using DD.Persistence.Models.Common;
using DD.Persistence.Repositories;
using Microsoft.EntityFrameworkCore;
namespace DD.Persistence.Repository.Repositories;
public class TimestampedValuesRepository : ITimestampedValuesRepository
{
private readonly DbContext db;
public TimestampedValuesRepository(DbContext db)
{
this.db = db;
}
protected virtual IQueryable<TimestampedValues> GetQueryReadOnly() => this.db.Set<TimestampedValues>();
public async virtual Task<int> AddRange(Guid discriminatorId, IEnumerable<TimestampedValuesDto> dtos, CancellationToken token)
{
var timestampedValuesEntities = new List<TimestampedValues>();
foreach (var dto in dtos)
{
var timestampedValuesEntity = new TimestampedValues()
{
DiscriminatorId = discriminatorId,
Timestamp = dto.Timestamp.ToUniversalTime(),
Values = dto.Values.Values.ToArray()
};
timestampedValuesEntities.Add(timestampedValuesEntity);
}
await db.Set<TimestampedValues>().AddRangeAsync(timestampedValuesEntities, token);
var result = await db.SaveChangesAsync(token);
return result;
}
public async virtual Task<IEnumerable<Tuple<DateTimeOffset, object[]>>> Get(Guid discriminatorId, DateTimeOffset? timestampBegin, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token)
{
var query = GetQueryReadOnly()
.Where(entity => entity.DiscriminatorId == discriminatorId);
// Фильтрация по дате
if (timestampBegin.HasValue)
{
query = ApplyGeTimestamp(query, timestampBegin.Value);
}
query = query
.OrderBy(item => item.Timestamp)
.Skip(skip)
.Take(take);
var entities = await query.ToArrayAsync(token);
var result = entities.Select(e => Tuple.Create(
e.Timestamp,
e.Values
));
return result;
}
public async virtual Task<IEnumerable<Tuple<DateTimeOffset, object[]>>> GetFirst(Guid discriminatorId, int takeCount, CancellationToken token)
{
var query = GetQueryReadOnly()
.OrderBy(e => e.Timestamp)
.Take(takeCount);
var entities = await query.ToArrayAsync(token);
var result = entities.Select(e => Tuple.Create(
e.Timestamp,
e.Values
));
return result;
}
public async virtual Task<IEnumerable<Tuple<DateTimeOffset, object[]>>> GetLast(Guid discriminatorId, int takeCount, CancellationToken token)
{
var query = GetQueryReadOnly()
.OrderByDescending(e => e.Timestamp)
.Take(takeCount);
var entities = await query.ToArrayAsync(token);
var result = entities.Select(e => Tuple.Create(
e.Timestamp,
e.Values
));
return result;
}
// ToDo: прореживание должно осуществляться до материализации
public async virtual Task<IEnumerable<Tuple<DateTimeOffset, object[]>>> GetResampledData(
Guid discriminatorId,
DateTimeOffset dateBegin,
double intervalSec = 600d,
int approxPointsCount = 1024,
CancellationToken token = default)
{
var result = await GetGtDate(discriminatorId, dateBegin, token);
var dateEnd = dateBegin.AddSeconds(intervalSec);
result = result
.Where(i => i.Item1 <= dateEnd);
var ratio = result.Count() / approxPointsCount;
if (ratio > 1)
result = result
.Where((_, index) => index % ratio == 0);
return result;
}
public async virtual Task<IEnumerable<Tuple<DateTimeOffset, object[]>>> GetGtDate(Guid discriminatorId, DateTimeOffset timestampBegin, CancellationToken token)
{
var query = GetQueryReadOnly()
.Where(e => e.Timestamp > timestampBegin);
var entities = await query.ToArrayAsync(token);
var result = entities.Select(e => Tuple.Create(
e.Timestamp,
e.Values
));
return result;
}
public async virtual Task<DatesRangeDto?> GetDatesRange(Guid discriminatorId, CancellationToken token)
{
var query = GetQueryReadOnly()
.GroupBy(entity => entity.DiscriminatorId)
.Select(group => new
{
Min = group.Min(entity => entity.Timestamp),
Max = group.Max(entity => entity.Timestamp),
});
var item = await query.FirstOrDefaultAsync(token);
if (item is null)
{
return null;
}
var dto = new DatesRangeDto
{
From = item.Min,
To = item.Max,
};
return dto;
}
public virtual Task<int> Count(Guid discriminatorId, CancellationToken token)
{
var dbSet = db.Set<TimestampedValues>();
var query = dbSet.Where(entity => entity.DiscriminatorId == discriminatorId);
return query.CountAsync(token);
}
/// <summary>
/// Применить фильтр по дате
/// </summary>
/// <param name="query"></param>
/// <param name="timestampBegin"></param>
/// <returns></returns>
private IQueryable<TimestampedValues> ApplyGeTimestamp(IQueryable<TimestampedValues> query, DateTimeOffset timestampBegin)
{
var geTimestampUtc = timestampBegin.ToUniversalTime();
var result = query
.Where(entity => entity.Timestamp >= geTimestampUtc);
return result;
}
}

View File

@ -1,21 +1,21 @@
using DD.Persistence.Models;
using DD.Persistence.Repository.Repositories;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using DD.Persistence.Database.Repositories;
namespace DD.Persistence.Database.RepositoriesCached;
public class SchemePropertyCachedRepository : SchemePropertyRepository
namespace DD.Persistence.Repository.RepositoriesCached;
public class DataSchemeCachedRepository : DataSchemeRepository
{
private readonly IMemoryCache memoryCache;
public SchemePropertyCachedRepository(DbContext db, IMemoryCache memoryCache) : base(db)
public DataSchemeCachedRepository(DbContext db, IMemoryCache memoryCache) : base(db)
{
this.memoryCache = memoryCache;
}
public override async Task AddRange(DataSchemeDto dataSourceSystemDto, CancellationToken token)
public override async Task Add(DataSchemeDto dataSourceSystemDto, CancellationToken token)
{
await base.AddRange(dataSourceSystemDto, token);
await base.Add(dataSourceSystemDto, token);
memoryCache.Set(dataSourceSystemDto.DiscriminatorId, dataSourceSystemDto);
}

View File

@ -1,10 +1,10 @@
using DD.Persistence.Database.Entity;
using DD.Persistence.Models;
using DD.Persistence.Database.Postgres.Repositories;
using DD.Persistence.Repository.Repositories;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
namespace DD.Persistence.Database.Postgres.RepositoriesCached;
namespace DD.Persistence.Repository.RepositoriesCached;
public class DataSourceSystemCachedRepository : DataSourceSystemRepository
{
private static readonly string SystemCacheKey = $"{typeof(DataSourceSystem).FullName}CacheKey";

View File

@ -0,0 +1,103 @@
//using DD.Persistence.Models;
//using DD.Persistence.Models.Common;
//using DD.Persistence.Repositories;
//using Microsoft.EntityFrameworkCore;
//namespace DD.Persistence.Repository.Repositories;
//public class TimestampedValuesCachedRepository : TimestampedValuesRepository
//{
// public static TimestampedValuesDto? FirstByDate { get; private set; }
// public static CyclicArray<TimestampedValuesDto> LastData { get; } = new CyclicArray<TimestampedValuesDto>(CacheItemsCount);
// private const int CacheItemsCount = 3600;
// public TimestampedValuesCachedRepository(DbContext db, IDataSourceSystemRepository<ValuesIdentityDto> relatedDataRepository) : base(db, relatedDataRepository)
// {
// //Task.Run(async () =>
// //{
// // var firstDateItem = await base.GetFirst(CancellationToken.None);
// // if (firstDateItem == null)
// // {
// // return;
// // }
// // FirstByDate = firstDateItem;
// // var dtos = await base.GetLast(CacheItemsCount, CancellationToken.None);
// // dtos = dtos.OrderBy(d => d.Timestamp);
// // LastData.AddRange(dtos);
// //}).Wait();
// }
// public override async Task<IEnumerable<TimestampedValuesDto>> GetGtDate(Guid discriminatorId, DateTimeOffset dateBegin, CancellationToken token)
// {
// if (LastData.Count == 0 || LastData[0].Timestamp > dateBegin)
// {
// var dtos = await base.GetGtDate(discriminatorId, dateBegin, token);
// return dtos;
// }
// var items = LastData
// .Where(i => i.Timestamp >= dateBegin);
// return items;
// }
// public override async Task<int> AddRange(Guid discriminatorId, IEnumerable<TimestampedValuesDto> dtos, CancellationToken token)
// {
// var result = await base.AddRange(discriminatorId, dtos, token);
// if (result > 0)
// {
// dtos = dtos.OrderBy(x => x.Timestamp);
// FirstByDate = dtos.First();
// LastData.AddRange(dtos);
// }
// return result;
// }
// public override async Task<DatesRangeDto?> GetDatesRange(Guid discriminatorId, CancellationToken token)
// {
// if (FirstByDate == null)
// return null;
// return await Task.Run(() =>
// {
// return new DatesRangeDto
// {
// From = FirstByDate.Timestamp,
// To = LastData[^1].Timestamp
// };
// });
// }
// public override async Task<IEnumerable<TimestampedValuesDto>> GetResampledData(
// Guid discriminatorId,
// DateTimeOffset dateBegin,
// double intervalSec = 600d,
// int approxPointsCount = 1024,
// CancellationToken token = default)
// {
// var dtos = LastData.Where(i => i.Timestamp >= dateBegin);
// if (LastData.Count == 0 || LastData[0].Timestamp > dateBegin)
// {
// dtos = await base.GetGtDate(discriminatorId, dateBegin, token);
// }
// var dateEnd = dateBegin.AddSeconds(intervalSec);
// dtos = dtos
// .Where(i => i.Timestamp <= dateEnd);
// var ratio = dtos.Count() / approxPointsCount;
// if (ratio > 1)
// dtos = dtos
// .Where((_, index) => index % ratio == 0);
// return dtos;
// }
//}

View File

@ -16,7 +16,8 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DD.Persistence.Database.Postgres\DD.Persistence.Database.Postgres.csproj" />
<ProjectReference Include="..\DD.Persistence.Database\DD.Persistence.Database.csproj" />
<ProjectReference Include="..\DD.Persistence\DD.Persistence.csproj" />
</ItemGroup>
<ItemGroup>

View File

@ -1,278 +0,0 @@
using Ardalis.Specification.EntityFrameworkCore;
using DD.Persistence.Database.Entity;
using DD.Persistence.Filter.Models;
using DD.Persistence.Filter.Models.Enumerations;
using DD.Persistence.Models;
using DD.Persistence.Database.Postgres.Helpers;
using System.Text.Json;
namespace DD.Persistence.Test;
/// ToDo: переписать под Theory
public class FilterBuilderShould
{
private readonly SpecificationEvaluator SpecificationEvaluator;
public FilterBuilderShould()
{
this.SpecificationEvaluator = new SpecificationEvaluator();
}
[Fact]
public void TestFilterBuilding()
{
//arrange
var discriminatorId = Guid.NewGuid();
var dataSchemeProperties = new SchemePropertyDto[]
{
new SchemePropertyDto()
{
Index = 0,
PropertyName = "A",
PropertyKind = JsonValueKind.String
},
new SchemePropertyDto()
{
Index = 1,
PropertyName = "B",
PropertyKind = JsonValueKind.Number
},
new SchemePropertyDto()
{
Index = 2,
PropertyName = "C",
PropertyKind = JsonValueKind.String
}
};
var dataScheme = new DataSchemeDto(discriminatorId, dataSchemeProperties);
var filterDate = DateTime.Now.AddMinutes(-1);
var root = new TVertex(
OperationEnum.Or,
new TVertex(
OperationEnum.And,
new TLeaf(OperationEnum.Greate, "A", filterDate),
new TLeaf(OperationEnum.Less, "B", 2.22)
),
new TLeaf(OperationEnum.Equal, "C", "IsEqualText")
);
var queryableData = new[]
{
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-1),
Values = new object[] { filterDate.AddMinutes(-1), 200, "IsEqualText" } // true
},
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-2),
Values = new object[] { filterDate.AddMinutes(1), 2.21, "IsNotEqualText" } // true
},
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-3),
Values = new object[] { filterDate.AddMinutes(-1), 2.22, "IsNotEqualText" } // false
},
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-4),
Values = new object[] { filterDate.AddMinutes(-1), 2.21, "IsNotEqualText" } // false
}
}
.AsQueryable();
//act
queryableData = queryableData.ApplyFilter(dataScheme, root);
//assert
var result = queryableData.ToList();
Assert.NotNull(result);
Assert.NotEmpty(result);
var expectedCount = 2;
var actualCount = result.Count();
Assert.Equal(expectedCount, actualCount);
}
[Fact]
public void TestFilterOperations()
{
//arrange
var discriminatorId = Guid.NewGuid();
var dataSchemeProperties = new SchemePropertyDto[]
{
new SchemePropertyDto()
{
Index = 0,
PropertyName = "A",
PropertyKind = JsonValueKind.Number
}
};
var dataScheme = new DataSchemeDto(discriminatorId, dataSchemeProperties);
var root = new TVertex(
OperationEnum.Or,
new TVertex(
OperationEnum.And,
new TVertex(
OperationEnum.And,
new TVertex(
OperationEnum.And,
new TVertex(
OperationEnum.And,
new TLeaf(OperationEnum.Less, "A", 2),
new TLeaf(OperationEnum.LessOrEqual, "A", 1.99)
),
new TLeaf(OperationEnum.GreateOrEqual, "A", 1.97)
),
new TLeaf(OperationEnum.Greate, "A", 1.96)
),
new TLeaf(OperationEnum.NotEqual, "A", 1.98)
),
new TLeaf(OperationEnum.Equal, "A", 1)
);
var queryableData = new[]
{
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-1),
Values = new object[] { 1 } // true
},
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-2),
Values = new object[] { 1.96 } // false
},
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-3),
Values = new object[] { 1.97 } // true
},
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-4),
Values = new object[] { 1.98 } // false
},
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-5),
Values = new object[] { 1.99 } // true
},
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-6),
Values = new object[] { 2 } // false
}
}
.AsQueryable();
//act
queryableData = queryableData.ApplyFilter(dataScheme, root);
//assert
var result = queryableData.ToList();
Assert.NotNull(result);
Assert.NotEmpty(result);
Assert.NotNull(result);
Assert.NotEmpty(result);
var expectedCount = 3;
var actualCount = result.Count();
Assert.Equal(expectedCount, actualCount);
}
[Fact]
public void TestFilterValues()
{
//arrange
var discriminatorId = Guid.NewGuid();
var filterDate = DateTimeOffset.Now;
var dataSchemeProperties = new SchemePropertyDto[]
{
new SchemePropertyDto()
{
Index = 0,
PropertyName = "A",
PropertyKind = JsonValueKind.Number
},
new SchemePropertyDto()
{
Index = 1,
PropertyName = "B",
PropertyKind = JsonValueKind.Number
},
new SchemePropertyDto()
{
Index = 2,
PropertyName = "C",
PropertyKind = JsonValueKind.String
},
new SchemePropertyDto()
{
Index = 3,
PropertyName = "D",
PropertyKind = JsonValueKind.String
}
};
var dataScheme = new DataSchemeDto(discriminatorId, dataSchemeProperties);
var root = new TVertex(
OperationEnum.Or,
new TVertex(
OperationEnum.Or,
new TVertex(
OperationEnum.Or,
new TLeaf(OperationEnum.Equal, "A", 1),
new TLeaf(OperationEnum.Equal, "B", 1.11)
),
new TLeaf(OperationEnum.Equal, "C", "IsEqualText")
),
new TLeaf(OperationEnum.Equal, "D", filterDate)
);
var queryableData = new[]
{
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-1),
Values = new object[] { 1, 2.22, "IsNotEqualText", DateTimeOffset.Now.AddMinutes(-1) } // true
},
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-2),
Values = new object[] { 2, 1.11, "IsNotEqualText", DateTimeOffset.Now.AddMinutes(-1) } // true
},
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-3),
Values = new object[] { 2, 2.22, "IsEqualText", DateTimeOffset.Now.AddMinutes(-1) } // true
},
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-4),
Values = new object[] { 2, 2.22, "IsNotEqualText", filterDate } // true
},
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-1),
Values = new object[] { 2, 2.22, "IsNotEqualText", DateTimeOffset.Now.AddMinutes(-1) } // false
}
}
.AsQueryable();
//act
queryableData = queryableData.ApplyFilter(dataScheme, root);
//assert
var result = queryableData.ToList();
Assert.NotNull(result);
Assert.NotEmpty(result);
Assert.NotNull(result);
Assert.NotEmpty(result);
var expectedCount = 4;
var actualCount = result.Count();
Assert.Equal(expectedCount, actualCount);
}
}

View File

@ -1,15 +1,14 @@
using DD.Persistence.Models;
using DD.Persistence.Models;
using DD.Persistence.Repositories;
using DD.Persistence.Services;
using NSubstitute;
using System.Text.Json;
namespace DD.Persistence.Test;
namespace DD.Persistence.Repository.Test;
public class TimestampedValuesServiceShould
{
private readonly ITimestampedValuesRepository timestampedValuesRepository = Substitute.For<ITimestampedValuesRepository>();
private readonly ISchemePropertyRepository dataSchemeRepository = Substitute.For<ISchemePropertyRepository>();
private readonly TimestampedValuesService timestampedValuesService;
private readonly IDataSchemeRepository dataSchemeRepository = Substitute.For<IDataSchemeRepository>();
private TimestampedValuesService timestampedValuesService;
public TimestampedValuesServiceShould()
{
@ -19,14 +18,11 @@ public class TimestampedValuesServiceShould
[Fact]
public async Task TestServiceEfficiency()
{
var discriminatorIds = new[] { Guid.NewGuid(), Guid.NewGuid() };
var discriminatorId = Guid.NewGuid();
const int count = 10;
var dtos = Generate(count, DateTimeOffset.UtcNow);
var addRangeResult = await timestampedValuesService
.AddRange(discriminatorIds.First(), dtos, CancellationToken.None);
Assert.Equal(0, addRangeResult);
addRangeResult = await timestampedValuesService
.AddRange(discriminatorIds.Last(), dtos, CancellationToken.None);
.AddRange(discriminatorId, dtos, CancellationToken.None);
Assert.Equal(0, addRangeResult);
var columnNames = new[] { "A", "B", "C", "D" };
@ -34,21 +30,22 @@ public class TimestampedValuesServiceShould
.AddHours(-1)
.ToUniversalTime();
var getResult = await timestampedValuesService
.Get(discriminatorIds, geTimestamp, null, columnNames, 0, count, CancellationToken.None);
.Get(discriminatorId, geTimestamp, columnNames, 0, count, CancellationToken.None);
Assert.NotNull(getResult);
Assert.Empty(getResult);
}
private static IEnumerable<TimestampedValuesDto> Generate(int countToCreate, DateTimeOffset from)
{
var result = new List<TimestampedValuesDto>();
for (int i = 0; i < countToCreate; i++)
{
var values = new Dictionary<string, object>()
{
{ "A", GetJsonFromObject(i) },
{ "B", GetJsonFromObject(i * 1.1) },
{ "C", GetJsonFromObject($"Any{i}") },
{ "D", GetJsonFromObject(DateTimeOffset.Now) }
{ "A", i },
{ "B", i * 1.1 },
{ "C", $"Any{i}" },
{ "D", DateTimeOffset.Now },
};
yield return new TimestampedValuesDto()
@ -58,11 +55,4 @@ public class TimestampedValuesServiceShould
};
}
}
private static JsonElement GetJsonFromObject(object value)
{
var jsonString = JsonSerializer.Serialize(value);
var doc = JsonDocument.Parse(jsonString);
return doc.RootElement;
}
}

View File

@ -1,107 +0,0 @@
using DD.Persistence.Filter.Models;
using DD.Persistence.Filter.Models.Enumerations;
using DD.Persistence.Filter.TreeBuilder;
using Newtonsoft.Json;
namespace DD.Persistence.Test;
public class TreeBuilderTest
{
[Fact]
public void TreeBuildingShouldBuilt()
{
//arrange
var treeString = "(\"A\"==1)||(\"B\"==2)&&(\"C\"==3)||((\"D\"==4)||(\"E\"==5))&&(\"F\"==6)";
//act
var root = treeString.BuildTree();
//assert
Assert.NotNull(root);
var expectedRoot = JsonConvert.SerializeObject(new TVertex(
OperationEnum.And,
new TVertex(
OperationEnum.And,
new TVertex(
OperationEnum.Or,
new TLeaf(OperationEnum.Equal, "A", 1.0),
new TLeaf(OperationEnum.Equal, "B", 2.0)
),
new TVertex(
OperationEnum.Or,
new TLeaf(OperationEnum.Equal, "C", 3.0),
new TVertex(
OperationEnum.Or,
new TLeaf(OperationEnum.Equal, "D", 4.0),
new TLeaf(OperationEnum.Equal, "E", 5.0)
)
)
),
new TLeaf(OperationEnum.Equal, "F", 6.0)
));
var actualRoot = JsonConvert.SerializeObject(root);
Assert.Equal(expectedRoot, actualRoot);
}
[Fact]
public void TreeOperationsShouldBuilt()
{
//arrange
var treeString = "(\"A\"==1)||(\"B\"!=1)||(\"C\">1)||(\"D\">=1)||(\"E\"<1)||(\"F\"<=1)";
//act
var root = treeString.BuildTree();
//assert
Assert.NotNull(root);
var expectedRoot = JsonConvert.SerializeObject(new TVertex(
OperationEnum.Or,
new TVertex(
OperationEnum.Or,
new TVertex(
OperationEnum.Or,
new TLeaf(OperationEnum.Equal, "A", 1.0),
new TLeaf(OperationEnum.NotEqual, "B", 1.0)
),
new TVertex(
OperationEnum.Or,
new TLeaf(OperationEnum.Greate, "C", 1.0),
new TLeaf(OperationEnum.GreateOrEqual, "D", 1.0)
)
),
new TVertex(
OperationEnum.Or,
new TLeaf(OperationEnum.Less, "E", 1.0),
new TLeaf(OperationEnum.LessOrEqual, "F", 1.0)
)
));
var actualRoot = JsonConvert.SerializeObject(root);
Assert.Equal(expectedRoot, actualRoot);
}
[Fact]
public void LeafValuesShouldBuilt()
{
//arrange
var treeString = "(\"A\"==1.2345)||(\"B\"==12345)||(\"C\"==\"12345\")";
//act
var root = treeString.BuildTree();
//assert
Assert.NotNull(root);
var expectedRoot = JsonConvert.SerializeObject(new TVertex(
OperationEnum.Or,
new TVertex(
OperationEnum.Or,
new TLeaf(OperationEnum.Equal, "A", 1.2345),
new TLeaf(OperationEnum.Equal, "B", 12345.0)
),
new TLeaf(OperationEnum.Equal, "C", "12345")
));
var actualRoot = JsonConvert.SerializeObject(root);
Assert.Equal(expectedRoot, actualRoot);
}
}

View File

@ -7,6 +7,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DD.Persistence", "DD.Persis
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DD.Persistence.API", "DD.Persistence.API\DD.Persistence.API.csproj", "{8650A227-929E-45F0-AEF7-2C91F45FE884}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DD.Persistence.Repository", "DD.Persistence.Repository\DD.Persistence.Repository.csproj", "{493D6D92-231B-4CB6-831B-BE13884B0DE4}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DD.Persistence.Database", "DD.Persistence.Database\DD.Persistence.Database.csproj", "{F77475D1-D074-407A-9D69-2FADDDAE2056}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DD.Persistence.IntegrationTests", "DD.Persistence.IntegrationTests\DD.Persistence.IntegrationTests.csproj", "{10752C25-3773-4081-A1F2-215A1D950126}"
@ -28,13 +30,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionIt
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DD.Persistence.Test", "DD.Persistence.Test\DD.Persistence.Test.csproj", "{B8C774E6-6B75-41AC-B3CF-10BD3623B2FA}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Элементы решения", "Элементы решения", "{E0CEF8FC-C131-4CF3-9F0A-E7B4F895B811}"
ProjectSection(SolutionItems) = preProject
.gitea\workflows\integrationTests.yaml = .gitea\workflows\integrationTests.yaml
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DD.Persistence.Database.Postgres.Test", "DD.Persistence.Database.Postgres.Test\DD.Persistence.Database.Postgres.Test.csproj", "{47142566-9EAB-4FB5-92EC-9DCB02CAC890}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -49,6 +44,10 @@ Global
{8650A227-929E-45F0-AEF7-2C91F45FE884}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8650A227-929E-45F0-AEF7-2C91F45FE884}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8650A227-929E-45F0-AEF7-2C91F45FE884}.Release|Any CPU.Build.0 = Release|Any CPU
{493D6D92-231B-4CB6-831B-BE13884B0DE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{493D6D92-231B-4CB6-831B-BE13884B0DE4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{493D6D92-231B-4CB6-831B-BE13884B0DE4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{493D6D92-231B-4CB6-831B-BE13884B0DE4}.Release|Any CPU.Build.0 = Release|Any CPU
{F77475D1-D074-407A-9D69-2FADDDAE2056}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F77475D1-D074-407A-9D69-2FADDDAE2056}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F77475D1-D074-407A-9D69-2FADDDAE2056}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -81,10 +80,6 @@ Global
{B8C774E6-6B75-41AC-B3CF-10BD3623B2FA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B8C774E6-6B75-41AC-B3CF-10BD3623B2FA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B8C774E6-6B75-41AC-B3CF-10BD3623B2FA}.Release|Any CPU.Build.0 = Release|Any CPU
{47142566-9EAB-4FB5-92EC-9DCB02CAC890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{47142566-9EAB-4FB5-92EC-9DCB02CAC890}.Debug|Any CPU.Build.0 = Debug|Any CPU
{47142566-9EAB-4FB5-92EC-9DCB02CAC890}.Release|Any CPU.ActiveCfg = Release|Any CPU
{47142566-9EAB-4FB5-92EC-9DCB02CAC890}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@ -7,7 +7,7 @@ namespace DD.Persistence.API;
/// <summary>
/// Интерфейс для работы с API журнала изменений
/// </summary>
public interface IChangeLogApi : ISyncWithDiscriminatorApi<ChangeLogValuesDto>
public interface IChangeLogApi : ISyncWithDiscriminatorApi<DataWithWellDepthAndSectionDto>
{
/// <summary>
/// Импорт с заменой: удаление старых строк и добавление новых
@ -16,26 +16,28 @@ public interface IChangeLogApi : ISyncWithDiscriminatorApi<ChangeLogValuesDto>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IActionResult> ClearAndAddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token);
Task<IActionResult> ClearAndAddRange(Guid idDiscriminator, IEnumerable<DataWithWellDepthAndSectionDto> dtos, CancellationToken token);
/// <summary>
/// Получение данных на текущую дату (с пагинацией)
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="filterRequest">параметры запроса фильтрации</param>
/// <param name="paginationRequest">параметры запроса пагинации</param>
/// <param name="token"></param>
/// <returns></returns>
Task<IActionResult> GetCurrent(Guid idDiscriminator, PaginationRequest paginationRequest, CancellationToken token);
Task<IActionResult> GetCurrent(Guid idDiscriminator, SectionPartRequest filterRequest, PaginationRequest paginationRequest, CancellationToken token);
/// <summary>
/// Получение данных на определенную дату (с пагинацией)
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="moment"></param>
/// <param name="filterRequest">параметры запроса фильтрации</param>
/// <param name="paginationRequest">параметры запроса пагинации</param>
/// <param name="token"></param>
/// <returns></returns>
Task<IActionResult> GetByDate(Guid idDiscriminator, DateTimeOffset moment, PaginationRequest paginationRequest, CancellationToken token);
Task<IActionResult> GetByDate(Guid idDiscriminator, DateTimeOffset moment, SectionPartRequest filterRequest, PaginationRequest paginationRequest, CancellationToken token);
/// <summary>
/// Получение исторических данных за определенный период времени
@ -54,7 +56,7 @@ public interface IChangeLogApi : ISyncWithDiscriminatorApi<ChangeLogValuesDto>
/// <param name="dto"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IActionResult> Add(Guid idDiscriminator, ChangeLogValuesDto dto, CancellationToken token);
Task<IActionResult> Add(Guid idDiscriminator, DataWithWellDepthAndSectionDto dto, CancellationToken token);
/// <summary>
/// Добавить несколько записей
@ -63,7 +65,7 @@ public interface IChangeLogApi : ISyncWithDiscriminatorApi<ChangeLogValuesDto>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IActionResult> AddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token);
Task<IActionResult> AddRange(Guid idDiscriminator, IEnumerable<DataWithWellDepthAndSectionDto> dtos, CancellationToken token);
/// <summary>
/// Обновить одну запись
@ -71,7 +73,7 @@ public interface IChangeLogApi : ISyncWithDiscriminatorApi<ChangeLogValuesDto>
/// <param name="dto"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IActionResult> Update(ChangeLogValuesDto dto, CancellationToken token);
Task<IActionResult> Update(DataWithWellDepthAndSectionDto dto, CancellationToken token);
/// <summary>
/// Обновить несколько записей
@ -79,7 +81,7 @@ public interface IChangeLogApi : ISyncWithDiscriminatorApi<ChangeLogValuesDto>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IActionResult> UpdateRange(IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token);
Task<IActionResult> UpdateRange(IEnumerable<DataWithWellDepthAndSectionDto> dtos, CancellationToken token);
/// <summary>
/// Удалить одну запись

View File

@ -1,9 +1,7 @@
namespace DD.Persistence.Extensions;
/// <inheritdoc/>
public static class IEnumerableExtensions
{
/// <inheritdoc/>
public static void ForEach<T>(this IEnumerable<T> source, Action<T> action)
{
if (source == null)
@ -17,7 +15,6 @@ public static class IEnumerableExtensions
}
}
/// <inheritdoc/>
public static bool IsNullOrEmpty<T>(this IEnumerable<T>? enumerable)
{
if (enumerable == null)

View File

@ -1,22 +0,0 @@
namespace DD.Persistence.Filter.Models.Abstractions;
/// <summary>
/// Посетитель бинарного дерева
/// </summary>
/// <typeparam name="TVisitResult"></typeparam>
public interface INodeVisitor<TVisitResult>
{
/// <summary>
/// Посетить узел
/// </summary>
/// <param name="vertex"></param>
/// <returns></returns>
TVisitResult Visit(TVertex vertex);
/// <summary>
/// Посетить лист
/// </summary>
/// <param name="leaf"></param>
/// <returns></returns>
TVisitResult Visit(TLeaf leaf);
}

View File

@ -1,55 +0,0 @@
using DD.Persistence.Filter.Models.Enumerations;
using DD.Persistence.Filter.TreeBuilder;
using System.Diagnostics.CodeAnalysis;
namespace DD.Persistence.Filter.Models.Abstractions;
/// <summary>
/// Абстрактная модель вершины
/// </summary>
public abstract class TNode : IParsable<TNode?>
{
/// <inheritdoc/>
public TNode(OperationEnum operation)
{
Operation = operation;
}
/// <summary>
/// Логическая операция
/// </summary>
public OperationEnum Operation { get; }
/// <inheritdoc/>
public static TNode? Parse(string s, IFormatProvider? provider)
{
var result = s.BuildTree();
return result;
}
/// <inheritdoc/>
public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out TNode result)
{
if (string.IsNullOrEmpty(s))
{
result = default(TNode);
return false;
}
result = s.BuildTree();
if (result is null)
return false;
return true;
}
/// <summary>
/// Принять посетителя
/// </summary>
/// <typeparam name="TVisitResult"></typeparam>
/// <param name="visitor"></param>
/// <returns></returns>
public abstract TVisitResult AcceptVisitor<TVisitResult>(INodeVisitor<TVisitResult> visitor);
}

View File

@ -1,47 +0,0 @@
namespace DD.Persistence.Filter.Models.Enumerations;
/// <summary>
/// Логические операции
/// </summary>
public enum OperationEnum
{
/// <summary>
/// И
/// </summary>
And = 1,
/// <summary>
/// ИЛИ
/// </summary>
Or = 2,
/// <summary>
/// РАВНО
/// </summary>
Equal = 3,
/// <summary>
/// НЕ РАВНО
/// </summary>
NotEqual = 4,
/// <summary>
/// БОЛЬШЕ
/// </summary>
Greate = 5,
/// <summary>
/// БОЛЬШЕ ЛИБО РАВНО
/// </summary>
GreateOrEqual = 6,
/// <summary>
/// МЕНЬШЕ
/// </summary>
Less = 7,
/// <summary>
/// МЕНЬШЕ ЛИБО РАВНО
/// </summary>
LessOrEqual = 8
}

View File

@ -1,33 +0,0 @@
using DD.Persistence.Filter.Models.Abstractions;
using DD.Persistence.Filter.Models.Enumerations;
namespace DD.Persistence.Filter.Models;
/// <summary>
/// Модель листа
/// </summary>
public class TLeaf : TNode
{
/// <summary>
/// Наименование поля
/// </summary>
public string PropName { get; }
/// <summary>
/// Значение для фильтрации
/// </summary>
public object? Value { get; }
/// <inheritdoc/>
public TLeaf(OperationEnum operation, string fieldName, object? value) : base(operation)
{
PropName = fieldName;
Value = value;
}
/// <inheritdoc/>
public override TVisitResult AcceptVisitor<TVisitResult>(INodeVisitor<TVisitResult> visitor)
{
return visitor.Visit(this);
}
}

View File

@ -1,33 +0,0 @@
using DD.Persistence.Filter.Models.Abstractions;
using DD.Persistence.Filter.Models.Enumerations;
namespace DD.Persistence.Filter.Models;
/// <summary>
/// Модель узла
/// </summary>
public class TVertex : TNode
{
/// <summary>
/// Левый потомок
/// </summary>
public TNode Left { get; }
/// <summary>
/// Правый потомок
/// </summary>
public TNode Rigth { get; }
/// <inheritdoc/>
public TVertex(OperationEnum operation, TNode left, TNode rigth) : base(operation)
{
Left = left;
Rigth = rigth;
}
/// <inheritdoc/>
public override TVisitResult AcceptVisitor<TVisitResult>(INodeVisitor<TVisitResult> visitor)
{
return visitor.Visit(this);
}
}

View File

@ -1,27 +0,0 @@
using DD.Persistence.Filter.Models.Enumerations;
namespace DD.Persistence.Filter.TreeBuilder.Expressions.Abstractions;
/// <summary>
/// Интерфейс для выражений
/// </summary>
interface IExpression
{
/// <summary>
/// Получить логическую операцию
/// </summary>
/// <returns></returns>
OperationEnum GetOperation();
/// <summary>
/// Получить логическую операцию в виде строки (для регулярных выражений)
/// </summary>
/// <returns></returns>
string GetOperationString();
/// <summary>
/// Реализация правила
/// </summary>
/// <param name="context"></param>
void Interpret(InterpreterContext context);
}

View File

@ -1,83 +0,0 @@
using DD.Persistence.Extensions;
using DD.Persistence.Filter.Models;
using DD.Persistence.Filter.Models.Enumerations;
using DD.Persistence.Filter.TreeBuilder.Expressions.Abstractions;
using System.Text.RegularExpressions;
namespace DD.Persistence.Filter.TreeBuilder.Expressions.NonTerminal.Abstractions;
/// <summary>
/// Абстрактный класс для нетерминальных выражений
/// </summary>
abstract class NonTerminalExpression : IExpression
{
/// <summary>
/// Реализация правила для нетерминальных выражений
/// </summary>
/// <param name="context"></param>
public void Interpret(InterpreterContext context)
{
var operation = GetOperation();
var operationString = GetOperationString();
var matches = GetMatches(context, operationString);
while (matches.Length != 0)
{
matches.ForEach(m =>
{
var matchString = m.ToString();
var separator = operationString.Replace("\\", string.Empty);
var pair = matchString
.Trim(['(', ')'])
.Split(separator)
.Select(e => int.Parse(e));
var leftNode = context.TreeNodes
.FirstOrDefault(e => e.Key == pair.First())
.Value;
var rigthNode = context.TreeNodes
.FirstOrDefault(e => e.Key == pair.Last())
.Value;
var node = new TVertex(operation, leftNode, rigthNode);
var key = context.TreeNodes.Count;
context.TreeNodes.Add(key, node);
var keyString = key.ToString();
context.TreeString = context.TreeString.Replace(matchString, keyString);
});
matches = GetMatches(context, operationString);
}
var isRoot = int.TryParse(context.TreeString, out _);
if (isRoot)
{
context.TreeString = string.Empty;
context.Root = context.TreeNodes.Last().Value;
}
}
/// <inheritdoc/>
public abstract OperationEnum GetOperation();
/// <inheritdoc/>
public abstract string GetOperationString();
/// <summary>
/// Получить из акткуального состояния строки все совпадения для текущего выражения
/// </summary>
private static Match[] GetMatches(InterpreterContext context, string operationString)
{
string pattern = context.TreeString.Contains('(') && context.TreeString.Contains(')')
? $@"\(\d+{operationString}\d+\)" : $@"\d+{operationString}\d+";
Regex regex = new(pattern);
var matches = regex
.Matches(context.TreeString)
.ToArray();
return matches;
}
}

Some files were not shown because too many files have changed in this diff Show More