refactor: rename ScadaLink → ZB.MOM.WW.ScadaBridge (code + projects + namespaces)
Solution + 23 src projects + 26 test projects renamed; folders, csproj, namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated. ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated. SQL roles/logins, LDAP domains, CLI command name, and CLI config dir (~/.scadalink → ~/.scadabridge) also renamed. Build green; 5 Host.Tests fail awaiting SQL login rename in next commit. Pre-existing StaleTagMonitor timing flakes unchanged. Rename script committed at tools/rename-to-scadabridge.sh.
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol;
|
||||
|
||||
public enum QualityCode { Good, Bad, Uncertain }
|
||||
|
||||
public record TagValue(object? Value, QualityCode Quality, DateTimeOffset Timestamp);
|
||||
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>
|
||||
/// Raised when the adapter detects an unexpected connection loss (e.g., gRPC stream error,
|
||||
/// network timeout). The DataConnectionActor listens for this to trigger reconnection
|
||||
/// and push bad quality to all subscribed tags.
|
||||
/// </summary>
|
||||
event Action? Disconnected;
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Append-only data access for the central <c>AuditLog</c> table (Audit Log #23).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The append-only invariant is enforced both at the SQL level (the
|
||||
/// <c>scadabridge_audit_writer</c> role has only INSERT + SELECT — UPDATE and DELETE
|
||||
/// are not granted) and at the API level: this interface deliberately exposes no
|
||||
/// Update and no single-row Delete. Bulk purge is performed exclusively via
|
||||
/// monthly partition switch-out (<see cref="SwitchOutPartitionAsync"/>).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Ingest is idempotent on <c>EventId</c>: <see cref="InsertIfNotExistsAsync"/> is
|
||||
/// first-write-wins, so retrying telemetry and reconciliation pulls can both feed
|
||||
/// the same writer without producing duplicates.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public interface IAuditLogRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Inserts <paramref name="evt"/> if no row with the same
|
||||
/// <see cref="AuditEvent.EventId"/> exists; otherwise silently leaves the
|
||||
/// 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>
|
||||
/// Returns up to <see cref="AuditLogPaging.PageSize"/> rows matching
|
||||
/// <paramref name="filter"/>, ordered by <c>(OccurredAtUtc DESC, EventId DESC)</c>.
|
||||
/// Use keyset paging by passing the last returned row's
|
||||
/// <c>OccurredAtUtc</c> + <c>EventId</c> back via
|
||||
/// <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,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Switches out (purges) the monthly partition whose lower bound is
|
||||
/// <paramref name="monthBoundary"/> and returns the approximate number
|
||||
/// of rows discarded — sampled inside the transaction BEFORE the switch
|
||||
/// so the row count reflects what the switch removed, not a post-purge
|
||||
/// scan of a table that no longer exists.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <b>Drop-and-rebuild dance.</b> <c>UX_AuditLog_EventId</c> is intentionally
|
||||
/// non-partition-aligned (it lives on <c>[PRIMARY]</c> so single-column
|
||||
/// EventId uniqueness — required by <see cref="InsertIfNotExistsAsync"/> —
|
||||
/// can be enforced cheaply). SQL Server rejects
|
||||
/// <c>ALTER TABLE … SWITCH PARTITION</c> while a non-aligned unique index
|
||||
/// is present, so the M6 implementation drops the index, creates a staging
|
||||
/// table with byte-identical schema, switches the partition's data into
|
||||
/// staging, drops staging (discarding the rows), and rebuilds the unique
|
||||
/// index. The CATCH branch guarantees the index is rebuilt even on partial
|
||||
/// failure so the table never returns to live traffic without its
|
||||
/// idempotency-supporting index.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Outage window.</b> The dance briefly removes the unique index, so
|
||||
/// concurrent <see cref="InsertIfNotExistsAsync"/> calls during the switch
|
||||
/// could in principle race past the IF NOT EXISTS check without the index
|
||||
/// catching the duplicate. This is acceptable for the daily purge cadence
|
||||
/// — the inserts that the IF NOT EXISTS check guards are themselves rare
|
||||
/// enough that a sub-second collision window is operationally negligible,
|
||||
/// 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>
|
||||
/// Returns the set of <c>pf_AuditLog_Month</c> partition lower-bound
|
||||
/// boundaries whose partitions contain only rows with
|
||||
/// <see cref="AuditEvent.OccurredAtUtc"/> strictly older than
|
||||
/// <paramref name="threshold"/>. Boundaries whose partition is empty are
|
||||
/// 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);
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log (#23) M7 Bundle E (T13) — returns aggregate counts over the
|
||||
/// trailing <paramref name="window"/> driving the central Health
|
||||
/// dashboard's Audit KPI tiles.
|
||||
/// </summary>
|
||||
/// <param name="window">
|
||||
/// Trailing time window (e.g. <c>TimeSpan.FromHours(1)</c>). Rows whose
|
||||
/// <c>OccurredAtUtc >= nowUtc - window</c> are counted; the upper
|
||||
/// bound is <paramref name="nowUtc"/>.
|
||||
/// </param>
|
||||
/// <param name="nowUtc">
|
||||
/// Optional explicit "now" timestamp used to anchor the trailing window.
|
||||
/// Defaults to <see cref="DateTime.UtcNow"/> at call time when null —
|
||||
/// production callers should leave this null; tests pin a deterministic
|
||||
/// value so the window is reproducible across runs.
|
||||
/// </param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>
|
||||
/// A snapshot with <c>TotalEventsLastHour</c> + <c>ErrorEventsLastHour</c>
|
||||
/// populated; <c>BacklogTotal</c> is left at zero (this method has no
|
||||
/// visibility into per-site backlogs — the service layer composes it in
|
||||
/// from <see cref="ZB.MOM.WW.ScadaBridge.HealthMonitoring.ICentralHealthAggregator"/>).
|
||||
/// <c>AsOfUtc</c> is set to the server-side <c>UtcNow</c> at the time of
|
||||
/// the query.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Implemented as a single aggregate query
|
||||
/// (<c>SELECT COUNT_BIG(*) AS Total, SUM(CASE …) AS Errors</c>) rather than
|
||||
/// two round trips so the volume + error rate tiles read a consistent
|
||||
/// snapshot — the denominator and numerator come from the same scan.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Errors are defined as <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditStatus.Failed"/>,
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditStatus.Parked"/>, or
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditStatus.Discarded"/>
|
||||
/// — every non-success terminal lifecycle state. <c>Submitted</c>,
|
||||
/// <c>Forwarded</c>, <c>Attempted</c> are in-flight and are NOT errors;
|
||||
/// <c>Delivered</c> is success; <c>Skipped</c> is an intentional no-op.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
Task<AuditLogKpiSnapshot> GetKpiSnapshotAsync(
|
||||
TimeSpan window,
|
||||
DateTime? nowUtc = null,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log ParentExecutionId feature (Task 8) — given any
|
||||
/// <paramref name="executionId"/> in an execution chain, returns the whole
|
||||
/// chain rooted at the topmost ancestor: one <see cref="ExecutionTreeNode"/>
|
||||
/// per distinct execution, summarising its <c>AuditLog</c> rows. The Central
|
||||
/// UI renders the result as a tree.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The input id may be any node in the chain — a leaf, the root, or a middle
|
||||
/// node. The implementation first walks <em>up</em> via
|
||||
/// <c>ParentExecutionId</c> to find the root, then walks <em>down</em> from
|
||||
/// the root via a recursive CTE, so the full chain is returned regardless of
|
||||
/// entry point.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The <c>ParentExecutionId</c> graph is a tree (acyclic by construction —
|
||||
/// each execution is minted fresh and its parent always pre-exists). Both
|
||||
/// the upward walk and the downward CTE are nonetheless bounded at 32 levels
|
||||
/// as a guard against corrupt/pathological data: a depth that exceeds the
|
||||
/// guard raises an error rather than hanging the server. Chains are shallow
|
||||
/// (1-2 levels typical) so the guard is never reached in practice.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// A "stub" node — an execution that emitted no rows of its own yet is
|
||||
/// referenced by a child via <c>ParentExecutionId</c>, or whose rows have
|
||||
/// been purged — still appears, with <see cref="ExecutionTreeNode.RowCount"/>
|
||||
/// = 0. A purged/missing parent simply ends the upward walk.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// When no <c>AuditLog</c> row carries <paramref name="executionId"/> in
|
||||
/// either <c>ExecutionId</c> or <c>ParentExecutionId</c>, the result is a
|
||||
/// single stub node for <paramref name="executionId"/> itself
|
||||
/// (<see cref="ExecutionTreeNode.RowCount"/> = 0) — consistent with the
|
||||
/// 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);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the distinct, non-null <c>SourceNode</c> values present in the
|
||||
/// <c>AuditLog</c> table, in ascending order. Backs the Audit Log page's
|
||||
/// "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);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Deployment;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.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,
|
||||
string? action = null,
|
||||
DateTimeOffset? from = null,
|
||||
DateTimeOffset? to = null,
|
||||
string? entityId = null,
|
||||
string? entityName = null,
|
||||
Guid? bundleImportId = null,
|
||||
int page = 1,
|
||||
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);
|
||||
}
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Deployment;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.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, enforcing optimistic concurrency against the
|
||||
/// supplied <paramref name="expectedRowVersion"/>. The caller MUST pass the
|
||||
/// <c>RowVersion</c> it last observed on the record so EF emits
|
||||
/// <c>DELETE ... WHERE Id = @id AND RowVersion = @prior</c>. A concurrent edit
|
||||
/// surfaces as <see cref="Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException"/>
|
||||
/// on <see cref="SaveChangesAsync(CancellationToken)"/>, matching the documented
|
||||
/// "Optimistic concurrency is used on deployment status records" design rule.
|
||||
/// </summary>
|
||||
/// <param name="id">The deployment record ID to delete.</param>
|
||||
/// <param name="expectedRowVersion">The RowVersion the caller observed; used as the optimistic-concurrency token.</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, byte[] expectedRowVersion, 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>
|
||||
/// Removes an instance and everything that depends on it: deployment
|
||||
/// 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);
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.ExternalSystems;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.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>
|
||||
/// Returns the external system with the given name, or <c>null</c> if no such
|
||||
/// system exists. A name-keyed lookup so hot-path resolution (e.g. a script's
|
||||
/// <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>
|
||||
/// Returns the method with the given name belonging to the given external system,
|
||||
/// or <c>null</c> if no such method exists. A name-keyed lookup so hot-path
|
||||
/// 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>
|
||||
/// Returns the database connection definition with the given name, or <c>null</c>
|
||||
/// if no such connection exists. A name-keyed lookup so hot-path resolution (e.g.
|
||||
/// a script's <c>Database.Connection()</c> / <c>Database.CachedWrite()</c>) does
|
||||
/// 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);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.InboundApi;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.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);
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Notifications;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Data access for the central notification outbox — the queue of <see cref="Notification"/>
|
||||
/// rows the outbox actor drains, retries, and audits. Distinct from
|
||||
/// <see cref="INotificationRepository"/>, which manages notification list configuration.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Persistence model: <see cref="InsertIfNotExistsAsync"/> and <see cref="UpdateAsync"/> commit
|
||||
/// internally, so each call is its own transaction — suited to the outbox actor committing one
|
||||
/// row's status transition at a time. The standalone <see cref="SaveChangesAsync"/> is available
|
||||
/// for callers that stage multiple changes and want to flush them together.
|
||||
/// </remarks>
|
||||
public interface INotificationOutboxRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Inserts <paramref name="n"/> only if no row with the same
|
||||
/// <see cref="Notification.NotificationId"/> exists. Returns <c>true</c> when a new
|
||||
/// 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>
|
||||
/// Returns notifications ready for a delivery attempt: <c>Pending</c> rows, plus
|
||||
/// <c>Retrying</c> rows whose <c>NextAttemptAt</c> is at or before <paramref name="now"/>.
|
||||
/// 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);
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
/// Computes a point-in-time <see cref="NotificationKpiSnapshot"/>. The stuck and
|
||||
/// 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);
|
||||
|
||||
/// <summary>
|
||||
/// Computes a point-in-time <see cref="SiteNotificationKpiSnapshot"/> per source site.
|
||||
/// Sites with no notification rows at all are omitted. The stuck and 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 list of per-site KPI snapshots.</returns>
|
||||
Task<IReadOnlyList<SiteNotificationKpiSnapshot>> ComputePerSiteKpisAsync(
|
||||
DateTimeOffset stuckCutoff, DateTimeOffset deliveredSince, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Persists pending changes tracked on the underlying context. Use this when staging
|
||||
/// 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);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.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);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Security;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.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);
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Operational-state data access for the central <c>SiteCalls</c> table
|
||||
/// (Site Call Audit #22, Audit Log #23 M3 Bundle B). One row per
|
||||
/// <see cref="TrackedOperationId"/>; sites remain the source of truth and this
|
||||
/// table is an eventually-consistent mirror fed by best-effort gRPC telemetry
|
||||
/// plus periodic reconciliation pulls.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Unlike the partitioned append-only <c>AuditLog</c> (M1), this table holds
|
||||
/// mutable operational state. <see cref="UpsertAsync"/> is insert-if-not-exists
|
||||
/// then monotonic update — a status update with rank less than or equal to the
|
||||
/// stored status is a silent no-op so out-of-order telemetry, duplicate gRPC
|
||||
/// packets, and reconciliation pulls can all feed the same writer without
|
||||
/// rolling state backward.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Status rank for monotonic comparison (lower wins): <c>Submitted=0,
|
||||
/// Forwarded=1, Attempted=2, Skipped=2, Delivered=3, Failed=3, Parked=3,
|
||||
/// Discarded=3</c>. Terminal statuses share rank 3 and are mutually exclusive
|
||||
/// — an attempt to upsert e.g. <c>Delivered</c> over an existing <c>Parked</c>
|
||||
/// row is a no-op.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public interface ISiteCallAuditRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Inserts <paramref name="siteCall"/> if no row with the same
|
||||
/// <see cref="SiteCall.TrackedOperationId"/> exists; otherwise updates the
|
||||
/// existing row IF AND ONLY IF the incoming status' rank strictly exceeds
|
||||
/// 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>
|
||||
/// Returns up to <see cref="SiteCallPaging.PageSize"/> rows matching
|
||||
/// <paramref name="filter"/>, ordered by <c>(CreatedAtUtc DESC,
|
||||
/// TrackedOperationId DESC)</c>. Use keyset paging via
|
||||
/// <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,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes terminal rows whose <see cref="SiteCall.TerminalAtUtc"/> is
|
||||
/// strictly older than <paramref name="olderThanUtc"/>. Non-terminal rows
|
||||
/// (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>
|
||||
/// Computes a point-in-time global <see cref="SiteCallKpiSnapshot"/> from the
|
||||
/// <c>SiteCalls</c> table. Counts are aggregated server-side (no row
|
||||
/// materialisation): <c>StuckCount</c> uses <paramref name="stuckCutoff"/>;
|
||||
/// <c>FailedLastInterval</c> / <c>DeliveredLastInterval</c> use
|
||||
/// <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,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Computes a point-in-time <see cref="SiteCallSiteKpiSnapshot"/> per source
|
||||
/// 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,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository interface for site and data connection management.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
+306
@@ -0,0 +1,306 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Scripts;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.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>
|
||||
/// Bulk variant of <see cref="GetTemplateWithChildrenAsync(int, CancellationToken)"/>
|
||||
/// that fetches every template whose <see cref="Template.Name"/> matches one of
|
||||
/// <paramref name="names"/> in a single SQL/EF query, eager-loading
|
||||
/// Attributes / Alarms / Scripts / Compositions. Resolves the Transport-008
|
||||
/// N+1 in <c>BundleImporter.PreviewAsync</c> — names that don't match an
|
||||
/// existing template are omitted from the result rather than producing a
|
||||
/// null entry, so callers should look up by name into the returned list.
|
||||
/// </summary>
|
||||
/// <param name="names">Template names to load. Duplicate / null / empty names are filtered out.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
Task<IReadOnlyList<Template>> GetTemplatesWithChildrenAsync(IEnumerable<string> names, 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
|
||||
/// <paramref name="composedTemplateId"/>. Each result is eager-loaded with
|
||||
/// 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);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace ZB.MOM.WW.ScadaBridge.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);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Boundary-side abstraction for emitting Audit Log (#23) events.
|
||||
/// Implementations on the site write to local SQLite hot-path; on central they write to MS SQL directly.
|
||||
/// Failures must NEVER abort the user-facing action.
|
||||
/// </summary>
|
||||
public interface IAuditWriter
|
||||
{
|
||||
/// <summary>
|
||||
/// 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);
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23 (M3 Bundle E — Tasks E4/E5): site-side hook the
|
||||
/// store-and-forward retry loop invokes after every cached-call attempt and
|
||||
/// at terminal-state transitions, so the audit pipeline can emit
|
||||
/// <c>ApiCallCached</c>/<c>DbWriteCached</c> per-attempt rows and the
|
||||
/// <c>CachedResolve</c> terminal row under the original
|
||||
/// <see cref="TrackedOperationId"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The interface deliberately uses <see cref="CachedCallAttemptOutcome"/>
|
||||
/// rather than <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditStatus"/> so the
|
||||
/// S&F project does not need to depend on the audit vocabulary — the
|
||||
/// bridge living in <c>ZB.MOM.WW.ScadaBridge.AuditLog</c> maps the outcome to the right
|
||||
/// audit kind + status when materialising the <c>CachedCallTelemetry</c>
|
||||
/// packet.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Best-effort contract (alog.md §7):</b> implementations MUST swallow
|
||||
/// internal failures rather than propagating to the S&F service — a
|
||||
/// thrown observer must not be misclassified as a transient delivery
|
||||
/// failure and must not corrupt the retry-count bookkeeping.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public interface ICachedCallLifecycleObserver
|
||||
{
|
||||
/// <summary>
|
||||
/// Called by the store-and-forward retry loop after every cached-call
|
||||
/// delivery attempt. Receives the message's TrackedOperationId-bearing id,
|
||||
/// 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Per-attempt context handed to <see cref="ICachedCallLifecycleObserver"/>.
|
||||
/// </summary>
|
||||
/// <param name="TrackedOperationId">
|
||||
/// Tracking id parsed from the underlying <c>StoreAndForwardMessage.Id</c>.
|
||||
/// </param>
|
||||
/// <param name="Channel">
|
||||
/// Trust-boundary channel string — <c>"ApiOutbound"</c> for ExternalSystem
|
||||
/// cached calls, <c>"DbOutbound"</c> for cached DB writes.
|
||||
/// </param>
|
||||
/// <param name="Target">Human-readable target (system name / DB connection).</param>
|
||||
/// <param name="SourceSite">Site id that submitted the cached call.</param>
|
||||
/// <param name="Outcome">Per-attempt outcome.</param>
|
||||
/// <param name="RetryCount">Number of retries performed so far (S&F bookkeeping).</param>
|
||||
/// <param name="LastError">Most recent error message (null on success).</param>
|
||||
/// <param name="HttpStatus">Most recent HTTP status (null when not applicable).</param>
|
||||
/// <param name="CreatedAtUtc">When the underlying S&F message was first enqueued.</param>
|
||||
/// <param name="OccurredAtUtc">When this attempt completed.</param>
|
||||
/// <param name="DurationMs">Duration of the attempt in milliseconds (null when not measured).</param>
|
||||
/// <param name="SourceInstanceId">Originating instance, when known.</param>
|
||||
/// <param name="ExecutionId">
|
||||
/// Audit Log #23 (ExecutionId Task 4): the originating script execution's
|
||||
/// per-run correlation id, threaded through the store-and-forward buffer from
|
||||
/// the cached-call enqueue path. The audit bridge stamps it onto the
|
||||
/// retry-loop <c>ApiCallCached</c>/<c>DbWriteCached</c> Attempted and
|
||||
/// <c>CachedResolve</c> rows so they correlate with the rest of the run.
|
||||
/// <c>null</c> for rows buffered before Task 4 (back-compat).
|
||||
/// </param>
|
||||
/// <param name="SourceScript">
|
||||
/// Audit Log #23 (ExecutionId Task 4): the originating script identifier,
|
||||
/// threaded alongside <paramref name="ExecutionId"/> so the retry-loop audit
|
||||
/// rows carry the same <c>SourceScript</c> provenance the script-side cached
|
||||
/// rows already do. <c>null</c> when not known.
|
||||
/// </param>
|
||||
/// <param name="ParentExecutionId">
|
||||
/// Audit Log #23 (ParentExecutionId Task 6): the <c>ExecutionId</c> of the
|
||||
/// inbound-API request that spawned the originating script execution,
|
||||
/// threaded through the store-and-forward buffer alongside
|
||||
/// <paramref name="ExecutionId"/>. The audit bridge stamps it onto the
|
||||
/// retry-loop <c>ApiCallCached</c>/<c>DbWriteCached</c> Attempted and
|
||||
/// <c>CachedResolve</c> rows so they correlate back to the spawning run.
|
||||
/// <c>null</c> for a non-routed run and for rows buffered before Task 6
|
||||
/// (back-compat).
|
||||
/// </param>
|
||||
public sealed record CachedCallAttemptContext(
|
||||
TrackedOperationId TrackedOperationId,
|
||||
string Channel,
|
||||
string Target,
|
||||
string SourceSite,
|
||||
CachedCallAttemptOutcome Outcome,
|
||||
int RetryCount,
|
||||
string? LastError,
|
||||
int? HttpStatus,
|
||||
DateTime CreatedAtUtc,
|
||||
DateTime OccurredAtUtc,
|
||||
int? DurationMs,
|
||||
string? SourceInstanceId,
|
||||
Guid? ExecutionId = null,
|
||||
string? SourceScript = null,
|
||||
Guid? ParentExecutionId = null);
|
||||
|
||||
/// <summary>
|
||||
/// Coarse outcome of one cached-call delivery attempt, observed from inside
|
||||
/// the store-and-forward retry loop. The audit bridge maps this to the
|
||||
/// <c>ApiCallCached</c>/<c>DbWriteCached</c> Attempted row and, when terminal,
|
||||
/// the corresponding <c>CachedResolve</c> row.
|
||||
/// </summary>
|
||||
public enum CachedCallAttemptOutcome
|
||||
{
|
||||
/// <summary>Attempt delivered successfully — terminal Delivered state.</summary>
|
||||
Delivered,
|
||||
|
||||
/// <summary>Attempt failed transiently; another retry will follow.</summary>
|
||||
TransientFailure,
|
||||
|
||||
/// <summary>Attempt returned permanent failure — terminal Parked state (S&F semantics).</summary>
|
||||
PermanentFailure,
|
||||
|
||||
/// <summary>Retry budget exhausted — terminal Parked state.</summary>
|
||||
ParkedMaxRetries,
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Integration;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Site-side fan-out abstraction for cached-call lifecycle telemetry
|
||||
/// (Audit Log #23 / M3). One <see cref="CachedCallTelemetry"/> packet carries
|
||||
/// both an audit row and an operational <c>SiteCalls</c> upsert; the
|
||||
/// implementation routes the audit half through <see cref="IAuditWriter"/>
|
||||
/// and the operational half through the site-local tracking SQLite store.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Defined in Commons so the script runtime (and the StoreAndForward retry
|
||||
/// loop, Bundle E4) can take a dependency on the abstraction rather than on
|
||||
/// the concrete forwarder living inside <c>ZB.MOM.WW.ScadaBridge.AuditLog</c> — the
|
||||
/// existing dependency arrow runs from <c>SiteRuntime</c> to Commons, not to
|
||||
/// AuditLog.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Best-effort contract (alog.md §7):</b> implementations MUST swallow
|
||||
/// internal failures rather than propagating to the calling script.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public interface ICachedCallTelemetryForwarder
|
||||
{
|
||||
/// <summary>
|
||||
/// Fan one combined-telemetry packet out to the audit writer and the
|
||||
/// tracking store. Best-effort — failures on either half are logged and
|
||||
/// 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);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Central-only audit writer for the direct-write path (Notification Outbox dispatch, Inbound API).
|
||||
/// Distinct from <see cref="IAuditWriter"/> so DI binding can differ between site and central hosts.
|
||||
/// </summary>
|
||||
public interface ICentralAuditWriter
|
||||
{
|
||||
/// <summary>
|
||||
/// 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);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using System.Data.Common;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for database access from scripts.
|
||||
/// Implemented by ExternalSystemGateway, consumed by ScriptRuntimeContext.
|
||||
/// </summary>
|
||||
public interface IDatabaseGateway
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns an ADO.NET DbConnection (typically SqlConnection) from the named connection.
|
||||
/// 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);
|
||||
|
||||
/// <summary>
|
||||
/// Submits a SQL write to the store-and-forward engine for reliable delivery.
|
||||
/// </summary>
|
||||
/// <param name="trackedOperationId">
|
||||
/// Audit Log #23 (M3): caller-supplied tracking id used as the
|
||||
/// store-and-forward message id so the S&F retry loop can read it
|
||||
/// back via <c>StoreAndForwardMessage.Id</c> and emit per-attempt /
|
||||
/// terminal cached-write telemetry under the same id. Defaults to
|
||||
/// <c>null</c> — when omitted the S&F engine mints a fresh GUID and no
|
||||
/// M3 telemetry is correlated (pre-M3 caller behaviour).
|
||||
/// </param>
|
||||
/// <param name="executionId">
|
||||
/// Audit Log #23 (ExecutionId Task 4): the originating script execution's
|
||||
/// per-run correlation id. When the write is buffered on a transient
|
||||
/// failure this is threaded onto the S&F message so the retry-loop
|
||||
/// cached-write audit rows carry it. <c>null</c> when not threaded.
|
||||
/// </param>
|
||||
/// <param name="sourceScript">
|
||||
/// Audit Log #23 (ExecutionId Task 4): the originating script identifier,
|
||||
/// threaded onto the buffered S&F message alongside
|
||||
/// <paramref name="executionId"/>. <c>null</c> when not known.
|
||||
/// </param>
|
||||
/// <param name="parentExecutionId">
|
||||
/// Audit Log #23 (ParentExecutionId Task 6): the <c>ExecutionId</c> of the
|
||||
/// inbound-API request that spawned the originating script execution.
|
||||
/// When the write is buffered on a transient failure this is threaded onto
|
||||
/// the S&F message alongside <paramref name="executionId"/> so the
|
||||
/// 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,
|
||||
IReadOnlyDictionary<string, object?>? parameters = null,
|
||||
string? originInstanceName = null,
|
||||
CancellationToken cancellationToken = default,
|
||||
TrackedOperationId? trackedOperationId = null,
|
||||
Guid? executionId = null,
|
||||
string? sourceScript = null,
|
||||
Guid? parentExecutionId = null);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for invoking external system HTTP APIs.
|
||||
/// Implemented by ExternalSystemGateway, consumed by ScriptRuntimeContext.
|
||||
/// </summary>
|
||||
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,
|
||||
IReadOnlyDictionary<string, object?>? parameters = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Attempt immediate delivery; on transient failure, hand to S&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&F retry loop can read it
|
||||
/// back via <c>StoreAndForwardMessage.Id</c> and emit per-attempt /
|
||||
/// terminal cached-call telemetry under the same id. Defaults to
|
||||
/// <c>null</c> — when omitted the S&F engine mints a fresh GUID and no
|
||||
/// M3 telemetry is correlated (the legacy behaviour pre-M3 callers rely
|
||||
/// on).
|
||||
/// </param>
|
||||
/// <param name="executionId">
|
||||
/// Audit Log #23 (ExecutionId Task 4): the originating script execution's
|
||||
/// per-run correlation id. When the call is buffered on a transient
|
||||
/// failure this is threaded onto the S&F message so the retry-loop
|
||||
/// cached-call audit rows carry it. <c>null</c> when not threaded.
|
||||
/// </param>
|
||||
/// <param name="sourceScript">
|
||||
/// Audit Log #23 (ExecutionId Task 4): the originating script identifier,
|
||||
/// threaded onto the buffered S&F message alongside
|
||||
/// <paramref name="executionId"/>. <c>null</c> when not known.
|
||||
/// </param>
|
||||
/// <param name="parentExecutionId">
|
||||
/// Audit Log #23 (ParentExecutionId Task 6): the <c>ExecutionId</c> of the
|
||||
/// inbound-API request that spawned the originating script execution.
|
||||
/// When the call is buffered on a transient failure this is threaded onto
|
||||
/// the S&F message alongside <paramref name="executionId"/> so the
|
||||
/// 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,
|
||||
IReadOnlyDictionary<string, object?>? parameters = null,
|
||||
string? originInstanceName = null,
|
||||
CancellationToken cancellationToken = default,
|
||||
TrackedOperationId? trackedOperationId = null,
|
||||
Guid? executionId = null,
|
||||
string? sourceScript = null,
|
||||
Guid? parentExecutionId = null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of an external system call.
|
||||
/// </summary>
|
||||
public record ExternalCallResult(
|
||||
bool Success,
|
||||
string? ResponseJson,
|
||||
string? ErrorMessage,
|
||||
bool WasBuffered = false)
|
||||
{
|
||||
// Commons-021: thread-safe lazy parse — `Lazy<T>` with the default
|
||||
// `LazyThreadSafetyMode.ExecutionAndPublication` guarantees that two
|
||||
// concurrent readers see the same `DynamicJsonElement` instance, the
|
||||
// `JsonDocument.Parse` runs at most once, and the published value is
|
||||
// safe under .NET's memory model. The closure captures `ResponseJson`
|
||||
// by reference to the property — the record's positional property is
|
||||
// an init-only field set in the constructor, so the snapshot read at
|
||||
// first-access time is stable for the lifetime of the result.
|
||||
private readonly Lazy<dynamic?> _response = new(() =>
|
||||
string.IsNullOrEmpty(ResponseJson)
|
||||
? null
|
||||
: new DynamicJsonElement(System.Text.Json.JsonDocument.Parse(ResponseJson).RootElement));
|
||||
|
||||
/// <summary>
|
||||
/// Parsed response as a dynamic object. Returns null if ResponseJson is null or empty.
|
||||
/// Access properties directly: result.Response.result, result.Response.items[0].name, etc.
|
||||
/// Thread-safe: concurrent readers share a single parsed instance (Commons-021).
|
||||
/// </summary>
|
||||
public dynamic? Response => _response.Value;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves an instance unique name to its site identifier.
|
||||
/// Used by Inbound API's Route.To() to determine which site to route requests to.
|
||||
/// </summary>
|
||||
public interface IInstanceLocator
|
||||
{
|
||||
/// <summary>
|
||||
/// 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);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Surfaces the local node's semantic role-within-cluster name so downstream
|
||||
/// audit writers can stamp it on the SourceNode column.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Conventional values follow the pattern <c>node-a</c>/<c>node-b</c> on site
|
||||
/// nodes and <c>central-a</c>/<c>central-b</c> on central nodes. The value is
|
||||
/// a free-form operator-supplied label — there is no enforced format. When the
|
||||
/// configuration value is missing, empty, or whitespace, implementations
|
||||
/// return <c>null</c> so audit writers can persist NULL rather than an empty
|
||||
/// string.
|
||||
/// </remarks>
|
||||
public interface INodeIdentityProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// The configured semantic node name, trimmed of surrounding whitespace.
|
||||
/// <c>null</c> when unconfigured.
|
||||
/// </summary>
|
||||
string? NodeName { get; }
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
|
||||
// Commons-018: physically lives under Interfaces/Services/ to match the
|
||||
// established subfolder convention (REQ-COM-5b), but the namespace stays
|
||||
// `ZB.MOM.WW.ScadaBridge.Commons.Interfaces` to avoid a cascading update to 9+ consumer
|
||||
// files across ZB.MOM.WW.ScadaBridge.SiteRuntime, ZB.MOM.WW.ScadaBridge.AuditLog and ZB.MOM.WW.ScadaBridge.Host.
|
||||
// Adopting the canonical `ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services` namespace
|
||||
// can be picked up alongside any future Commons-wide namespace tidy-up.
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Site-local source of truth for cached-operation tracking
|
||||
/// (<c>ExternalSystem.CachedCall</c> / <c>Database.CachedWrite</c>) — alongside the
|
||||
/// Store-and-Forward buffer, this is the row that <c>Tracking.Status(id)</c>
|
||||
/// reads (Audit Log #23 / M3). One row per <see cref="TrackedOperationId"/>;
|
||||
/// terminal rows are purged after a configurable retention window
|
||||
/// (default 7 days).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The store is intentionally a thin write-API on top of SQLite — not a
|
||||
/// dispatcher. Status transitions follow
|
||||
/// <c>Submitted → Retrying → Delivered / Parked / Failed / Discarded</c>; rows
|
||||
/// in a terminal state never roll back. Implementations must:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><see cref="RecordEnqueueAsync"/> is insert-if-not-exists
|
||||
/// (caller-supplied id is the idempotency key — duplicate enqueues are no-ops).</description></item>
|
||||
/// <item><description><see cref="RecordAttemptAsync"/> only updates non-terminal rows.</description></item>
|
||||
/// <item><description><see cref="RecordTerminalAsync"/> only flips a non-terminal row to terminal.</description></item>
|
||||
/// <item><description><see cref="PurgeTerminalAsync"/> deletes terminal rows whose
|
||||
/// <c>TerminalAtUtc</c> is strictly older than the supplied threshold.</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public interface IOperationTrackingStore
|
||||
{
|
||||
/// <summary>
|
||||
/// Insert a new tracking row in <c>Submitted</c> state with <c>RetryCount = 0</c>.
|
||||
/// Idempotent — a duplicate id is silently ignored (the existing row is left
|
||||
/// 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,
|
||||
string? targetSummary,
|
||||
string? sourceInstanceId,
|
||||
string? sourceScript,
|
||||
string? sourceNode,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Advance an in-flight tracking row's status, retry counter, and most-
|
||||
/// recent error/HTTP-status. Terminal rows (<see cref="RecordTerminalAsync"/>
|
||||
/// 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,
|
||||
int retryCount,
|
||||
string? lastError,
|
||||
int? httpStatus,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Flip a non-terminal tracking row to terminal — sets
|
||||
/// <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,
|
||||
string? lastError,
|
||||
int? httpStatus,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// 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);
|
||||
|
||||
/// <summary>
|
||||
/// Delete terminal rows whose <c>TerminalAtUtc</c> is strictly older than
|
||||
/// <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);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Commons-018: physically lives under Interfaces/Services/ to match the
|
||||
// established subfolder convention (REQ-COM-5b), but the namespace stays
|
||||
// `ZB.MOM.WW.ScadaBridge.Commons.Interfaces` to avoid a cascading update to consumers
|
||||
// across ZB.MOM.WW.ScadaBridge.AuditLog and ZB.MOM.WW.ScadaBridge.ConfigurationDatabase. Adopting
|
||||
// the canonical `ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services` namespace can be
|
||||
// picked up alongside any future Commons-wide namespace tidy-up.
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Abstraction over the central AuditLog partition-function roll-forward
|
||||
/// operation. M6-T5 introduces a daily-cadence hosted service
|
||||
/// (<c>AuditLogPartitionMaintenanceService</c>) that calls
|
||||
/// <see cref="EnsureLookaheadAsync"/> to make sure
|
||||
/// <c>pf_AuditLog_Month</c> always has at least <c>LookaheadMonths</c> of
|
||||
/// future boundaries available — otherwise inserts past the highest
|
||||
/// boundary land in a single ever-growing tail partition that
|
||||
/// <c>SwitchOutPartitionAsync</c> cannot purge cleanly.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The interface lives in <c>ZB.MOM.WW.ScadaBridge.Commons</c> so the central hosted
|
||||
/// service in <c>ZB.MOM.WW.ScadaBridge.AuditLog</c> can depend on it without taking a
|
||||
/// reference on <c>ZB.MOM.WW.ScadaBridge.ConfigurationDatabase</c>; the EF-based
|
||||
/// implementation ships in
|
||||
/// <c>ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Maintenance.AuditLogPartitionMaintenance</c>
|
||||
/// and is registered by <c>AddConfigurationDatabase</c>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Both methods read <c>sys.partition_range_values</c> / mutate
|
||||
/// <c>pf_AuditLog_Month</c> via raw SQL — there is no EF model for a
|
||||
/// partition function. The interface deliberately exposes only the two
|
||||
/// operations the hosted service needs; it is not a general partition-DDL
|
||||
/// surface.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public interface IPartitionMaintenance
|
||||
{
|
||||
/// <summary>
|
||||
/// Splits new monthly boundaries on <c>pf_AuditLog_Month</c> so the
|
||||
/// function covers at least <paramref name="lookaheadMonths"/> future
|
||||
/// months relative to <see cref="DateTime.UtcNow"/>. Idempotent — a
|
||||
/// 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>
|
||||
/// Reads the current maximum boundary value from
|
||||
/// <c>sys.partition_range_values</c> for <c>pf_AuditLog_Month</c>.
|
||||
/// 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);
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Site-local audit-log queue surface consumed by the site
|
||||
/// <c>SiteAuditTelemetryActor</c> drain loop and the M6
|
||||
/// <c>SiteStreamGrpcServer.PullAuditEvents</c> reconciliation handler.
|
||||
/// Extracted from <c>SqliteAuditWriter</c> so both consumers can be
|
||||
/// unit-tested against a stub without touching SQLite; the
|
||||
/// <c>SqliteAuditWriter</c> production type implements this interface
|
||||
/// and DI wires the same singleton instance to every consumer.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Lives in Commons (rather than alongside <c>SqliteAuditWriter</c> in
|
||||
/// <c>ZB.MOM.WW.ScadaBridge.AuditLog</c>) because <c>ZB.MOM.WW.ScadaBridge.Communication</c> — which
|
||||
/// hosts the M6 gRPC pull handler — must depend on this interface and
|
||||
/// <c>ZB.MOM.WW.ScadaBridge.AuditLog</c> already depends on <c>ZB.MOM.WW.ScadaBridge.Communication</c>.
|
||||
/// Pulling the interface up to Commons breaks the would-be cycle while
|
||||
/// keeping the implementation in the AuditLog component.
|
||||
///
|
||||
/// Only the methods the drain and pull paths need are exposed — the
|
||||
/// hot-path <c>WriteAsync</c> stays on <see cref="IAuditWriter"/>
|
||||
/// (script-thread surface), separated by concern so each side can be
|
||||
/// mocked independently.
|
||||
/// </remarks>
|
||||
public interface ISiteAuditQueue
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns up to <paramref name="limit"/> rows currently in
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState.Pending"/>,
|
||||
/// oldest first. Idempotent — repeated calls before
|
||||
/// <see cref="MarkForwardedAsync"/> will yield the same rows again.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// AuditLog-001: cached-lifecycle <see cref="AuditEvent.Kind"/>s
|
||||
/// (<see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditKind.CachedSubmit"/>,
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditKind.ApiCallCached"/>,
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditKind.DbWriteCached"/>,
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditKind.CachedResolve"/>) are
|
||||
/// EXCLUDED from this result — they ride the combined-telemetry drain via
|
||||
/// <see cref="ReadPendingCachedTelemetryAsync"/> + the central
|
||||
/// <c>OnCachedTelemetryAsync</c> dual-write transaction. The audit-only
|
||||
/// drain handled by this method covers everything else (sync ApiCall /
|
||||
/// DbWrite, NotifySend, InboundRequest, etc.).
|
||||
/// </remarks>
|
||||
/// <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>
|
||||
/// AuditLog-001: returns up to <paramref name="limit"/> rows in
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState.Pending"/>
|
||||
/// whose <see cref="AuditEvent.Kind"/> belongs to the cached-call lifecycle
|
||||
/// vocabulary (<see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditKind.CachedSubmit"/>,
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditKind.ApiCallCached"/>,
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditKind.DbWriteCached"/>,
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditKind.CachedResolve"/>),
|
||||
/// oldest first. The site-side <c>SiteAuditTelemetryActor</c> drains these
|
||||
/// rows separately, joining each with the matching operational tracking row
|
||||
/// (<c>IOperationTrackingStore.GetStatusAsync</c>) before pushing the
|
||||
/// combined <c>CachedTelemetryBatch</c> via
|
||||
/// <c>ISiteStreamAuditClient.IngestCachedTelemetryAsync</c>. Idempotent —
|
||||
/// repeated calls before <see cref="MarkForwardedAsync"/> yield the same
|
||||
/// rows again.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The two-drain partition is the production wiring of the combined-telemetry
|
||||
/// transport specified in Component-AuditLog.md §"Cached Operations —
|
||||
/// Combined Telemetry": cached rows MUST flow with their matching
|
||||
/// <c>SiteCalls</c> upsert through one MS SQL transaction at central. The
|
||||
/// pre-AuditLog-001 implementation drained cached rows through the
|
||||
/// audit-only path, leaving the operational half unsent and the central
|
||||
/// dual-write handler unreachable. Returning them via this dedicated read
|
||||
/// surface lets the new drain join with the tracking store before push.
|
||||
/// </remarks>
|
||||
/// <param name="limit">Maximum number of rows to return.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task<IReadOnlyList<AuditEvent>> ReadPendingCachedTelemetryAsync(int limit, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Flips the supplied EventIds from
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState.Pending"/> to
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.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>
|
||||
/// M6 reconciliation-pull read surface: returns up to <paramref name="batchSize"/>
|
||||
/// rows whose <see cref="AuditEvent.OccurredAtUtc"/> >= <paramref name="sinceUtc"/>
|
||||
/// and whose <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState"/> is still
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState.Pending"/> or
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState.Forwarded"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Rows in the brief race window between site-Forwarded and central-ingest are
|
||||
/// intentionally included: the central reconciliation puller dedups on
|
||||
/// <see cref="AuditEvent.EventId"/>, so re-shipping is safe and avoids losing rows
|
||||
/// whose telemetry ack was acted on locally but never landed centrally. Ordering
|
||||
/// 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);
|
||||
|
||||
/// <summary>
|
||||
/// M6 reconciliation-pull commit surface: flips the supplied EventIds to
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState.Reconciled"/>,
|
||||
/// but ONLY for rows currently in
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState.Pending"/> or
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState.Forwarded"/>.
|
||||
/// Rows already in <see cref="ZB.MOM.WW.ScadaBridge.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>
|
||||
/// M6 Bundle E (T6) health-metric surface: returns a point-in-time snapshot
|
||||
/// of the site queue's pending count + oldest pending timestamp + on-disk
|
||||
/// SQLite file size. Surfaced on
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Messages.Health.SiteHealthReport"/> as
|
||||
/// <c>SiteAuditBacklog</c> by the periodic <c>SiteAuditBacklogReporter</c>
|
||||
/// hosted service so a stuck site→central drain is visible on the central
|
||||
/// health dashboard. Safe to call concurrently with hot-path writes —
|
||||
/// 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);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Transport;
|
||||
|
||||
/// <summary>
|
||||
/// Service the bundle importer sets to thread a BundleImportId through to the
|
||||
/// audit log entries emitted by the audited repository methods invoked during
|
||||
/// ApplyAsync. AuditService reads this and stamps every AuditLogEntry it writes.
|
||||
/// <para>
|
||||
/// Thread-safety / concurrency contract (Transport-009): the in-tree
|
||||
/// implementation backs <see cref="BundleImportId"/> with an
|
||||
/// <see cref="System.Threading.AsyncLocal{T}"/> so each logical asynchronous
|
||||
/// call chain — every distinct <c>BundleImporter.ApplyAsync</c> invocation —
|
||||
/// observes its own value, even when two imports share the same DI scope (e.g.
|
||||
/// awaited via <c>Task.WhenAll</c> on a single Blazor circuit, or driven by a
|
||||
/// misconfigured singleton registration). The value flows through every
|
||||
/// <c>await</c> naturally; no cross-contamination of BundleImportIds between
|
||||
/// concurrent imports.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Alternative implementations (e.g. ambient-context-free explicit-parameter
|
||||
/// threading) MUST preserve the same per-call-context isolation guarantee.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public interface IAuditCorrelationContext
|
||||
{
|
||||
/// <summary>Gets or sets the bundle import id used to correlate audit rows written during a bundle apply operation. Implementations MUST isolate the value per-logical-call-context to prevent concurrent imports from cross-contaminating audit rows.</summary>
|
||||
Guid? BundleImportId { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Transport;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.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,
|
||||
string sourceEnvironment,
|
||||
string? passphrase,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Transport;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.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,
|
||||
string user,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Transport;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.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();
|
||||
|
||||
/// <summary>
|
||||
/// T-003: returns the current unlock-failure count for a bundle keyed by its
|
||||
/// content hash. The counter is server-owned so a second tab / CLI caller
|
||||
/// cannot side-step the lockout by re-uploading the same bytes.
|
||||
/// </summary>
|
||||
/// <param name="bundleContentHash">SHA-256 hex from <c>BundleManifest.ContentHash</c>.</param>
|
||||
/// <returns>Number of recorded failures for this bundle (0 if none, or if any record has expired).</returns>
|
||||
int GetUnlockFailureCount(string bundleContentHash);
|
||||
|
||||
/// <summary>
|
||||
/// T-003: atomically increments the unlock-failure counter for a bundle and
|
||||
/// returns the new count. Tracking is scoped by content hash so retries
|
||||
/// against identical bundle bytes are throttled regardless of client.
|
||||
/// </summary>
|
||||
/// <param name="bundleContentHash">SHA-256 hex from <c>BundleManifest.ContentHash</c>.</param>
|
||||
int IncrementUnlockFailureCount(string bundleContentHash);
|
||||
|
||||
/// <summary>
|
||||
/// T-003: clears the unlock-failure counter for a bundle (called on a
|
||||
/// successful unlock so a legitimate operator who eventually types the
|
||||
/// right passphrase is not penalised for earlier typos).
|
||||
/// </summary>
|
||||
/// <param name="bundleContentHash">SHA-256 hex from <c>BundleManifest.ContentHash</c>.</param>
|
||||
void ClearUnlockFailures(string bundleContentHash);
|
||||
}
|
||||
Reference in New Issue
Block a user