diff --git a/Core/Resgrid.Model/Repositories/IDocumentDbRepository.cs b/Core/Resgrid.Model/Repositories/IDocumentDbRepository.cs new file mode 100644 index 000000000..72ff7cb5d --- /dev/null +++ b/Core/Resgrid.Model/Repositories/IDocumentDbRepository.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; + +namespace Resgrid.Model.Repositories +{ + /// + /// Interface IDocumentDbRepository + /// + public interface IDocumentDbRepository + { + /// + /// Updates the Postgres document database schema. + /// + /// If the operation was successful + Task UpdateDocumentDatabaseAsync(); + } +} diff --git a/Providers/Resgrid.Providers.MigrationsPg/Sql/EF0003_PopulateDocDb.sql b/Providers/Resgrid.Providers.MigrationsPg/Sql/EF0003_PopulateDocDb.sql index f2ac3af0e..9ace3f60e 100644 --- a/Providers/Resgrid.Providers.MigrationsPg/Sql/EF0003_PopulateDocDb.sql +++ b/Providers/Resgrid.Providers.MigrationsPg/Sql/EF0003_PopulateDocDb.sql @@ -41,7 +41,7 @@ END IF; CREATE TABLE IF NOT EXISTS public.maplayers( id serial, departmentid integer, - oid text + oid text, data jsonb NOT NULL ); diff --git a/Repositories/Resgrid.Repositories.NoSqlRepository/DocumentDbRepository.cs b/Repositories/Resgrid.Repositories.NoSqlRepository/DocumentDbRepository.cs new file mode 100644 index 000000000..d5990ad14 --- /dev/null +++ b/Repositories/Resgrid.Repositories.NoSqlRepository/DocumentDbRepository.cs @@ -0,0 +1,62 @@ +using Npgsql; +using Resgrid.Config; +using Resgrid.Model.Repositories; +using System; +using System.IO; +using System.Reflection; +using System.Threading.Tasks; + +namespace Resgrid.Repositories.NoSqlRepository +{ + public class DocumentDbRepository : IDocumentDbRepository + { + public async Task UpdateDocumentDatabaseAsync() + { + try + { + if (DataConfig.DocDatabaseType != DatabaseTypes.Postgres) + return true; + + if (string.IsNullOrWhiteSpace(DataConfig.DocumentConnectionString)) + throw new InvalidOperationException("DocumentConnectionString is required when DocDatabaseType is Postgres."); + + var assembly = Assembly.Load("Resgrid.Providers.MigrationsPg"); + const string resourceName = "Resgrid.Providers.MigrationsPg.Sql.EF0003_PopulateDocDb.sql"; + + using Stream stream = assembly.GetManifestResourceStream(resourceName) + ?? throw new InvalidOperationException($"Unable to find document database migration resource '{resourceName}'."); + using StreamReader reader = new StreamReader(stream); + + string migrationScript = await reader.ReadToEndAsync(); + + if (string.IsNullOrWhiteSpace(migrationScript)) + throw new InvalidOperationException("Document database migration script is empty."); + + await using var conn = new NpgsqlConnection(DataConfig.DocumentConnectionString); + await using var cmd = conn.CreateCommand(); + await conn.OpenAsync(); + await using var tran = await conn.BeginTransactionAsync(); + + try + { + cmd.Transaction = tran; + cmd.CommandText = migrationScript; + await cmd.ExecuteNonQueryAsync(); + await tran.CommitAsync(); + } + catch + { + await tran.RollbackAsync(); + throw; + } + + return true; + } + catch (Exception ex) + { + Framework.Logging.LogException(ex); + return false; + } + } + } +} diff --git a/Repositories/Resgrid.Repositories.NoSqlRepository/NoSqlDataModule.cs b/Repositories/Resgrid.Repositories.NoSqlRepository/NoSqlDataModule.cs index 6543844d8..085502ca9 100644 --- a/Repositories/Resgrid.Repositories.NoSqlRepository/NoSqlDataModule.cs +++ b/Repositories/Resgrid.Repositories.NoSqlRepository/NoSqlDataModule.cs @@ -10,6 +10,7 @@ protected override void Load(ContainerBuilder builder) { builder.RegisterGeneric(typeof(MongoRepository<>)).As(typeof(IMongoRepository<>)).InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); diff --git a/Tools/Resgrid.Console/Commands/DbUpdateCommand.cs b/Tools/Resgrid.Console/Commands/DbUpdateCommand.cs index 53d791965..7c4c0dbce 100644 --- a/Tools/Resgrid.Console/Commands/DbUpdateCommand.cs +++ b/Tools/Resgrid.Console/Commands/DbUpdateCommand.cs @@ -21,7 +21,8 @@ namespace Resgrid.Console.Commands public sealed class DbUpdateCommand( IConfiguration configuration, ILogger logger, - IMigrationRunner migrationRunner) : ICommandService + IMigrationRunner migrationRunner, + IDocumentDbRepository documentDbRepository) : ICommandService { /// /// Executes the main functionality of the application. @@ -38,6 +39,17 @@ public async Task ExecuteMainAsync(string[] args, CancellationToken ca { migrationRunner.MigrateUp(); + if (Config.DataConfig.DocDatabaseType == Config.DatabaseTypes.Postgres) + { + var result = await documentDbRepository.UpdateDocumentDatabaseAsync(); + + if (!result) + { + logger.LogError("Postgres document database update did not complete successfully."); + return ExitCode.Failed; + } + } + logger.LogInformation("Completed updating the Resgrid Database!"); } catch (Exception ex) diff --git a/Tools/Resgrid.Console/Commands/MigrateDocsDbCommand.cs b/Tools/Resgrid.Console/Commands/MigrateDocsDbCommand.cs index a82f8b297..a55f869fc 100644 --- a/Tools/Resgrid.Console/Commands/MigrateDocsDbCommand.cs +++ b/Tools/Resgrid.Console/Commands/MigrateDocsDbCommand.cs @@ -21,6 +21,7 @@ namespace Resgrid.Console.Commands public sealed class MigrateDocsDbCommand( IConfiguration configuration, ILogger logger, + IDocumentDbRepository documentDbRepository, IMongoRepository mapLayersRepository, IMongoRepository unitsLocationRepository, IMongoRepository personnelLocationRepository, @@ -41,6 +42,19 @@ public async Task ExecuteMainAsync(string[] args, CancellationToken ca try { + if (Config.DataConfig.DocDatabaseType == Config.DatabaseTypes.Postgres) + { + logger.LogInformation("Ensuring Postgres document tables exist..."); + + var schemaUpdated = await documentDbRepository.UpdateDocumentDatabaseAsync(); + + if (!schemaUpdated) + { + logger.LogError("Failed to update the Postgres document database schema."); + return ExitCode.Failed; + } + } + logger.LogInformation("Migrating Map Layers..."); var layers = mapLayersRepository.AsQueryable().ToList(); diff --git a/Workers/Resgrid.Workers.Console/Program.cs b/Workers/Resgrid.Workers.Console/Program.cs index eb0afb3f8..b903f6c90 100644 --- a/Workers/Resgrid.Workers.Console/Program.cs +++ b/Workers/Resgrid.Workers.Console/Program.cs @@ -71,14 +71,17 @@ static async Task Main(string[] args) services.AddOptions(); var upgradeDatabase = Environment.GetEnvironmentVariable("RESGRID__DODBUPGRADE"); + var runDatabaseUpgrade = !String.IsNullOrWhiteSpace(upgradeDatabase) && upgradeDatabase.ToLower() == "true"; - if (!String.IsNullOrWhiteSpace(upgradeDatabase) && upgradeDatabase.ToLower() == "true") + if (runDatabaseUpgrade) { services.AddSingleton(); } - - services.AddSingleton(); - services.AddSingleton(); + else + { + services.AddSingleton(); + services.AddSingleton(); + } }) .ConfigureLogging((hostingContext, logging) => { @@ -411,10 +414,12 @@ await Client.ScheduleAsync("Weather Alert Import", public class DatabaseUpgradeService : BackgroundService { private ILogger _logger; + private readonly IHostApplicationLifetime _hostApplicationLifetime; - public DatabaseUpgradeService(ILogger logger) + public DatabaseUpgradeService(ILogger logger, IHostApplicationLifetime hostApplicationLifetime) { _logger = logger; + _hostApplicationLifetime = hostApplicationLifetime; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) @@ -434,9 +439,11 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) { UpdateDatabase(scope.ServiceProvider); await UpdateOidcDatabaseAsync(logger, scope.ServiceProvider); + await UpdateDocumentDatabaseAsync(logger, scope.ServiceProvider); } _logger.Log(LogLevel.Information, "Completed updating the Resgrid Database!"); + _hostApplicationLifetime.StopApplication(); } catch (Exception ex) { @@ -482,6 +489,33 @@ private static async Task UpdateOidcDatabaseAsync(ILogger logger, IServ } } + /// + /// Update the document database + /// + private static async Task UpdateDocumentDatabaseAsync(ILogger logger, IServiceProvider serviceProvider) + { + if (Config.DataConfig.DocDatabaseType != Config.DatabaseTypes.Postgres) + return; + + logger.Log(LogLevel.Information, "Starting Document Database Upgrade"); + + try + { + var documentDbRepository = Bootstrapper.GetKernel().Resolve(); + bool result = await documentDbRepository.UpdateDocumentDatabaseAsync(); + + if (result) + logger.Log(LogLevel.Information, "Completed updating the Document Database!"); + else + throw new InvalidOperationException("UpdateDocumentDatabaseAsync returned false; the document database was not fully updated."); + } + catch (Exception ex) + { + logger.Log(LogLevel.Error, ex, "There was an error trying to update the Document Database."); + throw; + } + } + private static IServiceProvider CreateServices() { if (Config.DataConfig.DatabaseType == Config.DatabaseTypes.Postgres)