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 }); } }