Добавить таблицу для учета комментариев и действий пользователя для вывода статистики по ChangeLog #30

Open
on.nemtina wants to merge 24 commits from feature/#956-change-log-table-comment into master
25 changed files with 1117 additions and 396 deletions

View File

@ -1,107 +1,124 @@
using Microsoft.AspNetCore.Authorization; using DD.Persistence.API;
using Microsoft.AspNetCore.Mvc; using DD.Persistence.API.Services;
using DD.Persistence.Models; using DD.Persistence.Models;
using DD.Persistence.Models.Requests;
using DD.Persistence.Repositories;
using System.Net;
using DD.Persistence.Models.Common; using DD.Persistence.Models.Common;
using DD.Persistence.Models.Requests;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Net;
using UuidExtensions;
namespace DD.Persistence.API.Controllers; /// <summary>
/// Контроллер по работе с журналом изменений
/// </summary>
[ApiController] [ApiController]
[Authorize] [Authorize]
[Route("api/[controller]")] [Route("api/[controller]")]
public class ChangeLogController : ControllerBase, IChangeLogApi public class ChangeLogController : ControllerBase, IChangeLogApi
{ {
private readonly IChangeLogRepository repository; private ChangeLogService service { get; }
public ChangeLogController(IChangeLogRepository repository) /// <summary>
/// ctor
/// </summary>
/// <param name="service"></param>
public ChangeLogController(ChangeLogService service)
{ {
this.repository = repository; this.service = service;
} }
/// <summary>
/// Добавить записи в журнал изменений по дискриминатору
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dtos"></param>
/// <param name="comment"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpPost("{idDiscriminator}")] [HttpPost("{idDiscriminator}")]
[ProducesResponseType(typeof(int), (int)HttpStatusCode.Created)] [ProducesResponseType(typeof(int), (int)HttpStatusCode.Created)]
public async Task<IActionResult> Add(
[FromRoute] Guid idDiscriminator,
[FromBody] ChangeLogValuesDto dto,
CancellationToken token)
{
var userId = User.GetUserId<Guid>();
var result = await repository.AddRange(userId, idDiscriminator, [dto], token);
return CreatedAtAction(nameof(Add), result);
}
[HttpPost("range/{idDiscriminator}")]
[ProducesResponseType(typeof(int), (int)HttpStatusCode.Created)]
public async Task<IActionResult> AddRange( public async Task<IActionResult> AddRange(
[FromRoute] Guid idDiscriminator, [FromRoute] Guid idDiscriminator,
[FromBody] IEnumerable<ChangeLogValuesDto> dtos, [FromBody] IEnumerable<ChangeLogValuesDto> dtos,
string comment,
CancellationToken token) CancellationToken token)
{ {
var userId = User.GetUserId<Guid>(); var userId = User.GetUserId<Guid>();
var result = await repository.AddRange(userId, idDiscriminator, dtos, token); var changeLogCommitRequest = new CreateChangeLogCommitRequest(Uuid7.Guid(), comment);
var result = await service.AddRange(idDiscriminator, changeLogCommitRequest, dtos, token);
return CreatedAtAction(nameof(AddRange), result); return CreatedAtAction(nameof(AddRange), result);
} }
/// <summary>
/// Удалить записи в журнале изменений
/// </summary>
/// <param name="ids"></param>
/// <param name="comment"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpDelete] [HttpDelete]
[ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)]
public async Task<IActionResult> Delete(Guid id, CancellationToken token) public async Task<IActionResult> DeleteRange(IEnumerable<Guid> ids, string comment, CancellationToken token)
{ {
var userId = User.GetUserId<Guid>(); var userId = User.GetUserId<Guid>();
var result = await repository.MarkAsDeleted(userId, [id], token); var changeLogCommitRequest = new CreateChangeLogCommitRequest(userId, comment);
var result = await service.MarkAsDeleted(ids, changeLogCommitRequest, token);
return Ok(result); return Ok(result);
} }
[HttpDelete("range")]
[ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)]
public async Task<IActionResult> DeleteRange(IEnumerable<Guid> ids, CancellationToken token)
{
var userId = User.GetUserId<Guid>();
var result = await repository.MarkAsDeleted(userId, ids, token);
return Ok(result);
}
/// <summary>
/// Очистить все записи в журнале изменений (по дискриминатору) и добавить новые
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dtos"></param>
/// <param name="comment"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpPost("replace/{idDiscriminator}")] [HttpPost("replace/{idDiscriminator}")]
[ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)]
public async Task<IActionResult> ClearAndAddRange( public async Task<IActionResult> ClearAndAddRange(
[FromRoute] Guid idDiscriminator, [FromRoute] Guid idDiscriminator,
[FromBody] IEnumerable<ChangeLogValuesDto> dtos, [FromBody] IEnumerable<ChangeLogValuesDto> dtos,
string comment,
CancellationToken token) CancellationToken token)
{ {
var userId = User.GetUserId<Guid>(); var userId = User.GetUserId<Guid>();
var result = await repository.ClearAndAddRange(userId, idDiscriminator, dtos, token); var changeLogCommitRequest = new CreateChangeLogCommitRequest(userId, comment);
var result = await service.ClearAndAddRange(idDiscriminator, changeLogCommitRequest, dtos, token);
return Ok(result); return Ok(result);
} }
/// <summary>
/// сохранить изменения в записях журнала изменений
/// </summary>
/// <param name="dtos"></param>
/// <param name="comment"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpPut] [HttpPut]
[ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)]
public async Task<IActionResult> Update(
ChangeLogValuesDto dto,
CancellationToken token)
{
var userId = User.GetUserId<Guid>();
var result = await repository.UpdateRange(userId, [dto], token);
return Ok(result);
}
[HttpPut("range")]
[ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)]
public async Task<IActionResult> UpdateRange( public async Task<IActionResult> UpdateRange(
IEnumerable<ChangeLogValuesDto> dtos, IEnumerable<ChangeLogValuesDto> dtos,
string comment,
CancellationToken token) CancellationToken token)
{ {
var userId = User.GetUserId<Guid>(); var userId = User.GetUserId<Guid>();
var result = await repository.UpdateRange(userId, dtos, token); var changeLogCommitRequest = new CreateChangeLogCommitRequest(userId, comment);
var result = await service.UpdateRange(changeLogCommitRequest, dtos, token);
return Ok(result); return Ok(result);
} }
/// <summary>
/// Получение актуальных записей (с пагинацией)
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="paginationRequest"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpGet("{idDiscriminator}")] [HttpGet("{idDiscriminator}")]
[ProducesResponseType(typeof(PaginationContainer<ChangeLogValuesDto>), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(PaginationContainer<ChangeLogValuesDto>), (int)HttpStatusCode.OK)]
public async Task<IActionResult> GetCurrent( public async Task<IActionResult> GetCurrent(
@ -110,11 +127,19 @@ public class ChangeLogController : ControllerBase, IChangeLogApi
CancellationToken token) CancellationToken token)
{ {
var moment = new DateTimeOffset(3000, 1, 1, 0, 0, 0, TimeSpan.Zero); var moment = new DateTimeOffset(3000, 1, 1, 0, 0, 0, TimeSpan.Zero);
var result = await repository.GetByDate(idDiscriminator, moment, paginationRequest, token); var result = await service.GetByDate(idDiscriminator, moment, paginationRequest, token);
return Ok(result); return Ok(result);
} }
/// <summary>
/// Получение записей на определенный момент времени (с пагинацией)
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="moment"></param>
/// <param name="paginationRequest"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpGet("moment/{idDiscriminator}")] [HttpGet("moment/{idDiscriminator}")]
[ProducesResponseType(typeof(PaginationContainer<ChangeLogValuesDto>), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(PaginationContainer<ChangeLogValuesDto>), (int)HttpStatusCode.OK)]
public async Task<IActionResult> GetByDate( public async Task<IActionResult> GetByDate(
@ -123,11 +148,19 @@ public class ChangeLogController : ControllerBase, IChangeLogApi
[FromQuery] PaginationRequest paginationRequest, [FromQuery] PaginationRequest paginationRequest,
CancellationToken token) CancellationToken token)
{ {
var result = await repository.GetByDate(idDiscriminator, moment, paginationRequest, token); var result = await service.GetByDate(idDiscriminator, moment, paginationRequest, token);
return Ok(result); return Ok(result);
} }
/// <summary>
/// Получение измененных записей за период времени
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dateBegin"></param>
/// <param name="dateEnd"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpGet("history/{idDiscriminator}")] [HttpGet("history/{idDiscriminator}")]
[ProducesResponseType(typeof(IEnumerable<ChangeLogDto>), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(IEnumerable<ChangeLogDto>), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)] [ProducesResponseType((int)HttpStatusCode.NoContent)]
@ -137,37 +170,57 @@ public class ChangeLogController : ControllerBase, IChangeLogApi
DateTimeOffset dateEnd, DateTimeOffset dateEnd,
CancellationToken token) CancellationToken token)
{ {
var result = await repository.GetChangeLogForInterval(idDiscriminator, dateBegin, dateEnd, token); var result = await service.GetChangeLogForInterval(idDiscriminator, dateBegin, dateEnd, token);
return Ok(result); return Ok(result);
} }
/// <summary>
/// Получение списка дат, в которые происходили изменения (день, месяц, год, без времени)
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpGet("datesChange/{idDiscriminator}")] [HttpGet("datesChange/{idDiscriminator}")]
[ProducesResponseType(typeof(IEnumerable<DateOnly>), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(IEnumerable<DateOnly>), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)] [ProducesResponseType((int)HttpStatusCode.NoContent)]
public async Task<IActionResult> GetDatesChange([FromRoute] Guid idDiscriminator, CancellationToken token) public async Task<IActionResult> GetDatesChange([FromRoute] Guid idDiscriminator, CancellationToken token)
{ {
var result = await repository.GetDatesChange(idDiscriminator, token); var result = await service.GetDatesChange(idDiscriminator, token);
return Ok(result); return Ok(result);
} }
/// <summary>
/// Получение данных, начиная с определенной даты
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dateBegin"></param>
/// <param name="take"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpGet("part/{idDiscriminator}")] [HttpGet("part/{idDiscriminator}")]
[ProducesResponseType(typeof(IEnumerable<ChangeLogValuesDto>), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(IEnumerable<ChangeLogValuesDto>), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)] [ProducesResponseType((int)HttpStatusCode.NoContent)]
public async Task<ActionResult<IEnumerable<ChangeLogValuesDto>>> GetPart([FromRoute] Guid idDiscriminator, DateTimeOffset dateBegin, int take = 86400, CancellationToken token = default) public async Task<ActionResult<IEnumerable<ChangeLogValuesDto>>> GetPart([FromRoute] Guid idDiscriminator, DateTimeOffset dateBegin, int take = 86400, CancellationToken token = default)
{ {
var result = await repository.GetGtDate(idDiscriminator, dateBegin, token); var result = await service.GetGtDate(idDiscriminator, dateBegin, token);
return Ok(result); return Ok(result);
} }
/// <summary>
/// Получить диапазон дат, для которых есть данные в репозитории
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpGet("datesRange/{idDiscriminator}")] [HttpGet("datesRange/{idDiscriminator}")]
[ProducesResponseType(typeof(DatesRangeDto), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(DatesRangeDto), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)] [ProducesResponseType((int)HttpStatusCode.NoContent)]
public async Task<ActionResult<DatesRangeDto>> GetDatesRangeAsync([FromRoute] Guid idDiscriminator, CancellationToken token) public async Task<ActionResult<DatesRangeDto>> GetDatesRangeAsync([FromRoute] Guid idDiscriminator, CancellationToken token)
{ {
var result = await repository.GetDatesRange(idDiscriminator, token); var result = await service.GetDatesRange(idDiscriminator, token);
if (result is null) if (result is null)
return NoContent(); return NoContent();

View File

@ -9,6 +9,8 @@ using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen; using Swashbuckle.AspNetCore.SwaggerGen;
using System.Reflection; using System.Reflection;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
using DD.Persistence.Database.Entity;
using DD.Persistence.API.Services;
namespace DD.Persistence.API; namespace DD.Persistence.API;
@ -53,6 +55,7 @@ public static class DependencyInjection
{ {
services.AddTransient<IWitsDataService, WitsDataService>(); services.AddTransient<IWitsDataService, WitsDataService>();
services.AddTransient<ITimestampedValuesService, TimestampedValuesService>(); services.AddTransient<ITimestampedValuesService, TimestampedValuesService>();
services.AddTransient<ChangeLogService>();
} }
#region Authentication #region Authentication

View File

@ -0,0 +1,198 @@
using DD.Persistence.Models.Common;
using DD.Persistence.Models;
using DD.Persistence.Models.Requests;
using DD.Persistence.Repositories;
using Microsoft.Extensions.Caching.Memory;
using DD.Persistence.Database.Entity;
namespace DD.Persistence.API.Services;
/// <summary>
/// Сервис по работе с журналом изменений
/// </summary>
public class ChangeLogService
{
private readonly IMemoryCache memoryCache;
private readonly IChangeLogCommitRepository commitRepository;
private readonly IChangeLogRepository repository;
private readonly TimeSpan? AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(60);
/// <summary>
/// ctor
/// </summary>
/// <param name="memoryCache"></param>
/// <param name="commitRepository"></param>
/// <param name="repository"></param>
public ChangeLogService(
IMemoryCache memoryCache,
IChangeLogCommitRepository commitRepository,
IChangeLogRepository repository)
{
this.memoryCache = memoryCache;
this.commitRepository = commitRepository;
this.repository = repository;
}
/// <summary>
/// Чтение ключа коммита из кеша или (если коммита в кеше нет) создание коммита
/// </summary>
/// <param name="commitDto"></param>
/// <param name="token"></param>
/// <returns></returns>
private async Task<Guid> GetOrCreateCommitAsync(CreateChangeLogCommitRequest commitDto, CancellationToken token)
Review

Давай в кеше хранить комит целиком не только ID

Давай в кеше хранить комит целиком не только ID
{
var key = (commitDto.IdAuthor, commitDto.Comment);
var commitId = await memoryCache.GetOrCreateAsync(key, async (cacheEntry) =>
{
cacheEntry.AbsoluteExpirationRelativeToNow = AbsoluteExpirationRelativeToNow;
var commitId = await commitRepository.Add(commitDto, token);
return commitId;
});
return commitId;
}
/// <summary>
/// Добавление записи в журнал изменений
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="commitRequestDto"></param>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
public async Task<int> AddRange(Guid idDiscriminator, CreateChangeLogCommitRequest commitRequestDto, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token)
{
var commitId = await GetOrCreateCommitAsync(commitRequestDto, token);
var commitDto = new ChangeLogCommitDto(commitId, commitRequestDto);
var result = await repository.AddRange(idDiscriminator, commitDto, dtos, token);
return result;
}
/// <summary>
/// Пометить запись журнала изменений как удаленную
/// </summary>
/// <param name="ids"></param>
/// <param name="commitRequestDto"></param>
/// <param name="token"></param>
/// <returns></returns>
public async Task<int> MarkAsDeleted(IEnumerable<Guid> ids, CreateChangeLogCommitRequest commitRequestDto, CancellationToken token)
{
var commitId = await GetOrCreateCommitAsync(commitRequestDto, token);
var commitDto = new ChangeLogCommitDto(commitId, commitRequestDto);
var result = await repository.MarkAsDeleted(commitId, ids, commitDto.Creation, token);
return result;
}
/// <summary>
/// Очистить старые и добавить новые записи в журнал изменений
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="commitRequestDto"></param>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
public async Task<int> ClearAndAddRange(Guid idDiscriminator, CreateChangeLogCommitRequest commitRequestDto, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token)
{
var commitId = await GetOrCreateCommitAsync(commitRequestDto, token);
var commitDto = new ChangeLogCommitDto(commitId, commitRequestDto);
var result = await repository.ClearAndAddRange(idDiscriminator, commitDto, dtos, token);
return result;
}
/// <summary>
/// Обновить записи в журнале изменений
/// </summary>
/// <param name="commitRequestDto"></param>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
public async Task<int> UpdateRange(CreateChangeLogCommitRequest commitRequestDto, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token)
{
var commitId = await GetOrCreateCommitAsync(commitRequestDto, token);
var commitDto = new ChangeLogCommitDto(commitId, commitRequestDto);
var result = await repository.UpdateRange(commitDto, dtos, token);
return result;
}
/// <summary>
/// Получение актуальных записей на определенный момент времени (с пагинацией)
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="momentUtc"></param>
/// <param name="paginationRequest"></param>
/// <param name="token"></param>
/// <returns></returns>
public async Task<PaginationContainer<ChangeLogValuesDto>> GetByDate(
Guid idDiscriminator,
DateTimeOffset momentUtc,
PaginationRequest paginationRequest,
CancellationToken token)
{
var result = await repository.GetByDate(idDiscriminator, momentUtc, paginationRequest, token);
return result;
}
/// <summary>
/// Получение измененных записей за период времени
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dateBegin"></param>
/// <param name="dateEnd"></param>
/// <param name="token"></param>
/// <returns></returns>
public async Task<IEnumerable<ChangeLogDto>> GetChangeLogForInterval(Guid idDiscriminator, DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token)
{
var result = await repository.GetChangeLogForInterval(idDiscriminator, dateBegin, dateEnd, token);
return result;
}
/// <summary>
/// Получение списка дат, в которые происходили изменения (день, месяц, год, без времени)
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="token"></param>
/// <returns></returns>
public async Task<IEnumerable<DateOnly>> GetDatesChange(Guid idDiscriminator, CancellationToken token)
{
var result = await repository.GetDatesChange(idDiscriminator, token);
return result;
}
/// <summary>
/// Получить данные, начиная с определенной даты
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dateBegin"></param>
/// <param name="token"></param>
/// <returns></returns>
public async Task<IEnumerable<ChangeLogValuesDto>> GetGtDate(Guid idDiscriminator, DateTimeOffset dateBegin, CancellationToken token)
{
var result = await repository.GetGtDate(idDiscriminator, dateBegin, token);
return result;
}
/// <summary>
/// Получить диапазон дат, для которых есть данные в репозитории
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="token"></param>
/// <returns></returns>
public async Task<DatesRangeDto?> GetDatesRange(Guid idDiscriminator, CancellationToken token)
{
var result = await repository.GetDatesRange(idDiscriminator, token);
return result;
}
}

View File

@ -19,10 +19,10 @@ public class ChangeLogClient : BaseClient, IChangeLogClient
} }
/// <inheritdoc/> /// <inheritdoc/>
public async Task<int> ClearAndAddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token) public async Task<int> ClearAndAddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, string comment, CancellationToken token)
{ {
var result = await ExecuteGetResponse( var result = await ExecuteGetResponse(
async () => await refitChangeLogClient.ClearAndAddRange(idDiscriminator, dtos, token), token); async () => await refitChangeLogClient.ClearAndAddRange(idDiscriminator, dtos, comment, token), token);
return result; return result;
} }
@ -47,55 +47,28 @@ public class ChangeLogClient : BaseClient, IChangeLogClient
} }
/// <inheritdoc/> /// <inheritdoc/>
public async Task<int> Add(Guid idDiscriminator, ChangeLogValuesDto dto, CancellationToken token) public async Task<int> AddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, string comment, CancellationToken token)
{ {
var result = await ExecutePostResponse( var result = await ExecutePostResponse(
async () => await refitChangeLogClient.Add(idDiscriminator, dto, token), token); async () => await refitChangeLogClient.AddRange(idDiscriminator, dtos, comment, token), token);
return result; return result;
} }
/// <inheritdoc/> /// <inheritdoc/>
public async Task<int> AddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token) public async Task<int> UpdateRange(IEnumerable<ChangeLogValuesDto> dtos, string comment, CancellationToken token)
{ {
var result = await ExecutePostResponse( var result = await ExecutePostResponse(
async () => await refitChangeLogClient.AddRange(idDiscriminator, dtos, token), token); async () => await refitChangeLogClient.UpdateRange(dtos, comment, token), token);
return result; return result;
} }
/// <inheritdoc/> /// <inheritdoc/>
public async Task<int> Update(ChangeLogValuesDto dto, CancellationToken token) public async Task<int> DeleteRange(IEnumerable<Guid> ids, string comment, CancellationToken token)
{ {
var result = await ExecutePostResponse( var result = await ExecutePostResponse(
async () => await refitChangeLogClient.Update(dto, token), token); async () => await refitChangeLogClient.DeleteRange(ids, comment, token), token);
return result;
}
/// <inheritdoc/>
public async Task<int> UpdateRange(IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token)
{
var result = await ExecutePostResponse(
async () => await refitChangeLogClient.UpdateRange(dtos, token), token);
return result;
}
/// <inheritdoc/>
public async Task<int> Delete(Guid id, CancellationToken token)
{
var result = await ExecutePostResponse(
async () => await refitChangeLogClient.Delete(id, token), token);
return result;
}
/// <inheritdoc/>
public async Task<int> DeleteRange(IEnumerable<Guid> ids, CancellationToken token)
{
var result = await ExecutePostResponse(
async () => await refitChangeLogClient.DeleteRange(ids, token), token);
return result; return result;
} }

View File

@ -9,48 +9,34 @@ namespace DD.Persistence.Client.Clients.Interfaces;
/// </summary> /// </summary>
public interface IChangeLogClient : IDisposable public interface IChangeLogClient : IDisposable
{ {
/// <summary> /// <summary>
/// Добавить одну запись /// Добавить несколько записей
/// </summary> /// </summary>
/// <param name="idDiscriminator"></param> /// <param name="idDiscriminator"></param>
/// <param name="dto"></param> /// <param name="dtos"></param>
/// <param name="token"></param> /// <param name="comment"></param>
/// <returns></returns> /// <param name="token"></param>
Task<int> Add(Guid idDiscriminator, ChangeLogValuesDto dto, CancellationToken token); /// <returns></returns>
Task<int> AddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, string comment, CancellationToken token);
/// <summary> /// <summary>
/// Добавить несколько записей /// Импорт с заменой: удаление старых строк и добавление новых
/// </summary> /// </summary>
/// <param name="idDiscriminator"></param> /// <param name="idDiscriminator"></param>
/// <param name="dtos"></param> /// <param name="dtos"></param>
/// <param name="token"></param> /// <param name="comment"></param>
/// <returns></returns> /// <param name="token"></param>
Task<int> AddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token); /// <returns></returns>
Task<int> ClearAndAddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, string comment, CancellationToken token);
/// <summary> /// <summary>
/// Импорт с заменой: удаление старых строк и добавление новых /// Удалить несколько записей
/// </summary> /// </summary>
/// <param name="idDiscriminator"></param> /// <param name="ids"></param>
/// <param name="dtos"></param> /// <param name="comment"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<int> ClearAndAddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token); Task<int> DeleteRange(IEnumerable<Guid> ids, string comment, CancellationToken token);
/// <summary>
/// Удалить одну запись
/// </summary>
/// <param name="id"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> Delete(Guid id, CancellationToken token);
/// <summary>
/// Удалить несколько записей
/// </summary>
/// <param name="ids"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> DeleteRange(IEnumerable<Guid> ids, CancellationToken token);
/// <summary> /// <summary>
/// Получение актуальных данных на определенную дату (с пагинацией) /// Получение актуальных данных на определенную дату (с пагинацией)
@ -80,19 +66,12 @@ public interface IChangeLogClient : IDisposable
/// <returns></returns> /// <returns></returns>
Task<DatesRangeDto?> GetDatesRange(Guid idDiscriminator, CancellationToken token); Task<DatesRangeDto?> GetDatesRange(Guid idDiscriminator, CancellationToken token);
/// <summary> /// <summary>
/// Обновить одну запись /// Обновить несколько записей
/// </summary> /// </summary>
/// <param name="dto"></param> /// <param name="dtos"></param>
/// <param name="token"></param> /// <param name="comment"></param>
/// <returns></returns> /// <param name="token"></param>
Task<int> Update(ChangeLogValuesDto dto, CancellationToken token); /// <returns></returns>
Task<int> UpdateRange(IEnumerable<ChangeLogValuesDto> dtos, string comment, CancellationToken token);
/// <summary>
/// Обновить несколько записей
/// </summary>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> UpdateRange(IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token);
} }

View File

@ -16,7 +16,7 @@ public interface IRefitChangeLogClient : IRefitClient, IDisposable
/// Импорт с заменой: удаление старых строк и добавление новых /// Импорт с заменой: удаление старых строк и добавление новых
/// </summary> /// </summary>
[Post($"{BaseRoute}/replace/{{idDiscriminator}}")] [Post($"{BaseRoute}/replace/{{idDiscriminator}}")]
Task<IApiResponse<int>> ClearAndAddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token); Task<IApiResponse<int>> ClearAndAddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, string comment, CancellationToken token);
/// <summary> /// <summary>
/// Получение актуальных данных на определенную дату (с пагинацией) /// Получение актуальных данных на определенную дату (с пагинацией)
@ -34,41 +34,23 @@ public interface IRefitChangeLogClient : IRefitClient, IDisposable
[Get($"{BaseRoute}/history/{{idDiscriminator}}")] [Get($"{BaseRoute}/history/{{idDiscriminator}}")]
Task<IApiResponse<IEnumerable<ChangeLogDto>>> GetChangeLogForInterval(Guid idDiscriminator, DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token); Task<IApiResponse<IEnumerable<ChangeLogDto>>> GetChangeLogForInterval(Guid idDiscriminator, DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token);
/// <summary>
/// Добавить одну запись
/// </summary>
[Post($"{BaseRoute}/{{idDiscriminator}}")]
Task<IApiResponse<int>> Add(Guid idDiscriminator, ChangeLogValuesDto dto, CancellationToken token);
/// <summary> /// <summary>
/// Добавить несколько записей /// Добавить несколько записей
/// </summary> /// </summary>
[Post($"{BaseRoute}/range/{{idDiscriminator}}")] [Post($"{BaseRoute}/{{idDiscriminator}}")]
Task<IApiResponse<int>> AddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token); Task<IApiResponse<int>> AddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, string comment, CancellationToken token);
/// <summary>
/// Обновить одну запись
/// </summary>
[Put($"{BaseRoute}")]
Task<IApiResponse<int>> Update(ChangeLogValuesDto dto, CancellationToken token);
/// <summary> /// <summary>
/// Обновить несколько записей /// Обновить несколько записей
/// </summary> /// </summary>
[Put($"{BaseRoute}/range")] [Put($"{BaseRoute}")]
Task<IApiResponse<int>> UpdateRange(IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token); Task<IApiResponse<int>> UpdateRange(IEnumerable<ChangeLogValuesDto> dtos, string comment, CancellationToken token);
/// <summary>
/// Удалить одну запись
/// </summary>
[Delete($"{BaseRoute}")]
Task<IApiResponse<int>> Delete(Guid id, CancellationToken token);
/// <summary> /// <summary>
/// Удалить несколько записей /// Удалить несколько записей
/// </summary> /// </summary>
[Delete($"{BaseRoute}/range")] [Delete($"{BaseRoute}")]
Task<IApiResponse<int>> DeleteRange([Body] IEnumerable<Guid> ids, CancellationToken token); Task<IApiResponse<int>> DeleteRange([Body] IEnumerable<Guid> ids, string comment, CancellationToken token);
/// <summary> /// <summary>
/// Получение списка дат, в которые происходили изменения (день, месяц, год, без времени) /// Получение списка дат, в которые происходили изменения (день, месяц, год, без времени)

View File

@ -13,7 +13,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace DD.Persistence.Database.Postgres.Migrations namespace DD.Persistence.Database.Postgres.Migrations
{ {
[DbContext(typeof(PersistencePostgresContext))] [DbContext(typeof(PersistencePostgresContext))]
[Migration("20250210055116_Init")] [Migration("20250221053248_Init")]
partial class Init partial class Init
{ {
/// <inheritdoc /> /// <inheritdoc />
@ -41,18 +41,18 @@ namespace DD.Persistence.Database.Postgres.Migrations
.HasColumnType("uuid") .HasColumnType("uuid")
.HasComment("Дискриминатор таблицы"); .HasComment("Дискриминатор таблицы");
b.Property<Guid>("IdAuthor") b.Property<Guid>("IdCreatedCommit")
.HasColumnType("uuid") .HasColumnType("uuid")
.HasComment("Автор изменения"); .HasComment("Id коммита на создание записи");
b.Property<Guid?>("IdEditor")
.HasColumnType("uuid")
.HasComment("Редактор");
b.Property<Guid?>("IdNext") b.Property<Guid?>("IdNext")
.HasColumnType("uuid") .HasColumnType("uuid")
.HasComment("Id заменяющей записи"); .HasComment("Id заменяющей записи");
b.Property<Guid?>("IdObsoletedCommit")
.HasColumnType("uuid")
.HasComment("Id коммита на устаревание записи");
b.Property<DateTimeOffset?>("Obsolete") b.Property<DateTimeOffset?>("Obsolete")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
.HasComment("Дата устаревания (например при удалении)"); .HasComment("Дата устаревания (например при удалении)");
@ -64,9 +64,38 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("IdCreatedCommit");
b.HasIndex("IdObsoletedCommit");
b.ToTable("change_log"); b.ToTable("change_log");
}); });
modelBuilder.Entity("DD.Persistence.Database.Entity.ChangeLogCommit", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasComment("Id коммита");
b.Property<string>("Comment")
.IsRequired()
.HasColumnType("text")
.HasComment("Комментарий к коммиту");
b.Property<DateTimeOffset>("Creation")
.HasColumnType("timestamp with time zone")
.HasComment("Дата создания коммита");
b.Property<Guid>("IdAuthor")
.HasColumnType("uuid")
.HasComment("Пользователь, создавший коммит");
b.HasKey("Id");
b.ToTable("change_log_commit");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.DataSourceSystem", b => modelBuilder.Entity("DD.Persistence.Database.Entity.DataSourceSystem", b =>
{ {
b.Property<Guid>("SystemId") b.Property<Guid>("SystemId")
@ -214,6 +243,23 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.ToTable("timestamped_values"); b.ToTable("timestamped_values");
}); });
modelBuilder.Entity("DD.Persistence.Database.Entity.ChangeLog", b =>
{
b.HasOne("DD.Persistence.Database.Entity.ChangeLogCommit", "CreatedCommit")
.WithMany("ChangeLogCreatedItems")
.HasForeignKey("IdCreatedCommit")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DD.Persistence.Database.Entity.ChangeLogCommit", "ObsoletedCommit")
.WithMany("ChangeLogObsoletedItems")
.HasForeignKey("IdObsoletedCommit");
b.Navigation("CreatedCommit");
b.Navigation("ObsoletedCommit");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.TechMessage", b => modelBuilder.Entity("DD.Persistence.Database.Entity.TechMessage", b =>
{ {
b.HasOne("DD.Persistence.Database.Entity.DataSourceSystem", "System") b.HasOne("DD.Persistence.Database.Entity.DataSourceSystem", "System")
@ -224,6 +270,13 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.Navigation("System"); b.Navigation("System");
}); });
modelBuilder.Entity("DD.Persistence.Database.Entity.ChangeLogCommit", b =>
{
b.Navigation("ChangeLogCreatedItems");
b.Navigation("ChangeLogObsoletedItems");
});
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }
} }

View File

@ -13,21 +13,17 @@ namespace DD.Persistence.Database.Postgres.Migrations
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "change_log", name: "change_log_commit",
columns: table => new columns: table => new
{ {
Id = table.Column<Guid>(type: "uuid", nullable: false, comment: "Ключ записи"), Id = table.Column<Guid>(type: "uuid", nullable: false, comment: "Id коммита"),
DiscriminatorId = table.Column<Guid>(type: "uuid", nullable: false, comment: "Дискриминатор таблицы"), IdAuthor = table.Column<Guid>(type: "uuid", nullable: false, comment: "Пользователь, создавший коммит"),
IdAuthor = table.Column<Guid>(type: "uuid", nullable: false, comment: "Автор изменения"), Creation = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, comment: "Дата создания коммита"),
IdEditor = table.Column<Guid>(type: "uuid", nullable: true, comment: "Редактор"), Comment = table.Column<string>(type: "text", nullable: false, comment: "Комментарий к коммиту")
Creation = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, comment: "Дата создания записи"),
Obsolete = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true, comment: "Дата устаревания (например при удалении)"),
IdNext = table.Column<Guid>(type: "uuid", nullable: true, comment: "Id заменяющей записи"),
Value = table.Column<string>(type: "jsonb", nullable: false, comment: "Значение")
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_change_log", x => x.Id); table.PrimaryKey("PK_change_log_commit", x => x.Id);
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
@ -98,6 +94,35 @@ namespace DD.Persistence.Database.Postgres.Migrations
table.PrimaryKey("PK_timestamped_values", x => new { x.DiscriminatorId, x.Timestamp }); table.PrimaryKey("PK_timestamped_values", x => new { x.DiscriminatorId, x.Timestamp });
}); });
migrationBuilder.CreateTable(
name: "change_log",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false, comment: "Ключ записи"),
DiscriminatorId = table.Column<Guid>(type: "uuid", nullable: false, comment: "Дискриминатор таблицы"),
Creation = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, comment: "Дата создания записи"),
Obsolete = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true, comment: "Дата устаревания (например при удалении)"),
IdNext = table.Column<Guid>(type: "uuid", nullable: true, comment: "Id заменяющей записи"),
Value = table.Column<string>(type: "jsonb", nullable: false, comment: "Значение"),
IdCreatedCommit = table.Column<Guid>(type: "uuid", nullable: false, comment: "Id коммита на создание записи"),
IdObsoletedCommit = table.Column<Guid>(type: "uuid", nullable: true, comment: "Id коммита на устаревание записи")
},
constraints: table =>
{
table.PrimaryKey("PK_change_log", x => x.Id);
table.ForeignKey(
name: "FK_change_log_change_log_commit_IdCreatedCommit",
column: x => x.IdCreatedCommit,
principalTable: "change_log_commit",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_change_log_change_log_commit_IdObsoletedCommit",
column: x => x.IdObsoletedCommit,
principalTable: "change_log_commit",
principalColumn: "Id");
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "tech_message", name: "tech_message",
columns: table => new columns: table => new
@ -120,6 +145,16 @@ namespace DD.Persistence.Database.Postgres.Migrations
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateIndex(
name: "IX_change_log_IdCreatedCommit",
table: "change_log",
column: "IdCreatedCommit");
migrationBuilder.CreateIndex(
name: "IX_change_log_IdObsoletedCommit",
table: "change_log",
column: "IdObsoletedCommit");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_tech_message_SystemId", name: "IX_tech_message_SystemId",
table: "tech_message", table: "tech_message",
@ -147,6 +182,9 @@ namespace DD.Persistence.Database.Postgres.Migrations
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "timestamped_values"); name: "timestamped_values");
migrationBuilder.DropTable(
name: "change_log_commit");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "data_source_system"); name: "data_source_system");
} }

View File

@ -38,18 +38,18 @@ namespace DD.Persistence.Database.Postgres.Migrations
.HasColumnType("uuid") .HasColumnType("uuid")
.HasComment("Дискриминатор таблицы"); .HasComment("Дискриминатор таблицы");
b.Property<Guid>("IdAuthor") b.Property<Guid>("IdCreatedCommit")
.HasColumnType("uuid") .HasColumnType("uuid")
.HasComment("Автор изменения"); .HasComment("Id коммита на создание записи");
b.Property<Guid?>("IdEditor")
.HasColumnType("uuid")
.HasComment("Редактор");
b.Property<Guid?>("IdNext") b.Property<Guid?>("IdNext")
.HasColumnType("uuid") .HasColumnType("uuid")
.HasComment("Id заменяющей записи"); .HasComment("Id заменяющей записи");
b.Property<Guid?>("IdObsoletedCommit")
.HasColumnType("uuid")
.HasComment("Id коммита на устаревание записи");
b.Property<DateTimeOffset?>("Obsolete") b.Property<DateTimeOffset?>("Obsolete")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
.HasComment("Дата устаревания (например при удалении)"); .HasComment("Дата устаревания (например при удалении)");
@ -61,9 +61,38 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("IdCreatedCommit");
b.HasIndex("IdObsoletedCommit");
b.ToTable("change_log"); b.ToTable("change_log");
}); });
modelBuilder.Entity("DD.Persistence.Database.Entity.ChangeLogCommit", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasComment("Id коммита");
b.Property<string>("Comment")
.IsRequired()
.HasColumnType("text")
.HasComment("Комментарий к коммиту");
b.Property<DateTimeOffset>("Creation")
.HasColumnType("timestamp with time zone")
.HasComment("Дата создания коммита");
b.Property<Guid>("IdAuthor")
.HasColumnType("uuid")
.HasComment("Пользователь, создавший коммит");
b.HasKey("Id");
b.ToTable("change_log_commit");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.DataSourceSystem", b => modelBuilder.Entity("DD.Persistence.Database.Entity.DataSourceSystem", b =>
{ {
b.Property<Guid>("SystemId") b.Property<Guid>("SystemId")
@ -211,6 +240,23 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.ToTable("timestamped_values"); b.ToTable("timestamped_values");
}); });
modelBuilder.Entity("DD.Persistence.Database.Entity.ChangeLog", b =>
{
b.HasOne("DD.Persistence.Database.Entity.ChangeLogCommit", "CreatedCommit")
.WithMany("ChangeLogCreatedItems")
.HasForeignKey("IdCreatedCommit")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DD.Persistence.Database.Entity.ChangeLogCommit", "ObsoletedCommit")
.WithMany("ChangeLogObsoletedItems")
.HasForeignKey("IdObsoletedCommit");
b.Navigation("CreatedCommit");
b.Navigation("ObsoletedCommit");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.TechMessage", b => modelBuilder.Entity("DD.Persistence.Database.Entity.TechMessage", b =>
{ {
b.HasOne("DD.Persistence.Database.Entity.DataSourceSystem", "System") b.HasOne("DD.Persistence.Database.Entity.DataSourceSystem", "System")
@ -221,6 +267,13 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.Navigation("System"); b.Navigation("System");
}); });
modelBuilder.Entity("DD.Persistence.Database.Entity.ChangeLogCommit", b =>
{
b.Navigation("ChangeLogCreatedItems");
b.Navigation("ChangeLogObsoletedItems");
});
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }
} }

View File

@ -45,6 +45,7 @@ public static class DependencyInjection
//services.AddTransient(typeof(PersistenceRepository<TimestampedValues>)); //services.AddTransient(typeof(PersistenceRepository<TimestampedValues>));
services.AddTransient<ISetpointRepository, SetpointRepository>(); services.AddTransient<ISetpointRepository, SetpointRepository>();
services.AddTransient<IChangeLogCommitRepository, ChangeLogCommitRepository>();
services.AddTransient<IChangeLogRepository, ChangeLogRepository>(); services.AddTransient<IChangeLogRepository, ChangeLogRepository>();
services.AddTransient<ITimestampedValuesRepository, TimestampedValuesRepository>(); services.AddTransient<ITimestampedValuesRepository, TimestampedValuesRepository>();
services.AddTransient<ITechMessagesRepository, TechMessagesRepository>(); services.AddTransient<ITechMessagesRepository, TechMessagesRepository>();

View File

@ -19,12 +19,6 @@ public class ChangeLog : IDiscriminatorItem, IChangeLog
[Comment("Дискриминатор таблицы")] [Comment("Дискриминатор таблицы")]
public Guid DiscriminatorId { get; set; } public Guid DiscriminatorId { get; set; }
[Comment("Автор изменения")]
public Guid IdAuthor { get; set; }
[Comment("Редактор")]
public Guid? IdEditor { get; set; }
[Comment("Дата создания записи")] [Comment("Дата создания записи")]
public DateTimeOffset Creation { get; set; } public DateTimeOffset Creation { get; set; }
Review

Комментарий про денормализацию БД

Комментарий про денормализацию БД
@ -36,4 +30,16 @@ public class ChangeLog : IDiscriminatorItem, IChangeLog
[Column(TypeName = "jsonb"), Comment("Значение")] [Column(TypeName = "jsonb"), Comment("Значение")]
public required IDictionary<string, object> Value { get; set; } public required IDictionary<string, object> Value { get; set; }
[Required, Comment("Id коммита на создание записи")]
public Guid IdCreatedCommit { get; set; }
[Comment("Id коммита на устаревание записи")]
public Guid? IdObsoletedCommit { get; set; }
[Required, ForeignKey(nameof(IdCreatedCommit)), Comment("Коммит пользователя на создание записи")]
public virtual ChangeLogCommit CreatedCommit { get; set; } = null!;
[ForeignKey(nameof(IdObsoletedCommit)), Comment("Коммит пользователя на устаревание записи")]
public virtual ChangeLogCommit? ObsoletedCommit { get; set; }
} }

View File

@ -0,0 +1,35 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DD.Persistence.Database.Entity;
/// <summary>
/// Таблица c коммитами пользователей
/// </summary>
[Table("change_log_commit")]
public class ChangeLogCommit
{
[Key, Comment("Id коммита")]
public Guid Id { get; set; }
[Comment("Пользователь, создавший коммит")]
public Guid IdAuthor { get; set; }
[Comment("Дата создания коммита")]
public DateTimeOffset Creation { get; set; }
[Comment("Комментарий к коммиту")]
public required string Comment { get; set; }
[Required, InverseProperty(nameof(ChangeLog.CreatedCommit)), Comment("Записи, добавленные в журнал изменений")]
public virtual ICollection<ChangeLog> ChangeLogCreatedItems { get; set; } = null!;
[InverseProperty(nameof(ChangeLog.ObsoletedCommit)), Comment("Устаревшие записи в журнале изменений")]
public virtual ICollection<ChangeLog>? ChangeLogObsoletedItems { get; set; } = null!;
}

View File

@ -10,16 +10,6 @@ public interface IChangeLog
/// </summary> /// </summary>
public Guid Id { get; set; } public Guid Id { get; set; }
/// <summary>
/// Автор изменения
/// </summary>
public Guid IdAuthor { get; set; }
/// <summary>
/// Редактор
/// </summary>
public Guid? IdEditor { get; set; }
/// <summary> /// <summary>
/// Дата создания записи /// Дата создания записи
/// </summary> /// </summary>

View File

@ -16,6 +16,8 @@ public class PersistenceDbContext : DbContext
public DbSet<ChangeLog> ChangeLog => Set<ChangeLog>(); public DbSet<ChangeLog> ChangeLog => Set<ChangeLog>();
public DbSet<ChangeLogCommit> ChangeLogCommit => Set<ChangeLogCommit>();
public DbSet<TechMessage> TechMessage => Set<TechMessage>(); public DbSet<TechMessage> TechMessage => Set<TechMessage>();
public DbSet<ParameterData> ParameterData => Set<ParameterData>(); public DbSet<ParameterData> ParameterData => Set<ParameterData>();

View File

@ -0,0 +1,39 @@
using DD.Persistence.Database.Entity;
using DD.Persistence.Models.Requests;
using DD.Persistence.Repositories;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UuidExtensions;
namespace DD.Persistence.Database.Repositories;
public class ChangeLogCommitRepository : IChangeLogCommitRepository
{
private DbContext db;
public ChangeLogCommitRepository(DbContext db)
{
this.db = db;
}
public async Task<Guid> Add(CreateChangeLogCommitRequest commitRequestDto, CancellationToken token)
Review

commitRequestDto -> commitRequest или request

commitRequestDto -> commitRequest или request
{
var commit = new ChangeLogCommit()
{
Id = Uuid7.Guid(),
IdAuthor = commitRequestDto.IdAuthor,
Comment = commitRequestDto.Comment,
Creation = commitRequestDto.Creation,
};
db.Add(commit);
await db.SaveChangesAsync();
return commit.Id;
}
}

View File

@ -6,6 +6,7 @@ using DD.Persistence.Models.Requests;
using DD.Persistence.Repositories; using DD.Persistence.Repositories;
using Mapster; using Mapster;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System;
using UuidExtensions; using UuidExtensions;
namespace DD.Persistence.Database.Repositories; namespace DD.Persistence.Database.Repositories;
@ -18,12 +19,20 @@ public class ChangeLogRepository : IChangeLogRepository
this.db = db; this.db = db;
} }
public async Task<int> AddRange(Guid idAuthor, Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token) public async Task<int> AddRange(Guid idDiscriminator, ChangeLogCommitDto commitDto, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token)
{ {
var entities = new List<ChangeLog>(); var entities = new List<ChangeLog>();
foreach (var dto in dtos) foreach (var values in dtos)
{ {
var entity = CreateEntityFromDto(idAuthor, idDiscriminator, dto); var entity = new ChangeLog()
{
Id = Uuid7.Guid(),
Creation = commitDto.Creation,
DiscriminatorId = idDiscriminator,
Value = values.Value,
IdCreatedCommit = commitDto.Id,
};
entities.Add(entity); entities.Add(entity);
} }
db.Set<ChangeLog>().AddRange(entities); db.Set<ChangeLog>().AddRange(entities);
@ -33,7 +42,7 @@ public class ChangeLogRepository : IChangeLogRepository
return result; return result;
} }
public async Task<int> MarkAsDeleted(Guid idEditor, IEnumerable<Guid> ids, CancellationToken token) public async Task<int> MarkAsDeleted(Guid idCommit, IEnumerable<Guid> ids, DateTimeOffset updateTime, CancellationToken token)
Review

Как будто и idCommit и updateTime есть в ChangeLogCommitDto

Как будто и idCommit и updateTime есть в ChangeLogCommitDto
{ {
var query = db.Set<ChangeLog>() var query = db.Set<ChangeLog>()
.Where(s => ids.Contains(s.Id)) .Where(s => ids.Contains(s.Id))
@ -46,12 +55,16 @@ public class ChangeLogRepository : IChangeLogRepository
var entities = await query.ToArrayAsync(token); var entities = await query.ToArrayAsync(token);
var result = await MarkAsObsolete(idEditor, entities, token); foreach (var entity in entities)
{
entity.Obsolete = updateTime;
entity.IdObsoletedCommit = idCommit;
}
return result; return await db.SaveChangesAsync(token);
} }
public async Task<int> MarkAsDeleted(Guid idEditor, Guid idDiscriminator, CancellationToken token) public async Task<int> MarkAsDeleted(Guid idDiscriminator, Guid idCommit, DateTimeOffset updateTime, CancellationToken token)
{ {
var query = db.Set<ChangeLog>() var query = db.Set<ChangeLog>()
.Where(s => s.DiscriminatorId == idDiscriminator) .Where(s => s.DiscriminatorId == idDiscriminator)
@ -59,40 +72,34 @@ public class ChangeLogRepository : IChangeLogRepository
var entities = await query.ToArrayAsync(token); var entities = await query.ToArrayAsync(token);
var result = await MarkAsObsolete(idEditor, entities, token);
return result;
}
private async Task<int> MarkAsObsolete(Guid idEditor, IEnumerable<ChangeLog> entities, CancellationToken token)
{
var updateTime = DateTimeOffset.UtcNow;
foreach (var entity in entities) foreach (var entity in entities)
{ {
entity.Obsolete = updateTime; entity.Obsolete = updateTime;
entity.IdEditor = idEditor; entity.DiscriminatorId = idCommit;
} }
return await db.SaveChangesAsync(token); return await db.SaveChangesAsync(token);
} }
public async Task<int> ClearAndAddRange(Guid idAuthor, Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token)
public async Task<int> ClearAndAddRange(Guid idDiscriminator, ChangeLogCommitDto commitDto, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token)
{ {
var result = 0; var result = 0;
var changeLogIds = dtos.Select(c => c.Id);
var comment = commitDto.Comment;
Review

эта логкальная переменная дальше нигде не используется. Зачем она здесь?

эта логкальная переменная дальше нигде не используется. Зачем она здесь?
using var transaction = await db.Database.BeginTransactionAsync(token); using var transaction = await db.Database.BeginTransactionAsync(token);
result += await MarkAsDeleted(idAuthor, idDiscriminator, token); result += await MarkAsDeleted(commitDto.Id, changeLogIds, commitDto.Creation, token);
Review

Это не то.
Покрой этот метод интеграционным тестом плиз.

// arrange
add some data winth 2 discriminators
// act replace data for 1 discriminaqtor
// assert
check thar othe data with othe discr

Это не то. Покрой этот метод интеграционным тестом плиз. // arrange add some data winth 2 discriminators // act replace data for 1 discriminaqtor // assert check thar othe data with othe discr
result += await AddRange(idAuthor, idDiscriminator, dtos, token); result += await AddRange(idDiscriminator, commitDto, dtos, token);
await transaction.CommitAsync(token); await transaction.CommitAsync(token);
return result; return result;
} }
public async Task<int> UpdateRange(Guid idEditor, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token) public async Task<int> UpdateRange(ChangeLogCommitDto commitDto, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token)
{ {
var dbSet = db.Set<ChangeLog>(); var dbSet = db.Set<ChangeLog>();
@ -101,7 +108,6 @@ public class ChangeLogRepository : IChangeLogRepository
.Where(s => updatedIds.Contains(s.Id)) .Where(s => updatedIds.Contains(s.Id))
.ToDictionary(s => s.Id); .ToDictionary(s => s.Id);
var result = 0;
using var transaction = await db.Database.BeginTransactionAsync(token); using var transaction = await db.Database.BeginTransactionAsync(token);
foreach (var dto in dtos) foreach (var dto in dtos)
@ -112,20 +118,25 @@ public class ChangeLogRepository : IChangeLogRepository
throw new ArgumentException($"Entity with id = {dto.Id} doesn't exist in Db", nameof(dto)); throw new ArgumentException($"Entity with id = {dto.Id} doesn't exist in Db", nameof(dto));
} }
var newEntity = CreateEntityFromDto(idEditor, updatedEntity.DiscriminatorId, dto); var newEntity = new ChangeLog()
{
Id = Uuid7.Guid(),
Creation = commitDto.Creation,
DiscriminatorId = updatedEntity.DiscriminatorId,
Value = dto.Value,
IdCreatedCommit = commitDto.Id,
};
dbSet.Add(newEntity); dbSet.Add(newEntity);
updatedEntity.IdNext = newEntity.Id; updatedEntity.IdNext = newEntity.Id;
updatedEntity.Obsolete = DateTimeOffset.UtcNow; updatedEntity.Obsolete = commitDto.Creation;
updatedEntity.IdEditor = idEditor; updatedEntity.IdObsoletedCommit = commitDto.Id;
} }
result = await db.SaveChangesAsync(token); var result = await db.SaveChangesAsync(token);
await transaction.CommitAsync(token); await transaction.CommitAsync(token);
return result; return result;
} }
public async Task<PaginationContainer<ChangeLogValuesDto>> GetByDate( public async Task<PaginationContainer<ChangeLogValuesDto>> GetByDate(
@ -195,22 +206,6 @@ public class ChangeLogRepository : IChangeLogRepository
return datesOnly; return datesOnly;
} }
private static ChangeLog CreateEntityFromDto(Guid idAuthor, Guid idDiscriminator, ChangeLogValuesDto dto)
{
var entity = new ChangeLog()
{
Id = Uuid7.Guid(),
Creation = DateTimeOffset.UtcNow,
IdAuthor = idAuthor,
DiscriminatorId = idDiscriminator,
IdEditor = idAuthor,
Value = dto.Value
};
return entity;
}
public async Task<IEnumerable<ChangeLogValuesDto>> GetGtDate(Guid idDiscriminator, DateTimeOffset dateBegin, CancellationToken token) public async Task<IEnumerable<ChangeLogValuesDto>> GetGtDate(Guid idDiscriminator, DateTimeOffset dateBegin, CancellationToken token)
{ {
var date = dateBegin.ToUniversalTime(); var date = dateBegin.ToUniversalTime();

View File

@ -15,6 +15,7 @@ namespace DD.Persistence.IntegrationTests.Controllers;
public class ChangeLogControllerTest : BaseIntegrationTest public class ChangeLogControllerTest : BaseIntegrationTest
{ {
private readonly IChangeLogClient client; private readonly IChangeLogClient client;
private readonly PaginationRequest paginationRequest;
private static readonly Random generatorRandomDigits = new(); private static readonly Random generatorRandomDigits = new();
public ChangeLogControllerTest(WebAppFactoryFixture factory) : base(factory) public ChangeLogControllerTest(WebAppFactoryFixture factory) : base(factory)
@ -25,22 +26,13 @@ public class ChangeLogControllerTest : BaseIntegrationTest
client = scope.ServiceProvider client = scope.ServiceProvider
.GetRequiredService<IChangeLogClient>(); .GetRequiredService<IChangeLogClient>();
}
[Fact] paginationRequest = new PaginationRequest()
public async Task ClearAndInsertRange_InEmptyDb() {
{ Skip = 0,
// arrange Take = 10,
dbContext.CleanupDbSet<ChangeLog>(); SortSettings = String.Empty,
};
var idDiscriminator = Guid.NewGuid();
var dtos = Generate(2);
// act
var result = await client.ClearAndAddRange(idDiscriminator, dtos, new CancellationToken());
// assert
Assert.Equal(2, result);
} }
[Fact] [Fact]
@ -48,33 +40,17 @@ public class ChangeLogControllerTest : BaseIntegrationTest
{ {
// arrange // arrange
var insertedCount = 10; var insertedCount = 10;
var createdResult = CreateChangeLogItems(insertedCount, (-15, 15)); var newEntitiesData = await CreateAndReturnNewDtos(insertedCount, (-15, -1));
var idDiscriminator = createdResult.Item1; var idDiscriminator = newEntitiesData.Item1;
var dtos = createdResult.Item2.Select(e => e.Adapt<ChangeLogValuesDto>()); var dtos = newEntitiesData.Item2;
// act //act
var result = await client.ClearAndAddRange(idDiscriminator, dtos, new CancellationToken()); var result = await client.ClearAndAddRange(idDiscriminator, dtos, "Добавление новых элементов и очистка старых", CancellationToken.None);
// assert // assert
Assert.Equal(insertedCount * 2, result); Assert.Equal(insertedCount * 2, result);
} }
[Fact]
public async Task Add_returns_success()
{
// arrange
var count = 1;
var idDiscriminator = Guid.NewGuid();
var dtos = Generate(count);
var dto = dtos.FirstOrDefault()!;
// act
var result = await client.Add(idDiscriminator, dto, new CancellationToken());
// assert
Assert.Equal(count, result);
}
[Fact] [Fact]
public async Task AddRange_returns_success() public async Task AddRange_returns_success()
{ {
@ -82,9 +58,10 @@ public class ChangeLogControllerTest : BaseIntegrationTest
var count = 3; var count = 3;
var idDiscriminator = Guid.NewGuid(); var idDiscriminator = Guid.NewGuid();
var dtos = Generate(count); var dtos = Generate(count);
var comment = "Создаю 3 элемента";
// act // act
var result = await client.AddRange(idDiscriminator, dtos, new CancellationToken()); var result = await client.AddRange(idDiscriminator, dtos, comment, CancellationToken.None);
// assert // assert
Assert.Equal(count, result); Assert.Equal(count, result);
@ -93,13 +70,14 @@ public class ChangeLogControllerTest : BaseIntegrationTest
[Fact] [Fact]
public async Task Update_returns_success() public async Task Update_returns_success()
{ {
// arrange //arrange
dbContext.CleanupDbSet<ChangeLog>(); dbContext.CleanupDbSet<ChangeLog>();
var idDiscriminator = Guid.NewGuid(); var idDiscriminator = Guid.NewGuid();
var dtos = Generate(1); var dtos = Generate(1);
var dto = dtos.FirstOrDefault()!; var dto = dtos.FirstOrDefault()!;
var result = await client.Add(idDiscriminator, dto, new CancellationToken()); var comment = "Создаю 1 элемент";
var result = await client.AddRange(idDiscriminator, [dto], comment, CancellationToken.None);
var entity = dbContext.ChangeLog var entity = dbContext.ChangeLog
.Where(x => x.DiscriminatorId == idDiscriminator) .Where(x => x.DiscriminatorId == idDiscriminator)
@ -107,7 +85,8 @@ public class ChangeLogControllerTest : BaseIntegrationTest
dto = entity.Adapt<ChangeLogValuesDto>(); dto = entity.Adapt<ChangeLogValuesDto>();
// act // act
result = await client.Update(dto, new CancellationToken()); comment = "Обновляю 1 элемент";
result = await client.UpdateRange([dto], comment, CancellationToken.None);
// assert // assert
Assert.Equal(2, result); Assert.Equal(2, result);
@ -139,71 +118,49 @@ public class ChangeLogControllerTest : BaseIntegrationTest
[Fact] [Fact]
public async Task UpdateRange_returns_success() public async Task UpdateRange_returns_success()
{ {
// arrange
var count = 2; var count = 2;
var idDiscriminator = Guid.NewGuid();
var dtos = Generate(count); var dtos = Generate(count);
var entities = dtos.Select(d => d.Adapt<ChangeLog>()).ToArray(); var comment = "Создаю 3 элемента";
dbContext.ChangeLog.AddRange(entities);
dbContext.SaveChanges();
dtos = entities.Select(c => new ChangeLogValuesDto()
{
Id = c.Id,
Value = c.Value
}).ToArray();
// act // act
var result = await client.UpdateRange(dtos, new CancellationToken()); var result = await client.AddRange(idDiscriminator, dtos, comment, CancellationToken.None);
var paginatedResult = await client.GetByDate(idDiscriminator, DateTimeOffset.UtcNow.AddDays(1), paginationRequest, CancellationToken.None);
// act
comment = "Обновляю 3 элемента";
result = await client.UpdateRange(paginatedResult.Items, comment, CancellationToken.None);
// assert // assert
Assert.Equal(count * 2, result); Assert.Equal(count * 2, result);
} }
[Fact]
public async Task Delete_returns_success()
{
// arrange
var dtos = Generate(1);
var dto = dtos.FirstOrDefault()!;
var entity = dto.Adapt<ChangeLog>();
dbContext.ChangeLog.Add(entity);
dbContext.SaveChanges();
// act
var result = await client.Delete(entity.Id, new CancellationToken());
// assert
Assert.Equal(1, result);
}
[Fact] [Fact]
public async Task DeleteRange_returns_success() public async Task DeleteRange_returns_success()
{ {
// arrange // arrange
var count = 10; var insertedCount = 10;
var dtos = Generate(count); var newEntitiesData = await CreateAndReturnNewDtos(insertedCount, (-15, -1));
var entities = dtos.Select(d => d.Adapt<ChangeLog>()).ToArray(); var idDiscriminator = newEntitiesData.Item1;
dbContext.ChangeLog.AddRange(entities); var dtos = newEntitiesData.Item2;
dbContext.SaveChanges();
// act // act
var ids = entities.Select(e => e.Id); var ids = dtos.Select(e => e.Id);
var result = await client.DeleteRange(ids, new CancellationToken()); var result = await client.DeleteRange(ids, "Удаление нескольких записей", CancellationToken.None);
// assert // assert
Assert.Equal(count, result); Assert.Equal(insertedCount, result);
} }
[Fact] [Fact]
public async Task GetDatesRange_returns_success() public async Task GetDatesRange_returns_success()
{ {
// arrange //arrange
var changeLogItems = CreateChangeLogItems(3, (-15, 15)); var changeLogItems = await CreateAndReturnNewEntities(3, (-15, -1));
var idDiscriminator = changeLogItems.Item1; var idDiscriminator = changeLogItems.Item1;
var entities = changeLogItems.Item2.OrderBy(e => e.Creation); var entities = changeLogItems.Item2.OrderBy(c => c.Creation);
// act // act
var result = await client.GetDatesRange(idDiscriminator, new CancellationToken()); var result = await client.GetDatesRange(idDiscriminator, CancellationToken.None);
// assert // assert
Assert.NotNull(result); Assert.NotNull(result);
@ -228,7 +185,7 @@ public class ChangeLogControllerTest : BaseIntegrationTest
//создаем записи //создаем записи
var count = 5; var count = 5;
var changeLogItems = CreateChangeLogItems(count, (-15, 15)); var changeLogItems = await CreateAndReturnNewDtos(count, (-15, -1));
var idDiscriminator = changeLogItems.Item1; var idDiscriminator = changeLogItems.Item1;
var entities = changeLogItems.Item2; var entities = changeLogItems.Item2;
@ -237,14 +194,7 @@ public class ChangeLogControllerTest : BaseIntegrationTest
var ids = entities.Select(e => e.Id); var ids = entities.Select(e => e.Id);
var idsToDelete = ids.Skip(2); var idsToDelete = ids.Skip(2);
var deletedCount = await client.DeleteRange(idsToDelete, new CancellationToken()); var deletedCount = await client.DeleteRange(idsToDelete, "Удаление нескольких записей", CancellationToken.None);
var paginationRequest = new PaginationRequest()
{
Skip = 0,
Take = 10,
SortSettings = String.Empty,
};
var moment = DateTimeOffset.UtcNow.AddDays(16); var moment = DateTimeOffset.UtcNow.AddDays(16);
var result = await client.GetByDate(idDiscriminator, moment, paginationRequest, new CancellationToken()); var result = await client.GetByDate(idDiscriminator, moment, paginationRequest, new CancellationToken());
@ -260,8 +210,8 @@ public class ChangeLogControllerTest : BaseIntegrationTest
} }
[Theory] [Theory]
[InlineData(5, -15, 15, -20, 20, 10)] [InlineData(5, -15, -5, -20, 20, 10)]
[InlineData(5, -15, -10, -16, -9, 5)] [InlineData(5, -15, -10, -16, 9, 10)]
public async Task GetChangeLogForInterval_returns_success( public async Task GetChangeLogForInterval_returns_success(
int insertedCount, int insertedCount,
int daysBeforeNowChangeLog, int daysBeforeNowChangeLog,
@ -276,17 +226,16 @@ public class ChangeLogControllerTest : BaseIntegrationTest
//создаем записи //создаем записи
var count = insertedCount; var count = insertedCount;
var daysRange = (daysBeforeNowChangeLog, daysAfterNowChangeLog); var daysRange = (daysBeforeNowChangeLog, daysAfterNowChangeLog);
var changeLogItems = CreateChangeLogItems(count, daysRange); var changeLogItems = await CreateAndReturnNewDtos(count, daysRange);
var idDiscriminator = changeLogItems.Item1; var idDiscriminator = changeLogItems.Item1;
var entities = changeLogItems.Item2; var dtos = changeLogItems.Item2;
var dtos = entities.Select(e => e.Adapt<ChangeLogValuesDto>()).ToArray(); await client.UpdateRange(dtos, "Обновляем несколько записей", CancellationToken.None);
await client.UpdateRange(dtos, new CancellationToken());
//act //act
var dateBegin = DateTimeOffset.UtcNow.AddDays(daysBeforeNowFilter); var dateBegin = DateTimeOffset.UtcNow.AddDays(daysBeforeNowFilter);
var dateEnd = DateTimeOffset.UtcNow.AddDays(daysAfterNowFilter); var dateEnd = DateTimeOffset.UtcNow.AddDays(daysAfterNowFilter);
var result = await client.GetChangeLogForInterval(idDiscriminator, dateBegin, dateEnd, new CancellationToken()); var result = await client.GetChangeLogForInterval(idDiscriminator, dateBegin, dateEnd, CancellationToken.None);
//assert //assert
Assert.NotNull(result); Assert.NotNull(result);
@ -308,7 +257,7 @@ public class ChangeLogControllerTest : BaseIntegrationTest
} }
private (Guid, ChangeLog[]) CreateChangeLogItems(int count, (int, int) daysRange) private async Task<(Guid, IEnumerable<ChangeLogValuesDto>)> CreateAndReturnNewDtos(int count, (int, int) daysRange)
{ {
var minDayCount = daysRange.Item1; var minDayCount = daysRange.Item1;
var maxDayCount = daysRange.Item2; var maxDayCount = daysRange.Item2;
@ -323,8 +272,43 @@ public class ChangeLogControllerTest : BaseIntegrationTest
return entity; return entity;
}).ToArray(); }).ToArray();
dtos = entities.Select(e => e.Adapt<ChangeLogValuesDto>());
// act
var result = await client.AddRange(idDiscriminator, dtos, "Добавление элементов", CancellationToken.None);
var paginatedResult = await client.GetByDate(idDiscriminator, DateTimeOffset.UtcNow.AddDays(1), paginationRequest, CancellationToken.None);
return (idDiscriminator, paginatedResult.Items);
}
private async Task<(Guid, IEnumerable<ChangeLog>)> CreateAndReturnNewEntities(int count, (int, int) daysRange)
{
var commit = new ChangeLogCommit()
{
Comment = "Комментарий к коммиту",
Creation = DateTimeOffset.UtcNow,
Id = Guid.NewGuid(),
};
dbContext.ChangeLogCommit.Add(commit);
await dbContext.SaveChangesAsync();
var minDayCount = daysRange.Item1;
var maxDayCount = daysRange.Item2;
Guid idDiscriminator = Guid.NewGuid();
var dtos = Generate(count);
var entities = dtos.Select(d =>
{
var entity = d.Adapt<ChangeLog>();
entity.DiscriminatorId = idDiscriminator;
entity.Creation = DateTimeOffset.UtcNow.AddDays(generatorRandomDigits.Next(minDayCount, maxDayCount));
entity.IdCreatedCommit = commit.Id;
return entity;
}).ToArray();
dbContext.ChangeLog.AddRange(entities); dbContext.ChangeLog.AddRange(entities);
dbContext.SaveChanges(); await dbContext.SaveChangesAsync();
return (idDiscriminator, entities); return (idDiscriminator, entities);
} }

View File

@ -0,0 +1,21 @@
namespace DD.Persistence.Models.Requests;
/// <summary>
/// Модель коммита с изменениями
/// </summary>
public class ChangeLogCommitDto : CreateChangeLogCommitRequest
{
/// <summary>
/// Id
/// </summary>
public Guid Id { get; set; }
/// <summary>
///
/// </summary>
public ChangeLogCommitDto(Guid id, CreateChangeLogCommitRequest request) : base(request.IdAuthor, request.Comment)
{
Id = id;
}
}

View File

@ -0,0 +1,33 @@
namespace DD.Persistence.Models.Requests;
/// <summary>
/// Модель для создания коммита
/// </summary>
public class CreateChangeLogCommitRequest
{
/// <summary>
/// Дата создания
/// </summary>
public DateTimeOffset Creation { get; set; }
/// <summary>
/// Пользователь, совершающий коммит
/// </summary>
public Guid IdAuthor { get; set; }
/// <summary>
/// Комментарий
/// </summary>
public string Comment { get; set; } = string.Empty;
/// <summary>
///
/// </summary>
public CreateChangeLogCommitRequest(Guid idAuthor, string comment)
{
IdAuthor = idAuthor;
Comment = comment;
Creation = DateTimeOffset.UtcNow;
}
}

View File

@ -11,8 +11,8 @@
<PackageReference Include="coverlet.collector" Version="6.0.2" /> <PackageReference Include="coverlet.collector" Version="6.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="Shouldly" Version="4.2.1" /> <PackageReference Include="Shouldly" Version="4.2.1" />
<PackageReference Include="Testcontainers" Version="4.1.0" /> <PackageReference Include="Testcontainers" Version="4.2.0" />
<PackageReference Include="Testcontainers.PostgreSql" Version="4.1.0" /> <PackageReference Include="Testcontainers.PostgreSql" Version="4.2.0" />
<PackageReference Include="xunit" Version="2.9.2" /> <PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.extensibility.core" Version="2.9.2" /> <PackageReference Include="xunit.extensibility.core" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />

View File

@ -0,0 +1,279 @@
using DD.Persistence.API.Services;
using DD.Persistence.Models;
using DD.Persistence.Models.Common;
using DD.Persistence.Models.Requests;
using DD.Persistence.Repositories;
using Microsoft.Extensions.Caching.Memory;
using NSubstitute;
using UuidExtensions;
namespace DD.Persistence.Test;
public class ChangeLogTest
{
private readonly IChangeLogCommitRepository changeLogCommitRepository = Substitute.For<IChangeLogCommitRepository>();
private readonly IChangeLogRepository changeLogRepository = Substitute.For<IChangeLogRepository>();
private ChangeLogService service;
public ChangeLogTest()
{
var memoryCache = new MemoryCache(new MemoryCacheOptions());
service = new ChangeLogService(memoryCache, changeLogCommitRepository, changeLogRepository);
}
[Fact]
public async Task AddRange()
{
//arrange
var discriminatorId = Uuid7.Guid();
var expectedCommitId = Uuid7.Guid();
var comment = "Добавление нескольких значений";
var commitRequest = new CreateChangeLogCommitRequest(Uuid7.Guid(), comment);
var commit = new ChangeLogCommitDto(expectedCommitId, commitRequest);
var dtos = GenerateChangeLogValuesDto(2);
changeLogCommitRepository.Add(Arg.Any<CreateChangeLogCommitRequest>(), Arg.Any<CancellationToken>()).Returns(Uuid7.Guid());
changeLogRepository
.AddRange(
Arg.Any<Guid>(),
Arg.Any<ChangeLogCommitDto>(),
Arg.Any<IEnumerable<ChangeLogValuesDto>>(),
Arg.Any<CancellationToken>())
.Returns(2);
//act
var addRangeResult = await service
.AddRange(discriminatorId, commitRequest, dtos, CancellationToken.None);
addRangeResult = await service
.AddRange(discriminatorId, commitRequest, dtos, CancellationToken.None);
//assert
await changeLogCommitRepository.Received(1).Add(commitRequest, CancellationToken.None);
await changeLogRepository.Received(2).AddRange(discriminatorId, Arg.Any<ChangeLogCommitDto>(), dtos, CancellationToken.None);
}
[Fact]
public async Task UpdateRange()
{
//arrange
var discriminatorId = Uuid7.Guid();
var expectedCommitId = Uuid7.Guid();
var comment = "Изменение нескольких значений";
var commitRequest = new CreateChangeLogCommitRequest(Uuid7.Guid(), comment);
var commit = new ChangeLogCommitDto(expectedCommitId, commitRequest);
var dtos = GenerateChangeLogValuesDto(2);
changeLogCommitRepository.Add(Arg.Any<CreateChangeLogCommitRequest>(), Arg.Any<CancellationToken>()).Returns(commit.Id);
changeLogRepository
.UpdateRange(
Arg.Any<ChangeLogCommitDto>(),
Arg.Any<IEnumerable<ChangeLogValuesDto>>(),
Arg.Any<CancellationToken>())
.Returns(2);
//act
var updateRangeResult = await service
.UpdateRange(commitRequest, dtos, CancellationToken.None);
updateRangeResult = await service
.UpdateRange(commitRequest, dtos, CancellationToken.None);
updateRangeResult = await service
.UpdateRange(commitRequest, dtos, CancellationToken.None);
//assert
await changeLogCommitRepository.Received(1).Add(commitRequest, CancellationToken.None);
await changeLogRepository.Received(3).UpdateRange(Arg.Any<ChangeLogCommitDto>(), dtos, CancellationToken.None);
}
[Fact]
public async Task MarkAsDeleted()
{
//arrange
var discriminatorId = Uuid7.Guid();
var expectedCommitId = Uuid7.Guid();
var comment = "Удаление нескольких значений";
var commitRequest = new CreateChangeLogCommitRequest(Uuid7.Guid(), comment);
var commit = new ChangeLogCommitDto(expectedCommitId, commitRequest);
var dtos = GenerateChangeLogValuesDto(2);
var dtoIds = dtos.Select(d => d.Id);
changeLogCommitRepository.Add(Arg.Any<CreateChangeLogCommitRequest>(), Arg.Any<CancellationToken>()).Returns(expectedCommitId);
changeLogRepository
.MarkAsDeleted(
Arg.Any<Guid>(),
Arg.Any<IEnumerable<Guid>>(),
Arg.Any<DateTimeOffset>(),
Arg.Any<CancellationToken>())
.Returns(2);
//act
var markAsDeletedResult = await service
.MarkAsDeleted(dtoIds, commitRequest, CancellationToken.None);
markAsDeletedResult = await service
.MarkAsDeleted(dtoIds, commitRequest, CancellationToken.None);
//assert
await changeLogCommitRepository.Received(1).Add(commitRequest, CancellationToken.None);
await changeLogRepository.Received(2).MarkAsDeleted(commit.Id, dtoIds, Arg.Any<DateTimeOffset>(), CancellationToken.None);
}
[Fact]
public async Task ClearAndAddRange()
{
//arrange
var discriminatorId = Uuid7.Guid();
var expectedCommitId = Uuid7.Guid();
var comment = "Удаление и добавление нескольких значений";
var commitRequest = new CreateChangeLogCommitRequest(expectedCommitId, comment);
var commit = new ChangeLogCommitDto(expectedCommitId, commitRequest);
var dtos = GenerateChangeLogValuesDto(2);
var dtoIds = dtos.Select(d => d.Id);
changeLogCommitRepository.Add(Arg.Any<CreateChangeLogCommitRequest>(), Arg.Any<CancellationToken>()).Returns(Uuid7.Guid());
changeLogRepository
.ClearAndAddRange(
Arg.Any<Guid>(),
Arg.Any<ChangeLogCommitDto>(),
Arg.Any<IEnumerable<ChangeLogValuesDto>>(),
Arg.Any<CancellationToken>())
.Returns(2);
//act
var clearAndAddResult = await service
.ClearAndAddRange(discriminatorId, commitRequest, dtos, CancellationToken.None);
clearAndAddResult = await service
.ClearAndAddRange(discriminatorId, commitRequest, dtos, CancellationToken.None);
//assert
await changeLogCommitRepository.Received(1).Add(commitRequest, CancellationToken.None);
await changeLogRepository.Received(2).ClearAndAddRange(discriminatorId, Arg.Any<ChangeLogCommitDto>(), dtos, CancellationToken.None);
}
[Fact]
public async Task GetByDate()
{
//arrange
var discriminatorId = Uuid7.Guid();
var paginationRequest = new PaginationRequest()
{
Skip = 0,
Take = 1000
};
var dtos = GenerateChangeLogValuesDto(5);
var items = new PaginationContainer<ChangeLogValuesDto>()
{
Take = paginationRequest.Take,
Skip = paginationRequest.Skip,
Items = dtos,
Count = 10
};
var momentDate = DateTime.UtcNow;
changeLogRepository
.GetByDate(
Arg.Any<Guid>(),
Arg.Any<DateTimeOffset>(),
Arg.Any<PaginationRequest>(),
Arg.Any<CancellationToken>())
.Returns(items);
//act
var actualItems = await service
.GetByDate(discriminatorId, momentDate, paginationRequest, CancellationToken.None);
//assert
await changeLogRepository.Received(1).GetByDate(discriminatorId, momentDate, paginationRequest, CancellationToken.None);
}
[Fact]
public async Task GetChangeLogForInterval()
{
//arrange
var discriminatorId = Uuid7.Guid();
var dtos = GenerateChangeLogDto(5);
var dateBegin = DateTimeOffset.UtcNow.AddDays(-5);
var dateEnd = DateTimeOffset.UtcNow;
changeLogRepository
.GetChangeLogForInterval(
Arg.Any<Guid>(),
Arg.Any<DateTimeOffset>(),
Arg.Any<DateTimeOffset>(),
Arg.Any<CancellationToken>())
.Returns(dtos);
//act
var actualItems = await service
.GetChangeLogForInterval(discriminatorId, dateBegin, dateEnd, CancellationToken.None);
//assert
await changeLogRepository.Received(1).GetChangeLogForInterval(discriminatorId, dateBegin, dateEnd, CancellationToken.None);
}
[Fact]
public async Task GetDatesChange()
{
//arrange
var discriminatorId = Uuid7.Guid();
var dateBegin = DateTimeOffset.UtcNow.AddDays(-5);
var dateEnd = DateTimeOffset.UtcNow;
var dateOnlyBegin = new DateOnly(dateBegin.Year, dateBegin.Month, dateBegin.Day);
var dateOnlyEnd = new DateOnly(dateEnd.Year, dateEnd.Month, dateEnd.Day);
var dtos = new List<DateOnly>() { dateOnlyBegin, dateOnlyEnd };
changeLogRepository
.GetDatesChange(
Arg.Any<Guid>(),
Arg.Any<CancellationToken>())
.Returns(dtos);
//act
var actualItems = await service
.GetDatesChange(discriminatorId, CancellationToken.None);
//assert
await changeLogRepository.Received(1).GetDatesChange(discriminatorId, CancellationToken.None);
}
private IEnumerable<ChangeLogValuesDto> GenerateChangeLogValuesDto(int count)
{
var items = new List<ChangeLogValuesDto>();
for (int i = 0; i < count; i++)
{
items.Add(new ChangeLogValuesDto()
{
Id = Uuid7.Guid(),
Value = new Dictionary<string, object>
{
{ "1", 1 },
{ "2", 2 }
}
});
}
return items;
}
private IEnumerable<ChangeLogDto> GenerateChangeLogDto(int count)
{
var items = new List<ChangeLogDto>();
for (int i = 0; i < count; i++)
{
items.Add(new ChangeLogDto()
{
Id = Uuid7.Guid(),
});
}
return items;
}
}

View File

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

View File

@ -14,9 +14,10 @@ public interface IChangeLogApi : ISyncWithDiscriminatorApi<ChangeLogValuesDto>
/// </summary> /// </summary>
/// <param name="idDiscriminator"></param> /// <param name="idDiscriminator"></param>
/// <param name="dtos"></param> /// <param name="dtos"></param>
/// <param name="comment"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<IActionResult> ClearAndAddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token); Task<IActionResult> ClearAndAddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, string comment, CancellationToken token);
/// <summary> /// <summary>
/// Получение данных на текущую дату (с пагинацией) /// Получение данных на текущую дату (с пагинацией)
@ -47,55 +48,33 @@ public interface IChangeLogApi : ISyncWithDiscriminatorApi<ChangeLogValuesDto>
/// <returns></returns> /// <returns></returns>
Task<IActionResult> GetChangeLogForDate(Guid idDiscriminator, DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token); Task<IActionResult> GetChangeLogForDate(Guid idDiscriminator, DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token);
/// <summary>
/// Добавить одну запись
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dto"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IActionResult> Add(Guid idDiscriminator, ChangeLogValuesDto dto, CancellationToken token);
/// <summary> /// <summary>
/// Добавить несколько записей /// Добавить несколько записей
/// </summary> /// </summary>
/// <param name="idDiscriminator"></param> /// <param name="idDiscriminator"></param>
/// <param name="dtos"></param> /// <param name="dtos"></param>
/// <param name="comment">комментарий</param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<IActionResult> AddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token); Task<IActionResult> AddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, string comment, CancellationToken token);
/// <summary>
/// Обновить одну запись
/// </summary>
/// <param name="dto"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IActionResult> Update(ChangeLogValuesDto dto, CancellationToken token);
/// <summary> /// <summary>
/// Обновить несколько записей /// Обновить несколько записей
/// </summary> /// </summary>
/// <param name="dtos"></param> /// <param name="dtos"></param>
/// <param name="comment">комментарий</param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<IActionResult> UpdateRange(IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token); Task<IActionResult> UpdateRange(IEnumerable<ChangeLogValuesDto> dtos, string comment, CancellationToken token);
/// <summary>
/// Удалить одну запись
/// </summary>
/// <param name="id"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IActionResult> Delete(Guid id, CancellationToken token);
/// <summary> /// <summary>
/// Удалить несколько записей /// Удалить несколько записей
/// </summary> /// </summary>
/// <param name="ids"></param> /// <param name="ids"></param>
/// <param name="comment">комментарий к удалению</param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<IActionResult> DeleteRange(IEnumerable<Guid> ids, CancellationToken token); Task<IActionResult> DeleteRange(IEnumerable<Guid> ids, string comment, CancellationToken token);
/// <summary> /// <summary>
/// Получение списка дат, в которые происходили изменения (день, месяц, год, без времени) /// Получение списка дат, в которые происходили изменения (день, месяц, год, без времени)

View File

@ -0,0 +1,22 @@
using DD.Persistence.Models.Requests;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DD.Persistence.Repositories;
/// <summary>
/// Интерфейс для работы с коммитами журнала изменений
/// </summary>
public interface IChangeLogCommitRepository
{
/// <summary>
/// Добавить коммит для журнала изменений
/// </summary>
/// <param name="commitDto"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<Guid> Add(CreateChangeLogCommitRequest commitDto, CancellationToken token);
}

View File

@ -1,61 +1,63 @@
using DD.Persistence.Models; using DD.Persistence.Models;
using DD.Persistence.Models.Common; using DD.Persistence.Models.Common;
using DD.Persistence.Models.Requests; using DD.Persistence.Models.Requests;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion.Internal;
namespace DD.Persistence.Repositories; namespace DD.Persistence.Repositories;
/// <summary> /// <summary>
/// Интерфейс для работы с историческими данными /// Интерфейс для работы с историческими данными
/// </summary> /// </summary>
/// <typeparam name="TDto"></typeparam>
public interface IChangeLogRepository : ISyncWithDiscriminatorRepository<ChangeLogValuesDto> public interface IChangeLogRepository : ISyncWithDiscriminatorRepository<ChangeLogValuesDto>
{ {
/// <summary> /// <summary>
/// Добавление записей /// Добавление записей
/// </summary> /// </summary>
/// <param name="idAuthor">пользователь, который добавляет</param>
/// <param name="idDiscriminator">ключ справочника</param> /// <param name="idDiscriminator">ключ справочника</param>
/// <param name="dto">коммит с изменениями</param>
/// <param name="dtos"></param> /// <param name="dtos"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<int> AddRange(Guid idAuthor, Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token); Task<int> AddRange(Guid idDiscriminator, ChangeLogCommitDto dto, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token);
/// <summary> /// <summary>
/// Пометить записи как удаленные /// Пометить записи как удаленные
/// </summary> /// </summary>
/// <param name="idEditor"></param> /// <param name="idCommit"></param>
/// <param name="ids">ключи записей</param> /// <param name="ids">ключи записей</param>
/// <param name="updateTime"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<int> MarkAsDeleted(Guid idEditor, IEnumerable<Guid> ids, CancellationToken token); Task<int> MarkAsDeleted(Guid idCommit, IEnumerable<Guid> ids, DateTimeOffset updateTime, CancellationToken token);
/// <summary> /// <summary>
/// Пометить записи как удаленные /// Пометить записи как удаленные
/// </summary> /// </summary>
/// <param name="idEditor"></param>
/// <param name="idDiscriminator">дискриминатор таблицы</param> /// <param name="idDiscriminator">дискриминатор таблицы</param>
/// <param name="idCommit"></param>
/// <param name="updateTime"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<int> MarkAsDeleted(Guid idEditor, Guid idDiscriminator, CancellationToken token); Task<int> MarkAsDeleted(Guid idDiscriminator, Guid idCommit, DateTimeOffset updateTime, CancellationToken token);
/// <summary> /// <summary>
/// Очистить и добавить новые /// Очистить и добавить новые
/// </summary> /// </summary>
/// <param name="idAuthor"></param>
/// <param name="idDiscriminator"></param> /// <param name="idDiscriminator"></param>
/// <param name="dto">коммит с изменениями</param>
/// <param name="dtos"></param> /// <param name="dtos"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<int> ClearAndAddRange(Guid idAuthor, Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token); Task<int> ClearAndAddRange(Guid idDiscriminator, ChangeLogCommitDto dto, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token);
/// <summary> /// <summary>
/// Редактирование записей /// Редактирование записей
/// </summary> /// </summary>
/// <param name="idEditor">пользователь, который редактирует</param> /// <param name="commitDto">коммит с изменениями</param>
/// <param name="dtos"></param> /// <param name="dtos"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<int> UpdateRange(Guid idEditor, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token); Task<int> UpdateRange(ChangeLogCommitDto commitDto, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token);
/// <summary> /// <summary>
/// Получение актуальных записей на определенный момент времени (с пагинацией) /// Получение актуальных записей на определенный момент времени (с пагинацией)