using System.Text.Json;
using System.Text.Json.Serialization;
namespace StellaOps.Infrastructure.Postgres.Migrations;
///
/// Represents a dependency between migrations in different modules.
///
public sealed record MigrationDependency
{
///
/// The module that has the dependency.
///
public required string Module { get; init; }
///
/// The migration file that has the dependency.
///
public required string Migration { get; init; }
///
/// The module being depended upon.
///
public required string DependsOnModule { get; init; }
///
/// The schema being depended upon.
///
public required string DependsOnSchema { get; init; }
///
/// The specific table or object being depended upon (optional).
///
public string? DependsOnObject { get; init; }
///
/// Whether this is a soft dependency (FK created conditionally).
///
public bool IsSoft { get; init; }
///
/// Description of why this dependency exists.
///
public string? Description { get; init; }
}
///
/// Module schema configuration for dependency resolution.
///
public sealed record ModuleSchemaConfig
{
///
/// The module name (e.g., "Authority", "Concelier").
///
public required string Module { get; init; }
///
/// The PostgreSQL schema name (e.g., "auth", "vuln").
///
public required string Schema { get; init; }
///
/// The WebService that owns this module's migrations.
///
public string? OwnerService { get; init; }
///
/// The assembly containing migrations for this module.
///
public string? MigrationAssembly { get; init; }
}
///
/// Registry of module schemas and their dependencies.
///
public sealed class ModuleDependencyRegistry
{
private readonly Dictionary _modules = new(StringComparer.OrdinalIgnoreCase);
private readonly List _dependencies = [];
///
/// Gets all registered modules.
///
public IReadOnlyDictionary Modules => _modules;
///
/// Gets all registered dependencies.
///
public IReadOnlyList Dependencies => _dependencies;
///
/// Registers a module schema configuration.
///
public ModuleDependencyRegistry RegisterModule(ModuleSchemaConfig config)
{
ArgumentNullException.ThrowIfNull(config);
_modules[config.Module] = config;
return this;
}
///
/// Registers a dependency between modules.
///
public ModuleDependencyRegistry RegisterDependency(MigrationDependency dependency)
{
ArgumentNullException.ThrowIfNull(dependency);
_dependencies.Add(dependency);
return this;
}
///
/// Gets the schema name for a module.
///
public string? GetSchemaForModule(string moduleName)
{
return _modules.TryGetValue(moduleName, out var config) ? config.Schema : null;
}
///
/// Gets the module name for a schema.
///
public string? GetModuleForSchema(string schemaName)
{
return _modules.Values
.FirstOrDefault(m => string.Equals(m.Schema, schemaName, StringComparison.OrdinalIgnoreCase))
?.Module;
}
///
/// Gets dependencies for a specific module.
///
public IReadOnlyList GetDependenciesForModule(string moduleName)
{
return _dependencies
.Where(d => string.Equals(d.Module, moduleName, StringComparison.OrdinalIgnoreCase))
.ToList();
}
///
/// Gets modules that depend on a specific module.
///
public IReadOnlyList GetDependentsOfModule(string moduleName)
{
return _dependencies
.Where(d => string.Equals(d.DependsOnModule, moduleName, StringComparison.OrdinalIgnoreCase))
.ToList();
}
///
/// Validates that all dependencies can be satisfied.
///
public IReadOnlyList ValidateDependencies()
{
var errors = new List();
foreach (var dep in _dependencies)
{
// Check that the dependent module exists
if (!_modules.ContainsKey(dep.Module))
{
errors.Add($"Unknown module '{dep.Module}' in dependency declaration.");
}
// Check that the target module exists
if (!_modules.ContainsKey(dep.DependsOnModule))
{
errors.Add($"Unknown dependency target module '{dep.DependsOnModule}' from '{dep.Module}'.");
}
// Check that the target schema matches
if (_modules.TryGetValue(dep.DependsOnModule, out var targetConfig))
{
if (!string.Equals(targetConfig.Schema, dep.DependsOnSchema, StringComparison.OrdinalIgnoreCase))
{
errors.Add(
$"Schema mismatch for dependency '{dep.Module}' -> '{dep.DependsOnModule}': " +
$"expected schema '{targetConfig.Schema}', got '{dep.DependsOnSchema}'.");
}
}
}
return errors;
}
///
/// Creates the default registry with all StellaOps modules.
///
public static ModuleDependencyRegistry CreateDefault()
{
var registry = new ModuleDependencyRegistry();
// Register all modules with their schemas
registry
.RegisterModule(new ModuleSchemaConfig { Module = "Authority", Schema = "auth", OwnerService = "Authority.WebService" })
.RegisterModule(new ModuleSchemaConfig { Module = "Concelier", Schema = "vuln", OwnerService = "Concelier.WebService" })
.RegisterModule(new ModuleSchemaConfig { Module = "Excititor", Schema = "vex", OwnerService = "Excititor.WebService" })
.RegisterModule(new ModuleSchemaConfig { Module = "Policy", Schema = "policy", OwnerService = "Policy.Gateway" })
.RegisterModule(new ModuleSchemaConfig { Module = "Scheduler", Schema = "scheduler", OwnerService = "Scheduler.WebService" })
.RegisterModule(new ModuleSchemaConfig { Module = "Notify", Schema = "notify", OwnerService = "Notify.WebService" })
.RegisterModule(new ModuleSchemaConfig { Module = "Scanner", Schema = "scanner", OwnerService = "Scanner.WebService" })
.RegisterModule(new ModuleSchemaConfig { Module = "Attestor", Schema = "proofchain", OwnerService = "Attestor.WebService" })
.RegisterModule(new ModuleSchemaConfig { Module = "Signer", Schema = "signer", OwnerService = "Signer.WebService" })
.RegisterModule(new ModuleSchemaConfig { Module = "Signals", Schema = "signals", OwnerService = "Signals" })
.RegisterModule(new ModuleSchemaConfig { Module = "EvidenceLocker", Schema = "evidence", OwnerService = "EvidenceLocker.WebService" })
.RegisterModule(new ModuleSchemaConfig { Module = "ExportCenter", Schema = "export", OwnerService = "ExportCenter.WebService" })
.RegisterModule(new ModuleSchemaConfig { Module = "IssuerDirectory", Schema = "issuer", OwnerService = "IssuerDirectory.WebService" })
.RegisterModule(new ModuleSchemaConfig { Module = "Orchestrator", Schema = "orchestrator", OwnerService = "Orchestrator.WebService" })
.RegisterModule(new ModuleSchemaConfig { Module = "Findings", Schema = "findings", OwnerService = "Findings.Ledger.WebService" })
.RegisterModule(new ModuleSchemaConfig { Module = "BinaryIndex", Schema = "binaries", OwnerService = "Scanner.WebService" })
.RegisterModule(new ModuleSchemaConfig { Module = "VexHub", Schema = "vexhub", OwnerService = "VexHub.WebService" })
.RegisterModule(new ModuleSchemaConfig { Module = "Unknowns", Schema = "unknowns", OwnerService = "Policy.Gateway" });
// Register known cross-module dependencies
registry
.RegisterDependency(new MigrationDependency
{
Module = "Signer",
Migration = "20251214000001_AddKeyManagementSchema.sql",
DependsOnModule = "Attestor",
DependsOnSchema = "proofchain",
DependsOnObject = "trust_anchors",
IsSoft = true,
Description = "Optional FK from signer.key_history to proofchain.trust_anchors"
})
.RegisterDependency(new MigrationDependency
{
Module = "Scanner",
Migration = "N/A",
DependsOnModule = "Concelier",
DependsOnSchema = "vuln",
IsSoft = true,
Description = "Scanner uses Concelier linksets for advisory data"
})
.RegisterDependency(new MigrationDependency
{
Module = "Policy",
Migration = "N/A",
DependsOnModule = "Concelier",
DependsOnSchema = "vuln",
IsSoft = true,
Description = "Policy uses vulnerability data from Concelier"
})
.RegisterDependency(new MigrationDependency
{
Module = "Policy",
Migration = "N/A",
DependsOnModule = "Excititor",
DependsOnSchema = "vex",
IsSoft = true,
Description = "Policy uses VEX data from Excititor"
});
return registry;
}
///
/// Serializes the registry to JSON.
///
public string ToJson()
{
var data = new
{
modules = _modules.Values.ToList(),
dependencies = _dependencies
};
return JsonSerializer.Serialize(data, new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
});
}
}