Files
ScadaBridge/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/SharedScriptService.cs
T
Joseph Doherty 7b0b9c7365 refactor: rename ScadaLink → ZB.MOM.WW.ScadaBridge (code + projects + namespaces)
Solution + 23 src projects + 26 test projects renamed; folders, csproj,
namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated.
ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated.
SQL roles/logins, LDAP domains, CLI command name, and CLI config dir
(~/.scadalink → ~/.scadabridge) also renamed.

Build green; 5 Host.Tests fail awaiting SQL login rename in next commit.
Pre-existing StaleTagMonitor timing flakes unchanged.

Rename script committed at tools/rename-to-scadabridge.sh.
2026-05-28 09:37:45 -04:00

208 lines
9.8 KiB
C#

using ZB.MOM.WW.ScadaBridge.Commons.Entities.Scripts;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Types;
namespace ZB.MOM.WW.ScadaBridge.TemplateEngine;
/// <summary>
/// WP-5: Shared Script CRUD.
/// System-wide scripts not associated with templates.
/// Same parameter/return definition structure as template scripts.
/// Includes syntax/structural validation (basic C# compilation check).
/// </summary>
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,
string? parameterDefinitions,
string? returnDefinition,
string user,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(name))
return Result<SharedScript>.Failure("Shared script name is required.");
if (string.IsNullOrWhiteSpace(code))
return Result<SharedScript>.Failure("Shared script code is required.");
// Check unique name
var existing = await _repository.GetSharedScriptByNameAsync(name, cancellationToken);
if (existing != null)
return Result<SharedScript>.Failure($"A shared script named '{name}' already exists.");
// Syntax/structural validation
var syntaxError = ValidateSyntax(code);
if (syntaxError != null)
return Result<SharedScript>.Failure(syntaxError);
var script = new SharedScript(name, code)
{
ParameterDefinitions = parameterDefinitions,
ReturnDefinition = returnDefinition
};
// TemplateEngine-020: save the entity first so EF Core populates the
// auto-generated key, then write the audit row with the real
// script.Id, then save the audit row. The pre-fix order logged
// EntityId = "0" because the audit row was queued before
// SaveChangesAsync ran.
await _repository.AddSharedScriptAsync(script, cancellationToken);
await _repository.SaveChangesAsync(cancellationToken);
await _auditService.LogAsync(user, "Create", "SharedScript", script.Id.ToString(), name, script, cancellationToken);
await _repository.SaveChangesAsync(cancellationToken);
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,
string? parameterDefinitions,
string? returnDefinition,
string user,
CancellationToken cancellationToken = default)
{
var script = await _repository.GetSharedScriptByIdAsync(scriptId, cancellationToken);
if (script == null)
return Result<SharedScript>.Failure($"Shared script with ID {scriptId} not found.");
if (string.IsNullOrWhiteSpace(code))
return Result<SharedScript>.Failure("Shared script code is required.");
// Syntax/structural validation
var syntaxError = ValidateSyntax(code);
if (syntaxError != null)
return Result<SharedScript>.Failure(syntaxError);
script.Code = code;
script.ParameterDefinitions = parameterDefinitions;
script.ReturnDefinition = returnDefinition;
await _repository.UpdateSharedScriptAsync(script, cancellationToken);
await _auditService.LogAsync(user, "Update", "SharedScript", scriptId.ToString(), script.Name, script, cancellationToken);
await _repository.SaveChangesAsync(cancellationToken);
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,
CancellationToken cancellationToken = default)
{
var script = await _repository.GetSharedScriptByIdAsync(scriptId, cancellationToken);
if (script == null)
return Result<bool>.Failure($"Shared script with ID {scriptId} not found.");
await _repository.DeleteSharedScriptAsync(scriptId, cancellationToken);
await _auditService.LogAsync(user, "Delete", "SharedScript", scriptId.ToString(), script.Name, null, cancellationToken);
await _repository.SaveChangesAsync(cancellationToken);
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);
}
/// <summary>
/// Basic structural validation of C# script code.
/// Checks for balanced braces/brackets/parentheses. The scan is string- and
/// comment-aware (see <see cref="Validation.CSharpDelimiterScanner"/>) so a
/// delimiter inside a regular/verbatim/interpolated/raw string literal, a
/// 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))
return "Script code cannot be empty.";
return Validation.CSharpDelimiterScanner.Scan(code) switch
{
Validation.CSharpDelimiterScanner.Mismatch.None => null,
Validation.CSharpDelimiterScanner.Mismatch.UnexpectedCloseBrace =>
"Syntax error: unmatched closing brace '}'.",
Validation.CSharpDelimiterScanner.Mismatch.UnclosedBrace =>
"Syntax error: unmatched opening brace '{'.",
Validation.CSharpDelimiterScanner.Mismatch.UnexpectedCloseBracket =>
"Syntax error: unmatched closing bracket ']'.",
Validation.CSharpDelimiterScanner.Mismatch.UnclosedBracket =>
"Syntax error: unmatched opening bracket '['.",
Validation.CSharpDelimiterScanner.Mismatch.UnexpectedCloseParen =>
"Syntax error: unmatched closing parenthesis ')'.",
Validation.CSharpDelimiterScanner.Mismatch.UnclosedParen =>
"Syntax error: unmatched opening parenthesis '('.",
Validation.CSharpDelimiterScanner.Mismatch.UnclosedBlockComment =>
"Syntax error: unclosed block comment.",
Validation.CSharpDelimiterScanner.Mismatch.UnterminatedString =>
"Syntax error: unterminated string literal.",
Validation.CSharpDelimiterScanner.Mismatch.UnterminatedChar =>
"Syntax error: unterminated character literal.",
_ => null,
};
}
}