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 5cdc8c15..68146dd1 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ITemplateEngineRepository.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ITemplateEngineRepository.cs @@ -56,6 +56,19 @@ public interface ITemplateEngineRepository /// A task that resolves to a read-only list of all templates without script bodies. Task> GetAllTemplatesForAnalysisAsync(CancellationToken cancellationToken = default); /// + /// Returns the same untracked, script-body-free projection as + /// , but only for the templates an analysis walk + /// rooted at can reach: its inheritance chain plus the + /// composition closure of every chain member (see + /// ). + /// Backs member add/update validation (Gitea #66), which previously loaded every template. + /// Unknown ids yield an empty list. + /// + /// The template whose chain and composition closure to load. + /// Cancellation token. + /// A task that resolves to the closure templates without script bodies. + Task> GetTemplateAnalysisClosureAsync(int templateId, CancellationToken cancellationToken = default); + /// /// Returns an untracked identity/edge projection of every template (no children), ordered by name. /// /// Cancellation token. diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Templates/TemplateAnalysisClosure.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Templates/TemplateAnalysisClosure.cs new file mode 100644 index 00000000..11f97e76 --- /dev/null +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Templates/TemplateAnalysisClosure.cs @@ -0,0 +1,113 @@ +using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates; + +namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Templates; + +/// +/// Computes the set of templates a member add/update analysis walk can reach from one template +/// (Gitea #66): the template's inheritance chain (itself, its parent, grandparent, …) plus +/// the composition closure of every chain member (each composed module, the modules they +/// compose, and so on). +/// +/// +/// This mirrors exactly what the analysis walkers read. TemplateResolver.ResolveAllMembers +/// and CollisionDetector.DetectCollisions climb the parent chain of the template being +/// analysed and descend composition edges from every chain member, but never climb a composed +/// module's own parent chain (a derived module carries its inherited members as placeholder rows). +/// So a composed module's ancestors are not loaded — only chain members expand their parent edge. +/// Every other template in the library is irrelevant to the result, which is why member edits no +/// longer need the whole analysis graph. +/// +/// +/// +/// The walk is breadth-first by batch: each round asks loadBatch for every id not yet +/// loaded, so a store-backed loader issues one query per closure depth rather than one per +/// template. Missing ids (a dangling edge) are simply absent from the result, exactly as the +/// walkers skip them. Cycles terminate: an id is expanded at most twice (once as a module, once +/// more if it is later reached as a chain member). +/// +/// +public static class TemplateAnalysisClosure +{ + /// + /// Collects the inheritance chain and composition closure of . + /// + /// The template whose analysis closure is wanted. + /// Loads the templates with the given ids (unknown ids are omitted). + /// Cancellation token. + /// The closure templates, each once, in load order (the root first when it exists). + public static async Task> CollectAsync( + int templateId, + Func, CancellationToken, Task>> loadBatch, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(loadBatch); + + var loaded = new Dictionary(); + var result = new List