refactor: remove unused classes and consolidate ViewModels in Core
Remove 9 unused types from Core (duplicate extension classes, TableSpec, ColumnSpec, LotLocation), move ComponentLotViewModel and OperatorViewModel from Client to Core, and refactor DataSync.Dev to use pipeline-based configuration. Fix Login.razor to use UserInfoDto directly.
This commit is contained in:
@@ -2,6 +2,7 @@ using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using JdeScoping.Api.Extensions;
|
||||
using JdeScoping.Core.ApiContracts;
|
||||
using JdeScoping.Core.ApiContracts.Auth;
|
||||
using JdeScoping.Core.Interfaces;
|
||||
using JdeScoping.Core.Models;
|
||||
using JdeScoping.Core.Models.Auth;
|
||||
@@ -98,7 +99,8 @@ public class AuthController : ApiControllerBase
|
||||
new AuthenticationProperties { IsPersistent = false });
|
||||
|
||||
_logger.LogInformation("User {Username} logged in successfully", loginModel.Username);
|
||||
return Ok(new LoginResultModel(true, null, result.User));
|
||||
var userDto = UserInfoDto.FromUserInfo(result.User!);
|
||||
return Ok(new LoginResultModel(true, null, userDto));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -122,10 +124,11 @@ public class AuthController : ApiControllerBase
|
||||
/// <returns>User info on success, 401 if not authenticated</returns>
|
||||
[HttpGet("me")]
|
||||
[Authorize]
|
||||
[ProducesResponseType(typeof(UserInfo), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(UserInfoDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public ActionResult<UserInfo> GetCurrentUser()
|
||||
public ActionResult<UserInfoDto> GetCurrentUser()
|
||||
{
|
||||
return Ok(CurrentUser);
|
||||
// CurrentUser is guaranteed non-null due to [Authorize] attribute
|
||||
return Ok(UserInfoDto.FromUserInfo(CurrentUser!));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ public partial class FileIOController
|
||||
|
||||
try
|
||||
{
|
||||
using var stream = file.OpenReadStream();
|
||||
await using var stream = file.OpenReadStream();
|
||||
var lotViewModels = _parserService.ParseComponentLots(stream);
|
||||
|
||||
var lots = await _repository.LookupLotsAsync(lotViewModels, ct);
|
||||
|
||||
@@ -31,7 +31,7 @@ public partial class FileIOController
|
||||
|
||||
try
|
||||
{
|
||||
using var stream = file.OpenReadStream();
|
||||
await using var stream = file.OpenReadStream();
|
||||
var itemNumbers = _parserService.ParseItems(stream);
|
||||
|
||||
var items = await _repository.LookupItemsAsync(itemNumbers, ct);
|
||||
|
||||
@@ -31,7 +31,7 @@ public partial class FileIOController
|
||||
|
||||
try
|
||||
{
|
||||
using var stream = file.OpenReadStream();
|
||||
await using var stream = file.OpenReadStream();
|
||||
var workOrderNumbers = _parserService.ParseWorkOrders(stream);
|
||||
|
||||
var workOrders = await _repository.LookupWorkordersAsync(workOrderNumbers, ct);
|
||||
|
||||
@@ -149,10 +149,14 @@ public class PipelineController : ControllerBase
|
||||
matchCols,
|
||||
config.Destination.ExcludeFromUpdate?.ToList());
|
||||
|
||||
// Mass uses massQuery with no parameters; Daily/Hourly use query with parameters
|
||||
var parameters = config.Source.Parameters?.Select(p => new PipelineParameterDto(
|
||||
p.Key, p.Value.Format, p.Value.Source)).ToList() ?? [];
|
||||
|
||||
var schedules = new PipelineSchedulesDto(
|
||||
MapSchedule(config.Schedules?.Mass, defaults.Mass),
|
||||
MapSchedule(config.Schedules?.Daily, defaults.Daily),
|
||||
MapSchedule(config.Schedules?.Hourly, defaults.Hourly));
|
||||
MapSchedule(config.Schedules?.Mass, defaults.Mass, config.Source.MassQuery, [], config.PreScripts, config.PostScripts),
|
||||
MapSchedule(config.Schedules?.Daily, defaults.Daily, config.Source.Query, parameters, config.PreScripts, config.PostScripts),
|
||||
MapSchedule(config.Schedules?.Hourly, defaults.Hourly, config.Source.Query, parameters, config.PreScripts, config.PostScripts));
|
||||
|
||||
return new PipelineConfigDto(
|
||||
name,
|
||||
@@ -167,7 +171,11 @@ public class PipelineController : ControllerBase
|
||||
|
||||
private static PipelineScheduleDto MapSchedule(
|
||||
ScheduleConfig? config,
|
||||
ScheduleConfig defaults)
|
||||
ScheduleConfig defaults,
|
||||
string? query,
|
||||
List<PipelineParameterDto> parameters,
|
||||
List<string>? preScripts,
|
||||
List<string>? postScripts)
|
||||
{
|
||||
return new PipelineScheduleDto(
|
||||
config?.Enabled ?? defaults.Enabled,
|
||||
@@ -176,7 +184,11 @@ public class PipelineController : ControllerBase
|
||||
config?.ReIndex ?? defaults.ReIndex,
|
||||
config?.IntervalMinutes > 0 && config.IntervalMinutes != defaults.IntervalMinutes,
|
||||
config?.PrePurge != null && config.PrePurge != defaults.PrePurge,
|
||||
config?.ReIndex != null && config.ReIndex != defaults.ReIndex);
|
||||
config?.ReIndex != null && config.ReIndex != defaults.ReIndex,
|
||||
query,
|
||||
parameters,
|
||||
preScripts,
|
||||
postScripts);
|
||||
}
|
||||
|
||||
private static ScheduleConfig? GetScheduleConfig(
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using JdeScoping.Core.Models;
|
||||
using JdeScoping.Core.Models.Infrastructure;
|
||||
using JdeScoping.Core.Models.Search;
|
||||
using JdeScoping.Core.ApiContracts.SignalR;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -11,7 +9,7 @@ namespace JdeScoping.Api.Hubs;
|
||||
/// </summary>
|
||||
public class StatusHub : Hub
|
||||
{
|
||||
private static StatusUpdate _cachedStatus = new()
|
||||
private static StatusUpdateDto _cachedStatus = new()
|
||||
{
|
||||
Message = "Unknown",
|
||||
Timestamp = DateTime.UtcNow
|
||||
@@ -29,7 +27,7 @@ public class StatusHub : Hub
|
||||
/// Caches the update and broadcasts to all clients.
|
||||
/// </summary>
|
||||
/// <param name="statusUpdate">Status update to broadcast</param>
|
||||
public async Task SetStatus(StatusUpdate statusUpdate)
|
||||
public async Task SetStatus(StatusUpdateDto statusUpdate)
|
||||
{
|
||||
_cachedStatus = statusUpdate;
|
||||
await Clients.All.SendAsync("statusUpdate", statusUpdate);
|
||||
@@ -40,7 +38,7 @@ public class StatusHub : Hub
|
||||
/// Called by clients to get initial cached status on connection.
|
||||
/// </summary>
|
||||
/// <returns>The most recent status update</returns>
|
||||
public StatusUpdate GetCachedStatus()
|
||||
public StatusUpdateDto GetCachedStatus()
|
||||
{
|
||||
return _cachedStatus;
|
||||
}
|
||||
@@ -49,7 +47,7 @@ public class StatusHub : Hub
|
||||
/// Called by controllers/services to broadcast search updates.
|
||||
/// </summary>
|
||||
/// <param name="searchUpdate">Search update to broadcast</param>
|
||||
public async Task PublishSearchUpdate(SearchUpdate searchUpdate)
|
||||
public async Task PublishSearchUpdate(SearchUpdateDto searchUpdate)
|
||||
{
|
||||
await Clients.All.SendAsync("searchUpdate", searchUpdate);
|
||||
_logger.LogDebug("Search update published: ID={Id}, Status={Status}", searchUpdate.Id, searchUpdate.Status);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using JdeScoping.Api.Hubs;
|
||||
using JdeScoping.Core.ApiContracts.SignalR;
|
||||
using JdeScoping.Core.Interfaces;
|
||||
using JdeScoping.Core.Models.Infrastructure;
|
||||
using JdeScoping.Core.Models.Search;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -34,7 +34,7 @@ public class SearchNotificationService : ISearchNotificationService
|
||||
{
|
||||
try
|
||||
{
|
||||
var update = new SearchUpdate(search);
|
||||
var update = SearchUpdateDto.FromSearch(search);
|
||||
await _hubContext.Clients.All.SendAsync("searchUpdate", update, ct);
|
||||
_logger.LogDebug(
|
||||
"Search update notification sent: Id={SearchId}, Status={Status}",
|
||||
@@ -56,7 +56,7 @@ public class SearchNotificationService : ISearchNotificationService
|
||||
{
|
||||
try
|
||||
{
|
||||
var update = new StatusUpdate(status);
|
||||
var update = StatusUpdateDto.Create(status);
|
||||
await _hubContext.Clients.All.SendAsync("statusUpdate", update, ct);
|
||||
_logger.LogDebug("Status notification sent: {Status}", status);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Security.Claims;
|
||||
using JdeScoping.Client.Models;
|
||||
using JdeScoping.Core.ApiContracts.Auth;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
|
||||
namespace JdeScoping.Client.Auth;
|
||||
@@ -46,14 +46,14 @@ public class AuthStateProvider : AuthenticationStateProvider
|
||||
/// Validates the current session by calling /api/auth/me.
|
||||
/// Returns null if not authenticated.
|
||||
/// </summary>
|
||||
private async Task<UserInfoViewModel?> ValidateSessionAsync()
|
||||
private async Task<UserInfoDto?> ValidateSessionAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync("api/auth/me");
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return await response.Content.ReadFromJsonAsync<UserInfoViewModel>();
|
||||
return await response.Content.ReadFromJsonAsync<UserInfoDto>();
|
||||
}
|
||||
}
|
||||
catch
|
||||
@@ -67,7 +67,7 @@ public class AuthStateProvider : AuthenticationStateProvider
|
||||
/// <summary>
|
||||
/// Creates an authenticated state from user info.
|
||||
/// </summary>
|
||||
private static AuthenticationState CreateAuthState(UserInfoViewModel user)
|
||||
private static AuthenticationState CreateAuthState(UserInfoDto user)
|
||||
{
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
@@ -87,7 +87,7 @@ public class AuthStateProvider : AuthenticationStateProvider
|
||||
/// <summary>
|
||||
/// Called after successful login to update auth state.
|
||||
/// </summary>
|
||||
public async Task MarkUserAsAuthenticated(UserInfoViewModel user)
|
||||
public async Task MarkUserAsAuthenticated(UserInfoDto user)
|
||||
{
|
||||
await _userStorage.SetUserAsync(user);
|
||||
NotifyAuthenticationStateChanged(Task.FromResult(CreateAuthState(user)));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using JdeScoping.Client.Models;
|
||||
using JdeScoping.Core.ApiContracts.Auth;
|
||||
|
||||
namespace JdeScoping.Client.Auth;
|
||||
|
||||
@@ -12,12 +12,12 @@ public interface IUserStorageService
|
||||
/// <summary>
|
||||
/// Gets the stored user info.
|
||||
/// </summary>
|
||||
Task<UserInfoViewModel?> GetUserAsync();
|
||||
Task<UserInfoDto?> GetUserAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Stores the user info.
|
||||
/// </summary>
|
||||
Task SetUserAsync(UserInfoViewModel user);
|
||||
Task SetUserAsync(UserInfoDto user);
|
||||
|
||||
/// <summary>
|
||||
/// Removes the stored user info.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Text.Json;
|
||||
using JdeScoping.Client.Models;
|
||||
using JdeScoping.Core.ApiContracts.Auth;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace JdeScoping.Client.Auth;
|
||||
@@ -19,7 +19,7 @@ public class UserStorageService : IUserStorageService
|
||||
_jsRuntime = jsRuntime;
|
||||
}
|
||||
|
||||
public async Task<UserInfoViewModel?> GetUserAsync()
|
||||
public async Task<UserInfoDto?> GetUserAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -29,7 +29,7 @@ public class UserStorageService : IUserStorageService
|
||||
return null;
|
||||
}
|
||||
|
||||
return JsonSerializer.Deserialize<UserInfoViewModel>(json, new JsonSerializerOptions
|
||||
return JsonSerializer.Deserialize<UserInfoDto>(json, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
});
|
||||
@@ -40,7 +40,7 @@ public class UserStorageService : IUserStorageService
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SetUserAsync(UserInfoViewModel user)
|
||||
public async Task SetUserAsync(UserInfoDto user)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(user);
|
||||
await _jsRuntime.InvokeVoidAsync("jdeScopingInterop.setSessionStorage", UserKey, json);
|
||||
|
||||
@@ -51,16 +51,42 @@
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(QueryPreview))
|
||||
@if (Config.Parameters?.Count > 0)
|
||||
{
|
||||
<RadzenText TextStyle="TextStyle.Subtitle1" class="rz-mb-2">Parameters</RadzenText>
|
||||
<ul class="rz-mb-4">
|
||||
@foreach (var param in Config.Parameters)
|
||||
{
|
||||
<li><strong>@param.Name</strong>: @(param.Format ?? "default") (source: @param.Source)</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(Config.Query))
|
||||
{
|
||||
<RadzenText TextStyle="TextStyle.Subtitle1" class="rz-mb-2">Query</RadzenText>
|
||||
<div style="background: #f5f5f5; padding: 0.75rem; border-radius: 4px; font-family: monospace; font-size: 0.875rem;">
|
||||
@QueryPreview
|
||||
</div>
|
||||
@if (!string.IsNullOrWhiteSpace(FullQuery))
|
||||
<pre style="background: #f8f9fa; padding: 1rem; border-radius: 4px; font-family: 'Consolas', 'Monaco', 'Courier New', monospace; font-size: 0.875rem; line-height: 1.5; overflow-x: auto; white-space: pre-wrap; word-break: break-word; max-height: 400px; overflow-y: auto;">@FormatSql(Config.Query)</pre>
|
||||
}
|
||||
|
||||
@if (Config.PreScripts?.Count > 0)
|
||||
{
|
||||
<RadzenText TextStyle="TextStyle.Subtitle1" class="rz-mt-4 rz-mb-2">Pre-Scripts (@Config.PreScripts.Count)</RadzenText>
|
||||
@for (int i = 0; i < Config.PreScripts.Count; i++)
|
||||
{
|
||||
<RadzenButton Text="View Full Query" Icon="open_in_new" ButtonStyle="ButtonStyle.Light" Size="ButtonSize.Small"
|
||||
Click="@(() => OnViewQuery.InvokeAsync(FullQuery))" class="rz-mt-2" />
|
||||
var script = Config.PreScripts[i];
|
||||
<RadzenText TextStyle="TextStyle.Body2" class="rz-mb-1"><strong>Script @(i + 1):</strong></RadzenText>
|
||||
<pre style="background: #fff3cd; padding: 0.75rem; border-radius: 4px; font-family: 'Consolas', 'Monaco', 'Courier New', monospace; font-size: 0.8rem; line-height: 1.4; overflow-x: auto; white-space: pre-wrap; word-break: break-word; max-height: 200px; overflow-y: auto; margin-bottom: 0.5rem;">@FormatSql(script)</pre>
|
||||
}
|
||||
}
|
||||
|
||||
@if (Config.PostScripts?.Count > 0)
|
||||
{
|
||||
<RadzenText TextStyle="TextStyle.Subtitle1" class="rz-mt-4 rz-mb-2">Post-Scripts (@Config.PostScripts.Count)</RadzenText>
|
||||
@for (int i = 0; i < Config.PostScripts.Count; i++)
|
||||
{
|
||||
var script = Config.PostScripts[i];
|
||||
<RadzenText TextStyle="TextStyle.Body2" class="rz-mb-1"><strong>Script @(i + 1):</strong></RadzenText>
|
||||
<pre style="background: #d1ecf1; padding: 0.75rem; border-radius: 4px; font-family: 'Consolas', 'Monaco', 'Courier New', monospace; font-size: 0.8rem; line-height: 1.4; overflow-x: auto; white-space: pre-wrap; word-break: break-word; max-height: 200px; overflow-y: auto; margin-bottom: 0.5rem;">@FormatSql(script)</pre>
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,9 +95,6 @@
|
||||
@code {
|
||||
[Parameter] public UpdateTypes ScheduleType { get; set; }
|
||||
[Parameter] public PipelineScheduleDto? Config { get; set; }
|
||||
[Parameter] public string? QueryPreview { get; set; }
|
||||
[Parameter] public string? FullQuery { get; set; }
|
||||
[Parameter] public EventCallback<string> OnViewQuery { get; set; }
|
||||
|
||||
private static string GetScheduleTypeName(UpdateTypes type) => type switch
|
||||
{
|
||||
@@ -89,4 +112,50 @@
|
||||
return $"{minutes / 60} hour(s) ({minutes} min)";
|
||||
return $"{minutes} minutes";
|
||||
}
|
||||
|
||||
private static string FormatSql(string? sql)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sql))
|
||||
return "";
|
||||
|
||||
// Format SELECT columns - put each column on its own line
|
||||
var result = FormatSelectColumns(sql);
|
||||
|
||||
// Add line breaks before major clauses
|
||||
result = result
|
||||
.Replace(" FROM ", "\nFROM ")
|
||||
.Replace(" WHERE ", "\nWHERE ")
|
||||
.Replace(" AND ", "\n AND ")
|
||||
.Replace(" OR ", "\n OR ")
|
||||
.Replace(" LEFT ", "\nLEFT ")
|
||||
.Replace(" RIGHT ", "\nRIGHT ")
|
||||
.Replace(" INNER ", "\nINNER ")
|
||||
.Replace(" OUTER ", "\nOUTER ")
|
||||
.Replace(" JOIN ", " JOIN\n ")
|
||||
.Replace(" ORDER BY ", "\nORDER BY ")
|
||||
.Replace(" GROUP BY ", "\nGROUP BY ")
|
||||
.Replace(" HAVING ", "\nHAVING ");
|
||||
|
||||
return result.Trim();
|
||||
}
|
||||
|
||||
private static string FormatSelectColumns(string sql)
|
||||
{
|
||||
var selectIndex = sql.IndexOf("SELECT", StringComparison.OrdinalIgnoreCase);
|
||||
var fromIndex = sql.IndexOf(" FROM ", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (selectIndex < 0 || fromIndex < 0 || fromIndex <= selectIndex)
|
||||
return sql;
|
||||
|
||||
var beforeSelect = sql[..selectIndex];
|
||||
var selectKeyword = sql.Substring(selectIndex, 6);
|
||||
var columnsStart = selectIndex + 6;
|
||||
var columns = sql[columnsStart..fromIndex];
|
||||
var afterColumns = sql[fromIndex..];
|
||||
|
||||
var columnList = columns.Split(',');
|
||||
var formattedColumns = string.Join(",\n ", columnList.Select(c => c.Trim()));
|
||||
|
||||
return $"{beforeSelect}{selectKeyword} {formattedColumns}{afterColumns}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,9 +90,11 @@
|
||||
if (string.IsNullOrWhiteSpace(sql))
|
||||
return "";
|
||||
|
||||
// Basic SQL formatting - add line breaks before major clauses
|
||||
return sql
|
||||
.Replace(" SELECT ", "\nSELECT ")
|
||||
// Format SELECT columns - put each column on its own line
|
||||
var result = FormatSelectColumns(sql);
|
||||
|
||||
// Add line breaks before major clauses
|
||||
result = result
|
||||
.Replace(" FROM ", "\nFROM ")
|
||||
.Replace(" WHERE ", "\nWHERE ")
|
||||
.Replace(" AND ", "\n AND ")
|
||||
@@ -104,8 +106,31 @@
|
||||
.Replace(" JOIN ", " JOIN\n ")
|
||||
.Replace(" ORDER BY ", "\nORDER BY ")
|
||||
.Replace(" GROUP BY ", "\nGROUP BY ")
|
||||
.Replace(" HAVING ", "\nHAVING ")
|
||||
.Trim();
|
||||
.Replace(" HAVING ", "\nHAVING ");
|
||||
|
||||
return result.Trim();
|
||||
}
|
||||
|
||||
private static string FormatSelectColumns(string sql)
|
||||
{
|
||||
// Find SELECT and FROM positions (case-insensitive)
|
||||
var selectIndex = sql.IndexOf("SELECT", StringComparison.OrdinalIgnoreCase);
|
||||
var fromIndex = sql.IndexOf(" FROM ", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (selectIndex < 0 || fromIndex < 0 || fromIndex <= selectIndex)
|
||||
return sql;
|
||||
|
||||
var beforeSelect = sql[..selectIndex];
|
||||
var selectKeyword = sql.Substring(selectIndex, 6); // "SELECT"
|
||||
var columnsStart = selectIndex + 6;
|
||||
var columns = sql[columnsStart..fromIndex];
|
||||
var afterColumns = sql[fromIndex..];
|
||||
|
||||
// Split columns by comma and rejoin with newlines
|
||||
var columnList = columns.Split(',');
|
||||
var formattedColumns = string.Join(",\n ", columnList.Select(c => c.Trim()));
|
||||
|
||||
return $"{beforeSelect}{selectKeyword} {formattedColumns}{afterColumns}";
|
||||
}
|
||||
|
||||
private async Task CopyToClipboard()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using JdeScoping.Client.Models;
|
||||
using JdeScoping.Core.Models.Enums;
|
||||
using JdeScoping.Core.Models.Search;
|
||||
using JdeScoping.Core.ViewModels;
|
||||
using CoreSearch = JdeScoping.Core.ViewModels.SearchViewModel;
|
||||
using CoreItem = JdeScoping.Core.ViewModels.ItemViewModel;
|
||||
using CoreWorkOrder = JdeScoping.Core.ViewModels.WorkOrderViewModel;
|
||||
@@ -9,6 +10,7 @@ using CoreWorkCenter = JdeScoping.Core.ViewModels.WorkCenterViewModel;
|
||||
using CoreLot = JdeScoping.Core.ViewModels.LotViewModel;
|
||||
using CorePartOp = JdeScoping.Core.ViewModels.PartOperationViewModel;
|
||||
using CoreJdeUser = JdeScoping.Core.ViewModels.JdeUserViewModel;
|
||||
using SearchViewModel = JdeScoping.Client.Models.SearchViewModel;
|
||||
|
||||
namespace JdeScoping.Client.Extensions;
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
namespace JdeScoping.Client.Models;
|
||||
|
||||
/// <summary>
|
||||
/// SignalR message for search status updates.
|
||||
/// </summary>
|
||||
public record SearchUpdate
|
||||
{
|
||||
public int Id { get; init; }
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public string UserName { get; init; } = string.Empty;
|
||||
public string Status { get; init; } = string.Empty;
|
||||
public DateTime? SubmitDt { get; init; }
|
||||
public DateTime? StartDt { get; init; }
|
||||
public DateTime? EndDt { get; init; }
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace JdeScoping.Client.Models;
|
||||
|
||||
/// <summary>
|
||||
/// SignalR message for processor status updates.
|
||||
/// </summary>
|
||||
public record StatusUpdate
|
||||
{
|
||||
public string Message { get; init; } = string.Empty;
|
||||
public DateTime? Timestamp { get; init; }
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
namespace JdeScoping.Client.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Client-side view model for authenticated user information.
|
||||
/// Mirrors the server-side UserInfo model returned by /api/auth/login and /api/auth/me.
|
||||
/// </summary>
|
||||
public class UserInfoViewModel
|
||||
{
|
||||
/// <summary>
|
||||
/// User's login username.
|
||||
/// </summary>
|
||||
public string Username { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// User's first name.
|
||||
/// </summary>
|
||||
public string FirstName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// User's last name.
|
||||
/// </summary>
|
||||
public string LastName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// User's display name (computed on server, provided here for convenience).
|
||||
/// </summary>
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// User's organization title.
|
||||
/// </summary>
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// User's email address.
|
||||
/// </summary>
|
||||
public string EmailAddress { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -6,17 +6,17 @@ namespace JdeScoping.Client.Models;
|
||||
/// </summary>
|
||||
public class ValidCombination
|
||||
{
|
||||
public int Id { get; init; }
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public bool Timespan { get; init; }
|
||||
public bool WorkOrder { get; init; }
|
||||
public bool ItemNumber { get; init; }
|
||||
public bool ProfitCenter { get; init; }
|
||||
public bool WorkCenter { get; init; }
|
||||
public bool ComponentLot { get; init; }
|
||||
public bool Operator { get; init; }
|
||||
public bool ItemOperationMis { get; init; }
|
||||
public bool ExtractMis { get; init; }
|
||||
public int Id { get; private init; }
|
||||
public string Name { get; private init; } = string.Empty;
|
||||
public bool Timespan { get; private init; }
|
||||
public bool WorkOrder { get; private init; }
|
||||
public bool ItemNumber { get; private init; }
|
||||
public bool ProfitCenter { get; private init; }
|
||||
public bool WorkCenter { get; private init; }
|
||||
public bool ComponentLot { get; private init; }
|
||||
public bool Operator { get; private init; }
|
||||
public bool ItemOperationMis { get; private init; }
|
||||
public bool ExtractMis { get; private init; }
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the given filter flags match this combination.
|
||||
@@ -48,7 +48,7 @@ public class ValidCombination
|
||||
/// </summary>
|
||||
public static IReadOnlyList<ValidCombination> GetAll() =>
|
||||
[
|
||||
new ValidCombination
|
||||
new()
|
||||
{
|
||||
Id = 10,
|
||||
Name = "Work Order",
|
||||
@@ -62,7 +62,7 @@ public class ValidCombination
|
||||
ItemOperationMis = false,
|
||||
ExtractMis = false
|
||||
},
|
||||
new ValidCombination
|
||||
new()
|
||||
{
|
||||
Id = 20,
|
||||
Name = "Component Lot",
|
||||
@@ -76,7 +76,7 @@ public class ValidCombination
|
||||
ItemOperationMis = false,
|
||||
ExtractMis = false
|
||||
},
|
||||
new ValidCombination
|
||||
new()
|
||||
{
|
||||
Id = 30,
|
||||
Name = "Time Span + Profit Center",
|
||||
@@ -90,7 +90,7 @@ public class ValidCombination
|
||||
ItemOperationMis = false,
|
||||
ExtractMis = false
|
||||
},
|
||||
new ValidCombination
|
||||
new()
|
||||
{
|
||||
Id = 40,
|
||||
Name = "Time Span + Work Center",
|
||||
@@ -104,7 +104,7 @@ public class ValidCombination
|
||||
ItemOperationMis = false,
|
||||
ExtractMis = false
|
||||
},
|
||||
new ValidCombination
|
||||
new()
|
||||
{
|
||||
Id = 50,
|
||||
Name = "Time Span + Operator",
|
||||
@@ -118,7 +118,7 @@ public class ValidCombination
|
||||
ItemOperationMis = false,
|
||||
ExtractMis = false
|
||||
},
|
||||
new ValidCombination
|
||||
new()
|
||||
{
|
||||
Id = 60,
|
||||
Name = "Time Span + Profit Center + Item Number",
|
||||
@@ -132,7 +132,7 @@ public class ValidCombination
|
||||
ItemOperationMis = false,
|
||||
ExtractMis = false
|
||||
},
|
||||
new ValidCombination
|
||||
new()
|
||||
{
|
||||
Id = 70,
|
||||
Name = "Time Span + Profit Center + Item/Operation/MIS",
|
||||
@@ -146,7 +146,7 @@ public class ValidCombination
|
||||
ItemOperationMis = true,
|
||||
ExtractMis = false
|
||||
},
|
||||
new ValidCombination
|
||||
new()
|
||||
{
|
||||
Id = 80,
|
||||
Name = "Time Span + Profit Center + Work Order + Item/Operation/MIS",
|
||||
@@ -160,7 +160,7 @@ public class ValidCombination
|
||||
ItemOperationMis = true,
|
||||
ExtractMis = false
|
||||
},
|
||||
new ValidCombination
|
||||
new()
|
||||
{
|
||||
Id = 90,
|
||||
Name = "Time Span + Profit Center + Extract MIS",
|
||||
@@ -174,7 +174,7 @@ public class ValidCombination
|
||||
ItemOperationMis = false,
|
||||
ExtractMis = true
|
||||
},
|
||||
new ValidCombination
|
||||
new()
|
||||
{
|
||||
Id = 100,
|
||||
Name = "Time Span + Work Center + Item Number",
|
||||
@@ -188,7 +188,7 @@ public class ValidCombination
|
||||
ItemOperationMis = false,
|
||||
ExtractMis = false
|
||||
},
|
||||
new ValidCombination
|
||||
new()
|
||||
{
|
||||
Id = 110,
|
||||
Name = "Time Span + Work Center + Extract MIS",
|
||||
@@ -202,7 +202,7 @@ public class ValidCombination
|
||||
ItemOperationMis = false,
|
||||
ExtractMis = true
|
||||
},
|
||||
new ValidCombination
|
||||
new()
|
||||
{
|
||||
Id = 120,
|
||||
Name = "Time Span + Work Center + Item/Operation/MIS",
|
||||
@@ -216,7 +216,7 @@ public class ValidCombination
|
||||
ItemOperationMis = true,
|
||||
ExtractMis = false
|
||||
},
|
||||
new ValidCombination
|
||||
new()
|
||||
{
|
||||
Id = 130,
|
||||
Name = "Time Span + Work Center + Work Order + Item/Operation/MIS",
|
||||
@@ -230,7 +230,7 @@ public class ValidCombination
|
||||
ItemOperationMis = true,
|
||||
ExtractMis = false
|
||||
},
|
||||
new ValidCombination
|
||||
new()
|
||||
{
|
||||
Id = 140,
|
||||
Name = "Time Span + Item Number",
|
||||
@@ -244,7 +244,7 @@ public class ValidCombination
|
||||
ItemOperationMis = false,
|
||||
ExtractMis = false
|
||||
},
|
||||
new ValidCombination
|
||||
new()
|
||||
{
|
||||
Id = 150,
|
||||
Name = "Time Span + Work Center + Operator",
|
||||
@@ -258,7 +258,7 @@ public class ValidCombination
|
||||
ItemOperationMis = false,
|
||||
ExtractMis = false
|
||||
},
|
||||
new ValidCombination
|
||||
new()
|
||||
{
|
||||
Id = 160,
|
||||
Name = "Time Span + Profit Center + Operator",
|
||||
|
||||
@@ -107,7 +107,7 @@ else if (_config is not null)
|
||||
<!-- Common Pipeline Info -->
|
||||
<RadzenRow Gap="1rem" class="rz-mb-4">
|
||||
<!-- Source Card -->
|
||||
<RadzenColumn Size="4">
|
||||
<RadzenColumn Size="6">
|
||||
<RadzenCard Style="height: 100%;">
|
||||
<RadzenText TextStyle="TextStyle.H6" class="rz-mb-2">Source</RadzenText>
|
||||
<p><strong>Connection:</strong>
|
||||
@@ -127,21 +127,11 @@ else if (_config is not null)
|
||||
break;
|
||||
}
|
||||
</p>
|
||||
@if (_config.Source.Parameters.Count > 0)
|
||||
{
|
||||
<p><strong>Parameters:</strong></p>
|
||||
<ul>
|
||||
@foreach (var param in _config.Source.Parameters)
|
||||
{
|
||||
<li>@param.Name (@(param.Format ?? "default"))</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</RadzenCard>
|
||||
</RadzenColumn>
|
||||
|
||||
<!-- Destination Card -->
|
||||
<RadzenColumn Size="4">
|
||||
<RadzenColumn Size="6">
|
||||
<RadzenCard Style="height: 100%;">
|
||||
<RadzenText TextStyle="TextStyle.H6" class="rz-mb-2">Destination</RadzenText>
|
||||
<p><strong>Table:</strong> @_config.Destination.Table</p>
|
||||
@@ -151,67 +141,36 @@ else if (_config is not null)
|
||||
</p>
|
||||
@if (_config.Destination.MatchColumns?.Count > 0)
|
||||
{
|
||||
<p><strong>Match Columns:</strong> @string.Join(", ", _config.Destination.MatchColumns)</p>
|
||||
<p><strong>Match Columns:</strong></p>
|
||||
<ul>
|
||||
@foreach (var col in _config.Destination.MatchColumns)
|
||||
{
|
||||
<li>@col</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
@if (_config.Destination.ExcludeFromUpdate?.Count > 0)
|
||||
{
|
||||
<p><strong>Exclude:</strong> @string.Join(", ", _config.Destination.ExcludeFromUpdate)</p>
|
||||
}
|
||||
</RadzenCard>
|
||||
</RadzenColumn>
|
||||
|
||||
<!-- Scripts Card -->
|
||||
<RadzenColumn Size="4">
|
||||
<RadzenCard Style="height: 100%;">
|
||||
<RadzenText TextStyle="TextStyle.H6" class="rz-mb-2">Scripts</RadzenText>
|
||||
<p><strong>Pre-Scripts:</strong> @_config.PreScriptCount</p>
|
||||
<p><strong>Post-Scripts:</strong> @_config.PostScriptCount</p>
|
||||
@if (_config.PreScripts?.Count > 0)
|
||||
{
|
||||
<RadzenText TextStyle="TextStyle.Subtitle2" class="rz-mt-2">Pre-Scripts:</RadzenText>
|
||||
@for (int i = 0; i < _config.PreScripts.Count; i++)
|
||||
{
|
||||
var script = _config.PreScripts[i];
|
||||
var index = i + 1;
|
||||
<div>
|
||||
<RadzenButton Text="@($"Script {index}")" Icon="code" ButtonStyle="ButtonStyle.Light" Size="ButtonSize.Small"
|
||||
Click="@(() => ShowSqlModal($"Pre-Script {index}", script))" />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@if (_config.PostScripts?.Count > 0)
|
||||
{
|
||||
<RadzenText TextStyle="TextStyle.Subtitle2" class="rz-mt-2">Post-Scripts:</RadzenText>
|
||||
@for (int i = 0; i < _config.PostScripts.Count; i++)
|
||||
{
|
||||
var script = _config.PostScripts[i];
|
||||
var index = i + 1;
|
||||
<div>
|
||||
<RadzenButton Text="@($"Script {index}")" Icon="code" ButtonStyle="ButtonStyle.Light" Size="ButtonSize.Small"
|
||||
Click="@(() => ShowSqlModal($"Post-Script {index}", script))" />
|
||||
</div>
|
||||
}
|
||||
<p><strong>Exclude:</strong></p>
|
||||
<ul>
|
||||
@foreach (var col in _config.Destination.ExcludeFromUpdate)
|
||||
{
|
||||
<li>@col</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</RadzenCard>
|
||||
</RadzenColumn>
|
||||
</RadzenRow>
|
||||
|
||||
<!-- Schedule Sections -->
|
||||
<PipelineScheduleSection ScheduleType="UpdateTypes.Mass" Config="@_config.Schedules.Mass"
|
||||
QueryPreview="@_config.Source.MassQueryPreview" FullQuery="@_config.Source.MassQuery"
|
||||
OnViewQuery="@(q => ShowSqlModal("Mass Query", q))" />
|
||||
<PipelineScheduleSection ScheduleType="UpdateTypes.Mass" Config="@_config.Schedules.Mass" />
|
||||
|
||||
<PipelineScheduleSection ScheduleType="UpdateTypes.Daily" Config="@_config.Schedules.Daily"
|
||||
QueryPreview="@_config.Source.QueryPreview" FullQuery="@_config.Source.Query"
|
||||
OnViewQuery="@(q => ShowSqlModal("Daily Query", q))" />
|
||||
<PipelineScheduleSection ScheduleType="UpdateTypes.Daily" Config="@_config.Schedules.Daily" />
|
||||
|
||||
<PipelineScheduleSection ScheduleType="UpdateTypes.Hourly" Config="@_config.Schedules.Hourly"
|
||||
QueryPreview="@_config.Source.QueryPreview" FullQuery="@_config.Source.Query"
|
||||
OnViewQuery="@(q => ShowSqlModal("Hourly Query", q))" />
|
||||
<PipelineScheduleSection ScheduleType="UpdateTypes.Hourly" Config="@_config.Schedules.Hourly" />
|
||||
}
|
||||
|
||||
<SqlQueryModal @bind-Visible="_showSqlModal" Title="@_sqlModalTitle" Sql="@_sqlModalContent" />
|
||||
|
||||
@code {
|
||||
private List<string> _pipelineNames = [];
|
||||
private string? _selectedPipeline;
|
||||
@@ -220,10 +179,6 @@ else if (_config is not null)
|
||||
private List<PipelineScheduleStatusDto> _statuses = [];
|
||||
private List<PipelineExecutionDto> _executions = [];
|
||||
|
||||
private bool _showSqlModal;
|
||||
private string? _sqlModalTitle;
|
||||
private string? _sqlModalContent;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var result = await PipelineApi.GetPipelineNamesAsync();
|
||||
@@ -271,13 +226,6 @@ else if (_config is not null)
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowSqlModal(string title, string sql)
|
||||
{
|
||||
_sqlModalTitle = $"{title} - {_selectedPipeline}";
|
||||
_sqlModalContent = sql;
|
||||
_showSqlModal = true;
|
||||
}
|
||||
|
||||
private static string FormatDuration(TimeSpan? duration)
|
||||
{
|
||||
if (!duration.HasValue) return "-";
|
||||
|
||||
@@ -74,16 +74,7 @@
|
||||
if (loginResult.Success && loginResult.User is not null)
|
||||
{
|
||||
// Notify auth state provider of successful login
|
||||
var userViewModel = new JdeScoping.Client.Models.UserInfoViewModel
|
||||
{
|
||||
Username = loginResult.User.Username,
|
||||
FirstName = loginResult.User.FirstName,
|
||||
LastName = loginResult.User.LastName,
|
||||
DisplayName = loginResult.User.DisplayName,
|
||||
EmailAddress = loginResult.User.EmailAddress,
|
||||
Title = loginResult.User.Title
|
||||
};
|
||||
_ = AuthStateProvider.MarkUserAsAuthenticated(userViewModel);
|
||||
_ = AuthStateProvider.MarkUserAsAuthenticated(loginResult.User);
|
||||
|
||||
var returnUrl = string.IsNullOrEmpty(ReturnUrl) ? "/" : ReturnUrl;
|
||||
NavigationManager.NavigateTo(returnUrl);
|
||||
|
||||
@@ -315,7 +315,7 @@ else
|
||||
await HubConnection.StartAsync();
|
||||
}
|
||||
|
||||
private void HandleSearchUpdate(SearchUpdate update)
|
||||
private void HandleSearchUpdate(SearchUpdateDto update)
|
||||
{
|
||||
if (update.Id == _search.Id)
|
||||
{
|
||||
|
||||
@@ -120,7 +120,7 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleSearchUpdate(SearchUpdate update)
|
||||
private void HandleSearchUpdate(SearchUpdateDto update)
|
||||
{
|
||||
InvokeAsync(() =>
|
||||
{
|
||||
@@ -162,7 +162,7 @@ else
|
||||
});
|
||||
}
|
||||
|
||||
private void HandleStatusUpdate(StatusUpdate update)
|
||||
private void HandleStatusUpdate(StatusUpdateDto update)
|
||||
{
|
||||
InvokeAsync(() =>
|
||||
{
|
||||
|
||||
@@ -93,7 +93,7 @@ else
|
||||
await HubConnection.StartAsync();
|
||||
}
|
||||
|
||||
private void HandleSearchUpdate(SearchUpdate update)
|
||||
private void HandleSearchUpdate(SearchUpdateDto update)
|
||||
{
|
||||
InvokeAsync(() =>
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using JdeScoping.Core.ApiContracts;
|
||||
using JdeScoping.Core.ApiContracts.Auth;
|
||||
using JdeScoping.Core.ApiContracts.Results;
|
||||
using JdeScoping.Core.Models;
|
||||
using JdeScoping.Core.Models.Auth;
|
||||
|
||||
namespace JdeScoping.Client.Services;
|
||||
@@ -21,6 +21,6 @@ public class AuthApiClient : ApiClientBase, IAuthApiClient
|
||||
public Task<ApiResult<Unit>> LogoutAsync(CancellationToken ct = default)
|
||||
=> PostAsync<Unit>(ApiRoutes.Auth.Logout, ct);
|
||||
|
||||
public Task<ApiResult<UserInfo>> GetCurrentUserAsync(CancellationToken ct = default)
|
||||
=> GetAsync<UserInfo>(ApiRoutes.Auth.Me, ct);
|
||||
public Task<ApiResult<UserInfoDto>> GetCurrentUserAsync(CancellationToken ct = default)
|
||||
=> GetAsync<UserInfoDto>(ApiRoutes.Auth.Me, ct);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Net.Http.Json;
|
||||
using JdeScoping.Client.Auth;
|
||||
using JdeScoping.Client.Models;
|
||||
using JdeScoping.Core.Models.Auth;
|
||||
|
||||
namespace JdeScoping.Client.Services;
|
||||
@@ -43,17 +42,8 @@ public class AuthService : IAuthService
|
||||
|
||||
if (result.Success && result.User is not null)
|
||||
{
|
||||
// Notify auth state provider of the login
|
||||
var userViewModel = new UserInfoViewModel
|
||||
{
|
||||
Username = result.User.Username,
|
||||
FirstName = result.User.FirstName,
|
||||
LastName = result.User.LastName,
|
||||
DisplayName = result.User.DisplayName,
|
||||
EmailAddress = result.User.EmailAddress,
|
||||
Title = result.User.Title
|
||||
};
|
||||
await _authStateProvider.MarkUserAsAuthenticated(userViewModel);
|
||||
// LoginResultModel.User is already UserInfoDto - pass directly
|
||||
await _authStateProvider.MarkUserAsAuthenticated(result.User);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using JdeScoping.Client.Models;
|
||||
using JdeScoping.Core.ApiContracts.SignalR;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
|
||||
@@ -13,8 +13,8 @@ public class HubConnectionService : IHubConnectionService, IAsyncDisposable
|
||||
private readonly NavigationManager _navigationManager;
|
||||
private HubConnection? _hubConnection;
|
||||
|
||||
public event Action<SearchUpdate>? OnSearchUpdate;
|
||||
public event Action<StatusUpdate>? OnStatusUpdate;
|
||||
public event Action<SearchUpdateDto>? OnSearchUpdate;
|
||||
public event Action<StatusUpdateDto>? OnStatusUpdate;
|
||||
|
||||
public bool IsConnected => _hubConnection?.State == HubConnectionState.Connected;
|
||||
|
||||
@@ -43,12 +43,12 @@ public class HubConnectionService : IHubConnectionService, IAsyncDisposable
|
||||
])
|
||||
.Build();
|
||||
|
||||
_hubConnection.On<SearchUpdate>("searchUpdate", update =>
|
||||
_hubConnection.On<SearchUpdateDto>("searchUpdate", update =>
|
||||
{
|
||||
OnSearchUpdate?.Invoke(update);
|
||||
});
|
||||
|
||||
_hubConnection.On<StatusUpdate>("statusUpdate", update =>
|
||||
_hubConnection.On<StatusUpdateDto>("statusUpdate", update =>
|
||||
{
|
||||
OnStatusUpdate?.Invoke(update);
|
||||
});
|
||||
@@ -92,7 +92,7 @@ public class HubConnectionService : IHubConnectionService, IAsyncDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<StatusUpdate?> GetCachedStatusAsync()
|
||||
public async Task<StatusUpdateDto?> GetCachedStatusAsync()
|
||||
{
|
||||
if (_hubConnection == null || _hubConnection.State != HubConnectionState.Connected)
|
||||
{
|
||||
@@ -101,7 +101,7 @@ public class HubConnectionService : IHubConnectionService, IAsyncDisposable
|
||||
|
||||
try
|
||||
{
|
||||
return await _hubConnection.InvokeAsync<StatusUpdate>("GetCachedStatus");
|
||||
return await _hubConnection.InvokeAsync<StatusUpdateDto>("GetCachedStatus");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using JdeScoping.Client.Models;
|
||||
using JdeScoping.Core.ApiContracts.SignalR;
|
||||
|
||||
namespace JdeScoping.Client.Services;
|
||||
|
||||
@@ -10,12 +10,12 @@ public interface IHubConnectionService
|
||||
/// <summary>
|
||||
/// Event fired when a search update is received.
|
||||
/// </summary>
|
||||
event Action<SearchUpdate>? OnSearchUpdate;
|
||||
event Action<SearchUpdateDto>? OnSearchUpdate;
|
||||
|
||||
/// <summary>
|
||||
/// Event fired when a processor status update is received.
|
||||
/// </summary>
|
||||
event Action<StatusUpdate>? OnStatusUpdate;
|
||||
event Action<StatusUpdateDto>? OnStatusUpdate;
|
||||
|
||||
/// <summary>
|
||||
/// Starts the SignalR connection.
|
||||
@@ -30,7 +30,7 @@ public interface IHubConnectionService
|
||||
/// <summary>
|
||||
/// Gets the cached processor status from the server.
|
||||
/// </summary>
|
||||
Task<StatusUpdate?> GetCachedStatusAsync();
|
||||
Task<StatusUpdateDto?> GetCachedStatusAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current connection state.
|
||||
|
||||
@@ -21,5 +21,7 @@
|
||||
@using JdeScoping.Client.Models
|
||||
@using JdeScoping.Client.Pages
|
||||
@using JdeScoping.Client.Services
|
||||
@using JdeScoping.Core.ApiContracts.Auth
|
||||
@using JdeScoping.Core.ApiContracts.SignalR
|
||||
@using JdeScoping.Core.ViewModels
|
||||
@using ClientSearchViewModel = JdeScoping.Client.Models.SearchViewModel
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
using JdeScoping.Core.Models;
|
||||
|
||||
namespace JdeScoping.Core.ApiContracts.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// API response DTO for authenticated user information.
|
||||
/// </summary>
|
||||
public record UserInfoDto
|
||||
{
|
||||
/// <summary>
|
||||
/// User's login username.
|
||||
/// </summary>
|
||||
public string Username { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// User's first name.
|
||||
/// </summary>
|
||||
public string FirstName { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// User's last name.
|
||||
/// </summary>
|
||||
public string LastName { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// User's display name (computed on server).
|
||||
/// </summary>
|
||||
public string DisplayName { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// User's organization title.
|
||||
/// </summary>
|
||||
public string Title { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// User's email address.
|
||||
/// </summary>
|
||||
public string EmailAddress { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a UserInfoDto from a UserInfo entity.
|
||||
/// </summary>
|
||||
/// <param name="userInfo">The UserInfo entity to convert.</param>
|
||||
/// <returns>A new UserInfoDto instance.</returns>
|
||||
public static UserInfoDto FromUserInfo(UserInfo userInfo) => new()
|
||||
{
|
||||
Username = userInfo.Username,
|
||||
FirstName = userInfo.FirstName,
|
||||
LastName = userInfo.LastName,
|
||||
DisplayName = userInfo.DisplayName,
|
||||
Title = userInfo.Title,
|
||||
EmailAddress = userInfo.EmailAddress
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
using JdeScoping.Core.ApiContracts.Auth;
|
||||
using JdeScoping.Core.ApiContracts.Results;
|
||||
using JdeScoping.Core.Models;
|
||||
using JdeScoping.Core.Models.Auth;
|
||||
|
||||
namespace JdeScoping.Core.ApiContracts;
|
||||
@@ -19,5 +19,5 @@ public interface IAuthApiClient
|
||||
Task<ApiResult<Unit>> LogoutAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>Gets the current authenticated user's information.</summary>
|
||||
Task<ApiResult<UserInfo>> GetCurrentUserAsync(CancellationToken ct = default);
|
||||
Task<ApiResult<UserInfoDto>> GetCurrentUserAsync(CancellationToken ct = default);
|
||||
}
|
||||
|
||||
@@ -44,4 +44,8 @@ public record PipelineScheduleDto(
|
||||
bool ReIndex,
|
||||
bool IntervalIsOverride,
|
||||
bool PrePurgeIsOverride,
|
||||
bool ReIndexIsOverride);
|
||||
bool ReIndexIsOverride,
|
||||
string? Query,
|
||||
List<PipelineParameterDto> Parameters,
|
||||
List<string>? PreScripts,
|
||||
List<string>? PostScripts);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace JdeScoping.Core.ApiContracts.Pipelines;
|
||||
|
||||
using JdeScoping.Core.Models.Enums;
|
||||
using Models.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Pipeline execution history.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace JdeScoping.Core.ApiContracts.Pipelines;
|
||||
|
||||
using JdeScoping.Core.Models.Enums;
|
||||
using Models.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Pipeline schedule status for each update type.
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
using JdeScoping.Core.Models.Search;
|
||||
|
||||
namespace JdeScoping.Core.ApiContracts.SignalR;
|
||||
|
||||
/// <summary>
|
||||
/// SignalR message DTO for search status updates.
|
||||
/// </summary>
|
||||
public record SearchUpdateDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Search PK ID.
|
||||
/// </summary>
|
||||
public int Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// User-friendly name for the search.
|
||||
/// </summary>
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// User name of user that submitted the search.
|
||||
/// </summary>
|
||||
public string UserName { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Current search status (serialized as string).
|
||||
/// </summary>
|
||||
public string Status { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp search was submitted.
|
||||
/// </summary>
|
||||
public DateTime? SubmitDt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp search was started.
|
||||
/// </summary>
|
||||
public DateTime? StartDt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp search was completed.
|
||||
/// </summary>
|
||||
public DateTime? EndDt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a SearchUpdateDto from a Search entity.
|
||||
/// </summary>
|
||||
/// <param name="search">The Search entity to convert.</param>
|
||||
/// <returns>A new SearchUpdateDto instance.</returns>
|
||||
public static SearchUpdateDto FromSearch(Search search) => new()
|
||||
{
|
||||
Id = search.Id,
|
||||
Name = search.Name,
|
||||
UserName = search.UserName,
|
||||
Status = search.Status.ToString(),
|
||||
SubmitDt = search.SubmitDt,
|
||||
StartDt = search.StartDt,
|
||||
EndDt = search.EndDt
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace JdeScoping.Core.ApiContracts.SignalR;
|
||||
|
||||
/// <summary>
|
||||
/// SignalR message DTO for processor status updates.
|
||||
/// </summary>
|
||||
public record StatusUpdateDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Status message to display.
|
||||
/// </summary>
|
||||
public string Message { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp when message was generated.
|
||||
/// </summary>
|
||||
public DateTime? Timestamp { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a StatusUpdateDto with the current timestamp.
|
||||
/// </summary>
|
||||
/// <param name="message">Status message.</param>
|
||||
/// <returns>A new StatusUpdateDto instance.</returns>
|
||||
public static StatusUpdateDto Create(string message) => new()
|
||||
{
|
||||
Message = message,
|
||||
Timestamp = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
using JdeScoping.Core.Models;
|
||||
using JdeScoping.Core.Models.Inventory;
|
||||
using JdeScoping.Core.ViewModels;
|
||||
|
||||
namespace JdeScoping.Core.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for Item entity
|
||||
/// </summary>
|
||||
public static class ItemExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts an Item entity to its ViewModel projection
|
||||
/// </summary>
|
||||
/// <param name="item">Item entity to convert</param>
|
||||
/// <returns>ItemViewModel projection</returns>
|
||||
public static ItemViewModel ToViewModel(this Item item)
|
||||
{
|
||||
return new ItemViewModel
|
||||
{
|
||||
ItemNumber = item.ItemNumber,
|
||||
Description = item.Description
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a collection of Item entities to ViewModels
|
||||
/// </summary>
|
||||
/// <param name="items">Collection of Item entities</param>
|
||||
/// <returns>Collection of ItemViewModel projections</returns>
|
||||
public static IEnumerable<ItemViewModel> ToViewModels(this IEnumerable<Item> items)
|
||||
{
|
||||
return items.Select(i => i.ToViewModel());
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
using JdeScoping.Core.Models;
|
||||
using JdeScoping.Core.Models.Organization;
|
||||
using JdeScoping.Core.ViewModels;
|
||||
|
||||
namespace JdeScoping.Core.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for JdeUser entity
|
||||
/// </summary>
|
||||
public static class JdeUserExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a JdeUser entity to its ViewModel projection
|
||||
/// </summary>
|
||||
/// <param name="jdeUser">JdeUser entity to convert</param>
|
||||
/// <returns>JdeUserViewModel projection</returns>
|
||||
public static JdeUserViewModel ToViewModel(this JdeUser jdeUser)
|
||||
{
|
||||
return new JdeUserViewModel
|
||||
{
|
||||
AddressNumber = jdeUser.AddressNumber,
|
||||
UserId = jdeUser.UserId,
|
||||
FullName = jdeUser.FullName
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a collection of JdeUser entities to ViewModels
|
||||
/// </summary>
|
||||
/// <param name="jdeUsers">Collection of JdeUser entities</param>
|
||||
/// <returns>Collection of JdeUserViewModel projections</returns>
|
||||
public static IEnumerable<JdeUserViewModel> ToViewModels(this IEnumerable<JdeUser> jdeUsers)
|
||||
{
|
||||
return jdeUsers.Select(u => u.ToViewModel());
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
using JdeScoping.Core.Models;
|
||||
using JdeScoping.Core.Models.Inventory;
|
||||
using JdeScoping.Core.ViewModels;
|
||||
|
||||
namespace JdeScoping.Core.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for Lot entity
|
||||
/// </summary>
|
||||
public static class LotExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a Lot entity to its ViewModel projection
|
||||
/// </summary>
|
||||
/// <param name="lot">Lot entity to convert</param>
|
||||
/// <returns>LotViewModel projection</returns>
|
||||
public static LotViewModel ToViewModel(this Lot lot)
|
||||
{
|
||||
return new LotViewModel
|
||||
{
|
||||
LotNumber = lot.LotNumber,
|
||||
ItemNumber = lot.ItemNumber
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a collection of Lot entities to ViewModels
|
||||
/// </summary>
|
||||
/// <param name="lots">Collection of Lot entities</param>
|
||||
/// <returns>Collection of LotViewModel projections</returns>
|
||||
public static IEnumerable<LotViewModel> ToViewModels(this IEnumerable<Lot> lots)
|
||||
{
|
||||
return lots.Select(l => l.ToViewModel());
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
using JdeScoping.Core.Models;
|
||||
using JdeScoping.Core.Models.Organization;
|
||||
using JdeScoping.Core.ViewModels;
|
||||
|
||||
namespace JdeScoping.Core.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for ProfitCenter entity
|
||||
/// </summary>
|
||||
public static class ProfitCenterExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a ProfitCenter entity to its ViewModel projection
|
||||
/// </summary>
|
||||
/// <param name="profitCenter">ProfitCenter entity to convert</param>
|
||||
/// <returns>ProfitCenterViewModel projection</returns>
|
||||
public static ProfitCenterViewModel ToViewModel(this ProfitCenter profitCenter)
|
||||
{
|
||||
return new ProfitCenterViewModel
|
||||
{
|
||||
Code = profitCenter.Code,
|
||||
Description = profitCenter.Description
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a collection of ProfitCenter entities to ViewModels
|
||||
/// </summary>
|
||||
/// <param name="profitCenters">Collection of ProfitCenter entities</param>
|
||||
/// <returns>Collection of ProfitCenterViewModel projections</returns>
|
||||
public static IEnumerable<ProfitCenterViewModel> ToViewModels(this IEnumerable<ProfitCenter> profitCenters)
|
||||
{
|
||||
return profitCenters.Select(pc => pc.ToViewModel());
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
using JdeScoping.Core.Models;
|
||||
using JdeScoping.Core.Models.Organization;
|
||||
using JdeScoping.Core.ViewModels;
|
||||
|
||||
namespace JdeScoping.Core.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for WorkCenter entity
|
||||
/// </summary>
|
||||
public static class WorkCenterExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a WorkCenter entity to its ViewModel projection
|
||||
/// </summary>
|
||||
/// <param name="workCenter">WorkCenter entity to convert</param>
|
||||
/// <returns>WorkCenterViewModel projection</returns>
|
||||
public static WorkCenterViewModel ToViewModel(this WorkCenter workCenter)
|
||||
{
|
||||
return new WorkCenterViewModel
|
||||
{
|
||||
Code = workCenter.Code,
|
||||
Description = workCenter.Description
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a collection of WorkCenter entities to ViewModels
|
||||
/// </summary>
|
||||
/// <param name="workCenters">Collection of WorkCenter entities</param>
|
||||
/// <returns>Collection of WorkCenterViewModel projections</returns>
|
||||
public static IEnumerable<WorkCenterViewModel> ToViewModels(this IEnumerable<WorkCenter> workCenters)
|
||||
{
|
||||
return workCenters.Select(wc => wc.ToViewModel());
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
using JdeScoping.Core.Models;
|
||||
using JdeScoping.Core.Models.WorkOrders;
|
||||
using JdeScoping.Core.ViewModels;
|
||||
|
||||
namespace JdeScoping.Core.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for WorkOrder entity
|
||||
/// </summary>
|
||||
public static class WorkOrderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a WorkOrder entity to its ViewModel projection
|
||||
/// </summary>
|
||||
/// <param name="workOrder">WorkOrder entity to convert</param>
|
||||
/// <returns>WorkOrderViewModel projection</returns>
|
||||
public static WorkOrderViewModel ToViewModel(this WorkOrder workOrder)
|
||||
{
|
||||
return new WorkOrderViewModel
|
||||
{
|
||||
WorkOrderNumber = workOrder.WorkOrderNumber,
|
||||
ItemNumber = workOrder.ItemNumber
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a collection of WorkOrder entities to ViewModels
|
||||
/// </summary>
|
||||
/// <param name="workOrders">Collection of WorkOrder entities</param>
|
||||
/// <returns>Collection of WorkOrderViewModel projections</returns>
|
||||
public static IEnumerable<WorkOrderViewModel> ToViewModels(this IEnumerable<WorkOrder> workOrders)
|
||||
{
|
||||
return workOrders.Select(wo => wo.ToViewModel());
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using JdeScoping.Core.Models;
|
||||
using JdeScoping.Core.ApiContracts.Auth;
|
||||
|
||||
namespace JdeScoping.Core.Models.Auth;
|
||||
|
||||
@@ -11,4 +11,4 @@ namespace JdeScoping.Core.Models.Auth;
|
||||
public record LoginResultModel(
|
||||
bool Success,
|
||||
string? ErrorMessage,
|
||||
UserInfo? User);
|
||||
UserInfoDto? User);
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
namespace JdeScoping.Core.Models.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// Database column specification
|
||||
/// </summary>
|
||||
public class ColumnSpec
|
||||
{
|
||||
/// <summary>
|
||||
/// Column name
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Column definition (SQL type and constraints)
|
||||
/// </summary>
|
||||
public string Definition { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Default constructor
|
||||
/// </summary>
|
||||
public ColumnSpec()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor with name and definition
|
||||
/// </summary>
|
||||
/// <param name="name">Column name</param>
|
||||
/// <param name="definition">Column definition</param>
|
||||
public ColumnSpec(string name, string definition)
|
||||
{
|
||||
Name = name;
|
||||
Definition = definition;
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
namespace JdeScoping.Core.Models.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// Database table specification for dynamic SQL generation
|
||||
/// </summary>
|
||||
public class TableSpec
|
||||
{
|
||||
/// <summary>
|
||||
/// Table name
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Temporary table name (computed as # + Name)
|
||||
/// </summary>
|
||||
public string TempTableName => $"#{Name}";
|
||||
|
||||
/// <summary>
|
||||
/// Table columns
|
||||
/// </summary>
|
||||
public List<ColumnSpec> Columns { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Table columns that form the primary key
|
||||
/// </summary>
|
||||
public List<ColumnSpec> PrimaryKey { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Default constructor
|
||||
/// </summary>
|
||||
public TableSpec()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor with table name
|
||||
/// </summary>
|
||||
/// <param name="name">Table name</param>
|
||||
public TableSpec(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates SQL for creating an index on the primary key (stub)
|
||||
/// </summary>
|
||||
/// <returns>SQL CREATE INDEX statement</returns>
|
||||
public string GenerateIndex()
|
||||
{
|
||||
// Stub implementation - to be expanded based on spec
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates SQL for dropping the table (stub)
|
||||
/// </summary>
|
||||
/// <returns>SQL DROP TABLE statement</returns>
|
||||
public string GenerateDrop()
|
||||
{
|
||||
// Stub implementation - to be expanded based on spec
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates SQL for creating the table (stub)
|
||||
/// </summary>
|
||||
/// <returns>SQL CREATE TABLE statement</returns>
|
||||
public string GenerateCreate()
|
||||
{
|
||||
// Stub implementation - to be expanded based on spec
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a column specification by name (stub)
|
||||
/// </summary>
|
||||
/// <param name="columnName">Name of column to find</param>
|
||||
/// <returns>ColumnSpec or null if not found</returns>
|
||||
public ColumnSpec? GetColumn(string columnName)
|
||||
{
|
||||
return Columns.Find(c => c.Name == columnName);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
namespace JdeScoping.Core.Models.Inventory;
|
||||
|
||||
/// <summary>
|
||||
/// JDE lot location entity
|
||||
/// </summary>
|
||||
public class LotLocation
|
||||
{
|
||||
/// <summary>
|
||||
/// Lot unique number
|
||||
/// </summary>
|
||||
public string LotNumber { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Short item number
|
||||
/// </summary>
|
||||
public long ShortItemNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Business unit unique code
|
||||
/// </summary>
|
||||
public string BranchCode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Location code where lot is located
|
||||
/// </summary>
|
||||
public string Location { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp of last update to record
|
||||
/// </summary>
|
||||
public DateTime? LastUpdateDt { get; set; }
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace JdeScoping.Client.Models;
|
||||
namespace JdeScoping.Core.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// View model for component lot filter.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace JdeScoping.Client.Models;
|
||||
namespace JdeScoping.Core.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// View model for operator filter.
|
||||
@@ -1,29 +0,0 @@
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using JdeScoping.DataSync.Etl.Destinations;
|
||||
using JdeScoping.DataSync.Etl.Pipeline;
|
||||
using JdeScoping.DataSync.Dev.Sources;
|
||||
|
||||
namespace JdeScoping.DataSync.Dev;
|
||||
|
||||
/// <summary>
|
||||
/// Development ETL pipeline for the Branch table.
|
||||
/// </summary>
|
||||
public static class BranchDevEtl
|
||||
{
|
||||
public static readonly string TableName = "Branch";
|
||||
public static readonly string CacheFileName = "branch.pb.zstd";
|
||||
|
||||
public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(connectionFactory);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cacheFilePath))
|
||||
throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath));
|
||||
|
||||
return new EtlPipelineBuilder()
|
||||
.WithName($"{TableName}_Dev")
|
||||
.WithSource(new ProtobufZstdFileSource(cacheFilePath))
|
||||
.WithDestination(new DbBulkImportDestination(connectionFactory, TableName))
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
namespace JdeScoping.DataSync.Dev.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Root configuration for development ETL pipelines.
|
||||
/// </summary>
|
||||
public record DevPipelinesRoot(
|
||||
DevPipelineSettings? Settings,
|
||||
Dictionary<string, DevPipelineConfig> Pipelines)
|
||||
{
|
||||
public DevPipelineSettings EffectiveSettings => Settings ?? new DevPipelineSettings();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Settings for development pipeline execution.
|
||||
/// </summary>
|
||||
public record DevPipelineSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Size categories for parallel/sequential execution.
|
||||
/// Very large tables run sequentially to avoid IO contention.
|
||||
/// </summary>
|
||||
public SizeCategories? SizeCategories { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Table size categorization for execution strategy.
|
||||
/// </summary>
|
||||
public record SizeCategories
|
||||
{
|
||||
/// <summary>Small tables (less than 1 MB) - run in parallel.</summary>
|
||||
public List<string>? Small { get; init; }
|
||||
|
||||
/// <summary>Medium tables (1-20 MB) - run in parallel.</summary>
|
||||
public List<string>? Medium { get; init; }
|
||||
|
||||
/// <summary>Large tables (20-200 MB) - run in parallel.</summary>
|
||||
public List<string>? Large { get; init; }
|
||||
|
||||
/// <summary>Very large tables (200+ MB) - run sequentially.</summary>
|
||||
public List<string>? VeryLarge { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for a single development ETL pipeline.
|
||||
/// </summary>
|
||||
public record DevPipelineConfig(
|
||||
DevSourceConfig Source,
|
||||
DevDestinationConfig Destination);
|
||||
|
||||
/// <summary>
|
||||
/// Source configuration for a development pipeline (file-based).
|
||||
/// </summary>
|
||||
public record DevSourceConfig(string FileName);
|
||||
|
||||
/// <summary>
|
||||
/// Destination configuration for a development pipeline.
|
||||
/// </summary>
|
||||
public record DevDestinationConfig(string Table);
|
||||
@@ -0,0 +1,29 @@
|
||||
using JdeScoping.DataSync.Etl.Pipeline;
|
||||
|
||||
namespace JdeScoping.DataSync.Dev.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating development ETL pipelines from JSON configuration.
|
||||
/// </summary>
|
||||
public interface IDevEtlPipelineFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the list of available table names.
|
||||
/// </summary>
|
||||
IEnumerable<string> GetAvailableTables();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a pipeline for the specified table.
|
||||
/// </summary>
|
||||
/// <param name="tableName">The table name.</param>
|
||||
/// <param name="cacheDirectory">The directory containing cache files.</param>
|
||||
/// <returns>The configured ETL pipeline.</returns>
|
||||
EtlPipeline GetPipeline(string tableName, string cacheDirectory);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a table is categorized as very large (should run sequentially).
|
||||
/// </summary>
|
||||
/// <param name="tableName">The table name.</param>
|
||||
/// <returns>True if the table is very large.</returns>
|
||||
bool IsVeryLargeTable(string tableName);
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Collections.Concurrent;
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using JdeScoping.DataSync.Etl.Pipeline;
|
||||
using JdeScoping.DataSync.Dev.Contracts;
|
||||
using JdeScoping.DataSync.Etl.Results;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -8,69 +7,20 @@ namespace JdeScoping.DataSync.Dev;
|
||||
|
||||
/// <summary>
|
||||
/// Registry for development ETL pipelines that load from cached protobuf files.
|
||||
/// Uses JSON configuration via IDevEtlPipelineFactory.
|
||||
/// </summary>
|
||||
public class DevEtlRegistry
|
||||
{
|
||||
private readonly IDbConnectionFactory _connectionFactory;
|
||||
private readonly IDevEtlPipelineFactory _pipelineFactory;
|
||||
private readonly string _cacheDirectory;
|
||||
private readonly ILogger<DevEtlRegistry>? _logger;
|
||||
|
||||
private readonly Dictionary<string, Func<IDbConnectionFactory, string, EtlPipeline>> _pipelineFactories = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
// Small tables (< 1 MB)
|
||||
[BranchDevEtl.TableName] = (factory, cacheDir) =>
|
||||
BranchDevEtl.Create(factory, Path.Combine(cacheDir, BranchDevEtl.CacheFileName)),
|
||||
[OrgHierarchyDevEtl.TableName] = (factory, cacheDir) =>
|
||||
OrgHierarchyDevEtl.Create(factory, Path.Combine(cacheDir, OrgHierarchyDevEtl.CacheFileName)),
|
||||
[WorkCenterDevEtl.TableName] = (factory, cacheDir) =>
|
||||
WorkCenterDevEtl.Create(factory, Path.Combine(cacheDir, WorkCenterDevEtl.CacheFileName)),
|
||||
[ProfitCenterDevEtl.TableName] = (factory, cacheDir) =>
|
||||
ProfitCenterDevEtl.Create(factory, Path.Combine(cacheDir, ProfitCenterDevEtl.CacheFileName)),
|
||||
// Medium tables (1-20 MB)
|
||||
[JdeUserDevEtl.TableName] = (factory, cacheDir) =>
|
||||
JdeUserDevEtl.Create(factory, Path.Combine(cacheDir, JdeUserDevEtl.CacheFileName)),
|
||||
[FunctionCodeDevEtl.TableName] = (factory, cacheDir) =>
|
||||
FunctionCodeDevEtl.Create(factory, Path.Combine(cacheDir, FunctionCodeDevEtl.CacheFileName)),
|
||||
[ItemDevEtl.TableName] = (factory, cacheDir) =>
|
||||
ItemDevEtl.Create(factory, Path.Combine(cacheDir, ItemDevEtl.CacheFileName)),
|
||||
[RouteMasterDevEtl.TableName] = (factory, cacheDir) =>
|
||||
RouteMasterDevEtl.Create(factory, Path.Combine(cacheDir, RouteMasterDevEtl.CacheFileName)),
|
||||
// Large tables (20-200 MB)
|
||||
[LotDevEtl.TableName] = (factory, cacheDir) =>
|
||||
LotDevEtl.Create(factory, Path.Combine(cacheDir, LotDevEtl.CacheFileName)),
|
||||
[MisDataDevEtl.TableName] = (factory, cacheDir) =>
|
||||
MisDataDevEtl.Create(factory, Path.Combine(cacheDir, MisDataDevEtl.CacheFileName)),
|
||||
[WorkOrderCurrDevEtl.TableName] = (factory, cacheDir) =>
|
||||
WorkOrderCurrDevEtl.Create(factory, Path.Combine(cacheDir, WorkOrderCurrDevEtl.CacheFileName)),
|
||||
[WorkOrderHistDevEtl.TableName] = (factory, cacheDir) =>
|
||||
WorkOrderHistDevEtl.Create(factory, Path.Combine(cacheDir, WorkOrderHistDevEtl.CacheFileName)),
|
||||
[LotUsageHistDevEtl.TableName] = (factory, cacheDir) =>
|
||||
LotUsageHistDevEtl.Create(factory, Path.Combine(cacheDir, LotUsageHistDevEtl.CacheFileName)),
|
||||
[WorkOrderComponentHistDevEtl.TableName] = (factory, cacheDir) =>
|
||||
WorkOrderComponentHistDevEtl.Create(factory, Path.Combine(cacheDir, WorkOrderComponentHistDevEtl.CacheFileName)),
|
||||
// Very large tables (200+ MB)
|
||||
[WorkOrderStepHistDevEtl.TableName] = (factory, cacheDir) =>
|
||||
WorkOrderStepHistDevEtl.Create(factory, Path.Combine(cacheDir, WorkOrderStepHistDevEtl.CacheFileName)),
|
||||
[WorkOrderComponentCurrDevEtl.TableName] = (factory, cacheDir) =>
|
||||
WorkOrderComponentCurrDevEtl.Create(factory, Path.Combine(cacheDir, WorkOrderComponentCurrDevEtl.CacheFileName)),
|
||||
[WorkOrderRoutingDevEtl.TableName] = (factory, cacheDir) =>
|
||||
WorkOrderRoutingDevEtl.Create(factory, Path.Combine(cacheDir, WorkOrderRoutingDevEtl.CacheFileName)),
|
||||
[LotUsageCurrDevEtl.TableName] = (factory, cacheDir) =>
|
||||
LotUsageCurrDevEtl.Create(factory, Path.Combine(cacheDir, LotUsageCurrDevEtl.CacheFileName)),
|
||||
[WorkOrderStepCurrDevEtl.TableName] = (factory, cacheDir) =>
|
||||
WorkOrderStepCurrDevEtl.Create(factory, Path.Combine(cacheDir, WorkOrderStepCurrDevEtl.CacheFileName)),
|
||||
[WorkOrderTimeHistDevEtl.TableName] = (factory, cacheDir) =>
|
||||
WorkOrderTimeHistDevEtl.Create(factory, Path.Combine(cacheDir, WorkOrderTimeHistDevEtl.CacheFileName)),
|
||||
[WorkOrderTimeCurrDevEtl.TableName] = (factory, cacheDir) =>
|
||||
WorkOrderTimeCurrDevEtl.Create(factory, Path.Combine(cacheDir, WorkOrderTimeCurrDevEtl.CacheFileName)),
|
||||
};
|
||||
|
||||
public DevEtlRegistry(
|
||||
IDbConnectionFactory connectionFactory,
|
||||
IDevEtlPipelineFactory pipelineFactory,
|
||||
string cacheDirectory,
|
||||
ILogger<DevEtlRegistry>? logger = null)
|
||||
{
|
||||
_connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory));
|
||||
_pipelineFactory = pipelineFactory ?? throw new ArgumentNullException(nameof(pipelineFactory));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cacheDirectory))
|
||||
throw new ArgumentException("Cache directory is required.", nameof(cacheDirectory));
|
||||
@@ -82,21 +32,13 @@ public class DevEtlRegistry
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetAvailableTables() => _pipelineFactories.Keys;
|
||||
|
||||
public EtlPipeline GetPipeline(string tableName)
|
||||
{
|
||||
if (!_pipelineFactories.TryGetValue(tableName, out var factory))
|
||||
throw new ArgumentException($"No pipeline registered for table '{tableName}'.", nameof(tableName));
|
||||
|
||||
return factory(_connectionFactory, _cacheDirectory);
|
||||
}
|
||||
public IEnumerable<string> GetAvailableTables() => _pipelineFactory.GetAvailableTables();
|
||||
|
||||
public async Task<PipelineResult> RunAsync(string tableName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger?.LogInformation("Running dev ETL for {TableName}", tableName);
|
||||
|
||||
var pipeline = GetPipeline(tableName);
|
||||
var pipeline = _pipelineFactory.GetPipeline(tableName, _cacheDirectory);
|
||||
var result = await pipeline.ExecuteAsync(cancellationToken);
|
||||
|
||||
if (result.Success)
|
||||
@@ -138,10 +80,10 @@ public class DevEtlRegistry
|
||||
|
||||
// Separate tables by size - run very large ones sequentially at the end
|
||||
var smallMediumTables = GetAvailableTables()
|
||||
.Where(t => !IsVeryLargeTable(t))
|
||||
.Where(t => !_pipelineFactory.IsVeryLargeTable(t))
|
||||
.ToList();
|
||||
var veryLargeTables = GetAvailableTables()
|
||||
.Where(IsVeryLargeTable)
|
||||
.Where(t => _pipelineFactory.IsVeryLargeTable(t))
|
||||
.ToList();
|
||||
|
||||
_logger?.LogInformation(
|
||||
@@ -175,14 +117,4 @@ public class DevEtlRegistry
|
||||
|
||||
return results.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Identifies very large tables that should be loaded sequentially to avoid IO contention.
|
||||
/// </summary>
|
||||
private static bool IsVeryLargeTable(string tableName) =>
|
||||
tableName.Contains("WorkOrderTime", StringComparison.OrdinalIgnoreCase) ||
|
||||
tableName.Contains("WorkOrderStep", StringComparison.OrdinalIgnoreCase) ||
|
||||
tableName.Contains("WorkOrderRouting", StringComparison.OrdinalIgnoreCase) ||
|
||||
tableName.Contains("WorkOrderComponent", StringComparison.OrdinalIgnoreCase) ||
|
||||
tableName.Contains("LotUsage", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using JdeScoping.DataSync.Etl.Destinations;
|
||||
using JdeScoping.DataSync.Etl.Pipeline;
|
||||
using JdeScoping.DataSync.Dev.Sources;
|
||||
|
||||
namespace JdeScoping.DataSync.Dev;
|
||||
|
||||
/// <summary>
|
||||
/// Development ETL pipeline for the FunctionCode table.
|
||||
/// </summary>
|
||||
public static class FunctionCodeDevEtl
|
||||
{
|
||||
public static readonly string TableName = "FunctionCode";
|
||||
public static readonly string CacheFileName = "functioncode.pb.zstd";
|
||||
|
||||
public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(connectionFactory);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cacheFilePath))
|
||||
throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath));
|
||||
|
||||
return new EtlPipelineBuilder()
|
||||
.WithName($"{TableName}_Dev")
|
||||
.WithSource(new ProtobufZstdFileSource(cacheFilePath))
|
||||
.WithDestination(new DbBulkImportDestination(connectionFactory, TableName))
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using JdeScoping.DataSync.Etl.Destinations;
|
||||
using JdeScoping.DataSync.Etl.Pipeline;
|
||||
using JdeScoping.DataSync.Dev.Sources;
|
||||
|
||||
namespace JdeScoping.DataSync.Dev;
|
||||
|
||||
/// <summary>
|
||||
/// Development ETL pipeline for the Item table.
|
||||
/// </summary>
|
||||
public static class ItemDevEtl
|
||||
{
|
||||
public static readonly string TableName = "Item";
|
||||
public static readonly string CacheFileName = "item.pb.zstd";
|
||||
|
||||
public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(connectionFactory);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cacheFilePath))
|
||||
throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath));
|
||||
|
||||
return new EtlPipelineBuilder()
|
||||
.WithName($"{TableName}_Dev")
|
||||
.WithSource(new ProtobufZstdFileSource(cacheFilePath))
|
||||
.WithDestination(new DbBulkImportDestination(connectionFactory, TableName))
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,11 @@
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="JdeScoping.DataSync.Dev.Tests" />
|
||||
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="protobuf-net-data" Version="4.1.0" />
|
||||
</ItemGroup>
|
||||
@@ -14,4 +19,10 @@
|
||||
<ProjectReference Include="..\JdeScoping.DataSync\JdeScoping.DataSync.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="Pipelines\dev-pipelines.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using JdeScoping.DataSync.Etl.Destinations;
|
||||
using JdeScoping.DataSync.Etl.Pipeline;
|
||||
using JdeScoping.DataSync.Dev.Sources;
|
||||
|
||||
namespace JdeScoping.DataSync.Dev;
|
||||
|
||||
/// <summary>
|
||||
/// Development ETL pipeline for the JdeUser table.
|
||||
/// </summary>
|
||||
public static class JdeUserDevEtl
|
||||
{
|
||||
public static readonly string TableName = "JdeUser";
|
||||
public static readonly string CacheFileName = "jdeuser.pb.zstd";
|
||||
|
||||
public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(connectionFactory);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cacheFilePath))
|
||||
throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath));
|
||||
|
||||
return new EtlPipelineBuilder()
|
||||
.WithName($"{TableName}_Dev")
|
||||
.WithSource(new ProtobufZstdFileSource(cacheFilePath))
|
||||
.WithDestination(new DbBulkImportDestination(connectionFactory, TableName))
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using JdeScoping.DataSync.Etl.Destinations;
|
||||
using JdeScoping.DataSync.Etl.Pipeline;
|
||||
using JdeScoping.DataSync.Dev.Sources;
|
||||
|
||||
namespace JdeScoping.DataSync.Dev;
|
||||
|
||||
/// <summary>
|
||||
/// Development ETL pipeline for the Lot table.
|
||||
/// </summary>
|
||||
public static class LotDevEtl
|
||||
{
|
||||
public static readonly string TableName = "Lot";
|
||||
public static readonly string CacheFileName = "lot.pb.zstd";
|
||||
|
||||
public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(connectionFactory);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cacheFilePath))
|
||||
throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath));
|
||||
|
||||
return new EtlPipelineBuilder()
|
||||
.WithName($"{TableName}_Dev")
|
||||
.WithSource(new ProtobufZstdFileSource(cacheFilePath))
|
||||
.WithDestination(new DbBulkImportDestination(connectionFactory, TableName))
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using JdeScoping.DataSync.Etl.Destinations;
|
||||
using JdeScoping.DataSync.Etl.Pipeline;
|
||||
using JdeScoping.DataSync.Dev.Sources;
|
||||
|
||||
namespace JdeScoping.DataSync.Dev;
|
||||
|
||||
/// <summary>
|
||||
/// Development ETL pipeline for the LotUsage_Curr table.
|
||||
/// </summary>
|
||||
public static class LotUsageCurrDevEtl
|
||||
{
|
||||
public static readonly string TableName = "LotUsage_Curr";
|
||||
public static readonly string CacheFileName = "lotusage_curr.pb.zstd";
|
||||
|
||||
public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(connectionFactory);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cacheFilePath))
|
||||
throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath));
|
||||
|
||||
return new EtlPipelineBuilder()
|
||||
.WithName($"{TableName}_Dev")
|
||||
.WithSource(new ProtobufZstdFileSource(cacheFilePath))
|
||||
.WithDestination(new DbBulkImportDestination(connectionFactory, TableName))
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using JdeScoping.DataSync.Etl.Destinations;
|
||||
using JdeScoping.DataSync.Etl.Pipeline;
|
||||
using JdeScoping.DataSync.Dev.Sources;
|
||||
|
||||
namespace JdeScoping.DataSync.Dev;
|
||||
|
||||
/// <summary>
|
||||
/// Development ETL pipeline for the LotUsage_Hist table.
|
||||
/// </summary>
|
||||
public static class LotUsageHistDevEtl
|
||||
{
|
||||
public static readonly string TableName = "LotUsage_Hist";
|
||||
public static readonly string CacheFileName = "lotusage_hist.pb.zstd";
|
||||
|
||||
public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(connectionFactory);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cacheFilePath))
|
||||
throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath));
|
||||
|
||||
return new EtlPipelineBuilder()
|
||||
.WithName($"{TableName}_Dev")
|
||||
.WithSource(new ProtobufZstdFileSource(cacheFilePath))
|
||||
.WithDestination(new DbBulkImportDestination(connectionFactory, TableName))
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using JdeScoping.DataSync.Etl.Destinations;
|
||||
using JdeScoping.DataSync.Etl.Pipeline;
|
||||
using JdeScoping.DataSync.Dev.Sources;
|
||||
|
||||
namespace JdeScoping.DataSync.Dev;
|
||||
|
||||
/// <summary>
|
||||
/// Development ETL pipeline for the MisData table.
|
||||
/// </summary>
|
||||
public static class MisDataDevEtl
|
||||
{
|
||||
public static readonly string TableName = "MisData";
|
||||
public static readonly string CacheFileName = "misdata.pb.zstd";
|
||||
|
||||
public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(connectionFactory);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cacheFilePath))
|
||||
throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath));
|
||||
|
||||
return new EtlPipelineBuilder()
|
||||
.WithName($"{TableName}_Dev")
|
||||
.WithSource(new ProtobufZstdFileSource(cacheFilePath))
|
||||
.WithDestination(new DbBulkImportDestination(connectionFactory, TableName))
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace JdeScoping.DataSync.Dev.Options;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for development ETL pipelines.
|
||||
/// </summary>
|
||||
public class DevPipelineOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration section name.
|
||||
/// </summary>
|
||||
public const string SectionName = "DevPipelines";
|
||||
|
||||
/// <summary>
|
||||
/// Path to the dev-pipelines.json configuration file.
|
||||
/// Relative to the assembly directory.
|
||||
/// </summary>
|
||||
public string ConfigPath { get; set; } = "Pipelines/dev-pipelines.json";
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using JdeScoping.DataSync.Etl.Destinations;
|
||||
using JdeScoping.DataSync.Etl.Pipeline;
|
||||
using JdeScoping.DataSync.Dev.Sources;
|
||||
|
||||
namespace JdeScoping.DataSync.Dev;
|
||||
|
||||
/// <summary>
|
||||
/// Development ETL pipeline for the OrgHierarchy table.
|
||||
/// </summary>
|
||||
public static class OrgHierarchyDevEtl
|
||||
{
|
||||
public static readonly string TableName = "OrgHierarchy";
|
||||
public static readonly string CacheFileName = "orghierarchy.pb.zstd";
|
||||
|
||||
public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(connectionFactory);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cacheFilePath))
|
||||
throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath));
|
||||
|
||||
return new EtlPipelineBuilder()
|
||||
.WithName($"{TableName}_Dev")
|
||||
.WithSource(new ProtobufZstdFileSource(cacheFilePath))
|
||||
.WithDestination(new DbBulkImportDestination(connectionFactory, TableName))
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"settings": {
|
||||
"sizeCategories": {
|
||||
"small": ["Branch", "OrgHierarchy", "WorkCenter", "ProfitCenter"],
|
||||
"medium": ["JdeUser", "FunctionCode", "Item", "RouteMaster"],
|
||||
"large": ["Lot", "MisData", "WorkOrder_Curr", "WorkOrder_Hist", "LotUsage_Hist", "WorkOrderComponent_Hist"],
|
||||
"veryLarge": ["WorkOrderStep_Hist", "WorkOrderComponent_Curr", "WorkOrderRouting", "LotUsage_Curr", "WorkOrderStep_Curr", "WorkOrderTime_Hist", "WorkOrderTime_Curr"]
|
||||
}
|
||||
},
|
||||
"pipelines": {
|
||||
"Branch": {
|
||||
"source": { "fileName": "branch.pb.zstd" },
|
||||
"destination": { "table": "Branch" }
|
||||
},
|
||||
"OrgHierarchy": {
|
||||
"source": { "fileName": "orghierarchy.pb.zstd" },
|
||||
"destination": { "table": "OrgHierarchy" }
|
||||
},
|
||||
"WorkCenter": {
|
||||
"source": { "fileName": "workcenter.pb.zstd" },
|
||||
"destination": { "table": "WorkCenter" }
|
||||
},
|
||||
"ProfitCenter": {
|
||||
"source": { "fileName": "profitcenter.pb.zstd" },
|
||||
"destination": { "table": "ProfitCenter" }
|
||||
},
|
||||
"JdeUser": {
|
||||
"source": { "fileName": "jdeuser.pb.zstd" },
|
||||
"destination": { "table": "JdeUser" }
|
||||
},
|
||||
"FunctionCode": {
|
||||
"source": { "fileName": "functioncode.pb.zstd" },
|
||||
"destination": { "table": "FunctionCode" }
|
||||
},
|
||||
"Item": {
|
||||
"source": { "fileName": "item.pb.zstd" },
|
||||
"destination": { "table": "Item" }
|
||||
},
|
||||
"RouteMaster": {
|
||||
"source": { "fileName": "routemaster.pb.zstd" },
|
||||
"destination": { "table": "RouteMaster" }
|
||||
},
|
||||
"Lot": {
|
||||
"source": { "fileName": "lot.pb.zstd" },
|
||||
"destination": { "table": "Lot" }
|
||||
},
|
||||
"MisData": {
|
||||
"source": { "fileName": "misdata.pb.zstd" },
|
||||
"destination": { "table": "MisData" }
|
||||
},
|
||||
"WorkOrder_Curr": {
|
||||
"source": { "fileName": "workorder_curr.pb.zstd" },
|
||||
"destination": { "table": "WorkOrder_Curr" }
|
||||
},
|
||||
"WorkOrder_Hist": {
|
||||
"source": { "fileName": "workorder_hist.pb.zstd" },
|
||||
"destination": { "table": "WorkOrder_Hist" }
|
||||
},
|
||||
"LotUsage_Curr": {
|
||||
"source": { "fileName": "lotusage_curr.pb.zstd" },
|
||||
"destination": { "table": "LotUsage_Curr" }
|
||||
},
|
||||
"LotUsage_Hist": {
|
||||
"source": { "fileName": "lotusage_hist.pb.zstd" },
|
||||
"destination": { "table": "LotUsage_Hist" }
|
||||
},
|
||||
"WorkOrderComponent_Curr": {
|
||||
"source": { "fileName": "workordercomponent_curr.pb.zstd" },
|
||||
"destination": { "table": "WorkOrderComponent_Curr" }
|
||||
},
|
||||
"WorkOrderComponent_Hist": {
|
||||
"source": { "fileName": "workordercomponent_hist.pb.zstd" },
|
||||
"destination": { "table": "WorkOrderComponent_Hist" }
|
||||
},
|
||||
"WorkOrderStep_Curr": {
|
||||
"source": { "fileName": "workorderstep_curr.pb.zstd" },
|
||||
"destination": { "table": "WorkOrderStep_Curr" }
|
||||
},
|
||||
"WorkOrderStep_Hist": {
|
||||
"source": { "fileName": "workorderstep_hist.pb.zstd" },
|
||||
"destination": { "table": "WorkOrderStep_Hist" }
|
||||
},
|
||||
"WorkOrderTime_Curr": {
|
||||
"source": { "fileName": "workordertime_curr.pb.zstd" },
|
||||
"destination": { "table": "WorkOrderTime_Curr" }
|
||||
},
|
||||
"WorkOrderTime_Hist": {
|
||||
"source": { "fileName": "workordertime_hist.pb.zstd" },
|
||||
"destination": { "table": "WorkOrderTime_Hist" }
|
||||
},
|
||||
"WorkOrderRouting": {
|
||||
"source": { "fileName": "workorderrouting.pb.zstd" },
|
||||
"destination": { "table": "WorkOrderRouting" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using JdeScoping.DataSync.Etl.Destinations;
|
||||
using JdeScoping.DataSync.Etl.Pipeline;
|
||||
using JdeScoping.DataSync.Dev.Sources;
|
||||
|
||||
namespace JdeScoping.DataSync.Dev;
|
||||
|
||||
/// <summary>
|
||||
/// Development ETL pipeline for the ProfitCenter table.
|
||||
/// </summary>
|
||||
public static class ProfitCenterDevEtl
|
||||
{
|
||||
public static readonly string TableName = "ProfitCenter";
|
||||
public static readonly string CacheFileName = "profitcenter.pb.zstd";
|
||||
|
||||
public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(connectionFactory);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cacheFilePath))
|
||||
throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath));
|
||||
|
||||
return new EtlPipelineBuilder()
|
||||
.WithName($"{TableName}_Dev")
|
||||
.WithSource(new ProtobufZstdFileSource(cacheFilePath))
|
||||
.WithDestination(new DbBulkImportDestination(connectionFactory, TableName))
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using JdeScoping.DataSync.Etl.Destinations;
|
||||
using JdeScoping.DataSync.Etl.Pipeline;
|
||||
using JdeScoping.DataSync.Dev.Sources;
|
||||
|
||||
namespace JdeScoping.DataSync.Dev;
|
||||
|
||||
/// <summary>
|
||||
/// Development ETL pipeline for the RouteMaster table.
|
||||
/// </summary>
|
||||
public static class RouteMasterDevEtl
|
||||
{
|
||||
public static readonly string TableName = "RouteMaster";
|
||||
public static readonly string CacheFileName = "routemaster.pb.zstd";
|
||||
|
||||
public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(connectionFactory);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cacheFilePath))
|
||||
throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath));
|
||||
|
||||
return new EtlPipelineBuilder()
|
||||
.WithName($"{TableName}_Dev")
|
||||
.WithSource(new ProtobufZstdFileSource(cacheFilePath))
|
||||
.WithDestination(new DbBulkImportDestination(connectionFactory, TableName))
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
using System.Text.Json;
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using JdeScoping.DataSync.Dev.Configuration;
|
||||
using JdeScoping.DataSync.Dev.Contracts;
|
||||
using JdeScoping.DataSync.Dev.Options;
|
||||
using JdeScoping.DataSync.Dev.Sources;
|
||||
using JdeScoping.DataSync.Etl.Destinations;
|
||||
using JdeScoping.DataSync.Etl.Pipeline;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace JdeScoping.DataSync.Dev.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating development ETL pipelines from JSON configuration.
|
||||
/// </summary>
|
||||
public class DevEtlPipelineFactory : IDevEtlPipelineFactory
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||
AllowTrailingCommas = true
|
||||
};
|
||||
|
||||
private readonly IDbConnectionFactory _connectionFactory;
|
||||
private readonly ILogger<EtlPipeline> _logger;
|
||||
private readonly DevPipelinesRoot _config;
|
||||
private readonly HashSet<string> _veryLargeTables;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new development pipeline factory.
|
||||
/// </summary>
|
||||
public DevEtlPipelineFactory(
|
||||
IDbConnectionFactory connectionFactory,
|
||||
IOptions<DevPipelineOptions> options,
|
||||
ILogger<EtlPipeline> logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(connectionFactory);
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
_connectionFactory = connectionFactory;
|
||||
_logger = logger;
|
||||
_config = LoadPipelineConfigs(options.Value.ConfigPath);
|
||||
_veryLargeTables = BuildVeryLargeTableSet();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new development pipeline factory with pre-loaded config (for testing).
|
||||
/// </summary>
|
||||
internal DevEtlPipelineFactory(
|
||||
IDbConnectionFactory connectionFactory,
|
||||
DevPipelinesRoot config,
|
||||
ILogger<EtlPipeline> logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(connectionFactory);
|
||||
ArgumentNullException.ThrowIfNull(config);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
_connectionFactory = connectionFactory;
|
||||
_logger = logger;
|
||||
_config = config;
|
||||
_veryLargeTables = BuildVeryLargeTableSet();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<string> GetAvailableTables() => _config.Pipelines.Keys;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsVeryLargeTable(string tableName) =>
|
||||
_veryLargeTables.Contains(tableName);
|
||||
|
||||
/// <inheritdoc />
|
||||
public EtlPipeline GetPipeline(string tableName, string cacheDirectory)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(tableName);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(cacheDirectory);
|
||||
|
||||
if (!_config.Pipelines.TryGetValue(tableName, out var pipelineConfig))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"No pipeline configured for table: {tableName}. " +
|
||||
$"Available tables: {string.Join(", ", _config.Pipelines.Keys)}");
|
||||
}
|
||||
|
||||
var cacheFilePath = Path.Combine(cacheDirectory, pipelineConfig.Source.FileName);
|
||||
|
||||
return new EtlPipelineBuilder()
|
||||
.WithName($"{tableName}_Dev")
|
||||
.WithSource(new ProtobufZstdFileSource(cacheFilePath))
|
||||
.WithDestination(new DbBulkImportDestination(_connectionFactory, pipelineConfig.Destination.Table))
|
||||
.WithLogger(_logger)
|
||||
.Build();
|
||||
}
|
||||
|
||||
private DevPipelinesRoot LoadPipelineConfigs(string configPath)
|
||||
{
|
||||
var assemblyDir = Path.GetDirectoryName(typeof(DevEtlPipelineFactory).Assembly.Location)!;
|
||||
var fullPath = Path.Combine(assemblyDir, configPath);
|
||||
|
||||
if (!File.Exists(fullPath))
|
||||
{
|
||||
throw new FileNotFoundException(
|
||||
$"Dev pipeline config not found: {fullPath}. " +
|
||||
"Ensure the config file is included in the build output.");
|
||||
}
|
||||
|
||||
var json = File.ReadAllText(fullPath);
|
||||
var root = JsonSerializer.Deserialize<DevPipelinesRoot>(json, JsonOptions)
|
||||
?? throw new InvalidOperationException("Failed to deserialize dev pipeline config: result was null.");
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
private HashSet<string> BuildVeryLargeTableSet()
|
||||
{
|
||||
var veryLarge = _config.EffectiveSettings.SizeCategories?.VeryLarge;
|
||||
return veryLarge != null
|
||||
? new HashSet<string>(veryLarge, StringComparer.OrdinalIgnoreCase)
|
||||
: new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using JdeScoping.DataSync.Etl.Destinations;
|
||||
using JdeScoping.DataSync.Etl.Pipeline;
|
||||
using JdeScoping.DataSync.Dev.Sources;
|
||||
|
||||
namespace JdeScoping.DataSync.Dev;
|
||||
|
||||
/// <summary>
|
||||
/// Development ETL pipeline for the WorkCenter table.
|
||||
/// </summary>
|
||||
public static class WorkCenterDevEtl
|
||||
{
|
||||
public static readonly string TableName = "WorkCenter";
|
||||
public static readonly string CacheFileName = "workcenter.pb.zstd";
|
||||
|
||||
public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(connectionFactory);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cacheFilePath))
|
||||
throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath));
|
||||
|
||||
return new EtlPipelineBuilder()
|
||||
.WithName($"{TableName}_Dev")
|
||||
.WithSource(new ProtobufZstdFileSource(cacheFilePath))
|
||||
.WithDestination(new DbBulkImportDestination(connectionFactory, TableName))
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using JdeScoping.DataSync.Etl.Destinations;
|
||||
using JdeScoping.DataSync.Etl.Pipeline;
|
||||
using JdeScoping.DataSync.Dev.Sources;
|
||||
|
||||
namespace JdeScoping.DataSync.Dev;
|
||||
|
||||
/// <summary>
|
||||
/// Development ETL pipeline for the WorkOrderComponent_Curr table.
|
||||
/// </summary>
|
||||
public static class WorkOrderComponentCurrDevEtl
|
||||
{
|
||||
public static readonly string TableName = "WorkOrderComponent_Curr";
|
||||
public static readonly string CacheFileName = "workordercomponent_curr.pb.zstd";
|
||||
|
||||
public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(connectionFactory);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cacheFilePath))
|
||||
throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath));
|
||||
|
||||
return new EtlPipelineBuilder()
|
||||
.WithName($"{TableName}_Dev")
|
||||
.WithSource(new ProtobufZstdFileSource(cacheFilePath))
|
||||
.WithDestination(new DbBulkImportDestination(connectionFactory, TableName))
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using JdeScoping.DataSync.Etl.Destinations;
|
||||
using JdeScoping.DataSync.Etl.Pipeline;
|
||||
using JdeScoping.DataSync.Dev.Sources;
|
||||
|
||||
namespace JdeScoping.DataSync.Dev;
|
||||
|
||||
/// <summary>
|
||||
/// Development ETL pipeline for the WorkOrderComponent_Hist table.
|
||||
/// </summary>
|
||||
public static class WorkOrderComponentHistDevEtl
|
||||
{
|
||||
public static readonly string TableName = "WorkOrderComponent_Hist";
|
||||
public static readonly string CacheFileName = "workordercomponent_hist.pb.zstd";
|
||||
|
||||
public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(connectionFactory);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cacheFilePath))
|
||||
throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath));
|
||||
|
||||
return new EtlPipelineBuilder()
|
||||
.WithName($"{TableName}_Dev")
|
||||
.WithSource(new ProtobufZstdFileSource(cacheFilePath))
|
||||
.WithDestination(new DbBulkImportDestination(connectionFactory, TableName))
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using JdeScoping.DataSync.Etl.Destinations;
|
||||
using JdeScoping.DataSync.Etl.Pipeline;
|
||||
using JdeScoping.DataSync.Dev.Sources;
|
||||
|
||||
namespace JdeScoping.DataSync.Dev;
|
||||
|
||||
/// <summary>
|
||||
/// Development ETL pipeline for the WorkOrder_Curr table.
|
||||
/// </summary>
|
||||
public static class WorkOrderCurrDevEtl
|
||||
{
|
||||
public static readonly string TableName = "WorkOrder_Curr";
|
||||
public static readonly string CacheFileName = "workorder_curr.pb.zstd";
|
||||
|
||||
public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(connectionFactory);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cacheFilePath))
|
||||
throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath));
|
||||
|
||||
return new EtlPipelineBuilder()
|
||||
.WithName($"{TableName}_Dev")
|
||||
.WithSource(new ProtobufZstdFileSource(cacheFilePath))
|
||||
.WithDestination(new DbBulkImportDestination(connectionFactory, TableName))
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using JdeScoping.DataSync.Etl.Destinations;
|
||||
using JdeScoping.DataSync.Etl.Pipeline;
|
||||
using JdeScoping.DataSync.Dev.Sources;
|
||||
|
||||
namespace JdeScoping.DataSync.Dev;
|
||||
|
||||
/// <summary>
|
||||
/// Development ETL pipeline for the WorkOrder_Hist table.
|
||||
/// </summary>
|
||||
public static class WorkOrderHistDevEtl
|
||||
{
|
||||
public static readonly string TableName = "WorkOrder_Hist";
|
||||
public static readonly string CacheFileName = "workorder_hist.pb.zstd";
|
||||
|
||||
public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(connectionFactory);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cacheFilePath))
|
||||
throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath));
|
||||
|
||||
return new EtlPipelineBuilder()
|
||||
.WithName($"{TableName}_Dev")
|
||||
.WithSource(new ProtobufZstdFileSource(cacheFilePath))
|
||||
.WithDestination(new DbBulkImportDestination(connectionFactory, TableName))
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using JdeScoping.DataSync.Etl.Destinations;
|
||||
using JdeScoping.DataSync.Etl.Pipeline;
|
||||
using JdeScoping.DataSync.Dev.Sources;
|
||||
|
||||
namespace JdeScoping.DataSync.Dev;
|
||||
|
||||
/// <summary>
|
||||
/// Development ETL pipeline for the WorkOrderRouting table.
|
||||
/// </summary>
|
||||
public static class WorkOrderRoutingDevEtl
|
||||
{
|
||||
public static readonly string TableName = "WorkOrderRouting";
|
||||
public static readonly string CacheFileName = "workorderrouting.pb.zstd";
|
||||
|
||||
public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(connectionFactory);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cacheFilePath))
|
||||
throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath));
|
||||
|
||||
return new EtlPipelineBuilder()
|
||||
.WithName($"{TableName}_Dev")
|
||||
.WithSource(new ProtobufZstdFileSource(cacheFilePath))
|
||||
.WithDestination(new DbBulkImportDestination(connectionFactory, TableName))
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using JdeScoping.DataSync.Etl.Destinations;
|
||||
using JdeScoping.DataSync.Etl.Pipeline;
|
||||
using JdeScoping.DataSync.Dev.Sources;
|
||||
|
||||
namespace JdeScoping.DataSync.Dev;
|
||||
|
||||
/// <summary>
|
||||
/// Development ETL pipeline for the WorkOrderStep_Curr table.
|
||||
/// </summary>
|
||||
public static class WorkOrderStepCurrDevEtl
|
||||
{
|
||||
public static readonly string TableName = "WorkOrderStep_Curr";
|
||||
public static readonly string CacheFileName = "workorderstep_curr.pb.zstd";
|
||||
|
||||
public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(connectionFactory);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cacheFilePath))
|
||||
throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath));
|
||||
|
||||
return new EtlPipelineBuilder()
|
||||
.WithName($"{TableName}_Dev")
|
||||
.WithSource(new ProtobufZstdFileSource(cacheFilePath))
|
||||
.WithDestination(new DbBulkImportDestination(connectionFactory, TableName))
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using JdeScoping.DataSync.Etl.Destinations;
|
||||
using JdeScoping.DataSync.Etl.Pipeline;
|
||||
using JdeScoping.DataSync.Dev.Sources;
|
||||
|
||||
namespace JdeScoping.DataSync.Dev;
|
||||
|
||||
/// <summary>
|
||||
/// Development ETL pipeline for the WorkOrderStep_Hist table.
|
||||
/// </summary>
|
||||
public static class WorkOrderStepHistDevEtl
|
||||
{
|
||||
public static readonly string TableName = "WorkOrderStep_Hist";
|
||||
public static readonly string CacheFileName = "workorderstep_hist.pb.zstd";
|
||||
|
||||
public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(connectionFactory);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cacheFilePath))
|
||||
throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath));
|
||||
|
||||
return new EtlPipelineBuilder()
|
||||
.WithName($"{TableName}_Dev")
|
||||
.WithSource(new ProtobufZstdFileSource(cacheFilePath))
|
||||
.WithDestination(new DbBulkImportDestination(connectionFactory, TableName))
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using JdeScoping.DataSync.Etl.Destinations;
|
||||
using JdeScoping.DataSync.Etl.Pipeline;
|
||||
using JdeScoping.DataSync.Dev.Sources;
|
||||
|
||||
namespace JdeScoping.DataSync.Dev;
|
||||
|
||||
/// <summary>
|
||||
/// Development ETL pipeline for the WorkOrderTime_Curr table.
|
||||
/// </summary>
|
||||
public static class WorkOrderTimeCurrDevEtl
|
||||
{
|
||||
public static readonly string TableName = "WorkOrderTime_Curr";
|
||||
public static readonly string CacheFileName = "workordertime_curr.pb.zstd";
|
||||
|
||||
public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(connectionFactory);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cacheFilePath))
|
||||
throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath));
|
||||
|
||||
return new EtlPipelineBuilder()
|
||||
.WithName($"{TableName}_Dev")
|
||||
.WithSource(new ProtobufZstdFileSource(cacheFilePath))
|
||||
.WithDestination(new DbBulkImportDestination(connectionFactory, TableName))
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using JdeScoping.DataSync.Etl.Destinations;
|
||||
using JdeScoping.DataSync.Etl.Pipeline;
|
||||
using JdeScoping.DataSync.Dev.Sources;
|
||||
|
||||
namespace JdeScoping.DataSync.Dev;
|
||||
|
||||
/// <summary>
|
||||
/// Development ETL pipeline for the WorkOrderTime_Hist table.
|
||||
/// </summary>
|
||||
public static class WorkOrderTimeHistDevEtl
|
||||
{
|
||||
public static readonly string TableName = "WorkOrderTime_Hist";
|
||||
public static readonly string CacheFileName = "workordertime_hist.pb.zstd";
|
||||
|
||||
public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(connectionFactory);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cacheFilePath))
|
||||
throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath));
|
||||
|
||||
return new EtlPipelineBuilder()
|
||||
.WithName($"{TableName}_Dev")
|
||||
.WithSource(new ProtobufZstdFileSource(cacheFilePath))
|
||||
.WithDestination(new DbBulkImportDestination(connectionFactory, TableName))
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
@@ -113,7 +113,7 @@ public sealed class FluentTableWriter
|
||||
else if (column.AutoWidth)
|
||||
{
|
||||
worksheet.Column(col).AdjustToContents();
|
||||
worksheet.Column(col).Width *= Formatting.ExcelFormats.DataPaddingFactor;
|
||||
worksheet.Column(col).Width *= ExcelFormats.DataPaddingFactor;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using JdeScoping.Api.Controllers;
|
||||
using JdeScoping.Core.ApiContracts.Auth;
|
||||
using JdeScoping.Core.Interfaces;
|
||||
using JdeScoping.Core.Models;
|
||||
using JdeScoping.Core.Models.Auth;
|
||||
@@ -180,7 +181,7 @@ public class AuthControllerTests
|
||||
// Assert
|
||||
result.Result.ShouldBeOfType<OkObjectResult>();
|
||||
var okResult = (OkObjectResult)result.Result!;
|
||||
var user = okResult.Value.ShouldBeOfType<UserInfo>();
|
||||
var user = okResult.Value.ShouldBeOfType<UserInfoDto>();
|
||||
user.Username.ShouldBe("testuser");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
using JdeScoping.Api.Hubs;
|
||||
using JdeScoping.Core.Models;
|
||||
using JdeScoping.Core.Models.Enums;
|
||||
using JdeScoping.Core.Models.Infrastructure;
|
||||
using JdeScoping.Core.Models.Search;
|
||||
using JdeScoping.Core.ApiContracts.SignalR;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
@@ -36,7 +33,7 @@ public class StatusHubTests
|
||||
var clientsProperty = typeof(Hub).GetProperty("Clients");
|
||||
clientsProperty?.SetValue(_hub, hubClients);
|
||||
|
||||
var statusUpdate = new StatusUpdate
|
||||
var statusUpdate = new StatusUpdateDto
|
||||
{
|
||||
Message = "Processing",
|
||||
Timestamp = DateTime.UtcNow
|
||||
@@ -93,13 +90,12 @@ public class StatusHubTests
|
||||
var clientsProperty = typeof(Hub).GetProperty("Clients");
|
||||
clientsProperty?.SetValue(_hub, hubClients);
|
||||
|
||||
var searchUpdate = new SearchUpdate
|
||||
var searchUpdate = new SearchUpdateDto
|
||||
{
|
||||
Id = 42,
|
||||
UserName = "testuser",
|
||||
Name = "Test Search",
|
||||
Status = SearchStatus.Running,
|
||||
Timestamp = DateTime.UtcNow
|
||||
Status = "Running"
|
||||
};
|
||||
|
||||
// Act
|
||||
|
||||
@@ -2,8 +2,8 @@ using System.Net;
|
||||
using System.Text.Json;
|
||||
using JdeScoping.Client.Services;
|
||||
using JdeScoping.Core.ApiContracts;
|
||||
using JdeScoping.Core.ApiContracts.Auth;
|
||||
using JdeScoping.Core.ApiContracts.Results;
|
||||
using JdeScoping.Core.Models;
|
||||
using JdeScoping.Core.Models.Auth;
|
||||
using RichardSzalay.MockHttp;
|
||||
using Shouldly;
|
||||
@@ -70,7 +70,7 @@ public class AuthApiClientTests
|
||||
public async Task GetCurrentUserAsync_CallsCorrectRoute()
|
||||
{
|
||||
// Arrange
|
||||
var user = new UserInfo { Username = "testuser", FirstName = "Test", LastName = "User" };
|
||||
var user = new UserInfoDto { Username = "testuser", FirstName = "Test", LastName = "User" };
|
||||
var request = _mockHttp.Expect(HttpMethod.Get, $"http://localhost/{ApiRoutes.Auth.Me}")
|
||||
.Respond("application/json", JsonSerializer.Serialize(user));
|
||||
|
||||
@@ -103,7 +103,7 @@ public class AuthApiClientTests
|
||||
public async Task LoginAsync_Success_ReturnsLoginResult()
|
||||
{
|
||||
// Arrange
|
||||
var user = new UserInfo { Username = "testuser", FirstName = "Test", LastName = "User" };
|
||||
var user = new UserInfoDto { Username = "testuser", FirstName = "Test", LastName = "User" };
|
||||
var loginResult = new LoginResultModel(true, null, user);
|
||||
_mockHttp.When(HttpMethod.Post, "*")
|
||||
.Respond("application/json", JsonSerializer.Serialize(loginResult));
|
||||
@@ -135,7 +135,7 @@ public class AuthApiClientTests
|
||||
public async Task GetCurrentUserAsync_Success_ReturnsUserInfo()
|
||||
{
|
||||
// Arrange
|
||||
var userInfo = new UserInfo { Username = "testuser", FirstName = "Test", LastName = "User" };
|
||||
var userInfo = new UserInfoDto { Username = "testuser", FirstName = "Test", LastName = "User" };
|
||||
_mockHttp.When(HttpMethod.Get, "*")
|
||||
.Respond("application/json", JsonSerializer.Serialize(userInfo));
|
||||
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
using Dapper;
|
||||
using JdeScoping.DataAccess;
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using JdeScoping.DataSync.Dev;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NSubstitute;
|
||||
using Shouldly;
|
||||
|
||||
namespace JdeScoping.DataSync.Dev.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for Branch development ETL.
|
||||
/// Requires: Local SQL Server, CACHED_DB_FILES directory with branch.json.zstd
|
||||
/// </summary>
|
||||
public class BranchDevEtlTests : IAsyncLifetime
|
||||
{
|
||||
private readonly string _connectionString;
|
||||
private readonly string _cacheDirectory;
|
||||
private readonly IDbConnectionFactory _connectionFactory;
|
||||
|
||||
public BranchDevEtlTests()
|
||||
{
|
||||
// Load configuration
|
||||
var config = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddJsonFile("appsettings.json", optional: true)
|
||||
.AddEnvironmentVariables()
|
||||
.Build();
|
||||
|
||||
_connectionString = config.GetConnectionString("LotFinderDB")
|
||||
?? throw new InvalidOperationException("LotFinderDB connection string not configured.");
|
||||
|
||||
_cacheDirectory = config["DevEtl:CacheDirectory"]
|
||||
?? Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "..", "CACHED_DB_FILES");
|
||||
|
||||
_connectionFactory = new DbConnectionFactory(config, NullLogger<DbConnectionFactory>.Instance);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
// Ensure Branch table is empty before test
|
||||
await using var connection = new SqlConnection(_connectionString);
|
||||
await connection.OpenAsync();
|
||||
await connection.ExecuteAsync("TRUNCATE TABLE dbo.Branch");
|
||||
}
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
public void Create_ReturnsValidPipeline()
|
||||
{
|
||||
// Arrange
|
||||
var cacheFilePath = Path.Combine(_cacheDirectory, BranchDevEtl.CacheFileName);
|
||||
if (!File.Exists(cacheFilePath))
|
||||
{
|
||||
// Skip test if cache file doesn't exist
|
||||
return;
|
||||
}
|
||||
|
||||
// Act
|
||||
var pipeline = BranchDevEtl.Create(_connectionFactory, cacheFilePath);
|
||||
|
||||
// Assert
|
||||
pipeline.ShouldNotBeNull();
|
||||
pipeline.PipelineName.ShouldBe("Branch_Dev");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_LoadsBranchData()
|
||||
{
|
||||
// Arrange
|
||||
var cacheFilePath = Path.Combine(_cacheDirectory, BranchDevEtl.CacheFileName);
|
||||
if (!File.Exists(cacheFilePath))
|
||||
{
|
||||
// Skip test if cache file doesn't exist
|
||||
return;
|
||||
}
|
||||
|
||||
var pipeline = BranchDevEtl.Create(_connectionFactory, cacheFilePath);
|
||||
|
||||
// Act
|
||||
var result = await pipeline.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed");
|
||||
result.TotalRows.ShouldBeGreaterThan(0, "Should load at least one row");
|
||||
|
||||
// Verify data in database
|
||||
await using var connection = new SqlConnection(_connectionString);
|
||||
await connection.OpenAsync();
|
||||
var count = await connection.ExecuteScalarAsync<int>("SELECT COUNT(*) FROM dbo.Branch");
|
||||
|
||||
count.ShouldBe((int)result.TotalRows, "Database row count should match pipeline result");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Registry_RunAsync_LoadsBranch()
|
||||
{
|
||||
// Arrange
|
||||
if (!Directory.Exists(_cacheDirectory))
|
||||
{
|
||||
// Skip test if cache directory doesn't exist
|
||||
return;
|
||||
}
|
||||
|
||||
var registry = new DevEtlRegistry(_connectionFactory, _cacheDirectory);
|
||||
|
||||
// Act
|
||||
var result = await registry.RunAsync("Branch");
|
||||
|
||||
// Assert
|
||||
result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed");
|
||||
result.TotalRows.ShouldBeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithNullConnectionFactory_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
var cacheFilePath = Path.Combine(_cacheDirectory, BranchDevEtl.CacheFileName);
|
||||
|
||||
// Act & Assert
|
||||
Should.Throw<ArgumentNullException>(() => BranchDevEtl.Create(null!, cacheFilePath));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithEmptyCacheFilePath_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange
|
||||
var mockFactory = Substitute.For<IDbConnectionFactory>();
|
||||
|
||||
// Act & Assert
|
||||
Should.Throw<ArgumentException>(() => BranchDevEtl.Create(mockFactory, string.Empty));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithNonExistentCacheFile_ThrowsFileNotFoundException()
|
||||
{
|
||||
// Arrange
|
||||
var mockFactory = Substitute.For<IDbConnectionFactory>();
|
||||
var nonExistentPath = "/nonexistent/path/branch.json.zstd";
|
||||
|
||||
// Act & Assert
|
||||
Should.Throw<FileNotFoundException>(() => BranchDevEtl.Create(mockFactory, nonExistentPath));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DevEtlRegistry_WithNonExistentCacheDirectory_ThrowsDirectoryNotFoundException()
|
||||
{
|
||||
// Arrange
|
||||
var mockFactory = Substitute.For<IDbConnectionFactory>();
|
||||
var nonExistentPath = "/nonexistent/cache/directory";
|
||||
|
||||
// Act & Assert
|
||||
Should.Throw<DirectoryNotFoundException>(() => new DevEtlRegistry(mockFactory, nonExistentPath));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DevEtlRegistry_GetAvailableTables_IncludesBranch()
|
||||
{
|
||||
// Arrange
|
||||
if (!Directory.Exists(_cacheDirectory))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var registry = new DevEtlRegistry(_connectionFactory, _cacheDirectory);
|
||||
|
||||
// Act
|
||||
var tables = registry.GetAvailableTables().ToList();
|
||||
|
||||
// Assert
|
||||
tables.ShouldContain("Branch");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
using Dapper;
|
||||
using JdeScoping.DataAccess;
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using JdeScoping.DataSync.Dev.Configuration;
|
||||
using JdeScoping.DataSync.Dev.Contracts;
|
||||
using JdeScoping.DataSync.Dev.Options;
|
||||
using JdeScoping.DataSync.Dev.Services;
|
||||
using JdeScoping.DataSync.Etl.Pipeline;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NSubstitute;
|
||||
using Shouldly;
|
||||
|
||||
namespace JdeScoping.DataSync.Dev.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for DevEtlPipelineFactory.
|
||||
/// </summary>
|
||||
public class DevEtlPipelineFactoryTests
|
||||
{
|
||||
private readonly IDbConnectionFactory _connectionFactory;
|
||||
private readonly ILogger<EtlPipeline> _logger;
|
||||
private readonly string _cacheDirectory;
|
||||
|
||||
public DevEtlPipelineFactoryTests()
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddJsonFile("appsettings.json", optional: true)
|
||||
.AddEnvironmentVariables()
|
||||
.Build();
|
||||
|
||||
_connectionFactory = new DbConnectionFactory(config, NullLogger<DbConnectionFactory>.Instance);
|
||||
_logger = NullLogger<EtlPipeline>.Instance;
|
||||
_cacheDirectory = config["DevEtl:CacheDirectory"]
|
||||
?? Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "..", "CACHED_DB_FILES");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithValidConfig_LoadsPipelines()
|
||||
{
|
||||
// Arrange
|
||||
var pipelines = new Dictionary<string, DevPipelineConfig>
|
||||
{
|
||||
["TestTable"] = new(new DevSourceConfig("test.pb.zstd"), new DevDestinationConfig("TestTable"))
|
||||
};
|
||||
var config = new DevPipelinesRoot(null, pipelines);
|
||||
|
||||
// Act
|
||||
var factory = new DevEtlPipelineFactory(_connectionFactory, config, _logger);
|
||||
|
||||
// Assert
|
||||
factory.GetAvailableTables().ShouldContain("TestTable");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAvailableTables_Returns21Tables()
|
||||
{
|
||||
// Arrange
|
||||
var factory = CreateFactoryFromConfig();
|
||||
|
||||
// Act
|
||||
var tables = factory.GetAvailableTables().ToList();
|
||||
|
||||
// Assert
|
||||
tables.Count.ShouldBe(21);
|
||||
tables.ShouldContain("Branch");
|
||||
tables.ShouldContain("WorkOrder_Curr");
|
||||
tables.ShouldContain("LotUsage_Curr");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsVeryLargeTable_ReturnsTrueForVeryLargeTables()
|
||||
{
|
||||
// Arrange
|
||||
var factory = CreateFactoryFromConfig();
|
||||
|
||||
// Act & Assert
|
||||
factory.IsVeryLargeTable("WorkOrderTime_Curr").ShouldBeTrue();
|
||||
factory.IsVeryLargeTable("WorkOrderStep_Curr").ShouldBeTrue();
|
||||
factory.IsVeryLargeTable("LotUsage_Curr").ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsVeryLargeTable_ReturnsFalseForSmallTables()
|
||||
{
|
||||
// Arrange
|
||||
var factory = CreateFactoryFromConfig();
|
||||
|
||||
// Act & Assert
|
||||
factory.IsVeryLargeTable("Branch").ShouldBeFalse();
|
||||
factory.IsVeryLargeTable("Item").ShouldBeFalse();
|
||||
factory.IsVeryLargeTable("JdeUser").ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPipeline_WithValidTable_ReturnsPipeline()
|
||||
{
|
||||
// Arrange
|
||||
var factory = CreateFactoryFromConfig();
|
||||
|
||||
// Skip if cache directory doesn't exist
|
||||
if (!Directory.Exists(_cacheDirectory))
|
||||
return;
|
||||
|
||||
var cacheFilePath = Path.Combine(_cacheDirectory, "branch.pb.zstd");
|
||||
if (!File.Exists(cacheFilePath))
|
||||
return;
|
||||
|
||||
// Act
|
||||
var pipeline = factory.GetPipeline("Branch", _cacheDirectory);
|
||||
|
||||
// Assert
|
||||
pipeline.ShouldNotBeNull();
|
||||
pipeline.PipelineName.ShouldBe("Branch_Dev");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPipeline_WithInvalidTable_ThrowsInvalidOperationException()
|
||||
{
|
||||
// Arrange
|
||||
var factory = CreateFactoryFromConfig();
|
||||
|
||||
// Act & Assert
|
||||
Should.Throw<InvalidOperationException>(() => factory.GetPipeline("NonExistentTable", _cacheDirectory));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPipeline_WithNullTableName_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange
|
||||
var factory = CreateFactoryFromConfig();
|
||||
|
||||
// Act & Assert
|
||||
Should.Throw<ArgumentException>(() => factory.GetPipeline(null!, _cacheDirectory));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPipeline_WithEmptyCacheDirectory_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange
|
||||
var factory = CreateFactoryFromConfig();
|
||||
|
||||
// Act & Assert
|
||||
Should.Throw<ArgumentException>(() => factory.GetPipeline("Branch", string.Empty));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithNullConnectionFactory_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
var config = new DevPipelinesRoot(null, new Dictionary<string, DevPipelineConfig>());
|
||||
|
||||
// Act & Assert
|
||||
Should.Throw<ArgumentNullException>(() => new DevEtlPipelineFactory(null!, config, _logger));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithNullConfig_ThrowsArgumentNullException()
|
||||
{
|
||||
// Act & Assert
|
||||
Should.Throw<ArgumentNullException>(() => new DevEtlPipelineFactory(_connectionFactory, (DevPipelinesRoot)null!, _logger));
|
||||
}
|
||||
|
||||
private DevEtlPipelineFactory CreateFactoryFromConfig()
|
||||
{
|
||||
// Load actual config from JSON file
|
||||
var settings = new DevPipelineSettings
|
||||
{
|
||||
SizeCategories = new SizeCategories
|
||||
{
|
||||
Small = ["Branch", "OrgHierarchy", "WorkCenter", "ProfitCenter"],
|
||||
Medium = ["JdeUser", "FunctionCode", "Item", "RouteMaster"],
|
||||
Large = ["Lot", "MisData", "WorkOrder_Curr", "WorkOrder_Hist", "LotUsage_Hist", "WorkOrderComponent_Hist"],
|
||||
VeryLarge = ["WorkOrderStep_Hist", "WorkOrderComponent_Curr", "WorkOrderRouting", "LotUsage_Curr", "WorkOrderStep_Curr", "WorkOrderTime_Hist", "WorkOrderTime_Curr"]
|
||||
}
|
||||
};
|
||||
|
||||
var pipelines = new Dictionary<string, DevPipelineConfig>
|
||||
{
|
||||
["Branch"] = new(new DevSourceConfig("branch.pb.zstd"), new DevDestinationConfig("Branch")),
|
||||
["OrgHierarchy"] = new(new DevSourceConfig("orghierarchy.pb.zstd"), new DevDestinationConfig("OrgHierarchy")),
|
||||
["WorkCenter"] = new(new DevSourceConfig("workcenter.pb.zstd"), new DevDestinationConfig("WorkCenter")),
|
||||
["ProfitCenter"] = new(new DevSourceConfig("profitcenter.pb.zstd"), new DevDestinationConfig("ProfitCenter")),
|
||||
["JdeUser"] = new(new DevSourceConfig("jdeuser.pb.zstd"), new DevDestinationConfig("JdeUser")),
|
||||
["FunctionCode"] = new(new DevSourceConfig("functioncode.pb.zstd"), new DevDestinationConfig("FunctionCode")),
|
||||
["Item"] = new(new DevSourceConfig("item.pb.zstd"), new DevDestinationConfig("Item")),
|
||||
["RouteMaster"] = new(new DevSourceConfig("routemaster.pb.zstd"), new DevDestinationConfig("RouteMaster")),
|
||||
["Lot"] = new(new DevSourceConfig("lot.pb.zstd"), new DevDestinationConfig("Lot")),
|
||||
["MisData"] = new(new DevSourceConfig("misdata.pb.zstd"), new DevDestinationConfig("MisData")),
|
||||
["WorkOrder_Curr"] = new(new DevSourceConfig("workorder_curr.pb.zstd"), new DevDestinationConfig("WorkOrder_Curr")),
|
||||
["WorkOrder_Hist"] = new(new DevSourceConfig("workorder_hist.pb.zstd"), new DevDestinationConfig("WorkOrder_Hist")),
|
||||
["LotUsage_Curr"] = new(new DevSourceConfig("lotusage_curr.pb.zstd"), new DevDestinationConfig("LotUsage_Curr")),
|
||||
["LotUsage_Hist"] = new(new DevSourceConfig("lotusage_hist.pb.zstd"), new DevDestinationConfig("LotUsage_Hist")),
|
||||
["WorkOrderComponent_Curr"] = new(new DevSourceConfig("workordercomponent_curr.pb.zstd"), new DevDestinationConfig("WorkOrderComponent_Curr")),
|
||||
["WorkOrderComponent_Hist"] = new(new DevSourceConfig("workordercomponent_hist.pb.zstd"), new DevDestinationConfig("WorkOrderComponent_Hist")),
|
||||
["WorkOrderStep_Curr"] = new(new DevSourceConfig("workorderstep_curr.pb.zstd"), new DevDestinationConfig("WorkOrderStep_Curr")),
|
||||
["WorkOrderStep_Hist"] = new(new DevSourceConfig("workorderstep_hist.pb.zstd"), new DevDestinationConfig("WorkOrderStep_Hist")),
|
||||
["WorkOrderTime_Curr"] = new(new DevSourceConfig("workordertime_curr.pb.zstd"), new DevDestinationConfig("WorkOrderTime_Curr")),
|
||||
["WorkOrderTime_Hist"] = new(new DevSourceConfig("workordertime_hist.pb.zstd"), new DevDestinationConfig("WorkOrderTime_Hist")),
|
||||
["WorkOrderRouting"] = new(new DevSourceConfig("workorderrouting.pb.zstd"), new DevDestinationConfig("WorkOrderRouting"))
|
||||
};
|
||||
|
||||
var config = new DevPipelinesRoot(settings, pipelines);
|
||||
return new DevEtlPipelineFactory(_connectionFactory, config, _logger);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
using JdeScoping.DataSync.Dev.Configuration;
|
||||
using JdeScoping.DataSync.Dev.Contracts;
|
||||
using JdeScoping.DataSync.Dev.Services;
|
||||
using JdeScoping.DataSync.Etl.Pipeline;
|
||||
using JdeScoping.DataSync.Etl.Results;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NSubstitute;
|
||||
using Shouldly;
|
||||
|
||||
namespace JdeScoping.DataSync.Dev.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for DevEtlRegistry.
|
||||
/// </summary>
|
||||
public class DevEtlRegistryTests : IDisposable
|
||||
{
|
||||
private readonly IDevEtlPipelineFactory _mockFactory;
|
||||
private readonly ILogger<DevEtlRegistry> _logger;
|
||||
private readonly string _tempDirectory;
|
||||
|
||||
public DevEtlRegistryTests()
|
||||
{
|
||||
_mockFactory = Substitute.For<IDevEtlPipelineFactory>();
|
||||
_logger = NullLogger<DevEtlRegistry>.Instance;
|
||||
|
||||
// Create a temp directory for tests
|
||||
_tempDirectory = Path.Combine(Path.GetTempPath(), $"DevEtlRegistryTests_{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_tempDirectory);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithValidArguments_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
_mockFactory.GetAvailableTables().Returns(new[] { "Branch" });
|
||||
|
||||
// Act
|
||||
var registry = new DevEtlRegistry(_mockFactory, _tempDirectory, _logger);
|
||||
|
||||
// Assert
|
||||
registry.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithNullFactory_ThrowsArgumentNullException()
|
||||
{
|
||||
// Act & Assert
|
||||
Should.Throw<ArgumentNullException>(() => new DevEtlRegistry(null!, _tempDirectory, _logger));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithEmptyCacheDirectory_ThrowsArgumentException()
|
||||
{
|
||||
// Act & Assert
|
||||
Should.Throw<ArgumentException>(() => new DevEtlRegistry(_mockFactory, string.Empty, _logger));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithNonExistentCacheDirectory_ThrowsDirectoryNotFoundException()
|
||||
{
|
||||
// Arrange
|
||||
var nonExistentPath = "/nonexistent/cache/directory";
|
||||
|
||||
// Act & Assert
|
||||
Should.Throw<DirectoryNotFoundException>(() => new DevEtlRegistry(_mockFactory, nonExistentPath, _logger));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAvailableTables_DelegatesToFactory()
|
||||
{
|
||||
// Arrange
|
||||
var expectedTables = new[] { "Branch", "Item", "Lot" };
|
||||
_mockFactory.GetAvailableTables().Returns(expectedTables);
|
||||
|
||||
var registry = new DevEtlRegistry(_mockFactory, _tempDirectory, _logger);
|
||||
|
||||
// Act
|
||||
var tables = registry.GetAvailableTables().ToList();
|
||||
|
||||
// Assert
|
||||
tables.ShouldBe(expectedTables);
|
||||
_mockFactory.Received(1).GetAvailableTables();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Cleanup temp directory
|
||||
if (Directory.Exists(_tempDirectory))
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.Delete(_tempDirectory, recursive: true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
using Dapper;
|
||||
using JdeScoping.DataAccess;
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using JdeScoping.DataSync.Dev;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NSubstitute;
|
||||
using Shouldly;
|
||||
|
||||
namespace JdeScoping.DataSync.Dev.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for FunctionCode development ETL.
|
||||
/// </summary>
|
||||
public class FunctionCodeDevEtlTests : IAsyncLifetime
|
||||
{
|
||||
private readonly string _connectionString;
|
||||
private readonly string _cacheDirectory;
|
||||
private readonly IDbConnectionFactory _connectionFactory;
|
||||
|
||||
public FunctionCodeDevEtlTests()
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddJsonFile("appsettings.json", optional: true)
|
||||
.AddEnvironmentVariables()
|
||||
.Build();
|
||||
|
||||
_connectionString = config.GetConnectionString("LotFinderDB")
|
||||
?? throw new InvalidOperationException("LotFinderDB connection string not configured.");
|
||||
|
||||
_cacheDirectory = config["DevEtl:CacheDirectory"]
|
||||
?? Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "..", "CACHED_DB_FILES");
|
||||
|
||||
_connectionFactory = new DbConnectionFactory(config, NullLogger<DbConnectionFactory>.Instance);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
await using var connection = new SqlConnection(_connectionString);
|
||||
await connection.OpenAsync();
|
||||
await connection.ExecuteAsync("TRUNCATE TABLE dbo.FunctionCode");
|
||||
}
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
public void Create_ReturnsValidPipeline()
|
||||
{
|
||||
var cacheFilePath = Path.Combine(_cacheDirectory, FunctionCodeDevEtl.CacheFileName);
|
||||
if (!File.Exists(cacheFilePath)) return;
|
||||
|
||||
var pipeline = FunctionCodeDevEtl.Create(_connectionFactory, cacheFilePath);
|
||||
|
||||
pipeline.ShouldNotBeNull();
|
||||
pipeline.PipelineName.ShouldBe("FunctionCode_Dev");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_LoadsData()
|
||||
{
|
||||
var cacheFilePath = Path.Combine(_cacheDirectory, FunctionCodeDevEtl.CacheFileName);
|
||||
if (!File.Exists(cacheFilePath)) return;
|
||||
|
||||
var pipeline = FunctionCodeDevEtl.Create(_connectionFactory, cacheFilePath);
|
||||
var result = await pipeline.ExecuteAsync();
|
||||
|
||||
result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed");
|
||||
result.TotalRows.ShouldBeGreaterThan(0);
|
||||
|
||||
await using var connection = new SqlConnection(_connectionString);
|
||||
await connection.OpenAsync();
|
||||
var count = await connection.ExecuteScalarAsync<int>("SELECT COUNT(*) FROM dbo.FunctionCode");
|
||||
count.ShouldBe((int)result.TotalRows);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Registry_RunAsync_LoadsTable()
|
||||
{
|
||||
if (!Directory.Exists(_cacheDirectory)) return;
|
||||
|
||||
var registry = new DevEtlRegistry(_connectionFactory, _cacheDirectory);
|
||||
var result = await registry.RunAsync("FunctionCode");
|
||||
|
||||
result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed");
|
||||
result.TotalRows.ShouldBeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithNullConnectionFactory_ThrowsArgumentNullException()
|
||||
{
|
||||
var cacheFilePath = Path.Combine(_cacheDirectory, FunctionCodeDevEtl.CacheFileName);
|
||||
Should.Throw<ArgumentNullException>(() => FunctionCodeDevEtl.Create(null!, cacheFilePath));
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
using Dapper;
|
||||
using JdeScoping.DataAccess;
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using JdeScoping.DataSync.Dev;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NSubstitute;
|
||||
using Shouldly;
|
||||
|
||||
namespace JdeScoping.DataSync.Dev.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for Item development ETL.
|
||||
/// </summary>
|
||||
public class ItemDevEtlTests : IAsyncLifetime
|
||||
{
|
||||
private readonly string _connectionString;
|
||||
private readonly string _cacheDirectory;
|
||||
private readonly IDbConnectionFactory _connectionFactory;
|
||||
|
||||
public ItemDevEtlTests()
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddJsonFile("appsettings.json", optional: true)
|
||||
.AddEnvironmentVariables()
|
||||
.Build();
|
||||
|
||||
_connectionString = config.GetConnectionString("LotFinderDB")
|
||||
?? throw new InvalidOperationException("LotFinderDB connection string not configured.");
|
||||
|
||||
_cacheDirectory = config["DevEtl:CacheDirectory"]
|
||||
?? Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "..", "CACHED_DB_FILES");
|
||||
|
||||
_connectionFactory = new DbConnectionFactory(config, NullLogger<DbConnectionFactory>.Instance);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
await using var connection = new SqlConnection(_connectionString);
|
||||
await connection.OpenAsync();
|
||||
await connection.ExecuteAsync("TRUNCATE TABLE dbo.Item");
|
||||
}
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
public void Create_ReturnsValidPipeline()
|
||||
{
|
||||
var cacheFilePath = Path.Combine(_cacheDirectory, ItemDevEtl.CacheFileName);
|
||||
if (!File.Exists(cacheFilePath)) return;
|
||||
|
||||
var pipeline = ItemDevEtl.Create(_connectionFactory, cacheFilePath);
|
||||
|
||||
pipeline.ShouldNotBeNull();
|
||||
pipeline.PipelineName.ShouldBe("Item_Dev");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_LoadsData()
|
||||
{
|
||||
var cacheFilePath = Path.Combine(_cacheDirectory, ItemDevEtl.CacheFileName);
|
||||
if (!File.Exists(cacheFilePath)) return;
|
||||
|
||||
var pipeline = ItemDevEtl.Create(_connectionFactory, cacheFilePath);
|
||||
var result = await pipeline.ExecuteAsync();
|
||||
|
||||
result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed");
|
||||
result.TotalRows.ShouldBeGreaterThan(0);
|
||||
|
||||
await using var connection = new SqlConnection(_connectionString);
|
||||
await connection.OpenAsync();
|
||||
var count = await connection.ExecuteScalarAsync<int>("SELECT COUNT(*) FROM dbo.Item");
|
||||
count.ShouldBe((int)result.TotalRows);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Registry_RunAsync_LoadsTable()
|
||||
{
|
||||
if (!Directory.Exists(_cacheDirectory)) return;
|
||||
|
||||
var registry = new DevEtlRegistry(_connectionFactory, _cacheDirectory);
|
||||
var result = await registry.RunAsync("Item");
|
||||
|
||||
result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed");
|
||||
result.TotalRows.ShouldBeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithNullConnectionFactory_ThrowsArgumentNullException()
|
||||
{
|
||||
var cacheFilePath = Path.Combine(_cacheDirectory, ItemDevEtl.CacheFileName);
|
||||
Should.Throw<ArgumentNullException>(() => ItemDevEtl.Create(null!, cacheFilePath));
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
using Dapper;
|
||||
using JdeScoping.DataAccess;
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using JdeScoping.DataSync.Dev;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NSubstitute;
|
||||
using Shouldly;
|
||||
|
||||
namespace JdeScoping.DataSync.Dev.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for JdeUser development ETL.
|
||||
/// </summary>
|
||||
public class JdeUserDevEtlTests : IAsyncLifetime
|
||||
{
|
||||
private readonly string _connectionString;
|
||||
private readonly string _cacheDirectory;
|
||||
private readonly IDbConnectionFactory _connectionFactory;
|
||||
|
||||
public JdeUserDevEtlTests()
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddJsonFile("appsettings.json", optional: true)
|
||||
.AddEnvironmentVariables()
|
||||
.Build();
|
||||
|
||||
_connectionString = config.GetConnectionString("LotFinderDB")
|
||||
?? throw new InvalidOperationException("LotFinderDB connection string not configured.");
|
||||
|
||||
_cacheDirectory = config["DevEtl:CacheDirectory"]
|
||||
?? Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "..", "CACHED_DB_FILES");
|
||||
|
||||
_connectionFactory = new DbConnectionFactory(config, NullLogger<DbConnectionFactory>.Instance);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
await using var connection = new SqlConnection(_connectionString);
|
||||
await connection.OpenAsync();
|
||||
await connection.ExecuteAsync("TRUNCATE TABLE dbo.JdeUser");
|
||||
}
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
public void Create_ReturnsValidPipeline()
|
||||
{
|
||||
var cacheFilePath = Path.Combine(_cacheDirectory, JdeUserDevEtl.CacheFileName);
|
||||
if (!File.Exists(cacheFilePath)) return;
|
||||
|
||||
var pipeline = JdeUserDevEtl.Create(_connectionFactory, cacheFilePath);
|
||||
|
||||
pipeline.ShouldNotBeNull();
|
||||
pipeline.PipelineName.ShouldBe("JdeUser_Dev");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_LoadsData()
|
||||
{
|
||||
var cacheFilePath = Path.Combine(_cacheDirectory, JdeUserDevEtl.CacheFileName);
|
||||
if (!File.Exists(cacheFilePath)) return;
|
||||
|
||||
var pipeline = JdeUserDevEtl.Create(_connectionFactory, cacheFilePath);
|
||||
var result = await pipeline.ExecuteAsync();
|
||||
|
||||
result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed");
|
||||
result.TotalRows.ShouldBeGreaterThan(0);
|
||||
|
||||
await using var connection = new SqlConnection(_connectionString);
|
||||
await connection.OpenAsync();
|
||||
var count = await connection.ExecuteScalarAsync<int>("SELECT COUNT(*) FROM dbo.JdeUser");
|
||||
count.ShouldBe((int)result.TotalRows);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Registry_RunAsync_LoadsTable()
|
||||
{
|
||||
if (!Directory.Exists(_cacheDirectory)) return;
|
||||
|
||||
var registry = new DevEtlRegistry(_connectionFactory, _cacheDirectory);
|
||||
var result = await registry.RunAsync("JdeUser");
|
||||
|
||||
result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed");
|
||||
result.TotalRows.ShouldBeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithNullConnectionFactory_ThrowsArgumentNullException()
|
||||
{
|
||||
var cacheFilePath = Path.Combine(_cacheDirectory, JdeUserDevEtl.CacheFileName);
|
||||
Should.Throw<ArgumentNullException>(() => JdeUserDevEtl.Create(null!, cacheFilePath));
|
||||
}
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
using Dapper;
|
||||
using JdeScoping.DataAccess;
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using JdeScoping.DataSync.Dev;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NSubstitute;
|
||||
using Shouldly;
|
||||
|
||||
namespace JdeScoping.DataSync.Dev.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for OrgHierarchy development ETL.
|
||||
/// Requires: Local SQL Server, CACHED_DB_FILES directory with orghierarchy.json.zstd
|
||||
/// </summary>
|
||||
public class OrgHierarchyDevEtlTests : IAsyncLifetime
|
||||
{
|
||||
private readonly string _connectionString;
|
||||
private readonly string _cacheDirectory;
|
||||
private readonly IDbConnectionFactory _connectionFactory;
|
||||
|
||||
public OrgHierarchyDevEtlTests()
|
||||
{
|
||||
// Load configuration
|
||||
var config = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddJsonFile("appsettings.json", optional: true)
|
||||
.AddEnvironmentVariables()
|
||||
.Build();
|
||||
|
||||
_connectionString = config.GetConnectionString("LotFinderDB")
|
||||
?? throw new InvalidOperationException("LotFinderDB connection string not configured.");
|
||||
|
||||
_cacheDirectory = config["DevEtl:CacheDirectory"]
|
||||
?? Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "..", "CACHED_DB_FILES");
|
||||
|
||||
_connectionFactory = new DbConnectionFactory(config, NullLogger<DbConnectionFactory>.Instance);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
// Ensure OrgHierarchy table is empty before test
|
||||
await using var connection = new SqlConnection(_connectionString);
|
||||
await connection.OpenAsync();
|
||||
await connection.ExecuteAsync("TRUNCATE TABLE dbo.OrgHierarchy");
|
||||
}
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
public void Create_ReturnsValidPipeline()
|
||||
{
|
||||
// Arrange
|
||||
var cacheFilePath = Path.Combine(_cacheDirectory, OrgHierarchyDevEtl.CacheFileName);
|
||||
if (!File.Exists(cacheFilePath))
|
||||
{
|
||||
// Skip test if cache file doesn't exist
|
||||
return;
|
||||
}
|
||||
|
||||
// Act
|
||||
var pipeline = OrgHierarchyDevEtl.Create(_connectionFactory, cacheFilePath);
|
||||
|
||||
// Assert
|
||||
pipeline.ShouldNotBeNull();
|
||||
pipeline.PipelineName.ShouldBe("OrgHierarchy_Dev");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_LoadsOrgHierarchyData()
|
||||
{
|
||||
// Arrange
|
||||
var cacheFilePath = Path.Combine(_cacheDirectory, OrgHierarchyDevEtl.CacheFileName);
|
||||
if (!File.Exists(cacheFilePath))
|
||||
{
|
||||
// Skip test if cache file doesn't exist
|
||||
return;
|
||||
}
|
||||
|
||||
var pipeline = OrgHierarchyDevEtl.Create(_connectionFactory, cacheFilePath);
|
||||
|
||||
// Act
|
||||
var result = await pipeline.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed");
|
||||
result.TotalRows.ShouldBeGreaterThan(0, "Should load at least one row");
|
||||
|
||||
// Verify data in database
|
||||
await using var connection = new SqlConnection(_connectionString);
|
||||
await connection.OpenAsync();
|
||||
var count = await connection.ExecuteScalarAsync<int>("SELECT COUNT(*) FROM dbo.OrgHierarchy");
|
||||
|
||||
count.ShouldBe((int)result.TotalRows, "Database row count should match pipeline result");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Registry_RunAsync_LoadsOrgHierarchy()
|
||||
{
|
||||
// Arrange
|
||||
if (!Directory.Exists(_cacheDirectory))
|
||||
{
|
||||
// Skip test if cache directory doesn't exist
|
||||
return;
|
||||
}
|
||||
|
||||
var registry = new DevEtlRegistry(_connectionFactory, _cacheDirectory);
|
||||
|
||||
// Act
|
||||
var result = await registry.RunAsync("OrgHierarchy");
|
||||
|
||||
// Assert
|
||||
result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed");
|
||||
result.TotalRows.ShouldBeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithNullConnectionFactory_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
var cacheFilePath = Path.Combine(_cacheDirectory, OrgHierarchyDevEtl.CacheFileName);
|
||||
|
||||
// Act & Assert
|
||||
Should.Throw<ArgumentNullException>(() => OrgHierarchyDevEtl.Create(null!, cacheFilePath));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithEmptyCacheFilePath_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange
|
||||
var mockFactory = Substitute.For<IDbConnectionFactory>();
|
||||
|
||||
// Act & Assert
|
||||
Should.Throw<ArgumentException>(() => OrgHierarchyDevEtl.Create(mockFactory, string.Empty));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithNonExistentCacheFile_ThrowsFileNotFoundException()
|
||||
{
|
||||
// Arrange
|
||||
var mockFactory = Substitute.For<IDbConnectionFactory>();
|
||||
var nonExistentPath = "/nonexistent/path/orghierarchy.json.zstd";
|
||||
|
||||
// Act & Assert
|
||||
Should.Throw<FileNotFoundException>(() => OrgHierarchyDevEtl.Create(mockFactory, nonExistentPath));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DevEtlRegistry_GetAvailableTables_IncludesOrgHierarchy()
|
||||
{
|
||||
// Arrange
|
||||
if (!Directory.Exists(_cacheDirectory))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var registry = new DevEtlRegistry(_connectionFactory, _cacheDirectory);
|
||||
|
||||
// Act
|
||||
var tables = registry.GetAvailableTables().ToList();
|
||||
|
||||
// Assert
|
||||
tables.ShouldContain("OrgHierarchy");
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
using Dapper;
|
||||
using JdeScoping.DataAccess;
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using JdeScoping.DataSync.Dev;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NSubstitute;
|
||||
using Shouldly;
|
||||
|
||||
namespace JdeScoping.DataSync.Dev.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for ProfitCenter development ETL.
|
||||
/// Requires: Local SQL Server, CACHED_DB_FILES directory with profitcenter.json.zstd
|
||||
/// </summary>
|
||||
public class ProfitCenterDevEtlTests : IAsyncLifetime
|
||||
{
|
||||
private readonly string _connectionString;
|
||||
private readonly string _cacheDirectory;
|
||||
private readonly IDbConnectionFactory _connectionFactory;
|
||||
|
||||
public ProfitCenterDevEtlTests()
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddJsonFile("appsettings.json", optional: true)
|
||||
.AddEnvironmentVariables()
|
||||
.Build();
|
||||
|
||||
_connectionString = config.GetConnectionString("LotFinderDB")
|
||||
?? throw new InvalidOperationException("LotFinderDB connection string not configured.");
|
||||
|
||||
_cacheDirectory = config["DevEtl:CacheDirectory"]
|
||||
?? Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "..", "CACHED_DB_FILES");
|
||||
|
||||
_connectionFactory = new DbConnectionFactory(config, NullLogger<DbConnectionFactory>.Instance);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
await using var connection = new SqlConnection(_connectionString);
|
||||
await connection.OpenAsync();
|
||||
await connection.ExecuteAsync("TRUNCATE TABLE dbo.ProfitCenter");
|
||||
}
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
public void Create_ReturnsValidPipeline()
|
||||
{
|
||||
var cacheFilePath = Path.Combine(_cacheDirectory, ProfitCenterDevEtl.CacheFileName);
|
||||
if (!File.Exists(cacheFilePath)) return;
|
||||
|
||||
var pipeline = ProfitCenterDevEtl.Create(_connectionFactory, cacheFilePath);
|
||||
|
||||
pipeline.ShouldNotBeNull();
|
||||
pipeline.PipelineName.ShouldBe("ProfitCenter_Dev");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_LoadsProfitCenterData()
|
||||
{
|
||||
var cacheFilePath = Path.Combine(_cacheDirectory, ProfitCenterDevEtl.CacheFileName);
|
||||
if (!File.Exists(cacheFilePath)) return;
|
||||
|
||||
var pipeline = ProfitCenterDevEtl.Create(_connectionFactory, cacheFilePath);
|
||||
|
||||
var result = await pipeline.ExecuteAsync();
|
||||
|
||||
result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed");
|
||||
result.TotalRows.ShouldBeGreaterThan(0, "Should load at least one row");
|
||||
|
||||
await using var connection = new SqlConnection(_connectionString);
|
||||
await connection.OpenAsync();
|
||||
var count = await connection.ExecuteScalarAsync<int>("SELECT COUNT(*) FROM dbo.ProfitCenter");
|
||||
|
||||
count.ShouldBe((int)result.TotalRows);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Registry_RunAsync_LoadsProfitCenter()
|
||||
{
|
||||
if (!Directory.Exists(_cacheDirectory)) return;
|
||||
|
||||
var registry = new DevEtlRegistry(_connectionFactory, _cacheDirectory);
|
||||
var result = await registry.RunAsync("ProfitCenter");
|
||||
|
||||
result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed");
|
||||
result.TotalRows.ShouldBeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithNullConnectionFactory_ThrowsArgumentNullException()
|
||||
{
|
||||
var cacheFilePath = Path.Combine(_cacheDirectory, ProfitCenterDevEtl.CacheFileName);
|
||||
Should.Throw<ArgumentNullException>(() => ProfitCenterDevEtl.Create(null!, cacheFilePath));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithEmptyCacheFilePath_ThrowsArgumentException()
|
||||
{
|
||||
var mockFactory = Substitute.For<IDbConnectionFactory>();
|
||||
Should.Throw<ArgumentException>(() => ProfitCenterDevEtl.Create(mockFactory, string.Empty));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DevEtlRegistry_GetAvailableTables_IncludesProfitCenter()
|
||||
{
|
||||
if (!Directory.Exists(_cacheDirectory)) return;
|
||||
|
||||
var registry = new DevEtlRegistry(_connectionFactory, _cacheDirectory);
|
||||
var tables = registry.GetAvailableTables().ToList();
|
||||
|
||||
tables.ShouldContain("ProfitCenter");
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
using Dapper;
|
||||
using JdeScoping.DataAccess;
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using JdeScoping.DataSync.Dev;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NSubstitute;
|
||||
using Shouldly;
|
||||
|
||||
namespace JdeScoping.DataSync.Dev.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for RouteMaster development ETL.
|
||||
/// </summary>
|
||||
public class RouteMasterDevEtlTests : IAsyncLifetime
|
||||
{
|
||||
private readonly string _connectionString;
|
||||
private readonly string _cacheDirectory;
|
||||
private readonly IDbConnectionFactory _connectionFactory;
|
||||
|
||||
public RouteMasterDevEtlTests()
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddJsonFile("appsettings.json", optional: true)
|
||||
.AddEnvironmentVariables()
|
||||
.Build();
|
||||
|
||||
_connectionString = config.GetConnectionString("LotFinderDB")
|
||||
?? throw new InvalidOperationException("LotFinderDB connection string not configured.");
|
||||
|
||||
_cacheDirectory = config["DevEtl:CacheDirectory"]
|
||||
?? Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "..", "CACHED_DB_FILES");
|
||||
|
||||
_connectionFactory = new DbConnectionFactory(config, NullLogger<DbConnectionFactory>.Instance);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
await using var connection = new SqlConnection(_connectionString);
|
||||
await connection.OpenAsync();
|
||||
await connection.ExecuteAsync("TRUNCATE TABLE dbo.RouteMaster");
|
||||
}
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
public void Create_ReturnsValidPipeline()
|
||||
{
|
||||
var cacheFilePath = Path.Combine(_cacheDirectory, RouteMasterDevEtl.CacheFileName);
|
||||
if (!File.Exists(cacheFilePath)) return;
|
||||
|
||||
var pipeline = RouteMasterDevEtl.Create(_connectionFactory, cacheFilePath);
|
||||
|
||||
pipeline.ShouldNotBeNull();
|
||||
pipeline.PipelineName.ShouldBe("RouteMaster_Dev");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_LoadsData()
|
||||
{
|
||||
var cacheFilePath = Path.Combine(_cacheDirectory, RouteMasterDevEtl.CacheFileName);
|
||||
if (!File.Exists(cacheFilePath)) return;
|
||||
|
||||
var pipeline = RouteMasterDevEtl.Create(_connectionFactory, cacheFilePath);
|
||||
var result = await pipeline.ExecuteAsync();
|
||||
|
||||
result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed");
|
||||
result.TotalRows.ShouldBeGreaterThan(0);
|
||||
|
||||
await using var connection = new SqlConnection(_connectionString);
|
||||
await connection.OpenAsync();
|
||||
var count = await connection.ExecuteScalarAsync<int>("SELECT COUNT(*) FROM dbo.RouteMaster");
|
||||
count.ShouldBe((int)result.TotalRows);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Registry_RunAsync_LoadsTable()
|
||||
{
|
||||
if (!Directory.Exists(_cacheDirectory)) return;
|
||||
|
||||
var registry = new DevEtlRegistry(_connectionFactory, _cacheDirectory);
|
||||
var result = await registry.RunAsync("RouteMaster");
|
||||
|
||||
result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed");
|
||||
result.TotalRows.ShouldBeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithNullConnectionFactory_ThrowsArgumentNullException()
|
||||
{
|
||||
var cacheFilePath = Path.Combine(_cacheDirectory, RouteMasterDevEtl.CacheFileName);
|
||||
Should.Throw<ArgumentNullException>(() => RouteMasterDevEtl.Create(null!, cacheFilePath));
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
using Dapper;
|
||||
using JdeScoping.DataAccess;
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using JdeScoping.DataSync.Dev;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NSubstitute;
|
||||
using Shouldly;
|
||||
|
||||
namespace JdeScoping.DataSync.Dev.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for WorkCenter development ETL.
|
||||
/// Requires: Local SQL Server, CACHED_DB_FILES directory with workcenter.json.zstd
|
||||
/// </summary>
|
||||
public class WorkCenterDevEtlTests : IAsyncLifetime
|
||||
{
|
||||
private readonly string _connectionString;
|
||||
private readonly string _cacheDirectory;
|
||||
private readonly IDbConnectionFactory _connectionFactory;
|
||||
|
||||
public WorkCenterDevEtlTests()
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddJsonFile("appsettings.json", optional: true)
|
||||
.AddEnvironmentVariables()
|
||||
.Build();
|
||||
|
||||
_connectionString = config.GetConnectionString("LotFinderDB")
|
||||
?? throw new InvalidOperationException("LotFinderDB connection string not configured.");
|
||||
|
||||
_cacheDirectory = config["DevEtl:CacheDirectory"]
|
||||
?? Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "..", "CACHED_DB_FILES");
|
||||
|
||||
_connectionFactory = new DbConnectionFactory(config, NullLogger<DbConnectionFactory>.Instance);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
await using var connection = new SqlConnection(_connectionString);
|
||||
await connection.OpenAsync();
|
||||
await connection.ExecuteAsync("TRUNCATE TABLE dbo.WorkCenter");
|
||||
}
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
public void Create_ReturnsValidPipeline()
|
||||
{
|
||||
var cacheFilePath = Path.Combine(_cacheDirectory, WorkCenterDevEtl.CacheFileName);
|
||||
if (!File.Exists(cacheFilePath)) return;
|
||||
|
||||
var pipeline = WorkCenterDevEtl.Create(_connectionFactory, cacheFilePath);
|
||||
|
||||
pipeline.ShouldNotBeNull();
|
||||
pipeline.PipelineName.ShouldBe("WorkCenter_Dev");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_LoadsWorkCenterData()
|
||||
{
|
||||
var cacheFilePath = Path.Combine(_cacheDirectory, WorkCenterDevEtl.CacheFileName);
|
||||
if (!File.Exists(cacheFilePath)) return;
|
||||
|
||||
var pipeline = WorkCenterDevEtl.Create(_connectionFactory, cacheFilePath);
|
||||
|
||||
var result = await pipeline.ExecuteAsync();
|
||||
|
||||
result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed");
|
||||
result.TotalRows.ShouldBeGreaterThan(0, "Should load at least one row");
|
||||
|
||||
await using var connection = new SqlConnection(_connectionString);
|
||||
await connection.OpenAsync();
|
||||
var count = await connection.ExecuteScalarAsync<int>("SELECT COUNT(*) FROM dbo.WorkCenter");
|
||||
|
||||
count.ShouldBe((int)result.TotalRows);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Registry_RunAsync_LoadsWorkCenter()
|
||||
{
|
||||
if (!Directory.Exists(_cacheDirectory)) return;
|
||||
|
||||
var registry = new DevEtlRegistry(_connectionFactory, _cacheDirectory);
|
||||
var result = await registry.RunAsync("WorkCenter");
|
||||
|
||||
result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed");
|
||||
result.TotalRows.ShouldBeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithNullConnectionFactory_ThrowsArgumentNullException()
|
||||
{
|
||||
var cacheFilePath = Path.Combine(_cacheDirectory, WorkCenterDevEtl.CacheFileName);
|
||||
Should.Throw<ArgumentNullException>(() => WorkCenterDevEtl.Create(null!, cacheFilePath));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithEmptyCacheFilePath_ThrowsArgumentException()
|
||||
{
|
||||
var mockFactory = Substitute.For<IDbConnectionFactory>();
|
||||
Should.Throw<ArgumentException>(() => WorkCenterDevEtl.Create(mockFactory, string.Empty));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DevEtlRegistry_GetAvailableTables_IncludesWorkCenter()
|
||||
{
|
||||
if (!Directory.Exists(_cacheDirectory)) return;
|
||||
|
||||
var registry = new DevEtlRegistry(_connectionFactory, _cacheDirectory);
|
||||
var tables = registry.GetAvailableTables().ToList();
|
||||
|
||||
tables.ShouldContain("WorkCenter");
|
||||
}
|
||||
}
|
||||
@@ -66,7 +66,7 @@ public class SearchRepositoryTests
|
||||
var repository = new SearchRepository(_connectionFactory, _logger);
|
||||
|
||||
// Assert
|
||||
repository.ShouldBeAssignableTo<JdeScoping.DataSync.Contracts.ISearchRepository>();
|
||||
repository.ShouldBeAssignableTo<Contracts.ISearchRepository>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user