Compare commits

...

67 Commits

Author SHA1 Message Date
a5037b8967 Merge pull request '#1003 Убрать ненужные модификаторы virtual' (#29) from VirtualCleaning into master
All checks were successful
Unit tests / tests-and-publication (push) Successful in 2m29s
Reviewed-on: #29
2025-02-20 14:41:44 +05:00
27b9728912 Убрать ненужные модификаторы virtual 2025-02-13 16:53:29 +05:00
93472f7933 Обновить DD.Persistence.API/Readme.md
All checks were successful
Unit tests / tests-and-publication (push) Successful in 1m56s
2025-02-13 13:27:58 +05:00
0d9c61c905 Обновить DD.Persistence.API/Readme.md
Some checks failed
Unit tests / tests-and-publication (push) Has been cancelled
2025-02-13 13:27:06 +05:00
36274b3a5e Обновить DD.Persistence.API/Readme.md
Some checks failed
Unit tests / tests-and-publication (push) Has been cancelled
2025-02-13 13:26:45 +05:00
b30d28dbeb Обновления в файле readme.md
Some checks failed
Unit tests / tests-and-publication (push) Has been cancelled
2025-02-13 13:23:00 +05:00
233850483c UML-диаграмма процесса пакетного редактирования
Some checks failed
Unit tests / tests-and-publication (push) Has been cancelled
2025-02-13 13:22:28 +05:00
ca0b1f0031 Убрана ссылка на удаленный проект с репозиториями из Dockerfile
All checks were successful
Unit tests / tests-and-publication (push) Successful in 5m16s
2025-02-12 16:43:34 +05:00
3cf7cbcc7c Фикс теста
Some checks failed
Unit tests / tests-and-publication (push) Failing after 2m22s
2025-02-12 16:33:42 +05:00
a49a0f567a Merge pull request '#974 Модифицировать метод Get для TimestampedValues по части применения фильтра' (#28) from TimestampedValuesFilter into master
Some checks failed
Unit tests / tests-and-publication (push) Failing after 1m22s
Reviewed-on: #28
Reviewed-by: on.nemtina <on.nemtina@digitaldrilling.ru>
2025-02-12 15:16:09 +05:00
c904c117d8 Правки после ревью #1 2025-02-12 15:15:43 +05:00
0aca5d2d43 Добавить комментарий для клиента 2025-02-11 12:41:55 +05:00
4b9a4b4db7 Merge branch 'master' into TimestampedValuesFilter 2025-02-11 12:37:04 +05:00
e1f84f3091 Перед материализацией вычитываемых сущностей применить построенный фильтр 2025-02-11 12:34:37 +05:00
2fe369d49e Перевести TimestampedValuesRepository под спецификации 2025-02-10 17:25:45 +05:00
8e2c3a3a55 Добавить парсинг дерева в Get-запрос 2025-02-10 09:27:13 +05:00
52f7bca9f1 Merge pull request '#959 Реализовать обход бинарного дерева и создание фильтра на основе спецификаций' (#26) from FilterBuilder into master
Some checks failed
Unit tests / tests-and-publication (push) Failing after 1m6s
Reviewed-on: #26
2025-02-07 15:11:33 +05:00
2ef3efed33 Merge from dev 2025-02-07 15:11:09 +05:00
4739a043f4 Обновить README.md
Some checks failed
Unit tests / tests-and-publication (push) Failing after 56s
2025-02-07 12:30:59 +05:00
49cc9d6e39 Обновить README.md
Some checks failed
Unit tests / tests-and-publication (push) Failing after 1m1s
2025-02-07 12:15:35 +05:00
c997bfe9a4 Обновить README.md
Some checks failed
Unit tests / tests-and-publication (push) Failing after 1m5s
2025-02-07 12:01:11 +05:00
fca2ccb8aa Обновить README.md
Some checks failed
Unit tests / tests-and-publication (push) Has been cancelled
2025-02-07 12:00:23 +05:00
bfb18cab8a Обновить README.md
Some checks failed
Unit tests / tests-and-publication (push) Has been cancelled
2025-02-07 11:59:22 +05:00
c247489477 readme-файл, как запускать контейнер с persistence
Some checks failed
Unit tests / tests-and-publication (push) Failing after 57s
2025-02-07 11:57:07 +05:00
11eea8db67 Папка docker, где находится compose.yaml - файл для docker
Some checks failed
Unit tests / tests-and-publication (push) Failing after 1m5s
2025-02-07 11:33:45 +05:00
83c744939b Merge branch 'FilterBuilder' into FilterBinder 2025-02-07 08:33:40 +05:00
43289d939b Merge pull request 'fix/mock-methods-for-change-log' (#27) from fix/mock-methods-for-change-log into master
Some checks failed
Unit tests / tests-and-publication (push) Failing after 2m25s
Reviewed-on: #27
2025-02-06 17:23:32 +05:00
Оля Бизюкова
1f3df26e9a Правка по ревью 2025-02-06 17:21:28 +05:00
Оля Бизюкова
e0f0d9fdd0 Исправлен тип возвращаемых данных для GetStatisticsCountAsync и HistoryChangeLogAsync 2025-02-06 17:07:02 +05:00
Оля Бизюкова
ed6df68f7d Merge branch 'fix/mock-methods-for-change-log' of ssh://git.ddrilling.ru:2221/on.nemtina/persistence into fix/mock-methods-for-change-log 2025-02-06 16:57:31 +05:00
Оля Бизюкова
2ab8101258 Moq-данные для статистики и получения истории журнала изменений 2025-02-06 16:57:13 +05:00
d6038ca579 Merge branch 'master' into fix/mock-methods-for-change-log 2025-02-06 16:15:42 +05:00
Оля Бизюкова
86bf78f31f HistoryChangeLogDto для описания истории изменений 2025-02-06 16:14:02 +05:00
5ce5fa139b Merge pull request '#958 Перенести DD.Persistence.Repository + доработать схему данных' (#25) from RepositoriesRework into master
Some checks failed
Unit tests / tests-and-publication (push) Failing after 1m53s
Reviewed-on: #25
Reviewed-by: on.nemtina <on.nemtina@digitaldrilling.ru>
2025-02-06 15:53:37 +05:00
272e164482 Merge branch 'master' into RepositoriesRework 2025-02-06 15:52:03 +05:00
aad8c57511 Исправить сломанный тест
Some checks failed
Unit tests / tests-and-publication (push) Has been cancelled
2025-02-06 15:50:04 +05:00
Оля Бизюкова
8fa661b605 Merge from dev
Some checks failed
Unit tests / tests-and-publication (push) Failing after 54s
2025-02-06 15:38:56 +05:00
Оля Бизюкова
979a651328 Merge from dev 2025-02-06 15:37:39 +05:00
baf04ae3a6 Merge pull request '#957 Реализовать построение дерева из строки' (#24) from TreeBuilder into master
Some checks failed
Unit tests / tests-and-publication (push) Failing after 2m56s
Reviewed-on: #24
2025-02-06 15:20:57 +05:00
598056c6d7 Merge branch 'RepositoriesRework' into FilterBuilder 2025-02-06 12:39:52 +05:00
c5da82c210 Правки по ревью #4 2025-02-06 12:39:40 +05:00
bcb9749b1a Правки по ревью #4 2025-02-06 12:32:28 +05:00
44bc335151 Merge branch 'RepositoriesRework' into FilterBuilder 2025-02-06 09:31:20 +05:00
7d973ba859 Правки по ревью #3 2025-02-06 09:31:10 +05:00
5abfcc0d50 Merge branch 'RepositoriesRework' into FilterBuilder 2025-02-06 09:25:08 +05:00
0c27a0148d Merge branch 'TreeBuilder' into FilterBuilder 2025-02-06 09:15:48 +05:00
a340fbe23b Наработка 2025-02-05 18:04:36 +05:00
9ca49cb1b5 Правки по ревью #2 2025-02-05 17:20:18 +05:00
431c7278cb Правки по ревью #1 2025-02-05 14:30:36 +05:00
4513de06fa Перенести содержимое проекта DD.Persistence.Repository в DD.Persistence.Database 2025-02-05 12:19:45 +05:00
63e6816e35 Правки по ревью #3 2025-02-05 12:10:01 +05:00
560073ed07 Правки по ревью #2 2025-02-05 11:45:47 +05:00
e3c1d02650 Правки по ревью #1 2025-02-05 10:58:26 +05:00
4b5477207d Реализовать обход бинарного дерева и создание фильтра на основе спецификаций 2025-02-05 10:40:34 +05:00
87264fd8db Merge branch 'TreeBuilder' into FilterBuilder 2025-02-05 10:17:08 +05:00
1d0921c3e8 Доработать схему данных по части хранения типов полей в соответствии с индексацией 2025-02-05 09:56:49 +05:00
f955aab218 Перенести содержимое проекта DD.Persistence.Repository в DD.Persistence.Database.Postgres 2025-02-05 09:16:28 +05:00
cb0508afe9 Убрать лишний комментарий 2025-02-05 08:48:20 +05:00
287d6e7111 Реализовать построение дерева из строки 2025-02-05 08:46:08 +05:00
Оля Бизюкова
d67eaca0e6 тестирование и публикация в одном job
All checks were successful
Unit tests / tests-and-publication (push) Successful in 1m33s
2025-02-04 11:46:46 +05:00
Оля Бизюкова
3835bf284b правка экспериментальная
All checks were successful
Unit tests / tests (push) Successful in 1m39s
2025-02-04 11:41:47 +05:00
Оля Бизюкова
7bb79af104 Перемещена строчка с авторизацией Gitea Docker Registry
Some checks failed
Unit tests / tests (push) Failing after 57s
2025-02-04 11:36:55 +05:00
Оля Бизюкова
f8718d7694 Теперь все в одном файле
Some checks failed
Unit tests / tests (push) Failing after 1m18s
2025-02-04 11:31:22 +05:00
Оля Бизюкова
3ba6f98793 2 jobs в одном файле, а не 2 файла с одной job
All checks were successful
Unit tests / tests (push) Successful in 1m5s
Unit tests / pubcontainer (push) Successful in 1m11s
2025-02-04 11:16:23 +05:00
Оля Бизюкова
ca4d0543be 2 yaml файла: для публикации и для тестирования
All checks were successful
Unit tests / tests (push) Successful in 1m5s
.NET Persistence / pubcontainer (push) Successful in 1m10s
2025-02-04 11:12:41 +05:00
Оля Бизюкова
5ddb77b07b правка
Some checks failed
Unit tests / test-and-publication (push) Failing after 1m20s
2025-02-04 11:07:41 +05:00
Оля Бизюкова
8fcd0d8456 pipline для публикации убран пока в integrationTests
Some checks failed
Unit tests / test (push) Successful in 1m4s
Unit tests / pubcontainer (push) Failing after 2s
2025-02-04 11:04:04 +05:00
96 changed files with 2522 additions and 537 deletions

25
.docker/appsettings.json Normal file
View File

@ -0,0 +1,25 @@
{
"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/"
}

31
.docker/compose.yaml Normal file
View File

@ -0,0 +1,31 @@
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

@ -1,43 +0,0 @@
name: .NET Persistence
on:
push:
branches:
- master
jobs:
pubcontainer:
runs-on: ubuntu-latest
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: 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/on.nemtina/persistence:latest --network=host --build-arg PUBLISHERPASSWORD="${{ secrets.PUBLISHER_PASSWORD }}" -f DD.Persistence.App/Dockerfile .
# run: docker build -t git.ddrilling.ru/ddrilling/iocollector:latest --network=host --build-arg PUBLISHERPASSWORD="${{ secrets.PUBLISHER_PASSWORD }}" -f DD.IoCollector.App/Dockerfile .
- name: Push Docker image to Gitea
run: docker push git.ddrilling.ru/ddrilling/persistence:latest

View File

@ -6,9 +6,9 @@ on:
- master
jobs:
test:
tests-and-publication:
runs-on: ubuntu-latest
container: node
# container: node
# Service containers to run with `runner-job`
services:
@ -31,7 +31,11 @@ jobs:
- 5442:5432
steps:
- name: Setup dotnet
- 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
@ -39,8 +43,11 @@ jobs:
- 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: Check out repository code
uses: actions/checkout@v4
- name: Restore dependencies
run: dotnet restore
- name: Build solution
run: dotnet build --no-restore
- name: Run integration tests
run: dotnet test DD.Persistence.IntegrationTests
@ -59,3 +66,15 @@ jobs:
- 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;
@ -174,4 +174,87 @@ 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,4 +1,5 @@
using DD.Persistence.Models;
using DD.Persistence.Filter.Models.Abstractions;
using DD.Persistence.Models;
using DD.Persistence.Models.Common;
using DD.Persistence.Repositories;
using DD.Persistence.Services.Interfaces;
@ -45,6 +46,7 @@ public class TimestampedValuesController : ControllerBase
/// </summary>
/// <param name="discriminatorIds">Набор дискриминаторов</param>
/// <param name="timestampBegin">Фильтр позднее даты</param>
/// <param name="filterTree">Кастомный фильтр по набору значений</param>
/// <param name="columnNames">Фильтр свойств набора</param>
/// <param name="skip"></param>
/// <param name="take"></param>
@ -52,9 +54,14 @@ 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] string[]? columnNames, int skip, int take, CancellationToken token)
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)
{
var result = await timestampedValuesService.Get(discriminatorIds, timestampBegin, columnNames, skip, take, token);
var result = await timestampedValuesService.Get(discriminatorIds, timestampBegin, filterTree, columnNames, skip, take, token);
return result.Any() ? Ok(result) : NoContent();
}

View File

@ -25,8 +25,11 @@
<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,16 +1,14 @@
using Mapster;
using DD.Persistence.Filter.Models.Abstractions;
using DD.Persistence.Models.Configurations;
using DD.Persistence.Services;
using DD.Persistence.Services.Interfaces;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using DD.Persistence.Models;
using DD.Persistence.Models.Configurations;
using DD.Persistence.Services;
using DD.Persistence.Services.Interfaces;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.Reflection;
using System.Text.Json.Nodes;
using DD.Persistence.Database.Entity;
namespace DD.Persistence.API;
@ -30,6 +28,7 @@ public static class DependencyInjection
new OpenApiSchema {Type = "number", Format = "float" }
]
});
c.MapType<TNode>(() => new OpenApiSchema { Type = "string" });
c.CustomOperationIds(e =>
{

View File

@ -0,0 +1,359 @@
<?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,4 +48,7 @@ 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.Model;
using DD.Persistence.Database;
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)
{
// Add services to the container.
// AddRange services to the container.
services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle

View File

@ -28,7 +28,6 @@ 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

@ -24,12 +24,14 @@ public interface ITimestampedValuesClient : IDisposable
/// </summary>
/// <param name="discriminatorIds">Набор дискриминаторов (идентификаторов)</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,
@ -40,11 +42,12 @@ public interface ITimestampedValuesClient : IDisposable
/// </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, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token);
Task<IEnumerable<T>> Get<T>(Guid discriminatorId, DateTimeOffset? geTimestamp, string? filterTree, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token);
/// <summary>
/// Получить данные, начиная с заданной отметки времени

View File

@ -23,6 +23,7 @@ public interface IRefitTimestampedValuesClient : IRefitClient, IDisposable
[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,

View File

@ -32,17 +32,17 @@ public class TimestampedValuesClient : BaseClient, ITimestampedValuesClient
}
/// <inheritdoc/>
public async Task<IEnumerable<TimestampedValuesDto>> Get(IEnumerable<Guid> discriminatorIds, DateTimeOffset? geTimestamp, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token)
public async Task<IEnumerable<TimestampedValuesDto>> Get(IEnumerable<Guid> discriminatorIds, DateTimeOffset? geTimestamp, string? filterTree, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token)
{
var result = await ExecuteGetResponse(
async () => await refitTimestampedSetClient.Get(discriminatorIds, geTimestamp, columnNames, skip, take, token), token);
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, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token)
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, columnNames, skip, take, token);
var data = await Get([discriminatorId], geTimestamp, filterTree, columnNames, skip, take, token);
var mapper = GetMapper<T>(discriminatorId);
return data.Select(mapper.DeserializeTimeStampedData);

View File

@ -1,6 +1,13 @@
using Microsoft.EntityFrameworkCore;
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.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System.Reflection;
namespace DD.Persistence.Database.Model;

View File

@ -13,7 +13,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace DD.Persistence.Database.Postgres.Migrations
{
[DbContext(typeof(PersistencePostgresContext))]
[Migration("20250203061429_Init")]
[Migration("20250210055116_Init")]
partial class Init
{
/// <inheritdoc />
@ -37,14 +37,14 @@ namespace DD.Persistence.Database.Postgres.Migrations
.HasColumnType("timestamp with time zone")
.HasComment("Дата создания записи");
b.Property<Guid>("DiscriminatorId")
.HasColumnType("uuid")
.HasComment("Дискриминатор таблицы");
b.Property<Guid>("IdAuthor")
.HasColumnType("uuid")
.HasComment("Автор изменения");
b.Property<Guid>("IdDiscriminator")
.HasColumnType("uuid")
.HasComment("Дискриминатор таблицы");
b.Property<Guid?>("IdEditor")
.HasColumnType("uuid")
.HasComment("Редактор");
@ -67,23 +67,6 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.ToTable("change_log");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.DataScheme", b =>
{
b.Property<Guid>("DiscriminatorId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasComment("Идентификатор схемы данных");
b.Property<string>("PropNames")
.IsRequired()
.HasColumnType("jsonb")
.HasComment("Наименования полей в порядке индексации");
b.HasKey("DiscriminatorId");
b.ToTable("data_scheme");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.DataSourceSystem", b =>
{
b.Property<Guid>("SystemId")
@ -129,6 +112,30 @@ 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")
@ -217,17 +224,6 @@ 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,7 +17,7 @@ namespace DD.Persistence.Database.Postgres.Migrations
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false, comment: "Ключ записи"),
IdDiscriminator = table.Column<Guid>(type: "uuid", nullable: false, comment: "Дискриминатор таблицы"),
DiscriminatorId = 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: "Дата создания записи"),
@ -30,18 +30,6 @@ 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
@ -69,6 +57,20 @@ 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
@ -94,12 +96,6 @@ 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(
@ -139,6 +135,9 @@ namespace DD.Persistence.Database.Postgres.Migrations
migrationBuilder.DropTable(
name: "parameter_data");
migrationBuilder.DropTable(
name: "scheme_property");
migrationBuilder.DropTable(
name: "setpoint");
@ -150,9 +149,6 @@ namespace DD.Persistence.Database.Postgres.Migrations
migrationBuilder.DropTable(
name: "data_source_system");
migrationBuilder.DropTable(
name: "data_scheme");
}
}
}

View File

@ -34,14 +34,14 @@ namespace DD.Persistence.Database.Postgres.Migrations
.HasColumnType("timestamp with time zone")
.HasComment("Дата создания записи");
b.Property<Guid>("DiscriminatorId")
.HasColumnType("uuid")
.HasComment("Дискриминатор таблицы");
b.Property<Guid>("IdAuthor")
.HasColumnType("uuid")
.HasComment("Автор изменения");
b.Property<Guid>("IdDiscriminator")
.HasColumnType("uuid")
.HasComment("Дискриминатор таблицы");
b.Property<Guid?>("IdEditor")
.HasColumnType("uuid")
.HasComment("Редактор");
@ -64,23 +64,6 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.ToTable("change_log");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.DataScheme", b =>
{
b.Property<Guid>("DiscriminatorId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasComment("Идентификатор схемы данных");
b.Property<string>("PropNames")
.IsRequired()
.HasColumnType("jsonb")
.HasComment("Наименования полей в порядке индексации");
b.HasKey("DiscriminatorId");
b.ToTable("data_scheme");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.DataSourceSystem", b =>
{
b.Property<Guid>("SystemId")
@ -126,6 +109,30 @@ 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")
@ -214,17 +221,6 @@ 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,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
@ -7,11 +7,14 @@
</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,15 +1,19 @@
using DD.Persistence.Database.Entity;
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.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.Repository;
namespace DD.Persistence.Database;
public static class DependencyInjection
{
// ToDo: перенести в другой файл
public static void MapsterSetup()
{
TypeAdapterConfig.GlobalSettings.Default.Config
@ -22,6 +26,12 @@ public static class DependencyInjection
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)
@ -32,13 +42,15 @@ 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<IDataSchemeRepository, DataSchemeCachedRepository>();
services.AddTransient<IDataSourceSystemRepository, DataSourceSystemCachedRepository>();
services.AddTransient<ISchemePropertyRepository, SchemePropertyCachedRepository>();
return services;
}

View File

@ -11,13 +11,13 @@ namespace DD.Persistence.Database.Entity;
/// Часть записи, описывающая изменение
/// </summary>
[Table("change_log")]
public class ChangeLog : IChangeLog
public class ChangeLog : IDiscriminatorItem, IChangeLog
{
[Key, Comment("Ключ записи")]
public Guid Id { get; set; }
[Comment("Дискриминатор таблицы")]
public Guid IdDiscriminator { get; set; }
public Guid DiscriminatorId { get; set; }
[Comment("Автор изменения")]
public Guid IdAuthor { get; set; }

View File

@ -1,15 +0,0 @@
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 : ITimestampedItem
public class ParameterData : IDiscriminatorItem, ITimestampedItem
{
[Required, Comment("Дискриминатор системы")]
public Guid DiscriminatorId { get; set; }

View File

@ -0,0 +1,24 @@
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

@ -17,15 +17,15 @@ public class TechMessage : ITimestampedItem
[Comment("Дата возникновения")]
public DateTimeOffset Timestamp { get; set; }
[Column(TypeName = "varchar(512)"), Comment("Текст сообщения")]
public required string Text { get; set; }
[Column(TypeName = "varchar(512)"), Comment("Текст сообщения")]
public required string Text { get; set; }
[Required, Comment("Id системы, к которой относится сообщение")]
public required Guid SystemId { get; set; }
[Required, Comment("Id системы, к которой относится сообщение")]
public required Guid SystemId { get; set; }
[Required, ForeignKey(nameof(SystemId)), Comment("Система, к которой относится сообщение")]
public virtual required DataSourceSystem System { get; set; }
[Required, ForeignKey(nameof(SystemId)), Comment("Система, к которой относится сообщение")]
public virtual required DataSourceSystem System { get; set; }
[Comment("Статус события")]
public int EventState { get; set; }
}
[Comment("Статус события")]
public int EventState { get; set; }
}

View File

@ -7,7 +7,7 @@ namespace DD.Persistence.Database.Entity;
[Table("timestamped_values")]
[PrimaryKey(nameof(DiscriminatorId), nameof(Timestamp))]
public class TimestampedValues : ITimestampedItem
public class TimestampedValues : IDiscriminatorItem, ITimestampedItem, IValuesItem
{
[Comment("Временная отметка"), Key]
public DateTimeOffset Timestamp { get; set; }
@ -17,7 +17,4 @@ public class TimestampedValues : ITimestampedItem
[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 IdDiscriminator { get; set; }
public Guid DiscriminatorId { get; set; }
/// <summary>
/// Значение

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,116 @@
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

@ -0,0 +1,20 @@
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

@ -1,11 +1,10 @@
using Microsoft.EntityFrameworkCore;
using DD.Persistence.Models.Requests;
using DD.Persistence.Models.Common;
using DD.Persistence.ModelsAbstractions;
using DD.Persistence.Database.EntityAbstractions;
using DD.Persistence.Database.EntityAbstractions;
using DD.Persistence.Extensions;
using DD.Persistence.Models.Common;
using DD.Persistence.Models.Requests;
using Microsoft.EntityFrameworkCore;
namespace DD.Persistence.Repository;
namespace DD.Persistence.Database.Postgres.Helpers;
/// <summary>
/// класс с набором методов, необходимых для фильтрации записей
@ -24,7 +23,6 @@ public static class QueryBuilders
return query;
}
public static async Task<PaginationContainer<TDto>> ApplyPagination<TEntity, TDto>(
this IQueryable<TEntity> query,
PaginationRequest request,
@ -55,4 +53,4 @@ public static class QueryBuilders
return result;
}
}
}

View File

@ -10,7 +10,7 @@ public class PersistenceDbContext : DbContext
{
public DbSet<Setpoint> Setpoint => Set<Setpoint>();
public DbSet<DataScheme> DataSchemes => Set<DataScheme>();
public DbSet<SchemeProperty> SchemeProperty => Set<SchemeProperty>();
public DbSet<TimestampedValues> TimestampedValues => Set<TimestampedValues>();
@ -30,10 +30,6 @@ 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,4 +1,5 @@
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;
@ -7,7 +8,7 @@ using Mapster;
using Microsoft.EntityFrameworkCore;
using UuidExtensions;
namespace DD.Persistence.Repository.Repositories;
namespace DD.Persistence.Database.Repositories;
public class ChangeLogRepository : IChangeLogRepository
{
private readonly DbContext db;
@ -53,7 +54,7 @@ public class ChangeLogRepository : IChangeLogRepository
public async Task<int> MarkAsDeleted(Guid idEditor, Guid idDiscriminator, CancellationToken token)
{
var query = db.Set<ChangeLog>()
.Where(s => s.IdDiscriminator == idDiscriminator)
.Where(s => s.DiscriminatorId == idDiscriminator)
.Where(e => e.Obsolete == null);
var entities = await query.ToArrayAsync(token);
@ -111,7 +112,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.IdDiscriminator, dto);
var newEntity = CreateEntityFromDto(idEditor, updatedEntity.DiscriminatorId, dto);
dbSet.Add(newEntity);
updatedEntity.IdNext = newEntity.Id;
@ -143,14 +144,14 @@ public class ChangeLogRepository : IChangeLogRepository
private IQueryable<ChangeLog> CreateQuery(Guid idDiscriminator)
{
var query = db.Set<ChangeLog>().Where(e => e.IdDiscriminator == idDiscriminator);
var query = db.Set<ChangeLog>().Where(e => e.DiscriminatorId == 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.IdDiscriminator == idDiscriminator);
var query = db.Set<ChangeLog>().Where(s => s.DiscriminatorId == idDiscriminator);
var min = new DateTimeOffset(dateBegin.ToUniversalTime().Date, TimeSpan.Zero);
var max = new DateTimeOffset(dateEnd.ToUniversalTime().Date, TimeSpan.Zero);
@ -170,7 +171,7 @@ public class ChangeLogRepository : IChangeLogRepository
public async Task<IEnumerable<DateOnly>> GetDatesChange(Guid idDiscriminator, CancellationToken token)
{
var query = db.Set<ChangeLog>().Where(e => e.IdDiscriminator == idDiscriminator);
var query = db.Set<ChangeLog>().Where(e => e.DiscriminatorId == idDiscriminator);
var datesCreateQuery = query
.Select(e => e.Creation)
@ -185,7 +186,7 @@ public class ChangeLogRepository : IChangeLogRepository
var datesUpdate = await datesUpdateQuery.ToArrayAsync(token);
var dates = Enumerable.Concat(datesCreate, datesUpdate);
var dates = datesCreate.Concat(datesUpdate);
var datesOnly = dates
.Select(d => new DateOnly(d.Year, d.Month, d.Day))
.Distinct()
@ -201,7 +202,7 @@ public class ChangeLogRepository : IChangeLogRepository
Id = Uuid7.Guid(),
Creation = DateTimeOffset.UtcNow,
IdAuthor = idAuthor,
IdDiscriminator = idDiscriminator,
DiscriminatorId = idDiscriminator,
IdEditor = idAuthor,
Value = dto.Value
@ -213,8 +214,8 @@ public class ChangeLogRepository : IChangeLogRepository
public async Task<IEnumerable<ChangeLogValuesDto>> GetGtDate(Guid idDiscriminator, DateTimeOffset dateBegin, CancellationToken token)
{
var date = dateBegin.ToUniversalTime();
var query = this.db.Set<ChangeLog>()
.Where(e => e.IdDiscriminator == idDiscriminator)
var query = db.Set<ChangeLog>()
.Where(e => e.DiscriminatorId == idDiscriminator)
.Where(e => e.Creation >= date || e.Obsolete >= date);
var entities = await query.ToArrayAsync(token);
@ -227,12 +228,12 @@ public class ChangeLogRepository : IChangeLogRepository
public async Task<DatesRangeDto?> GetDatesRange(Guid idDiscriminator, CancellationToken token)
{
var query = db.Set<ChangeLog>()
.Where(e => e.IdDiscriminator == idDiscriminator)
.Where(e => e.DiscriminatorId == 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),
});

View File

@ -4,7 +4,7 @@ using DD.Persistence.Repositories;
using Mapster;
using Microsoft.EntityFrameworkCore;
namespace DD.Persistence.Repository.Repositories;
namespace DD.Persistence.Database.Postgres.Repositories;
public class DataSourceSystemRepository : IDataSourceSystemRepository
{
protected DbContext db;
@ -12,7 +12,7 @@ public class DataSourceSystemRepository : IDataSourceSystemRepository
{
this.db = db;
}
protected virtual IQueryable<DataSourceSystem> GetQueryReadOnly() => db.Set<DataSourceSystem>();
protected 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.Repository.Repositories;
namespace DD.Persistence.Database.Postgres.Repositories;
public class ParameterRepository : IParameterRepository
{
private DbContext db;
@ -15,7 +15,7 @@ public class ParameterRepository : IParameterRepository
this.db = db;
}
protected virtual IQueryable<ParameterData> GetQueryReadOnly() => db.Set<ParameterData>();
protected IQueryable<ParameterData> GetQueryReadOnly() => db.Set<ParameterData>();
public async Task<DatesRangeDto> GetDatesRangeAsync(Guid idDiscriminator, CancellationToken token)
{

View File

@ -0,0 +1,43 @@
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

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

View File

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

View File

@ -1,22 +1,25 @@
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.Repository.Repositories;
namespace DD.Persistence.Database.Postgres.Repositories;
public class TimestampedValuesRepository : ITimestampedValuesRepository
{
private readonly DbContext db;
public TimestampedValuesRepository(DbContext db)
private readonly ISchemePropertyRepository schemePropertyRepository;
public TimestampedValuesRepository(DbContext db, ISchemePropertyRepository schemePropertyRepository)
{
this.db = db;
this.schemePropertyRepository = schemePropertyRepository;
}
protected virtual IQueryable<TimestampedValues> GetQueryReadOnly() => this.db.Set<TimestampedValues>();
protected IQueryable<TimestampedValues> GetQueryReadOnly() => db.Set<TimestampedValues>();
public async virtual Task<int> AddRange(Guid discriminatorId, IEnumerable<TimestampedValuesDto> dtos, CancellationToken token)
public async Task<int> AddRange(Guid discriminatorId, IEnumerable<TimestampedValuesDto> dtos, CancellationToken token)
{
var timestampedValuesEntities = dtos.Select(dto => new TimestampedValues()
{
@ -24,36 +27,47 @@ public class TimestampedValuesRepository : ITimestampedValuesRepository
Timestamp = dto.Timestamp.ToUniversalTime(),
Values = dto.Values.Values.ToArray()
});
await db.Set<TimestampedValues>().AddRangeAsync(timestampedValuesEntities, token);
await db.AddRangeAsync(timestampedValuesEntities, token);
var result = await db.SaveChangesAsync(token);
return result;
}
public async virtual Task<IDictionary<Guid, IEnumerable<(DateTimeOffset Timestamp, object[] Values)>>> Get(IEnumerable<Guid> discriminatorIds,
DateTimeOffset? timestampBegin,
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 query = GetQueryReadOnly()
.Where(entity => discriminatorIds.Contains(entity.DiscriminatorId));
// Фильтрация по дате
if (timestampBegin.HasValue)
var resultQuery = Array.Empty<TimestampedValues>().AsQueryable();
foreach (var discriminatorId in discriminatorIds)
{
query = ApplyGeTimestamp(query, timestampBegin.Value);
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;
}
// Группировка отсортированных значений по DiscriminatorId
var groupQuery = 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 groupQuery.ToArrayAsync(token);
.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
@ -62,7 +76,7 @@ public class TimestampedValuesRepository : ITimestampedValuesRepository
return result;
}
public async virtual Task<IEnumerable<(DateTimeOffset Timestamp, object[] Values)>> GetFirst(Guid discriminatorId, int takeCount, CancellationToken token)
public async Task<IEnumerable<(DateTimeOffset Timestamp, object[] Values)>> GetFirst(Guid discriminatorId, int takeCount, CancellationToken token)
{
var query = GetQueryReadOnly()
.OrderBy(e => e.Timestamp)
@ -77,7 +91,7 @@ public class TimestampedValuesRepository : ITimestampedValuesRepository
return result;
}
public async virtual Task<IEnumerable<(DateTimeOffset Timestamp, object[] Values)>> GetLast(Guid discriminatorId, int takeCount, CancellationToken token)
public async Task<IEnumerable<(DateTimeOffset Timestamp, object[] Values)>> GetLast(Guid discriminatorId, int takeCount, CancellationToken token)
{
var query = GetQueryReadOnly()
.OrderByDescending(e => e.Timestamp)
@ -93,7 +107,7 @@ public class TimestampedValuesRepository : ITimestampedValuesRepository
}
// ToDo: прореживание должно осуществляться до материализации
public async virtual Task<IEnumerable<(DateTimeOffset Timestamp, object[] Values)>> GetResampledData(
public async Task<IEnumerable<(DateTimeOffset Timestamp, object[] Values)>> GetResampledData(
Guid discriminatorId,
DateTimeOffset dateBegin,
double intervalSec = 600d,
@ -114,10 +128,11 @@ public class TimestampedValuesRepository : ITimestampedValuesRepository
return result;
}
public async virtual Task<IEnumerable<(DateTimeOffset Timestamp, object[] Values)>> GetGtDate(Guid discriminatorId, DateTimeOffset timestampBegin, CancellationToken token)
public async Task<IEnumerable<(DateTimeOffset Timestamp, object[] Values)>> GetGtDate(Guid discriminatorId, DateTimeOffset gtTimestamp, CancellationToken token)
{
var gtTimestampUtc = gtTimestamp.ToUniversalTime();
var query = GetQueryReadOnly()
.Where(e => e.Timestamp > timestampBegin);
.Where(entity => entity.Timestamp > gtTimestampUtc);
var entities = await query.ToArrayAsync(token);
var result = entities.Select(e => (
@ -128,7 +143,7 @@ public class TimestampedValuesRepository : ITimestampedValuesRepository
return result;
}
public async virtual Task<DatesRangeDto?> GetDatesRange(Guid discriminatorId, CancellationToken token)
public async Task<DatesRangeDto?> GetDatesRange(Guid discriminatorId, CancellationToken token)
{
var query = GetQueryReadOnly()
.GroupBy(entity => entity.DiscriminatorId)
@ -153,26 +168,12 @@ public class TimestampedValuesRepository : ITimestampedValuesRepository
return dto;
}
public virtual Task<int> Count(Guid discriminatorId, CancellationToken token)
public async Task<int> Count(Guid discriminatorId, CancellationToken token)
{
var dbSet = db.Set<TimestampedValues>();
var query = dbSet.Where(entity => entity.DiscriminatorId == discriminatorId);
var query = GetQueryReadOnly()
.Where(e => e.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);
var result = await query.CountAsync(token);
return result;
}

View File

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

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.Repository.RepositoriesCached;
public class DataSchemeCachedRepository : DataSchemeRepository
namespace DD.Persistence.Database.RepositoriesCached;
public class SchemePropertyCachedRepository : SchemePropertyRepository
{
private readonly IMemoryCache memoryCache;
public DataSchemeCachedRepository(DbContext db, IMemoryCache memoryCache) : base(db)
public SchemePropertyCachedRepository(DbContext db, IMemoryCache memoryCache) : base(db)
{
this.memoryCache = memoryCache;
}
public override async Task Add(DataSchemeDto dataSourceSystemDto, CancellationToken token)
public override async Task AddRange(DataSchemeDto dataSourceSystemDto, CancellationToken token)
{
await base.Add(dataSourceSystemDto, token);
await base.AddRange(dataSourceSystemDto, token);
memoryCache.Set(dataSourceSystemDto.DiscriminatorId, dataSourceSystemDto);
}

View File

@ -0,0 +1,22 @@
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

@ -0,0 +1,15 @@
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

@ -0,0 +1,22 @@
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

@ -0,0 +1,22 @@
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

@ -0,0 +1,22 @@
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

@ -0,0 +1,22 @@
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

@ -0,0 +1,22 @@
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

@ -0,0 +1,22 @@
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

@ -102,7 +102,7 @@ public class ChangeLogControllerTest : BaseIntegrationTest
var result = await client.Add(idDiscriminator, dto, new CancellationToken());
var entity = dbContext.ChangeLog
.Where(x => x.IdDiscriminator == idDiscriminator)
.Where(x => x.DiscriminatorId == idDiscriminator)
.FirstOrDefault();
dto = entity.Adapt<ChangeLogValuesDto>();
@ -318,7 +318,7 @@ public class ChangeLogControllerTest : BaseIntegrationTest
var entities = dtos.Select(d =>
{
var entity = d.Adapt<ChangeLog>();
entity.IdDiscriminator = idDiscriminator;
entity.DiscriminatorId = idDiscriminator;
entity.Creation = DateTimeOffset.UtcNow.AddDays(generatorRandomDigits.Next(minDayCount, maxDayCount));
return entity;

View File

@ -3,7 +3,6 @@ using DD.Persistence.Client.Clients;
using DD.Persistence.Client.Clients.Interfaces;
using DD.Persistence.Client.Clients.Interfaces.Refit;
using DD.Persistence.Database.Entity;
using DD.Persistence.Extensions;
using DD.Persistence.Models;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
@ -39,7 +38,7 @@ public class TimestampedValuesControllerTest : BaseIntegrationTest
}
[Fact]
public async Task Get_returns_success()
public async Task Get_returns_BadRequest()
{
//arrange
Cleanup();
@ -50,11 +49,18 @@ public class TimestampedValuesControllerTest : BaseIntegrationTest
var secondDiscriminatorId = Guid.NewGuid();
discriminatorIds.Append(secondDiscriminatorId);
//act
var response = await timestampedValuesClient.Get([firstDiscriminatorId, secondDiscriminatorId], null, null, 0, 1, CancellationToken.None);
try
{
//act
var response = await timestampedValuesClient.Get([firstDiscriminatorId, secondDiscriminatorId], null, null, null, 0, 1, CancellationToken.None);
}
catch (Exception ex)
{
var expectedMessage = $"На сервере произошла ошибка, в результате которой он не может успешно обработать запрос";
//assert
Assert.Null(response);
//assert
Assert.Equal(expectedMessage, ex.Message);
}
}
[Fact]
@ -71,22 +77,25 @@ public class TimestampedValuesControllerTest : BaseIntegrationTest
var timestampBegin = DateTimeOffset.UtcNow.AddDays(-1);
var columnNames = new List<string>() { "A", "C" };
var skip = 2;
var take = 16;
var skip = 0;
var take = 6; // Ровно столько значений будет удовлетворять фильтру (\"A\">3) (для одного дискриминатора)
var customFilter = "(\"A\">3)";
var dtos = (await AddRange(firstDiscriminatorId)).ToList();
dtos.AddRange(await AddRange(secondDiscriminatorId));
//act
var response = await timestampedValuesClient.Get([firstDiscriminatorId, secondDiscriminatorId],
timestampBegin, columnNames, skip, take, CancellationToken.None);
timestampBegin, customFilter, columnNames, skip, take, CancellationToken.None);
//assert
Assert.NotNull(response);
Assert.NotEmpty(response);
var expectedCount = take * 2;
var actualCount = response.Count();
Assert.Equal(take, actualCount);
Assert.Equal(expectedCount, actualCount);
var actualColumnNames = response.SelectMany(e => e.Values.Keys).Distinct().ToList();
Assert.Equal(columnNames, actualColumnNames);
@ -267,7 +276,7 @@ public class TimestampedValuesControllerTest : BaseIntegrationTest
var count = 2048;
var timestampBegin = DateTimeOffset.UtcNow;
var dtos = await AddRange(discriminatorId, count);
//act
var response = await timestampedValuesClient.GetResampledData(discriminatorId, timestampBegin, count);
@ -379,7 +388,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;
}
@ -407,8 +416,12 @@ public class TimestampedValuesControllerTest : BaseIntegrationTest
private void Cleanup()
{
foreach (var item in discriminatorIds)
{
memoryCache.Remove(item);
}
discriminatorIds = [];
dbContext.CleanupDbSet<TimestampedValues>();
dbContext.CleanupDbSet<DataScheme>();
dbContext.CleanupDbSet<SchemeProperty>();
}
}

View File

@ -1,9 +1,11 @@
namespace DD.Persistence.Models;
using System.Collections;
namespace DD.Persistence.Models;
/// <summary>
/// Схема для набора данных
/// </summary>
public class DataSchemeDto
public class DataSchemeDto : IEnumerable<SchemePropertyDto>, IEquatable<IEnumerable<SchemePropertyDto>>
{
/// <summary>
/// Дискриминатор
@ -11,7 +13,30 @@ public class DataSchemeDto
public Guid DiscriminatorId { get; set; }
/// <summary>
/// Наименования полей
/// Поля
/// </summary>
public string[] PropNames { get; set; } = [];
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);
}
}

View File

@ -0,0 +1,32 @@
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,17 @@
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,30 @@
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

@ -0,0 +1,23 @@
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

@ -0,0 +1,23 @@
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,7 +20,6 @@
<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,12 +1,7 @@
using DD.Persistence.Database.Model;
using DD.Persistence.Repository.Repositories;
using DD.Persistence.Database.Postgres.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>
@ -29,7 +24,6 @@ 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,19 +0,0 @@
<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,34 +0,0 @@
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

@ -1,103 +0,0 @@
//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,8 +16,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DD.Persistence.Database\DD.Persistence.Database.csproj" />
<ProjectReference Include="..\DD.Persistence\DD.Persistence.csproj" />
<ProjectReference Include="..\DD.Persistence.Database.Postgres\DD.Persistence.Database.Postgres.csproj" />
</ItemGroup>
<ItemGroup>

View File

@ -0,0 +1,278 @@
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,14 +1,15 @@
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.Repository.Test;
namespace DD.Persistence.Test;
public class TimestampedValuesServiceShould
{
private readonly ITimestampedValuesRepository timestampedValuesRepository = Substitute.For<ITimestampedValuesRepository>();
private readonly IDataSchemeRepository dataSchemeRepository = Substitute.For<IDataSchemeRepository>();
private TimestampedValuesService timestampedValuesService;
private readonly ISchemePropertyRepository dataSchemeRepository = Substitute.For<ISchemePropertyRepository>();
private readonly TimestampedValuesService timestampedValuesService;
public TimestampedValuesServiceShould()
{
@ -33,22 +34,21 @@ public class TimestampedValuesServiceShould
.AddHours(-1)
.ToUniversalTime();
var getResult = await timestampedValuesService
.Get(discriminatorIds, geTimestamp, columnNames, 0, count, CancellationToken.None);
.Get(discriminatorIds, geTimestamp, null, 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", i },
{ "B", i * 1.1 },
{ "C", $"Any{i}" },
{ "D", DateTimeOffset.Now },
{ "A", GetJsonFromObject(i) },
{ "B", GetJsonFromObject(i * 1.1) },
{ "C", GetJsonFromObject($"Any{i}") },
{ "D", GetJsonFromObject(DateTimeOffset.Now) }
};
yield return new TimestampedValuesDto()
@ -58,4 +58,11 @@ public class TimestampedValuesServiceShould
};
}
}
private static JsonElement GetJsonFromObject(object value)
{
var jsonString = JsonSerializer.Serialize(value);
var doc = JsonDocument.Parse(jsonString);
return doc.RootElement;
}
}

View File

@ -0,0 +1,107 @@
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,8 +7,6 @@ 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}"
@ -51,10 +49,6 @@ 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

View File

@ -0,0 +1,22 @@
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

@ -0,0 +1,55 @@
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

@ -0,0 +1,47 @@
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

@ -0,0 +1,33 @@
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

@ -0,0 +1,33 @@
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

@ -0,0 +1,27 @@
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

@ -0,0 +1,83 @@
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;
}
}

View File

@ -0,0 +1,24 @@
using DD.Persistence.Filter.Models.Enumerations;
using DD.Persistence.Filter.TreeBuilder.Expressions.NonTerminal.Abstractions;
namespace DD.Persistence.Filter.TreeBuilder.Expressions.NonTerminal;
/// <summary>
/// Выражение для "И"
/// </summary>
class AndExpression : NonTerminalExpression
{
private const string AndString = "&&";
/// <inheritdoc/>
public override OperationEnum GetOperation()
{
return OperationEnum.And;
}
/// <inheritdoc/>
public override string GetOperationString()
{
return AndString;
}
}

View File

@ -0,0 +1,24 @@
using DD.Persistence.Filter.Models.Enumerations;
using DD.Persistence.Filter.TreeBuilder.Expressions.NonTerminal.Abstractions;
namespace DD.Persistence.Filter.TreeBuilder.Expressions.NonTerminal;
/// <summary>
/// Выражение для "ИЛИ"
/// </summary>
class OrExpression : NonTerminalExpression
{
private const string OrString = @"\|\|";
/// <inheritdoc/>
public override OperationEnum GetOperation()
{
return OperationEnum.Or;
}
/// <inheritdoc/>
public override string GetOperationString()
{
return OrString;
}
}

View File

@ -0,0 +1,80 @@
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.Terminal.Abstract;
/// <summary>
/// Абстрактный класс для терминальных выражений
/// </summary>
abstract class TerminalExpression : IExpression
{
/// <summary>
/// Реализация правила для терминальных выражений
/// </summary>
/// <param name="context"></param>
public void Interpret(InterpreterContext context)
{
var operation = GetOperation();
var operationString = GetOperationString();
var matches = GetMatches(context, operationString);
matches.ForEach(m =>
{
var matchString = m.ToString();
var pair = matchString
.Trim(['(', ')'])
.Split(operationString);
var fieldName = pair
.First()
.Trim('\"');
var value = ParseValue(pair.Last());
var node = new TLeaf(operation, fieldName, value);
var key = context.TreeNodes.Count;
context.TreeNodes.Add(key, node);
var keyString = key.ToString();
context.TreeString = context.TreeString.Replace(matchString, keyString);
});
}
/// <inheritdoc/>
public abstract OperationEnum GetOperation();
/// <inheritdoc/>
public abstract string GetOperationString();
/// <summary>
/// Получить из акткуального состояния строки все совпадения для текущего выражения
/// </summary>
private static Match[] GetMatches(InterpreterContext context, string operationString)
{
string pattern = $@"\([^()]*{operationString}.*?\)";
Regex regex = new(pattern);
var matches = regex.Matches(context.TreeString).ToArray();
return matches;
}
private static object? ParseValue(string value)
{
if (double.TryParse(value, out _))
{
return double.Parse(value);
}
// ToDo: избавиться
var doubleValue= value.Replace('.', ',');
if (double.TryParse(doubleValue, out _))
{
return double.Parse(doubleValue);
}
value = value.Trim('\"');
return value;
}
}

View File

@ -0,0 +1,24 @@
using DD.Persistence.Filter.Models.Enumerations;
using DD.Persistence.Filter.TreeBuilder.Expressions.Terminal.Abstract;
namespace DD.Persistence.Filter.TreeBuilder.Expressions.Terminal;
/// <summary>
/// Выражение для "РАВНО"
/// </summary>
class EqualExpression : TerminalExpression
{
private const string EqualString = "==";
/// <inheritdoc/>
public override OperationEnum GetOperation()
{
return OperationEnum.Equal;
}
/// <inheritdoc/>
public override string GetOperationString()
{
return EqualString;
}
}

View File

@ -0,0 +1,24 @@
using DD.Persistence.Filter.Models.Enumerations;
using DD.Persistence.Filter.TreeBuilder.Expressions.Terminal.Abstract;
namespace DD.Persistence.Filter.TreeBuilder.Expressions.Terminal;
/// <summary>
/// Выражение для "МЕНЬШЕ"
/// </summary>
class LessExpression : TerminalExpression
{
private const string EqualString = "<";
/// <inheritdoc/>
public override OperationEnum GetOperation()
{
return OperationEnum.Less;
}
/// <inheritdoc/>
public override string GetOperationString()
{
return EqualString;
}
}

View File

@ -0,0 +1,24 @@
using DD.Persistence.Filter.Models.Enumerations;
using DD.Persistence.Filter.TreeBuilder.Expressions.Terminal.Abstract;
namespace DD.Persistence.Filter.TreeBuilder.Expressions.Terminal;
/// <summary>
/// Выражение для "МЕНЬШЕ ЛИБО РАВНО"
/// </summary>
class LessOrEqualExpression : TerminalExpression
{
private const string EqualString = "<=";
/// <inheritdoc/>
public override OperationEnum GetOperation()
{
return OperationEnum.LessOrEqual;
}
/// <inheritdoc/>
public override string GetOperationString()
{
return EqualString;
}
}

View File

@ -0,0 +1,24 @@
using DD.Persistence.Filter.Models.Enumerations;
using DD.Persistence.Filter.TreeBuilder.Expressions.Terminal.Abstract;
namespace DD.Persistence.Filter.TreeBuilder.Expressions.Terminal;
/// <summary>
/// Выражение для "БОЛЬШЕ"
/// </summary>
class MoreExpression : TerminalExpression
{
private const string EqualString = ">";
/// <inheritdoc/>
public override OperationEnum GetOperation()
{
return OperationEnum.Greate;
}
/// <inheritdoc/>
public override string GetOperationString()
{
return EqualString;
}
}

View File

@ -0,0 +1,24 @@
using DD.Persistence.Filter.Models.Enumerations;
using DD.Persistence.Filter.TreeBuilder.Expressions.Terminal.Abstract;
namespace DD.Persistence.Filter.TreeBuilder.Expressions.Terminal;
/// <summary>
/// Выражение для "БОЛЬШЕ ЛИБО РАВНО"
/// </summary>
class MoreOrEqualExpression : TerminalExpression
{
private const string EqualString = ">=";
/// <inheritdoc/>
public override OperationEnum GetOperation()
{
return OperationEnum.GreateOrEqual;
}
/// <inheritdoc/>
public override string GetOperationString()
{
return EqualString;
}
}

View File

@ -0,0 +1,24 @@
using DD.Persistence.Filter.Models.Enumerations;
using DD.Persistence.Filter.TreeBuilder.Expressions.Terminal.Abstract;
namespace DD.Persistence.Filter.TreeBuilder.Expressions.Terminal;
/// <summary>
/// Выражение для "НЕРАВНО"
/// </summary>
class NotEqualExpression : TerminalExpression
{
private const string NotEqulString = "!=";
/// <inheritdoc/>
public override OperationEnum GetOperation()
{
return OperationEnum.NotEqual;
}
/// <inheritdoc/>
public override string GetOperationString()
{
return NotEqulString;
}
}

View File

@ -0,0 +1,54 @@
using DD.Persistence.Filter.Models.Abstractions;
using DD.Persistence.Filter.TreeBuilder.Expressions.Abstractions;
using DD.Persistence.Filter.TreeBuilder.Expressions.NonTerminal;
using DD.Persistence.Filter.TreeBuilder.Expressions.Terminal;
namespace DD.Persistence.Filter.TreeBuilder;
/// <summary>
/// Строитель бинарных деревьев
/// </summary>
public static class FilterTreeBuilder
{
/// <summary>
/// Построить бинарное дерево логических операций сравнения из строки
/// </summary>
/// <param name="treeString"></param>
/// <returns></returns>
public static TNode? BuildTree(this string treeString)
{
InterpreterContext context = new(treeString);
// Порядок важен
List<IExpression> terminalExpressions =
[
new EqualExpression(),
new NotEqualExpression(),
new MoreOrEqualExpression(),
new LessOrEqualExpression(),
new MoreExpression(),
new LessExpression()
];
terminalExpressions.ForEach(e =>
{
e.Interpret(context);
});
// Порядок важен
List<IExpression> nonTerminalExpressions =
[
new OrExpression(),
new AndExpression()
];
while (!string.IsNullOrEmpty(context.TreeString))
{
nonTerminalExpressions.ForEach(e =>
{
e.Interpret(context);
});
}
return context.Root;
}
}

View File

@ -0,0 +1,30 @@
using DD.Persistence.Filter.Models.Abstractions;
namespace DD.Persistence.Filter.TreeBuilder;
/// <summary>
/// Контекст интерпретатора
/// </summary>
class InterpreterContext
{
/// <summary>
/// Корень дерева (результат интерпретации)
/// </summary>
public TNode? Root { get; set; }
/// <summary>
/// Дерево в виде строки (входной параметр)
/// </summary>
public string TreeString { get; set; }
/// <summary>
/// Проиндексированные вершины дерева
/// </summary>
public Dictionary<int, TNode> TreeNodes { get; set; } = [];
/// <inheritdoc/>
public InterpreterContext(string theeString)
{
TreeString = theeString;
}
}

View File

@ -0,0 +1,24 @@
using DD.Persistence.Filter.Models;
using DD.Persistence.Filter.Models.Abstractions;
namespace DD.Persistence.Filter.Visitors;
/// <inheritdoc/>
public class NodeVisitor<TVisitResult> : INodeVisitor<TVisitResult>
{
private readonly Func<TVertex, TVisitResult> _ifVertex;
private readonly Func<TLeaf, TVisitResult> _ifLeaf;
/// <inheritdoc/>
public NodeVisitor(Func<TVertex, TVisitResult> ifVertex, Func<TLeaf, TVisitResult> ifLeaf)
{
_ifVertex = ifVertex;
_ifLeaf = ifLeaf;
}
/// <inheritdoc/>
public TVisitResult Visit(TVertex vertex) => _ifVertex(vertex);
/// <inheritdoc/>
public TVisitResult Visit(TLeaf leaf) => _ifLeaf(leaf);
}

View File

@ -5,7 +5,7 @@ namespace DD.Persistence.Repositories;
/// <summary>
/// Репозиторий для работы со схемами наборов данных
/// </summary>
public interface IDataSchemeRepository
public interface ISchemePropertyRepository
{
/// <summary>
/// Добавить схему
@ -13,7 +13,7 @@ public interface IDataSchemeRepository
/// <param name="dataSourceSystemDto"></param>
/// <param name="token"></param>
/// <returns></returns>
Task Add(DataSchemeDto dataSourceSystemDto, CancellationToken token);
Task AddRange(DataSchemeDto dataSourceSystemDto, CancellationToken token);
/// <summary>
/// Вычитать схему

View File

@ -1,4 +1,5 @@
using DD.Persistence.Models;
using DD.Persistence.Filter.Models.Abstractions;
using DD.Persistence.Models;
using DD.Persistence.RepositoriesAbstractions;
namespace DD.Persistence.Repositories;
@ -30,6 +31,7 @@ public interface ITimestampedValuesRepository : ISyncRepository, ITimeSeriesBase
/// </summary>
/// <param name="idDiscriminators">Набор дискриминаторов (идентификаторов)</param>
/// <param name="geTimestamp">Фильтр позднее даты</param>
/// <param name="filterTree"></param>
/// <param name="columnNames">Фильтр свойств набора. Можно запросить только некоторые свойства из набора</param>
/// <param name="skip"></param>
/// <param name="take"></param>
@ -37,6 +39,7 @@ public interface ITimestampedValuesRepository : ISyncRepository, ITimeSeriesBase
/// <returns></returns>
Task<IDictionary<Guid, IEnumerable<(DateTimeOffset Timestamp, object[] Values)>>> Get(IEnumerable<Guid> idDiscriminators,
DateTimeOffset? geTimestamp,
TNode? filterTree,
IEnumerable<string>? columnNames,
int skip,
int take,

View File

@ -1,4 +1,5 @@
using DD.Persistence.Models;
using DD.Persistence.Filter.Models.Abstractions;
using DD.Persistence.Models;
using DD.Persistence.Models.Common;
namespace DD.Persistence.Services.Interfaces;
@ -22,13 +23,14 @@ public interface ITimestampedValuesService
/// </summary>
/// <param name="discriminatorIds">Набор дискриминаторов (идентификаторов)</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>
/// <returns></returns>
Task<IEnumerable<TimestampedValuesDto>> Get(IEnumerable<Guid> discriminatorIds, DateTimeOffset? geTimestamp,
IEnumerable<string>? columnNames, int skip, int take, CancellationToken token);
TNode? filterTree, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token);
/// <summary>
/// Получение данных с начала

View File

@ -1,8 +1,9 @@
using DD.Persistence.Extensions;
using DD.Persistence.Filter.Models.Abstractions;
using DD.Persistence.Models;
using DD.Persistence.Models.Common;
using DD.Persistence.Repositories;
using DD.Persistence.Services.Interfaces;
using System.Text.Json;
namespace DD.Persistence.Services;
@ -10,10 +11,10 @@ namespace DD.Persistence.Services;
public class TimestampedValuesService : ITimestampedValuesService
{
private readonly ITimestampedValuesRepository timestampedValuesRepository;
private readonly IDataSchemeRepository dataSchemeRepository;
private readonly ISchemePropertyRepository dataSchemeRepository;
/// <inheritdoc/>
public TimestampedValuesService(ITimestampedValuesRepository timestampedValuesRepository, IDataSchemeRepository relatedDataRepository)
public TimestampedValuesService(ITimestampedValuesRepository timestampedValuesRepository, ISchemePropertyRepository relatedDataRepository)
{
this.timestampedValuesRepository = timestampedValuesRepository;
this.dataSchemeRepository = relatedDataRepository;
@ -25,8 +26,7 @@ public class TimestampedValuesService : ITimestampedValuesService
// ToDo: реализовать без foreach
foreach (var dto in dtos)
{
var keys = dto.Values.Keys.ToArray();
await CreateSystemSpecificationIfNotExist(discriminatorId, keys, token);
await CreateDataSchemeIfNotExist(discriminatorId, dto, token);
}
var result = await timestampedValuesRepository.AddRange(discriminatorId, dtos, token);
@ -35,11 +35,11 @@ public class TimestampedValuesService : ITimestampedValuesService
}
/// <inheritdoc/>
public async Task<IEnumerable<TimestampedValuesDto>> Get(IEnumerable<Guid> discriminatorIds, DateTimeOffset? geTimestamp, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token)
public async Task<IEnumerable<TimestampedValuesDto>> Get(IEnumerable<Guid> discriminatorIds, DateTimeOffset? geTimestamp, TNode? filterTree, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token)
{
var result = await timestampedValuesRepository.Get(discriminatorIds, geTimestamp, columnNames, skip, take, token);
var result = await timestampedValuesRepository.Get(discriminatorIds, geTimestamp, filterTree, columnNames, skip, take, token);
var dtos = await Materialize(result, token);
var dtos = await BindingToDataScheme(result, token);
if (!columnNames.IsNullOrEmpty())
{
@ -54,9 +54,9 @@ public class TimestampedValuesService : ITimestampedValuesService
{
var result = await timestampedValuesRepository.GetFirst(discriminatorId, takeCount, token);
var resultToMaterialize = new[] { KeyValuePair.Create(discriminatorId, result) }
var resultBeforeBinding = new[] { KeyValuePair.Create(discriminatorId, result) }
.ToDictionary();
var dtos = await Materialize(resultToMaterialize, token);
var dtos = await BindingToDataScheme(resultBeforeBinding, token);
return dtos;
}
@ -66,9 +66,9 @@ public class TimestampedValuesService : ITimestampedValuesService
{
var result = await timestampedValuesRepository.GetLast(discriminatorId, takeCount, token);
var resultToMaterialize = new[] { KeyValuePair.Create(discriminatorId, result) }
var resultBeforeBinding = new[] { KeyValuePair.Create(discriminatorId, result) }
.ToDictionary();
var dtos = await Materialize(resultToMaterialize, token);
var dtos = await BindingToDataScheme(resultBeforeBinding, token);
return dtos;
}
@ -83,9 +83,9 @@ public class TimestampedValuesService : ITimestampedValuesService
{
var result = await timestampedValuesRepository.GetResampledData(discriminatorId, beginTimestamp, intervalSec, approxPointsCount, token);
var resultToMaterialize = new[] { KeyValuePair.Create(discriminatorId, result) }
var resultBeforeBinding = new[] { KeyValuePair.Create(discriminatorId, result) }
.ToDictionary();
var dtos = await Materialize(resultToMaterialize, token);
var dtos = await BindingToDataScheme(resultBeforeBinding, token);
return dtos;
}
@ -94,42 +94,37 @@ public class TimestampedValuesService : ITimestampedValuesService
public async Task<IEnumerable<TimestampedValuesDto>> GetGtDate(Guid discriminatorId, DateTimeOffset beginTimestamp, CancellationToken token)
{
var result = await timestampedValuesRepository.GetGtDate(discriminatorId, beginTimestamp, token);
var resultToMaterialize = new[] { KeyValuePair.Create(discriminatorId, result) }
var resultBeforeBinding = new[] { KeyValuePair.Create(discriminatorId, result) }
.ToDictionary();
var dtos = await Materialize(resultToMaterialize, token);
var dtos = await BindingToDataScheme(resultBeforeBinding, token);
return dtos;
}
// ToDo: рефакторинг, переименовать (текущее название не отражает суть)
/// <summary>
/// Преобразовать результат запроса в набор dto
/// </summary>
/// <param name="queryResult"></param>
/// <param name="token"></param>
/// <returns></returns>
private async Task<IEnumerable<TimestampedValuesDto>> Materialize(IDictionary<Guid, IEnumerable<(DateTimeOffset Timestamp, object[] Values)>> queryResult, CancellationToken token)
private async Task<IEnumerable<TimestampedValuesDto>> BindingToDataScheme(IDictionary<Guid, IEnumerable<(DateTimeOffset Timestamp, object[] Values)>> queryResult, CancellationToken token)
{
IEnumerable<TimestampedValuesDto> result = [];
foreach (var keyValuePair in queryResult)
{
var dataScheme = await dataSchemeRepository.Get(keyValuePair.Key, token);
if (dataScheme is null)
{
continue;
}
foreach (var tuple in keyValuePair.Value)
foreach (var (Timestamp, Values) in keyValuePair.Value)
{
var identity = dataScheme!.PropNames;
var indexedIdentity = identity
.Select((value, index) => new { index, value });
var dto = new TimestampedValuesDto()
{
Timestamp = tuple.Timestamp.ToUniversalTime(),
Values = indexedIdentity
.ToDictionary(x => x.value, x => tuple.Values[x.index])
Timestamp = Timestamp.ToUniversalTime(),
Values = dataScheme
.ToDictionary(k => k.PropertyName, v => Values[v.Index])
};
result = result.Append(dto);
@ -140,34 +135,36 @@ public class TimestampedValuesService : ITimestampedValuesService
}
/// <summary>
/// Создать спецификацию, при отсутствии таковой
/// Создать схему данных, при отсутствии таковой
/// </summary>
/// <param name="discriminatorId">Дискриминатор системы</param>
/// <param name="fieldNames">Набор наименований полей</param>
/// <param name="discriminatorId">Дискриминатор схемы</param>
/// <param name="dto">Набор данных, по образу которого будет создана соответствующая схема</param>
/// <param name="token"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException">Некорректный набор наименований полей</exception>
private async Task CreateSystemSpecificationIfNotExist(Guid discriminatorId, string[] fieldNames, CancellationToken token)
private async Task CreateDataSchemeIfNotExist(Guid discriminatorId, TimestampedValuesDto dto, CancellationToken token)
{
var systemSpecification = await dataSchemeRepository.Get(discriminatorId, token);
if (systemSpecification is null)
var valuesList = dto.Values.ToList();
var properties = valuesList.Select((e, index) => new SchemePropertyDto()
{
systemSpecification = new DataSchemeDto()
{
DiscriminatorId = discriminatorId,
PropNames = fieldNames
};
await dataSchemeRepository.Add(systemSpecification, token);
Index = index,
PropertyName = e.Key,
PropertyKind = ((JsonElement)e.Value).ValueKind
});
var dataScheme = await dataSchemeRepository.Get(discriminatorId, token);
if (dataScheme is null)
{
dataScheme = new DataSchemeDto(discriminatorId, properties);
await dataSchemeRepository.AddRange(dataScheme, token);
return;
}
if (!systemSpecification.PropNames.SequenceEqual(fieldNames))
if (!dataScheme.Equals(properties))
{
var expectedFieldNames = string.Join(", ", systemSpecification.PropNames);
var actualFieldNames = string.Join(", ", fieldNames);
throw new InvalidOperationException($"Для системы {discriminatorId.ToString()} " +
$"характерен набор данных: [{expectedFieldNames}], однако был передан набор: [{actualFieldNames}]");
$"был передан нехарактерный набор данных");
}
}
@ -177,7 +174,7 @@ public class TimestampedValuesService : ITimestampedValuesService
/// <param name="dtos"></param>
/// <param name="fieldNames">Поля, которые необходимо оставить</param>
/// <returns></returns>
private IEnumerable<TimestampedValuesDto> ReduceSetColumnsByNames(IEnumerable<TimestampedValuesDto> dtos, IEnumerable<string> fieldNames)
private static IEnumerable<TimestampedValuesDto> ReduceSetColumnsByNames(IEnumerable<TimestampedValuesDto> dtos, IEnumerable<string> fieldNames)
{
var result = dtos.Select(dto =>
{

View File

@ -1 +1,10 @@
# Persistence
# Persistence
## Инструкция по развертыванию persistence в docker
1. Необходимо скопировать себе локально папку **.docker**, которая находится внутри проекта **persistence**
2. Авторизоваться в gitea-registry при помощи командры: `docker login -u пользователь -p пароль https://git.ddrilling.ru`
3. Из папки **.docker** запустить команду:
`docker-compose up`
4. При успешном старте persistence необходимо откорректировать ссылку в браузере: `[host]:[port]/swagger/index.html`