feat(m9/T32c): schema-library CRUD commands + handlers + Central UI page + read-accessor
This commit is contained in:
@@ -12,6 +12,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.Entities.Notifications;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Schemas;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.SecuredWrites;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Security;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
|
||||
@@ -194,6 +195,7 @@ public class ManagementActor : ReceiveActor
|
||||
or AddTemplateScriptCommand or UpdateTemplateScriptCommand or DeleteTemplateScriptCommand
|
||||
or AddTemplateCompositionCommand or DeleteTemplateCompositionCommand
|
||||
or CreateSharedScriptCommand or UpdateSharedScriptCommand or DeleteSharedScriptCommand
|
||||
or CreateSharedSchemaCommand or UpdateSharedSchemaCommand or DeleteSharedSchemaCommand
|
||||
or CreateDatabaseConnectionDefCommand or UpdateDatabaseConnectionDefCommand or DeleteDatabaseConnectionDefCommand
|
||||
or CreateApiMethodCommand or UpdateApiMethodCommand or DeleteApiMethodCommand
|
||||
or UpdateAreaCommand
|
||||
@@ -337,6 +339,13 @@ public class ManagementActor : ReceiveActor
|
||||
UpdateSharedScriptCommand cmd => await HandleUpdateSharedScript(sp, cmd, user.Username),
|
||||
DeleteSharedScriptCommand cmd => await HandleDeleteSharedScript(sp, cmd, user.Username),
|
||||
|
||||
// Schema Library (M9-T32c)
|
||||
ListSharedSchemasCommand => await HandleListSharedSchemas(sp),
|
||||
GetSharedSchemaCommand cmd => await HandleGetSharedSchema(sp, cmd),
|
||||
CreateSharedSchemaCommand cmd => await HandleCreateSharedSchema(sp, cmd, user.Username),
|
||||
UpdateSharedSchemaCommand cmd => await HandleUpdateSharedSchema(sp, cmd, user.Username),
|
||||
DeleteSharedSchemaCommand cmd => await HandleDeleteSharedSchema(sp, cmd, user.Username),
|
||||
|
||||
// Database Connections (External System)
|
||||
ListDatabaseConnectionsCommand => await HandleListDatabaseConnections(sp),
|
||||
GetDatabaseConnectionCommand cmd => await HandleGetDatabaseConnection(sp, cmd),
|
||||
@@ -2214,6 +2223,131 @@ public class ManagementActor : ReceiveActor
|
||||
return result.IsSuccess ? result.Value : throw new ManagementCommandException(result.Error);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Schema Library handlers (M9-T32c)
|
||||
//
|
||||
// Reusable named JSON-Schema library (SharedSchema + ISharedSchemaRepository,
|
||||
// T32a). Repo-backed, mirroring the DatabaseConnection handler idiom: direct
|
||||
// repository writes plus an explicit AuditAsync after each mutation. The unique
|
||||
// Name is enforced with a clear duplicate-name error and the SchemaJson is
|
||||
// validated as a parseable JSON Schema (reusing InboundApiSchema.Parse) so a
|
||||
// malformed library entry never reaches the $ref resolver (T32b) or the
|
||||
// schema-driven value-entry forms (T30).
|
||||
// ========================================================================
|
||||
|
||||
private static async Task<object?> HandleListSharedSchemas(IServiceProvider sp)
|
||||
{
|
||||
var repo = sp.GetRequiredService<ISharedSchemaRepository>();
|
||||
return await repo.ListAsync();
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleGetSharedSchema(IServiceProvider sp, GetSharedSchemaCommand cmd)
|
||||
{
|
||||
var repo = sp.GetRequiredService<ISharedSchemaRepository>();
|
||||
return await repo.GetByIdAsync(cmd.SharedSchemaId);
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleCreateSharedSchema(IServiceProvider sp, CreateSharedSchemaCommand cmd, string user)
|
||||
{
|
||||
var repo = sp.GetRequiredService<ISharedSchemaRepository>();
|
||||
var name = ValidateSchemaName(cmd.Name);
|
||||
ValidateSchemaJson(cmd.SchemaJson);
|
||||
|
||||
var existing = await repo.GetByNameAsync(name);
|
||||
if (existing is not null)
|
||||
{
|
||||
throw new ManagementCommandException($"A schema named '{name}' already exists.");
|
||||
}
|
||||
|
||||
var schema = new SharedSchema { Name = name, Scope = cmd.Scope, SchemaJson = cmd.SchemaJson };
|
||||
await repo.AddAsync(schema);
|
||||
await AuditAsync(sp, user, "Create", "SharedSchema", schema.Id.ToString(), schema.Name, new { schema.Id, schema.Name, schema.Scope });
|
||||
return schema;
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleUpdateSharedSchema(IServiceProvider sp, UpdateSharedSchemaCommand cmd, string user)
|
||||
{
|
||||
var repo = sp.GetRequiredService<ISharedSchemaRepository>();
|
||||
var name = ValidateSchemaName(cmd.Name);
|
||||
ValidateSchemaJson(cmd.SchemaJson);
|
||||
|
||||
var schema = await repo.GetByIdAsync(cmd.SharedSchemaId)
|
||||
?? throw new ManagementCommandException($"Schema with ID {cmd.SharedSchemaId} not found.");
|
||||
|
||||
// A rename may not collide with a DIFFERENT row's name. The same name on the
|
||||
// same row is allowed (a schema-body-only edit keeps its name).
|
||||
var byName = await repo.GetByNameAsync(name);
|
||||
if (byName is not null && byName.Id != schema.Id)
|
||||
{
|
||||
throw new ManagementCommandException($"A schema named '{name}' already exists.");
|
||||
}
|
||||
|
||||
schema.Name = name;
|
||||
schema.Scope = cmd.Scope;
|
||||
schema.SchemaJson = cmd.SchemaJson;
|
||||
await repo.UpdateAsync(schema);
|
||||
await AuditAsync(sp, user, "Update", "SharedSchema", schema.Id.ToString(), schema.Name, new { schema.Id, schema.Name, schema.Scope });
|
||||
return schema;
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleDeleteSharedSchema(IServiceProvider sp, DeleteSharedSchemaCommand cmd, string user)
|
||||
{
|
||||
var repo = sp.GetRequiredService<ISharedSchemaRepository>();
|
||||
await repo.DeleteAsync(cmd.SharedSchemaId);
|
||||
await AuditAsync(sp, user, "Delete", "SharedSchema", cmd.SharedSchemaId.ToString(), cmd.SharedSchemaId.ToString(), null);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates and trims a library schema name. A non-empty unique name is the
|
||||
/// lookup key for the <c>lib:Name</c> <c>$ref</c> resolver, so an empty name is a
|
||||
/// caller-safe validation error (surfaced verbatim via
|
||||
/// <see cref="ManagementCommandException"/>).
|
||||
/// </summary>
|
||||
private static string ValidateSchemaName(string? rawName)
|
||||
{
|
||||
var name = rawName?.Trim();
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
throw new ManagementCommandException("Schema name is required.");
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that <paramref name="schemaJson"/> parses as a JSON Schema, reusing the
|
||||
/// authoritative <see cref="InboundApiSchema.ParseWithRefs"/> parser so a library
|
||||
/// entry obeys the same STRUCTURAL rules as every other stored schema (object form or
|
||||
/// legacy flat-array; over-depth and root-scalar inputs rejected). A structural parse
|
||||
/// failure is re-thrown as a caller-safe <see cref="ManagementCommandException"/>
|
||||
/// naming the fault.
|
||||
/// <para>
|
||||
/// The <c>ParseWithRefs</c> (collecting) variant is used deliberately so a library
|
||||
/// entry that itself carries a <c>{"$ref":"lib:Other"}</c> pointer is NOT rejected at
|
||||
/// save time merely because the resolver is not consulted here — a dangling/cyclic
|
||||
/// ref surfaces later on the deploy-time / runtime resolution path (T32b), exactly
|
||||
/// like a forward reference to a not-yet-created entry. Only malformed JSON is a
|
||||
/// save-blocking error.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private static void ValidateSchemaJson(string? schemaJson)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(schemaJson))
|
||||
{
|
||||
throw new ManagementCommandException("Schema JSON is required.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_ = InboundApiSchema.ParseWithRefs(schemaJson, resolveRef: null);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
throw new ManagementCommandException($"Schema JSON is not a valid JSON Schema: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Database Connection Definition handlers
|
||||
// ========================================================================
|
||||
|
||||
Reference in New Issue
Block a user