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:
2026-07-11 09:15:58 +00:00
committed by javier
parent 9774454bba
commit dc6b22e648
45 changed files with 1791 additions and 254 deletions
@@ -0,0 +1,63 @@
import FluentKit
import Foundation
import HummingbirdFluent
/// A `Sendable` snapshot of an ``ExampleRecord``, safe to return across concurrency boundaries.
///
/// Repositories return these value-type snapshots rather than FluentKit models, which are mutable reference
/// types that must not escape the database's execution context.
public struct Example: Sendable, Equatable {
// MARK: Properties
/// The record's primary key, or `nil` if it has never been saved.
public let id: UUID?
/// The record's name.
public let name: String
}
/// Reads and writes ``ExampleRecord`` rows through the default database.
///
/// This is the shape every real repository takes: it holds the `Sendable` `Fluent` service, resolves the
/// default database per call, and maps FluentKit models to `Sendable` snapshots before returning so no
/// model ever escapes across an async boundary. It is reference scaffolding paired with ``ExampleRecord``;
/// replace it with the first real repository once a domain model is defined.
public struct ExampleRepository: Sendable {
// MARK: Properties
/// The service providing the default database the repository reads and writes through.
private let fluent: Fluent
// MARK: Initializers
/// Creates a repository backed by the given `Fluent` service.
/// - Parameter fluent: the service whose default database the repository operates on.
public init(fluent: Fluent) {
self.fluent = fluent
}
// MARK: Methods
/// Inserts a record with the given name.
/// - Parameter name: the name of the record to insert.
/// - Returns: a `Sendable` snapshot of the inserted record, including its assigned identifier.
public func create(name: String) async throws -> Example {
let record = ExampleRecord(name: name)
try await record.save(on: fluent.db())
return Example(id: record.id, name: record.name)
}
/// Fetches every record, ordered by name.
/// - Returns: a `Sendable` snapshot of each record, sorted by name.
public func all() async throws -> [Example] {
try await ExampleRecord.query(on: fluent.db())
.sort(\.$name)
.all()
.map { Example(id: $0.id, name: $0.name) }
}
}