feat(m9/T32a): SharedSchema entity + EF config + idempotent migration + repository
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Entities.Schemas;
|
||||
|
||||
/// <summary>
|
||||
/// A reusable, named JSON-Schema library entry (M9 template-level JSON-Schema library,
|
||||
/// Task T32a). Schemas are referenced from inbound-API / template schema definitions via a
|
||||
/// <c>{"$ref":"lib:Name"}</c> pointer resolved against this library (the resolver lands in
|
||||
/// T32b). One row per named schema in the central <c>SharedSchemas</c> MS SQL table.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Persistence-ignorant POCO; the EF Core mapping lives in the Configuration Database
|
||||
/// component (<c>SharedSchemaConfiguration</c>). Mirrors the single-table
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Entities.Scripts.SharedScript"/> entity/config/
|
||||
/// repository shape — surrogate <c>Id</c>, a unique <see cref="Name"/>, and an unbounded
|
||||
/// text body.
|
||||
/// </remarks>
|
||||
public class SharedSchema
|
||||
{
|
||||
/// <summary>Primary key.</summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>Unique schema name used to reference this entry (e.g. <c>lib:Name</c>).</summary>
|
||||
public required string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional scope discriminator for future namespacing/visibility partitioning;
|
||||
/// <c>null</c> means an unscoped (global) library entry.
|
||||
/// </summary>
|
||||
public string? Scope { get; set; }
|
||||
|
||||
/// <summary>The JSON Schema document text.</summary>
|
||||
public required string SchemaJson { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Schemas;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Data access for the central <c>SharedSchemas</c> JSON-Schema library table
|
||||
/// (M9 template-level JSON-Schema library, Task T32a). One row per named schema; the
|
||||
/// unique <see cref="SharedSchema.Name"/> is the lookup key used by the <c>lib:Name</c>
|
||||
/// <c>$ref</c> resolver (T32b).
|
||||
/// </summary>
|
||||
public interface ISharedSchemaRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Inserts <paramref name="schema"/> and returns the store-generated
|
||||
/// <see cref="SharedSchema.Id"/>.
|
||||
/// </summary>
|
||||
/// <param name="schema">The schema to persist.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>A task that resolves to the generated identity of the inserted row.</returns>
|
||||
Task<int> AddAsync(SharedSchema schema, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the row for the given id, or <c>null</c> if none exists.
|
||||
/// </summary>
|
||||
/// <param name="id">The identity to look up.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>A task that resolves to the matching <see cref="SharedSchema"/>, or <c>null</c>.</returns>
|
||||
Task<SharedSchema?> GetByIdAsync(int id, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the row whose <see cref="SharedSchema.Name"/> matches
|
||||
/// <paramref name="name"/>, or <c>null</c> if none exists.
|
||||
/// </summary>
|
||||
/// <param name="name">The unique schema name to look up.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>A task that resolves to the matching <see cref="SharedSchema"/>, or <c>null</c>.</returns>
|
||||
Task<SharedSchema?> GetByNameAsync(string name, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Returns all schemas ordered by <see cref="SharedSchema.Name"/>.
|
||||
/// </summary>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>A task that resolves to every library entry, name-ordered.</returns>
|
||||
Task<IReadOnlyList<SharedSchema>> ListAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Persists the current state of <paramref name="schema"/> (matched by
|
||||
/// <see cref="SharedSchema.Id"/>).
|
||||
/// </summary>
|
||||
/// <param name="schema">The entity whose changes to persist.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
Task UpdateAsync(SharedSchema schema, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the row with the given id; a no-op if none exists.
|
||||
/// </summary>
|
||||
/// <param name="id">The identity to delete.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
Task DeleteAsync(int id, CancellationToken ct = default);
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Schemas;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Configurations;
|
||||
|
||||
/// <summary>
|
||||
/// Maps the <see cref="SharedSchema"/> entity (M9 template-level JSON-Schema library,
|
||||
/// Task T32a). Mirrors <c>SharedScriptConfiguration</c>: surrogate key, a UNIQUE index on
|
||||
/// <see cref="SharedSchema.Name"/>, a bounded <see cref="SharedSchema.Scope"/>, and an
|
||||
/// unbounded (<c>nvarchar(max)</c>) <see cref="SharedSchema.SchemaJson"/> body.
|
||||
/// Auto-discovered by <c>ApplyConfigurationsFromAssembly</c> in <c>ScadaBridgeDbContext</c>.
|
||||
/// </summary>
|
||||
public class SharedSchemaConfiguration : IEntityTypeConfiguration<SharedSchema>
|
||||
{
|
||||
/// <summary>Configures the EF Core mapping for the <see cref="SharedSchema"/> entity.</summary>
|
||||
/// <param name="builder">Entity type builder used to apply the configuration.</param>
|
||||
public void Configure(EntityTypeBuilder<SharedSchema> builder)
|
||||
{
|
||||
builder.HasKey(s => s.Id);
|
||||
|
||||
builder.Property(s => s.Name)
|
||||
.IsRequired()
|
||||
.HasMaxLength(200);
|
||||
|
||||
builder.Property(s => s.Scope)
|
||||
.HasMaxLength(200);
|
||||
|
||||
// Unbounded JSON Schema document body (nvarchar(max)) — no length cap, like SharedScript.Code.
|
||||
builder.Property(s => s.SchemaJson)
|
||||
.IsRequired();
|
||||
|
||||
builder.HasIndex(s => s.Name).IsUnique();
|
||||
}
|
||||
}
|
||||
+1906
File diff suppressed because it is too large
Load Diff
+42
@@ -0,0 +1,42 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddSharedSchema : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SharedSchemas",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
Name = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
|
||||
Scope = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
|
||||
SchemaJson = table.Column<string>(type: "nvarchar(max)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SharedSchemas", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SharedSchemas_Name",
|
||||
table: "SharedSchemas",
|
||||
column: "Name",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "SharedSchemas");
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
@@ -894,6 +894,35 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
||||
b.ToTable("SmtpConfigurations");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Schemas.SharedSchema", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("SchemaJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Scope")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("SharedSchemas");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ZB.MOM.WW.ScadaBridge.Commons.Entities.Scripts.SharedScript", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Schemas;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// EF Core implementation of <see cref="ISharedSchemaRepository"/> over the central
|
||||
/// <c>SharedSchemas</c> JSON-Schema library table (M9, Task T32a). Plain tracked EF
|
||||
/// reads/writes against the shared <see cref="ScadaBridgeDbContext"/>, saving on each
|
||||
/// mutating call — mirrors the <c>SecuredWriteRepository</c> data-access shape.
|
||||
/// </summary>
|
||||
public class SharedSchemaRepository : ISharedSchemaRepository
|
||||
{
|
||||
private readonly ScadaBridgeDbContext _context;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SharedSchemaRepository"/> class.
|
||||
/// </summary>
|
||||
/// <param name="context">The EF Core database context.</param>
|
||||
public SharedSchemaRepository(ScadaBridgeDbContext context)
|
||||
{
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<int> AddAsync(SharedSchema schema, CancellationToken ct = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(schema);
|
||||
|
||||
await _context.Set<SharedSchema>().AddAsync(schema, ct);
|
||||
await _context.SaveChangesAsync(ct);
|
||||
return schema.Id;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<SharedSchema?> GetByIdAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
return await _context.Set<SharedSchema>().FindAsync([id], ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<SharedSchema?> GetByNameAsync(string name, CancellationToken ct = default)
|
||||
{
|
||||
return await _context.Set<SharedSchema>()
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(s => s.Name == name, ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<SharedSchema>> ListAsync(CancellationToken ct = default)
|
||||
{
|
||||
return await _context.Set<SharedSchema>()
|
||||
.AsNoTracking()
|
||||
.OrderBy(s => s.Name)
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task UpdateAsync(SharedSchema schema, CancellationToken ct = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(schema);
|
||||
|
||||
_context.Set<SharedSchema>().Update(schema);
|
||||
await _context.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
var entity = await _context.Set<SharedSchema>().FindAsync([id], ct);
|
||||
if (entity is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_context.Set<SharedSchema>().Remove(entity);
|
||||
await _context.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Entities.InboundApi;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Kpi;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Schemas;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Scripts;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.SecuredWrites;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Security;
|
||||
@@ -111,6 +112,10 @@ public class ScadaBridgeDbContext : DbContext, IDataProtectionKeyContext
|
||||
/// <summary>Gets the set of shared scripts.</summary>
|
||||
public DbSet<SharedScript> SharedScripts => Set<SharedScript>();
|
||||
|
||||
// Schemas (M9 template-level JSON-Schema library, T32a)
|
||||
/// <summary>Gets the set of shared JSON-Schema library entries.</summary>
|
||||
public DbSet<SharedSchema> SharedSchemas => Set<SharedSchema>();
|
||||
|
||||
// Security
|
||||
/// <summary>Gets the set of LDAP group mappings.</summary>
|
||||
public DbSet<LdapGroupMapping> LdapGroupMappings => Set<LdapGroupMapping>();
|
||||
|
||||
@@ -55,6 +55,7 @@ public static class ServiceCollectionExtensions
|
||||
services.AddScoped<IAuditLogRepository, AuditLogRepository>();
|
||||
services.AddScoped<ISiteCallAuditRepository, SiteCallAuditRepository>();
|
||||
services.AddScoped<ISecuredWriteRepository, SecuredWriteRepository>();
|
||||
services.AddScoped<ISharedSchemaRepository, SharedSchemaRepository>();
|
||||
services.AddScoped<IKpiHistoryRepository, KpiHistoryRepository>();
|
||||
// Auth re-arch (C5): inbound API keys are no longer persisted in SQL Server —
|
||||
// the repository now exposes only API-method access, so a plain scoped
|
||||
|
||||
Reference in New Issue
Block a user