docs: add XML doc comments across src + Sister Projects section in CLAUDE.md

Bulk CommentChecker pass: fills in <param>/<inheritdoc> tags on public
APIs across all 23 src/ projects so the doc-coverage gate is green. Also
adds a Sister Projects section to CLAUDE.md pointing at the MxAccess
Gateway and OtOpcUa sibling repos, and gitignores local credential
captures (*login*.txt) and the wonder-app-vd03 deploy/ artifacts.
This commit is contained in:
Joseph Doherty
2026-05-28 01:55:24 -04:00
parent 6731845473
commit 1eb6e972b0
381 changed files with 5788 additions and 532 deletions
@@ -2,16 +2,33 @@ namespace ScadaLink.Commons.Entities.Audit;
public class AuditLogEntry
{
/// <summary>Auto-incremented primary key.</summary>
public int Id { get; set; }
/// <summary>Username of the actor who performed the action.</summary>
public string User { get; set; }
/// <summary>Action performed (e.g. Created, Updated, Deleted).</summary>
public string Action { get; set; }
/// <summary>Entity type name (e.g. Template, ExternalSystem).</summary>
public string EntityType { get; set; }
/// <summary>String representation of the entity's primary key.</summary>
public string EntityId { get; set; }
/// <summary>Human-readable name of the affected entity.</summary>
public string EntityName { get; set; }
/// <summary>JSON snapshot of the entity's state after the action; null for deletes.</summary>
public string? AfterStateJson { get; set; }
/// <summary>UTC timestamp when the audit entry was recorded.</summary>
public DateTimeOffset Timestamp { get; set; }
/// <summary>Bundle import session id when this entry was created during a bundle import; otherwise null.</summary>
public Guid? BundleImportId { get; set; }
/// <summary>
/// Creates an audit log entry for the specified user action on a named entity.
/// </summary>
/// <param name="user">Username of the actor performing the action.</param>
/// <param name="action">Action name (e.g. Created, Updated, Deleted).</param>
/// <param name="entityType">Entity type name.</param>
/// <param name="entityId">String primary key of the affected entity.</param>
/// <param name="entityName">Human-readable name of the affected entity.</param>
public AuditLogEntry(string user, string action, string entityType, string entityId, string entityName)
{
User = user ?? throw new ArgumentNullException(nameof(user));
@@ -6,9 +6,13 @@ namespace ScadaLink.Commons.Entities.Deployment;
/// </summary>
public class DeployedConfigSnapshot
{
/// <summary>Primary key.</summary>
public int Id { get; set; }
/// <summary>Foreign key to the owning <c>Instance</c> entity.</summary>
public int InstanceId { get; set; }
/// <summary>Unique deployment identifier assigned at deploy time for idempotency.</summary>
public string DeploymentId { get; set; }
/// <summary>Revision hash of the flattened configuration at deploy time, used for staleness detection.</summary>
public string RevisionHash { get; set; }
/// <summary>
@@ -16,8 +20,13 @@ public class DeployedConfigSnapshot
/// </summary>
public string ConfigurationJson { get; set; }
/// <summary>UTC timestamp when this snapshot was persisted.</summary>
public DateTimeOffset DeployedAt { get; set; }
/// <summary>Initializes a new snapshot with the deployment identity, revision hash, and serialized configuration.</summary>
/// <param name="deploymentId">Unique deployment identifier.</param>
/// <param name="revisionHash">Revision hash of the flattened configuration.</param>
/// <param name="configurationJson">JSON-serialized flattened configuration.</param>
public DeployedConfigSnapshot(string deploymentId, string revisionHash, string configurationJson)
{
DeploymentId = deploymentId ?? throw new ArgumentNullException(nameof(deploymentId));
@@ -4,14 +4,49 @@ namespace ScadaLink.Commons.Entities.Deployment;
public class DeploymentRecord
{
/// <summary>
/// The deployment record identifier.
/// </summary>
public int Id { get; set; }
/// <summary>
/// The instance identifier being deployed.
/// </summary>
public int InstanceId { get; set; }
/// <summary>
/// The current deployment status.
/// </summary>
public DeploymentStatus Status { get; set; }
/// <summary>
/// The deployment identifier.
/// </summary>
public string DeploymentId { get; set; }
/// <summary>
/// The revision hash of the deployed configuration, or null.
/// </summary>
public string? RevisionHash { get; set; }
/// <summary>
/// The user who initiated the deployment.
/// </summary>
public string DeployedBy { get; set; }
/// <summary>
/// The time when the deployment was initiated.
/// </summary>
public DateTimeOffset DeployedAt { get; set; }
/// <summary>
/// The time when the deployment completed, or null if still in progress.
/// </summary>
public DateTimeOffset? CompletedAt { get; set; }
/// <summary>
/// Error message if the deployment failed, or null.
/// </summary>
public string? ErrorMessage { get; set; }
/// <summary>
@@ -19,6 +54,11 @@ public class DeploymentRecord
/// </summary>
public byte[] RowVersion { get; set; } = [];
/// <summary>
/// Initializes a new instance of the DeploymentRecord.
/// </summary>
/// <param name="deploymentId">The deployment identifier.</param>
/// <param name="deployedBy">The user initiating the deployment.</param>
public DeploymentRecord(string deploymentId, string deployedBy)
{
DeploymentId = deploymentId ?? throw new ArgumentNullException(nameof(deploymentId));
@@ -1,13 +1,24 @@
namespace ScadaLink.Commons.Entities.Deployment;
/// <summary>
/// Records a system-wide artifact deployment operation, tracking status per site.
/// </summary>
public class SystemArtifactDeploymentRecord
{
/// <summary>Primary key.</summary>
public int Id { get; set; }
/// <summary>Type identifier for the deployed artifact (e.g. the artifact category name).</summary>
public string ArtifactType { get; set; }
/// <summary>Username of the operator who initiated the deployment.</summary>
public string DeployedBy { get; set; }
/// <summary>UTC timestamp when the deployment was initiated.</summary>
public DateTimeOffset DeployedAt { get; set; }
/// <summary>JSON-serialized per-site deployment status map, or null if not yet computed.</summary>
public string? PerSiteStatus { get; set; }
/// <summary>Initializes a new <see cref="SystemArtifactDeploymentRecord"/> with required fields.</summary>
/// <param name="artifactType">The artifact type being deployed.</param>
/// <param name="deployedBy">The username of the initiating operator.</param>
public SystemArtifactDeploymentRecord(string artifactType, string deployedBy)
{
ArtifactType = artifactType ?? throw new ArgumentNullException(nameof(artifactType));
@@ -2,12 +2,20 @@ namespace ScadaLink.Commons.Entities.ExternalSystems;
public class DatabaseConnectionDefinition
{
/// <summary>Gets or sets the primary key.</summary>
public int Id { get; set; }
/// <summary>Gets or sets the human-readable connection name.</summary>
public string Name { get; set; }
/// <summary>Gets or sets the ADO.NET connection string for this database.</summary>
public string ConnectionString { get; set; }
/// <summary>Gets or sets the maximum number of retry attempts for transient failures.</summary>
public int MaxRetries { get; set; }
/// <summary>Gets or sets the delay between retry attempts.</summary>
public TimeSpan RetryDelay { get; set; }
/// <summary>Initializes a new <see cref="DatabaseConnectionDefinition"/> with the required name and connection string.</summary>
/// <param name="name">The human-readable connection name.</param>
/// <param name="connectionString">The ADO.NET connection string.</param>
public DatabaseConnectionDefinition(string name, string connectionString)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
@@ -2,14 +2,27 @@ namespace ScadaLink.Commons.Entities.ExternalSystems;
public class ExternalSystemDefinition
{
/// <summary>Database primary key.</summary>
public int Id { get; set; }
/// <summary>Display name for the external system.</summary>
public string Name { get; set; }
/// <summary>Base URL of the external system's HTTP endpoint.</summary>
public string EndpointUrl { get; set; }
/// <summary>Authentication type identifier (e.g., "ApiKey", "Basic").</summary>
public string AuthType { get; set; }
/// <summary>JSON-serialized authentication configuration for the selected <see cref="AuthType"/>.</summary>
public string? AuthConfiguration { get; set; }
/// <summary>Maximum number of retry attempts for transient failures.</summary>
public int MaxRetries { get; set; }
/// <summary>Fixed delay between retry attempts.</summary>
public TimeSpan RetryDelay { get; set; }
/// <summary>
/// Initializes a new <see cref="ExternalSystemDefinition"/>.
/// </summary>
/// <param name="name">Display name for the external system.</param>
/// <param name="endpointUrl">Base URL of the external system's HTTP endpoint.</param>
/// <param name="authType">Authentication type identifier.</param>
public ExternalSystemDefinition(string name, string endpointUrl, string authType)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
@@ -1,15 +1,29 @@
namespace ScadaLink.Commons.Entities.ExternalSystems;
/// <summary>
/// Defines a callable HTTP method on an external system definition.
/// </summary>
public class ExternalSystemMethod
{
/// <summary>Primary key.</summary>
public int Id { get; set; }
/// <summary>Foreign key referencing the owning <c>ExternalSystemDefinition</c>.</summary>
public int ExternalSystemDefinitionId { get; set; }
/// <summary>Name of the method as referenced in scripts.</summary>
public string Name { get; set; }
/// <summary>HTTP method (GET, POST, PUT, DELETE, etc.).</summary>
public string HttpMethod { get; set; }
/// <summary>URL path relative to the external system's base URL.</summary>
public string Path { get; set; }
/// <summary>JSON-serialized parameter definitions for this method, or null if there are none.</summary>
public string? ParameterDefinitions { get; set; }
/// <summary>JSON-serialized return type definition for this method, or null if void.</summary>
public string? ReturnDefinition { get; set; }
/// <summary>Initializes a new instance of <see cref="ExternalSystemMethod"/> with the required fields.</summary>
/// <param name="name">The method name.</param>
/// <param name="httpMethod">The HTTP method verb.</param>
/// <param name="path">The URL path.</param>
public ExternalSystemMethod(string name, string httpMethod, string path)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
@@ -10,7 +10,9 @@ namespace ScadaLink.Commons.Entities.InboundApi;
/// </summary>
public class ApiKey
{
/// <summary>Database primary key.</summary>
public int Id { get; set; }
/// <summary>Display name for the API key.</summary>
public string Name { get; set; }
/// <summary>
@@ -20,6 +22,7 @@ public class ApiKey
/// </summary>
public string KeyHash { get; set; }
/// <summary>When false, the key is rejected even if the hash matches.</summary>
public bool IsEnabled { get; set; }
/// <summary>
@@ -28,6 +31,8 @@ public class ApiKey
/// never holds the plaintext. Production code paths that have a configured pepper
/// should use <see cref="FromHash(string, string)"/> with a peppered hash instead.
/// </summary>
/// <param name="name">Display name for the API key.</param>
/// <param name="keyValue">Plaintext key value; hashed immediately and never stored.</param>
public ApiKey(string name, string keyValue)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
@@ -50,6 +55,8 @@ public class ApiKey
/// path, which generates a random key, hashes it with the configured (peppered)
/// <see cref="IApiKeyHasher"/>, and stores only the resulting hash.
/// </summary>
/// <param name="name">Display name for the API key.</param>
/// <param name="keyHash">Pre-computed keyed hash of the API key value.</param>
public static ApiKey FromHash(string name, string keyHash)
{
return new ApiKey
@@ -2,14 +2,24 @@ namespace ScadaLink.Commons.Entities.InboundApi;
public class ApiMethod
{
/// <summary>Gets or sets the primary key.</summary>
public int Id { get; set; }
/// <summary>Gets or sets the method name used in the route (<c>/api/{Name}</c>).</summary>
public string Name { get; set; }
/// <summary>Gets or sets the C# script body executed when the method is invoked.</summary>
public string Script { get; set; }
/// <summary>Gets or sets the JSON-serialised list of API key IDs approved for this method, or <c>null</c> for unrestricted.</summary>
public string? ApprovedApiKeyIds { get; set; }
/// <summary>Gets or sets the JSON Schema describing the accepted parameters, or <c>null</c> if the method takes no parameters.</summary>
public string? ParameterDefinitions { get; set; }
/// <summary>Gets or sets the JSON Schema describing the return type, or <c>null</c> if the method returns nothing.</summary>
public string? ReturnDefinition { get; set; }
/// <summary>Gets or sets the script execution timeout in seconds.</summary>
public int TimeoutSeconds { get; set; }
/// <summary>Initializes a new <see cref="ApiMethod"/> with the required name and script.</summary>
/// <param name="name">The method name (used as the route segment).</param>
/// <param name="script">The C# script body to execute.</param>
public ApiMethod(string name, string script)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
@@ -2,12 +2,21 @@ namespace ScadaLink.Commons.Entities.Instances;
public class Area
{
/// <summary>Gets or sets the database primary key.</summary>
public int Id { get; set; }
/// <summary>Gets or sets the id of the site this area belongs to.</summary>
public int SiteId { get; set; }
/// <summary>Gets or sets the display name of the area.</summary>
public string Name { get; set; }
/// <summary>Gets or sets the id of the parent area, or null for a root area.</summary>
public int? ParentAreaId { get; set; }
/// <summary>Gets or sets the child areas nested under this area.</summary>
public ICollection<Area> Children { get; set; } = new List<Area>();
/// <summary>
/// Initializes a new <see cref="Area"/> with the given name.
/// </summary>
/// <param name="name">Display name for the area.</param>
public Area(string name)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
@@ -4,16 +4,29 @@ namespace ScadaLink.Commons.Entities.Instances;
public class Instance
{
/// <summary>Primary key.</summary>
public int Id { get; set; }
/// <summary>Foreign key to the template this instance is based on.</summary>
public int TemplateId { get; set; }
/// <summary>Foreign key to the site where this instance is deployed.</summary>
public int SiteId { get; set; }
/// <summary>Optional foreign key to the organisational area this instance belongs to.</summary>
public int? AreaId { get; set; }
/// <summary>System-wide unique name that identifies this instance.</summary>
public string UniqueName { get; set; }
/// <summary>Current lifecycle state of the instance.</summary>
public InstanceState State { get; set; }
/// <summary>Per-attribute value overrides applied on top of the template defaults.</summary>
public ICollection<InstanceAttributeOverride> AttributeOverrides { get; set; } = new List<InstanceAttributeOverride>();
/// <summary>Per-alarm configuration overrides applied on top of the template defaults.</summary>
public ICollection<InstanceAlarmOverride> AlarmOverrides { get; set; } = new List<InstanceAlarmOverride>();
/// <summary>Data-connection bindings that map template tags to site data sources.</summary>
public ICollection<InstanceConnectionBinding> ConnectionBindings { get; set; } = new List<InstanceConnectionBinding>();
/// <summary>
/// Initializes a new instance with the required unique name.
/// </summary>
/// <param name="uniqueName">System-wide unique name for this instance.</param>
public Instance(string uniqueName)
{
UniqueName = uniqueName ?? throw new ArgumentNullException(nameof(uniqueName));
@@ -19,7 +19,9 @@ namespace ScadaLink.Commons.Entities.Instances;
/// </summary>
public class InstanceAlarmOverride
{
/// <summary>Primary key.</summary>
public int Id { get; set; }
/// <summary>Foreign key to the instance this override belongs to.</summary>
public int InstanceId { get; set; }
/// <summary>
@@ -41,6 +43,10 @@ public class InstanceAlarmOverride
/// </summary>
public int? PriorityLevelOverride { get; set; }
/// <summary>
/// Initializes a new alarm override for the specified alarm.
/// </summary>
/// <param name="alarmCanonicalName">Canonical name of the alarm to override.</param>
public InstanceAlarmOverride(string alarmCanonicalName)
{
AlarmCanonicalName = alarmCanonicalName ?? throw new ArgumentNullException(nameof(alarmCanonicalName));
@@ -2,11 +2,17 @@ namespace ScadaLink.Commons.Entities.Instances;
public class InstanceAttributeOverride
{
/// <summary>Gets or sets the primary key.</summary>
public int Id { get; set; }
/// <summary>Gets or sets the foreign key of the owning instance.</summary>
public int InstanceId { get; set; }
/// <summary>Gets or sets the attribute name this override targets.</summary>
public string AttributeName { get; set; }
/// <summary>Gets or sets the override value, or <c>null</c> to clear a previous override.</summary>
public string? OverrideValue { get; set; }
/// <summary>Initializes a new <see cref="InstanceAttributeOverride"/> for the given attribute name.</summary>
/// <param name="attributeName">The name of the attribute to override.</param>
public InstanceAttributeOverride(string attributeName)
{
AttributeName = attributeName ?? throw new ArgumentNullException(nameof(attributeName));
@@ -2,11 +2,19 @@ namespace ScadaLink.Commons.Entities.Instances;
public class InstanceConnectionBinding
{
/// <summary>Auto-incremented primary key.</summary>
public int Id { get; set; }
/// <summary>Foreign key to the owning instance.</summary>
public int InstanceId { get; set; }
/// <summary>Name of the attribute on the instance that this binding maps to a data connection tag.</summary>
public string AttributeName { get; set; }
/// <summary>Foreign key to the data connection that provides values for this attribute.</summary>
public int DataConnectionId { get; set; }
/// <summary>
/// Creates a binding for the specified attribute name.
/// </summary>
/// <param name="attributeName">Name of the attribute being bound to a data connection.</param>
public InstanceConnectionBinding(string attributeName)
{
AttributeName = attributeName ?? throw new ArgumentNullException(nameof(attributeName));
@@ -11,19 +11,27 @@ public class Notification
{
/// <summary>GUID primary key, generated at the originating site.</summary>
public string NotificationId { get; set; }
/// <summary>Gets or sets the notification type.</summary>
public NotificationType Type { get; set; }
/// <summary>Gets or sets the notification list name.</summary>
public string ListName { get; set; }
/// <summary>Gets or sets the notification subject.</summary>
public string Subject { get; set; }
/// <summary>Gets or sets the notification body.</summary>
public string Body { get; set; }
/// <summary>JSON extensibility hook for channel-specific payload data.</summary>
public string? TypeData { get; set; }
/// <summary>Gets or sets the notification delivery status.</summary>
public NotificationStatus Status { get; set; } = NotificationStatus.Pending;
/// <summary>Gets or sets the delivery retry count.</summary>
public int RetryCount { get; set; }
/// <summary>Gets or sets the last error message, if any.</summary>
public string? LastError { get; set; }
/// <summary>Resolved delivery targets snapshotted at delivery time, for audit.</summary>
public string? ResolvedTargets { get; set; }
/// <summary>Gets or sets the originating site ID.</summary>
public string SourceSiteId { get; set; }
/// <summary>
@@ -34,7 +42,9 @@ public class Notification
/// central; nullable so rows submitted before the column existed don't block ingest.
/// </summary>
public string? SourceNode { get; set; }
/// <summary>Gets or sets the originating instance ID, if any.</summary>
public string? SourceInstanceId { get; set; }
/// <summary>Gets or sets the originating script name, if any.</summary>
public string? SourceScript { get; set; }
/// <summary>
@@ -54,14 +64,27 @@ public class Notification
/// non-routed runs, or for notifications submitted before the column existed.
/// </summary>
public Guid? OriginParentExecutionId { get; set; }
/// <summary>Gets or sets the time when the notification was enqueued at the site.</summary>
public DateTimeOffset SiteEnqueuedAt { get; set; }
/// <summary>Central ingest time.</summary>
public DateTimeOffset CreatedAt { get; set; }
/// <summary>Gets or sets the time of the last delivery attempt, if any.</summary>
public DateTimeOffset? LastAttemptAt { get; set; }
/// <summary>Gets or sets the time of the next scheduled delivery attempt, if any.</summary>
public DateTimeOffset? NextAttemptAt { get; set; }
/// <summary>Gets or sets the time the notification was delivered, if any.</summary>
public DateTimeOffset? DeliveredAt { get; set; }
/// <summary>
/// Initializes a new instance of the Notification class.
/// </summary>
/// <param name="notificationId">The notification ID (GUID).</param>
/// <param name="type">The notification type.</param>
/// <param name="listName">The notification list name.</param>
/// <param name="subject">The notification subject.</param>
/// <param name="body">The notification body text.</param>
/// <param name="sourceSiteId">The originating site ID.</param>
public Notification(string notificationId, NotificationType type, string listName,
string subject, string body, string sourceSiteId)
{
@@ -4,11 +4,17 @@ namespace ScadaLink.Commons.Entities.Notifications;
public class NotificationList
{
/// <summary>Primary key.</summary>
public int Id { get; set; }
/// <summary>Display name of the notification list.</summary>
public string Name { get; set; }
/// <summary>Delivery type discriminator (e.g., Email).</summary>
public NotificationType Type { get; set; } = NotificationType.Email;
/// <summary>Recipients belonging to this list.</summary>
public ICollection<NotificationRecipient> Recipients { get; set; } = new List<NotificationRecipient>();
/// <summary>Initializes the notification list with the given name.</summary>
/// <param name="name">Display name of the notification list.</param>
public NotificationList(string name)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
@@ -2,11 +2,20 @@ namespace ScadaLink.Commons.Entities.Notifications;
public class NotificationRecipient
{
/// <summary>Gets or sets the database primary key.</summary>
public int Id { get; set; }
/// <summary>Gets or sets the id of the parent notification list.</summary>
public int NotificationListId { get; set; }
/// <summary>Gets or sets the display name of the recipient.</summary>
public string Name { get; set; }
/// <summary>Gets or sets the recipient's email address.</summary>
public string EmailAddress { get; set; }
/// <summary>
/// Initializes a new <see cref="NotificationRecipient"/> with the required fields.
/// </summary>
/// <param name="name">Display name of the recipient.</param>
/// <param name="emailAddress">Email address of the recipient.</param>
public NotificationRecipient(string name, string emailAddress)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
@@ -2,18 +2,35 @@ namespace ScadaLink.Commons.Entities.Notifications;
public class SmtpConfiguration
{
/// <summary>Gets or sets the primary key.</summary>
public int Id { get; set; }
/// <summary>Gets or sets the SMTP server hostname or IP address.</summary>
public string Host { get; set; }
/// <summary>Gets or sets the SMTP server port number.</summary>
public int Port { get; set; }
/// <summary>Gets or sets the authentication type (e.g. Basic, OAuth2ClientCredentials).</summary>
public string AuthType { get; set; }
/// <summary>Gets or sets the serialized credentials (password or OAuth2 client secret), or null when not applicable.</summary>
public string? Credentials { get; set; }
/// <summary>Gets or sets the TLS mode (None, StartTLS, or SSL), or null to use the provider default.</summary>
public string? TlsMode { get; set; }
/// <summary>Gets or sets the sender address placed in the From header.</summary>
public string FromAddress { get; set; }
/// <summary>Gets or sets the connection timeout in seconds.</summary>
public int ConnectionTimeoutSeconds { get; set; }
/// <summary>Gets or sets the maximum number of concurrent SMTP connections.</summary>
public int MaxConcurrentConnections { get; set; }
/// <summary>Gets or sets the maximum number of delivery retries before parking.</summary>
public int MaxRetries { get; set; }
/// <summary>Gets or sets the delay between retry attempts.</summary>
public TimeSpan RetryDelay { get; set; }
/// <summary>
/// Initializes a new <see cref="SmtpConfiguration"/> with required fields.
/// </summary>
/// <param name="host">SMTP server hostname or IP address.</param>
/// <param name="authType">Authentication type string (e.g. Basic, OAuth2ClientCredentials).</param>
/// <param name="fromAddress">Sender address for the From header.</param>
public SmtpConfiguration(string host, string authType, string fromAddress)
{
Host = host ?? throw new ArgumentNullException(nameof(host));
@@ -2,12 +2,22 @@ namespace ScadaLink.Commons.Entities.Scripts;
public class SharedScript
{
/// <summary>Primary key.</summary>
public int Id { get; set; }
/// <summary>Unique script name used to reference this script from templates.</summary>
public string Name { get; set; }
/// <summary>C# script source code.</summary>
public string Code { get; set; }
/// <summary>JSON-serialized parameter definitions, or null when the script takes no parameters.</summary>
public string? ParameterDefinitions { get; set; }
/// <summary>JSON-serialized return type definition, or null when the script has no return value.</summary>
public string? ReturnDefinition { get; set; }
/// <summary>
/// Initializes a new shared script with the required name and code.
/// </summary>
/// <param name="name">Unique script name.</param>
/// <param name="code">C# script source code.</param>
public SharedScript(string name, string code)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
@@ -2,13 +2,19 @@ namespace ScadaLink.Commons.Entities.Security;
public class LdapGroupMapping
{
/// <summary>Gets or sets the primary key.</summary>
public int Id { get; set; }
/// <summary>Gets or sets the LDAP/AD group CN that this mapping targets.</summary>
public string LdapGroupName { get; set; }
/// <summary>Gets or sets the ScadaLink role name this group maps to.</summary>
public string Role { get; set; }
// Parameterless constructor for EF Core seed data
private LdapGroupMapping() { LdapGroupName = null!; Role = null!; }
/// <summary>Initializes a new <see cref="LdapGroupMapping"/> linking an LDAP group to a ScadaLink role.</summary>
/// <param name="ldapGroupName">The LDAP group name (CN).</param>
/// <param name="role">The ScadaLink role name to assign.</param>
public LdapGroupMapping(string ldapGroupName, string role)
{
LdapGroupName = ldapGroupName ?? throw new ArgumentNullException(nameof(ldapGroupName));
@@ -2,7 +2,10 @@ namespace ScadaLink.Commons.Entities.Security;
public class SiteScopeRule
{
/// <summary>Database primary key.</summary>
public int Id { get; set; }
/// <summary>Foreign key to the <see cref="LdapGroupMapping"/> this rule restricts.</summary>
public int LdapGroupMappingId { get; set; }
/// <summary>Foreign key to the site this rule limits the mapping to.</summary>
public int SiteId { get; set; }
}
@@ -2,14 +2,27 @@ namespace ScadaLink.Commons.Entities.Sites;
public class DataConnection
{
/// <summary>Gets or sets the database primary key.</summary>
public int Id { get; set; }
/// <summary>Gets or sets the owning site's id.</summary>
public int SiteId { get; set; }
/// <summary>Gets or sets the unique name of this data connection within the site.</summary>
public string Name { get; set; }
/// <summary>Gets or sets the protocol type string (e.g. "OpcUa").</summary>
public string Protocol { get; set; }
/// <summary>Gets or sets the primary protocol-specific configuration JSON.</summary>
public string? PrimaryConfiguration { get; set; }
/// <summary>Gets or sets the backup protocol-specific configuration JSON used on failover.</summary>
public string? BackupConfiguration { get; set; }
/// <summary>Gets or sets the number of failover retry attempts before the connection is marked failed.</summary>
public int FailoverRetryCount { get; set; } = 3;
/// <summary>
/// Initializes a new <see cref="DataConnection"/> with the required fields.
/// </summary>
/// <param name="name">Unique name of the connection within the site.</param>
/// <param name="protocol">Protocol type string (e.g. "OpcUa").</param>
/// <param name="siteId">Id of the owning site.</param>
public DataConnection(string name, string protocol, int siteId)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
@@ -2,15 +2,28 @@ namespace ScadaLink.Commons.Entities.Sites;
public class Site
{
/// <summary>Primary key.</summary>
public int Id { get; set; }
/// <summary>Human-readable display name for the site.</summary>
public string Name { get; set; }
/// <summary>Machine-readable identifier used in Akka addresses and API routing.</summary>
public string SiteIdentifier { get; set; }
/// <summary>Optional description of the site.</summary>
public string? Description { get; set; }
/// <summary>Akka remote address for site node A (ClusterClient contact point).</summary>
public string? NodeAAddress { get; set; }
/// <summary>Akka remote address for site node B (ClusterClient contact point).</summary>
public string? NodeBAddress { get; set; }
/// <summary>gRPC endpoint for site node A used by the central SiteStreamGrpcClient.</summary>
public string? GrpcNodeAAddress { get; set; }
/// <summary>gRPC endpoint for site node B used by the central SiteStreamGrpcClient.</summary>
public string? GrpcNodeBAddress { get; set; }
/// <summary>
/// Initializes a new site with the required name and identifier.
/// </summary>
/// <param name="name">Human-readable display name.</param>
/// <param name="siteIdentifier">Machine-readable identifier used in Akka addresses.</param>
public Site(string name, string siteIdentifier)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
@@ -2,14 +2,41 @@ namespace ScadaLink.Commons.Entities.Templates;
public class Template
{
/// <summary>
/// The unique identifier for the template.
/// </summary>
public int Id { get; set; }
/// <summary>
/// The name of the template.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Optional description of the template.
/// </summary>
public string? Description { get; set; }
/// <summary>
/// The identifier of the parent template, if this template inherits from another.
/// </summary>
public int? ParentTemplateId { get; set; }
/// <summary>
/// The identifier of the folder containing this template.
/// </summary>
public int? FolderId { get; set; }
/// <summary>
/// Collection of attributes defined in this template.
/// </summary>
public ICollection<TemplateAttribute> Attributes { get; set; } = new List<TemplateAttribute>();
/// <summary>
/// Collection of alarms defined in this template.
/// </summary>
public ICollection<TemplateAlarm> Alarms { get; set; } = new List<TemplateAlarm>();
/// <summary>
/// Collection of scripts defined in this template.
/// </summary>
public ICollection<TemplateScript> Scripts { get; set; } = new List<TemplateScript>();
/// <summary>
/// Collection of compositions defined in this template.
/// </summary>
public ICollection<TemplateComposition> Compositions { get; set; } = new List<TemplateComposition>();
/// <summary>
@@ -27,6 +54,10 @@ public class Template
/// </summary>
public int? OwnerCompositionId { get; set; }
/// <summary>
/// Initializes a new instance of the Template with the specified name.
/// </summary>
/// <param name="name">The name of the template.</param>
public Template(string name)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
@@ -4,14 +4,23 @@ namespace ScadaLink.Commons.Entities.Templates;
public class TemplateAlarm
{
/// <summary>Database primary key.</summary>
public int Id { get; set; }
/// <summary>Foreign key to the owning <see cref="Template"/>.</summary>
public int TemplateId { get; set; }
/// <summary>Unique alarm name within the template.</summary>
public string Name { get; set; }
/// <summary>Optional human-readable description of the alarm.</summary>
public string? Description { get; set; }
/// <summary>Alarm priority level; lower values indicate higher priority.</summary>
public int PriorityLevel { get; set; }
/// <summary>When true, this alarm is locked and cannot be overridden in derived templates.</summary>
public bool IsLocked { get; set; }
/// <summary>Type of trigger condition that activates this alarm.</summary>
public AlarmTriggerType TriggerType { get; set; }
/// <summary>JSON-serialized trigger configuration specific to the <see cref="TriggerType"/>.</summary>
public string? TriggerConfiguration { get; set; }
/// <summary>Optional ID of the script to execute when the alarm triggers.</summary>
public int? OnTriggerScriptId { get; set; }
/// <summary>
@@ -29,6 +38,10 @@ public class TemplateAlarm
/// </summary>
public bool LockedInDerived { get; set; }
/// <summary>
/// Initializes a new <see cref="TemplateAlarm"/> with the specified name.
/// </summary>
/// <param name="name">The unique alarm name within the template.</param>
public TemplateAlarm(string name)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
@@ -4,13 +4,37 @@ namespace ScadaLink.Commons.Entities.Templates;
public class TemplateAttribute
{
/// <summary>
/// Gets or sets the attribute ID.
/// </summary>
public int Id { get; set; }
/// <summary>
/// Gets or sets the template ID that owns this attribute.
/// </summary>
public int TemplateId { get; set; }
/// <summary>
/// Gets or sets the attribute name.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Gets or sets the attribute value.
/// </summary>
public string? Value { get; set; }
/// <summary>
/// Gets or sets the data type of the attribute.
/// </summary>
public DataType DataType { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the attribute is locked from override.
/// </summary>
public bool IsLocked { get; set; }
/// <summary>
/// Gets or sets the attribute description.
/// </summary>
public string? Description { get; set; }
/// <summary>
/// Gets or sets the data source reference for this attribute.
/// </summary>
public string? DataSourceReference { get; set; }
/// <summary>
@@ -28,6 +52,10 @@ public class TemplateAttribute
/// </summary>
public bool LockedInDerived { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="TemplateAttribute"/> class with the specified name.
/// </summary>
/// <param name="name">The attribute name.</param>
public TemplateAttribute(string name)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
@@ -2,11 +2,19 @@ namespace ScadaLink.Commons.Entities.Templates;
public class TemplateComposition
{
/// <summary>Primary key.</summary>
public int Id { get; set; }
/// <summary>Foreign key to the parent template that includes this composition.</summary>
public int TemplateId { get; set; }
/// <summary>Foreign key to the template being composed into the parent.</summary>
public int ComposedTemplateId { get; set; }
/// <summary>Name of the composition instance within the parent template's namespace.</summary>
public string InstanceName { get; set; }
/// <summary>
/// Initializes a new template composition with the required instance name.
/// </summary>
/// <param name="instanceName">Name of this composition slot within the parent template.</param>
public TemplateComposition(string instanceName)
{
InstanceName = instanceName ?? throw new ArgumentNullException(nameof(instanceName));
@@ -2,11 +2,19 @@ namespace ScadaLink.Commons.Entities.Templates;
public class TemplateFolder
{
/// <summary>Database primary key.</summary>
public int Id { get; set; }
/// <summary>Display name for the folder.</summary>
public string Name { get; set; }
/// <summary>ID of the parent folder, or null for root-level folders.</summary>
public int? ParentFolderId { get; set; }
/// <summary>Display ordering position within the parent folder.</summary>
public int SortOrder { get; set; }
/// <summary>
/// Initializes a new <see cref="TemplateFolder"/> with the specified name.
/// </summary>
/// <param name="name">The display name for the folder.</param>
public TemplateFolder(string name)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
@@ -2,15 +2,54 @@ namespace ScadaLink.Commons.Entities.Templates;
public class TemplateScript
{
/// <summary>
/// The script identifier.
/// </summary>
public int Id { get; set; }
/// <summary>
/// The template identifier this script belongs to.
/// </summary>
public int TemplateId { get; set; }
/// <summary>
/// The script name.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Whether the script is locked for editing.
/// </summary>
public bool IsLocked { get; set; }
/// <summary>
/// The script code.
/// </summary>
public string Code { get; set; }
/// <summary>
/// The trigger type for the script, or null.
/// </summary>
public string? TriggerType { get; set; }
/// <summary>
/// The trigger configuration, or null.
/// </summary>
public string? TriggerConfiguration { get; set; }
/// <summary>
/// The parameter definitions in JSON format, or null.
/// </summary>
public string? ParameterDefinitions { get; set; }
/// <summary>
/// The return type definition in JSON format, or null.
/// </summary>
public string? ReturnDefinition { get; set; }
/// <summary>
/// The minimum time between script runs, or null.
/// </summary>
public TimeSpan? MinTimeBetweenRuns { get; set; }
/// <summary>
@@ -23,11 +62,16 @@ public class TemplateScript
/// <summary>
/// Set on a base script. When true, derived templates may not override
/// the script body — the row is rendered readonly with a 🔒 in the derived
/// the script body — the row is rendered readonly in the derived
/// UI, and any attempt to update it through the API is rejected.
/// </summary>
public bool LockedInDerived { get; set; }
/// <summary>
/// Initializes a new instance of the TemplateScript.
/// </summary>
/// <param name="name">The script name.</param>
/// <param name="code">The script code.</param>
public TemplateScript(string name, string code)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
@@ -34,6 +34,13 @@ public interface IOperationTrackingStore
/// untouched), matching the at-least-once semantics of the calling site
/// store-and-forward path.
/// </summary>
/// <param name="id">Unique operation ID (idempotency key).</param>
/// <param name="kind">Kind of operation (e.g., cached call type).</param>
/// <param name="targetSummary">Optional summary of the operation target.</param>
/// <param name="sourceInstanceId">Optional ID of the source instance.</param>
/// <param name="sourceScript">Optional name of the source script.</param>
/// <param name="sourceNode">Optional source node identifier.</param>
/// <param name="ct">Cancellation token.</param>
Task RecordEnqueueAsync(
TrackedOperationId id,
string kind,
@@ -49,6 +56,12 @@ public interface IOperationTrackingStore
/// already applied) are NOT mutated — the operation has reached its final
/// outcome and any late-arriving attempt telemetry is dropped on the floor.
/// </summary>
/// <param name="id">Operation ID to update.</param>
/// <param name="status">Current operation status.</param>
/// <param name="retryCount">Number of retry attempts.</param>
/// <param name="lastError">Optional error message from the last attempt.</param>
/// <param name="httpStatus">Optional HTTP status code from the last attempt.</param>
/// <param name="ct">Cancellation token.</param>
Task RecordAttemptAsync(
TrackedOperationId id,
string status,
@@ -62,6 +75,11 @@ public interface IOperationTrackingStore
/// <c>TerminalAtUtc = now</c> and writes the final status / error. A row
/// already in terminal state is left untouched (first-write-wins).
/// </summary>
/// <param name="id">Operation ID to mark as terminal.</param>
/// <param name="status">Final operation status.</param>
/// <param name="lastError">Optional final error message.</param>
/// <param name="httpStatus">Optional final HTTP status code.</param>
/// <param name="ct">Cancellation token.</param>
Task RecordTerminalAsync(
TrackedOperationId id,
string status,
@@ -73,6 +91,9 @@ public interface IOperationTrackingStore
/// Return the latest snapshot for the supplied id, or <c>null</c> when no
/// tracking row exists (purged or never recorded).
/// </summary>
/// <param name="id">Operation ID to fetch status for.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Tracking status snapshot, or null if not found.</returns>
Task<TrackingStatusSnapshot?> GetStatusAsync(
TrackedOperationId id,
CancellationToken ct = default);
@@ -82,6 +103,8 @@ public interface IOperationTrackingStore
/// <paramref name="olderThanUtc"/>. Non-terminal rows are kept regardless
/// of age (the operation is still in flight).
/// </summary>
/// <param name="olderThanUtc">Cutoff timestamp; rows terminal before this are deleted.</param>
/// <param name="ct">Cancellation token.</param>
Task PurgeTerminalAsync(
DateTime olderThanUtc,
CancellationToken ct = default);
@@ -36,6 +36,8 @@ public interface IPartitionMaintenance
/// boundary that already exists is skipped rather than re-issued.
/// Returns the boundaries actually added, in chronological order.
/// </summary>
/// <param name="lookaheadMonths">Number of future monthly boundaries to ensure exist.</param>
/// <param name="ct">Cancellation token for the SQL operation.</param>
Task<IReadOnlyList<DateTime>> EnsureLookaheadAsync(int lookaheadMonths, CancellationToken ct = default);
/// <summary>
@@ -44,5 +46,6 @@ public interface IPartitionMaintenance
/// Returns <c>null</c> when the partition function does not exist or
/// has no boundaries.
/// </summary>
/// <param name="ct">Cancellation token for the SQL operation.</param>
Task<DateTime?> GetMaxBoundaryAsync(CancellationToken ct = default);
}
@@ -8,19 +8,62 @@ public record TagValue(object? Value, QualityCode Quality, DateTimeOffset Timest
public record ReadResult(bool Success, TagValue? Value, string? ErrorMessage);
public record WriteResult(bool Success, string? ErrorMessage);
/// <summary>Callback invoked when a subscribed tag value changes.</summary>
/// <param name="tagPath">The tag path whose value has changed.</param>
/// <param name="value">The new tag value including quality and timestamp.</param>
public delegate void SubscriptionCallback(string tagPath, TagValue value);
public interface IDataConnection : IAsyncDisposable
{
/// <summary>Establishes the protocol connection using the provided connection details.</summary>
/// <param name="connectionDetails">Protocol-specific key-value configuration pairs.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task ConnectAsync(IDictionary<string, string> connectionDetails, CancellationToken cancellationToken = default);
/// <summary>Gracefully terminates the protocol connection.</summary>
/// <param name="cancellationToken">Cancellation token.</param>
Task DisconnectAsync(CancellationToken cancellationToken = default);
/// <summary>Subscribes to value-change notifications for a tag path; returns a subscription ID.</summary>
/// <param name="tagPath">The tag path to subscribe to.</param>
/// <param name="callback">Callback invoked on each value change.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A subscription ID that can be passed to <see cref="UnsubscribeAsync"/>.</returns>
Task<string> SubscribeAsync(string tagPath, SubscriptionCallback callback, CancellationToken cancellationToken = default);
/// <summary>Cancels an active subscription by its ID.</summary>
/// <param name="subscriptionId">The subscription ID returned by <see cref="SubscribeAsync"/>.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task UnsubscribeAsync(string subscriptionId, CancellationToken cancellationToken = default);
/// <summary>Reads the current value of a single tag.</summary>
/// <param name="tagPath">The tag path to read.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The read result containing the value or an error.</returns>
Task<ReadResult> ReadAsync(string tagPath, CancellationToken cancellationToken = default);
/// <summary>Reads the current values of multiple tags in a single round-trip.</summary>
/// <param name="tagPaths">The tag paths to read.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A dictionary of tag paths to their read results.</returns>
Task<IReadOnlyDictionary<string, ReadResult>> ReadBatchAsync(IEnumerable<string> tagPaths, CancellationToken cancellationToken = default);
/// <summary>Writes a value to a single tag.</summary>
/// <param name="tagPath">The tag path to write.</param>
/// <param name="value">The value to write.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The write result indicating success or failure.</returns>
Task<WriteResult> WriteAsync(string tagPath, object? value, CancellationToken cancellationToken = default);
/// <summary>Writes values to multiple tags in a single round-trip.</summary>
/// <param name="values">A dictionary of tag paths to values.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A dictionary of tag paths to their write results.</returns>
Task<IReadOnlyDictionary<string, WriteResult>> WriteBatchAsync(IDictionary<string, object?> values, CancellationToken cancellationToken = default);
/// <summary>Writes a batch of values, then writes a flag and waits for a specific response value within the timeout.</summary>
/// <param name="values">Tag values to write before the flag.</param>
/// <param name="flagPath">Tag path of the trigger flag.</param>
/// <param name="flagValue">Value to write to the flag tag.</param>
/// <param name="responsePath">Tag path to monitor for the expected response value.</param>
/// <param name="responseValue">The response value that indicates completion.</param>
/// <param name="timeout">Maximum time to wait for the response.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns><c>true</c> if the response value was observed within the timeout; otherwise <c>false</c>.</returns>
Task<bool> WriteBatchAndWaitAsync(IDictionary<string, object?> values, string flagPath, object? flagValue, string responsePath, object? responseValue, TimeSpan timeout, CancellationToken cancellationToken = default);
/// <summary>Current connection health status.</summary>
ConnectionHealth Status { get; }
/// <summary>
@@ -29,6 +29,8 @@ public interface IAuditLogRepository
/// stored row untouched (first-write-wins). Bypasses the EF change tracker
/// so the row never enters a tracked state.
/// </summary>
/// <param name="evt">The audit event to insert.</param>
/// <param name="ct">Cancellation token.</param>
Task InsertIfNotExistsAsync(AuditEvent evt, CancellationToken ct = default);
/// <summary>
@@ -39,6 +41,9 @@ public interface IAuditLogRepository
/// <see cref="AuditLogPaging.AfterOccurredAtUtc"/> +
/// <see cref="AuditLogPaging.AfterEventId"/> to fetch the next page.
/// </summary>
/// <param name="filter">Filter criteria to apply to the query.</param>
/// <param name="paging">Paging cursor and page size.</param>
/// <param name="ct">Cancellation token.</param>
Task<IReadOnlyList<AuditEvent>> QueryAsync(
AuditLogQueryFilter filter,
AuditLogPaging paging,
@@ -75,6 +80,8 @@ public interface IAuditLogRepository
/// and the composite PK still rejects same-(EventId, OccurredAtUtc) rows.
/// </para>
/// </remarks>
/// <param name="monthBoundary">Lower-bound datetime of the monthly partition to switch out.</param>
/// <param name="ct">Cancellation token.</param>
Task<long> SwitchOutPartitionAsync(DateTime monthBoundary, CancellationToken ct = default);
/// <summary>
@@ -85,6 +92,8 @@ public interface IAuditLogRepository
/// excluded (a no-op switch is wasted work). Used by the M6 purge actor
/// to enumerate retention-eligible months on every tick.
/// </summary>
/// <param name="threshold">Only partitions whose data is entirely older than this UTC datetime are returned.</param>
/// <param name="ct">Cancellation token.</param>
Task<IReadOnlyList<DateTime>> GetPartitionBoundariesOlderThanAsync(
DateTime threshold,
CancellationToken ct = default);
@@ -172,6 +181,8 @@ public interface IAuditLogRepository
/// stub-node treatment of any other row-less execution.
/// </para>
/// </remarks>
/// <param name="executionId">Any execution id in the chain; the implementation walks to the root and back down.</param>
/// <param name="ct">Cancellation token.</param>
Task<IReadOnlyList<ExecutionTreeNode>> GetExecutionTreeAsync(
Guid executionId,
CancellationToken ct = default);
@@ -182,5 +193,6 @@ public interface IAuditLogRepository
/// "Node" multi-select filter dropdown — the Central UI caches the result
/// for ~60s so the repository is hit at most once per minute per circuit.
/// </summary>
/// <param name="ct">Cancellation token.</param>
Task<IReadOnlyList<string>> GetDistinctSourceNodesAsync(CancellationToken ct = default);
}
@@ -8,15 +8,49 @@ namespace ScadaLink.Commons.Interfaces.Repositories;
public interface ICentralUiRepository
{
/// <summary>Returns all configured sites.</summary>
/// <param name="cancellationToken">Cancellation token.</param>
Task<IReadOnlyList<Site>> GetAllSitesAsync(CancellationToken cancellationToken = default);
/// <summary>Returns all data connections for the specified site.</summary>
/// <param name="siteId">The site database ID to filter by.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<IReadOnlyList<DataConnection>> GetDataConnectionsBySiteIdAsync(int siteId, CancellationToken cancellationToken = default);
/// <summary>Returns all data connections across all sites.</summary>
/// <param name="cancellationToken">Cancellation token.</param>
Task<IReadOnlyList<DataConnection>> GetAllDataConnectionsAsync(CancellationToken cancellationToken = default);
/// <summary>Returns the full template tree including folders and templates.</summary>
/// <param name="cancellationToken">Cancellation token.</param>
Task<IReadOnlyList<Template>> GetTemplateTreeAsync(CancellationToken cancellationToken = default);
/// <summary>Returns instances filtered by optional site, template, or search term.</summary>
/// <param name="siteId">Optional site ID to filter by.</param>
/// <param name="templateId">Optional template ID to filter by.</param>
/// <param name="searchTerm">Optional keyword to filter instance names.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<IReadOnlyList<Instance>> GetInstancesFilteredAsync(int? siteId = null, int? templateId = null, string? searchTerm = null, CancellationToken cancellationToken = default);
/// <summary>Returns the most recent deployment records up to the specified count.</summary>
/// <param name="count">Maximum number of records to return.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<IReadOnlyList<DeploymentRecord>> GetRecentDeploymentsAsync(int count, CancellationToken cancellationToken = default);
/// <summary>Returns the area tree for the specified site.</summary>
/// <param name="siteId">The site database ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<IReadOnlyList<Area>> GetAreaTreeBySiteIdAsync(int siteId, CancellationToken cancellationToken = default);
// Audit log queries
/// <summary>
/// Queries audit log entries with optional filters, returning a page of results and the total matching count.
/// </summary>
/// <param name="user">Optional user filter.</param>
/// <param name="entityType">Optional entity type filter.</param>
/// <param name="action">Optional action filter.</param>
/// <param name="from">Optional start of date range filter.</param>
/// <param name="to">Optional end of date range filter.</param>
/// <param name="entityId">Optional entity ID filter.</param>
/// <param name="entityName">Optional entity name filter.</param>
/// <param name="bundleImportId">Optional bundle import correlation ID filter.</param>
/// <param name="page">One-based page number.</param>
/// <param name="pageSize">Number of entries per page.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<(IReadOnlyList<AuditLogEntry> Entries, int TotalCount)> GetAuditLogEntriesAsync(
string? user = null,
string? entityType = null,
@@ -30,5 +64,7 @@ public interface ICentralUiRepository
int pageSize = 50,
CancellationToken cancellationToken = default);
/// <summary>Persists pending changes to the underlying store.</summary>
/// <param name="cancellationToken">Cancellation token.</param>
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}
@@ -7,31 +7,149 @@ namespace ScadaLink.Commons.Interfaces.Repositories;
public interface IDeploymentManagerRepository
{
// DeploymentRecord
/// <summary>
/// Gets a deployment record by its ID.
/// </summary>
/// <param name="id">The deployment record ID.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>The deployment record, or null if not found.</returns>
Task<DeploymentRecord?> GetDeploymentRecordByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>
/// Gets all deployment records.
/// </summary>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A read-only list of all deployment records.</returns>
Task<IReadOnlyList<DeploymentRecord>> GetAllDeploymentRecordsAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Gets all deployment records for a specific instance.
/// </summary>
/// <param name="instanceId">The instance ID.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A read-only list of deployment records for the instance.</returns>
Task<IReadOnlyList<DeploymentRecord>> GetDeploymentsByInstanceIdAsync(int instanceId, CancellationToken cancellationToken = default);
/// <summary>
/// Gets the current deployment status for an instance.
/// </summary>
/// <param name="instanceId">The instance ID.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>The current deployment record, or null if no deployment exists.</returns>
Task<DeploymentRecord?> GetCurrentDeploymentStatusAsync(int instanceId, CancellationToken cancellationToken = default);
/// <summary>
/// Gets a deployment record by deployment ID.
/// </summary>
/// <param name="deploymentId">The deployment ID.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>The deployment record, or null if not found.</returns>
Task<DeploymentRecord?> GetDeploymentByDeploymentIdAsync(string deploymentId, CancellationToken cancellationToken = default);
/// <summary>
/// Adds a new deployment record.
/// </summary>
/// <param name="record">The deployment record to add.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task AddDeploymentRecordAsync(DeploymentRecord record, CancellationToken cancellationToken = default);
/// <summary>
/// Updates an existing deployment record.
/// </summary>
/// <param name="record">The deployment record to update.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task UpdateDeploymentRecordAsync(DeploymentRecord record, CancellationToken cancellationToken = default);
/// <summary>
/// Deletes a deployment record by ID.
/// </summary>
/// <param name="id">The deployment record ID to delete.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task DeleteDeploymentRecordAsync(int id, CancellationToken cancellationToken = default);
// SystemArtifactDeploymentRecord
/// <summary>
/// Gets a system artifact deployment record by ID.
/// </summary>
/// <param name="id">The system artifact deployment record ID.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>The system artifact deployment record, or null if not found.</returns>
Task<SystemArtifactDeploymentRecord?> GetSystemArtifactDeploymentByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>
/// Gets all system artifact deployment records.
/// </summary>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A read-only list of all system artifact deployment records.</returns>
Task<IReadOnlyList<SystemArtifactDeploymentRecord>> GetAllSystemArtifactDeploymentsAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Adds a new system artifact deployment record.
/// </summary>
/// <param name="record">The system artifact deployment record to add.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task AddSystemArtifactDeploymentAsync(SystemArtifactDeploymentRecord record, CancellationToken cancellationToken = default);
/// <summary>
/// Updates an existing system artifact deployment record.
/// </summary>
/// <param name="record">The system artifact deployment record to update.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task UpdateSystemArtifactDeploymentAsync(SystemArtifactDeploymentRecord record, CancellationToken cancellationToken = default);
/// <summary>
/// Deletes a system artifact deployment record by ID.
/// </summary>
/// <param name="id">The system artifact deployment record ID to delete.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task DeleteSystemArtifactDeploymentAsync(int id, CancellationToken cancellationToken = default);
// WP-8: DeployedConfigSnapshot
/// <summary>
/// Gets the deployed config snapshot for an instance.
/// </summary>
/// <param name="instanceId">The instance ID.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>The deployed config snapshot, or null if not found.</returns>
Task<DeployedConfigSnapshot?> GetDeployedSnapshotByInstanceIdAsync(int instanceId, CancellationToken cancellationToken = default);
/// <summary>
/// Adds a new deployed config snapshot.
/// </summary>
/// <param name="snapshot">The deployed config snapshot to add.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task AddDeployedSnapshotAsync(DeployedConfigSnapshot snapshot, CancellationToken cancellationToken = default);
/// <summary>
/// Updates an existing deployed config snapshot.
/// </summary>
/// <param name="snapshot">The deployed config snapshot to update.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task UpdateDeployedSnapshotAsync(DeployedConfigSnapshot snapshot, CancellationToken cancellationToken = default);
/// <summary>
/// Deletes the deployed config snapshot for an instance.
/// </summary>
/// <param name="instanceId">The instance ID.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task DeleteDeployedSnapshotAsync(int instanceId, CancellationToken cancellationToken = default);
// Instance lookups for deployment pipeline
/// <summary>
/// Gets an instance by ID.
/// </summary>
/// <param name="instanceId">The instance ID.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>The instance, or null if not found.</returns>
Task<Instance?> GetInstanceByIdAsync(int instanceId, CancellationToken cancellationToken = default);
/// <summary>
/// Gets an instance by unique name.
/// </summary>
/// <param name="uniqueName">The unique instance name.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>The instance, or null if not found.</returns>
Task<Instance?> GetInstanceByUniqueNameAsync(string uniqueName, CancellationToken cancellationToken = default);
/// <summary>
/// Updates an instance.
/// </summary>
/// <param name="instance">The instance to update.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task UpdateInstanceAsync(Instance instance, CancellationToken cancellationToken = default);
/// <summary>
@@ -39,7 +157,15 @@ public interface IDeploymentManagerRepository
/// records, deployed config snapshot, attribute/alarm overrides, and
/// connection bindings.
/// </summary>
/// <param name="instanceId">The instance ID to delete.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task DeleteInstanceAsync(int instanceId, CancellationToken cancellationToken = default);
/// <summary>
/// Saves all pending changes to the database.
/// </summary>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A task representing the number of entities saved.</returns>
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}
@@ -5,6 +5,12 @@ namespace ScadaLink.Commons.Interfaces.Repositories;
public interface IExternalSystemRepository
{
// ExternalSystemDefinition
/// <summary>
/// Gets an external system definition by ID.
/// </summary>
/// <param name="id">The external system ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The external system definition, or null if not found.</returns>
Task<ExternalSystemDefinition?> GetExternalSystemByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>
@@ -13,14 +19,46 @@ public interface IExternalSystemRepository
/// <c>ExternalSystem.Call()</c>) does not have to fetch every system and filter
/// in memory on each call (ExternalSystemGateway-011).
/// </summary>
/// <param name="name">The external system name.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The external system definition, or null if not found.</returns>
Task<ExternalSystemDefinition?> GetExternalSystemByNameAsync(string name, CancellationToken cancellationToken = default);
/// <summary>
/// Gets all external system definitions.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A read-only list of external system definitions.</returns>
Task<IReadOnlyList<ExternalSystemDefinition>> GetAllExternalSystemsAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Adds a new external system definition.
/// </summary>
/// <param name="definition">The external system definition to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task AddExternalSystemAsync(ExternalSystemDefinition definition, CancellationToken cancellationToken = default);
/// <summary>
/// Updates an existing external system definition.
/// </summary>
/// <param name="definition">The external system definition to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task UpdateExternalSystemAsync(ExternalSystemDefinition definition, CancellationToken cancellationToken = default);
/// <summary>
/// Deletes an external system definition by ID.
/// </summary>
/// <param name="id">The external system ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task DeleteExternalSystemAsync(int id, CancellationToken cancellationToken = default);
// ExternalSystemMethod
/// <summary>
/// Gets an external system method by ID.
/// </summary>
/// <param name="id">The method ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The external system method, or null if not found.</returns>
Task<ExternalSystemMethod?> GetExternalSystemMethodByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>
@@ -29,14 +67,48 @@ public interface IExternalSystemRepository
/// resolution does not have to fetch every method of the system and filter in
/// memory on each call (ExternalSystemGateway-011).
/// </summary>
/// <param name="externalSystemId">The external system ID.</param>
/// <param name="methodName">The method name.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The external system method, or null if not found.</returns>
Task<ExternalSystemMethod?> GetMethodByNameAsync(int externalSystemId, string methodName, CancellationToken cancellationToken = default);
/// <summary>
/// Gets all methods for a given external system.
/// </summary>
/// <param name="externalSystemId">The external system ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A read-only list of external system methods.</returns>
Task<IReadOnlyList<ExternalSystemMethod>> GetMethodsByExternalSystemIdAsync(int externalSystemId, CancellationToken cancellationToken = default);
/// <summary>
/// Adds a new external system method.
/// </summary>
/// <param name="method">The external system method to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task AddExternalSystemMethodAsync(ExternalSystemMethod method, CancellationToken cancellationToken = default);
/// <summary>
/// Updates an existing external system method.
/// </summary>
/// <param name="method">The external system method to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task UpdateExternalSystemMethodAsync(ExternalSystemMethod method, CancellationToken cancellationToken = default);
/// <summary>
/// Deletes an external system method by ID.
/// </summary>
/// <param name="id">The method ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task DeleteExternalSystemMethodAsync(int id, CancellationToken cancellationToken = default);
// DatabaseConnectionDefinition
/// <summary>
/// Gets a database connection definition by ID.
/// </summary>
/// <param name="id">The database connection ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The database connection definition, or null if not found.</returns>
Task<DatabaseConnectionDefinition?> GetDatabaseConnectionByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>
@@ -46,12 +118,43 @@ public interface IExternalSystemRepository
/// not have to fetch every connection and filter in memory on each call
/// (ExternalSystemGateway-011).
/// </summary>
/// <param name="name">The database connection name.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The database connection definition, or null if not found.</returns>
Task<DatabaseConnectionDefinition?> GetDatabaseConnectionByNameAsync(string name, CancellationToken cancellationToken = default);
/// <summary>
/// Gets all database connection definitions.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A read-only list of database connection definitions.</returns>
Task<IReadOnlyList<DatabaseConnectionDefinition>> GetAllDatabaseConnectionsAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Adds a new database connection definition.
/// </summary>
/// <param name="definition">The database connection definition to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task AddDatabaseConnectionAsync(DatabaseConnectionDefinition definition, CancellationToken cancellationToken = default);
/// <summary>
/// Updates an existing database connection definition.
/// </summary>
/// <param name="definition">The database connection definition to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task UpdateDatabaseConnectionAsync(DatabaseConnectionDefinition definition, CancellationToken cancellationToken = default);
/// <summary>
/// Deletes a database connection definition by ID.
/// </summary>
/// <param name="id">The database connection ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task DeleteDatabaseConnectionAsync(int id, CancellationToken cancellationToken = default);
/// <summary>
/// Saves pending changes to the repository.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The number of entities saved.</returns>
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}
@@ -5,21 +5,60 @@ namespace ScadaLink.Commons.Interfaces.Repositories;
public interface IInboundApiRepository
{
// ApiKey
/// <summary>Retrieves an API key by ID.</summary>
/// <param name="id">The API key ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<ApiKey?> GetApiKeyByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>Retrieves all API keys.</summary>
/// <param name="cancellationToken">Cancellation token.</param>
Task<IReadOnlyList<ApiKey>> GetAllApiKeysAsync(CancellationToken cancellationToken = default);
/// <summary>Retrieves an API key by value.</summary>
/// <param name="keyValue">The API key value.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<ApiKey?> GetApiKeyByValueAsync(string keyValue, CancellationToken cancellationToken = default);
/// <summary>Adds a new API key.</summary>
/// <param name="apiKey">The API key to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task AddApiKeyAsync(ApiKey apiKey, CancellationToken cancellationToken = default);
/// <summary>Updates an existing API key.</summary>
/// <param name="apiKey">The API key to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task UpdateApiKeyAsync(ApiKey apiKey, CancellationToken cancellationToken = default);
/// <summary>Deletes an API key by ID.</summary>
/// <param name="id">The API key ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task DeleteApiKeyAsync(int id, CancellationToken cancellationToken = default);
// ApiMethod
/// <summary>Retrieves an API method by ID.</summary>
/// <param name="id">The API method ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<ApiMethod?> GetApiMethodByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>Retrieves all API methods.</summary>
/// <param name="cancellationToken">Cancellation token.</param>
Task<IReadOnlyList<ApiMethod>> GetAllApiMethodsAsync(CancellationToken cancellationToken = default);
/// <summary>Retrieves an API method by name.</summary>
/// <param name="name">The API method name.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<ApiMethod?> GetMethodByNameAsync(string name, CancellationToken cancellationToken = default);
/// <summary>Retrieves API keys approved for a method.</summary>
/// <param name="methodId">The API method ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<IReadOnlyList<ApiKey>> GetApprovedKeysForMethodAsync(int methodId, CancellationToken cancellationToken = default);
/// <summary>Adds a new API method.</summary>
/// <param name="method">The API method to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task AddApiMethodAsync(ApiMethod method, CancellationToken cancellationToken = default);
/// <summary>Updates an existing API method.</summary>
/// <param name="method">The API method to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task UpdateApiMethodAsync(ApiMethod method, CancellationToken cancellationToken = default);
/// <summary>Deletes an API method by ID.</summary>
/// <param name="id">The API method ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task DeleteApiMethodAsync(int id, CancellationToken cancellationToken = default);
/// <summary>Saves pending changes to the database.</summary>
/// <param name="cancellationToken">Cancellation token.</param>
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}
@@ -22,6 +22,9 @@ public interface INotificationOutboxRepository
/// row was inserted, <c>false</c> when an existing row was left untouched.
/// Commits internally — this call is its own transaction.
/// </summary>
/// <param name="n">The notification to insert.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>True if inserted, false if already exists.</returns>
Task<bool> InsertIfNotExistsAsync(Notification n, CancellationToken cancellationToken = default);
/// <summary>
@@ -30,21 +33,35 @@ public interface INotificationOutboxRepository
/// Terminal rows are excluded. Ordered by <c>CreatedAt</c> ascending, capped at
/// <paramref name="batchSize"/>.
/// </summary>
/// <param name="now">The current time for evaluating due retries.</param>
/// <param name="batchSize">Maximum number of rows to return.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A list of notifications ready for delivery.</returns>
Task<IReadOnlyList<Notification>> GetDueAsync(DateTimeOffset now, int batchSize, CancellationToken cancellationToken = default);
/// <summary>Returns the notification with the given id, or <c>null</c>.</summary>
/// <param name="notificationId">The notification identifier.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The notification, or null if not found.</returns>
Task<Notification?> GetByIdAsync(string notificationId, CancellationToken cancellationToken = default);
/// <summary>
/// Marks <paramref name="n"/> modified and persists it (status transitions).
/// Commits internally — this call is its own transaction.
/// </summary>
/// <param name="n">The notification to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task UpdateAsync(Notification n, CancellationToken cancellationToken = default);
/// <summary>
/// Returns a page of notifications matching <paramref name="filter"/>, ordered by
/// <c>CreatedAt</c> descending, together with the total matching count.
/// </summary>
/// <param name="filter">The query filter.</param>
/// <param name="pageNumber">The page number (1-based).</param>
/// <param name="pageSize">The page size.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A tuple of rows and total count.</returns>
Task<(IReadOnlyList<Notification> Rows, int TotalCount)> QueryAsync(
NotificationOutboxFilter filter, int pageNumber, int pageSize, CancellationToken cancellationToken = default);
@@ -52,6 +69,9 @@ public interface INotificationOutboxRepository
/// Bulk-deletes terminal rows (Delivered/Parked/Discarded) whose <c>CreatedAt</c> is
/// older than <paramref name="cutoff"/>. Returns the number of rows deleted.
/// </summary>
/// <param name="cutoff">The cutoff time for deletion.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The number of rows deleted.</returns>
Task<int> DeleteTerminalOlderThanAsync(DateTimeOffset cutoff, CancellationToken cancellationToken = default);
/// <summary>
@@ -59,6 +79,10 @@ public interface INotificationOutboxRepository
/// delivered cutoffs are supplied by the caller; the current time used for
/// <c>OldestPendingAge</c> is captured inside the method.
/// </summary>
/// <param name="stuckCutoff">The time threshold for marking notifications as stuck.</param>
/// <param name="deliveredSince">The time threshold for counting delivered notifications.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A KPI snapshot.</returns>
Task<NotificationKpiSnapshot> ComputeKpisAsync(
DateTimeOffset stuckCutoff, DateTimeOffset deliveredSince, CancellationToken cancellationToken = default);
@@ -68,6 +92,10 @@ public interface INotificationOutboxRepository
/// are supplied by the caller; the current time used for <c>OldestPendingAge</c> is
/// captured inside the method.
/// </summary>
/// <param name="stuckCutoff">The time threshold for marking notifications as stuck.</param>
/// <param name="deliveredSince">The time threshold for counting delivered notifications.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A list of per-site KPI snapshots.</returns>
Task<IReadOnlyList<SiteNotificationKpiSnapshot>> ComputePerSiteKpisAsync(
DateTimeOffset stuckCutoff, DateTimeOffset deliveredSince, CancellationToken cancellationToken = default);
@@ -76,5 +104,7 @@ public interface INotificationOutboxRepository
/// multiple changes for a single commit; the individual mutating methods on this
/// interface already commit on their own.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The number of changes persisted.</returns>
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}
@@ -5,26 +5,95 @@ namespace ScadaLink.Commons.Interfaces.Repositories;
public interface INotificationRepository
{
// NotificationList
/// <summary>Gets a notification list by ID.</summary>
/// <param name="id">The notification list ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The notification list, or null if not found.</returns>
Task<NotificationList?> GetNotificationListByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>Gets all notification lists.</summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A read-only list of notification lists.</returns>
Task<IReadOnlyList<NotificationList>> GetAllNotificationListsAsync(CancellationToken cancellationToken = default);
/// <summary>Gets a notification list by name.</summary>
/// <param name="name">The notification list name.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The notification list, or null if not found.</returns>
Task<NotificationList?> GetListByNameAsync(string name, CancellationToken cancellationToken = default);
/// <summary>Adds a new notification list.</summary>
/// <param name="list">The notification list to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task AddNotificationListAsync(NotificationList list, CancellationToken cancellationToken = default);
/// <summary>Updates an existing notification list.</summary>
/// <param name="list">The notification list to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task UpdateNotificationListAsync(NotificationList list, CancellationToken cancellationToken = default);
/// <summary>Deletes a notification list by ID.</summary>
/// <param name="id">The notification list ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task DeleteNotificationListAsync(int id, CancellationToken cancellationToken = default);
// NotificationRecipient
/// <summary>Gets a notification recipient by ID.</summary>
/// <param name="id">The recipient ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The notification recipient, or null if not found.</returns>
Task<NotificationRecipient?> GetRecipientByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>Gets all recipients in a notification list.</summary>
/// <param name="notificationListId">The notification list ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A read-only list of recipients.</returns>
Task<IReadOnlyList<NotificationRecipient>> GetRecipientsByListIdAsync(int notificationListId, CancellationToken cancellationToken = default);
/// <summary>Adds a new notification recipient.</summary>
/// <param name="recipient">The recipient to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task AddRecipientAsync(NotificationRecipient recipient, CancellationToken cancellationToken = default);
/// <summary>Updates an existing notification recipient.</summary>
/// <param name="recipient">The recipient to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task UpdateRecipientAsync(NotificationRecipient recipient, CancellationToken cancellationToken = default);
/// <summary>Deletes a notification recipient by ID.</summary>
/// <param name="id">The recipient ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task DeleteRecipientAsync(int id, CancellationToken cancellationToken = default);
// SmtpConfiguration
/// <summary>Gets an SMTP configuration by ID.</summary>
/// <param name="id">The SMTP configuration ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The SMTP configuration, or null if not found.</returns>
Task<SmtpConfiguration?> GetSmtpConfigurationByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>Gets all SMTP configurations.</summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A read-only list of SMTP configurations.</returns>
Task<IReadOnlyList<SmtpConfiguration>> GetAllSmtpConfigurationsAsync(CancellationToken cancellationToken = default);
/// <summary>Adds a new SMTP configuration.</summary>
/// <param name="configuration">The SMTP configuration to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task AddSmtpConfigurationAsync(SmtpConfiguration configuration, CancellationToken cancellationToken = default);
/// <summary>Updates an existing SMTP configuration.</summary>
/// <param name="configuration">The SMTP configuration to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task UpdateSmtpConfigurationAsync(SmtpConfiguration configuration, CancellationToken cancellationToken = default);
/// <summary>Deletes an SMTP configuration by ID.</summary>
/// <param name="id">The SMTP configuration ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task DeleteSmtpConfigurationAsync(int id, CancellationToken cancellationToken = default);
/// <summary>Saves pending changes to the repository.</summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The number of entities saved.</returns>
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}
@@ -5,19 +5,89 @@ namespace ScadaLink.Commons.Interfaces.Repositories;
public interface ISecurityRepository
{
// LdapGroupMapping
/// <summary>
/// Gets an LDAP group mapping by ID.
/// </summary>
/// <param name="id">The mapping ID.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>The LDAP group mapping, or null if not found.</returns>
Task<LdapGroupMapping?> GetMappingByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>
/// Gets all LDAP group mappings.
/// </summary>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A read-only list of all LDAP group mappings.</returns>
Task<IReadOnlyList<LdapGroupMapping>> GetAllMappingsAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Gets all LDAP group mappings for a specific role.
/// </summary>
/// <param name="role">The role name.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A read-only list of LDAP group mappings for the role.</returns>
Task<IReadOnlyList<LdapGroupMapping>> GetMappingsByRoleAsync(string role, CancellationToken cancellationToken = default);
/// <summary>
/// Adds a new LDAP group mapping.
/// </summary>
/// <param name="mapping">The LDAP group mapping to add.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task AddMappingAsync(LdapGroupMapping mapping, CancellationToken cancellationToken = default);
/// <summary>
/// Updates an existing LDAP group mapping.
/// </summary>
/// <param name="mapping">The LDAP group mapping to update.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task UpdateMappingAsync(LdapGroupMapping mapping, CancellationToken cancellationToken = default);
/// <summary>
/// Deletes an LDAP group mapping by ID.
/// </summary>
/// <param name="id">The mapping ID to delete.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task DeleteMappingAsync(int id, CancellationToken cancellationToken = default);
// SiteScopeRule
/// <summary>
/// Gets a site scope rule by ID.
/// </summary>
/// <param name="id">The scope rule ID.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>The site scope rule, or null if not found.</returns>
Task<SiteScopeRule?> GetScopeRuleByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>
/// Gets all site scope rules for an LDAP group mapping.
/// </summary>
/// <param name="ldapGroupMappingId">The LDAP group mapping ID.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A read-only list of scope rules for the mapping.</returns>
Task<IReadOnlyList<SiteScopeRule>> GetScopeRulesForMappingAsync(int ldapGroupMappingId, CancellationToken cancellationToken = default);
/// <summary>
/// Adds a new site scope rule.
/// </summary>
/// <param name="rule">The site scope rule to add.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task AddScopeRuleAsync(SiteScopeRule rule, CancellationToken cancellationToken = default);
/// <summary>
/// Updates an existing site scope rule.
/// </summary>
/// <param name="rule">The site scope rule to update.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task UpdateScopeRuleAsync(SiteScopeRule rule, CancellationToken cancellationToken = default);
/// <summary>
/// Deletes a site scope rule by ID.
/// </summary>
/// <param name="id">The scope rule ID to delete.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task DeleteScopeRuleAsync(int id, CancellationToken cancellationToken = default);
/// <summary>
/// Saves all pending changes to the database.
/// </summary>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A task representing the number of entities saved.</returns>
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}
@@ -37,11 +37,15 @@ public interface ISiteCallAuditRepository
/// the stored status' rank. Out-of-order / duplicate updates are silently
/// dropped (monotonic forward-only progression).
/// </summary>
/// <param name="siteCall">The site call row to insert or monotonically update.</param>
/// <param name="ct">Cancellation token.</param>
Task UpsertAsync(SiteCall siteCall, CancellationToken ct = default);
/// <summary>
/// Returns the row for the given id, or <c>null</c> if none exists.
/// </summary>
/// <param name="id">The tracked operation id to look up.</param>
/// <param name="ct">Cancellation token.</param>
Task<SiteCall?> GetAsync(TrackedOperationId id, CancellationToken ct = default);
/// <summary>
@@ -51,6 +55,9 @@ public interface ISiteCallAuditRepository
/// <see cref="SiteCallPaging.AfterCreatedAtUtc"/> + <see cref="SiteCallPaging.AfterId"/>
/// to fetch subsequent pages.
/// </summary>
/// <param name="filter">Filter criteria for the query.</param>
/// <param name="paging">Keyset paging parameters.</param>
/// <param name="ct">Cancellation token.</param>
Task<IReadOnlyList<SiteCall>> QueryAsync(
SiteCallQueryFilter filter,
SiteCallPaging paging,
@@ -62,6 +69,8 @@ public interface ISiteCallAuditRepository
/// (TerminalAtUtc IS NULL) are NEVER purged. Returns the number of rows
/// deleted.
/// </summary>
/// <param name="olderThanUtc">UTC cutoff; terminal rows older than this are deleted.</param>
/// <param name="ct">Cancellation token.</param>
Task<int> PurgeTerminalAsync(DateTime olderThanUtc, CancellationToken ct = default);
/// <summary>
@@ -72,6 +81,9 @@ public interface ISiteCallAuditRepository
/// <paramref name="intervalSince"/>; the current time for <c>OldestPendingAge</c>
/// is captured inside the method.
/// </summary>
/// <param name="stuckCutoff">UTC threshold for classifying a row as stuck.</param>
/// <param name="intervalSince">UTC start of the delivered/failed interval window.</param>
/// <param name="ct">Cancellation token.</param>
Task<SiteCallKpiSnapshot> ComputeKpisAsync(
DateTime stuckCutoff,
DateTime intervalSince,
@@ -82,6 +94,9 @@ public interface ISiteCallAuditRepository
/// site. Sites with no <c>SiteCalls</c> rows at all are omitted. The stuck
/// cutoff and interval bounds are interpreted as in <see cref="ComputeKpisAsync"/>.
/// </summary>
/// <param name="stuckCutoff">UTC threshold for classifying a row as stuck.</param>
/// <param name="intervalSince">UTC start of the delivered/failed interval window.</param>
/// <param name="ct">Cancellation token.</param>
Task<IReadOnlyList<SiteCallSiteKpiSnapshot>> ComputePerSiteKpisAsync(
DateTime stuckCutoff,
DateTime intervalSince,
@@ -9,23 +9,62 @@ namespace ScadaLink.Commons.Interfaces.Repositories;
public interface ISiteRepository
{
// Sites
/// <summary>Retrieves a site by its ID.</summary>
/// <param name="id">The site primary key.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<Site?> GetSiteByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>Retrieves a site by its identifier.</summary>
/// <param name="siteIdentifier">The unique site identifier string.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<Site?> GetSiteByIdentifierAsync(string siteIdentifier, CancellationToken cancellationToken = default);
/// <summary>Retrieves all sites.</summary>
/// <param name="cancellationToken">Cancellation token.</param>
Task<IReadOnlyList<Site>> GetAllSitesAsync(CancellationToken cancellationToken = default);
/// <summary>Adds a new site.</summary>
/// <param name="site">The site entity to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task AddSiteAsync(Site site, CancellationToken cancellationToken = default);
/// <summary>Updates an existing site.</summary>
/// <param name="site">The site entity with updated values.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task UpdateSiteAsync(Site site, CancellationToken cancellationToken = default);
/// <summary>Deletes a site.</summary>
/// <param name="id">The site primary key.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task DeleteSiteAsync(int id, CancellationToken cancellationToken = default);
// Data Connections
/// <summary>Retrieves a data connection by its ID.</summary>
/// <param name="id">The data connection primary key.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<DataConnection?> GetDataConnectionByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>Retrieves all data connections.</summary>
/// <param name="cancellationToken">Cancellation token.</param>
Task<IReadOnlyList<DataConnection>> GetAllDataConnectionsAsync(CancellationToken cancellationToken = default);
/// <summary>Retrieves all data connections for a site.</summary>
/// <param name="siteId">The site primary key to filter by.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<IReadOnlyList<DataConnection>> GetDataConnectionsBySiteIdAsync(int siteId, CancellationToken cancellationToken = default);
/// <summary>Adds a new data connection.</summary>
/// <param name="connection">The data connection entity to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task AddDataConnectionAsync(DataConnection connection, CancellationToken cancellationToken = default);
/// <summary>Updates an existing data connection.</summary>
/// <param name="connection">The data connection entity with updated values.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task UpdateDataConnectionAsync(DataConnection connection, CancellationToken cancellationToken = default);
/// <summary>Deletes a data connection.</summary>
/// <param name="id">The data connection primary key.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task DeleteDataConnectionAsync(int id, CancellationToken cancellationToken = default);
// Instances (for deletion constraint checks)
/// <summary>Retrieves all instances deployed to a site.</summary>
/// <param name="siteId">The site primary key to filter by.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<IReadOnlyList<Instance>> GetInstancesBySiteIdAsync(int siteId, CancellationToken cancellationToken = default);
/// <summary>Saves all pending changes to the database.</summary>
/// <param name="cancellationToken">Cancellation token.</param>
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}
@@ -7,8 +7,16 @@ namespace ScadaLink.Commons.Interfaces.Repositories;
public interface ITemplateEngineRepository
{
// Template
/// <summary>Retrieves a template by ID.</summary>
/// <param name="id">The template ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<Template?> GetTemplateByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>Retrieves a template with its child entities by ID.</summary>
/// <param name="id">The template ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<Template?> GetTemplateWithChildrenAsync(int id, CancellationToken cancellationToken = default);
/// <summary>Retrieves all templates.</summary>
/// <param name="cancellationToken">Cancellation token.</param>
Task<IReadOnlyList<Template>> GetAllTemplatesAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Returns every template that contains a composition referencing
@@ -16,89 +24,271 @@ public interface ITemplateEngineRepository
/// its Attributes / Scripts / Compositions so the caller can build a
/// CompositionContext without a follow-up round-trip per parent.
/// </summary>
/// <param name="composedTemplateId">The composed template ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<IReadOnlyList<Template>> GetTemplatesComposingAsync(int composedTemplateId, CancellationToken cancellationToken = default);
/// <summary>Adds a new template.</summary>
/// <param name="template">The template to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task AddTemplateAsync(Template template, CancellationToken cancellationToken = default);
/// <summary>Updates an existing template.</summary>
/// <param name="template">The template to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task UpdateTemplateAsync(Template template, CancellationToken cancellationToken = default);
/// <summary>Deletes a template by ID.</summary>
/// <param name="id">The template ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task DeleteTemplateAsync(int id, CancellationToken cancellationToken = default);
// TemplateAttribute
/// <summary>Retrieves a template attribute by ID.</summary>
/// <param name="id">The attribute ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<TemplateAttribute?> GetTemplateAttributeByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>Retrieves attributes for a template.</summary>
/// <param name="templateId">The template ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<IReadOnlyList<TemplateAttribute>> GetAttributesByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default);
/// <summary>Adds a new template attribute.</summary>
/// <param name="attribute">The attribute to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task AddTemplateAttributeAsync(TemplateAttribute attribute, CancellationToken cancellationToken = default);
/// <summary>Updates an existing template attribute.</summary>
/// <param name="attribute">The attribute to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task UpdateTemplateAttributeAsync(TemplateAttribute attribute, CancellationToken cancellationToken = default);
/// <summary>Deletes a template attribute by ID.</summary>
/// <param name="id">The attribute ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task DeleteTemplateAttributeAsync(int id, CancellationToken cancellationToken = default);
// TemplateAlarm
/// <summary>Retrieves a template alarm by ID.</summary>
/// <param name="id">The alarm ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<TemplateAlarm?> GetTemplateAlarmByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>Retrieves alarms for a template.</summary>
/// <param name="templateId">The template ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<IReadOnlyList<TemplateAlarm>> GetAlarmsByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default);
/// <summary>Adds a new template alarm.</summary>
/// <param name="alarm">The alarm to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task AddTemplateAlarmAsync(TemplateAlarm alarm, CancellationToken cancellationToken = default);
/// <summary>Updates an existing template alarm.</summary>
/// <param name="alarm">The alarm to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task UpdateTemplateAlarmAsync(TemplateAlarm alarm, CancellationToken cancellationToken = default);
/// <summary>Deletes a template alarm by ID.</summary>
/// <param name="id">The alarm ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task DeleteTemplateAlarmAsync(int id, CancellationToken cancellationToken = default);
// TemplateScript
/// <summary>Retrieves a template script by ID.</summary>
/// <param name="id">The script ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<TemplateScript?> GetTemplateScriptByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>Retrieves scripts for a template.</summary>
/// <param name="templateId">The template ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<IReadOnlyList<TemplateScript>> GetScriptsByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default);
/// <summary>Adds a new template script.</summary>
/// <param name="script">The script to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task AddTemplateScriptAsync(TemplateScript script, CancellationToken cancellationToken = default);
/// <summary>Updates an existing template script.</summary>
/// <param name="script">The script to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task UpdateTemplateScriptAsync(TemplateScript script, CancellationToken cancellationToken = default);
/// <summary>Deletes a template script by ID.</summary>
/// <param name="id">The script ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task DeleteTemplateScriptAsync(int id, CancellationToken cancellationToken = default);
// TemplateComposition
/// <summary>Retrieves a template composition by ID.</summary>
/// <param name="id">The composition ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<TemplateComposition?> GetTemplateCompositionByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>Retrieves compositions for a template.</summary>
/// <param name="templateId">The template ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<IReadOnlyList<TemplateComposition>> GetCompositionsByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default);
/// <summary>Adds a new template composition.</summary>
/// <param name="composition">The composition to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task AddTemplateCompositionAsync(TemplateComposition composition, CancellationToken cancellationToken = default);
/// <summary>Updates an existing template composition.</summary>
/// <param name="composition">The composition to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task UpdateTemplateCompositionAsync(TemplateComposition composition, CancellationToken cancellationToken = default);
/// <summary>Deletes a template composition by ID.</summary>
/// <param name="id">The composition ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task DeleteTemplateCompositionAsync(int id, CancellationToken cancellationToken = default);
// Instance
/// <summary>Retrieves an instance by ID.</summary>
/// <param name="id">The instance ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<Instance?> GetInstanceByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>Retrieves all instances.</summary>
/// <param name="cancellationToken">Cancellation token.</param>
Task<IReadOnlyList<Instance>> GetAllInstancesAsync(CancellationToken cancellationToken = default);
/// <summary>Retrieves instances for a template.</summary>
/// <param name="templateId">The template ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<IReadOnlyList<Instance>> GetInstancesByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default);
/// <summary>Retrieves instances for a site.</summary>
/// <param name="siteId">The site ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<IReadOnlyList<Instance>> GetInstancesBySiteIdAsync(int siteId, CancellationToken cancellationToken = default);
/// <summary>Retrieves an instance by unique name.</summary>
/// <param name="uniqueName">The unique instance name.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<Instance?> GetInstanceByUniqueNameAsync(string uniqueName, CancellationToken cancellationToken = default);
/// <summary>Adds a new instance.</summary>
/// <param name="instance">The instance to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task AddInstanceAsync(Instance instance, CancellationToken cancellationToken = default);
/// <summary>Updates an existing instance.</summary>
/// <param name="instance">The instance to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task UpdateInstanceAsync(Instance instance, CancellationToken cancellationToken = default);
/// <summary>Deletes an instance by ID.</summary>
/// <param name="id">The instance ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task DeleteInstanceAsync(int id, CancellationToken cancellationToken = default);
// InstanceAttributeOverride
/// <summary>Retrieves attribute overrides for an instance.</summary>
/// <param name="instanceId">The instance ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<IReadOnlyList<InstanceAttributeOverride>> GetOverridesByInstanceIdAsync(int instanceId, CancellationToken cancellationToken = default);
/// <summary>Adds a new instance attribute override.</summary>
/// <param name="attributeOverride">The override to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task AddInstanceAttributeOverrideAsync(InstanceAttributeOverride attributeOverride, CancellationToken cancellationToken = default);
/// <summary>Updates an existing instance attribute override.</summary>
/// <param name="attributeOverride">The override to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task UpdateInstanceAttributeOverrideAsync(InstanceAttributeOverride attributeOverride, CancellationToken cancellationToken = default);
/// <summary>Deletes an instance attribute override by ID.</summary>
/// <param name="id">The override ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task DeleteInstanceAttributeOverrideAsync(int id, CancellationToken cancellationToken = default);
// InstanceAlarmOverride
/// <summary>Retrieves alarm overrides for an instance.</summary>
/// <param name="instanceId">The instance ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<IReadOnlyList<InstanceAlarmOverride>> GetAlarmOverridesByInstanceIdAsync(int instanceId, CancellationToken cancellationToken = default);
/// <summary>Retrieves an alarm override by instance and alarm name.</summary>
/// <param name="instanceId">The instance ID.</param>
/// <param name="alarmCanonicalName">The alarm canonical name.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<InstanceAlarmOverride?> GetAlarmOverrideAsync(int instanceId, string alarmCanonicalName, CancellationToken cancellationToken = default);
/// <summary>Adds a new instance alarm override.</summary>
/// <param name="alarmOverride">The override to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task AddInstanceAlarmOverrideAsync(InstanceAlarmOverride alarmOverride, CancellationToken cancellationToken = default);
/// <summary>Updates an existing instance alarm override.</summary>
/// <param name="alarmOverride">The override to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task UpdateInstanceAlarmOverrideAsync(InstanceAlarmOverride alarmOverride, CancellationToken cancellationToken = default);
/// <summary>Deletes an instance alarm override by ID.</summary>
/// <param name="id">The override ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task DeleteInstanceAlarmOverrideAsync(int id, CancellationToken cancellationToken = default);
// InstanceConnectionBinding
/// <summary>Retrieves connection bindings for an instance.</summary>
/// <param name="instanceId">The instance ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<IReadOnlyList<InstanceConnectionBinding>> GetBindingsByInstanceIdAsync(int instanceId, CancellationToken cancellationToken = default);
/// <summary>Adds a new instance connection binding.</summary>
/// <param name="binding">The binding to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task AddInstanceConnectionBindingAsync(InstanceConnectionBinding binding, CancellationToken cancellationToken = default);
/// <summary>Updates an existing instance connection binding.</summary>
/// <param name="binding">The binding to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task UpdateInstanceConnectionBindingAsync(InstanceConnectionBinding binding, CancellationToken cancellationToken = default);
/// <summary>Deletes an instance connection binding by ID.</summary>
/// <param name="id">The binding ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task DeleteInstanceConnectionBindingAsync(int id, CancellationToken cancellationToken = default);
// Area
/// <summary>Retrieves an area by ID.</summary>
/// <param name="id">The area ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<Area?> GetAreaByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>Retrieves areas for a site.</summary>
/// <param name="siteId">The site ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<IReadOnlyList<Area>> GetAreasBySiteIdAsync(int siteId, CancellationToken cancellationToken = default);
/// <summary>Adds a new area.</summary>
/// <param name="area">The area to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task AddAreaAsync(Area area, CancellationToken cancellationToken = default);
/// <summary>Updates an existing area.</summary>
/// <param name="area">The area to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task UpdateAreaAsync(Area area, CancellationToken cancellationToken = default);
/// <summary>Deletes an area by ID.</summary>
/// <param name="id">The area ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task DeleteAreaAsync(int id, CancellationToken cancellationToken = default);
// SharedScript
/// <summary>Retrieves a shared script by ID.</summary>
/// <param name="id">The script ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<SharedScript?> GetSharedScriptByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>Retrieves a shared script by name.</summary>
/// <param name="name">The script name.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<SharedScript?> GetSharedScriptByNameAsync(string name, CancellationToken cancellationToken = default);
/// <summary>Retrieves all shared scripts.</summary>
/// <param name="cancellationToken">Cancellation token.</param>
Task<IReadOnlyList<SharedScript>> GetAllSharedScriptsAsync(CancellationToken cancellationToken = default);
/// <summary>Adds a new shared script.</summary>
/// <param name="sharedScript">The script to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task AddSharedScriptAsync(SharedScript sharedScript, CancellationToken cancellationToken = default);
/// <summary>Updates an existing shared script.</summary>
/// <param name="sharedScript">The script to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task UpdateSharedScriptAsync(SharedScript sharedScript, CancellationToken cancellationToken = default);
/// <summary>Deletes a shared script by ID.</summary>
/// <param name="id">The script ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task DeleteSharedScriptAsync(int id, CancellationToken cancellationToken = default);
// TemplateFolder
/// <summary>Retrieves a template folder by ID.</summary>
/// <param name="id">The folder ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<TemplateFolder?> GetFolderByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>Retrieves all template folders.</summary>
/// <param name="cancellationToken">Cancellation token.</param>
Task<IReadOnlyList<TemplateFolder>> GetAllFoldersAsync(CancellationToken cancellationToken = default);
/// <summary>Adds a new template folder.</summary>
/// <param name="folder">The folder to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task AddFolderAsync(TemplateFolder folder, CancellationToken cancellationToken = default);
/// <summary>Updates an existing template folder.</summary>
/// <param name="folder">The folder to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task UpdateFolderAsync(TemplateFolder folder, CancellationToken cancellationToken = default);
/// <summary>Deletes a template folder by ID.</summary>
/// <param name="id">The folder ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task DeleteFolderAsync(int id, CancellationToken cancellationToken = default);
/// <summary>Saves pending changes to the database.</summary>
/// <param name="cancellationToken">Cancellation token.</param>
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}
@@ -2,5 +2,15 @@ namespace ScadaLink.Commons.Interfaces.Services;
public interface IAuditService
{
/// <summary>
/// Appends an audit log entry recording a user action on an entity.
/// </summary>
/// <param name="user">The authenticated username performing the action.</param>
/// <param name="action">The action performed (e.g., "Create", "Update", "Delete").</param>
/// <param name="entityType">The type name of the affected entity.</param>
/// <param name="entityId">The string representation of the entity's primary key.</param>
/// <param name="entityName">The display name of the affected entity.</param>
/// <param name="afterState">The entity state after the action; may be null for deletes.</param>
/// <param name="cancellationToken">Cancellation token for the log write.</param>
Task LogAsync(string user, string action, string entityType, string entityId, string entityName, object? afterState, CancellationToken cancellationToken = default);
}
@@ -13,5 +13,7 @@ public interface IAuditWriter
/// Persist an audit event. Best-effort: implementations must swallow/log internal failures
/// rather than propagating them to the calling boundary code.
/// </summary>
/// <param name="evt">The audit event to persist.</param>
/// <param name="ct">Cancellation token.</param>
Task WriteAsync(AuditEvent evt, CancellationToken ct = default);
}
@@ -34,6 +34,8 @@ public interface ICachedCallLifecycleObserver
/// the per-category channel discriminator, retry-count + last-error
/// context, and whether the outcome reached a terminal state.
/// </summary>
/// <param name="context">Per-attempt context including the tracking id, outcome, and audit provenance fields.</param>
/// <param name="ct">Cancellation token for the observation operation.</param>
Task OnAttemptCompletedAsync(CachedCallAttemptContext context, CancellationToken ct = default);
}
@@ -30,5 +30,7 @@ public interface ICachedCallTelemetryForwarder
/// swallowed; the returned Task completes when both halves have been
/// attempted.
/// </summary>
/// <param name="telemetry">The combined-telemetry packet to fan out.</param>
/// <param name="ct">Cancellation token.</param>
Task ForwardAsync(CachedCallTelemetry telemetry, CancellationToken ct = default);
}
@@ -12,5 +12,7 @@ public interface ICentralAuditWriter
/// Persist an audit event into the central AuditLog table directly (bypassing site telemetry).
/// Best-effort: implementations must swallow/log internal failures rather than propagating them.
/// </summary>
/// <param name="evt">The audit event to persist.</param>
/// <param name="ct">Cancellation token.</param>
Task WriteAsync(AuditEvent evt, CancellationToken ct = default);
}
@@ -14,6 +14,8 @@ public interface IDatabaseGateway
/// Connection pooling is managed by the underlying provider.
/// Caller is responsible for disposing.
/// </summary>
/// <param name="connectionName">Name of the configured database connection to open.</param>
/// <param name="cancellationToken">Cancellation token for the async open operation.</param>
Task<DbConnection> GetConnectionAsync(
string connectionName,
CancellationToken cancellationToken = default);
@@ -48,6 +50,11 @@ public interface IDatabaseGateway
/// retry-loop cached-write audit rows carry it. <c>null</c> for a
/// non-routed run.
/// </param>
/// <param name="connectionName">Name of the configured database connection to write to.</param>
/// <param name="sql">SQL statement to execute as a store-and-forward write.</param>
/// <param name="parameters">Optional SQL parameters for the statement.</param>
/// <param name="originInstanceName">Optional name of the instance that originated the write.</param>
/// <param name="cancellationToken">Cancellation token for the buffering operation.</param>
Task CachedWriteAsync(
string connectionName,
string sql,
@@ -11,6 +11,11 @@ public interface IExternalSystemClient
/// <summary>
/// Synchronous call to an external system. All failures returned to caller.
/// </summary>
/// <param name="systemName">The name of the external system.</param>
/// <param name="methodName">The name of the method to invoke.</param>
/// <param name="parameters">Method parameters as a dictionary, or null if none.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The result of the external call.</returns>
Task<ExternalCallResult> CallAsync(
string systemName,
string methodName,
@@ -21,6 +26,11 @@ public interface IExternalSystemClient
/// Attempt immediate delivery; on transient failure, hand to S&amp;F engine.
/// Permanent failures returned to caller.
/// </summary>
/// <param name="systemName">The name of the external system.</param>
/// <param name="methodName">The name of the method to invoke.</param>
/// <param name="parameters">Method parameters as a dictionary, or null if none.</param>
/// <param name="originInstanceName">The instance name originating the call, or null.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <param name="trackedOperationId">
/// Audit Log #23 (M3): caller-supplied tracking id used as the
/// store-and-forward message id so the S&amp;F retry loop can read it
@@ -49,6 +59,7 @@ public interface IExternalSystemClient
/// retry-loop cached-call audit rows carry it. <c>null</c> for a non-routed
/// run.
/// </param>
/// <returns>The result of the external call.</returns>
Task<ExternalCallResult> CachedCallAsync(
string systemName,
string methodName,
@@ -10,6 +10,8 @@ public interface IInstanceLocator
/// Resolves the site identifier for a given instance unique name.
/// Returns null if the instance is not found.
/// </summary>
/// <param name="instanceUniqueName">System-wide unique name of the instance to look up.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<string?> GetSiteIdForInstanceAsync(
string instanceUniqueName,
CancellationToken cancellationToken = default);
@@ -10,6 +10,11 @@ public interface INotificationDeliveryService
/// Sends a notification to a named list. Transient failures go to S&amp;F.
/// Permanent failures returned to caller.
/// </summary>
/// <param name="listName">Name of the notification list to deliver to.</param>
/// <param name="subject">Subject line of the notification.</param>
/// <param name="message">Plain-text body of the notification.</param>
/// <param name="originInstanceName">Optional name of the instance that triggered the send.</param>
/// <param name="cancellationToken">Cancellation token for the async operation.</param>
Task<NotificationResult> SendAsync(
string listName,
string subject,
@@ -33,6 +33,8 @@ public interface ISiteAuditQueue
/// oldest first. Idempotent — repeated calls before
/// <see cref="MarkForwardedAsync"/> will yield the same rows again.
/// </summary>
/// <param name="limit">Maximum number of rows to return.</param>
/// <param name="ct">Cancellation token.</param>
Task<IReadOnlyList<AuditEvent>> ReadPendingAsync(int limit, CancellationToken ct = default);
/// <summary>
@@ -41,6 +43,8 @@ public interface ISiteAuditQueue
/// <see cref="ScadaLink.Commons.Types.Enums.AuditForwardState.Forwarded"/>.
/// Non-existent or already-forwarded ids are silent no-ops.
/// </summary>
/// <param name="eventIds">Event IDs to mark as forwarded.</param>
/// <param name="ct">Cancellation token.</param>
Task MarkForwardedAsync(IReadOnlyList<Guid> eventIds, CancellationToken ct = default);
/// <summary>
@@ -58,6 +62,9 @@ public interface ISiteAuditQueue
/// is oldest <see cref="AuditEvent.OccurredAtUtc"/> first with
/// <see cref="AuditEvent.EventId"/> as the deterministic tiebreaker.
/// </remarks>
/// <param name="sinceUtc">Lower bound timestamp (UTC).</param>
/// <param name="batchSize">Maximum number of rows to return.</param>
/// <param name="ct">Cancellation token.</param>
Task<IReadOnlyList<AuditEvent>> ReadPendingSinceAsync(
DateTime sinceUtc, int batchSize, CancellationToken ct = default);
@@ -70,6 +77,8 @@ public interface ISiteAuditQueue
/// Rows already in <see cref="ScadaLink.Commons.Types.Enums.AuditForwardState.Reconciled"/>
/// are left untouched (idempotent re-call). Non-existent ids are silent no-ops.
/// </summary>
/// <param name="eventIds">Event IDs to mark as reconciled.</param>
/// <param name="ct">Cancellation token.</param>
Task MarkReconciledAsync(IReadOnlyList<Guid> eventIds, CancellationToken ct = default);
/// <summary>
@@ -83,5 +92,6 @@ public interface ISiteAuditQueue
/// implementations are expected to take the same connection lock used by
/// the hot-path INSERT batch and the drain queries.
/// </summary>
/// <param name="ct">Cancellation token.</param>
Task<SiteAuditBacklogSnapshot> GetBacklogStatsAsync(CancellationToken ct = default);
}
@@ -18,5 +18,6 @@ namespace ScadaLink.Commons.Interfaces.Transport;
/// </summary>
public interface IAuditCorrelationContext
{
/// <summary>Gets or sets the bundle import id used to correlate audit rows written during a bundle apply operation.</summary>
Guid? BundleImportId { get; set; }
}
@@ -4,6 +4,14 @@ namespace ScadaLink.Commons.Interfaces.Transport;
public interface IBundleExporter
{
/// <summary>
/// Exports the selected artifacts as an encrypted or plain bundle stream.
/// </summary>
/// <param name="selection">Specifies which artifact types and ids to include in the bundle.</param>
/// <param name="user">Username of the operator performing the export, stamped in the manifest.</param>
/// <param name="sourceEnvironment">Environment label stamped in the bundle manifest.</param>
/// <param name="passphrase">Optional passphrase to encrypt the bundle; null produces an unencrypted bundle.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<Stream> ExportAsync(
ExportSelection selection,
string user,
@@ -4,8 +4,28 @@ namespace ScadaLink.Commons.Interfaces.Transport;
public interface IBundleImporter
{
/// <summary>
/// Validates and decrypts the bundle stream, opens a session, and returns session metadata.
/// </summary>
/// <param name="bundleStream">Stream containing the bundle zip archive.</param>
/// <param name="passphrase">Optional passphrase for decrypting an encrypted bundle.</param>
/// <param name="ct">Cancellation token.</param>
Task<BundleSession> LoadAsync(Stream bundleStream, string? passphrase, CancellationToken ct = default);
/// <summary>
/// Diffs the loaded bundle against the target database and returns a per-artifact preview.
/// </summary>
/// <param name="sessionId">Session id returned by <see cref="LoadAsync"/>.</param>
/// <param name="ct">Cancellation token.</param>
Task<ImportPreview> PreviewAsync(Guid sessionId, CancellationToken ct = default);
/// <summary>
/// Applies the chosen conflict resolutions and commits the import transaction.
/// </summary>
/// <param name="sessionId">Session id returned by <see cref="LoadAsync"/>.</param>
/// <param name="resolutions">Per-artifact conflict resolutions from the preview step.</param>
/// <param name="user">Username of the operator performing the import, stamped in audit rows.</param>
/// <param name="ct">Cancellation token.</param>
Task<ImportResult> ApplyAsync(
Guid sessionId,
IReadOnlyList<ImportResolution> resolutions,
@@ -4,8 +4,15 @@ namespace ScadaLink.Commons.Interfaces.Transport;
public interface IBundleSessionStore
{
/// <summary>Stores the session and returns it; overwrites any existing session with the same id.</summary>
/// <param name="session">The session to store.</param>
BundleSession Open(BundleSession session);
/// <summary>Returns the session for the given id, or null if not found or expired.</summary>
/// <param name="sessionId">The session identifier to look up.</param>
BundleSession? Get(Guid sessionId);
/// <summary>Removes the session for the given id, if present.</summary>
/// <param name="sessionId">The session identifier to remove.</param>
void Remove(Guid sessionId);
/// <summary>Removes all sessions whose expiry has passed.</summary>
void EvictExpired();
}
@@ -25,6 +25,10 @@ public static class ManagementCommandRegistry
private static readonly FrozenDictionary<Type, string> NamesByType =
Commands.ToFrozenDictionary(kv => kv.Value, kv => kv.Key);
/// <summary>
/// Resolves a management command wire name to its CLR type, or null if not registered.
/// </summary>
/// <param name="commandName">The wire name of the management command (without the "Command" suffix).</param>
public static Type? Resolve(string commandName)
{
return Commands.GetValueOrDefault(commandName);
@@ -33,6 +37,7 @@ public static class ManagementCommandRegistry
/// <summary>
/// Returns the registered wire name for a management command type.
/// </summary>
/// <param name="commandType">The CLR type of the management command to look up.</param>
/// <exception cref="ArgumentException">
/// Thrown when <paramref name="commandType"/> is not a registered management
/// command — i.e. not a non-abstract <c>*Command</c> type in the
@@ -6,5 +6,6 @@ namespace ScadaLink.Commons.Messages.Streaming;
/// </summary>
public interface ISiteStreamEvent
{
/// <summary>The unique name of the instance that produced this event.</summary>
string InstanceUniqueName { get; }
}
@@ -34,6 +34,11 @@ public enum OpcUaConfigParseStatus
/// </summary>
public readonly record struct OpcUaConfigParseResult
{
/// <summary>
/// Initializes the result with the parsed config and its classification status.
/// </summary>
/// <param name="config">The parsed endpoint config (or an empty default on malformed input).</param>
/// <param name="status">Classification of the parse outcome.</param>
public OpcUaConfigParseResult(OpcUaEndpointConfig config, OpcUaConfigParseStatus status)
{
Config = config;
@@ -58,6 +63,8 @@ public readonly record struct OpcUaConfigParseResult
/// and <see cref="OpcUaConfigParseStatus.Malformed"/>; callers that need to tell those
/// apart should read <see cref="Status"/> directly.
/// </summary>
/// <param name="config">Receives the parsed endpoint config.</param>
/// <param name="isLegacy">Receives true when the source was the legacy flat-dict shape.</param>
public void Deconstruct(out OpcUaEndpointConfig config, out bool isLegacy)
{
config = Config;
@@ -83,6 +90,11 @@ public static class OpcUaEndpointConfigSerializer
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
};
/// <summary>
/// Serializes an <see cref="OpcUaEndpointConfig"/> to the current typed JSON shape.
/// </summary>
/// <param name="config">The endpoint configuration to serialize.</param>
/// <returns>A JSON string representing the configuration.</returns>
public static string Serialize(OpcUaEndpointConfig config)
=> JsonSerializer.Serialize(config, JsonOpts);
@@ -105,6 +117,7 @@ public static class OpcUaEndpointConfigSerializer
/// caller should surface an error rather than treating it as the user's saved data.</item>
/// </list>
/// </summary>
/// <param name="json">The stored JSON string to parse; null or blank yields a default typed result.</param>
public static OpcUaConfigParseResult Deserialize(string? json)
{
if (string.IsNullOrWhiteSpace(json))
@@ -161,6 +174,7 @@ public static class OpcUaEndpointConfigSerializer
/// IDataConnection.ConnectAsync expects. Keys match the historical convention
/// used by OpcUaDataConnection so the adapter can keep that interface.
/// </summary>
/// <param name="config">The endpoint configuration to flatten.</param>
public static IDictionary<string, string> ToFlatDict(OpcUaEndpointConfig config)
{
var dict = new Dictionary<string, string>
@@ -202,6 +216,11 @@ public static class OpcUaEndpointConfigSerializer
return dict;
}
/// <summary>
/// Reconstructs an <see cref="OpcUaEndpointConfig"/> from the legacy flat string-dict shape.
/// </summary>
/// <param name="dict">The flat key-value dictionary produced by the legacy shape.</param>
/// <returns>The reconstructed endpoint configuration.</returns>
public static OpcUaEndpointConfig FromFlatDict(IDictionary<string, string> dict)
{
var c = new OpcUaEndpointConfig();
@@ -35,6 +35,9 @@ public static class AuditQueryParamParsers
/// <paramref name="rawValues"/> is <c>null</c>, empty, or yields no parseable
/// value — so the filter dimension stays unconstrained.
/// </summary>
/// <typeparam name="TEnum">The enum type to parse each raw value into.</typeparam>
/// <param name="rawValues">Raw query-parameter string values to parse; may be null.</param>
/// <returns>A non-empty list of parsed values, or <c>null</c> if no values could be parsed.</returns>
public static IReadOnlyList<TEnum>? ParseEnumList<TEnum>(IEnumerable<string?>? rawValues)
where TEnum : struct, Enum
{
@@ -59,6 +62,8 @@ public static class AuditQueryParamParsers
/// <paramref name="rawValues"/> is <c>null</c>, empty, or every value was
/// blank.
/// </summary>
/// <param name="rawValues">Raw query-parameter string values to trim and filter; may be null.</param>
/// <returns>A non-empty list of trimmed strings, or <c>null</c> if no non-blank values remain.</returns>
public static IReadOnlyList<string>? ParseStringList(IEnumerable<string?>? rawValues)
{
if (rawValues is null)
@@ -2,6 +2,8 @@ namespace ScadaLink.Commons.Types.DataConnections;
public sealed class OpcUaDeadbandConfig
{
/// <summary>Gets or sets the OPC UA deadband type (Absolute or Percent).</summary>
public OpcUaDeadbandType Type { get; set; } = OpcUaDeadbandType.Absolute;
/// <summary>Gets or sets the deadband threshold value; meaning depends on <see cref="Type"/>.</summary>
public double Value { get; set; } = 0.0;
}
@@ -1,34 +1,91 @@
namespace ScadaLink.Commons.Types.DataConnections;
/// <summary>
/// OPC UA endpoint configuration.
/// </summary>
public sealed class OpcUaEndpointConfig
{
// Connection
/// <summary>
/// The OPC UA endpoint URL.
/// </summary>
public string EndpointUrl { get; set; } = "";
/// <summary>
/// The security mode for the connection.
/// </summary>
public OpcUaSecurityMode SecurityMode { get; set; } = OpcUaSecurityMode.None;
/// <summary>
/// Whether to automatically accept untrusted certificates.
/// </summary>
public bool AutoAcceptUntrustedCerts { get; set; } = true;
// Timing
/// <summary>
/// Session timeout in milliseconds.
/// </summary>
public int SessionTimeoutMs { get; set; } = 60000;
/// <summary>
/// Operation timeout in milliseconds.
/// </summary>
public int OperationTimeoutMs { get; set; } = 15000;
// Subscription
/// <summary>
/// Publishing interval in milliseconds.
/// </summary>
public int PublishingIntervalMs { get; set; } = 1000;
/// <summary>
/// Sampling interval in milliseconds.
/// </summary>
public int SamplingIntervalMs { get; set; } = 1000;
/// <summary>
/// Queue size for the subscription.
/// </summary>
public int QueueSize { get; set; } = 10;
/// <summary>
/// Keep-alive count for the subscription.
/// </summary>
public int KeepAliveCount { get; set; } = 10;
/// <summary>
/// Lifetime count for the subscription.
/// </summary>
public int LifetimeCount { get; set; } = 30;
/// <summary>
/// Maximum notifications per publish.
/// </summary>
public int MaxNotificationsPerPublish { get; set; } = 100;
/// <summary>
/// Whether to discard oldest notifications when queue is full.
/// </summary>
public bool DiscardOldest { get; set; } = true;
/// <summary>
/// Subscription priority level.
/// </summary>
public byte SubscriptionPriority { get; set; } = 0;
/// <summary>
/// Display name for the subscription.
/// </summary>
public string SubscriptionDisplayName { get; set; } = "ScadaLink";
// Read / filter
/// <summary>
/// Timestamps to return in read operations.
/// </summary>
public OpcUaTimestampsToReturn TimestampsToReturn { get; set; } = OpcUaTimestampsToReturn.Source;
/// <summary>
/// Deadband configuration for filtering notifications.
/// </summary>
public OpcUaDeadbandConfig? Deadband { get; set; }
// Authentication (optional; null = anonymous)
/// <summary>
/// User identity configuration for authentication.
/// </summary>
public OpcUaUserIdentityConfig? UserIdentity { get; set; }
// Heartbeat (optional)
/// <summary>
/// Heartbeat configuration for connection monitoring.
/// </summary>
public OpcUaHeartbeatConfig? Heartbeat { get; set; }
}
@@ -2,6 +2,8 @@ namespace ScadaLink.Commons.Types.DataConnections;
public sealed class OpcUaHeartbeatConfig
{
/// <summary>OPC UA node path of the heartbeat tag to monitor for activity.</summary>
public string TagPath { get; set; } = "";
/// <summary>Maximum number of seconds without a value update before the connection is considered unhealthy.</summary>
public int MaxSilenceSeconds { get; set; } = 30;
}
@@ -1,10 +1,18 @@
namespace ScadaLink.Commons.Types.DataConnections;
/// <summary>
/// OPC UA user identity configuration for a data connection endpoint.
/// </summary>
public sealed class OpcUaUserIdentityConfig
{
/// <summary>The OPC UA user token type (Anonymous, UserName, or Certificate).</summary>
public OpcUaUserTokenType TokenType { get; set; } = OpcUaUserTokenType.Anonymous;
/// <summary>Username for UserName token type authentication.</summary>
public string Username { get; set; } = "";
/// <summary>Password for UserName token type authentication.</summary>
public string Password { get; set; } = "";
/// <summary>File path to the X.509 certificate for Certificate token type authentication.</summary>
public string CertificatePath { get; set; } = "";
/// <summary>Password to unlock the certificate private key.</summary>
public string CertificatePassword { get; set; } = "";
}
@@ -20,6 +20,8 @@ public class DynamicJsonElement : DynamicObject
{
private readonly JsonElement _element;
/// <summary>Initializes a new <see cref="DynamicJsonElement"/> wrapping a clone of the given <see cref="JsonElement"/>.</summary>
/// <param name="element">The JSON element to wrap; it is cloned to decouple lifetime from the source document.</param>
public DynamicJsonElement(JsonElement element)
{
// Clone detaches the element from its owning JsonDocument so accessing it
@@ -27,6 +29,7 @@ public class DynamicJsonElement : DynamicObject
_element = element.Clone();
}
/// <inheritdoc />
public override bool TryGetMember(GetMemberBinder binder, out object? result)
{
if (_element.ValueKind == JsonValueKind.Object &&
@@ -39,6 +42,7 @@ public class DynamicJsonElement : DynamicObject
return false;
}
/// <inheritdoc />
public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object? result)
{
// Accept any integral index, not just int. DynamicJsonElement surfaces JSON
@@ -80,6 +84,7 @@ public class DynamicJsonElement : DynamicObject
}
}
/// <inheritdoc />
public override bool TryConvert(ConvertBinder binder, out object? result)
{
// Conversion to object (or dynamic): never null out a present value. Return the
@@ -97,6 +102,7 @@ public class DynamicJsonElement : DynamicObject
return result != null;
}
/// <inheritdoc />
public override string ToString()
{
return _element.ValueKind switch
@@ -6,13 +6,20 @@ namespace ScadaLink.Commons.Types.Flattening;
/// </summary>
public sealed record ConfigurationDiff
{
/// <summary>Unique name of the instance this diff applies to.</summary>
public string InstanceUniqueName { get; init; } = string.Empty;
/// <summary>Revision hash of the previously deployed configuration, or null if not previously deployed.</summary>
public string? OldRevisionHash { get; init; }
/// <summary>Revision hash of the new configuration being compared.</summary>
public string? NewRevisionHash { get; init; }
/// <summary>True when any attribute, alarm, or script changes are present.</summary>
public bool HasChanges => AttributeChanges.Count > 0 || AlarmChanges.Count > 0 || ScriptChanges.Count > 0;
/// <summary>Diff entries for resolved attributes.</summary>
public IReadOnlyList<DiffEntry<ResolvedAttribute>> AttributeChanges { get; init; } = [];
/// <summary>Diff entries for resolved alarms.</summary>
public IReadOnlyList<DiffEntry<ResolvedAlarm>> AlarmChanges { get; init; } = [];
/// <summary>Diff entries for resolved scripts.</summary>
public IReadOnlyList<DiffEntry<ResolvedScript>> ScriptChanges { get; init; } = [];
}
@@ -21,7 +28,9 @@ public sealed record ConfigurationDiff
/// </summary>
public sealed record DiffEntry<T>
{
/// <summary>The canonical name of the changed entity.</summary>
public string CanonicalName { get; init; } = string.Empty;
/// <summary>The type of change: Added, Removed, or Changed.</summary>
public DiffChangeType ChangeType { get; init; }
/// <summary>
@@ -9,13 +9,21 @@ namespace ScadaLink.Commons.Types.Flattening;
/// </summary>
public sealed record FlattenedConfiguration
{
/// <summary>Gets the instance unique name.</summary>
public string InstanceUniqueName { get; init; } = string.Empty;
/// <summary>Gets the template ID.</summary>
public int TemplateId { get; init; }
/// <summary>Gets the site ID.</summary>
public int SiteId { get; init; }
/// <summary>Gets the area ID, if any.</summary>
public int? AreaId { get; init; }
/// <summary>Gets the resolved attributes.</summary>
public IReadOnlyList<ResolvedAttribute> Attributes { get; init; } = [];
/// <summary>Gets the resolved alarms.</summary>
public IReadOnlyList<ResolvedAlarm> Alarms { get; init; } = [];
/// <summary>Gets the resolved scripts.</summary>
public IReadOnlyList<ResolvedScript> Scripts { get; init; } = [];
/// <summary>Gets the UTC timestamp when this configuration was generated.</summary>
public DateTimeOffset GeneratedAtUtc { get; init; } = DateTimeOffset.UtcNow;
/// <summary>
@@ -31,9 +39,13 @@ public sealed record FlattenedConfiguration
/// </summary>
public sealed record ConnectionConfig
{
/// <summary>Gets the protocol name (e.g., "OpcUa").</summary>
public string Protocol { get; init; } = string.Empty;
/// <summary>Gets the primary configuration as JSON.</summary>
public string? ConfigurationJson { get; init; }
/// <summary>Gets the backup configuration as JSON.</summary>
public string? BackupConfigurationJson { get; init; }
/// <summary>Gets the number of failover retries.</summary>
public int FailoverRetryCount { get; init; } = 3;
}
@@ -48,9 +60,13 @@ public sealed record ResolvedAttribute
/// </summary>
public string CanonicalName { get; init; } = string.Empty;
/// <summary>Gets the attribute value.</summary>
public string? Value { get; init; }
/// <summary>Gets the data type name.</summary>
public string DataType { get; init; } = string.Empty;
/// <summary>Gets whether the attribute is locked.</summary>
public bool IsLocked { get; init; }
/// <summary>Gets the attribute description.</summary>
public string? Description { get; init; }
/// <summary>
@@ -84,10 +100,15 @@ public sealed record ResolvedAttribute
/// </summary>
public sealed record ResolvedAlarm
{
/// <summary>Gets the path-qualified canonical name.</summary>
public string CanonicalName { get; init; } = string.Empty;
/// <summary>Gets the alarm description.</summary>
public string? Description { get; init; }
/// <summary>Gets the priority level.</summary>
public int PriorityLevel { get; init; }
/// <summary>Gets whether the alarm is locked.</summary>
public bool IsLocked { get; init; }
/// <summary>Gets the trigger type.</summary>
public string TriggerType { get; init; } = string.Empty;
/// <summary>
@@ -100,6 +121,7 @@ public sealed record ResolvedAlarm
/// </summary>
public string? OnTriggerScriptCanonicalName { get; init; }
/// <summary>Gets the source of this alarm value: "Template", "Inherited", "Composed", or "Override".</summary>
public string Source { get; init; } = "Template";
}
@@ -108,10 +130,15 @@ public sealed record ResolvedAlarm
/// </summary>
public sealed record ResolvedScript
{
/// <summary>Gets the path-qualified canonical name.</summary>
public string CanonicalName { get; init; } = string.Empty;
/// <summary>Gets the script code.</summary>
public string Code { get; init; } = string.Empty;
/// <summary>Gets whether the script is locked.</summary>
public bool IsLocked { get; init; }
/// <summary>Gets the trigger type.</summary>
public string? TriggerType { get; init; }
/// <summary>Gets the trigger configuration.</summary>
public string? TriggerConfiguration { get; init; }
/// <summary>
@@ -124,7 +151,9 @@ public sealed record ResolvedScript
/// </summary>
public string? ReturnDefinition { get; init; }
/// <summary>Gets the minimum time between script executions.</summary>
public TimeSpan? MinTimeBetweenRuns { get; init; }
/// <summary>Gets the source of this script.</summary>
public string Source { get; init; } = "Template";
/// <summary>
@@ -5,15 +5,23 @@ namespace ScadaLink.Commons.Types.Flattening;
/// </summary>
public sealed record ValidationResult
{
/// <summary>True when there are no validation errors.</summary>
public bool IsValid => Errors.Count == 0;
/// <summary>Validation errors that block the operation.</summary>
public IReadOnlyList<ValidationEntry> Errors { get; init; } = [];
/// <summary>Non-blocking validation warnings.</summary>
public IReadOnlyList<ValidationEntry> Warnings { get; init; } = [];
/// <summary>Returns a result with no errors or warnings.</summary>
public static ValidationResult Success() => new();
/// <summary>Returns a result containing the given errors.</summary>
/// <param name="errors">The validation errors to include.</param>
public static ValidationResult FromErrors(params ValidationEntry[] errors) =>
new() { Errors = errors };
/// <summary>Merges multiple validation results into a single combined result.</summary>
/// <param name="results">The results to merge.</param>
public static ValidationResult Merge(params ValidationResult[] results)
{
var errors = new List<ValidationEntry>();
@@ -32,7 +40,9 @@ public sealed record ValidationResult
/// </summary>
public sealed record ValidationEntry
{
/// <summary>The category classifying the kind of validation failure.</summary>
public ValidationCategory Category { get; init; }
/// <summary>Human-readable description of the validation issue.</summary>
public string Message { get; init; } = string.Empty;
/// <summary>
@@ -40,9 +50,17 @@ public sealed record ValidationEntry
/// </summary>
public string? EntityName { get; init; }
/// <summary>Creates an error entry with the given category, message, and optional entity name.</summary>
/// <param name="category">The validation category.</param>
/// <param name="message">The error message.</param>
/// <param name="entityName">The canonical name of the entity that caused the error, if any.</param>
public static ValidationEntry Error(ValidationCategory category, string message, string? entityName = null) =>
new() { Category = category, Message = message, EntityName = entityName };
/// <summary>Creates a warning entry with the given category, message, and optional entity name.</summary>
/// <param name="category">The validation category.</param>
/// <param name="message">The warning message.</param>
/// <param name="entityName">The canonical name of the entity that triggered the warning, if any.</param>
public static ValidationEntry Warning(ValidationCategory category, string message, string? entityName = null) =>
new() { Category = category, Message = message, EntityName = entityName };
}
@@ -16,6 +16,8 @@ public interface IApiKeyHasher
/// The same input always produces the same output (deterministic), which keeps
/// the by-value lookup working.
/// </summary>
/// <param name="apiKey">The raw API key to hash.</param>
/// <returns>A Base64-encoded HMAC-SHA256 hash of the key.</returns>
string Hash(string apiKey);
}
@@ -54,6 +56,7 @@ public sealed class ApiKeyHasher : IApiKeyHasher
/// <summary>
/// Creates a hasher keyed with the given server-side pepper.
/// </summary>
/// <param name="pepper">Server-side HMAC key; must be at least <see cref="MinimumPepperLength"/> characters.</param>
/// <exception cref="ArgumentException">
/// Thrown if <paramref name="pepper"/> is null, blank, or shorter than
/// <see cref="MinimumPepperLength"/> — a missing or weak pepper is a deployment
@@ -9,7 +9,10 @@ namespace ScadaLink.Commons.Types.InboundApi;
/// </summary>
public class ParameterDefinition
{
/// <summary>Gets or sets the parameter name as it must appear in the JSON request body.</summary>
public string Name { get; set; } = string.Empty;
/// <summary>Gets or sets the expected type (e.g. "String", "Integer", "Float", "Boolean", "Object", "List").</summary>
public string Type { get; set; } = "String";
/// <summary>Gets or sets whether this parameter must be present in the request body.</summary>
public bool Required { get; set; } = true;
}
+13
View File
@@ -22,27 +22,40 @@ public sealed class Result<T>
IsSuccess = false;
}
/// <summary>True when the result represents a successful outcome.</summary>
public bool IsSuccess { get; }
/// <summary>True when the result represents a failure outcome.</summary>
public bool IsFailure => !IsSuccess;
/// <summary>The success value; throws <see cref="InvalidOperationException"/> when <see cref="IsFailure"/> is true.</summary>
public T Value => IsSuccess
? _value!
: throw new InvalidOperationException("Cannot access Value on a failed Result. Error: " + _error);
/// <summary>The error message; throws <see cref="InvalidOperationException"/> when <see cref="IsSuccess"/> is true.</summary>
public string Error => IsFailure
? _error!
: throw new InvalidOperationException("Cannot access Error on a successful Result.");
/// <summary>Creates a successful result carrying the given value.</summary>
/// <param name="value">The success value.</param>
/// <returns>A successful <see cref="Result{T}"/>.</returns>
public static Result<T> Success(T value) => new(value);
/// <summary>
/// Creates a failed result carrying the given error message.
/// </summary>
/// <param name="error">Non-blank error message describing the failure.</param>
/// <exception cref="ArgumentNullException"><paramref name="error"/> is null.</exception>
/// <exception cref="ArgumentException"><paramref name="error"/> is empty or whitespace.</exception>
public static Result<T> Failure(string error) => new(error);
/// <summary>Pattern-matches the result, invoking either the success or failure delegate.</summary>
/// <typeparam name="TResult">The return type of both delegates.</typeparam>
/// <param name="onSuccess">Delegate invoked with the value when the result is successful.</param>
/// <param name="onFailure">Delegate invoked with the error message when the result is a failure.</param>
/// <returns>The value returned by the invoked delegate.</returns>
public TResult Match<TResult>(Func<T, TResult> onSuccess, Func<string, TResult> onFailure) =>
IsSuccess ? onSuccess(_value!) : onFailure(_error!);
}
@@ -16,6 +16,9 @@ namespace ScadaLink.Commons.Types;
/// </summary>
public static class ScriptArgs
{
/// <summary>Normalizes a loosely-typed parameters argument into a read-only string-keyed dictionary, or null if no parameters were supplied.</summary>
/// <param name="parameters">Null, an existing dictionary, or an anonymous object whose properties become parameter entries.</param>
/// <returns>A normalized read-only dictionary, or null when <paramref name="parameters"/> is null.</returns>
public static IReadOnlyDictionary<string, object?>? Normalize(object? parameters)
{
switch (parameters)
@@ -13,11 +13,18 @@ public class ScriptParameters : IReadOnlyDictionary<string, object?>
{
private readonly IReadOnlyDictionary<string, object?> _inner;
/// <summary>
/// Initializes a new instance of the <see cref="ScriptParameters"/> class with the specified parameters.
/// </summary>
/// <param name="parameters">The underlying parameter dictionary.</param>
public ScriptParameters(IReadOnlyDictionary<string, object?> parameters)
{
_inner = parameters ?? throw new ArgumentNullException(nameof(parameters));
}
/// <summary>
/// Initializes a new instance of the <see cref="ScriptParameters"/> class with an empty parameter dictionary.
/// </summary>
public ScriptParameters() : this(new Dictionary<string, object?>()) { }
/// <summary>
@@ -30,6 +37,9 @@ public class ScriptParameters : IReadOnlyDictionary<string, object?>
/// <item><c>Get&lt;List&lt;int&gt;&gt;("key")</c> — converts list to typed List; throws on first bad element.</item>
/// </list>
/// </summary>
/// <typeparam name="T">The target type for the parameter value.</typeparam>
/// <param name="key">The parameter key.</param>
/// <returns>The converted parameter value.</returns>
public T Get<T>(string key)
{
var targetType = typeof(T);
@@ -220,13 +230,43 @@ public class ScriptParameters : IReadOnlyDictionary<string, object?>
}
// IReadOnlyDictionary<string, object?> implementation
/// <summary>
/// Gets the value associated with the specified key.
/// </summary>
/// <param name="key">The parameter key.</param>
/// <returns>The parameter value, or null if the key is not found.</returns>
public object? this[string key] => _inner[key];
/// <summary>
/// Gets the collection of parameter keys.
/// </summary>
public IEnumerable<string> Keys => _inner.Keys;
/// <summary>
/// Gets the collection of parameter values.
/// </summary>
public IEnumerable<object?> Values => _inner.Values;
/// <summary>
/// Gets the number of parameters.
/// </summary>
public int Count => _inner.Count;
/// <summary>
/// Determines whether the parameters contain the specified key.
/// </summary>
/// <param name="key">The key to locate.</param>
/// <returns>True if the key is found; false otherwise.</returns>
public bool ContainsKey(string key) => _inner.ContainsKey(key);
/// <summary>
/// Attempts to get the value associated with the specified key.
/// </summary>
/// <param name="key">The key to locate.</param>
/// <param name="value">The value associated with the key if found; otherwise null.</param>
/// <returns>True if the key is found; false otherwise.</returns>
public bool TryGetValue(string key, out object? value) => _inner.TryGetValue(key, out value);
/// <summary>
/// Returns an enumerator that iterates through the parameters.
/// </summary>
/// <returns>An enumerator for the parameters.</returns>
public IEnumerator<KeyValuePair<string, object?>> GetEnumerator() => _inner.GetEnumerator();
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
@@ -236,7 +276,16 @@ public class ScriptParameters : IReadOnlyDictionary<string, object?>
/// </summary>
public class ScriptParameterException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="ScriptParameterException"/> class with a specified error message.
/// </summary>
/// <param name="message">The error message that explains the reason for the exception.</param>
public ScriptParameterException(string message) : base(message) { }
/// <summary>
/// Initializes a new instance of the <see cref="ScriptParameterException"/> class with a specified error message and inner exception.
/// </summary>
/// <param name="message">The error message that explains the reason for the exception.</param>
/// <param name="innerException">The exception that is the cause of the current exception.</param>
public ScriptParameterException(string message, Exception innerException)
: base(message, innerException) { }
}
@@ -11,8 +11,11 @@ namespace ScadaLink.Commons.Types.Scripts;
/// </summary>
public sealed class AlarmContext
{
/// <summary>Name of the alarm that fired.</summary>
public string Name { get; init; } = string.Empty;
/// <summary>Severity level of the alarm; <see cref="AlarmLevel.None"/> for binary trigger types.</summary>
public AlarmLevel Level { get; init; } = AlarmLevel.None;
/// <summary>Operator-assigned priority of the alarm.</summary>
public int Priority { get; init; }
/// <summary>
@@ -13,5 +13,6 @@ public sealed record ScriptScope(string SelfPath, string? ParentPath)
/// <summary>Scope for a script directly on the root template (no compositions).</summary>
public static readonly ScriptScope Root = new("", null);
/// <summary>Gets a value indicating whether this script has a parent composition path.</summary>
public bool HasParent => ParentPath != null;
}
@@ -33,6 +33,8 @@ public sealed class StaleTagMonitor : IDisposable
/// </summary>
private long _generation;
/// <summary>Initializes a new <see cref="StaleTagMonitor"/> that fires <see cref="Stale"/> if no value is received within <paramref name="maxSilence"/>.</summary>
/// <param name="maxSilence">The maximum time with no received value before the <see cref="Stale"/> event fires; must be positive.</param>
public StaleTagMonitor(TimeSpan maxSilence)
{
if (maxSilence <= TimeSpan.Zero)
@@ -46,6 +48,7 @@ public sealed class StaleTagMonitor : IDisposable
/// </summary>
public event Action? Stale;
/// <summary>Gets the maximum silence interval after which the <see cref="Stale"/> event fires.</summary>
public TimeSpan MaxSilence => _maxSilence;
/// <summary>
@@ -105,6 +108,7 @@ public sealed class StaleTagMonitor : IDisposable
}
}
/// <summary>Stops monitoring and disposes the internal timer.</summary>
public void Dispose()
{
Stop();
@@ -29,6 +29,7 @@ public readonly record struct TrackedOperationId(Guid Value)
/// is not a valid GUID — callers crossing untrusted boundaries should use
/// <see cref="TryParse"/> instead.
/// </summary>
/// <param name="s">GUID string to parse.</param>
public static TrackedOperationId Parse(string s) => new(Guid.Parse(s));
/// <summary>
@@ -36,6 +37,8 @@ public readonly record struct TrackedOperationId(Guid Value)
/// or non-GUID input; <paramref name="result"/> is <c>default</c> on
/// failure.
/// </summary>
/// <param name="s">GUID string to parse, or null.</param>
/// <param name="result">Parsed value on success; default on failure.</param>
public static bool TryParse(string? s, out TrackedOperationId result)
{
if (Guid.TryParse(s, out var g))
@@ -2,10 +2,16 @@ namespace ScadaLink.Commons.Types.Transport;
public sealed class BundleSession
{
/// <summary>Unique identifier for this import session.</summary>
public Guid SessionId { get; init; }
/// <summary>Parsed manifest from the uploaded bundle.</summary>
public BundleManifest Manifest { get; init; } = null!;
/// <summary>Decrypted bundle content bytes; empty until the bundle is successfully unlocked.</summary>
public byte[] DecryptedContent { get; init; } = Array.Empty<byte>();
/// <summary>UTC timestamp after which this session is considered expired and must be re-uploaded.</summary>
public DateTimeOffset ExpiresAt { get; init; }
/// <summary>Number of failed passphrase unlock attempts for this session.</summary>
public int FailedUnlockAttempts { get; set; }
/// <summary>True when three or more unlock attempts have failed, locking further attempts.</summary>
public bool Locked => FailedUnlockAttempts >= 3;
}
@@ -13,6 +13,7 @@ public static class ValueFormatter
/// Formats a value as a string. Returns the value's string representation for
/// scalars and comma-separated elements for array/collection types.
/// </summary>
/// <param name="value">The value to format; null returns an empty string.</param>
/// <remarks>
/// Formatting is <see cref="CultureInfo.InvariantCulture">culture-invariant</see>:
/// numbers and <see cref="DateTime"/> values render the same regardless of the
@@ -11,6 +11,11 @@ namespace ScadaLink.Commons.Validators;
/// </summary>
public static class OpcUaEndpointConfigValidator
{
/// <summary>
/// Validates all fields of an <see cref="OpcUaEndpointConfig"/>, returning errors with optionally-prefixed field names.
/// </summary>
/// <param name="config">The OPC UA endpoint configuration to validate.</param>
/// <param name="fieldPrefix">Optional prefix prepended to each field name in error entries (e.g., "Primary.").</param>
public static ValidationResult Validate(OpcUaEndpointConfig config, string fieldPrefix = "")
{
var errors = new List<ValidationEntry>();