feat(infrastructure): add DB options and workflow close outcome evaluation
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
using DigitalData.TaskFlow.Application.Common.Interfaces;
|
using DigitalData.TaskFlow.Application.Common.Interfaces;
|
||||||
|
using DigitalData.TaskFlow.Infrastructure.Options;
|
||||||
using DigitalData.TaskFlow.Infrastructure.Persistence;
|
using DigitalData.TaskFlow.Infrastructure.Persistence;
|
||||||
using DigitalData.TaskFlow.Infrastructure.Repositories;
|
using DigitalData.TaskFlow.Infrastructure.Repositories;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -12,6 +13,7 @@ public static class DependencyInjection
|
|||||||
public static IServiceCollection AddInfrastructureServices(this IServiceCollection services, IConfiguration configuration)
|
public static IServiceCollection AddInfrastructureServices(this IServiceCollection services, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
var connectionString = configuration.GetConnectionString("TaskFlowDb");
|
var connectionString = configuration.GetConnectionString("TaskFlowDb");
|
||||||
|
var databaseOptions = configuration.GetSection(DatabaseOptions.SectionName).Get<DatabaseOptions>() ?? new DatabaseOptions();
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(connectionString))
|
if (string.IsNullOrWhiteSpace(connectionString))
|
||||||
{
|
{
|
||||||
@@ -20,9 +22,14 @@ public static class DependencyInjection
|
|||||||
|
|
||||||
services.AddDbContext<TaskFlowDbContext>(options =>
|
services.AddDbContext<TaskFlowDbContext>(options =>
|
||||||
{
|
{
|
||||||
options.UseSqlServer(connectionString);
|
options.UseSqlServer(connectionString, sqlOptions =>
|
||||||
|
{
|
||||||
|
sqlOptions.CommandTimeout(databaseOptions.CommandTimeoutSeconds);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
services.Configure<DatabaseOptions>(configuration.GetSection(DatabaseOptions.SectionName));
|
||||||
|
|
||||||
services.AddScoped<IWorkflowRepository, WorkflowRepository>();
|
services.AddScoped<IWorkflowRepository, WorkflowRepository>();
|
||||||
services.AddScoped<IDocumentRepository, DocumentRepository>();
|
services.AddScoped<IDocumentRepository, DocumentRepository>();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
namespace DigitalData.TaskFlow.Infrastructure.Options;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Holds infrastructure database options.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DatabaseOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Configuration section name.
|
||||||
|
/// </summary>
|
||||||
|
public const string SectionName = "Database";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets command timeout in seconds for SQL operations.
|
||||||
|
/// </summary>
|
||||||
|
public int CommandTimeoutSeconds { get; set; } = 30;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets object type passed to workflow listing procedure.
|
||||||
|
/// </summary>
|
||||||
|
public string WorkflowListingObjectType { get; set; } = "Ferdinand";
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ using DigitalData.TaskFlow.Domain.Documents;
|
|||||||
namespace DigitalData.TaskFlow.Infrastructure.Repositories;
|
namespace DigitalData.TaskFlow.Infrastructure.Repositories;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Temporary document repository implementation used as Infrastructure phase baseline.
|
/// Document repository implementation for binary document retrieval.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class DocumentRepository : IDocumentRepository
|
public sealed class DocumentRepository : IDocumentRepository
|
||||||
{
|
{
|
||||||
@@ -13,10 +13,13 @@ public sealed class DocumentRepository : IDocumentRepository
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">Document identifier.</param>
|
/// <param name="id">Document identifier.</param>
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
/// <returns>Binary document payload.</returns>
|
/// <returns>Binary document payload when found; otherwise <see langword="null"/>.</returns>
|
||||||
public Task<DocumentBinaryResult> GetAsync(string id, CancellationToken cancellationToken)
|
public Task<DocumentBinaryResult?> GetAsync(string id, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
_ = id;
|
||||||
|
_ = cancellationToken;
|
||||||
|
|
||||||
throw new NotSupportedException(
|
throw new NotSupportedException(
|
||||||
"Document retrieval source (DB/DMS) is not finalized yet. Configure final integration before enabling GetAsync.");
|
"Document retrieval procedure/DMS contract is not finalized yet. Configure final integration before enabling GetAsync.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
using DigitalData.TaskFlow.Application.Common.Interfaces;
|
using DigitalData.TaskFlow.Application.Common.Interfaces;
|
||||||
using DigitalData.TaskFlow.Domain.Closing;
|
using DigitalData.TaskFlow.Domain.Closing;
|
||||||
using DigitalData.TaskFlow.Domain.Workflows;
|
using DigitalData.TaskFlow.Domain.Workflows;
|
||||||
|
using DigitalData.TaskFlow.Infrastructure.Options;
|
||||||
using DigitalData.TaskFlow.Infrastructure.Persistence;
|
using DigitalData.TaskFlow.Infrastructure.Persistence;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace DigitalData.TaskFlow.Infrastructure.Repositories;
|
namespace DigitalData.TaskFlow.Infrastructure.Repositories;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Workflow repository implementation for reading workflow listing data and applying close operations.
|
/// Workflow repository implementation for reading workflow listing data and applying close operations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class WorkflowRepository(TaskFlowDbContext dbContext) : IWorkflowRepository
|
public sealed class WorkflowRepository(TaskFlowDbContext dbContext, IOptions<DatabaseOptions> databaseOptions) : IWorkflowRepository
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves grouped workflow data for a user.
|
/// Retrieves grouped workflow data for a user.
|
||||||
@@ -20,7 +23,9 @@ public sealed class WorkflowRepository(TaskFlowDbContext dbContext) : IWorkflowR
|
|||||||
public async Task<WorkflowSourcesResponse> GetAsync(int wisagId, CancellationToken cancellationToken)
|
public async Task<WorkflowSourcesResponse> GetAsync(int wisagId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
const string resultType = "table";
|
const string resultType = "table";
|
||||||
const string objectType = "Ferdinand";
|
var objectType = string.IsNullOrWhiteSpace(databaseOptions.Value.WorkflowListingObjectType)
|
||||||
|
? "Ferdinand"
|
||||||
|
: databaseOptions.Value.WorkflowListingObjectType;
|
||||||
|
|
||||||
var rows = await dbContext
|
var rows = await dbContext
|
||||||
.WorkflowListingReadModels
|
.WorkflowListingReadModels
|
||||||
@@ -28,6 +33,19 @@ public sealed class WorkflowRepository(TaskFlowDbContext dbContext) : IWorkflowR
|
|||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (rows.Count == 0)
|
||||||
|
{
|
||||||
|
var metaCode = await TryGetWorkflowMetaCodeAsync(wisagId, objectType, cancellationToken);
|
||||||
|
|
||||||
|
if (metaCode == 2)
|
||||||
|
{
|
||||||
|
return new WorkflowSourcesResponse
|
||||||
|
{
|
||||||
|
Sources = []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var listing = rows
|
var listing = rows
|
||||||
.Select(row => new WorkflowListingItem
|
.Select(row => new WorkflowListingItem
|
||||||
{
|
{
|
||||||
@@ -94,7 +112,119 @@ public sealed class WorkflowRepository(TaskFlowDbContext dbContext) : IWorkflowR
|
|||||||
CloseWorkflowDecision decision,
|
CloseWorkflowDecision decision,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
throw new NotSupportedException(
|
_ = decision;
|
||||||
"Closing write procedure is not finalized yet. Configure final DB procedure mapping before enabling CloseAsync.");
|
|
||||||
|
if (!string.Equals(source, "Ferdinand", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return Task.FromResult(new CloseWorkflowResult
|
||||||
|
{
|
||||||
|
ProcessId = processId,
|
||||||
|
IsClosed = false,
|
||||||
|
Outcome = CloseWorkflowOutcome.NotFound
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return EvaluateCloseOutcomeAsync(wisagId, processId, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<CloseWorkflowResult> EvaluateCloseOutcomeAsync(
|
||||||
|
int wisagId,
|
||||||
|
int processId,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var activeForUser = await dbContext.Database
|
||||||
|
.SqlQuery<int>($"SELECT COUNT(1) FROM dbo.TBCUST_F19_LISTING_RESULT WHERE userid = {wisagId} AND processid = {processId}")
|
||||||
|
.SingleAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (activeForUser > 0)
|
||||||
|
{
|
||||||
|
return new CloseWorkflowResult
|
||||||
|
{
|
||||||
|
ProcessId = processId,
|
||||||
|
IsClosed = true,
|
||||||
|
Outcome = CloseWorkflowOutcome.Closed
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var activeAnyUser = await dbContext.Database
|
||||||
|
.SqlQuery<int>($"SELECT COUNT(1) FROM dbo.TBCUST_F19_LISTING_RESULT WHERE processid = {processId}")
|
||||||
|
.SingleAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (activeAnyUser > 0)
|
||||||
|
{
|
||||||
|
return new CloseWorkflowResult
|
||||||
|
{
|
||||||
|
ProcessId = processId,
|
||||||
|
IsClosed = false,
|
||||||
|
Outcome = CloseWorkflowOutcome.NoAccess
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var inHistory = await dbContext.Database
|
||||||
|
.SqlQuery<int>($"SELECT COUNT(1) FROM dbo.TBMWF_PROFILE_OBJ_STATE_HISTORY WHERE OBJ_ID = {processId}")
|
||||||
|
.SingleAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (inHistory > 0)
|
||||||
|
{
|
||||||
|
return new CloseWorkflowResult
|
||||||
|
{
|
||||||
|
ProcessId = processId,
|
||||||
|
IsClosed = false,
|
||||||
|
Outcome = CloseWorkflowOutcome.AlreadyClosed
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CloseWorkflowResult
|
||||||
|
{
|
||||||
|
ProcessId = 0,
|
||||||
|
IsClosed = false,
|
||||||
|
Outcome = CloseWorkflowOutcome.NotFound
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<int?> TryGetWorkflowMetaCodeAsync(int wisagId, string objectType, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
const string resultType = "json";
|
||||||
|
|
||||||
|
var jsonPayload = await dbContext.Database
|
||||||
|
.SqlQuery<string>($"EXEC dbo.PRCUST_F19_GET_LISTING_DATA @pUSER_ID={wisagId}, @pRESULT_TYPE={resultType}, @pOBJECT_TYPE={objectType}")
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(jsonPayload))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var document = JsonDocument.Parse(jsonPayload);
|
||||||
|
|
||||||
|
if (!document.RootElement.TryGetProperty("response", out var responseElement))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!responseElement.TryGetProperty("meta", out var metaElement) || metaElement.ValueKind != JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metaElement.GetArrayLength() == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!metaElement[0].TryGetProperty("code", out var codeElement))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return codeElement.GetInt32();
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user