Files
jdescopingtool/NEW/src/JdeScoping.DataAccess/Services/ManualSyncRequestService.cs
T
Joseph Doherty 29ac56006d feat: implement ETL pipeline redesign and ConfigManager improvements
- Add pipeline registry with JSON-based configuration and hot-reload support
- Implement manual sync request feature with API, client UI, and database
- Improve ConfigManager: connection string dropdown in pipeline editor,
  step delete/reorder functionality, and fix JSON parsing for ConnectionStrings
2026-01-22 17:48:33 -05:00

203 lines
7.1 KiB
C#

using Dapper;
using JdeScoping.DataAccess.Interfaces;
using JdeScoping.Domain.Models;
using Microsoft.Extensions.Logging;
namespace JdeScoping.DataAccess.Services;
/// <summary>
/// Service implementation for managing manual data sync requests.
/// </summary>
public sealed class ManualSyncRequestService : IManualSyncRequestService
{
private readonly IDbConnectionFactory _connectionFactory;
private readonly ILogger<ManualSyncRequestService> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="ManualSyncRequestService"/> class.
/// </summary>
/// <param name="connectionFactory">The database connection factory.</param>
/// <param name="logger">The logger instance.</param>
public ManualSyncRequestService(
IDbConnectionFactory connectionFactory,
ILogger<ManualSyncRequestService> logger)
{
_connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <inheritdoc/>
public async Task<IReadOnlyList<ManualSyncRequest>> GetRequestsAsync(
bool pendingOnly = false,
CancellationToken ct = default)
{
_logger.LogDebug("Getting manual sync requests (pendingOnly: {PendingOnly})", pendingOnly);
const string sqlAll = """
SELECT ID, PipelineName, SyncType, RequestDT, RequestedBy,
CompletedDT, CancelDT, CancelledBy, RowVersion
FROM dbo.ManualSyncRequest
ORDER BY RequestDT DESC
""";
const string sqlPending = """
SELECT ID, PipelineName, SyncType, RequestDT, RequestedBy,
CompletedDT, CancelDT, CancelledBy, RowVersion
FROM dbo.ManualSyncRequest
WHERE CompletedDT IS NULL AND CancelDT IS NULL
ORDER BY RequestDT ASC
""";
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
var results = await connection.QueryAsync<ManualSyncRequest>(
pendingOnly ? sqlPending : sqlAll);
var list = results.ToList();
_logger.LogDebug("Retrieved {Count} manual sync requests", list.Count);
return list;
}
/// <inheritdoc/>
public async Task<ManualSyncRequest?> GetNextPendingRequestAsync(CancellationToken ct = default)
{
_logger.LogDebug("Getting next pending manual sync request");
const string sql = """
SELECT TOP 1 ID, PipelineName, SyncType, RequestDT, RequestedBy,
CompletedDT, CancelDT, CancelledBy, RowVersion
FROM dbo.ManualSyncRequest
WHERE CompletedDT IS NULL AND CancelDT IS NULL
ORDER BY RequestDT ASC
""";
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
var result = await connection.QueryFirstOrDefaultAsync<ManualSyncRequest>(sql);
if (result != null)
{
_logger.LogDebug("Found pending request ID {Id} for pipeline {Pipeline}",
result.Id, result.PipelineName);
}
else
{
_logger.LogDebug("No pending manual sync requests found");
}
return result;
}
/// <inheritdoc/>
public async Task<ManualSyncRequest> CreateRequestAsync(
string pipelineName,
string syncType,
string requestedBy,
CancellationToken ct = default)
{
_logger.LogInformation(
"Creating manual sync request for pipeline {Pipeline}, type {SyncType}, by {User}",
pipelineName, syncType, requestedBy);
const string sql = """
INSERT INTO dbo.ManualSyncRequest (PipelineName, SyncType, RequestedBy)
OUTPUT INSERTED.ID, INSERTED.PipelineName, INSERTED.SyncType,
INSERTED.RequestDT, INSERTED.RequestedBy,
INSERTED.CompletedDT, INSERTED.CancelDT, INSERTED.CancelledBy,
INSERTED.RowVersion
VALUES (@PipelineName, @SyncType, @RequestedBy)
""";
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
var result = await connection.QuerySingleAsync<ManualSyncRequest>(sql, new
{
PipelineName = pipelineName,
SyncType = syncType,
RequestedBy = requestedBy
});
_logger.LogInformation("Created manual sync request with ID {Id}", result.Id);
return result;
}
/// <inheritdoc/>
public async Task<bool> CancelRequestAsync(
int id,
string cancelledBy,
byte[] rowVersion,
CancellationToken ct = default)
{
_logger.LogInformation(
"Cancelling manual sync request ID {Id} by {User}",
id, cancelledBy);
const string sql = """
UPDATE dbo.ManualSyncRequest
SET CancelDT = @CancelDT, CancelledBy = @CancelledBy
WHERE ID = @Id AND RowVersion = @RowVersion
AND CompletedDT IS NULL AND CancelDT IS NULL
""";
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
var affectedRows = await connection.ExecuteAsync(sql, new
{
Id = id,
CancelDT = DateTime.UtcNow,
CancelledBy = cancelledBy,
RowVersion = rowVersion
});
var success = affectedRows > 0;
if (success)
{
_logger.LogInformation("Successfully cancelled manual sync request ID {Id}", id);
}
else
{
_logger.LogWarning(
"Failed to cancel manual sync request ID {Id} - already completed/cancelled or version mismatch",
id);
}
return success;
}
/// <inheritdoc/>
public async Task<bool> CompleteRequestAsync(
int id,
byte[] rowVersion,
CancellationToken ct = default)
{
_logger.LogInformation("Completing manual sync request ID {Id}", id);
const string sql = """
UPDATE dbo.ManualSyncRequest
SET CompletedDT = @CompletedDT
WHERE ID = @Id AND RowVersion = @RowVersion
AND CompletedDT IS NULL AND CancelDT IS NULL
""";
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
var affectedRows = await connection.ExecuteAsync(sql, new
{
Id = id,
CompletedDT = DateTime.UtcNow,
RowVersion = rowVersion
});
var success = affectedRows > 0;
if (success)
{
_logger.LogInformation("Successfully completed manual sync request ID {Id}", id);
}
else
{
_logger.LogWarning(
"Failed to complete manual sync request ID {Id} - already completed/cancelled or version mismatch",
id);
}
return success;
}
}