feat(templates): phase 1 — derived-template schema (additive)
Phase 1 of the design at
docs/plans/2026-05-12-derive-on-compose-design.md.
Additive schema only — no behavior changes. Existing data and code
paths continue to work; subsequent phases will start writing the
new fields.
Template gains:
IsDerived true when this row was auto-created to back
a composition slot
OwnerCompositionId back-ref to the owning TemplateComposition
(plain int, not an EF nav property — managed
by TemplateService for cascade-delete)
TemplateAttribute / TemplateScript each gain:
IsInherited row copied from base and not yet overridden;
changes to the base flow downward
LockedInDerived on a base, blocks derived from overriding;
enforced at the service layer in later phases
EF Core migration AddDerivedTemplateFields adds four columns:
Templates.IsDerived bit NOT NULL DEFAULT 0
Templates.OwnerCompositionId int NULL
TemplateAttributes.IsInherited bit NOT NULL DEFAULT 0
TemplateAttributes.LockedInDerived bit NOT NULL DEFAULT 0
TemplateScripts.IsInherited bit NOT NULL DEFAULT 0
TemplateScripts.LockedInDerived bit NOT NULL DEFAULT 0
Existing rows get the defaults. Tests across SiteRuntime / TemplateEngine
/ CentralUI suites stay green (129 / 199 / 159).
Next: phase 2 — wire AddCompositionAsync to derive on compose for
new compositions. Old data still flows the direct-reference path
until phase 3's migration script.
This commit is contained in:
@@ -12,6 +12,21 @@ public class Template
|
|||||||
public ICollection<TemplateScript> Scripts { get; set; } = new List<TemplateScript>();
|
public ICollection<TemplateScript> Scripts { get; set; } = new List<TemplateScript>();
|
||||||
public ICollection<TemplateComposition> Compositions { get; set; } = new List<TemplateComposition>();
|
public ICollection<TemplateComposition> Compositions { get; set; } = new List<TemplateComposition>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True when this template was auto-derived to back a TemplateComposition
|
||||||
|
/// slot. Derived templates inherit from a base (see <see cref="ParentTemplateId"/>),
|
||||||
|
/// are owned by their composition row (see <see cref="OwnerCompositionId"/>),
|
||||||
|
/// and are hidden from the main template tree by default.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsDerived { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Back-reference to the <see cref="TemplateComposition"/> that owns this
|
||||||
|
/// derived template. Non-null only when <see cref="IsDerived"/>; cascade-
|
||||||
|
/// delete when the composition is removed. Always null on base templates.
|
||||||
|
/// </summary>
|
||||||
|
public int? OwnerCompositionId { get; set; }
|
||||||
|
|
||||||
public Template(string name)
|
public Template(string name)
|
||||||
{
|
{
|
||||||
Name = name ?? throw new ArgumentNullException(nameof(name));
|
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||||
|
|||||||
@@ -13,6 +13,21 @@ public class TemplateAttribute
|
|||||||
public string? Description { get; set; }
|
public string? Description { get; set; }
|
||||||
public string? DataSourceReference { get; set; }
|
public string? DataSourceReference { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True when this row was copied from the base template and has not been
|
||||||
|
/// overridden on the derived template. Changes to the base flow downward
|
||||||
|
/// for inherited rows; an explicit override flips this to false.
|
||||||
|
/// Always false on base (non-derived) templates.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsInherited { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set on a base attribute. When true, derived templates may not override
|
||||||
|
/// the value — the row is rendered readonly with a 🔒 in the derived UI,
|
||||||
|
/// and any attempt to update it through the API is rejected.
|
||||||
|
/// </summary>
|
||||||
|
public bool LockedInDerived { get; set; }
|
||||||
|
|
||||||
public TemplateAttribute(string name)
|
public TemplateAttribute(string name)
|
||||||
{
|
{
|
||||||
Name = name ?? throw new ArgumentNullException(nameof(name));
|
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||||
|
|||||||
@@ -13,6 +13,21 @@ public class TemplateScript
|
|||||||
public string? ReturnDefinition { get; set; }
|
public string? ReturnDefinition { get; set; }
|
||||||
public TimeSpan? MinTimeBetweenRuns { get; set; }
|
public TimeSpan? MinTimeBetweenRuns { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True when this row was copied from the base template and has not been
|
||||||
|
/// overridden on the derived template. Changes to the base flow downward
|
||||||
|
/// for inherited rows; an explicit override flips this to false.
|
||||||
|
/// Always false on base (non-derived) templates.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsInherited { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set on a base script. When true, derived templates may not override
|
||||||
|
/// the script body — the row is rendered readonly with a 🔒 in the derived
|
||||||
|
/// UI, and any attempt to update it through the API is rejected.
|
||||||
|
/// </summary>
|
||||||
|
public bool LockedInDerived { get; set; }
|
||||||
|
|
||||||
public TemplateScript(string name, string code)
|
public TemplateScript(string name, string code)
|
||||||
{
|
{
|
||||||
Name = name ?? throw new ArgumentNullException(nameof(name));
|
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||||
|
|||||||
1300
src/ScadaLink.ConfigurationDatabase/Migrations/20260512121446_AddDerivedTemplateFields.Designer.cs
generated
Normal file
1300
src/ScadaLink.ConfigurationDatabase/Migrations/20260512121446_AddDerivedTemplateFields.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,83 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ScadaLink.ConfigurationDatabase.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddDerivedTemplateFields : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "IsInherited",
|
||||||
|
table: "TemplateScripts",
|
||||||
|
type: "bit",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "LockedInDerived",
|
||||||
|
table: "TemplateScripts",
|
||||||
|
type: "bit",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "IsDerived",
|
||||||
|
table: "Templates",
|
||||||
|
type: "bit",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "OwnerCompositionId",
|
||||||
|
table: "Templates",
|
||||||
|
type: "int",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "IsInherited",
|
||||||
|
table: "TemplateAttributes",
|
||||||
|
type: "bit",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "LockedInDerived",
|
||||||
|
table: "TemplateAttributes",
|
||||||
|
type: "bit",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "IsInherited",
|
||||||
|
table: "TemplateScripts");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "LockedInDerived",
|
||||||
|
table: "TemplateScripts");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "IsDerived",
|
||||||
|
table: "Templates");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "OwnerCompositionId",
|
||||||
|
table: "Templates");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "IsInherited",
|
||||||
|
table: "TemplateAttributes");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "LockedInDerived",
|
||||||
|
table: "TemplateAttributes");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -848,11 +848,17 @@ namespace ScadaLink.ConfigurationDatabase.Migrations
|
|||||||
b.Property<int?>("FolderId")
|
b.Property<int?>("FolderId")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDerived")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(200)
|
.HasMaxLength(200)
|
||||||
.HasColumnType("nvarchar(200)");
|
.HasColumnType("nvarchar(200)");
|
||||||
|
|
||||||
|
b.Property<int?>("OwnerCompositionId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<int?>("ParentTemplateId")
|
b.Property<int?>("ParentTemplateId")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
@@ -935,9 +941,15 @@ namespace ScadaLink.ConfigurationDatabase.Migrations
|
|||||||
.HasMaxLength(2000)
|
.HasMaxLength(2000)
|
||||||
.HasColumnType("nvarchar(2000)");
|
.HasColumnType("nvarchar(2000)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsInherited")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
b.Property<bool>("IsLocked")
|
b.Property<bool>("IsLocked")
|
||||||
.HasColumnType("bit");
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<bool>("LockedInDerived")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(200)
|
.HasMaxLength(200)
|
||||||
@@ -1027,9 +1039,15 @@ namespace ScadaLink.ConfigurationDatabase.Migrations
|
|||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("nvarchar(max)");
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsInherited")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
b.Property<bool>("IsLocked")
|
b.Property<bool>("IsLocked")
|
||||||
.HasColumnType("bit");
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<bool>("LockedInDerived")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
b.Property<TimeSpan?>("MinTimeBetweenRuns")
|
b.Property<TimeSpan?>("MinTimeBetweenRuns")
|
||||||
.HasColumnType("time");
|
.HasColumnType("time");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user