docs: add XML doc comments across src + Sister Projects section in CLAUDE.md
Bulk CommentChecker pass: fills in <param>/<inheritdoc> tags on public APIs across all 23 src/ projects so the doc-coverage gate is green. Also adds a Sister Projects section to CLAUDE.md pointing at the MxAccess Gateway and OtOpcUa sibling repos, and gitignores local credential captures (*login*.txt) and the wonder-app-vd03 deploy/ artifacts.
This commit is contained in:
@@ -16,6 +16,7 @@ public static class CycleDetector
|
||||
/// per Id, and a real cycle through any duplicate would still be reachable.
|
||||
/// A plain <c>ToDictionary(t => t.Id)</c> would instead throw ArgumentException.
|
||||
/// </summary>
|
||||
/// <param name="allTemplates">All templates to build lookup from.</param>
|
||||
internal static Dictionary<int, Template> BuildLookup(IReadOnlyList<Template> allTemplates)
|
||||
{
|
||||
var lookup = new Dictionary<int, Template>();
|
||||
@@ -28,6 +29,9 @@ public static class CycleDetector
|
||||
/// Checks whether setting <paramref name="parentId"/> as the parent of template
|
||||
/// <paramref name="templateId"/> would introduce an inheritance cycle.
|
||||
/// </summary>
|
||||
/// <param name="templateId">The template being modified.</param>
|
||||
/// <param name="parentId">The proposed parent template ID.</param>
|
||||
/// <param name="allTemplates">All templates for cycle detection.</param>
|
||||
/// <returns>A description of the cycle if one would be created, or null if safe.</returns>
|
||||
public static string? DetectInheritanceCycle(
|
||||
int templateId,
|
||||
@@ -75,6 +79,9 @@ public static class CycleDetector
|
||||
/// Checks whether adding a composition of <paramref name="composedTemplateId"/> into
|
||||
/// <paramref name="templateId"/> would introduce a composition cycle.
|
||||
/// </summary>
|
||||
/// <param name="templateId">The template being modified.</param>
|
||||
/// <param name="composedTemplateId">The template to compose.</param>
|
||||
/// <param name="allTemplates">All templates for cycle detection.</param>
|
||||
/// <returns>A description of the cycle if one would be created, or null if safe.</returns>
|
||||
public static string? DetectCompositionCycle(
|
||||
int templateId,
|
||||
@@ -125,6 +132,10 @@ public static class CycleDetector
|
||||
/// A cross-graph cycle exists when following any combination of inheritance (parent)
|
||||
/// and composition edges from a template leads back to itself.
|
||||
/// </summary>
|
||||
/// <param name="templateId">The template being modified.</param>
|
||||
/// <param name="proposedParentId">Optional proposed parent template ID.</param>
|
||||
/// <param name="proposedComposedTemplateId">Optional proposed composition template ID.</param>
|
||||
/// <param name="allTemplates">All templates for cycle detection.</param>
|
||||
/// <returns>A description of the cycle if found, or null if safe.</returns>
|
||||
public static string? DetectCrossGraphCycle(
|
||||
int templateId,
|
||||
|
||||
@@ -452,6 +452,8 @@ public class FlatteningService
|
||||
/// Returns the derived config verbatim on parse failure of either input —
|
||||
/// the existing whole-replace behavior is the safe fallback.
|
||||
/// </summary>
|
||||
/// <param name="inheritedJson">The parent template's HiLo trigger JSON, or null.</param>
|
||||
/// <param name="derivedJson">The child template's HiLo trigger JSON override, or null.</param>
|
||||
public static string? MergeHiLoConfig(string? inheritedJson, string? derivedJson)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(inheritedJson)) return derivedJson;
|
||||
@@ -513,6 +515,8 @@ public class FlatteningService
|
||||
/// verbatim — safe fallback that matches the existing whole-replace
|
||||
/// semantics.
|
||||
/// </summary>
|
||||
/// <param name="inheritedJson">The parent template's HiLo trigger JSON, or null.</param>
|
||||
/// <param name="editedJson">The user-edited HiLo trigger JSON, or null.</param>
|
||||
public static string? DiffHiLoConfig(string? inheritedJson, string? editedJson)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(editedJson)) return null;
|
||||
|
||||
@@ -33,6 +33,7 @@ public class RevisionHashService
|
||||
/// The hash is computed over a canonical JSON representation with sorted keys,
|
||||
/// excluding volatile fields like GeneratedAtUtc.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The flattened configuration to hash.</param>
|
||||
public string ComputeHash(FlattenedConfiguration configuration)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
@@ -94,44 +95,125 @@ public class RevisionHashService
|
||||
// does not sort. See the DETERMINISM CONTRACT note on the class summary.
|
||||
private sealed record HashableConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Collection of alarms in the configuration.
|
||||
/// </summary>
|
||||
public List<HashableAlarm> Alarms { get; init; } = [];
|
||||
/// <summary>
|
||||
/// The area identifier.
|
||||
/// </summary>
|
||||
public int? AreaId { get; init; }
|
||||
/// <summary>
|
||||
/// Collection of attributes in the configuration.
|
||||
/// </summary>
|
||||
public List<HashableAttribute> Attributes { get; init; } = [];
|
||||
/// <summary>
|
||||
/// The unique instance name.
|
||||
/// </summary>
|
||||
public string InstanceUniqueName { get; init; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Collection of scripts in the configuration.
|
||||
/// </summary>
|
||||
public List<HashableScript> Scripts { get; init; } = [];
|
||||
/// <summary>
|
||||
/// The site identifier.
|
||||
/// </summary>
|
||||
public int SiteId { get; init; }
|
||||
/// <summary>
|
||||
/// The template identifier.
|
||||
/// </summary>
|
||||
public int TemplateId { get; init; }
|
||||
}
|
||||
|
||||
private sealed record HashableAttribute
|
||||
{
|
||||
/// <summary>
|
||||
/// The data connection identifier the attribute is bound to.
|
||||
/// </summary>
|
||||
public int? BoundDataConnectionId { get; init; }
|
||||
/// <summary>
|
||||
/// The canonical name of the attribute.
|
||||
/// </summary>
|
||||
public string CanonicalName { get; init; } = string.Empty;
|
||||
/// <summary>
|
||||
/// The data source reference for the attribute.
|
||||
/// </summary>
|
||||
public string? DataSourceReference { get; init; }
|
||||
/// <summary>
|
||||
/// The data type of the attribute.
|
||||
/// </summary>
|
||||
public string DataType { get; init; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Whether the attribute is locked.
|
||||
/// </summary>
|
||||
public bool IsLocked { get; init; }
|
||||
/// <summary>
|
||||
/// The attribute value.
|
||||
/// </summary>
|
||||
public string? Value { get; init; }
|
||||
}
|
||||
|
||||
private sealed record HashableAlarm
|
||||
{
|
||||
/// <summary>
|
||||
/// The canonical name of the alarm.
|
||||
/// </summary>
|
||||
public string CanonicalName { get; init; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Whether the alarm is locked.
|
||||
/// </summary>
|
||||
public bool IsLocked { get; init; }
|
||||
/// <summary>
|
||||
/// The canonical name of the script triggered by this alarm.
|
||||
/// </summary>
|
||||
public string? OnTriggerScriptCanonicalName { get; init; }
|
||||
/// <summary>
|
||||
/// The priority level of the alarm.
|
||||
/// </summary>
|
||||
public int PriorityLevel { get; init; }
|
||||
/// <summary>
|
||||
/// The trigger configuration for the alarm.
|
||||
/// </summary>
|
||||
public string? TriggerConfiguration { get; init; }
|
||||
/// <summary>
|
||||
/// The trigger type of the alarm.
|
||||
/// </summary>
|
||||
public string TriggerType { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
private sealed record HashableScript
|
||||
{
|
||||
/// <summary>
|
||||
/// The canonical name of the script.
|
||||
/// </summary>
|
||||
public string CanonicalName { get; init; } = string.Empty;
|
||||
/// <summary>
|
||||
/// The script code.
|
||||
/// </summary>
|
||||
public string Code { get; init; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Whether the script is locked.
|
||||
/// </summary>
|
||||
public bool IsLocked { get; init; }
|
||||
/// <summary>
|
||||
/// The minimum time between runs in ticks.
|
||||
/// </summary>
|
||||
public long? MinTimeBetweenRunsTicks { get; init; }
|
||||
/// <summary>
|
||||
/// JSON representation of parameter definitions.
|
||||
/// </summary>
|
||||
public string? ParameterDefinitions { get; init; }
|
||||
/// <summary>
|
||||
/// JSON representation of the return type definition.
|
||||
/// </summary>
|
||||
public string? ReturnDefinition { get; init; }
|
||||
/// <summary>
|
||||
/// The trigger configuration for the script.
|
||||
/// </summary>
|
||||
public string? TriggerConfiguration { get; init; }
|
||||
/// <summary>
|
||||
/// The trigger type of the script.
|
||||
/// </summary>
|
||||
public string? TriggerType { get; init; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ public static class LockEnforcer
|
||||
/// <summary>
|
||||
/// Validates that an attribute override does not violate lock or granularity rules.
|
||||
/// </summary>
|
||||
/// <param name="original">The parent template's attribute definition.</param>
|
||||
/// <param name="proposed">The child template's proposed override.</param>
|
||||
public static string? ValidateAttributeOverride(
|
||||
TemplateAttribute original,
|
||||
TemplateAttribute proposed)
|
||||
@@ -48,6 +50,8 @@ public static class LockEnforcer
|
||||
/// <summary>
|
||||
/// Validates that an alarm override does not violate lock or granularity rules.
|
||||
/// </summary>
|
||||
/// <param name="original">The parent template's alarm definition.</param>
|
||||
/// <param name="proposed">The child template's proposed override.</param>
|
||||
public static string? ValidateAlarmOverride(
|
||||
TemplateAlarm original,
|
||||
TemplateAlarm proposed)
|
||||
@@ -75,6 +79,8 @@ public static class LockEnforcer
|
||||
/// <summary>
|
||||
/// Validates that a script override does not violate lock or granularity rules.
|
||||
/// </summary>
|
||||
/// <param name="original">The parent template's script definition.</param>
|
||||
/// <param name="proposed">The child template's proposed override.</param>
|
||||
public static string? ValidateScriptOverride(
|
||||
TemplateScript original,
|
||||
TemplateScript proposed)
|
||||
@@ -97,6 +103,9 @@ public static class LockEnforcer
|
||||
/// Validates that a lock flag change is legal.
|
||||
/// Locking is allowed on unlocked members. Unlocking is never allowed.
|
||||
/// </summary>
|
||||
/// <param name="originalIsLocked">The current lock state of the member.</param>
|
||||
/// <param name="proposedIsLocked">The proposed lock state.</param>
|
||||
/// <param name="memberName">Name of the member being changed, for error messages.</param>
|
||||
public static string? ValidateLockChange(bool originalIsLocked, bool proposedIsLocked, string memberName)
|
||||
{
|
||||
if (originalIsLocked && !proposedIsLocked)
|
||||
|
||||
@@ -7,6 +7,10 @@ namespace ScadaLink.TemplateEngine;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers all template engine services (template, flattening, validation, and domain services).
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection to register into.</param>
|
||||
public static IServiceCollection AddTemplateEngine(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<TemplateService>();
|
||||
@@ -35,6 +39,10 @@ public static class ServiceCollectionExtensions
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers Akka.NET actors for the template engine (placeholder for future actor registration).
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection to register into.</param>
|
||||
public static IServiceCollection AddTemplateEngineActors(this IServiceCollection services)
|
||||
{
|
||||
// Phase 0: placeholder for Akka actor registration
|
||||
|
||||
@@ -16,6 +16,11 @@ public class AreaService
|
||||
private readonly ITemplateEngineRepository _repository;
|
||||
private readonly IAuditService _auditService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the AreaService with the specified repository and audit service.
|
||||
/// </summary>
|
||||
/// <param name="repository">The template engine repository for data access.</param>
|
||||
/// <param name="auditService">The audit service for logging area changes.</param>
|
||||
public AreaService(ITemplateEngineRepository repository, IAuditService auditService)
|
||||
{
|
||||
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
|
||||
@@ -25,6 +30,11 @@ public class AreaService
|
||||
/// <summary>
|
||||
/// Creates a new area within a site.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the new area.</param>
|
||||
/// <param name="siteId">The site identifier.</param>
|
||||
/// <param name="parentAreaId">Optional parent area identifier for hierarchical organization.</param>
|
||||
/// <param name="user">The user performing the action for audit logging.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public async Task<Result<Area>> CreateAreaAsync(
|
||||
string name, int siteId, int? parentAreaId, string user,
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -67,6 +77,10 @@ public class AreaService
|
||||
/// <summary>
|
||||
/// Updates an area's name.
|
||||
/// </summary>
|
||||
/// <param name="areaId">The area identifier.</param>
|
||||
/// <param name="name">The new name for the area.</param>
|
||||
/// <param name="user">The user performing the action for audit logging.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public async Task<Result<Area>> UpdateAreaAsync(
|
||||
int areaId, string name, string user,
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -100,6 +114,10 @@ public class AreaService
|
||||
/// Re-parents an area within its site. `newParentAreaId == null` moves the area to the site root.
|
||||
/// Rejects: self-parent, descendant-parent (cycle), cross-site parent, name collision at new level.
|
||||
/// </summary>
|
||||
/// <param name="areaId">The area identifier.</param>
|
||||
/// <param name="newParentAreaId">The new parent area identifier, or null to move to site root.</param>
|
||||
/// <param name="user">The user performing the action for audit logging.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public async Task<Result<Area>> MoveAreaAsync(
|
||||
int areaId, int? newParentAreaId, string user,
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -154,6 +172,9 @@ public class AreaService
|
||||
/// <summary>
|
||||
/// Deletes an area. Blocked if instances are assigned to this area or any descendant area.
|
||||
/// </summary>
|
||||
/// <param name="areaId">The area identifier.</param>
|
||||
/// <param name="user">The user performing the action for audit logging.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public async Task<Result<bool>> DeleteAreaAsync(
|
||||
int areaId, string user,
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -201,12 +222,16 @@ public class AreaService
|
||||
/// <summary>
|
||||
/// Gets all areas for a site.
|
||||
/// </summary>
|
||||
/// <param name="siteId">The site identifier.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public async Task<IReadOnlyList<Area>> GetAreasBySiteIdAsync(int siteId, CancellationToken cancellationToken = default) =>
|
||||
await _repository.GetAreasBySiteIdAsync(siteId, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a single area by ID.
|
||||
/// </summary>
|
||||
/// <param name="areaId">The area identifier.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public async Task<Area?> GetAreaByIdAsync(int areaId, CancellationToken cancellationToken = default) =>
|
||||
await _repository.GetAreaByIdAsync(areaId, cancellationToken);
|
||||
|
||||
|
||||
@@ -27,6 +27,11 @@ public class InstanceService
|
||||
private readonly ITemplateEngineRepository _repository;
|
||||
private readonly IAuditService _auditService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the InstanceService class.
|
||||
/// </summary>
|
||||
/// <param name="repository">Template engine repository for data access.</param>
|
||||
/// <param name="auditService">Service for audit logging.</param>
|
||||
public InstanceService(ITemplateEngineRepository repository, IAuditService auditService)
|
||||
{
|
||||
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
|
||||
@@ -36,6 +41,13 @@ public class InstanceService
|
||||
/// <summary>
|
||||
/// Creates a new instance from a template at a site.
|
||||
/// </summary>
|
||||
/// <param name="uniqueName">Unique name for the instance.</param>
|
||||
/// <param name="templateId">ID of the template to instantiate.</param>
|
||||
/// <param name="siteId">ID of the site where the instance will reside.</param>
|
||||
/// <param name="areaId">Optional ID of the area to assign to the instance.</param>
|
||||
/// <param name="user">Username of the user creating the instance.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Result containing the created instance or failure message.</returns>
|
||||
public async Task<Result<Instance>> CreateInstanceAsync(
|
||||
string uniqueName,
|
||||
int templateId,
|
||||
@@ -87,6 +99,11 @@ public class InstanceService
|
||||
/// <summary>
|
||||
/// Assigns an instance to an area.
|
||||
/// </summary>
|
||||
/// <param name="instanceId">ID of the instance.</param>
|
||||
/// <param name="areaId">ID of the area to assign to, or null to clear assignment.</param>
|
||||
/// <param name="user">Username of the user making the assignment.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Result containing the updated instance or failure message.</returns>
|
||||
public async Task<Result<Instance>> AssignToAreaAsync(
|
||||
int instanceId,
|
||||
int? areaId,
|
||||
@@ -120,6 +137,12 @@ public class InstanceService
|
||||
/// Sets an attribute override for an instance. Only non-locked attributes can be overridden.
|
||||
/// Cannot add or remove attributes — only override values of existing template attributes.
|
||||
/// </summary>
|
||||
/// <param name="instanceId">ID of the instance.</param>
|
||||
/// <param name="attributeName">Name of the attribute to override.</param>
|
||||
/// <param name="overrideValue">Override value, or null to clear.</param>
|
||||
/// <param name="user">Username of the user setting the override.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Result containing the attribute override or failure message.</returns>
|
||||
public async Task<Result<InstanceAttributeOverride>> SetAttributeOverrideAsync(
|
||||
int instanceId,
|
||||
string attributeName,
|
||||
@@ -182,6 +205,13 @@ public class InstanceService
|
||||
/// inherited TriggerConfiguration setpoint-by-setpoint; for binary trigger
|
||||
/// types, it replaces the whole config.
|
||||
/// </summary>
|
||||
/// <param name="instanceId">ID of the instance.</param>
|
||||
/// <param name="alarmCanonicalName">Canonical name of the alarm to override.</param>
|
||||
/// <param name="triggerConfigurationOverride">Override JSON for the trigger configuration.</param>
|
||||
/// <param name="priorityLevelOverride">Override priority level, or null to use template value.</param>
|
||||
/// <param name="user">Username of the user setting the override.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Result containing the alarm override or failure message.</returns>
|
||||
public async Task<Result<InstanceAlarmOverride>> SetAlarmOverrideAsync(
|
||||
int instanceId,
|
||||
string alarmCanonicalName,
|
||||
@@ -252,6 +282,11 @@ public class InstanceService
|
||||
/// Removes a per-instance alarm override. After removal the instance
|
||||
/// inherits the template alarm config unchanged.
|
||||
/// </summary>
|
||||
/// <param name="instanceId">ID of the instance.</param>
|
||||
/// <param name="alarmCanonicalName">Canonical name of the alarm to delete the override for.</param>
|
||||
/// <param name="user">Username of the user deleting the override.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Result indicating success or failure.</returns>
|
||||
public async Task<Result<bool>> DeleteAlarmOverrideAsync(
|
||||
int instanceId,
|
||||
string alarmCanonicalName,
|
||||
@@ -275,6 +310,11 @@ public class InstanceService
|
||||
/// <summary>
|
||||
/// Sets connection bindings for an instance in bulk.
|
||||
/// </summary>
|
||||
/// <param name="instanceId">ID of the instance.</param>
|
||||
/// <param name="bindings">Connection bindings to set.</param>
|
||||
/// <param name="user">Username of the user setting the bindings.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Result containing the updated bindings or failure message.</returns>
|
||||
public async Task<Result<IReadOnlyList<InstanceConnectionBinding>>> SetConnectionBindingsAsync(
|
||||
int instanceId,
|
||||
IReadOnlyList<ConnectionBinding> bindings,
|
||||
@@ -321,6 +361,10 @@ public class InstanceService
|
||||
/// <summary>
|
||||
/// Enables an instance.
|
||||
/// </summary>
|
||||
/// <param name="instanceId">ID of the instance.</param>
|
||||
/// <param name="user">Username of the user enabling the instance.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Result containing the enabled instance or failure message.</returns>
|
||||
public async Task<Result<Instance>> EnableAsync(int instanceId, string user, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var instance = await _repository.GetInstanceByIdAsync(instanceId, cancellationToken);
|
||||
@@ -340,6 +384,10 @@ public class InstanceService
|
||||
/// <summary>
|
||||
/// Disables an instance.
|
||||
/// </summary>
|
||||
/// <param name="instanceId">ID of the instance.</param>
|
||||
/// <param name="user">Username of the user disabling the instance.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Result containing the disabled instance or failure message.</returns>
|
||||
public async Task<Result<Instance>> DisableAsync(int instanceId, string user, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var instance = await _repository.GetInstanceByIdAsync(instanceId, cancellationToken);
|
||||
@@ -359,6 +407,10 @@ public class InstanceService
|
||||
/// <summary>
|
||||
/// Deletes an instance.
|
||||
/// </summary>
|
||||
/// <param name="instanceId">ID of the instance.</param>
|
||||
/// <param name="user">Username of the user deleting the instance.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Result indicating success or failure.</returns>
|
||||
public async Task<Result<bool>> DeleteAsync(int instanceId, string user, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var instance = await _repository.GetInstanceByIdAsync(instanceId, cancellationToken);
|
||||
@@ -377,12 +429,18 @@ public class InstanceService
|
||||
/// <summary>
|
||||
/// Gets an instance by ID.
|
||||
/// </summary>
|
||||
/// <param name="instanceId">ID of the instance.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The instance, or null if not found.</returns>
|
||||
public async Task<Instance?> GetByIdAsync(int instanceId, CancellationToken cancellationToken = default) =>
|
||||
await _repository.GetInstanceByIdAsync(instanceId, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all instances for a site.
|
||||
/// </summary>
|
||||
/// <param name="siteId">ID of the site.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>List of instances for the site.</returns>
|
||||
public async Task<IReadOnlyList<Instance>> GetBySiteIdAsync(int siteId, CancellationToken cancellationToken = default) =>
|
||||
await _repository.GetInstancesBySiteIdAsync(siteId, cancellationToken);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ public class SiteService
|
||||
private readonly ISiteRepository _repository;
|
||||
private readonly IAuditService _auditService;
|
||||
|
||||
/// <summary>Initializes the service with its repository and audit collaborators.</summary>
|
||||
/// <param name="repository">Repository for site and data connection persistence.</param>
|
||||
/// <param name="auditService">Audit service for logging CRUD operations.</param>
|
||||
public SiteService(ISiteRepository repository, IAuditService auditService)
|
||||
{
|
||||
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
|
||||
@@ -24,6 +27,13 @@ public class SiteService
|
||||
|
||||
// --- Site CRUD ---
|
||||
|
||||
/// <summary>Creates a new site with the given name and identifier, rejecting duplicates.</summary>
|
||||
/// <param name="name">Display name of the site.</param>
|
||||
/// <param name="siteIdentifier">Unique machine-readable site identifier.</param>
|
||||
/// <param name="description">Optional description.</param>
|
||||
/// <param name="user">Authenticated username for audit logging.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>A successful result containing the new <see cref="Site"/>, or a failure result with a message.</returns>
|
||||
public async Task<Result<Site>> CreateSiteAsync(
|
||||
string name, string siteIdentifier, string? description, string user,
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -46,6 +56,13 @@ public class SiteService
|
||||
return Result<Site>.Success(site);
|
||||
}
|
||||
|
||||
/// <summary>Updates the name and description of an existing site.</summary>
|
||||
/// <param name="siteId">Primary key of the site to update.</param>
|
||||
/// <param name="name">New display name.</param>
|
||||
/// <param name="description">New optional description.</param>
|
||||
/// <param name="user">Authenticated username for audit logging.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>A successful result containing the updated <see cref="Site"/>, or a failure result if not found.</returns>
|
||||
public async Task<Result<Site>> UpdateSiteAsync(
|
||||
int siteId, string name, string? description, string user,
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -64,6 +81,11 @@ public class SiteService
|
||||
return Result<Site>.Success(site);
|
||||
}
|
||||
|
||||
/// <summary>Deletes a site, rejecting the request if any instances are assigned to it.</summary>
|
||||
/// <param name="siteId">Primary key of the site to delete.</param>
|
||||
/// <param name="user">Authenticated username for audit logging.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>A successful result with <c>true</c>, or a failure result with a message if blocked.</returns>
|
||||
public async Task<Result<bool>> DeleteSiteAsync(int siteId, string user, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var site = await _repository.GetSiteByIdAsync(siteId, cancellationToken);
|
||||
@@ -87,14 +109,29 @@ public class SiteService
|
||||
return Result<bool>.Success(true);
|
||||
}
|
||||
|
||||
/// <summary>Returns the site with the given primary key, or <c>null</c> if not found.</summary>
|
||||
/// <param name="siteId">Primary key of the site.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public async Task<Site?> GetSiteByIdAsync(int siteId, CancellationToken cancellationToken = default) =>
|
||||
await _repository.GetSiteByIdAsync(siteId, cancellationToken);
|
||||
|
||||
/// <summary>Returns all sites.</summary>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public async Task<IReadOnlyList<Site>> GetAllSitesAsync(CancellationToken cancellationToken = default) =>
|
||||
await _repository.GetAllSitesAsync(cancellationToken);
|
||||
|
||||
// --- Data Connection CRUD ---
|
||||
|
||||
/// <summary>Creates a new data connection owned by the specified site.</summary>
|
||||
/// <param name="siteId">Primary key of the owning site.</param>
|
||||
/// <param name="name">Display name of the data connection.</param>
|
||||
/// <param name="protocol">Protocol identifier (e.g., "OpcUa").</param>
|
||||
/// <param name="primaryConfiguration">JSON configuration for the primary endpoint; may be null.</param>
|
||||
/// <param name="backupConfiguration">JSON configuration for the backup endpoint; may be null.</param>
|
||||
/// <param name="failoverRetryCount">Number of retries before switching to the backup endpoint.</param>
|
||||
/// <param name="user">Authenticated username for audit logging.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>A successful result containing the new <see cref="DataConnection"/>, or a failure result.</returns>
|
||||
public async Task<Result<DataConnection>> CreateDataConnectionAsync(
|
||||
int siteId, string name, string protocol, string? primaryConfiguration,
|
||||
string? backupConfiguration, int failoverRetryCount, string user,
|
||||
@@ -120,6 +157,16 @@ public class SiteService
|
||||
return Result<DataConnection>.Success(connection);
|
||||
}
|
||||
|
||||
/// <summary>Updates an existing data connection's configuration.</summary>
|
||||
/// <param name="connectionId">Primary key of the data connection to update.</param>
|
||||
/// <param name="name">New display name.</param>
|
||||
/// <param name="protocol">New protocol identifier.</param>
|
||||
/// <param name="primaryConfiguration">New primary endpoint JSON configuration; may be null.</param>
|
||||
/// <param name="backupConfiguration">New backup endpoint JSON configuration; may be null.</param>
|
||||
/// <param name="failoverRetryCount">New failover retry count.</param>
|
||||
/// <param name="user">Authenticated username for audit logging.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>A successful result containing the updated <see cref="DataConnection"/>, or a failure result if not found.</returns>
|
||||
public async Task<Result<DataConnection>> UpdateDataConnectionAsync(
|
||||
int connectionId, string name, string protocol, string? primaryConfiguration,
|
||||
string? backupConfiguration, int failoverRetryCount, string user,
|
||||
@@ -143,6 +190,11 @@ public class SiteService
|
||||
return Result<DataConnection>.Success(connection);
|
||||
}
|
||||
|
||||
/// <summary>Deletes a data connection by its primary key.</summary>
|
||||
/// <param name="connectionId">Primary key of the data connection to delete.</param>
|
||||
/// <param name="user">Authenticated username for audit logging.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>A successful result with <c>true</c>, or a failure result if not found.</returns>
|
||||
public async Task<Result<bool>> DeleteDataConnectionAsync(
|
||||
int connectionId, string user, CancellationToken cancellationToken = default)
|
||||
{
|
||||
|
||||
@@ -16,6 +16,10 @@ public class TemplateDeletionService
|
||||
{
|
||||
private readonly ITemplateEngineRepository _repository;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <see cref="TemplateDeletionService"/> with the given repository.
|
||||
/// </summary>
|
||||
/// <param name="repository">Repository used to check deletion constraints and perform the delete.</param>
|
||||
public TemplateDeletionService(ITemplateEngineRepository repository)
|
||||
{
|
||||
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
|
||||
@@ -24,6 +28,8 @@ public class TemplateDeletionService
|
||||
/// <summary>
|
||||
/// Checks whether a template can be safely deleted and returns any blocking reasons.
|
||||
/// </summary>
|
||||
/// <param name="templateId">The id of the template to check.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
public async Task<Result<bool>> CanDeleteTemplateAsync(int templateId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var template = await _repository.GetTemplateByIdAsync(templateId, cancellationToken);
|
||||
@@ -98,6 +104,8 @@ public class TemplateDeletionService
|
||||
/// <summary>
|
||||
/// Deletes a template after checking all constraints.
|
||||
/// </summary>
|
||||
/// <param name="templateId">The id of the template to delete.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
public async Task<Result<bool>> DeleteTemplateAsync(int templateId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var canDelete = await CanDeleteTemplateAsync(templateId, cancellationToken);
|
||||
|
||||
@@ -10,12 +10,24 @@ public class TemplateFolderService
|
||||
private readonly ITemplateEngineRepository _repository;
|
||||
private readonly IAuditService _auditService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the service with its required dependencies.
|
||||
/// </summary>
|
||||
/// <param name="repository">Template engine repository for folder persistence.</param>
|
||||
/// <param name="auditService">Audit service for logging folder operations.</param>
|
||||
public TemplateFolderService(ITemplateEngineRepository repository, IAuditService auditService)
|
||||
{
|
||||
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
|
||||
_auditService = auditService ?? throw new ArgumentNullException(nameof(auditService));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new template folder, enforcing name uniqueness within the parent.
|
||||
/// </summary>
|
||||
/// <param name="name">Display name for the new folder.</param>
|
||||
/// <param name="parentFolderId">Parent folder id, or null to create at root level.</param>
|
||||
/// <param name="user">Username of the actor performing the operation (for audit).</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public async Task<Result<TemplateFolder>> CreateFolderAsync(
|
||||
string name, int? parentFolderId, string user,
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -43,6 +55,13 @@ public class TemplateFolderService
|
||||
return Result<TemplateFolder>.Success(folder);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renames an existing template folder, enforcing name uniqueness within its parent.
|
||||
/// </summary>
|
||||
/// <param name="folderId">Id of the folder to rename.</param>
|
||||
/// <param name="newName">New display name for the folder.</param>
|
||||
/// <param name="user">Username of the actor performing the operation (for audit).</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public async Task<Result<TemplateFolder>> RenameFolderAsync(
|
||||
int folderId, string newName, string user,
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -68,6 +87,13 @@ public class TemplateFolderService
|
||||
return Result<TemplateFolder>.Success(folder);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves an existing template folder to a new parent, checking for cycles and name collisions.
|
||||
/// </summary>
|
||||
/// <param name="folderId">Id of the folder to move.</param>
|
||||
/// <param name="newParentId">Target parent folder id, or null to move to root level.</param>
|
||||
/// <param name="user">Username of the actor performing the operation (for audit).</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public async Task<Result<TemplateFolder>> MoveFolderAsync(
|
||||
int folderId, int? newParentId, string user,
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -123,6 +149,12 @@ public class TemplateFolderService
|
||||
return Result<TemplateFolder>.Success(folder);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes an empty template folder. Fails if the folder contains sub-folders or templates.
|
||||
/// </summary>
|
||||
/// <param name="folderId">Id of the folder to delete.</param>
|
||||
/// <param name="user">Username of the actor performing the operation (for audit).</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public async Task<Result<bool>> DeleteFolderAsync(
|
||||
int folderId, string user,
|
||||
CancellationToken cancellationToken = default)
|
||||
|
||||
@@ -16,12 +16,27 @@ public class SharedScriptService
|
||||
private readonly ITemplateEngineRepository _repository;
|
||||
private readonly IAuditService _auditService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SharedScriptService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="repository">The template engine repository for data access.</param>
|
||||
/// <param name="auditService">The audit service for logging operations.</param>
|
||||
public SharedScriptService(ITemplateEngineRepository repository, IAuditService auditService)
|
||||
{
|
||||
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
|
||||
_auditService = auditService ?? throw new ArgumentNullException(nameof(auditService));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new shared script.
|
||||
/// </summary>
|
||||
/// <param name="name">The shared script name.</param>
|
||||
/// <param name="code">The shared script code.</param>
|
||||
/// <param name="parameterDefinitions">Optional parameter definitions JSON.</param>
|
||||
/// <param name="returnDefinition">Optional return definition JSON.</param>
|
||||
/// <param name="user">The user creating the script.</param>
|
||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
|
||||
/// <returns>A result containing the created script or an error message.</returns>
|
||||
public async Task<Result<SharedScript>> CreateSharedScriptAsync(
|
||||
string name,
|
||||
string code,
|
||||
@@ -59,6 +74,16 @@ public class SharedScriptService
|
||||
return Result<SharedScript>.Success(script);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing shared script.
|
||||
/// </summary>
|
||||
/// <param name="scriptId">The shared script ID to update.</param>
|
||||
/// <param name="code">The updated shared script code.</param>
|
||||
/// <param name="parameterDefinitions">Optional updated parameter definitions JSON.</param>
|
||||
/// <param name="returnDefinition">Optional updated return definition JSON.</param>
|
||||
/// <param name="user">The user updating the script.</param>
|
||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
|
||||
/// <returns>A result containing the updated script or an error message.</returns>
|
||||
public async Task<Result<SharedScript>> UpdateSharedScriptAsync(
|
||||
int scriptId,
|
||||
string code,
|
||||
@@ -90,6 +115,13 @@ public class SharedScriptService
|
||||
return Result<SharedScript>.Success(script);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a shared script.
|
||||
/// </summary>
|
||||
/// <param name="scriptId">The shared script ID to delete.</param>
|
||||
/// <param name="user">The user deleting the script.</param>
|
||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
|
||||
/// <returns>A result containing true if successful or an error message.</returns>
|
||||
public async Task<Result<bool>> DeleteSharedScriptAsync(
|
||||
int scriptId,
|
||||
string user,
|
||||
@@ -106,11 +138,22 @@ public class SharedScriptService
|
||||
return Result<bool>.Success(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a shared script by ID.
|
||||
/// </summary>
|
||||
/// <param name="scriptId">The shared script ID.</param>
|
||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
|
||||
/// <returns>The shared script if found; null otherwise.</returns>
|
||||
public async Task<SharedScript?> GetSharedScriptByIdAsync(int scriptId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _repository.GetSharedScriptByIdAsync(scriptId, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all shared scripts.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
|
||||
/// <returns>A read-only list of all shared scripts.</returns>
|
||||
public async Task<IReadOnlyList<SharedScript>> GetAllSharedScriptsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _repository.GetAllSharedScriptsAsync(cancellationToken);
|
||||
@@ -124,6 +167,8 @@ public class SharedScriptService
|
||||
/// char literal, or a comment does not produce a false syntax error.
|
||||
/// Full Roslyn compilation would be added in a later phase when the scripting sandbox is available.
|
||||
/// </summary>
|
||||
/// <param name="code">The C# code to validate.</param>
|
||||
/// <returns>An error message if validation fails; null if valid.</returns>
|
||||
internal static string? ValidateSyntax(string code)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(code))
|
||||
|
||||
@@ -19,6 +19,10 @@ public static class TemplateNaming
|
||||
/// stops and falls back to the stored contained name, and a cycle (which
|
||||
/// the composition graph should never contain) is broken defensively.
|
||||
/// </summary>
|
||||
/// <param name="template">The template whose qualified name to compute.</param>
|
||||
/// <param name="byId">Lookup of all templates by primary key.</param>
|
||||
/// <param name="compById">Lookup of all template compositions by primary key.</param>
|
||||
/// <returns>The dotted hierarchical name (e.g., <c>Owner.Slot.Name</c>).</returns>
|
||||
public static string QualifiedName(
|
||||
Template template,
|
||||
IReadOnlyDictionary<int, Template> byId,
|
||||
|
||||
@@ -13,13 +13,40 @@ public static class TemplateResolver
|
||||
/// </summary>
|
||||
public sealed record ResolvedTemplateMember
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the canonical (path-qualified) name of the member.
|
||||
/// </summary>
|
||||
public string CanonicalName { get; init; }
|
||||
public string MemberType { get; init; } // "Attribute", "Alarm", "Script"
|
||||
/// <summary>
|
||||
/// Gets the member type: "Attribute", "Alarm", or "Script".
|
||||
/// </summary>
|
||||
public string MemberType { get; init; }
|
||||
/// <summary>
|
||||
/// Gets the ID of the template that originally defined this member.
|
||||
/// </summary>
|
||||
public int SourceTemplateId { get; init; }
|
||||
/// <summary>
|
||||
/// Gets the ID of the member within its source template.
|
||||
/// </summary>
|
||||
public int MemberId { get; init; }
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this member is locked from override.
|
||||
/// </summary>
|
||||
public bool IsLocked { get; init; }
|
||||
/// <summary>
|
||||
/// Gets the module path for composed members, or null for direct members.
|
||||
/// </summary>
|
||||
public string? ModulePath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ResolvedTemplateMember"/> record.
|
||||
/// </summary>
|
||||
/// <param name="canonicalName">The canonical name of the member.</param>
|
||||
/// <param name="memberType">The member type: "Attribute", "Alarm", or "Script".</param>
|
||||
/// <param name="sourceTemplateId">The ID of the source template.</param>
|
||||
/// <param name="memberId">The member ID within the source template.</param>
|
||||
/// <param name="isLocked">Whether the member is locked from override.</param>
|
||||
/// <param name="modulePath">The module path for composed members; null for direct members.</param>
|
||||
public ResolvedTemplateMember(string canonicalName, string memberType, int sourceTemplateId, int memberId, bool isLocked, string? modulePath = null)
|
||||
{
|
||||
CanonicalName = canonicalName;
|
||||
@@ -35,6 +62,9 @@ public static class TemplateResolver
|
||||
/// Resolves all effective members for a template, walking inheritance and composition chains.
|
||||
/// Child members override parent members of the same canonical name (unless locked).
|
||||
/// </summary>
|
||||
/// <param name="templateId">The template ID to resolve members for.</param>
|
||||
/// <param name="allTemplates">The complete list of all templates for lookups.</param>
|
||||
/// <returns>A read-only list of resolved members for the template.</returns>
|
||||
public static IReadOnlyList<ResolvedTemplateMember> ResolveAllMembers(
|
||||
int templateId,
|
||||
IReadOnlyList<Template> allTemplates)
|
||||
@@ -73,6 +103,9 @@ public static class TemplateResolver
|
||||
/// <summary>
|
||||
/// Gets the inheritance chain from root ancestor to the specified template.
|
||||
/// </summary>
|
||||
/// <param name="templateId">The template ID to build the chain for.</param>
|
||||
/// <param name="lookup">A dictionary mapping template IDs to templates.</param>
|
||||
/// <returns>A read-only list of templates from root to the specified template.</returns>
|
||||
public static IReadOnlyList<Template> BuildInheritanceChain(
|
||||
int templateId,
|
||||
IReadOnlyDictionary<int, Template> lookup)
|
||||
@@ -98,6 +131,10 @@ public static class TemplateResolver
|
||||
/// Finds a member by canonical name in the resolved member set.
|
||||
/// Used to check override/lock constraints.
|
||||
/// </summary>
|
||||
/// <param name="canonicalName">The canonical name of the member to find.</param>
|
||||
/// <param name="parentTemplateId">The template ID to resolve members from.</param>
|
||||
/// <param name="allTemplates">The complete list of all templates for lookups.</param>
|
||||
/// <returns>The resolved member if found; null otherwise.</returns>
|
||||
public static ResolvedTemplateMember? FindMemberByCanonicalName(
|
||||
string canonicalName,
|
||||
int parentTemplateId,
|
||||
|
||||
@@ -15,6 +15,11 @@ public class TemplateService
|
||||
private readonly ITemplateEngineRepository _repository;
|
||||
private readonly IAuditService _auditService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the TemplateService class.
|
||||
/// </summary>
|
||||
/// <param name="repository">Template engine repository for data access.</param>
|
||||
/// <param name="auditService">Service for audit logging.</param>
|
||||
public TemplateService(ITemplateEngineRepository repository, IAuditService auditService)
|
||||
{
|
||||
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
|
||||
@@ -25,6 +30,16 @@ public class TemplateService
|
||||
// WP-1: Template CRUD with Inheritance
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new template with optional inheritance and folder assignment.
|
||||
/// </summary>
|
||||
/// <param name="name">Name of the template.</param>
|
||||
/// <param name="description">Optional description.</param>
|
||||
/// <param name="parentTemplateId">Optional ID of the parent template for inheritance.</param>
|
||||
/// <param name="user">Username of the user creating the template.</param>
|
||||
/// <param name="folderId">Optional ID of the folder to place the template in.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Result containing the created template or failure message.</returns>
|
||||
public async Task<Result<Template>> CreateTemplateAsync(
|
||||
string name,
|
||||
string? description,
|
||||
@@ -65,6 +80,16 @@ public class TemplateService
|
||||
return Result<Template>.Success(template);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates a template's name and description. Parent template is immutable after creation.
|
||||
/// </summary>
|
||||
/// <param name="templateId">ID of the template to update.</param>
|
||||
/// <param name="name">New name for the template.</param>
|
||||
/// <param name="description">New description.</param>
|
||||
/// <param name="parentTemplateId">Must match the existing parent (cannot be changed).</param>
|
||||
/// <param name="user">Username of the user updating the template.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Result containing the updated template or failure message.</returns>
|
||||
public async Task<Result<Template>> UpdateTemplateAsync(
|
||||
int templateId,
|
||||
string name,
|
||||
@@ -103,6 +128,13 @@ public class TemplateService
|
||||
return Result<Template>.Success(template);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a template if no instances depend on it.
|
||||
/// </summary>
|
||||
/// <param name="templateId">ID of the template to delete.</param>
|
||||
/// <param name="user">Username of the user deleting the template.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Result indicating success or failure.</returns>
|
||||
public async Task<Result<bool>> DeleteTemplateAsync(
|
||||
int templateId,
|
||||
string user,
|
||||
@@ -130,6 +162,14 @@ public class TemplateService
|
||||
return Result<bool>.Success(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves a template to a different folder.
|
||||
/// </summary>
|
||||
/// <param name="templateId">ID of the template to move.</param>
|
||||
/// <param name="newFolderId">ID of the target folder, or null to remove from folder.</param>
|
||||
/// <param name="user">Username of the user moving the template.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Result containing the updated template or failure message.</returns>
|
||||
public async Task<Result<Template>> MoveTemplateAsync(
|
||||
int templateId,
|
||||
int? newFolderId,
|
||||
@@ -155,11 +195,22 @@ public class TemplateService
|
||||
return Result<Template>.Success(template);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a template by ID.
|
||||
/// </summary>
|
||||
/// <param name="templateId">ID of the template.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The template, or null if not found.</returns>
|
||||
public async Task<Template?> GetTemplateByIdAsync(int templateId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _repository.GetTemplateByIdAsync(templateId, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all templates.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>List of all templates.</returns>
|
||||
public async Task<IReadOnlyList<Template>> GetAllTemplatesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _repository.GetAllTemplatesAsync(cancellationToken);
|
||||
@@ -169,6 +220,14 @@ public class TemplateService
|
||||
// WP-2: Attribute Definitions with Lock Flags
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Adds an attribute definition to a template.
|
||||
/// </summary>
|
||||
/// <param name="templateId">ID of the template.</param>
|
||||
/// <param name="attribute">Attribute definition to add.</param>
|
||||
/// <param name="user">Username of the user adding the attribute.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Result containing the added attribute or failure message.</returns>
|
||||
public async Task<Result<TemplateAttribute>> AddAttributeAsync(
|
||||
int templateId,
|
||||
TemplateAttribute attribute,
|
||||
@@ -200,6 +259,14 @@ public class TemplateService
|
||||
return Result<TemplateAttribute>.Success(attribute);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an attribute definition with lock and inheritance validation.
|
||||
/// </summary>
|
||||
/// <param name="attributeId">ID of the attribute to update.</param>
|
||||
/// <param name="proposed">New attribute definition values.</param>
|
||||
/// <param name="user">Username of the user updating the attribute.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Result containing the updated attribute or failure message.</returns>
|
||||
public async Task<Result<TemplateAttribute>> UpdateAttributeAsync(
|
||||
int attributeId,
|
||||
TemplateAttribute proposed,
|
||||
@@ -262,6 +329,13 @@ public class TemplateService
|
||||
return Result<TemplateAttribute>.Success(existing);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes an attribute definition from a template.
|
||||
/// </summary>
|
||||
/// <param name="attributeId">ID of the attribute to delete.</param>
|
||||
/// <param name="user">Username of the user deleting the attribute.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Result indicating success or failure.</returns>
|
||||
public async Task<Result<bool>> DeleteAttributeAsync(
|
||||
int attributeId,
|
||||
string user,
|
||||
@@ -294,6 +368,14 @@ public class TemplateService
|
||||
// WP-3: Alarm Definitions
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Adds an alarm definition to a template.
|
||||
/// </summary>
|
||||
/// <param name="templateId">ID of the template.</param>
|
||||
/// <param name="alarm">Alarm definition to add.</param>
|
||||
/// <param name="user">Username of the user adding the alarm.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Result containing the added alarm or failure message.</returns>
|
||||
public async Task<Result<TemplateAlarm>> AddAlarmAsync(
|
||||
int templateId,
|
||||
TemplateAlarm alarm,
|
||||
@@ -328,6 +410,14 @@ public class TemplateService
|
||||
return Result<TemplateAlarm>.Success(alarm);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an alarm definition with lock and inheritance validation.
|
||||
/// </summary>
|
||||
/// <param name="alarmId">ID of the alarm to update.</param>
|
||||
/// <param name="proposed">New alarm definition values.</param>
|
||||
/// <param name="user">Username of the user updating the alarm.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Result containing the updated alarm or failure message.</returns>
|
||||
public async Task<Result<TemplateAlarm>> UpdateAlarmAsync(
|
||||
int alarmId,
|
||||
TemplateAlarm proposed,
|
||||
@@ -393,6 +483,13 @@ public class TemplateService
|
||||
return Result<TemplateAlarm>.Success(existing);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes an alarm definition from a template.
|
||||
/// </summary>
|
||||
/// <param name="alarmId">ID of the alarm to delete.</param>
|
||||
/// <param name="user">Username of the user deleting the alarm.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Result indicating success or failure.</returns>
|
||||
public async Task<Result<bool>> DeleteAlarmAsync(
|
||||
int alarmId,
|
||||
string user,
|
||||
@@ -424,6 +521,14 @@ public class TemplateService
|
||||
// WP-4: Script Definitions
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Adds a script definition to a template.
|
||||
/// </summary>
|
||||
/// <param name="templateId">ID of the template.</param>
|
||||
/// <param name="script">Script definition to add.</param>
|
||||
/// <param name="user">Username of the user adding the script.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Result containing the added script or failure message.</returns>
|
||||
public async Task<Result<TemplateScript>> AddScriptAsync(
|
||||
int templateId,
|
||||
TemplateScript script,
|
||||
@@ -454,6 +559,14 @@ public class TemplateService
|
||||
return Result<TemplateScript>.Success(script);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates a script definition with lock and inheritance validation.
|
||||
/// </summary>
|
||||
/// <param name="scriptId">ID of the script to update.</param>
|
||||
/// <param name="proposed">New script definition values.</param>
|
||||
/// <param name="user">Username of the user updating the script.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Result containing the updated script or failure message.</returns>
|
||||
public async Task<Result<TemplateScript>> UpdateScriptAsync(
|
||||
int scriptId,
|
||||
TemplateScript proposed,
|
||||
@@ -517,6 +630,13 @@ public class TemplateService
|
||||
return Result<TemplateScript>.Success(existing);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a script definition from a template.
|
||||
/// </summary>
|
||||
/// <param name="scriptId">ID of the script to delete.</param>
|
||||
/// <param name="user">Username of the user deleting the script.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Result indicating success or failure.</returns>
|
||||
public async Task<Result<bool>> DeleteScriptAsync(
|
||||
int scriptId,
|
||||
string user,
|
||||
@@ -548,6 +668,15 @@ public class TemplateService
|
||||
// WP-6: Composition with Recursive Nesting
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Adds a composition (template slot) to a template with acyclicity and collision validation.
|
||||
/// </summary>
|
||||
/// <param name="templateId">ID of the parent template.</param>
|
||||
/// <param name="composedTemplateId">ID of the template to compose.</param>
|
||||
/// <param name="instanceName">Name for the composition slot.</param>
|
||||
/// <param name="user">Username of the user adding the composition.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Result containing the added composition or failure message.</returns>
|
||||
public async Task<Result<TemplateComposition>> AddCompositionAsync(
|
||||
int templateId,
|
||||
int composedTemplateId,
|
||||
@@ -655,6 +784,14 @@ public class TemplateService
|
||||
return composition;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renames a composition slot while preserving the composed template and nested slots.
|
||||
/// </summary>
|
||||
/// <param name="compositionId">ID of the composition to rename.</param>
|
||||
/// <param name="newInstanceName">New slot name.</param>
|
||||
/// <param name="user">Username of the user renaming the composition.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Result containing the updated composition or failure message.</returns>
|
||||
public async Task<Result<TemplateComposition>> RenameCompositionAsync(
|
||||
int compositionId,
|
||||
string newInstanceName,
|
||||
@@ -697,6 +834,13 @@ public class TemplateService
|
||||
return Result<TemplateComposition>.Success(composition);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a composition (template slot) and cascades deletion of slot-owned derived templates.
|
||||
/// </summary>
|
||||
/// <param name="compositionId">ID of the composition to delete.</param>
|
||||
/// <param name="user">Username of the user deleting the composition.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Result indicating success or failure.</returns>
|
||||
public async Task<Result<bool>> DeleteCompositionAsync(
|
||||
int compositionId,
|
||||
string user,
|
||||
@@ -807,6 +951,9 @@ public class TemplateService
|
||||
/// <summary>
|
||||
/// Resolves all effective members for a template using canonical (path-qualified) names.
|
||||
/// </summary>
|
||||
/// <param name="templateId">ID of the template to resolve.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>List of resolved template members with canonical names.</returns>
|
||||
public async Task<IReadOnlyList<TemplateResolver.ResolvedTemplateMember>> ResolveTemplateMembersAsync(
|
||||
int templateId,
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -823,6 +970,10 @@ public class TemplateService
|
||||
/// Validates whether overriding a member by canonical name is allowed.
|
||||
/// Used for composition overrides (WP-11) and inheritance overrides (WP-10).
|
||||
/// </summary>
|
||||
/// <param name="templateId">ID of the template.</param>
|
||||
/// <param name="canonicalName">Canonical name of the member to validate.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Result indicating whether override is allowed.</returns>
|
||||
public async Task<Result<bool>> ValidateOverrideAsync(
|
||||
int templateId,
|
||||
string canonicalName,
|
||||
@@ -852,6 +1003,9 @@ public class TemplateService
|
||||
/// <summary>
|
||||
/// Checks a template for naming collisions across all its members.
|
||||
/// </summary>
|
||||
/// <param name="templateId">ID of the template to check.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>List of collision error messages, or empty if none found.</returns>
|
||||
public async Task<IReadOnlyList<string>> DetectCollisionsAsync(
|
||||
int templateId,
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -871,6 +1025,11 @@ public class TemplateService
|
||||
/// <summary>
|
||||
/// Validates that a proposed inheritance or composition change does not create a cycle.
|
||||
/// </summary>
|
||||
/// <param name="templateId">ID of the template being modified.</param>
|
||||
/// <param name="proposedParentId">Proposed parent template ID for inheritance.</param>
|
||||
/// <param name="proposedComposedTemplateId">Proposed composed template ID for composition.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Result indicating whether the change would create a cycle.</returns>
|
||||
public async Task<Result<bool>> ValidateAcyclicityAsync(
|
||||
int templateId,
|
||||
int? proposedParentId,
|
||||
|
||||
@@ -57,6 +57,8 @@ internal static class CSharpDelimiterScanner
|
||||
/// sandbox; this check is advisory only.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="code">The C# source code to scan.</param>
|
||||
/// <param name="pattern">The substring to search for in code regions only.</param>
|
||||
internal static bool ContainsInCode(string code, string pattern)
|
||||
{
|
||||
if (string.IsNullOrEmpty(pattern))
|
||||
@@ -159,6 +161,7 @@ internal static class CSharpDelimiterScanner
|
||||
/// balanced. Delimiters inside comments, strings, and char literals are
|
||||
/// ignored.
|
||||
/// </summary>
|
||||
/// <param name="code">The C# source code to scan for delimiter balance.</param>
|
||||
internal static Mismatch Scan(string code)
|
||||
{
|
||||
int brace = 0, bracket = 0, paren = 0;
|
||||
|
||||
@@ -256,6 +256,10 @@ public class SemanticValidator
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a parameter definitions JSON string (JSON Schema or legacy flat array) and returns the declared parameter names.
|
||||
/// </summary>
|
||||
/// <param name="parameterDefinitionsJson">JSON Schema or legacy flat-array string; null/empty returns an empty list.</param>
|
||||
internal static List<string> ParseParameterDefinitions(string? parameterDefinitionsJson)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(parameterDefinitionsJson))
|
||||
@@ -292,6 +296,7 @@ public class SemanticValidator
|
||||
/// Extracts call targets from script code by simple pattern matching.
|
||||
/// Looks for CallScript("name", ...) and CallShared("name", ...) patterns.
|
||||
/// </summary>
|
||||
/// <param name="code">The script source code to scan.</param>
|
||||
internal static List<CallTarget> ExtractCallTargets(string code)
|
||||
{
|
||||
var results = new List<CallTarget>();
|
||||
@@ -389,8 +394,11 @@ public class SemanticValidator
|
||||
|
||||
internal record CallTarget
|
||||
{
|
||||
/// <summary>Name of the script being called.</summary>
|
||||
public string TargetName { get; init; } = string.Empty;
|
||||
/// <summary>True when the call is to a shared script via <c>CallShared</c>.</summary>
|
||||
public bool IsShared { get; init; }
|
||||
/// <summary>Number of non-name arguments passed to the call.</summary>
|
||||
public int ArgumentCount { get; init; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,11 @@ public class ValidationService
|
||||
private readonly SemanticValidator _semanticValidator;
|
||||
private readonly ScriptCompiler _scriptCompiler;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the ValidationService with the specified dependencies.
|
||||
/// </summary>
|
||||
/// <param name="semanticValidator">The semantic validator for configuration validation.</param>
|
||||
/// <param name="scriptCompiler">The script compiler for validating script code.</param>
|
||||
public ValidationService(SemanticValidator semanticValidator, ScriptCompiler scriptCompiler)
|
||||
{
|
||||
_semanticValidator = semanticValidator;
|
||||
@@ -38,6 +43,8 @@ public class ValidationService
|
||||
/// <summary>
|
||||
/// Runs the full validation pipeline on a flattened configuration.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The flattened configuration to validate.</param>
|
||||
/// <param name="sharedScripts">Optional list of shared scripts for validation context.</param>
|
||||
public ValidationResult Validate(FlattenedConfiguration configuration, IReadOnlyList<ResolvedScript>? sharedScripts = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
@@ -60,6 +67,7 @@ public class ValidationService
|
||||
/// <summary>
|
||||
/// Validates that flattening produced a non-empty configuration.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The flattened configuration to validate.</param>
|
||||
public static ValidationResult ValidateFlatteningSuccess(FlattenedConfiguration configuration)
|
||||
{
|
||||
var errors = new List<ValidationEntry>();
|
||||
@@ -88,6 +96,7 @@ public class ValidationService
|
||||
/// Validates that there are no naming collisions across entity types.
|
||||
/// Canonical names must be unique within their entity type (attributes, alarms, scripts).
|
||||
/// </summary>
|
||||
/// <param name="configuration">The flattened configuration to validate.</param>
|
||||
public static ValidationResult ValidateNamingCollisions(FlattenedConfiguration configuration)
|
||||
{
|
||||
var errors = new List<ValidationEntry>();
|
||||
@@ -104,6 +113,7 @@ public class ValidationService
|
||||
/// <summary>
|
||||
/// Validates that all scripts compile successfully using the ScriptCompiler.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The flattened configuration to validate.</param>
|
||||
public ValidationResult ValidateScriptCompilation(FlattenedConfiguration configuration)
|
||||
{
|
||||
var errors = new List<ValidationEntry>();
|
||||
@@ -127,6 +137,7 @@ public class ValidationService
|
||||
/// Validates that alarm trigger configurations reference existing attributes.
|
||||
/// Alarm trigger configs are JSON with an "attributeName" field referencing a canonical attribute name.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The flattened configuration to validate.</param>
|
||||
public static ValidationResult ValidateAlarmTriggerReferences(FlattenedConfiguration configuration)
|
||||
{
|
||||
var errors = new List<ValidationEntry>();
|
||||
@@ -155,6 +166,7 @@ public class ValidationService
|
||||
/// <summary>
|
||||
/// Validates that script trigger configurations reference existing attributes.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The flattened configuration to validate.</param>
|
||||
public static ValidationResult ValidateScriptTriggerReferences(FlattenedConfiguration configuration)
|
||||
{
|
||||
var errors = new List<ValidationEntry>();
|
||||
@@ -196,6 +208,7 @@ public class ValidationService
|
||||
/// <see cref="ValidateScriptTriggerReferences"/> for the structured triggers.</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
/// <param name="configuration">The flattened configuration to validate.</param>
|
||||
public static ValidationResult ValidateExpressionTriggers(FlattenedConfiguration configuration)
|
||||
{
|
||||
var errors = new List<ValidationEntry>();
|
||||
@@ -288,6 +301,7 @@ public class ValidationService
|
||||
/// Reads the "expression" string from a <c>{ "expression": "..." }</c> trigger
|
||||
/// configuration. Returns <c>null</c> on malformed JSON or a missing key.
|
||||
/// </summary>
|
||||
/// <param name="triggerConfigJson">The trigger configuration JSON to parse.</param>
|
||||
internal static string? ExtractExpressionFromTriggerConfig(string? triggerConfigJson)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(triggerConfigJson))
|
||||
@@ -315,6 +329,7 @@ public class ValidationService
|
||||
/// brackets/quotes. Returns an error message, or <c>null</c> when the expression
|
||||
/// looks well-formed.
|
||||
/// </summary>
|
||||
/// <param name="expression">The expression to check for syntax errors.</param>
|
||||
internal static string? CheckExpressionSyntax(string expression)
|
||||
{
|
||||
// Advisory forbidden-API scan (TemplateEngine-006): code-region-aware so
|
||||
@@ -423,6 +438,7 @@ public class ValidationService
|
||||
/// Best-effort: only matches double-quoted literals (the form the editor emits)
|
||||
/// and skips keys built dynamically.
|
||||
/// </summary>
|
||||
/// <param name="expression">The expression to scan for attribute references.</param>
|
||||
internal static IEnumerable<string> ExtractAttributeReferences(string expression)
|
||||
{
|
||||
var seen = new HashSet<string>(StringComparer.Ordinal);
|
||||
@@ -473,6 +489,7 @@ public class ValidationService
|
||||
/// <summary>
|
||||
/// Validates that all data-sourced attributes have connection bindings.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The flattened configuration to validate.</param>
|
||||
public static ValidationResult ValidateConnectionBindingCompleteness(FlattenedConfiguration configuration)
|
||||
{
|
||||
var errors = new List<ValidationEntry>();
|
||||
@@ -510,6 +527,10 @@ public class ValidationService
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the attribute name from a trigger configuration JSON.
|
||||
/// </summary>
|
||||
/// <param name="triggerConfigJson">The trigger configuration JSON to parse.</param>
|
||||
internal static string? ExtractAttributeNameFromTriggerConfig(string triggerConfigJson)
|
||||
{
|
||||
// Accept both keys to stay consistent with FlatteningService.PrefixTriggerAttribute,
|
||||
@@ -536,6 +557,7 @@ public class ValidationService
|
||||
/// all-nulls on malformed JSON — callers should treat that as "nothing to
|
||||
/// validate" and let other checks surface the deeper problem.
|
||||
/// </summary>
|
||||
/// <param name="triggerConfigJson">The trigger configuration JSON to parse.</param>
|
||||
internal static HiLoSetpoints ExtractHiLoSetpoints(string triggerConfigJson)
|
||||
{
|
||||
try
|
||||
|
||||
Reference in New Issue
Block a user