Compare commits
2 Commits
phase-7-st
...
phase-7-st
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be1003c53e | ||
| dccaa11510 |
38
src/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Script.cs
Normal file
38
src/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Script.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Per Phase 7 plan decision #8 — user-authored C# script source, referenced by
|
||||||
|
/// <see cref="VirtualTag"/> and <see cref="ScriptedAlarm"/>. One row per script,
|
||||||
|
/// per generation. <c>SourceHash</c> is the compile-cache key.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// Scripts are generation-scoped: a draft's edit creates a new row in the draft
|
||||||
|
/// generation, the old row stays frozen in the published generation. Shape mirrors
|
||||||
|
/// the other generation-scoped entities (Equipment, Tag, etc.) — <c>ScriptId</c> is
|
||||||
|
/// the stable logical id that carries across generations; <c>ScriptRowId</c> is the
|
||||||
|
/// row identity.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public sealed class Script
|
||||||
|
{
|
||||||
|
public Guid ScriptRowId { get; set; }
|
||||||
|
public long GenerationId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Stable logical id. Carries across generations.</summary>
|
||||||
|
public required string ScriptId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Operator-friendly name for log filtering + Admin UI list view.</summary>
|
||||||
|
public required string Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Raw C# source. Size bounded by the DB column (nvarchar(max)).</summary>
|
||||||
|
public required string SourceCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>SHA-256 of <see cref="SourceCode"/> — compile-cache key for Phase 7 Stream A's <c>CompiledScriptCache</c>.</summary>
|
||||||
|
public required string SourceHash { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Language — always "CSharp" today; placeholder for future engines (Python/Lua).</summary>
|
||||||
|
public string Language { get; set; } = "CSharp";
|
||||||
|
|
||||||
|
public ConfigGeneration? Generation { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Per Phase 7 plan decisions #5, #13, #15 — a scripted OPC UA Part 9 alarm whose
|
||||||
|
/// condition is the predicate <see cref="Script"/> referenced by
|
||||||
|
/// <see cref="PredicateScriptId"/>. Materialized by <c>Core.ScriptedAlarms</c> as a
|
||||||
|
/// concrete <c>AlarmConditionType</c> subtype per <see cref="AlarmType"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// Message tokens (<c>{TagPath}</c>) resolved at emission time per plan decision #13.
|
||||||
|
/// <see cref="HistorizeToAveva"/> (plan decision #15) gates whether transitions
|
||||||
|
/// route through the Core.AlarmHistorian SQLite queue + Galaxy.Host to the Aveva
|
||||||
|
/// Historian alarm schema.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public sealed class ScriptedAlarm
|
||||||
|
{
|
||||||
|
public Guid ScriptedAlarmRowId { get; set; }
|
||||||
|
public long GenerationId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Stable logical id — drives <c>AlarmConditionType.ConditionName</c>.</summary>
|
||||||
|
public required string ScriptedAlarmId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Logical FK to <see cref="Equipment.EquipmentId"/> — owner of this alarm.</summary>
|
||||||
|
public required string EquipmentId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Operator-facing alarm name.</summary>
|
||||||
|
public required string Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Concrete Part 9 type — "AlarmCondition" / "LimitAlarm" / "OffNormalAlarm" / "DiscreteAlarm".</summary>
|
||||||
|
public required string AlarmType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Numeric severity 1..1000 per OPC UA Part 9 (usual bands: 1-250 Low, 251-500 Medium, 501-750 High, 751-1000 Critical).</summary>
|
||||||
|
public int Severity { get; set; } = 500;
|
||||||
|
|
||||||
|
/// <summary>Template with <c>{TagPath}</c> tokens resolved at emission time.</summary>
|
||||||
|
public required string MessageTemplate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Logical FK to <see cref="Script.ScriptId"/> — predicate script returning <c>bool</c>.</summary>
|
||||||
|
public required string PredicateScriptId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Plan decision #15 — when true, transitions route through the SQLite store-and-forward
|
||||||
|
/// queue to the Aveva Historian. Defaults on for scripted alarms because they are the
|
||||||
|
/// primary motivation for the historian sink; operator can disable per alarm.
|
||||||
|
/// </summary>
|
||||||
|
public bool HistorizeToAveva { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// OPC UA Part 9 <c>Retain</c> flag — whether the alarm keeps active-state between
|
||||||
|
/// sessions. Most plant alarms are retained; one-shot event-style alarms are not.
|
||||||
|
/// </summary>
|
||||||
|
public bool Retain { get; set; } = true;
|
||||||
|
|
||||||
|
public bool Enabled { get; set; } = true;
|
||||||
|
|
||||||
|
public ConfigGeneration? Generation { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Per Phase 7 plan decision #14 — persistent runtime state for each scripted alarm.
|
||||||
|
/// Survives process restart so operators don't re-ack and ack history survives for
|
||||||
|
/// GxP / 21 CFR Part 11 compliance. Keyed on <c>ScriptedAlarmId</c> logically (not
|
||||||
|
/// per-generation) because ack state follows the alarm's stable identity across
|
||||||
|
/// generations — a Modified alarm keeps its ack history.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// <c>ActiveState</c> is deliberately NOT persisted — it rederives from the current
|
||||||
|
/// predicate evaluation on startup. Only operator-supplied state (<see cref="AckedState"/>,
|
||||||
|
/// <see cref="ConfirmedState"/>, <see cref="ShelvingState"/>) + audit trail persist.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// <see cref="CommentsJson"/> is an append-only JSON array of <c>{user, utc, text}</c>
|
||||||
|
/// tuples — one per operator comment. Core.ScriptedAlarms' <c>AlarmConditionState.Comments</c>
|
||||||
|
/// serializes directly into this column.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public sealed class ScriptedAlarmState
|
||||||
|
{
|
||||||
|
/// <summary>Logical FK — matches <see cref="ScriptedAlarm.ScriptedAlarmId"/>. One row per alarm identity.</summary>
|
||||||
|
public required string ScriptedAlarmId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Enabled/Disabled. Persists across restart per plan decision #14.</summary>
|
||||||
|
public required string EnabledState { get; set; } = "Enabled";
|
||||||
|
|
||||||
|
/// <summary>Unacknowledged / Acknowledged.</summary>
|
||||||
|
public required string AckedState { get; set; } = "Unacknowledged";
|
||||||
|
|
||||||
|
/// <summary>Unconfirmed / Confirmed.</summary>
|
||||||
|
public required string ConfirmedState { get; set; } = "Unconfirmed";
|
||||||
|
|
||||||
|
/// <summary>Unshelved / OneShotShelved / TimedShelved.</summary>
|
||||||
|
public required string ShelvingState { get; set; } = "Unshelved";
|
||||||
|
|
||||||
|
/// <summary>When a TimedShelve expires — null if not shelved or OneShotShelved.</summary>
|
||||||
|
public DateTime? ShelvingExpiresUtc { get; set; }
|
||||||
|
|
||||||
|
/// <summary>User who last acknowledged. Null if never acked.</summary>
|
||||||
|
public string? LastAckUser { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Operator-supplied ack comment. Null if no comment or never acked.</summary>
|
||||||
|
public string? LastAckComment { get; set; }
|
||||||
|
|
||||||
|
public DateTime? LastAckUtc { get; set; }
|
||||||
|
|
||||||
|
/// <summary>User who last confirmed.</summary>
|
||||||
|
public string? LastConfirmUser { get; set; }
|
||||||
|
|
||||||
|
public string? LastConfirmComment { get; set; }
|
||||||
|
|
||||||
|
public DateTime? LastConfirmUtc { get; set; }
|
||||||
|
|
||||||
|
/// <summary>JSON array of operator comments, append-only (GxP audit).</summary>
|
||||||
|
public string CommentsJson { get; set; } = "[]";
|
||||||
|
|
||||||
|
/// <summary>Row write timestamp — tracks last state change.</summary>
|
||||||
|
public DateTime UpdatedAtUtc { get; set; }
|
||||||
|
}
|
||||||
53
src/ZB.MOM.WW.OtOpcUa.Configuration/Entities/VirtualTag.cs
Normal file
53
src/ZB.MOM.WW.OtOpcUa.Configuration/Entities/VirtualTag.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Per Phase 7 plan decision #2 — a virtual (calculated) tag that lives in the
|
||||||
|
/// Equipment tree alongside driver tags. Value is produced by the
|
||||||
|
/// <see cref="Script"/> referenced by <see cref="ScriptId"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// <see cref="EquipmentId"/> is mandatory — virtual tags are always scoped to an
|
||||||
|
/// Equipment node per plan decision #2 (unified Equipment tree, not a separate
|
||||||
|
/// /Virtual namespace). <see cref="DataType"/> matches the shape used by
|
||||||
|
/// <c>Tag.DataType</c>.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// <see cref="ChangeTriggered"/> and <see cref="TimerIntervalMs"/> together realize
|
||||||
|
/// plan decision #3 (change + timer). At least one must produce evaluations; the
|
||||||
|
/// Core.VirtualTags engine rejects an all-disabled tag at load time.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public sealed class VirtualTag
|
||||||
|
{
|
||||||
|
public Guid VirtualTagRowId { get; set; }
|
||||||
|
public long GenerationId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Stable logical id.</summary>
|
||||||
|
public required string VirtualTagId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Logical FK to <see cref="Equipment.EquipmentId"/> — owner of this virtual tag.</summary>
|
||||||
|
public required string EquipmentId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Browse name — unique within owning Equipment.</summary>
|
||||||
|
public required string Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>DataType string — same vocabulary as <see cref="Tag.DataType"/>.</summary>
|
||||||
|
public required string DataType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Logical FK to <see cref="Script.ScriptId"/> — the script that computes this tag's value.</summary>
|
||||||
|
public required string ScriptId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Re-evaluate when any referenced input tag changes. Default on.</summary>
|
||||||
|
public bool ChangeTriggered { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>Timer re-evaluation cadence in milliseconds. <c>null</c> = no timer.</summary>
|
||||||
|
public int? TimerIntervalMs { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Per plan decision #10 — checkbox to route this tag's values through <c>IHistoryWriter</c>.</summary>
|
||||||
|
public bool Historize { get; set; }
|
||||||
|
|
||||||
|
public bool Enabled { get; set; } = true;
|
||||||
|
|
||||||
|
public ConfigGeneration? Generation { get; set; }
|
||||||
|
}
|
||||||
1793
src/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260420231641_AddPhase7ScriptingTables.Designer.cs
generated
Normal file
1793
src/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260420231641_AddPhase7ScriptingTables.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,186 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddPhase7ScriptingTables : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Script",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
ScriptRowId = table.Column<Guid>(type: "uniqueidentifier", nullable: false, defaultValueSql: "NEWSEQUENTIALID()"),
|
||||||
|
GenerationId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
ScriptId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
|
||||||
|
Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||||
|
SourceCode = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||||
|
SourceHash = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||||
|
Language = table.Column<string>(type: "nvarchar(16)", maxLength: 16, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Script", x => x.ScriptRowId);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Script_ConfigGeneration_GenerationId",
|
||||||
|
column: x => x.GenerationId,
|
||||||
|
principalTable: "ConfigGeneration",
|
||||||
|
principalColumn: "GenerationId",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ScriptedAlarm",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
ScriptedAlarmRowId = table.Column<Guid>(type: "uniqueidentifier", nullable: false, defaultValueSql: "NEWSEQUENTIALID()"),
|
||||||
|
GenerationId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
ScriptedAlarmId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
|
||||||
|
EquipmentId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||||
|
Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||||
|
AlarmType = table.Column<string>(type: "nvarchar(32)", maxLength: 32, nullable: false),
|
||||||
|
Severity = table.Column<int>(type: "int", nullable: false),
|
||||||
|
MessageTemplate = table.Column<string>(type: "nvarchar(1024)", maxLength: 1024, nullable: false),
|
||||||
|
PredicateScriptId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||||
|
HistorizeToAveva = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
Retain = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
Enabled = table.Column<bool>(type: "bit", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ScriptedAlarm", x => x.ScriptedAlarmRowId);
|
||||||
|
table.CheckConstraint("CK_ScriptedAlarm_AlarmType", "AlarmType IN ('AlarmCondition','LimitAlarm','OffNormalAlarm','DiscreteAlarm')");
|
||||||
|
table.CheckConstraint("CK_ScriptedAlarm_Severity_Range", "Severity BETWEEN 1 AND 1000");
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ScriptedAlarm_ConfigGeneration_GenerationId",
|
||||||
|
column: x => x.GenerationId,
|
||||||
|
principalTable: "ConfigGeneration",
|
||||||
|
principalColumn: "GenerationId",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ScriptedAlarmState",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
ScriptedAlarmId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||||
|
EnabledState = table.Column<string>(type: "nvarchar(16)", maxLength: 16, nullable: false),
|
||||||
|
AckedState = table.Column<string>(type: "nvarchar(16)", maxLength: 16, nullable: false),
|
||||||
|
ConfirmedState = table.Column<string>(type: "nvarchar(16)", maxLength: 16, nullable: false),
|
||||||
|
ShelvingState = table.Column<string>(type: "nvarchar(16)", maxLength: 16, nullable: false),
|
||||||
|
ShelvingExpiresUtc = table.Column<DateTime>(type: "datetime2(3)", nullable: true),
|
||||||
|
LastAckUser = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: true),
|
||||||
|
LastAckComment = table.Column<string>(type: "nvarchar(1024)", maxLength: 1024, nullable: true),
|
||||||
|
LastAckUtc = table.Column<DateTime>(type: "datetime2(3)", nullable: true),
|
||||||
|
LastConfirmUser = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: true),
|
||||||
|
LastConfirmComment = table.Column<string>(type: "nvarchar(1024)", maxLength: 1024, nullable: true),
|
||||||
|
LastConfirmUtc = table.Column<DateTime>(type: "datetime2(3)", nullable: true),
|
||||||
|
CommentsJson = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||||
|
UpdatedAtUtc = table.Column<DateTime>(type: "datetime2(3)", nullable: false, defaultValueSql: "SYSUTCDATETIME()")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ScriptedAlarmState", x => x.ScriptedAlarmId);
|
||||||
|
table.CheckConstraint("CK_ScriptedAlarmState_CommentsJson_IsJson", "ISJSON(CommentsJson) = 1");
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "VirtualTag",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
VirtualTagRowId = table.Column<Guid>(type: "uniqueidentifier", nullable: false, defaultValueSql: "NEWSEQUENTIALID()"),
|
||||||
|
GenerationId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
VirtualTagId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
|
||||||
|
EquipmentId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||||
|
Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||||
|
DataType = table.Column<string>(type: "nvarchar(32)", maxLength: 32, nullable: false),
|
||||||
|
ScriptId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||||
|
ChangeTriggered = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
TimerIntervalMs = table.Column<int>(type: "int", nullable: true),
|
||||||
|
Historize = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
Enabled = table.Column<bool>(type: "bit", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_VirtualTag", x => x.VirtualTagRowId);
|
||||||
|
table.CheckConstraint("CK_VirtualTag_TimerInterval_Min", "TimerIntervalMs IS NULL OR TimerIntervalMs >= 50");
|
||||||
|
table.CheckConstraint("CK_VirtualTag_Trigger_AtLeastOne", "ChangeTriggered = 1 OR TimerIntervalMs IS NOT NULL");
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_VirtualTag_ConfigGeneration_GenerationId",
|
||||||
|
column: x => x.GenerationId,
|
||||||
|
principalTable: "ConfigGeneration",
|
||||||
|
principalColumn: "GenerationId",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Script_Generation_SourceHash",
|
||||||
|
table: "Script",
|
||||||
|
columns: new[] { "GenerationId", "SourceHash" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "UX_Script_Generation_LogicalId",
|
||||||
|
table: "Script",
|
||||||
|
columns: new[] { "GenerationId", "ScriptId" },
|
||||||
|
unique: true,
|
||||||
|
filter: "[ScriptId] IS NOT NULL");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ScriptedAlarm_Generation_Script",
|
||||||
|
table: "ScriptedAlarm",
|
||||||
|
columns: new[] { "GenerationId", "PredicateScriptId" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "UX_ScriptedAlarm_Generation_EquipmentPath",
|
||||||
|
table: "ScriptedAlarm",
|
||||||
|
columns: new[] { "GenerationId", "EquipmentId", "Name" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "UX_ScriptedAlarm_Generation_LogicalId",
|
||||||
|
table: "ScriptedAlarm",
|
||||||
|
columns: new[] { "GenerationId", "ScriptedAlarmId" },
|
||||||
|
unique: true,
|
||||||
|
filter: "[ScriptedAlarmId] IS NOT NULL");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_VirtualTag_Generation_Script",
|
||||||
|
table: "VirtualTag",
|
||||||
|
columns: new[] { "GenerationId", "ScriptId" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "UX_VirtualTag_Generation_EquipmentPath",
|
||||||
|
table: "VirtualTag",
|
||||||
|
columns: new[] { "GenerationId", "EquipmentId", "Name" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "UX_VirtualTag_Generation_LogicalId",
|
||||||
|
table: "VirtualTag",
|
||||||
|
columns: new[] { "GenerationId", "VirtualTagId" },
|
||||||
|
unique: true,
|
||||||
|
filter: "[VirtualTagId] IS NOT NULL");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Script");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ScriptedAlarm");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ScriptedAlarmState");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "VirtualTag");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1027,6 +1027,193 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.Script", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("ScriptRowId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier")
|
||||||
|
.HasDefaultValueSql("NEWSEQUENTIALID()");
|
||||||
|
|
||||||
|
b.Property<long>("GenerationId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<string>("Language")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(16)
|
||||||
|
.HasColumnType("nvarchar(16)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("nvarchar(128)");
|
||||||
|
|
||||||
|
b.Property<string>("ScriptId")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("nvarchar(64)");
|
||||||
|
|
||||||
|
b.Property<string>("SourceCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("SourceHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("nvarchar(64)");
|
||||||
|
|
||||||
|
b.HasKey("ScriptRowId");
|
||||||
|
|
||||||
|
b.HasIndex("GenerationId", "ScriptId")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("UX_Script_Generation_LogicalId")
|
||||||
|
.HasFilter("[ScriptId] IS NOT NULL");
|
||||||
|
|
||||||
|
b.HasIndex("GenerationId", "SourceHash")
|
||||||
|
.HasDatabaseName("IX_Script_Generation_SourceHash");
|
||||||
|
|
||||||
|
b.ToTable("Script", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ScriptedAlarm", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("ScriptedAlarmRowId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier")
|
||||||
|
.HasDefaultValueSql("NEWSEQUENTIALID()");
|
||||||
|
|
||||||
|
b.Property<string>("AlarmType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("nvarchar(32)");
|
||||||
|
|
||||||
|
b.Property<bool>("Enabled")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("EquipmentId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("nvarchar(64)");
|
||||||
|
|
||||||
|
b.Property<long>("GenerationId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<bool>("HistorizeToAveva")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("MessageTemplate")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("nvarchar(1024)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("nvarchar(128)");
|
||||||
|
|
||||||
|
b.Property<string>("PredicateScriptId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("nvarchar(64)");
|
||||||
|
|
||||||
|
b.Property<bool>("Retain")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("ScriptedAlarmId")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("nvarchar(64)");
|
||||||
|
|
||||||
|
b.Property<int>("Severity")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("ScriptedAlarmRowId");
|
||||||
|
|
||||||
|
b.HasIndex("GenerationId", "PredicateScriptId")
|
||||||
|
.HasDatabaseName("IX_ScriptedAlarm_Generation_Script");
|
||||||
|
|
||||||
|
b.HasIndex("GenerationId", "ScriptedAlarmId")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("UX_ScriptedAlarm_Generation_LogicalId")
|
||||||
|
.HasFilter("[ScriptedAlarmId] IS NOT NULL");
|
||||||
|
|
||||||
|
b.HasIndex("GenerationId", "EquipmentId", "Name")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("UX_ScriptedAlarm_Generation_EquipmentPath");
|
||||||
|
|
||||||
|
b.ToTable("ScriptedAlarm", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("CK_ScriptedAlarm_AlarmType", "AlarmType IN ('AlarmCondition','LimitAlarm','OffNormalAlarm','DiscreteAlarm')");
|
||||||
|
|
||||||
|
t.HasCheckConstraint("CK_ScriptedAlarm_Severity_Range", "Severity BETWEEN 1 AND 1000");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ScriptedAlarmState", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("ScriptedAlarmId")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("nvarchar(64)");
|
||||||
|
|
||||||
|
b.Property<string>("AckedState")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(16)
|
||||||
|
.HasColumnType("nvarchar(16)");
|
||||||
|
|
||||||
|
b.Property<string>("CommentsJson")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("ConfirmedState")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(16)
|
||||||
|
.HasColumnType("nvarchar(16)");
|
||||||
|
|
||||||
|
b.Property<string>("EnabledState")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(16)
|
||||||
|
.HasColumnType("nvarchar(16)");
|
||||||
|
|
||||||
|
b.Property<string>("LastAckComment")
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("nvarchar(1024)");
|
||||||
|
|
||||||
|
b.Property<string>("LastAckUser")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("nvarchar(128)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastAckUtc")
|
||||||
|
.HasColumnType("datetime2(3)");
|
||||||
|
|
||||||
|
b.Property<string>("LastConfirmComment")
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("nvarchar(1024)");
|
||||||
|
|
||||||
|
b.Property<string>("LastConfirmUser")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("nvarchar(128)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastConfirmUtc")
|
||||||
|
.HasColumnType("datetime2(3)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ShelvingExpiresUtc")
|
||||||
|
.HasColumnType("datetime2(3)");
|
||||||
|
|
||||||
|
b.Property<string>("ShelvingState")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(16)
|
||||||
|
.HasColumnType("nvarchar(16)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAtUtc")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2(3)")
|
||||||
|
.HasDefaultValueSql("SYSUTCDATETIME()");
|
||||||
|
|
||||||
|
b.HasKey("ScriptedAlarmId");
|
||||||
|
|
||||||
|
b.ToTable("ScriptedAlarmState", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("CK_ScriptedAlarmState_CommentsJson_IsJson", "ISJSON(CommentsJson) = 1");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ServerCluster", b =>
|
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ServerCluster", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("ClusterId")
|
b.Property<string>("ClusterId")
|
||||||
@@ -1274,6 +1461,74 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
|||||||
b.ToTable("UnsLine", (string)null);
|
b.ToTable("UnsLine", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.VirtualTag", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("VirtualTagRowId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier")
|
||||||
|
.HasDefaultValueSql("NEWSEQUENTIALID()");
|
||||||
|
|
||||||
|
b.Property<bool>("ChangeTriggered")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("DataType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("nvarchar(32)");
|
||||||
|
|
||||||
|
b.Property<bool>("Enabled")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("EquipmentId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("nvarchar(64)");
|
||||||
|
|
||||||
|
b.Property<long>("GenerationId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<bool>("Historize")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("nvarchar(128)");
|
||||||
|
|
||||||
|
b.Property<string>("ScriptId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("nvarchar(64)");
|
||||||
|
|
||||||
|
b.Property<int?>("TimerIntervalMs")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("VirtualTagId")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("nvarchar(64)");
|
||||||
|
|
||||||
|
b.HasKey("VirtualTagRowId");
|
||||||
|
|
||||||
|
b.HasIndex("GenerationId", "ScriptId")
|
||||||
|
.HasDatabaseName("IX_VirtualTag_Generation_Script");
|
||||||
|
|
||||||
|
b.HasIndex("GenerationId", "VirtualTagId")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("UX_VirtualTag_Generation_LogicalId")
|
||||||
|
.HasFilter("[VirtualTagId] IS NOT NULL");
|
||||||
|
|
||||||
|
b.HasIndex("GenerationId", "EquipmentId", "Name")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("UX_VirtualTag_Generation_EquipmentPath");
|
||||||
|
|
||||||
|
b.ToTable("VirtualTag", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("CK_VirtualTag_TimerInterval_Min", "TimerIntervalMs IS NULL OR TimerIntervalMs >= 50");
|
||||||
|
|
||||||
|
t.HasCheckConstraint("CK_VirtualTag_Trigger_AtLeastOne", "ChangeTriggered = 1 OR TimerIntervalMs IS NOT NULL");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ClusterNode", b =>
|
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ClusterNode", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ServerCluster", "Cluster")
|
b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ServerCluster", "Cluster")
|
||||||
@@ -1435,6 +1690,28 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
|||||||
b.Navigation("Generation");
|
b.Navigation("Generation");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.Script", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ConfigGeneration", "Generation")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("GenerationId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Generation");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ScriptedAlarm", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ConfigGeneration", "Generation")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("GenerationId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Generation");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.Tag", b =>
|
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.Tag", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ConfigGeneration", "Generation")
|
b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ConfigGeneration", "Generation")
|
||||||
@@ -1476,6 +1753,17 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
|||||||
b.Navigation("Generation");
|
b.Navigation("Generation");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.VirtualTag", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ConfigGeneration", "Generation")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("GenerationId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Generation");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ClusterNode", b =>
|
modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ClusterNode", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("Credentials");
|
b.Navigation("Credentials");
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
|
|||||||
public DbSet<LdapGroupRoleMapping> LdapGroupRoleMappings => Set<LdapGroupRoleMapping>();
|
public DbSet<LdapGroupRoleMapping> LdapGroupRoleMappings => Set<LdapGroupRoleMapping>();
|
||||||
public DbSet<EquipmentImportBatch> EquipmentImportBatches => Set<EquipmentImportBatch>();
|
public DbSet<EquipmentImportBatch> EquipmentImportBatches => Set<EquipmentImportBatch>();
|
||||||
public DbSet<EquipmentImportRow> EquipmentImportRows => Set<EquipmentImportRow>();
|
public DbSet<EquipmentImportRow> EquipmentImportRows => Set<EquipmentImportRow>();
|
||||||
|
public DbSet<Script> Scripts => Set<Script>();
|
||||||
|
public DbSet<VirtualTag> VirtualTags => Set<VirtualTag>();
|
||||||
|
public DbSet<ScriptedAlarm> ScriptedAlarms => Set<ScriptedAlarm>();
|
||||||
|
public DbSet<ScriptedAlarmState> ScriptedAlarmStates => Set<ScriptedAlarmState>();
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
@@ -56,6 +60,10 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
|
|||||||
ConfigureDriverInstanceResilienceStatus(modelBuilder);
|
ConfigureDriverInstanceResilienceStatus(modelBuilder);
|
||||||
ConfigureLdapGroupRoleMapping(modelBuilder);
|
ConfigureLdapGroupRoleMapping(modelBuilder);
|
||||||
ConfigureEquipmentImportBatch(modelBuilder);
|
ConfigureEquipmentImportBatch(modelBuilder);
|
||||||
|
ConfigureScript(modelBuilder);
|
||||||
|
ConfigureVirtualTag(modelBuilder);
|
||||||
|
ConfigureScriptedAlarm(modelBuilder);
|
||||||
|
ConfigureScriptedAlarmState(modelBuilder);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ConfigureServerCluster(ModelBuilder modelBuilder)
|
private static void ConfigureServerCluster(ModelBuilder modelBuilder)
|
||||||
@@ -619,4 +627,106 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
|
|||||||
e.HasIndex(x => x.BatchId).HasDatabaseName("IX_EquipmentImportRow_Batch");
|
e.HasIndex(x => x.BatchId).HasDatabaseName("IX_EquipmentImportRow_Batch");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void ConfigureScript(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
modelBuilder.Entity<Script>(e =>
|
||||||
|
{
|
||||||
|
e.ToTable("Script");
|
||||||
|
e.HasKey(x => x.ScriptRowId);
|
||||||
|
e.Property(x => x.ScriptRowId).HasDefaultValueSql("NEWSEQUENTIALID()");
|
||||||
|
e.Property(x => x.ScriptId).HasMaxLength(64);
|
||||||
|
e.Property(x => x.Name).HasMaxLength(128);
|
||||||
|
e.Property(x => x.SourceCode).HasColumnType("nvarchar(max)");
|
||||||
|
e.Property(x => x.SourceHash).HasMaxLength(64);
|
||||||
|
e.Property(x => x.Language).HasMaxLength(16);
|
||||||
|
|
||||||
|
e.HasOne(x => x.Generation).WithMany().HasForeignKey(x => x.GenerationId).OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
e.HasIndex(x => new { x.GenerationId, x.ScriptId }).IsUnique().HasDatabaseName("UX_Script_Generation_LogicalId");
|
||||||
|
e.HasIndex(x => new { x.GenerationId, x.SourceHash }).HasDatabaseName("IX_Script_Generation_SourceHash");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ConfigureVirtualTag(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
modelBuilder.Entity<VirtualTag>(e =>
|
||||||
|
{
|
||||||
|
e.ToTable("VirtualTag", t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("CK_VirtualTag_Trigger_AtLeastOne",
|
||||||
|
"ChangeTriggered = 1 OR TimerIntervalMs IS NOT NULL");
|
||||||
|
t.HasCheckConstraint("CK_VirtualTag_TimerInterval_Min",
|
||||||
|
"TimerIntervalMs IS NULL OR TimerIntervalMs >= 50");
|
||||||
|
});
|
||||||
|
e.HasKey(x => x.VirtualTagRowId);
|
||||||
|
e.Property(x => x.VirtualTagRowId).HasDefaultValueSql("NEWSEQUENTIALID()");
|
||||||
|
e.Property(x => x.VirtualTagId).HasMaxLength(64);
|
||||||
|
e.Property(x => x.EquipmentId).HasMaxLength(64);
|
||||||
|
e.Property(x => x.Name).HasMaxLength(128);
|
||||||
|
e.Property(x => x.DataType).HasMaxLength(32);
|
||||||
|
e.Property(x => x.ScriptId).HasMaxLength(64);
|
||||||
|
|
||||||
|
e.HasOne(x => x.Generation).WithMany().HasForeignKey(x => x.GenerationId).OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
e.HasIndex(x => new { x.GenerationId, x.VirtualTagId }).IsUnique().HasDatabaseName("UX_VirtualTag_Generation_LogicalId");
|
||||||
|
e.HasIndex(x => new { x.GenerationId, x.EquipmentId, x.Name }).IsUnique().HasDatabaseName("UX_VirtualTag_Generation_EquipmentPath");
|
||||||
|
e.HasIndex(x => new { x.GenerationId, x.ScriptId }).HasDatabaseName("IX_VirtualTag_Generation_Script");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ConfigureScriptedAlarm(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
modelBuilder.Entity<ScriptedAlarm>(e =>
|
||||||
|
{
|
||||||
|
e.ToTable("ScriptedAlarm", t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("CK_ScriptedAlarm_Severity_Range", "Severity BETWEEN 1 AND 1000");
|
||||||
|
t.HasCheckConstraint("CK_ScriptedAlarm_AlarmType",
|
||||||
|
"AlarmType IN ('AlarmCondition','LimitAlarm','OffNormalAlarm','DiscreteAlarm')");
|
||||||
|
});
|
||||||
|
e.HasKey(x => x.ScriptedAlarmRowId);
|
||||||
|
e.Property(x => x.ScriptedAlarmRowId).HasDefaultValueSql("NEWSEQUENTIALID()");
|
||||||
|
e.Property(x => x.ScriptedAlarmId).HasMaxLength(64);
|
||||||
|
e.Property(x => x.EquipmentId).HasMaxLength(64);
|
||||||
|
e.Property(x => x.Name).HasMaxLength(128);
|
||||||
|
e.Property(x => x.AlarmType).HasMaxLength(32);
|
||||||
|
e.Property(x => x.MessageTemplate).HasMaxLength(1024);
|
||||||
|
e.Property(x => x.PredicateScriptId).HasMaxLength(64);
|
||||||
|
|
||||||
|
e.HasOne(x => x.Generation).WithMany().HasForeignKey(x => x.GenerationId).OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
e.HasIndex(x => new { x.GenerationId, x.ScriptedAlarmId }).IsUnique().HasDatabaseName("UX_ScriptedAlarm_Generation_LogicalId");
|
||||||
|
e.HasIndex(x => new { x.GenerationId, x.EquipmentId, x.Name }).IsUnique().HasDatabaseName("UX_ScriptedAlarm_Generation_EquipmentPath");
|
||||||
|
e.HasIndex(x => new { x.GenerationId, x.PredicateScriptId }).HasDatabaseName("IX_ScriptedAlarm_Generation_Script");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ConfigureScriptedAlarmState(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
modelBuilder.Entity<ScriptedAlarmState>(e =>
|
||||||
|
{
|
||||||
|
// Logical-id keyed (not generation-scoped) because ack state follows the alarm's
|
||||||
|
// stable identity across generations — Modified alarms keep their ack audit trail.
|
||||||
|
e.ToTable("ScriptedAlarmState", t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("CK_ScriptedAlarmState_CommentsJson_IsJson", "ISJSON(CommentsJson) = 1");
|
||||||
|
});
|
||||||
|
e.HasKey(x => x.ScriptedAlarmId);
|
||||||
|
e.Property(x => x.ScriptedAlarmId).HasMaxLength(64);
|
||||||
|
e.Property(x => x.EnabledState).HasMaxLength(16);
|
||||||
|
e.Property(x => x.AckedState).HasMaxLength(16);
|
||||||
|
e.Property(x => x.ConfirmedState).HasMaxLength(16);
|
||||||
|
e.Property(x => x.ShelvingState).HasMaxLength(16);
|
||||||
|
e.Property(x => x.ShelvingExpiresUtc).HasColumnType("datetime2(3)");
|
||||||
|
e.Property(x => x.LastAckUser).HasMaxLength(128);
|
||||||
|
e.Property(x => x.LastAckComment).HasMaxLength(1024);
|
||||||
|
e.Property(x => x.LastAckUtc).HasColumnType("datetime2(3)");
|
||||||
|
e.Property(x => x.LastConfirmUser).HasMaxLength(128);
|
||||||
|
e.Property(x => x.LastConfirmComment).HasMaxLength(1024);
|
||||||
|
e.Property(x => x.LastConfirmUtc).HasColumnType("datetime2(3)");
|
||||||
|
e.Property(x => x.CommentsJson).HasColumnType("nvarchar(max)");
|
||||||
|
e.Property(x => x.UpdatedAtUtc).HasColumnType("datetime2(3)").HasDefaultValueSql("SYSUTCDATETIME()");
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Shouldly;
|
||||||
|
using Xunit;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.Configuration.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies the Phase 7 Stream E entities (<see cref="Script"/>, <see cref="VirtualTag"/>,
|
||||||
|
/// <see cref="ScriptedAlarm"/>, <see cref="ScriptedAlarmState"/>) register correctly in
|
||||||
|
/// the EF model, map to the expected tables/columns/indexes, and carry the check constraints
|
||||||
|
/// the plan decisions call for. Introspection only — no SQL Server required.
|
||||||
|
/// </summary>
|
||||||
|
[Trait("Category", "Unit")]
|
||||||
|
public sealed class Phase7ScriptingEntitiesTests
|
||||||
|
{
|
||||||
|
private static OtOpcUaConfigDbContext BuildCtx()
|
||||||
|
{
|
||||||
|
var options = new DbContextOptionsBuilder<OtOpcUaConfigDbContext>()
|
||||||
|
.UseSqlServer("Server=(local);Database=dummy;Integrated Security=true") // not connected
|
||||||
|
.Options;
|
||||||
|
return new OtOpcUaConfigDbContext(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Microsoft.EntityFrameworkCore.Metadata.IModel DesignModel(OtOpcUaConfigDbContext ctx)
|
||||||
|
=> ctx.GetService<IDesignTimeModel>().Model;
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Script_entity_registered_with_expected_table_and_columns()
|
||||||
|
{
|
||||||
|
using var ctx = BuildCtx();
|
||||||
|
var entity = ctx.Model.FindEntityType(typeof(Script)).ShouldNotBeNull();
|
||||||
|
|
||||||
|
entity.GetTableName().ShouldBe("Script");
|
||||||
|
entity.FindProperty(nameof(Script.ScriptRowId)).ShouldNotBeNull();
|
||||||
|
entity.FindProperty(nameof(Script.ScriptId)).ShouldNotBeNull()
|
||||||
|
.GetMaxLength().ShouldBe(64);
|
||||||
|
entity.FindProperty(nameof(Script.SourceCode)).ShouldNotBeNull()
|
||||||
|
.GetColumnType().ShouldBe("nvarchar(max)");
|
||||||
|
entity.FindProperty(nameof(Script.SourceHash)).ShouldNotBeNull()
|
||||||
|
.GetMaxLength().ShouldBe(64);
|
||||||
|
entity.FindProperty(nameof(Script.Language)).ShouldNotBeNull()
|
||||||
|
.GetMaxLength().ShouldBe(16);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Script_has_unique_logical_id_per_generation()
|
||||||
|
{
|
||||||
|
using var ctx = BuildCtx();
|
||||||
|
var entity = ctx.Model.FindEntityType(typeof(Script)).ShouldNotBeNull();
|
||||||
|
entity.GetIndexes().ShouldContain(
|
||||||
|
i => i.IsUnique && i.GetDatabaseName() == "UX_Script_Generation_LogicalId");
|
||||||
|
entity.GetIndexes().ShouldContain(
|
||||||
|
i => i.GetDatabaseName() == "IX_Script_Generation_SourceHash");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void VirtualTag_entity_registered_with_trigger_check_constraint()
|
||||||
|
{
|
||||||
|
using var ctx = BuildCtx();
|
||||||
|
var entity = DesignModel(ctx).FindEntityType(typeof(VirtualTag)).ShouldNotBeNull();
|
||||||
|
entity.GetTableName().ShouldBe("VirtualTag");
|
||||||
|
|
||||||
|
var checks = entity.GetCheckConstraints().Select(c => c.Name).ToArray();
|
||||||
|
checks.ShouldContain("CK_VirtualTag_Trigger_AtLeastOne");
|
||||||
|
checks.ShouldContain("CK_VirtualTag_TimerInterval_Min");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void VirtualTag_enforces_unique_name_per_Equipment()
|
||||||
|
{
|
||||||
|
using var ctx = BuildCtx();
|
||||||
|
var entity = ctx.Model.FindEntityType(typeof(VirtualTag)).ShouldNotBeNull();
|
||||||
|
entity.GetIndexes().ShouldContain(
|
||||||
|
i => i.IsUnique && i.GetDatabaseName() == "UX_VirtualTag_Generation_EquipmentPath");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void VirtualTag_has_ChangeTriggered_and_Historize_flags()
|
||||||
|
{
|
||||||
|
using var ctx = BuildCtx();
|
||||||
|
var entity = ctx.Model.FindEntityType(typeof(VirtualTag)).ShouldNotBeNull();
|
||||||
|
entity.FindProperty(nameof(VirtualTag.ChangeTriggered)).ShouldNotBeNull()
|
||||||
|
.ClrType.ShouldBe(typeof(bool));
|
||||||
|
entity.FindProperty(nameof(VirtualTag.Historize)).ShouldNotBeNull()
|
||||||
|
.ClrType.ShouldBe(typeof(bool));
|
||||||
|
entity.FindProperty(nameof(VirtualTag.TimerIntervalMs)).ShouldNotBeNull()
|
||||||
|
.ClrType.ShouldBe(typeof(int?));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ScriptedAlarm_entity_registered_with_severity_and_type_checks()
|
||||||
|
{
|
||||||
|
using var ctx = BuildCtx();
|
||||||
|
var entity = DesignModel(ctx).FindEntityType(typeof(ScriptedAlarm)).ShouldNotBeNull();
|
||||||
|
entity.GetTableName().ShouldBe("ScriptedAlarm");
|
||||||
|
|
||||||
|
var checks = entity.GetCheckConstraints().Select(c => c.Name).ToArray();
|
||||||
|
checks.ShouldContain("CK_ScriptedAlarm_Severity_Range");
|
||||||
|
checks.ShouldContain("CK_ScriptedAlarm_AlarmType");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ScriptedAlarm_has_HistorizeToAveva_default_true_per_plan_decision_15()
|
||||||
|
{
|
||||||
|
// Defaults live on the CLR default assignment — verify the initializer.
|
||||||
|
var alarm = new ScriptedAlarm
|
||||||
|
{
|
||||||
|
ScriptedAlarmId = "a1",
|
||||||
|
EquipmentId = "eq1",
|
||||||
|
Name = "n",
|
||||||
|
AlarmType = "LimitAlarm",
|
||||||
|
MessageTemplate = "m",
|
||||||
|
PredicateScriptId = "s1",
|
||||||
|
};
|
||||||
|
alarm.HistorizeToAveva.ShouldBeTrue();
|
||||||
|
alarm.Retain.ShouldBeTrue();
|
||||||
|
alarm.Severity.ShouldBe(500);
|
||||||
|
alarm.Enabled.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ScriptedAlarmState_keyed_on_ScriptedAlarmId_not_generation_scoped()
|
||||||
|
{
|
||||||
|
using var ctx = BuildCtx();
|
||||||
|
var entity = ctx.Model.FindEntityType(typeof(ScriptedAlarmState)).ShouldNotBeNull();
|
||||||
|
entity.GetTableName().ShouldBe("ScriptedAlarmState");
|
||||||
|
|
||||||
|
var pk = entity.FindPrimaryKey().ShouldNotBeNull();
|
||||||
|
pk.Properties.Count.ShouldBe(1);
|
||||||
|
pk.Properties[0].Name.ShouldBe(nameof(ScriptedAlarmState.ScriptedAlarmId));
|
||||||
|
|
||||||
|
// State is NOT generation-scoped — GenerationId column should not exist per plan decision #14.
|
||||||
|
entity.FindProperty("GenerationId").ShouldBeNull(
|
||||||
|
"ack state follows alarm identity across generations");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ScriptedAlarmState_default_state_values_match_Part9_initial_states()
|
||||||
|
{
|
||||||
|
var state = new ScriptedAlarmState
|
||||||
|
{
|
||||||
|
ScriptedAlarmId = "a1",
|
||||||
|
EnabledState = "Enabled",
|
||||||
|
AckedState = "Unacknowledged",
|
||||||
|
ConfirmedState = "Unconfirmed",
|
||||||
|
ShelvingState = "Unshelved",
|
||||||
|
};
|
||||||
|
state.CommentsJson.ShouldBe("[]");
|
||||||
|
state.LastAckUser.ShouldBeNull();
|
||||||
|
state.LastAckUtc.ShouldBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ScriptedAlarmState_has_JSON_check_constraint_on_CommentsJson()
|
||||||
|
{
|
||||||
|
using var ctx = BuildCtx();
|
||||||
|
var entity = DesignModel(ctx).FindEntityType(typeof(ScriptedAlarmState)).ShouldNotBeNull();
|
||||||
|
var checks = entity.GetCheckConstraints().Select(c => c.Name).ToArray();
|
||||||
|
checks.ShouldContain("CK_ScriptedAlarmState_CommentsJson_IsJson");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void All_new_entities_exposed_via_DbSet()
|
||||||
|
{
|
||||||
|
using var ctx = BuildCtx();
|
||||||
|
ctx.Scripts.ShouldNotBeNull();
|
||||||
|
ctx.VirtualTags.ShouldNotBeNull();
|
||||||
|
ctx.ScriptedAlarms.ShouldNotBeNull();
|
||||||
|
ctx.ScriptedAlarmStates.ShouldNotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddPhase7ScriptingTables_migration_exists_in_assembly()
|
||||||
|
{
|
||||||
|
// The migration type carries the design-time snapshot + Up/Down methods EF uses to
|
||||||
|
// apply the schema. Missing = schema won't roll forward in deployments.
|
||||||
|
var t = typeof(Migrations.AddPhase7ScriptingTables);
|
||||||
|
t.ShouldNotBeNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user