refactor: rename ScadaLink → ZB.MOM.WW.ScadaBridge (code + projects + namespaces)
Solution + 23 src projects + 26 test projects renamed; folders, csproj, namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated. ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated. SQL roles/logins, LDAP domains, CLI command name, and CLI config dir (~/.scadalink → ~/.scadabridge) also renamed. Build green; 5 Host.Tests fail awaiting SQL login rename in next commit. Pre-existing StaleTagMonitor timing flakes unchanged. Rename script committed at tools/rename-to-scadabridge.sh.
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// Hosted service that initializes the SQLite schema on startup.
|
||||
/// Runs before the Akka actor system starts creating actors.
|
||||
/// </summary>
|
||||
public class SiteStorageInitializer : IHostedService
|
||||
{
|
||||
private readonly SiteStorageService _storage;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <see cref="SiteStorageInitializer"/>.
|
||||
/// </summary>
|
||||
/// <param name="storage">The site storage service whose schema is initialized on startup.</param>
|
||||
public SiteStorageInitializer(SiteStorageService storage)
|
||||
{
|
||||
_storage = storage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the SQLite schema before the actor system starts.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token for the startup operation.</param>
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await _storage.InitializeAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// No-op stop; schema initialization does not require cleanup.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Unused cancellation token.</param>
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
@@ -0,0 +1,701 @@
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// Direct SQLite persistence for site-local deployment state.
|
||||
/// Stores deployed instance configurations (as JSON) and static attribute overrides.
|
||||
/// This is NOT EF Core — uses Microsoft.Data.Sqlite directly for lightweight site storage.
|
||||
/// </summary>
|
||||
public class SiteStorageService
|
||||
{
|
||||
private readonly string _connectionString;
|
||||
private readonly ILogger<SiteStorageService> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the SiteStorageService with the specified SQLite connection string and logger.
|
||||
/// </summary>
|
||||
/// <param name="connectionString">SQLite connection string for the site database.</param>
|
||||
/// <param name="logger">Logger instance for diagnostic messages.</param>
|
||||
public SiteStorageService(string connectionString, ILogger<SiteStorageService> logger)
|
||||
{
|
||||
_connectionString = connectionString;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new (unopened) SQLite connection against the site database.
|
||||
/// Exposed so site-local repositories can open their own connections without
|
||||
/// reaching into private state via reflection (SiteRuntime-006). The caller owns
|
||||
/// the connection and is responsible for opening and disposing it.
|
||||
/// </summary>
|
||||
public SqliteConnection CreateConnection() => new(_connectionString);
|
||||
|
||||
/// <summary>
|
||||
/// Creates the SQLite tables if they do not exist.
|
||||
/// Called once on site startup.
|
||||
/// </summary>
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
await using var connection = new SqliteConnection(_connectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = @"
|
||||
CREATE TABLE IF NOT EXISTS deployed_configurations (
|
||||
instance_unique_name TEXT PRIMARY KEY,
|
||||
config_json TEXT NOT NULL,
|
||||
deployment_id TEXT NOT NULL,
|
||||
revision_hash TEXT NOT NULL,
|
||||
is_enabled INTEGER NOT NULL DEFAULT 1,
|
||||
deployed_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS static_attribute_overrides (
|
||||
instance_unique_name TEXT NOT NULL,
|
||||
attribute_name TEXT NOT NULL,
|
||||
override_value TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
PRIMARY KEY (instance_unique_name, attribute_name)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS shared_scripts (
|
||||
name TEXT PRIMARY KEY,
|
||||
code TEXT NOT NULL,
|
||||
parameter_definitions TEXT,
|
||||
return_definition TEXT,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS external_systems (
|
||||
name TEXT PRIMARY KEY,
|
||||
endpoint_url TEXT NOT NULL,
|
||||
auth_type TEXT NOT NULL,
|
||||
auth_configuration TEXT,
|
||||
method_definitions TEXT,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS database_connections (
|
||||
name TEXT PRIMARY KEY,
|
||||
connection_string TEXT NOT NULL,
|
||||
max_retries INTEGER NOT NULL DEFAULT 3,
|
||||
retry_delay_ms INTEGER NOT NULL DEFAULT 1000,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS notification_lists (
|
||||
name TEXT PRIMARY KEY,
|
||||
recipient_emails TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS data_connection_definitions (
|
||||
name TEXT PRIMARY KEY,
|
||||
protocol TEXT NOT NULL,
|
||||
configuration TEXT,
|
||||
backup_configuration TEXT,
|
||||
failover_retry_count INTEGER NOT NULL DEFAULT 3,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS smtp_configurations (
|
||||
name TEXT PRIMARY KEY,
|
||||
server TEXT NOT NULL,
|
||||
port INTEGER NOT NULL,
|
||||
auth_mode TEXT NOT NULL,
|
||||
from_address TEXT NOT NULL,
|
||||
username TEXT,
|
||||
password TEXT,
|
||||
oauth_config TEXT,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
";
|
||||
await command.ExecuteNonQueryAsync();
|
||||
|
||||
// Schema migrations — add columns that may not exist on older databases
|
||||
await MigrateSchemaAsync(connection);
|
||||
|
||||
_logger.LogInformation("Site SQLite storage initialized at {ConnectionString}", _connectionString);
|
||||
}
|
||||
|
||||
private async Task MigrateSchemaAsync(SqliteConnection connection)
|
||||
{
|
||||
// Add backup_configuration and failover_retry_count to data_connection_definitions
|
||||
// (added in primary/backup data connections feature)
|
||||
await TryAddColumnAsync(connection, "data_connection_definitions", "backup_configuration", "TEXT");
|
||||
await TryAddColumnAsync(connection, "data_connection_definitions", "failover_retry_count", "INTEGER NOT NULL DEFAULT 3");
|
||||
}
|
||||
|
||||
private async Task TryAddColumnAsync(SqliteConnection connection, string table, string column, string type)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var cmd = connection.CreateCommand();
|
||||
cmd.CommandText = $"ALTER TABLE {table} ADD COLUMN {column} {type}";
|
||||
await cmd.ExecuteNonQueryAsync();
|
||||
_logger.LogInformation("Migrated: added column {Column} to {Table}", column, table);
|
||||
}
|
||||
catch (SqliteException ex) when (ex.Message.Contains("duplicate column"))
|
||||
{
|
||||
// Column already exists — no action needed
|
||||
}
|
||||
}
|
||||
|
||||
// ── Deployed Configuration CRUD ──
|
||||
|
||||
/// <summary>
|
||||
/// Returns all deployed instance configurations from SQLite.
|
||||
/// </summary>
|
||||
public async Task<List<DeployedInstance>> GetAllDeployedConfigsAsync()
|
||||
{
|
||||
await using var connection = new SqliteConnection(_connectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = @"
|
||||
SELECT instance_unique_name, config_json, deployment_id, revision_hash, is_enabled, deployed_at
|
||||
FROM deployed_configurations";
|
||||
|
||||
var results = new List<DeployedInstance>();
|
||||
await using var reader = await command.ExecuteReaderAsync();
|
||||
while (await reader.ReadAsync())
|
||||
{
|
||||
results.Add(new DeployedInstance
|
||||
{
|
||||
InstanceUniqueName = reader.GetString(0),
|
||||
ConfigJson = reader.GetString(1),
|
||||
DeploymentId = reader.GetString(2),
|
||||
RevisionHash = reader.GetString(3),
|
||||
IsEnabled = reader.GetInt64(4) != 0,
|
||||
DeployedAt = reader.GetString(5)
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores or updates a deployed instance configuration. Uses UPSERT semantics.
|
||||
/// </summary>
|
||||
/// <param name="instanceName">The unique name of the instance.</param>
|
||||
/// <param name="configJson">The deployed configuration as JSON.</param>
|
||||
/// <param name="deploymentId">The unique deployment identifier.</param>
|
||||
/// <param name="revisionHash">The configuration revision hash for staleness detection.</param>
|
||||
/// <param name="isEnabled">Whether the instance is enabled.</param>
|
||||
public async Task StoreDeployedConfigAsync(
|
||||
string instanceName,
|
||||
string configJson,
|
||||
string deploymentId,
|
||||
string revisionHash,
|
||||
bool isEnabled)
|
||||
{
|
||||
await using var connection = new SqliteConnection(_connectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = @"
|
||||
INSERT INTO deployed_configurations (instance_unique_name, config_json, deployment_id, revision_hash, is_enabled, deployed_at)
|
||||
VALUES (@name, @json, @depId, @hash, @enabled, @deployedAt)
|
||||
ON CONFLICT(instance_unique_name) DO UPDATE SET
|
||||
config_json = excluded.config_json,
|
||||
deployment_id = excluded.deployment_id,
|
||||
revision_hash = excluded.revision_hash,
|
||||
is_enabled = excluded.is_enabled,
|
||||
deployed_at = excluded.deployed_at";
|
||||
|
||||
command.Parameters.AddWithValue("@name", instanceName);
|
||||
command.Parameters.AddWithValue("@json", configJson);
|
||||
command.Parameters.AddWithValue("@depId", deploymentId);
|
||||
command.Parameters.AddWithValue("@hash", revisionHash);
|
||||
command.Parameters.AddWithValue("@enabled", isEnabled ? 1 : 0);
|
||||
command.Parameters.AddWithValue("@deployedAt", DateTimeOffset.UtcNow.ToString("O"));
|
||||
|
||||
await command.ExecuteNonQueryAsync();
|
||||
_logger.LogDebug("Stored deployed config for {Instance}, deploymentId={DeploymentId}", instanceName, deploymentId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a deployed instance configuration and its static overrides.
|
||||
/// </summary>
|
||||
/// <param name="instanceName">The unique name of the instance to remove.</param>
|
||||
public async Task RemoveDeployedConfigAsync(string instanceName)
|
||||
{
|
||||
await using var connection = new SqliteConnection(_connectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
await using var transaction = await connection.BeginTransactionAsync();
|
||||
|
||||
await using (var cmd = connection.CreateCommand())
|
||||
{
|
||||
cmd.Transaction = (SqliteTransaction)transaction;
|
||||
cmd.CommandText = "DELETE FROM static_attribute_overrides WHERE instance_unique_name = @name";
|
||||
cmd.Parameters.AddWithValue("@name", instanceName);
|
||||
await cmd.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
await using (var cmd = connection.CreateCommand())
|
||||
{
|
||||
cmd.Transaction = (SqliteTransaction)transaction;
|
||||
cmd.CommandText = "DELETE FROM deployed_configurations WHERE instance_unique_name = @name";
|
||||
cmd.Parameters.AddWithValue("@name", instanceName);
|
||||
await cmd.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
await transaction.CommitAsync();
|
||||
_logger.LogInformation("Removed deployed config and overrides for {Instance}", instanceName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the enabled/disabled state of a deployed instance.
|
||||
/// </summary>
|
||||
/// <param name="instanceName">The unique name of the instance.</param>
|
||||
/// <param name="isEnabled">Whether the instance should be enabled.</param>
|
||||
public async Task SetInstanceEnabledAsync(string instanceName, bool isEnabled)
|
||||
{
|
||||
await using var connection = new SqliteConnection(_connectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = @"
|
||||
UPDATE deployed_configurations
|
||||
SET is_enabled = @enabled
|
||||
WHERE instance_unique_name = @name";
|
||||
|
||||
command.Parameters.AddWithValue("@enabled", isEnabled ? 1 : 0);
|
||||
command.Parameters.AddWithValue("@name", instanceName);
|
||||
|
||||
var rows = await command.ExecuteNonQueryAsync();
|
||||
if (rows == 0)
|
||||
{
|
||||
_logger.LogWarning("SetInstanceEnabled: instance {Instance} not found", instanceName);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Static Attribute Override CRUD ──
|
||||
|
||||
/// <summary>
|
||||
/// Returns all static attribute overrides for an instance.
|
||||
/// </summary>
|
||||
/// <param name="instanceName">The unique name of the instance.</param>
|
||||
public async Task<Dictionary<string, string>> GetStaticOverridesAsync(string instanceName)
|
||||
{
|
||||
await using var connection = new SqliteConnection(_connectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = @"
|
||||
SELECT attribute_name, override_value
|
||||
FROM static_attribute_overrides
|
||||
WHERE instance_unique_name = @name";
|
||||
command.Parameters.AddWithValue("@name", instanceName);
|
||||
|
||||
var results = new Dictionary<string, string>();
|
||||
await using var reader = await command.ExecuteReaderAsync();
|
||||
while (await reader.ReadAsync())
|
||||
{
|
||||
results[reader.GetString(0)] = reader.GetString(1);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets or updates a single static attribute override for an instance.
|
||||
/// </summary>
|
||||
/// <param name="instanceName">The unique name of the instance.</param>
|
||||
/// <param name="attributeName">The name of the attribute to override.</param>
|
||||
/// <param name="value">The override value for the attribute.</param>
|
||||
public async Task SetStaticOverrideAsync(string instanceName, string attributeName, string value)
|
||||
{
|
||||
await using var connection = new SqliteConnection(_connectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = @"
|
||||
INSERT INTO static_attribute_overrides (instance_unique_name, attribute_name, override_value, updated_at)
|
||||
VALUES (@name, @attr, @val, @updatedAt)
|
||||
ON CONFLICT(instance_unique_name, attribute_name) DO UPDATE SET
|
||||
override_value = excluded.override_value,
|
||||
updated_at = excluded.updated_at";
|
||||
|
||||
command.Parameters.AddWithValue("@name", instanceName);
|
||||
command.Parameters.AddWithValue("@attr", attributeName);
|
||||
command.Parameters.AddWithValue("@val", value);
|
||||
command.Parameters.AddWithValue("@updatedAt", DateTimeOffset.UtcNow.ToString("O"));
|
||||
|
||||
await command.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all static attribute overrides for an instance.
|
||||
/// Called on redeployment to reset overrides.
|
||||
/// </summary>
|
||||
/// <param name="instanceName">The unique name of the instance.</param>
|
||||
public async Task ClearStaticOverridesAsync(string instanceName)
|
||||
{
|
||||
await using var connection = new SqliteConnection(_connectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = "DELETE FROM static_attribute_overrides WHERE instance_unique_name = @name";
|
||||
command.Parameters.AddWithValue("@name", instanceName);
|
||||
|
||||
await command.ExecuteNonQueryAsync();
|
||||
_logger.LogDebug("Cleared static overrides for {Instance}", instanceName);
|
||||
}
|
||||
|
||||
// ── WP-33: Shared Script CRUD ──
|
||||
|
||||
/// <summary>
|
||||
/// Stores or updates a shared script. Uses UPSERT semantics.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the shared script.</param>
|
||||
/// <param name="code">The script code.</param>
|
||||
/// <param name="parameterDefs">JSON representation of parameter definitions, if any.</param>
|
||||
/// <param name="returnDef">JSON representation of the return type definition, if any.</param>
|
||||
public async Task StoreSharedScriptAsync(string name, string code, string? parameterDefs, string? returnDef)
|
||||
{
|
||||
await using var connection = new SqliteConnection(_connectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = @"
|
||||
INSERT INTO shared_scripts (name, code, parameter_definitions, return_definition, updated_at)
|
||||
VALUES (@name, @code, @paramDefs, @returnDef, @updatedAt)
|
||||
ON CONFLICT(name) DO UPDATE SET
|
||||
code = excluded.code,
|
||||
parameter_definitions = excluded.parameter_definitions,
|
||||
return_definition = excluded.return_definition,
|
||||
updated_at = excluded.updated_at";
|
||||
|
||||
command.Parameters.AddWithValue("@name", name);
|
||||
command.Parameters.AddWithValue("@code", code);
|
||||
command.Parameters.AddWithValue("@paramDefs", (object?)parameterDefs ?? DBNull.Value);
|
||||
command.Parameters.AddWithValue("@returnDef", (object?)returnDef ?? DBNull.Value);
|
||||
command.Parameters.AddWithValue("@updatedAt", DateTimeOffset.UtcNow.ToString("O"));
|
||||
|
||||
await command.ExecuteNonQueryAsync();
|
||||
_logger.LogDebug("Stored shared script '{Name}'", name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all stored shared scripts.
|
||||
/// </summary>
|
||||
public async Task<List<StoredSharedScript>> GetAllSharedScriptsAsync()
|
||||
{
|
||||
await using var connection = new SqliteConnection(_connectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = "SELECT name, code, parameter_definitions, return_definition FROM shared_scripts";
|
||||
|
||||
var results = new List<StoredSharedScript>();
|
||||
await using var reader = await command.ExecuteReaderAsync();
|
||||
while (await reader.ReadAsync())
|
||||
{
|
||||
results.Add(new StoredSharedScript
|
||||
{
|
||||
Name = reader.GetString(0),
|
||||
Code = reader.GetString(1),
|
||||
ParameterDefinitions = reader.IsDBNull(2) ? null : reader.GetString(2),
|
||||
ReturnDefinition = reader.IsDBNull(3) ? null : reader.GetString(3)
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// ── WP-33: External System CRUD ──
|
||||
|
||||
/// <summary>
|
||||
/// Stores or updates an external system definition.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the external system.</param>
|
||||
/// <param name="endpointUrl">The REST API endpoint URL.</param>
|
||||
/// <param name="authType">The authentication type (e.g., 'ApiKey', 'BasicAuth').</param>
|
||||
/// <param name="authConfig">Authentication configuration JSON, if applicable.</param>
|
||||
/// <param name="methodDefs">JSON representation of available method definitions, if any.</param>
|
||||
public async Task StoreExternalSystemAsync(
|
||||
string name, string endpointUrl, string authType, string? authConfig, string? methodDefs)
|
||||
{
|
||||
await using var connection = new SqliteConnection(_connectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = @"
|
||||
INSERT INTO external_systems (name, endpoint_url, auth_type, auth_configuration, method_definitions, updated_at)
|
||||
VALUES (@name, @url, @authType, @authConfig, @methodDefs, @updatedAt)
|
||||
ON CONFLICT(name) DO UPDATE SET
|
||||
endpoint_url = excluded.endpoint_url,
|
||||
auth_type = excluded.auth_type,
|
||||
auth_configuration = excluded.auth_configuration,
|
||||
method_definitions = excluded.method_definitions,
|
||||
updated_at = excluded.updated_at";
|
||||
|
||||
command.Parameters.AddWithValue("@name", name);
|
||||
command.Parameters.AddWithValue("@url", endpointUrl);
|
||||
command.Parameters.AddWithValue("@authType", authType);
|
||||
command.Parameters.AddWithValue("@authConfig", (object?)authConfig ?? DBNull.Value);
|
||||
command.Parameters.AddWithValue("@methodDefs", (object?)methodDefs ?? DBNull.Value);
|
||||
command.Parameters.AddWithValue("@updatedAt", DateTimeOffset.UtcNow.ToString("O"));
|
||||
|
||||
await command.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
// ── WP-33: Database Connection CRUD ──
|
||||
|
||||
/// <summary>
|
||||
/// Stores or updates a database connection definition.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the database connection.</param>
|
||||
/// <param name="connectionString">The database connection string.</param>
|
||||
/// <param name="maxRetries">Maximum number of retry attempts.</param>
|
||||
/// <param name="retryDelay">Delay between retry attempts.</param>
|
||||
public async Task StoreDatabaseConnectionAsync(
|
||||
string name, string connectionString, int maxRetries, TimeSpan retryDelay)
|
||||
{
|
||||
await using var connection = new SqliteConnection(_connectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = @"
|
||||
INSERT INTO database_connections (name, connection_string, max_retries, retry_delay_ms, updated_at)
|
||||
VALUES (@name, @connStr, @maxRetries, @retryDelayMs, @updatedAt)
|
||||
ON CONFLICT(name) DO UPDATE SET
|
||||
connection_string = excluded.connection_string,
|
||||
max_retries = excluded.max_retries,
|
||||
retry_delay_ms = excluded.retry_delay_ms,
|
||||
updated_at = excluded.updated_at";
|
||||
|
||||
command.Parameters.AddWithValue("@name", name);
|
||||
command.Parameters.AddWithValue("@connStr", connectionString);
|
||||
command.Parameters.AddWithValue("@maxRetries", maxRetries);
|
||||
command.Parameters.AddWithValue("@retryDelayMs", (long)retryDelay.TotalMilliseconds);
|
||||
command.Parameters.AddWithValue("@updatedAt", DateTimeOffset.UtcNow.ToString("O"));
|
||||
|
||||
await command.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
// ── WP-33: Notification List CRUD ──
|
||||
|
||||
/// <summary>
|
||||
/// Stores or updates a notification list.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the notification list.</param>
|
||||
/// <param name="recipientEmails">List of recipient email addresses.</param>
|
||||
public async Task StoreNotificationListAsync(string name, IReadOnlyList<string> recipientEmails)
|
||||
{
|
||||
await using var connection = new SqliteConnection(_connectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = @"
|
||||
INSERT INTO notification_lists (name, recipient_emails, updated_at)
|
||||
VALUES (@name, @emails, @updatedAt)
|
||||
ON CONFLICT(name) DO UPDATE SET
|
||||
recipient_emails = excluded.recipient_emails,
|
||||
updated_at = excluded.updated_at";
|
||||
|
||||
command.Parameters.AddWithValue("@name", name);
|
||||
command.Parameters.AddWithValue("@emails", System.Text.Json.JsonSerializer.Serialize(recipientEmails));
|
||||
command.Parameters.AddWithValue("@updatedAt", DateTimeOffset.UtcNow.ToString("O"));
|
||||
|
||||
await command.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
// ── WP-33: SMTP Configuration CRUD ──
|
||||
|
||||
/// <summary>
|
||||
/// Stores or updates an SMTP configuration.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the SMTP configuration.</param>
|
||||
/// <param name="server">SMTP server hostname.</param>
|
||||
/// <param name="port">SMTP server port.</param>
|
||||
/// <param name="authMode">Authentication mode ('None', 'BasicAuth', 'OAuth2').</param>
|
||||
/// <param name="fromAddress">Email address used as the sender.</param>
|
||||
/// <param name="username">Username for authentication, if applicable.</param>
|
||||
/// <param name="password">Password for authentication, if applicable.</param>
|
||||
/// <param name="oauthConfig">OAuth2 configuration JSON, if applicable.</param>
|
||||
public async Task StoreSmtpConfigurationAsync(
|
||||
string name, string server, int port, string authMode, string fromAddress,
|
||||
string? username, string? password, string? oauthConfig)
|
||||
{
|
||||
await using var connection = new SqliteConnection(_connectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = @"
|
||||
INSERT INTO smtp_configurations (name, server, port, auth_mode, from_address, username, password, oauth_config, updated_at)
|
||||
VALUES (@name, @server, @port, @authMode, @fromAddress, @username, @password, @oauthConfig, @updatedAt)
|
||||
ON CONFLICT(name) DO UPDATE SET
|
||||
server = excluded.server,
|
||||
port = excluded.port,
|
||||
auth_mode = excluded.auth_mode,
|
||||
from_address = excluded.from_address,
|
||||
username = excluded.username,
|
||||
password = excluded.password,
|
||||
oauth_config = excluded.oauth_config,
|
||||
updated_at = excluded.updated_at";
|
||||
|
||||
command.Parameters.AddWithValue("@name", name);
|
||||
command.Parameters.AddWithValue("@server", server);
|
||||
command.Parameters.AddWithValue("@port", port);
|
||||
command.Parameters.AddWithValue("@authMode", authMode);
|
||||
command.Parameters.AddWithValue("@fromAddress", fromAddress);
|
||||
command.Parameters.AddWithValue("@username", (object?)username ?? DBNull.Value);
|
||||
command.Parameters.AddWithValue("@password", (object?)password ?? DBNull.Value);
|
||||
command.Parameters.AddWithValue("@oauthConfig", (object?)oauthConfig ?? DBNull.Value);
|
||||
command.Parameters.AddWithValue("@updatedAt", DateTimeOffset.UtcNow.ToString("O"));
|
||||
|
||||
await command.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
// ── Data Connection Definition CRUD ──
|
||||
|
||||
/// <summary>
|
||||
/// Stores or updates a data connection definition (OPC UA endpoint, etc.).
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the data connection.</param>
|
||||
/// <param name="protocol">The protocol type (e.g., 'OpcUa').</param>
|
||||
/// <param name="configJson">Primary configuration as JSON.</param>
|
||||
/// <param name="backupConfigJson">Backup configuration as JSON, if applicable.</param>
|
||||
/// <param name="failoverRetryCount">Number of retries for failover attempts.</param>
|
||||
public async Task StoreDataConnectionDefinitionAsync(
|
||||
string name, string protocol, string? configJson, string? backupConfigJson = null, int failoverRetryCount = 3)
|
||||
{
|
||||
await using var connection = new SqliteConnection(_connectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = @"
|
||||
INSERT INTO data_connection_definitions (name, protocol, configuration, backup_configuration, failover_retry_count, updated_at)
|
||||
VALUES (@name, @protocol, @config, @backupConfig, @failoverRetryCount, @updatedAt)
|
||||
ON CONFLICT(name) DO UPDATE SET
|
||||
protocol = excluded.protocol,
|
||||
configuration = excluded.configuration,
|
||||
backup_configuration = excluded.backup_configuration,
|
||||
failover_retry_count = excluded.failover_retry_count,
|
||||
updated_at = excluded.updated_at";
|
||||
|
||||
command.Parameters.AddWithValue("@name", name);
|
||||
command.Parameters.AddWithValue("@protocol", protocol);
|
||||
command.Parameters.AddWithValue("@config", (object?)configJson ?? DBNull.Value);
|
||||
command.Parameters.AddWithValue("@backupConfig", (object?)backupConfigJson ?? DBNull.Value);
|
||||
command.Parameters.AddWithValue("@failoverRetryCount", failoverRetryCount);
|
||||
command.Parameters.AddWithValue("@updatedAt", DateTimeOffset.UtcNow.ToString("O"));
|
||||
|
||||
await command.ExecuteNonQueryAsync();
|
||||
_logger.LogDebug("Stored data connection definition '{Name}' (protocol={Protocol})", name, protocol);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all stored data connection definitions.
|
||||
/// </summary>
|
||||
public async Task<List<StoredDataConnectionDefinition>> GetAllDataConnectionDefinitionsAsync()
|
||||
{
|
||||
await using var connection = new SqliteConnection(_connectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = "SELECT name, protocol, configuration, backup_configuration, failover_retry_count FROM data_connection_definitions";
|
||||
|
||||
var results = new List<StoredDataConnectionDefinition>();
|
||||
await using var reader = await command.ExecuteReaderAsync();
|
||||
while (await reader.ReadAsync())
|
||||
{
|
||||
results.Add(new StoredDataConnectionDefinition
|
||||
{
|
||||
Name = reader.GetString(0),
|
||||
Protocol = reader.GetString(1),
|
||||
ConfigurationJson = reader.IsDBNull(2) ? null : reader.GetString(2),
|
||||
BackupConfigurationJson = reader.IsDBNull(3) ? null : reader.GetString(3),
|
||||
FailoverRetryCount = reader.GetInt32(4)
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a deployed instance configuration as stored in SQLite.
|
||||
/// </summary>
|
||||
public class DeployedInstance
|
||||
{
|
||||
/// <summary>
|
||||
/// The unique name of the instance.
|
||||
/// </summary>
|
||||
public string InstanceUniqueName { get; init; } = string.Empty;
|
||||
/// <summary>
|
||||
/// The deployed configuration as JSON.
|
||||
/// </summary>
|
||||
public string ConfigJson { get; init; } = string.Empty;
|
||||
/// <summary>
|
||||
/// The unique deployment identifier.
|
||||
/// </summary>
|
||||
public string DeploymentId { get; init; } = string.Empty;
|
||||
/// <summary>
|
||||
/// The configuration revision hash for staleness detection.
|
||||
/// </summary>
|
||||
public string RevisionHash { get; init; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Whether the instance is enabled.
|
||||
/// </summary>
|
||||
public bool IsEnabled { get; init; }
|
||||
/// <summary>
|
||||
/// The timestamp when the configuration was deployed.
|
||||
/// </summary>
|
||||
public string DeployedAt { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a shared script stored locally in SQLite (WP-33).
|
||||
/// </summary>
|
||||
public class StoredSharedScript
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the shared script.
|
||||
/// </summary>
|
||||
public string Name { get; init; } = string.Empty;
|
||||
/// <summary>
|
||||
/// The script code.
|
||||
/// </summary>
|
||||
public string Code { get; init; } = string.Empty;
|
||||
/// <summary>
|
||||
/// JSON representation of parameter definitions, if any.
|
||||
/// </summary>
|
||||
public string? ParameterDefinitions { get; init; }
|
||||
/// <summary>
|
||||
/// JSON representation of the return type definition, if any.
|
||||
/// </summary>
|
||||
public string? ReturnDefinition { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a data connection definition stored locally in SQLite.
|
||||
/// </summary>
|
||||
public class StoredDataConnectionDefinition
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the data connection.
|
||||
/// </summary>
|
||||
public string Name { get; init; } = string.Empty;
|
||||
/// <summary>
|
||||
/// The protocol type (e.g., 'OpcUa').
|
||||
/// </summary>
|
||||
public string Protocol { get; init; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Primary configuration as JSON.
|
||||
/// </summary>
|
||||
public string? ConfigurationJson { get; init; }
|
||||
/// <summary>
|
||||
/// Backup configuration as JSON, if applicable.
|
||||
/// </summary>
|
||||
public string? BackupConfigurationJson { get; init; }
|
||||
/// <summary>
|
||||
/// Number of retries for failover attempts.
|
||||
/// </summary>
|
||||
public int FailoverRetryCount { get; init; } = 3;
|
||||
}
|
||||
Reference in New Issue
Block a user