Files
lmxopcua/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/IRawTreeService.cs
T

398 lines
25 KiB
C#

using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
/// <summary>
/// Detached, editable projection of a raw <c>Tag</c> — the read side of tag editing. WP1 exposes this
/// loader; WP4 owns the create/update authoring surface and should align its input model with these
/// fields (identity is the RawPath, formed from the tag's device + optional group + name).
/// </summary>
/// <param name="TagId">The tag's stable logical id.</param>
/// <param name="DeviceId">The owning device's logical id.</param>
/// <param name="TagGroupId">The containing tag group's logical id, or null when directly under the device.</param>
/// <param name="Name">The tag name (a RawPath segment).</param>
/// <param name="DataType">The OPC UA built-in type name.</param>
/// <param name="AccessLevel">The tag's access-level baseline.</param>
/// <param name="WriteIdempotent">Whether writes to this tag are retry-eligible.</param>
/// <param name="PollGroupId">The optional poll-group id.</param>
/// <param name="TagConfig">The schemaless per-driver TagConfig JSON blob.</param>
/// <param name="RowVersion">The optimistic-concurrency token.</param>
public readonly record struct RawTagEditDto(
string TagId,
string DeviceId,
string? TagGroupId,
string Name,
string DataType,
TagAccessLevel AccessLevel,
bool WriteIdempotent,
string? PollGroupId,
string TagConfig,
byte[] RowVersion);
/// <summary>
/// Detached, editable projection of a <c>DriverInstance</c> — the read side of the Wave-B (WP3)
/// "Configure driver" modal. <c>DriverType</c> is included so the modal can dispatch to the correct
/// typed driver-config editor; it is immutable, so <see cref="IRawTreeService.UpdateDriverAsync"/> does
/// not change it.
/// </summary>
/// <param name="DriverInstanceId">The driver instance's stable logical id.</param>
/// <param name="Name">The driver instance name (a RawPath segment).</param>
/// <param name="DriverType">The immutable driver-type string (see <c>DriverTypeNames</c>).</param>
/// <param name="DriverConfig">The schemaless per-driver-type <c>DriverConfig</c> JSON blob.</param>
/// <param name="ResilienceConfig">Optional per-instance resilience-pipeline overrides JSON, or null.</param>
/// <param name="RowVersion">The optimistic-concurrency token.</param>
public sealed record RawDriverEditDto(
string DriverInstanceId,
string Name,
string DriverType,
string DriverConfig,
string? ResilienceConfig,
byte[] RowVersion);
/// <summary>
/// Detached, editable projection of a <c>Device</c> — the read side of the Wave-B (WP3) "Configure
/// device" modal. <c>DriverType</c> is joined in from the parent <c>DriverInstance</c> so the modal can
/// dispatch to the correct typed device-config editor (endpoint/connection settings now live in
/// <c>DeviceConfig</c>).
/// </summary>
/// <param name="DeviceId">The device's stable logical id.</param>
/// <param name="DriverInstanceId">The owning driver instance's logical id.</param>
/// <param name="Name">The device name (a RawPath segment).</param>
/// <param name="DeviceConfig">The schemaless per-driver-type <c>DeviceConfig</c> JSON blob (host/endpoint + per-device settings).</param>
/// <param name="Enabled">Whether the device is enabled.</param>
/// <param name="DriverType">The parent driver's type string, joined in for editor dispatch.</param>
/// <param name="RowVersion">The optimistic-concurrency token.</param>
public sealed record RawDeviceEditDto(
string DeviceId,
string DriverInstanceId,
string Name,
string DeviceConfig,
bool Enabled,
string DriverType,
byte[] RowVersion);
/// <summary>
/// Loads and mutates the v3 Raw project tree (<c>/raw</c>) — the cluster-rooted
/// Enterprise → Cluster → Folder → Driver → Device → TagGroup → Tag hierarchy — from the config
/// database. The peer of <see cref="IUnsTreeService"/> for the Raw subtree.
/// <para>
/// <b>Contract split (Batch 2 Wave A):</b> this file's committed surface is the <b>read</b> contract
/// the <c>RawTree</c> component consumes — <see cref="LoadRootsAsync"/> (eager Enterprise→Cluster roots)
/// and <see cref="LoadChildrenAsync"/> (one lazy level per expand). B2-WP1 (<c>RawTreeService</c>)
/// <b>extends this interface</b> with the mutation surface — create/rename/delete per node type,
/// move-into-folder, and the per-node edit projections — enforcing the integrity rules with
/// user-readable errors (a delete blocked by a <c>UnsTagReference</c> names the referencing equipment).
/// Rename methods return <see cref="RawRenameResult"/> so the UI can show non-blocking warnings.
/// Mutations reuse <see cref="UnsMutationResult"/>. WP2 must not add to this interface.
/// </para>
/// </summary>
public interface IRawTreeService
{
/// <summary>
/// Loads the eager top of the Raw tree: Enterprise → Cluster roots, each cluster marked
/// <see cref="RawNode.HasLazyChildren"/> so its folders + drivers load on first expand. Empty
/// clusters are retained so they stay visible and authorable. The returned nodes are detached
/// view-models, safe to hold and mutate UI state on after the underlying context is disposed.
/// </summary>
/// <param name="ct">A token to cancel the load.</param>
/// <returns>The enterprise roots, populated down to (but not through) their clusters.</returns>
Task<IReadOnlyList<RawNode>> LoadRootsAsync(CancellationToken ct = default);
/// <summary>
/// Loads the direct children of one container node — the next level down, keyed off
/// <paramref name="parent"/>'s <see cref="RawNode.Kind"/> and <see cref="RawNode.EntityId"/>:
/// a Cluster yields its root Folders + Drivers; a Folder yields sub-Folders + Drivers; a Driver
/// yields its Devices; a Device yields its root TagGroups + Tags; a TagGroup yields sub-TagGroups
/// + Tags. Tags are leaves and yield nothing. Ordering is deterministic and ordinal. Reads
/// untracked; returns detached nodes. Per-device tag counts can be large — the implementation may
/// page, but the committed shape returns the level's nodes (paging knobs, if any, are WP1's to add
/// on the concrete type without changing this signature's meaning for the tree).
/// </summary>
/// <param name="parent">The container node whose direct children to load.</param>
/// <param name="ct">A token to cancel the load.</param>
/// <returns>The parent's direct child nodes; empty for a leaf or an empty container.</returns>
Task<IReadOnlyList<RawNode>> LoadChildrenAsync(RawNode parent, CancellationToken ct = default);
// --- Folder mutations ---
/// <summary>
/// Creates a new <c>RawFolder</c> under a cluster (<paramref name="parentFolderId"/> null) or under
/// another folder. The name is validated as a RawPath segment and must be unique among its siblings
/// (case-sensitive ordinal). On success the result's <c>CreatedId</c> carries the new folder's
/// logical id.
/// </summary>
/// <param name="clusterId">The owning cluster id.</param>
/// <param name="parentFolderId">The parent folder's logical id, or null for a cluster-root folder.</param>
/// <param name="name">The folder name (a RawPath segment).</param>
/// <param name="ct">A token to cancel the operation.</param>
/// <returns>The create outcome; <c>CreatedId</c> is the new <c>RawFolderId</c> on success.</returns>
Task<UnsMutationResult> CreateFolderAsync(string clusterId, string? parentFolderId, string name, CancellationToken ct = default);
/// <summary>
/// Renames a <c>RawFolder</c>. Validates the new name as a RawPath segment and sibling-unique.
/// Because a folder rename changes the RawPath of every tag beneath it, the returned
/// <see cref="RawRenameResult"/> carries advisory warnings for historized and equipment-referenced
/// tags in the folder's subtree.
/// </summary>
/// <param name="rawFolderId">The folder's logical id.</param>
/// <param name="newName">The new folder name.</param>
/// <param name="rowVersion">The optimistic-concurrency token echoed from the loaded node.</param>
/// <param name="ct">A token to cancel the operation.</param>
/// <returns>The rename outcome with any downstream-impact warnings.</returns>
Task<RawRenameResult> RenameFolderAsync(string rawFolderId, string newName, byte[] rowVersion, CancellationToken ct = default);
/// <summary>
/// Deletes an empty <c>RawFolder</c>. Blocked (with the blocker named) while it still contains child
/// folders or driver instances.
/// </summary>
/// <param name="rawFolderId">The folder's logical id.</param>
/// <param name="rowVersion">The optimistic-concurrency token echoed from the loaded node.</param>
/// <param name="ct">A token to cancel the operation.</param>
/// <returns>The delete outcome.</returns>
Task<UnsMutationResult> DeleteFolderAsync(string rawFolderId, byte[] rowVersion, CancellationToken ct = default);
/// <summary>
/// Moves a driver instance into a folder (<paramref name="targetFolderId"/>) or to the cluster root
/// (null target). The target folder must be in the driver's cluster and the driver's name must be
/// unique among the target's driver siblings.
/// </summary>
/// <param name="driverInstanceId">The driver instance's logical id.</param>
/// <param name="targetFolderId">The destination folder's logical id, or null for the cluster root.</param>
/// <param name="rowVersion">The optimistic-concurrency token echoed from the loaded node.</param>
/// <param name="ct">A token to cancel the operation.</param>
/// <returns>The move outcome.</returns>
Task<UnsMutationResult> MoveDriverToFolderAsync(string driverInstanceId, string? targetFolderId, byte[] rowVersion, CancellationToken ct = default);
// --- Driver mutations ---
/// <summary>
/// Creates a minimal driver instance and its auto-created default <c>Device</c> so the tree has a
/// child to show. Full typed driver/device config authoring is Wave B (WP3) — this create takes the
/// driver config JSON verbatim and seeds an empty (<c>{}</c>) default device. The driver name is a
/// RawPath segment, unique among its cluster/folder siblings.
/// </summary>
/// <param name="clusterId">The owning cluster id.</param>
/// <param name="folderId">The containing folder's logical id, or null for a cluster-root driver.</param>
/// <param name="name">The driver instance name (a RawPath segment).</param>
/// <param name="driverType">The driver-type string (see <c>DriverTypeNames</c>).</param>
/// <param name="driverConfigJson">The driver config JSON blob (opaque here; validated in Wave B).</param>
/// <param name="ct">A token to cancel the operation.</param>
/// <returns>The create outcome; <c>CreatedId</c> is the new <c>DriverInstanceId</c> on success.</returns>
Task<UnsMutationResult> CreateDriverAsync(string clusterId, string? folderId, string name, string driverType, string driverConfigJson, CancellationToken ct = default);
/// <summary>
/// Renames a driver instance. Validates the new name as a sibling-unique RawPath segment and returns
/// downstream-impact warnings for historized/equipment-referenced tags beneath the driver.
/// </summary>
/// <param name="driverInstanceId">The driver instance's logical id.</param>
/// <param name="newName">The new driver name.</param>
/// <param name="rowVersion">The optimistic-concurrency token echoed from the loaded node.</param>
/// <param name="ct">A token to cancel the operation.</param>
/// <returns>The rename outcome with any downstream-impact warnings.</returns>
Task<RawRenameResult> RenameDriverAsync(string driverInstanceId, string newName, byte[] rowVersion, CancellationToken ct = default);
/// <summary>Enables or disables a driver instance (last-write-wins on <paramref name="rowVersion"/>).</summary>
/// <param name="driverInstanceId">The driver instance's logical id.</param>
/// <param name="enabled">The new enabled state.</param>
/// <param name="rowVersion">The optimistic-concurrency token echoed from the loaded node.</param>
/// <param name="ct">A token to cancel the operation.</param>
/// <returns>The update outcome.</returns>
Task<UnsMutationResult> SetDriverEnabledAsync(string driverInstanceId, bool enabled, byte[] rowVersion, CancellationToken ct = default);
/// <summary>
/// Deletes a driver instance, cascading its auto-created empty devices. Blocked (with the blocking
/// device named) when any of its devices still contains tag groups or tags — remove those first.
/// </summary>
/// <param name="driverInstanceId">The driver instance's logical id.</param>
/// <param name="rowVersion">The optimistic-concurrency token echoed from the loaded node.</param>
/// <param name="ct">A token to cancel the operation.</param>
/// <returns>The delete outcome.</returns>
Task<UnsMutationResult> DeleteDriverAsync(string driverInstanceId, byte[] rowVersion, CancellationToken ct = default);
// --- Device mutations ---
/// <summary>
/// Creates a minimal <c>Device</c> under a driver. Full typed device-config authoring is Wave B
/// (WP3) — this create takes the device config JSON verbatim. The device name must be unique among
/// the driver's devices.
/// </summary>
/// <param name="driverInstanceId">The owning driver instance's logical id.</param>
/// <param name="name">The device name (a RawPath segment).</param>
/// <param name="deviceConfigJson">The device config JSON blob (opaque here; validated in Wave B).</param>
/// <param name="ct">A token to cancel the operation.</param>
/// <returns>The create outcome; <c>CreatedId</c> is the new <c>DeviceId</c> on success.</returns>
Task<UnsMutationResult> CreateDeviceAsync(string driverInstanceId, string name, string deviceConfigJson, CancellationToken ct = default);
/// <summary>
/// Renames a <c>Device</c>. Validates the new name as a driver-unique RawPath segment and returns
/// downstream-impact warnings for historized/equipment-referenced tags beneath the device.
/// </summary>
/// <param name="deviceId">The device's logical id.</param>
/// <param name="newName">The new device name.</param>
/// <param name="rowVersion">The optimistic-concurrency token echoed from the loaded node.</param>
/// <param name="ct">A token to cancel the operation.</param>
/// <returns>The rename outcome with any downstream-impact warnings.</returns>
Task<RawRenameResult> RenameDeviceAsync(string deviceId, string newName, byte[] rowVersion, CancellationToken ct = default);
/// <summary>Deletes a <c>Device</c>. Blocked (with the blocker named) while it holds tag groups or tags.</summary>
/// <param name="deviceId">The device's logical id.</param>
/// <param name="rowVersion">The optimistic-concurrency token echoed from the loaded node.</param>
/// <param name="ct">A token to cancel the operation.</param>
/// <returns>The delete outcome.</returns>
Task<UnsMutationResult> DeleteDeviceAsync(string deviceId, byte[] rowVersion, CancellationToken ct = default);
// --- TagGroup mutations ---
/// <summary>
/// Creates a new <c>TagGroup</c> under a device (<paramref name="parentGroupId"/> null) or under
/// another group. The name is validated as a RawPath segment, unique among its siblings.
/// </summary>
/// <param name="deviceId">The owning device's logical id.</param>
/// <param name="parentGroupId">The parent group's logical id, or null for a device-root group.</param>
/// <param name="name">The group name (a RawPath segment).</param>
/// <param name="ct">A token to cancel the operation.</param>
/// <returns>The create outcome; <c>CreatedId</c> is the new <c>TagGroupId</c> on success.</returns>
Task<UnsMutationResult> CreateTagGroupAsync(string deviceId, string? parentGroupId, string name, CancellationToken ct = default);
/// <summary>
/// Renames a <c>TagGroup</c>. Validates the new name as a sibling-unique RawPath segment and returns
/// downstream-impact warnings for historized/equipment-referenced tags beneath the group.
/// </summary>
/// <param name="tagGroupId">The group's logical id.</param>
/// <param name="newName">The new group name.</param>
/// <param name="rowVersion">The optimistic-concurrency token echoed from the loaded node.</param>
/// <param name="ct">A token to cancel the operation.</param>
/// <returns>The rename outcome with any downstream-impact warnings.</returns>
Task<RawRenameResult> RenameTagGroupAsync(string tagGroupId, string newName, byte[] rowVersion, CancellationToken ct = default);
/// <summary>Deletes a <c>TagGroup</c>. Blocked (with the blocker named) while it holds child groups or tags.</summary>
/// <param name="tagGroupId">The group's logical id.</param>
/// <param name="rowVersion">The optimistic-concurrency token echoed from the loaded node.</param>
/// <param name="ct">A token to cancel the operation.</param>
/// <returns>The delete outcome.</returns>
Task<UnsMutationResult> DeleteTagGroupAsync(string tagGroupId, byte[] rowVersion, CancellationToken ct = default);
// --- Tag mutations / projections (create + update are WP4) ---
/// <summary>
/// Deletes a raw <c>Tag</c>. Blocked while any <c>UnsTagReference</c> points at it; the failure
/// message names the referencing equipment(s) so the operator can remove the reference(s) first.
/// </summary>
/// <param name="tagId">The tag's logical id.</param>
/// <param name="rowVersion">The optimistic-concurrency token echoed from the loaded node.</param>
/// <param name="ct">A token to cancel the operation.</param>
/// <returns>The delete outcome.</returns>
Task<UnsMutationResult> DeleteTagAsync(string tagId, byte[] rowVersion, CancellationToken ct = default);
/// <summary>
/// Loads a raw tag's editable fields as a detached projection. WP1 provides the read-only projection;
/// the create/update authoring surface is WP4 (which should align its input shape with
/// <see cref="RawTagEditDto"/>).
/// </summary>
/// <param name="tagId">The tag's logical id.</param>
/// <param name="ct">A token to cancel the load.</param>
/// <returns>The tag's editable projection, or null when the tag does not exist.</returns>
Task<RawTagEditDto?> LoadTagForEditAsync(string tagId, CancellationToken ct = default);
/// <summary>
/// Creates a raw <c>Tag</c> under a device (<paramref name="tagGroupId"/> null) or a tag group. The
/// name is validated as a RawPath segment and must be unique among its siblings on the same
/// <c>(DeviceId, TagGroupId)</c>; the <see cref="RawTagInput.TagConfig"/> is validated for JSON
/// well-formedness. On success the result's <c>CreatedId</c> carries the new tag's logical id.
/// </summary>
/// <param name="deviceId">The owning device's logical id.</param>
/// <param name="tagGroupId">The containing tag group's logical id, or null for a device-root tag.</param>
/// <param name="input">The tag's editable fields (identity is the RawPath — no TagId here).</param>
/// <param name="ct">A token to cancel the operation.</param>
/// <returns>The create outcome; <c>CreatedId</c> is the new <c>TagId</c> on success.</returns>
Task<UnsMutationResult> CreateTagAsync(string deviceId, string? tagGroupId, RawTagInput input, CancellationToken ct = default);
/// <summary>
/// Updates a raw <c>Tag</c>'s editable fields. The new name is re-validated as a RawPath segment and
/// sibling-unique on the tag's existing <c>(DeviceId, TagGroupId)</c> excluding itself; the
/// <see cref="RawTagInput.TagConfig"/> is validated for JSON well-formedness. Last-write-wins on
/// <paramref name="rowVersion"/>. The tag's <c>DeviceId</c>/<c>TagGroupId</c> (its identity location)
/// are preserved — moving a tag is not this method's job.
/// </summary>
/// <param name="tagId">The tag's logical id.</param>
/// <param name="input">The tag's edited fields.</param>
/// <param name="rowVersion">The optimistic-concurrency token echoed from the loaded tag.</param>
/// <param name="ct">A token to cancel the operation.</param>
/// <returns>The update outcome.</returns>
Task<UnsMutationResult> UpdateTagAsync(string tagId, RawTagInput input, byte[] rowVersion, CancellationToken ct = default);
/// <summary>
/// Commits a batch of raw tags under a device — the WP5 CSV-import path. Each row's
/// <c>TagGroupPath</c> is a <c>/</c>-separated tag-group path under the device whose segments are
/// auto-created as nested <c>TagGroup</c>s (reusing existing ones). Per-row validation errors (bad
/// name segment, malformed <c>TagConfig</c> JSON, a duplicate name within the device+group) are
/// collected, not thrown. The import is <b>all-or-nothing</b>: if any row errors, nothing is inserted
/// and the errors are returned; otherwise all tags (and any newly-needed groups) are committed in one
/// <c>SaveChanges</c>.
/// </summary>
/// <param name="deviceId">The target device's logical id.</param>
/// <param name="rows">The rows to import.</param>
/// <param name="ct">A token to cancel the operation.</param>
/// <returns>The import outcome: the inserted count on success, or the per-row errors with a 0 count.</returns>
Task<RawTagImportResult> ImportTagsAsync(string deviceId, IReadOnlyList<RawTagImportRow> rows, CancellationToken ct = default);
/// <summary>
/// Loads a driver instance's editable fields as a detached projection for the WP3 "Configure driver"
/// modal. <c>DriverType</c> is included for typed-editor dispatch (it is immutable).
/// </summary>
/// <param name="driverInstanceId">The driver instance's logical id.</param>
/// <param name="ct">A token to cancel the load.</param>
/// <returns>The driver's editable projection, or null when it does not exist.</returns>
Task<RawDriverEditDto?> LoadDriverForEditAsync(string driverInstanceId, CancellationToken ct = default);
/// <summary>
/// Updates a driver instance's name + config from the WP3 "Configure driver" modal. The name is
/// validated as a sibling-unique RawPath segment (same scope as <see cref="RenameDriverAsync"/>);
/// last-write-wins on <paramref name="rowVersion"/>. <c>DriverType</c> is immutable and is not changed.
/// </summary>
/// <param name="driverInstanceId">The driver instance's logical id.</param>
/// <param name="name">The (possibly changed) driver name.</param>
/// <param name="driverConfigJson">The new <c>DriverConfig</c> JSON blob.</param>
/// <param name="resilienceConfig">The new resilience-config JSON, or null/blank to clear.</param>
/// <param name="rowVersion">The optimistic-concurrency token echoed from the loaded projection.</param>
/// <param name="ct">A token to cancel the operation.</param>
/// <returns>The update outcome.</returns>
Task<UnsMutationResult> UpdateDriverAsync(string driverInstanceId, string name, string driverConfigJson, string? resilienceConfig, byte[] rowVersion, CancellationToken ct = default);
/// <summary>
/// Loads a device's editable fields as a detached projection for the WP3 "Configure device" modal.
/// <c>DriverType</c> is joined in from the parent driver so the modal can dispatch to the correct typed
/// device-config editor.
/// </summary>
/// <param name="deviceId">The device's logical id.</param>
/// <param name="ct">A token to cancel the load.</param>
/// <returns>The device's editable projection, or null when it does not exist.</returns>
Task<RawDeviceEditDto?> LoadDeviceForEditAsync(string deviceId, CancellationToken ct = default);
/// <summary>
/// Updates a device's name + config + enabled state from the WP3 "Configure device" modal. The name
/// is validated as a RawPath segment unique among the driver's devices; last-write-wins on
/// <paramref name="rowVersion"/>.
/// </summary>
/// <param name="deviceId">The device's logical id.</param>
/// <param name="name">The (possibly changed) device name.</param>
/// <param name="deviceConfigJson">The new <c>DeviceConfig</c> JSON blob (endpoint + per-device settings).</param>
/// <param name="enabled">The new enabled state.</param>
/// <param name="rowVersion">The optimistic-concurrency token echoed from the loaded projection.</param>
/// <param name="ct">A token to cancel the operation.</param>
/// <returns>The update outcome.</returns>
Task<UnsMutationResult> UpdateDeviceAsync(string deviceId, string name, string deviceConfigJson, bool enabled, byte[] rowVersion, CancellationToken ct = default);
/// <summary>
/// Builds the single merged config blob the driver-probe (test-connect) path binds from for one device:
/// the parent driver's <c>DriverConfig</c> folded together with this device's <c>DeviceConfig</c> (via
/// <c>DriverDeviceConfigMerger</c>, endpoint keys merging up from <c>DeviceConfig</c>). Used by the
/// "Test connect" button inside the WP3 driver/device modals now that the endpoint lives in
/// <c>DeviceConfig</c>.
/// </summary>
/// <param name="deviceId">The device to probe.</param>
/// <param name="ct">A token to cancel the load.</param>
/// <returns>The parent driver's type + the merged config JSON, or null when the device/driver cannot be resolved.</returns>
Task<(string DriverType, string MergedConfigJson)?> LoadMergedProbeConfigAsync(string deviceId, CancellationToken ct = default);
}