Database setup for the Website service (#13)
This PR contains the work done to introduce a _Fluent_-based persistence layer for the Website service, selectable at runtime alongside the existing in-memory default, plus the local dev tooling and docs to support it. To provide further details about the work: * Persistence package * The `Driver` and `TLS` enumerations * The `Configuration` type * The `Service` factory that builds the service * `PrepareDB` for migrations registration * The `Probe` for readiness checks. * App integration * Builds the driver, registers migrations, and attaches `Fluent` to the service lifecycle so it starts/stops with the HTTP server. * Migrate-on-boot is gated to the in-memory backend; MySQL/MariaDB is migrated out of band via --database-migrate so shared databases never race on startup. * The `ConfigReader+Properties` extension maps database.* config keys onto the driver. * Library * Added database configuration constants. * The `HealthController` controller gains a readiness probe: `GET /health/ready` checks whether the database is reachable, separate from the existing liveness check. * Others * Updated the `docker-compose` files to support a database service behind a database profile, and hardened for local development * New database targets on the `Makefile` file and overall documentation updated * Updated the `.env.local`, `Dockerfile`, and `README` files to document the persistence workflow, config keys, and local DB commands Reviewed-on: rock-n-code/loud-amsterdam#13 Co-authored-by: Javier Cicchelli <javier@rock-n-code.com> Co-committed-by: Javier Cicchelli <javier@rock-n-code.com>
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
import Foundation
|
||||
import Logging
|
||||
import SQLKit
|
||||
import Testing
|
||||
|
||||
@testable import Persistence
|
||||
|
||||
@Suite("Service method")
|
||||
struct ServiceTests {
|
||||
|
||||
// MARK: Methods tests
|
||||
|
||||
@Test
|
||||
func `registers an SQLite database as the default for the in-memory driver`() async throws {
|
||||
let service = Service(
|
||||
driver: .inMemory,
|
||||
logger: Logger(label: "test")
|
||||
)
|
||||
|
||||
let fluent = service()
|
||||
let database = fluent.db() as? any SQLDatabase
|
||||
|
||||
try await fluent.shutdown()
|
||||
|
||||
let dialect = try #require(database?.dialect)
|
||||
|
||||
#expect(dialect.name == "sqlite")
|
||||
}
|
||||
|
||||
@Test
|
||||
func `registers a MySQL database as the default for the mysql driver`() async throws {
|
||||
// Resolving the default database opens no connection — pooling is lazy — so no server
|
||||
// needs to be listening on the configured host and port.
|
||||
let service = Service(
|
||||
driver: .mysql(
|
||||
.init(
|
||||
host: "127.0.0.1",
|
||||
port: 3306,
|
||||
name: "loud",
|
||||
username: "loud",
|
||||
password: "loud",
|
||||
tls: .off,
|
||||
maxConnectionsPerEventLoop: 1
|
||||
)
|
||||
),
|
||||
logger: Logger(label: "test")
|
||||
)
|
||||
|
||||
let fluent = service()
|
||||
let database = fluent.db() as? any SQLDatabase
|
||||
|
||||
try await fluent.shutdown()
|
||||
|
||||
let dialect = try #require(database?.dialect)
|
||||
|
||||
#expect(dialect.name == "mysql")
|
||||
}
|
||||
|
||||
@Test
|
||||
func `builds a usable in-memory database`() async throws {
|
||||
let service = Service(
|
||||
driver: .inMemory,
|
||||
logger: Logger(label: "test")
|
||||
)
|
||||
|
||||
let fluent = service()
|
||||
|
||||
do {
|
||||
let database = try #require(fluent.db() as? any SQLDatabase)
|
||||
|
||||
try await database.raw("SELECT 1").run()
|
||||
} catch {
|
||||
try? await fluent.shutdown()
|
||||
|
||||
throw error
|
||||
}
|
||||
|
||||
try await fluent.shutdown()
|
||||
}
|
||||
|
||||
@Test("in-memory: migrate, insert, read back")
|
||||
func inMemoryRoundTrip() async throws {
|
||||
try await roundTrip(driver: .inMemory)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"mysql: migrate, insert, read back",
|
||||
.enabled(if: mysqlDriver != nil)
|
||||
)
|
||||
func mysqlRoundTrip() async throws {
|
||||
try await roundTrip(
|
||||
driver: mysqlDriver!,
|
||||
revertAfter: true
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private extension ServiceTests {
|
||||
|
||||
/// Migrates, inserts, and reads back a record against the given driver, shutting the pool down after.
|
||||
///
|
||||
/// The `Fluent` service normally owns pool shutdown via its `run()` in the service group; outside that,
|
||||
/// the test must shut it down explicitly — even on failure — or the pool asserts on `deinit`.
|
||||
/// - Parameters:
|
||||
/// - driver: the persistence backend to exercise.
|
||||
/// - revertAfter: whether to revert the migrations afterwards; set for a shared database (the
|
||||
/// in-memory database is discarded on shutdown, so it needs no revert).
|
||||
func roundTrip(
|
||||
driver: Persistence.Driver,
|
||||
revertAfter: Bool = false
|
||||
) async throws {
|
||||
let service = Service(
|
||||
driver: driver,
|
||||
logger: Logger(label: "test")
|
||||
)
|
||||
let fluent = service()
|
||||
|
||||
do {
|
||||
await registerMigrations(fluent)
|
||||
try await fluent.migrate()
|
||||
|
||||
let repository = ExampleRepository(fluent: fluent)
|
||||
let created = try await repository.create(name: "loud")
|
||||
|
||||
#expect(try await repository.all().contains(created))
|
||||
|
||||
if revertAfter {
|
||||
try await fluent.revert()
|
||||
}
|
||||
} catch {
|
||||
try? await fluent.shutdown()
|
||||
|
||||
throw error
|
||||
}
|
||||
|
||||
try await fluent.shutdown()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// The MySQL/MariaDB driver built from the `MYSQL_TEST_*` environment variables, or `nil` when the gate
|
||||
/// variable `MYSQL_TEST_HOST` is unset — in which case the MySQL integration test is skipped, so the suite
|
||||
/// stays runnable with no database available.
|
||||
private let mysqlDriver: Persistence.Driver? = {
|
||||
let environment = ProcessInfo.processInfo.environment
|
||||
|
||||
guard let host = environment["MYSQL_TEST_HOST"] else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return .mysql(
|
||||
.init(
|
||||
host: host,
|
||||
port: environment["MYSQL_TEST_PORT"].flatMap(Int.init) ?? 3306,
|
||||
name: environment["MYSQL_TEST_NAME"] ?? "loud",
|
||||
username: environment["MYSQL_TEST_USERNAME"] ?? "loud",
|
||||
password: environment["MYSQL_TEST_PASSWORD"] ?? "loud",
|
||||
tls: .off,
|
||||
maxConnectionsPerEventLoop: 2
|
||||
)
|
||||
)
|
||||
}()
|
||||
Reference in New Issue
Block a user