mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-10-15 10:04:44 +00:00
Compare commits
2 Commits
5adcb55664
...
cc7a6486d1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc7a6486d1 | ||
|
|
f6be2cf9a7 |
@@ -7,215 +7,211 @@ using Microsoft.Data.SqlClient;
|
|||||||
namespace Database.Migrations;
|
namespace Database.Migrations;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Entry point for the database migration runner. Reads connection details from
|
/// Entry point for the database migration runner. Reads connection details from environment
|
||||||
/// environment variables, optionally clears and recreates the target database, and
|
/// variables, optionally drops and recreates the target database, and applies all SQL
|
||||||
/// applies all SQL migration scripts embedded in this assembly using DbUp.
|
/// migration scripts embedded in this assembly using DbUp.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class Program
|
public static class Program
|
||||||
{
|
{
|
||||||
/// <summary>The connection string for the target application database (<c>DB_NAME</c>).</summary>
|
/// <summary>
|
||||||
private static readonly string connectionString = BuildConnectionString();
|
/// Migration runner entry point. Drops the target database when <c>CLEAR_DATABASE</c> is
|
||||||
|
/// set to <c>true</c>, ensures the database exists, then deploys all pending migrations.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// <c>0</c> if migrations completed successfully; <c>1</c> if they failed or an error occurred.
|
||||||
|
/// </returns>
|
||||||
|
public static async Task<int> Main()
|
||||||
|
{
|
||||||
|
Console.WriteLine("Starting database migrations...");
|
||||||
|
|
||||||
/// <summary>The connection string for the <c>master</c> database, used for create/drop operations.</summary>
|
try
|
||||||
private static readonly string masterConnectionString = BuildConnectionString("master");
|
{
|
||||||
|
DatabaseSettings settings = DatabaseSettings.FromEnvironment();
|
||||||
|
|
||||||
|
if (settings.ClearDatabase)
|
||||||
|
{
|
||||||
|
Console.WriteLine(
|
||||||
|
$"CLEAR_DATABASE is enabled. Dropping database '{settings.DatabaseName}'..."
|
||||||
|
);
|
||||||
|
await DropDatabaseIfExistsAsync(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
await CreateDatabaseIfNotExistsAsync(settings);
|
||||||
|
|
||||||
|
return DeployMigrations(settings.ConnectionString) ? 0 : 1;
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is InvalidOperationException or SqlException)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("An error occurred during database migrations:");
|
||||||
|
Console.Error.WriteLine(ex.Message);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Builds a SQL Server connection string from the <c>DB_SERVER</c>, <c>DB_NAME</c>,
|
/// Drops the target database if it exists, first forcing it into single-user mode with
|
||||||
/// <c>DB_USER</c>, <c>DB_PASSWORD</c>, and <c>DB_TRUST_SERVER_CERTIFICATE</c>
|
/// rollback immediate so that any existing connections are terminated.
|
||||||
/// environment variables.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="databaseName">
|
/// <param name="settings">The resolved database settings.</param>
|
||||||
/// The database (initial catalog) to connect to. When <c>null</c>, falls back to the
|
/// <exception cref="SqlException">Thrown if the connection or the drop operation fails.</exception>
|
||||||
/// <c>DB_NAME</c> environment variable.
|
private static async Task DropDatabaseIfExistsAsync(DatabaseSettings settings)
|
||||||
/// </param>
|
|
||||||
/// <returns>A fully built SQL Server connection string.</returns>
|
|
||||||
/// <exception cref="InvalidOperationException">
|
|
||||||
/// Thrown when <c>DB_SERVER</c>, <c>DB_USER</c>, <c>DB_PASSWORD</c>, or (when
|
|
||||||
/// <paramref name="databaseName" /> is <c>null</c>) <c>DB_NAME</c> is not set.
|
|
||||||
/// </exception>
|
|
||||||
private static string BuildConnectionString(string? databaseName = null)
|
|
||||||
{
|
{
|
||||||
string server =
|
const string sql = """
|
||||||
Environment.GetEnvironmentVariable("DB_SERVER")
|
DECLARE @statement nvarchar(max);
|
||||||
?? throw new InvalidOperationException("DB_SERVER environment variable is not set");
|
|
||||||
|
|
||||||
string dbName =
|
IF EXISTS (SELECT 1 FROM sys.databases WHERE name = @databaseName)
|
||||||
databaseName
|
BEGIN
|
||||||
?? Environment.GetEnvironmentVariable("DB_NAME")
|
SET @statement =
|
||||||
?? throw new InvalidOperationException("DB_NAME environment variable is not set");
|
N'ALTER DATABASE ' + QUOTENAME(@databaseName)
|
||||||
|
+ N' SET SINGLE_USER WITH ROLLBACK IMMEDIATE;';
|
||||||
|
EXEC sys.sp_executesql @statement;
|
||||||
|
|
||||||
string user =
|
SET @statement = N'DROP DATABASE ' + QUOTENAME(@databaseName) + N';';
|
||||||
Environment.GetEnvironmentVariable("DB_USER")
|
EXEC sys.sp_executesql @statement;
|
||||||
?? throw new InvalidOperationException("DB_USER environment variable is not set");
|
END
|
||||||
|
""";
|
||||||
|
|
||||||
string password =
|
await ExecuteNonQueryAsync(settings.MasterConnectionString, sql, settings.DatabaseName);
|
||||||
Environment.GetEnvironmentVariable("DB_PASSWORD")
|
Console.WriteLine($"Database '{settings.DatabaseName}' dropped, or did not exist.");
|
||||||
?? throw new InvalidOperationException("DB_PASSWORD environment variable is not set");
|
}
|
||||||
|
|
||||||
string trustServerCertificate =
|
/// <summary>
|
||||||
Environment.GetEnvironmentVariable("DB_TRUST_SERVER_CERTIFICATE") ?? "True";
|
/// Creates the target database on the <c>master</c> connection if it does not already exist.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="settings">The resolved database settings.</param>
|
||||||
|
/// <exception cref="SqlException">Thrown if the connection or the create operation fails.</exception>
|
||||||
|
private static async Task CreateDatabaseIfNotExistsAsync(DatabaseSettings settings)
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
DECLARE @statement nvarchar(max);
|
||||||
|
|
||||||
SqlConnectionStringBuilder builder = new()
|
IF NOT EXISTS (SELECT 1 FROM sys.databases WHERE name = @databaseName)
|
||||||
{
|
BEGIN
|
||||||
DataSource = server,
|
SET @statement = N'CREATE DATABASE ' + QUOTENAME(@databaseName) + N';';
|
||||||
InitialCatalog = dbName,
|
EXEC sys.sp_executesql @statement;
|
||||||
UserID = user,
|
END
|
||||||
Password = password,
|
""";
|
||||||
TrustServerCertificate = bool.Parse(trustServerCertificate),
|
|
||||||
Encrypt = true,
|
|
||||||
};
|
|
||||||
|
|
||||||
return builder.ConnectionString;
|
await ExecuteNonQueryAsync(settings.MasterConnectionString, sql, settings.DatabaseName);
|
||||||
|
Console.WriteLine($"Database '{settings.DatabaseName}' is present.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Opens a connection, executes a non-query batch parameterised by database name, and
|
||||||
|
/// disposes both the command and the connection.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="connectionString">The connection string to open.</param>
|
||||||
|
/// <param name="sql">The Transact-SQL batch to execute.</param>
|
||||||
|
/// <param name="databaseName">The value bound to the <c>@databaseName</c> parameter.</param>
|
||||||
|
private static async Task ExecuteNonQueryAsync(
|
||||||
|
string connectionString,
|
||||||
|
string sql,
|
||||||
|
string databaseName
|
||||||
|
)
|
||||||
|
{
|
||||||
|
await using SqlConnection connection = new(connectionString);
|
||||||
|
await connection.OpenAsync();
|
||||||
|
|
||||||
|
await using SqlCommand command = new(sql, connection);
|
||||||
|
command.Parameters.Add("@databaseName", SqlDbType.NVarChar, 128).Value = databaseName;
|
||||||
|
|
||||||
|
await command.ExecuteNonQueryAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Applies all pending SQL migration scripts embedded in this assembly to the target
|
/// Applies all pending SQL migration scripts embedded in this assembly to the target
|
||||||
/// database using DbUp, logging progress to the console.
|
/// database using DbUp, logging progress to the console.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="connectionString">The connection string for the target application database.</param>
|
||||||
/// <returns><c>true</c> if the upgrade completed successfully; otherwise <c>false</c>.</returns>
|
/// <returns><c>true</c> if the upgrade completed successfully; otherwise <c>false</c>.</returns>
|
||||||
private static bool DeployMigrations()
|
private static bool DeployMigrations(string connectionString)
|
||||||
{
|
{
|
||||||
UpgradeEngine? upgrader = DeployChanges
|
UpgradeEngine upgrader = DeployChanges
|
||||||
.To.SqlDatabase(connectionString)
|
.To.SqlDatabase(connectionString)
|
||||||
.WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly())
|
.WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly())
|
||||||
|
.WithTransactionPerScript()
|
||||||
.LogToConsole()
|
.LogToConsole()
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
DatabaseUpgradeResult? result = upgrader.PerformUpgrade();
|
DatabaseUpgradeResult result = upgrader.PerformUpgrade();
|
||||||
return result.Successful;
|
|
||||||
|
if (result.Successful)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Database migrations completed successfully.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine($"Database migrations failed: {result.Error}");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Drops the <c>Biergarten</c> database if it exists, first forcing it into single-user
|
/// Connection details and runtime options resolved from environment variables.
|
||||||
/// mode with rollback to terminate any existing connections.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>
|
/// <param name="ConnectionString">The connection string for the target application database.</param>
|
||||||
/// <c>true</c> if the database was dropped (or did not exist) without error; <c>false</c>
|
/// <param name="MasterConnectionString">
|
||||||
/// if an error occurred while connecting or dropping the database.
|
/// The connection string for the <c>master</c> database, used for create and drop operations.
|
||||||
/// </returns>
|
/// </param>
|
||||||
private static bool ClearDatabase()
|
/// <param name="DatabaseName">The name of the target application database.</param>
|
||||||
|
/// <param name="ClearDatabase">Whether the target database should be dropped before migrating.</param>
|
||||||
|
private sealed record DatabaseSettings(
|
||||||
|
string ConnectionString,
|
||||||
|
string MasterConnectionString,
|
||||||
|
string DatabaseName,
|
||||||
|
bool ClearDatabase
|
||||||
|
)
|
||||||
{
|
{
|
||||||
SqlConnection myConn = new(masterConnectionString);
|
/// <summary>
|
||||||
|
/// Builds the settings from the <c>DB_SERVER</c>, <c>DB_NAME</c>, <c>DB_USER</c>,
|
||||||
try
|
/// <c>DB_PASSWORD</c>, <c>DB_TRUST_SERVER_CERTIFICATE</c>, and <c>CLEAR_DATABASE</c>
|
||||||
|
/// environment variables.
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="InvalidOperationException">
|
||||||
|
/// Thrown when a required environment variable is not set.
|
||||||
|
/// </exception>
|
||||||
|
public static DatabaseSettings FromEnvironment()
|
||||||
{
|
{
|
||||||
myConn.Open();
|
string server = Required("DB_SERVER");
|
||||||
|
string databaseName = Required("DB_NAME");
|
||||||
|
string user = Required("DB_USER");
|
||||||
|
string password = Required("DB_PASSWORD");
|
||||||
|
|
||||||
// First, set the database to single user mode to close all connections
|
bool trustServerCertificate =
|
||||||
SqlCommand setModeCommand = new(
|
!bool.TryParse(
|
||||||
"IF EXISTS (SELECT 1 FROM sys.databases WHERE name = 'Biergarten') "
|
Environment.GetEnvironmentVariable("DB_TRUST_SERVER_CERTIFICATE"),
|
||||||
+ "ALTER DATABASE [Biergarten] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;",
|
out bool parsedTrust
|
||||||
myConn
|
)
|
||||||
|
|| parsedTrust;
|
||||||
|
|
||||||
|
bool clearDatabase = string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("CLEAR_DATABASE"),
|
||||||
|
"true",
|
||||||
|
StringComparison.OrdinalIgnoreCase
|
||||||
);
|
);
|
||||||
try
|
|
||||||
{
|
|
||||||
setModeCommand.ExecuteNonQuery();
|
|
||||||
Console.WriteLine("Database set to single user mode.");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Warning: Could not set single user mode: {ex.Message}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then drop the database
|
return new DatabaseSettings(
|
||||||
SqlCommand dropCommand = new("DROP DATABASE IF EXISTS [Biergarten];", myConn);
|
Build(databaseName),
|
||||||
try
|
Build("master"),
|
||||||
{
|
databaseName,
|
||||||
dropCommand.ExecuteNonQuery();
|
clearDatabase
|
||||||
Console.WriteLine("Database cleared successfully.");
|
);
|
||||||
}
|
|
||||||
catch (Exception ex)
|
string Build(string initialCatalog) =>
|
||||||
{
|
new SqlConnectionStringBuilder
|
||||||
Console.WriteLine($"Error dropping database: {ex}");
|
{
|
||||||
return false;
|
DataSource = server,
|
||||||
}
|
InitialCatalog = initialCatalog,
|
||||||
}
|
UserID = user,
|
||||||
catch (Exception ex)
|
Password = password,
|
||||||
{
|
Encrypt = true,
|
||||||
Console.WriteLine($"Error clearing database: {ex}");
|
TrustServerCertificate = trustServerCertificate
|
||||||
return false;
|
}.ConnectionString;
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (myConn.State == ConnectionState.Open)
|
|
||||||
myConn.Close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
private static string Required(string variableName) =>
|
||||||
|
Environment.GetEnvironmentVariable(variableName)
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
$"The {variableName} environment variable is not set."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
/// <summary>
|
|
||||||
/// Creates the <c>Biergarten</c> database on the master connection if it does not
|
|
||||||
/// already exist. Errors encountered while creating the database are logged but do
|
|
||||||
/// not stop execution.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns><c>true</c> always; this method does not propagate database errors as a failure result.</returns>
|
|
||||||
private static bool CreateDatabaseIfNotExists()
|
|
||||||
{
|
|
||||||
SqlConnection myConn = new(masterConnectionString);
|
|
||||||
|
|
||||||
const string str = """
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM sys.databases WHERE name = 'Biergarten')
|
|
||||||
CREATE DATABASE [Biergarten]
|
|
||||||
""";
|
|
||||||
|
|
||||||
SqlCommand myCommand = new(str, myConn);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
myConn.Open();
|
|
||||||
myCommand.ExecuteNonQuery();
|
|
||||||
Console.WriteLine("Database creation command executed successfully.");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Error creating database: {ex}");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (myConn.State == ConnectionState.Open)
|
|
||||||
myConn.Close();
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Migration runner entry point. Optionally clears the existing database when the
|
|
||||||
/// <c>CLEAR_DATABASE</c> environment variable is set to <c>"true"</c>, ensures the
|
|
||||||
/// database exists, then deploys all pending migrations.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="args">Command-line arguments (unused).</param>
|
|
||||||
/// <returns><c>0</c> if migrations completed successfully; <c>1</c> if they failed or an error occurred.</returns>
|
|
||||||
public static int Main(string[] args)
|
|
||||||
{
|
|
||||||
Console.WriteLine("Starting database migrations...");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string? clearDatabase = Environment.GetEnvironmentVariable("CLEAR_DATABASE");
|
|
||||||
if (clearDatabase == "true")
|
|
||||||
{
|
|
||||||
Console.WriteLine("CLEAR_DATABASE is enabled. Clearing existing database...");
|
|
||||||
ClearDatabase();
|
|
||||||
}
|
|
||||||
|
|
||||||
CreateDatabaseIfNotExists();
|
|
||||||
bool success = DeployMigrations();
|
|
||||||
|
|
||||||
if (success)
|
|
||||||
{
|
|
||||||
Console.WriteLine("Database migrations completed successfully.");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.WriteLine("Database migrations failed.");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine("An error occurred during database migrations:");
|
|
||||||
Console.WriteLine(ex.Message);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,26 +8,29 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="idunno.Password.Generator" Version="1.0.1" />
|
<PackageReference Include="idunno.Password.Generator" Version="1.0.1"/>
|
||||||
<PackageReference Include="Konscious.Security.Cryptography.Argon2" Version="1.3.1" />
|
<PackageReference Include="Konscious.Security.Cryptography.Argon2" Version="1.3.1"/>
|
||||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.2" />
|
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.2"/>
|
||||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.9" />
|
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.9"/>
|
||||||
<PackageReference Include="dbup" Version="5.0.41" />
|
<PackageReference Include="dbup" Version="5.0.41"/>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
remove this override once microsoft ships official fix to vulnerability
|
remove this override once microsoft ships official fix to vulnerability
|
||||||
https:
|
https:
|
||||||
-->
|
-->
|
||||||
<PackageReference Include="SQLitePCLRaw.lib.e_sqlite3" Version="3.50.3" />
|
<PackageReference Include="SQLitePCLRaw.lib.e_sqlite3" Version="3.50.3"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\Domain\Domain.Entities\Domain.Entities.csproj" />
|
<ProjectReference Include="..\..\Domain\Domain.Entities\Domain.Entities.csproj"/>
|
||||||
|
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Sql\Infrastructure.Sql.csproj"/>
|
||||||
|
<ProjectReference Include="..\..\Features\Features.Breweries\Features.Breweries.csproj"/>
|
||||||
|
<ProjectReference Include="..\..\Features\Features.Auth\Features.Auth.csproj"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<!-- Pipeline-generated seed data, copied to the output directory so it can be opened via
|
<!-- Pipeline-generated seed data, copied to the output directory so it can be opened via
|
||||||
ConnectionStrings.SqliteConnectionString at runtime. -->
|
ConnectionStrings.SqliteConnectionString at runtime. -->
|
||||||
<None Include="SeedData\seed.sqlite" CopyToOutputDirectory="PreserveNewest" />
|
<None CopyToOutputDirectory="PreserveNewest" Include="SeedData\seed.sqlite"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
using Microsoft.Data.SqlClient;
|
#region
|
||||||
|
|
||||||
|
using Microsoft.Data.SqlClient;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
namespace Database.Seed.DatabaseHelpers;
|
namespace Database.Seed.DatabaseHelpers;
|
||||||
|
|
||||||
public class ConnectionStrings
|
public class ConnectionStrings
|
||||||
{
|
{
|
||||||
|
public static string SqlServerConnectionString => GetSqlServerConnectionString();
|
||||||
|
public static string SqliteConnectionString => "Data Source=SeedData/seed.sqlite";
|
||||||
|
|
||||||
private static string GetSqlServerConnectionString()
|
private static string GetSqlServerConnectionString()
|
||||||
{
|
{
|
||||||
string server =
|
string server =
|
||||||
@@ -32,12 +39,9 @@ public class ConnectionStrings
|
|||||||
UserID = user,
|
UserID = user,
|
||||||
Password = password,
|
Password = password,
|
||||||
TrustServerCertificate = bool.Parse(trustServerCertificate),
|
TrustServerCertificate = bool.Parse(trustServerCertificate),
|
||||||
Encrypt = true,
|
Encrypt = true
|
||||||
};
|
};
|
||||||
|
|
||||||
return builder.ConnectionString;
|
return builder.ConnectionString;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
public static string SqlServerConnectionString => GetSqlServerConnectionString();
|
|
||||||
public static string SqliteConnectionString => "Data Source=SeedData/seed.sqlite";
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,7 @@ FROM build AS publish
|
|||||||
ARG BUILD_CONFIGURATION=Release
|
ARG BUILD_CONFIGURATION=Release
|
||||||
RUN dotnet publish "./Database/Database.Seed/Database.Seed.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
RUN dotnet publish "./Database/Database.Seed/Database.Seed.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/runtime:10.0 AS final
|
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=publish /app/publish .
|
COPY --from=publish /app/publish .
|
||||||
ENTRYPOINT ["dotnet", "Database.Seed.dll"]
|
ENTRYPOINT ["dotnet", "Database.Seed.dll"]
|
||||||
|
|||||||
@@ -5,4 +5,4 @@ public sealed record BreweryAddress
|
|||||||
public required City City { get; init; }
|
public required City City { get; init; }
|
||||||
|
|
||||||
public required string PostalCode { get; init; }
|
public required string PostalCode { get; init; }
|
||||||
}
|
}
|
||||||
@@ -5,4 +5,4 @@ public sealed record BreweryRecord
|
|||||||
public required BreweryAddress Address { get; init; }
|
public required BreweryAddress Address { get; init; }
|
||||||
|
|
||||||
public required BreweryResult Brewery { get; init; }
|
public required BreweryResult Brewery { get; init; }
|
||||||
}
|
}
|
||||||
@@ -9,4 +9,4 @@ public sealed record BreweryResult
|
|||||||
public required string NameLocal { get; init; }
|
public required string NameLocal { get; init; }
|
||||||
|
|
||||||
public required string DescriptionLocal { get; init; }
|
public required string DescriptionLocal { get; init; }
|
||||||
}
|
}
|
||||||
@@ -15,4 +15,4 @@ public sealed record City
|
|||||||
public IReadOnlyList<string> LocalLanguages { get; init; } = [];
|
public IReadOnlyList<string> LocalLanguages { get; init; } = [];
|
||||||
|
|
||||||
public required PostalCodeSpec PostalCode { get; init; }
|
public required PostalCodeSpec PostalCode { get; init; }
|
||||||
}
|
}
|
||||||
@@ -7,4 +7,4 @@ public sealed record PostalCodeSpec
|
|||||||
public IReadOnlyList<string> CityRegexes { get; init; } = [];
|
public IReadOnlyList<string> CityRegexes { get; init; } = [];
|
||||||
|
|
||||||
public IReadOnlyList<string> Examples { get; init; } = [];
|
public IReadOnlyList<string> Examples { get; init; } = [];
|
||||||
}
|
}
|
||||||
@@ -5,4 +5,4 @@ public sealed record UserAddress
|
|||||||
public required City City { get; init; }
|
public required City City { get; init; }
|
||||||
|
|
||||||
public required string PostalCode { get; init; }
|
public required string PostalCode { get; init; }
|
||||||
}
|
}
|
||||||
@@ -9,4 +9,4 @@ public sealed record UserRecord
|
|||||||
public required string Email { get; init; }
|
public required string Email { get; init; }
|
||||||
|
|
||||||
public required string DateOfBirth { get; init; }
|
public required string DateOfBirth { get; init; }
|
||||||
}
|
}
|
||||||
@@ -13,4 +13,4 @@ public sealed record UserResult
|
|||||||
public required string Bio { get; init; }
|
public required string Bio { get; init; }
|
||||||
|
|
||||||
public required float ActivityWeight { get; init; }
|
public required float ActivityWeight { get; init; }
|
||||||
}
|
}
|
||||||
@@ -1,53 +1,78 @@
|
|||||||
|
|
||||||
|
|
||||||
using Database.Seed.DatabaseHelpers;
|
using Database.Seed.DatabaseHelpers;
|
||||||
using Database.Seed.PipelineData;
|
using Database.Seed.PipelineData;
|
||||||
using Database.Seed.Sqlite;
|
using Database.Seed.Sqlite;
|
||||||
|
using Features.Auth.DependencyInjection;
|
||||||
|
using Features.Auth.Repository;
|
||||||
|
using Features.Breweries.DependencyInjection;
|
||||||
|
using Features.Breweries.Repository;
|
||||||
|
using Infrastructure.Sql;
|
||||||
using Microsoft.Data.Sqlite;
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace Database.Seed;
|
|
||||||
|
|
||||||
public class Program
|
IReadOnlyList<BreweryRecord> breweries;
|
||||||
|
IReadOnlyList<UserRecord> users;
|
||||||
|
try
|
||||||
{
|
{
|
||||||
public static int Main()
|
PipelineSeedDataReader reader = new(ConnectionStrings.SqliteConnectionString);
|
||||||
{
|
breweries = reader.ReadBreweryRecords();
|
||||||
IReadOnlyList<BreweryRecord> breweries;
|
users = reader.ReadUserRecords();
|
||||||
IReadOnlyList<UserRecord> users;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
PipelineSeedDataReader reader = new(ConnectionStrings.SqliteConnectionString);
|
|
||||||
breweries = reader.ReadBreweryRecords();
|
|
||||||
users = reader.ReadUserRecords();
|
|
||||||
}
|
|
||||||
catch (SqliteException ex)
|
|
||||||
{
|
|
||||||
Console.ForegroundColor = ConsoleColor.Red;
|
|
||||||
Console.WriteLine($"Error opening database connection: {ex.Message}");
|
|
||||||
Console.ResetColor();
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Console.ForegroundColor = ConsoleColor.Green;
|
|
||||||
Console.WriteLine(
|
|
||||||
$"Loaded {breweries.Count} breweries."
|
|
||||||
);
|
|
||||||
Console.ResetColor();
|
|
||||||
|
|
||||||
for (int i = 0; i < breweries.Count; i++)
|
|
||||||
{
|
|
||||||
BreweryRecord brewery = breweries[i];
|
|
||||||
Console.WriteLine($"{i + 1}:\t{brewery.Brewery.NameEn}\t({brewery.Address.City.CityName}, {brewery.Address.City.Country})");
|
|
||||||
}
|
|
||||||
Console.ForegroundColor = ConsoleColor.Green;
|
|
||||||
Console.WriteLine($"Loaded {users.Count} users.");
|
|
||||||
Console.ResetColor();
|
|
||||||
|
|
||||||
for (int i = 0; i < users.Count; i++)
|
|
||||||
{
|
|
||||||
UserRecord user = users[i];
|
|
||||||
Console.WriteLine($"{i + 1}:\t{user.User.FirstName} {user.User.LastName}\t ({user.Email})");
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
catch (SqliteException ex)
|
||||||
|
{
|
||||||
|
Console.ForegroundColor = ConsoleColor.Red;
|
||||||
|
Console.WriteLine($"Error opening database connection: {ex.Message}");
|
||||||
|
Console.ResetColor();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ServiceCollection services = [];
|
||||||
|
services.AddSingleton<IConfiguration>(new ConfigurationBuilder().Build());
|
||||||
|
services.AddSingleton<ISqlConnectionFactory, DefaultSqlConnectionFactory>();
|
||||||
|
services.AddFeaturesBreweries();
|
||||||
|
services.AddFeaturesAuth();
|
||||||
|
|
||||||
|
using ServiceProvider provider = services.BuildServiceProvider();
|
||||||
|
|
||||||
|
IBreweryRepository breweryRepository;
|
||||||
|
IAuthRepository authRepository;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
breweryRepository = provider.GetRequiredService<IBreweryRepository>();
|
||||||
|
authRepository = provider.GetRequiredService<IAuthRepository>();
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
Console.ForegroundColor = ConsoleColor.Red;
|
||||||
|
Console.WriteLine($"Error configuring database connection: {ex.Message}");
|
||||||
|
Console.ResetColor();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.ForegroundColor = ConsoleColor.Green;
|
||||||
|
Console.WriteLine(
|
||||||
|
$"Loaded {breweries.Count} breweries."
|
||||||
|
);
|
||||||
|
Console.ResetColor();
|
||||||
|
|
||||||
|
for (int i = 0; i < breweries.Count; i++)
|
||||||
|
{
|
||||||
|
BreweryRecord brewery = breweries[i];
|
||||||
|
Console.WriteLine(
|
||||||
|
$"{i + 1}:\t{brewery.Brewery.NameEn}\t({brewery.Address.City.CityName}, {brewery.Address.City.Country})");
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.ForegroundColor = ConsoleColor.Green;
|
||||||
|
Console.WriteLine($"Loaded {users.Count} users.");
|
||||||
|
Console.ResetColor();
|
||||||
|
|
||||||
|
for (int i = 0; i < users.Count; i++)
|
||||||
|
{
|
||||||
|
UserRecord user = users[i];
|
||||||
|
Console.WriteLine($"{i + 1}:\t{user.User.FirstName} {user.User.LastName}\t ({user.Email})");
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
|
#region
|
||||||
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Database.Seed.PipelineData;
|
using Database.Seed.PipelineData;
|
||||||
using Microsoft.Data.Sqlite;
|
using Microsoft.Data.Sqlite;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
namespace Database.Seed.Sqlite;
|
namespace Database.Seed.Sqlite;
|
||||||
|
|
||||||
public sealed class PipelineSeedDataReader
|
public sealed class PipelineSeedDataReader
|
||||||
@@ -21,11 +25,11 @@ public sealed class PipelineSeedDataReader
|
|||||||
IReadOnlyDictionary<long, City> cities = ReadCities(connection);
|
IReadOnlyDictionary<long, City> cities = ReadCities(connection);
|
||||||
|
|
||||||
const string sql = """
|
const string sql = """
|
||||||
SELECT b.name_en, b.description_en, b.name_local, b.description_local,
|
SELECT b.name_en, b.description_en, b.name_local, b.description_local,
|
||||||
ba.city_id, ba.postal_code
|
ba.city_id, ba.postal_code
|
||||||
FROM breweries b
|
FROM breweries b
|
||||||
JOIN brewery_addresses ba ON ba.brewery_id = b.id;
|
JOIN brewery_addresses ba ON ba.brewery_id = b.id;
|
||||||
""";
|
""";
|
||||||
|
|
||||||
using SqliteCommand command = new(sql, connection);
|
using SqliteCommand command = new(sql, connection);
|
||||||
using SqliteDataReader reader = command.ExecuteReader();
|
using SqliteDataReader reader = command.ExecuteReader();
|
||||||
@@ -40,13 +44,13 @@ public sealed class PipelineSeedDataReader
|
|||||||
NameEn = reader.GetString(0),
|
NameEn = reader.GetString(0),
|
||||||
DescriptionEn = reader.GetString(1),
|
DescriptionEn = reader.GetString(1),
|
||||||
NameLocal = reader.GetString(2),
|
NameLocal = reader.GetString(2),
|
||||||
DescriptionLocal = reader.GetString(3),
|
DescriptionLocal = reader.GetString(3)
|
||||||
},
|
},
|
||||||
Address = new BreweryAddress
|
Address = new BreweryAddress
|
||||||
{
|
{
|
||||||
City = cities[reader.GetInt64(4)],
|
City = cities[reader.GetInt64(4)],
|
||||||
PostalCode = reader.GetString(5),
|
PostalCode = reader.GetString(5)
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -61,12 +65,12 @@ public sealed class PipelineSeedDataReader
|
|||||||
IReadOnlyDictionary<long, City> cities = ReadCities(connection);
|
IReadOnlyDictionary<long, City> cities = ReadCities(connection);
|
||||||
|
|
||||||
const string sql = """
|
const string sql = """
|
||||||
SELECT u.first_name, u.last_name, u.gender, u.username, u.bio,
|
SELECT u.first_name, u.last_name, u.gender, u.username, u.bio,
|
||||||
u.activity_weight, u.email, u.date_of_birth,
|
u.activity_weight, u.email, u.date_of_birth,
|
||||||
ua.city_id, ua.postal_code
|
ua.city_id, ua.postal_code
|
||||||
FROM users u
|
FROM users u
|
||||||
JOIN user_addresses ua ON ua.user_id = u.id;
|
JOIN user_addresses ua ON ua.user_id = u.id;
|
||||||
""";
|
""";
|
||||||
|
|
||||||
using SqliteCommand command = new(sql, connection);
|
using SqliteCommand command = new(sql, connection);
|
||||||
using SqliteDataReader reader = command.ExecuteReader();
|
using SqliteDataReader reader = command.ExecuteReader();
|
||||||
@@ -83,15 +87,15 @@ public sealed class PipelineSeedDataReader
|
|||||||
Gender = reader.GetString(2),
|
Gender = reader.GetString(2),
|
||||||
Username = reader.GetString(3),
|
Username = reader.GetString(3),
|
||||||
Bio = reader.GetString(4),
|
Bio = reader.GetString(4),
|
||||||
ActivityWeight = (float)reader.GetDouble(5),
|
ActivityWeight = (float)reader.GetDouble(5)
|
||||||
},
|
},
|
||||||
Email = reader.GetString(6),
|
Email = reader.GetString(6),
|
||||||
DateOfBirth = reader.GetString(7),
|
DateOfBirth = reader.GetString(7),
|
||||||
Address = new UserAddress
|
Address = new UserAddress
|
||||||
{
|
{
|
||||||
City = cities[reader.GetInt64(8)],
|
City = cities[reader.GetInt64(8)],
|
||||||
PostalCode = reader.GetString(9),
|
PostalCode = reader.GetString(9)
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -101,11 +105,11 @@ public sealed class PipelineSeedDataReader
|
|||||||
private static IReadOnlyDictionary<long, City> ReadCities(SqliteConnection connection)
|
private static IReadOnlyDictionary<long, City> ReadCities(SqliteConnection connection)
|
||||||
{
|
{
|
||||||
const string sql = """
|
const string sql = """
|
||||||
SELECT id, city, state_province, iso3166_2, country, iso3166_1,
|
SELECT id, city, state_province, iso3166_2, country, iso3166_1,
|
||||||
local_languages_json, postal_code_country_format_regex,
|
local_languages_json, postal_code_country_format_regex,
|
||||||
postal_code_city_regex_json
|
postal_code_city_regex_json
|
||||||
FROM cities;
|
FROM cities;
|
||||||
""";
|
""";
|
||||||
|
|
||||||
using SqliteCommand command = new(sql, connection);
|
using SqliteCommand command = new(sql, connection);
|
||||||
using SqliteDataReader reader = command.ExecuteReader();
|
using SqliteDataReader reader = command.ExecuteReader();
|
||||||
@@ -123,13 +127,15 @@ public sealed class PipelineSeedDataReader
|
|||||||
PostalCode = new PostalCodeSpec
|
PostalCode = new PostalCodeSpec
|
||||||
{
|
{
|
||||||
CountryFormatRegex = reader.GetString(7),
|
CountryFormatRegex = reader.GetString(7),
|
||||||
CityRegexes = DeserializeStringArray(reader.GetString(8)),
|
CityRegexes = DeserializeStringArray(reader.GetString(8))
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return cities;
|
return cities;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IReadOnlyList<string> DeserializeStringArray(string json) =>
|
private static IReadOnlyList<string> DeserializeStringArray(string json)
|
||||||
JsonSerializer.Deserialize<List<string>>(json) ?? [];
|
{
|
||||||
}
|
return JsonSerializer.Deserialize<List<string>>(json) ?? [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ services:
|
|||||||
SA_PASSWORD: "${DB_PASSWORD}"
|
SA_PASSWORD: "${DB_PASSWORD}"
|
||||||
MSSQL_PID: "Express"
|
MSSQL_PID: "Express"
|
||||||
ports:
|
ports:
|
||||||
- "1433:1433"
|
- "1434:1433"
|
||||||
volumes:
|
volumes:
|
||||||
- sqlserverdata-dev:/var/opt/mssql
|
- sqlserverdata-dev:/var/opt/mssql
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ services:
|
|||||||
SA_PASSWORD: "${DB_PASSWORD}"
|
SA_PASSWORD: "${DB_PASSWORD}"
|
||||||
MSSQL_PID: "Express"
|
MSSQL_PID: "Express"
|
||||||
ports:
|
ports:
|
||||||
- "1433:1433"
|
- "1434:1433"
|
||||||
volumes:
|
volumes:
|
||||||
- sqlserverdata-dev:/var/opt/mssql
|
- sqlserverdata-dev:/var/opt/mssql
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
|||||||
Reference in New Issue
Block a user