Initial commit: JDE Scoping Tool migration project

Set up repository with legacy .NET Framework 4.8 source (OLD/),
new .NET 10 Blazor solution (NEW/), OpenSpec specifications,
documentation, and project configuration.
This commit is contained in:
Joseph Doherty
2026-01-02 07:43:29 -05:00
commit 26ff8d9b4f
1761 changed files with 596509 additions and 0 deletions
@@ -0,0 +1,221 @@
using System.Runtime.CompilerServices;
using Dapper;
using JdeScoping.DataAccess.Configuration;
using JdeScoping.DataAccess.Interfaces;
using JdeScoping.DataAccess.Models;
using JdeScoping.DataAccess.Models.Results;
using JdeScoping.DataAccess.QueryBuilders;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using SqlKata.Compilers;
namespace JdeScoping.DataAccess.Services;
/// <summary>
/// Main search processor service that orchestrates search execution.
/// </summary>
public sealed class SearchProcessor
{
private readonly IDbConnectionFactory _connectionFactory;
private readonly ISearchQueryBuilder _queryBuilder;
private readonly IWorkOrderTraversalService _traversalService;
private readonly MisQueryBuilder _misQueryBuilder;
private readonly SearchProcessingConfiguration _options;
private readonly ILogger<SearchProcessor> _logger;
/// <summary>
/// Initializes a new instance of SearchProcessor.
/// </summary>
public SearchProcessor(
IDbConnectionFactory connectionFactory,
ISearchQueryBuilder queryBuilder,
IWorkOrderTraversalService traversalService,
SqlServerCompiler compiler,
IOptions<SearchProcessingConfiguration> options,
ILogger<SearchProcessor> logger)
{
_connectionFactory = connectionFactory;
_queryBuilder = queryBuilder;
_traversalService = traversalService;
_misQueryBuilder = new MisQueryBuilder(compiler);
_options = options.Value;
_logger = logger;
}
/// <summary>
/// Executes search and returns results as async stream.
/// </summary>
/// <param name="model">The search model containing filter criteria.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Async enumerable of search results.</returns>
public async IAsyncEnumerable<SearchResult> ExecuteSearchAsync(
SearchModel model,
[EnumeratorCancellation] CancellationToken ct = default)
{
_logger.LogInformation("Executing search {SearchId}", model.Id);
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
// Build the search query
var queryResult = _queryBuilder.BuildSearchQuery(model);
if (_options.EnableDebugSql && !string.IsNullOrEmpty(_options.DebugSqlPath))
{
await WriteDebugSqlAsync(model.Id, queryResult);
}
// Execute setup SQL (temp tables, filter population)
foreach (var setupSql in queryResult.TempTableSetupSql)
{
_logger.LogDebug("Executing setup SQL: {Sql}", setupSql[..Math.Min(100, setupSql.Length)]);
await connection.ExecuteAsync(
setupSql,
queryResult.Parameters,
commandTimeout: _options.QueryTimeoutSeconds);
}
// Execute downstream traversal
await _traversalService.TraverseDownstreamAsync(
connection,
_options.MaxTraversalIterations,
ct);
// Stream results using unbuffered query
_logger.LogDebug("Executing result query");
var reader = await connection.QueryAsync<SearchResult>(
queryResult.Sql,
queryResult.Parameters,
commandTimeout: _options.QueryTimeoutSeconds);
foreach (var result in reader)
{
ct.ThrowIfCancellationRequested();
yield return result;
}
_logger.LogInformation("Search {SearchId} completed", model.Id);
}
/// <summary>
/// Executes search and materializes all results into SearchModel.
/// </summary>
/// <param name="model">The search model containing filter criteria.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The SearchModel populated with results.</returns>
public async Task<SearchModel> ExecuteSearchToModelAsync(
SearchModel model,
CancellationToken ct = default)
{
_logger.LogInformation("Executing search {SearchId} to model", model.Id);
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
// Build the search query
var queryResult = _queryBuilder.BuildSearchQuery(model);
if (_options.EnableDebugSql && !string.IsNullOrEmpty(_options.DebugSqlPath))
{
await WriteDebugSqlAsync(model.Id, queryResult);
}
// Execute setup SQL (temp tables, filter population)
foreach (var setupSql in queryResult.TempTableSetupSql)
{
_logger.LogDebug("Executing setup SQL: {Sql}", setupSql[..Math.Min(100, setupSql.Length)]);
await connection.ExecuteAsync(
setupSql,
queryResult.Parameters,
commandTimeout: _options.QueryTimeoutSeconds);
}
// Execute downstream traversal
await _traversalService.TraverseDownstreamAsync(
connection,
_options.MaxTraversalIterations,
ct);
// Execute result query and materialize
_logger.LogDebug("Executing result query");
var results = await connection.QueryAsync<SearchResult>(
queryResult.Sql,
queryResult.Parameters,
commandTimeout: _options.QueryTimeoutSeconds);
model.Results = results.ToList();
_logger.LogInformation("Search {SearchId} returned {ResultCount} results", model.Id, model.Results.Count);
// Extract MIS data if requested
if (model.ExtractMisData)
{
await ExecuteMisExtractionAsync(model, connection, ct);
}
return model;
}
private async Task ExecuteMisExtractionAsync(
SearchModel model,
SqlConnection connection,
CancellationToken ct)
{
_logger.LogDebug("Extracting MIS data for search {SearchId}", model.Id);
// Build and execute MIS setup SQL
var misSetupStatements = _misQueryBuilder.BuildMisExtractionSql(model);
var misParameters = new Dictionary<string, object>();
if (model.MinimumDt.HasValue)
{
misParameters["p_MinimumDT"] = model.MinimumDt.Value;
}
if (model.MaximumDt.HasValue)
{
misParameters["p_MaximumDT"] = model.MaximumDt.Value;
}
foreach (var sql in misSetupStatements)
{
await connection.ExecuteAsync(
sql,
misParameters,
commandTimeout: _options.QueryTimeoutSeconds);
}
// Execute MIS result query
var misQueryResult = _queryBuilder.BuildMisQuery(model);
var misResults = await connection.QueryAsync<MisSearchResult>(
misQueryResult.Sql,
misQueryResult.Parameters,
commandTimeout: _options.QueryTimeoutSeconds);
model.MisResults = misResults.ToList();
_logger.LogDebug("Found {MisResultCount} MIS results", model.MisResults.Count);
// Execute MIS non-match query
var misNonMatchQueryResult = _queryBuilder.BuildMisNonMatchQuery(model);
var misNonMatchResults = await connection.QueryAsync<MisNonMatchSearchResult>(
misNonMatchQueryResult.Sql,
misNonMatchQueryResult.Parameters,
commandTimeout: _options.QueryTimeoutSeconds);
model.MisNonMatchResults = misNonMatchResults.ToList();
_logger.LogDebug("Found {MisNonMatchCount} MIS non-match results", model.MisNonMatchResults.Count);
}
private async Task WriteDebugSqlAsync(int searchId, SearchQueryResult queryResult)
{
try
{
var debugPath = Path.Combine(_options.DebugSqlPath!, $"search_{searchId}_{DateTime.UtcNow:yyyyMMdd_HHmmss}.sql");
var content = string.Join("\r\n\r\n-- ========================================\r\n\r\n",
queryResult.TempTableSetupSql.Append(queryResult.Sql));
await File.WriteAllTextAsync(debugPath, content);
_logger.LogDebug("Debug SQL written to {Path}", debugPath);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to write debug SQL");
}
}
}