From 6bcca4b344973e03fe007f35252bbfa44806853c Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 22 Oct 2026 06:06:27 -0400 Subject: [PATCH] feat(templates): GetTemplateStubsAsync untracked identity/edge projection --- .../Repositories/ITemplateEngineRepository.cs | 7 +++++++ .../Types/Templates/TemplateStub.cs | 19 +++++++++++++++++ .../Repositories/TemplateEngineRepository.cs | 10 +++++++++ .../TemplateEngineRepositoryTests.cs | 21 +++++++++++++++++++ 4 files changed, 57 insertions(+) create mode 100644 src/ZB.MOM.WW.ScadaBridge.Commons/Types/Templates/TemplateStub.cs diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ITemplateEngineRepository.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ITemplateEngineRepository.cs index 08f3106b..25c1098a 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ITemplateEngineRepository.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ITemplateEngineRepository.cs @@ -1,6 +1,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances; using ZB.MOM.WW.ScadaBridge.Commons.Entities.Scripts; using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates; +using ZB.MOM.WW.ScadaBridge.Commons.Types.Templates; namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories; @@ -55,6 +56,12 @@ public interface ITemplateEngineRepository /// A task that resolves to a read-only list of all templates without script bodies. Task> GetAllTemplatesForAnalysisAsync(CancellationToken cancellationToken = default); /// + /// Returns an untracked identity/edge projection of every template (no children), ordered by name. + /// + /// Cancellation token. + /// A task that resolves to a read-only list of template stubs. + Task> GetTemplateStubsAsync(CancellationToken cancellationToken = default); + /// /// Database-side paged listing of templates as row summaries (child /// collections reduced to counts, no script bodies). Backs the /// ListTemplates management command, which previously materialised diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Templates/TemplateStub.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Templates/TemplateStub.cs new file mode 100644 index 00000000..318886b8 --- /dev/null +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Templates/TemplateStub.cs @@ -0,0 +1,19 @@ +namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Templates; + +/// +/// Lightweight template identity/edge projection (no attributes, alarms, scripts or compositions) +/// for callers that only need names and the inheritance/folder/ownership edges. +/// +/// Template ID. +/// Template name. +/// Base template, if any. +/// Containing folder, if any. +/// True for auto-derived composition templates. +/// Owning composition row for derived templates. +public sealed record TemplateStub( + int Id, + string Name, + int? ParentTemplateId, + int? FolderId, + bool IsDerived, + int? OwnerCompositionId); diff --git a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/TemplateEngineRepository.cs b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/TemplateEngineRepository.cs index 573d139f..839153d4 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/TemplateEngineRepository.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/TemplateEngineRepository.cs @@ -90,6 +90,16 @@ public class TemplateEngineRepository : ITemplateEngineRepository .ToListAsync(cancellationToken); } + /// + public async Task> GetTemplateStubsAsync(CancellationToken cancellationToken = default) + { + return await _context.Templates + .AsNoTracking() + .OrderBy(t => t.Name) + .Select(t => new TemplateStub(t.Id, t.Name, t.ParentTemplateId, t.FolderId, t.IsDerived, t.OwnerCompositionId)) + .ToListAsync(cancellationToken); + } + /// // Two deliberate departures from : // diff --git a/tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/TemplateEngineRepositoryTests.cs b/tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/TemplateEngineRepositoryTests.cs index ee82df07..91bee9b5 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/TemplateEngineRepositoryTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/TemplateEngineRepositoryTests.cs @@ -31,6 +31,27 @@ public class TemplateEngineRepositoryTests : IDisposable _context.Dispose(); } + /// Verifies the stub projection carries identity and edges and leaves nothing tracked. + /// A task that represents the asynchronous operation. + [Fact] + public async Task GetTemplateStubs_ProjectsEdgesUntrackedOrderedByName() + { + var baseT = new Template("Base"); + _context.Templates.Add(baseT); + await _context.SaveChangesAsync(); + _context.Templates.Add(new Template("Alpha") { ParentTemplateId = baseT.Id }); + await _context.SaveChangesAsync(); + _context.ChangeTracker.Clear(); + + var stubs = await _repository.GetTemplateStubsAsync(); + + Assert.Equal(new[] { "Alpha", "Base" }, stubs.Select(s => s.Name)); + Assert.Equal(baseT.Id, stubs[0].ParentTemplateId); + Assert.Null(stubs[1].ParentTemplateId); + Assert.False(stubs[0].IsDerived); + Assert.Empty(_context.ChangeTracker.Entries()); + } + /// Verifies that the topology page is bounded, filtered and returned untracked. /// A task that represents the asynchronous operation. [Fact]