feat: implement ETL pipeline redesign and ConfigManager improvements

- Add pipeline registry with JSON-based configuration and hot-reload support
- Implement manual sync request feature with API, client UI, and database
- Improve ConfigManager: connection string dropdown in pipeline editor,
  step delete/reorder functionality, and fix JSON parsing for ConnectionStrings
This commit is contained in:
Joseph Doherty
2026-01-22 17:48:33 -05:00
parent 5a332232d0
commit 29ac56006d
82 changed files with 6257 additions and 296 deletions
@@ -0,0 +1,22 @@
namespace JdeScoping.DataSync.Configuration;
/// <summary>
/// Configuration for the pipeline destination.
/// </summary>
public class DestinationElement
{
/// <summary>
/// Target table name in the cache database.
/// </summary>
public string Table { get; set; } = string.Empty;
/// <summary>
/// Columns used to match existing records for upsert.
/// </summary>
public List<string> MatchColumns { get; set; } = [];
/// <summary>
/// Columns to exclude from UPDATE operations.
/// </summary>
public List<string> ExcludeFromUpdate { get; set; } = [];
}
@@ -0,0 +1,78 @@
namespace JdeScoping.DataSync.Configuration;
/// <summary>
/// Represents an ETL pipeline definition loaded from a JSON file.
/// </summary>
public class EtlPipelineConfig
{
/// <summary>
/// Unique name of the pipeline (must match filename, case-insensitive).
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Whether the pipeline is enabled for execution.
/// </summary>
public bool IsEnabled { get; set; } = true;
/// <summary>
/// If true, pipeline can only be triggered via ManualSyncRequest.
/// No interval validation is required for manual-only pipelines.
/// </summary>
public bool IsManualOnly { get; set; }
/// <summary>
/// Interval for mass sync in minutes. Null if mass sync not supported.
/// </summary>
public int? MassSyncIntervalMinutes { get; set; }
/// <summary>
/// Interval for daily sync in minutes. Null if daily sync not supported.
/// </summary>
public int? DailySyncIntervalMinutes { get; set; }
/// <summary>
/// Interval for hourly sync in minutes. Null if hourly sync not supported.
/// </summary>
public int? HourlySyncIntervalMinutes { get; set; }
/// <summary>
/// Scripts to run before the main sync. Optional.
/// </summary>
public List<ScriptElement> PreScripts { get; set; } = [];
/// <summary>
/// The data source configuration. Required.
/// </summary>
public SourceElement Source { get; set; } = null!;
/// <summary>
/// Data transformations to apply. Optional.
/// </summary>
public List<TransformElement> Transforms { get; set; } = [];
/// <summary>
/// The destination configuration. Required.
/// </summary>
public DestinationElement Destination { get; set; } = null!;
/// <summary>
/// Scripts to run after the main sync. Optional.
/// </summary>
public List<ScriptElement> PostScripts { get; set; } = [];
/// <summary>
/// Gets a value indicating whether the pipeline supports mass sync.
/// </summary>
public bool SupportsMassSync => MassSyncIntervalMinutes.HasValue;
/// <summary>
/// Gets a value indicating whether the pipeline supports daily sync.
/// </summary>
public bool SupportsDailySync => DailySyncIntervalMinutes.HasValue;
/// <summary>
/// Gets a value indicating whether the pipeline supports hourly sync.
/// </summary>
public bool SupportsHourlySync => HourlySyncIntervalMinutes.HasValue;
}
@@ -0,0 +1,27 @@
namespace JdeScoping.DataSync.Configuration;
/// <summary>
/// Configuration for a query parameter.
/// </summary>
public class ParameterElement
{
/// <summary>
/// Parameter name as used in query (e.g., ":dateUpdated").
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Format conversion (jdeJulian, jdeTime, etc.).
/// </summary>
public string? Format { get; set; }
/// <summary>
/// Source of the value (offset = from last sync time).
/// </summary>
public string Source { get; set; } = "offset";
/// <summary>
/// Static value if source is not offset.
/// </summary>
public string? Value { get; set; }
}
@@ -0,0 +1,17 @@
namespace JdeScoping.DataSync.Configuration;
/// <summary>
/// Configuration for a pre/post script.
/// </summary>
public class ScriptElement
{
/// <summary>
/// Connection identifier for script execution.
/// </summary>
public string Connection { get; set; } = "lotfinder";
/// <summary>
/// SQL script to execute.
/// </summary>
public string Script { get; set; } = string.Empty;
}
@@ -0,0 +1,27 @@
namespace JdeScoping.DataSync.Configuration;
/// <summary>
/// Configuration for the pipeline data source.
/// </summary>
public class SourceElement
{
/// <summary>
/// Connection identifier (jde, cms, giw, lotfinder).
/// </summary>
public string Connection { get; set; } = string.Empty;
/// <summary>
/// Query for incremental syncs (daily/hourly).
/// </summary>
public string Query { get; set; } = string.Empty;
/// <summary>
/// Query for mass sync. Falls back to Query if not specified.
/// </summary>
public string? MassQuery { get; set; }
/// <summary>
/// Query parameters with format and source configuration.
/// </summary>
public Dictionary<string, ParameterElement> Parameters { get; set; } = new();
}
@@ -0,0 +1,21 @@
using System.Text.Json;
namespace JdeScoping.DataSync.Configuration;
/// <summary>
/// Configuration for a data transformation.
/// </summary>
public class TransformElement
{
/// <summary>
/// Type of transformation (ColumnDrop, ColumnRename, JdeDate, Regex, etc.).
/// </summary>
public string TransformType { get; set; } = string.Empty;
/// <summary>
/// Transform-specific configuration as raw JSON.
/// Using JsonElement avoids Dictionary&lt;string, object&gt; deserialization issues
/// where values would become JsonElement anyway without custom converters.
/// </summary>
public JsonElement? Config { get; set; }
}