Tweaks and fixes throughout the project (#27)

This PR contains the work done to do a little bit of housekeeping pass across all packages and the Website service.

To provide further details about the work:
* Refreshed the READMEs and source documentation to match the current code;
* Tagged every test case consistently across the Infrastructure, Localization, Persistence, and Website test targets;
* Removed Website middleware tests now covered by Infrastructure's own suite;
* Conformed the `PrepareDB` method to Sendable;
* Relaxes the production Compose DATABASE_TLS default from require to prefer;
* Added Persistence test verifying the prefer posture falls back to plaintext connections.

Reviewed-on: rock-n-code/loud-amsterdam#27
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-30 06:33:57 +00:00
committed by javier
parent 0887135328
commit ea00b94841
71 changed files with 633 additions and 512 deletions
@@ -1,20 +1,17 @@
/// The persistence backend the service runs against.
///
/// The executable picks a driver at startup and hands it to ``Service``, which registers the
/// matching database as the default one. Repositories resolve that default and stay agnostic
/// of which backend is in use.
/// The executable picks a driver at startup and hands it to ``Service``, which registers the matching database as the default one. Repositories resolve
/// that default and stay agnostic of which backend is in use.
public enum Driver: Sendable {
/// A MySQL/MariaDB server, reached with the given connection parameters.
///
/// - Parameter configuration: the host, credentials, TLS posture, and pooling limits the
/// connection is opened with.
/// - Parameter configuration: the host, credentials, TLS posture, and pooling limits the connection is opened with.
case mysql(Configuration)
/// An ephemeral, in-process SQLite database held entirely in memory.
///
/// Nothing is written to disk, and all data is lost when the service stops intended for
/// local development and tests.
/// Nothing is written to disk, and all data is lost when the service stops intended for local development and tests.
case inMemory
}
@@ -2,9 +2,8 @@ import NIOSSL
/// The TLS posture used when connecting to the database.
///
/// The executable derives a posture from its `database.tls` configuration and passes it along as
/// part of ``Configuration``; the MySQL driver receives the resulting `TLSConfiguration` through
/// ``tlsConfiguration``.
/// The executable derives a posture from its `database.tls` configuration and passes it along as part of ``Configuration``; the MySQL driver
/// receives the resulting `TLSConfiguration` through ``tlsConfiguration``.
public enum TLS: Sendable {
/// Connect without TLS, in plaintext.
@@ -14,6 +13,9 @@ public enum TLS: Sendable {
case prefer
/// Connect only over TLS, refusing the connection when the server offers none.
///
/// - Important: the refusal is not yet enforced until it is, `require` behaves like ``prefer`` and silently falls back to plaintext when the
/// server offers no TLS.
case require
}
@@ -24,18 +26,15 @@ extension TLS {
/// The NIO TLS configuration passed to the MySQL driver for this posture.
///
/// Returns `nil` for ``off`` (connect in plaintext) and the default client configuration for
/// ``prefer`` and ``require``.
/// Returns `nil` for ``off`` (connect in plaintext) and the default client configuration for ``prefer`` and ``require``.
///
/// - Note: `prefer` and `require` currently map to the same client configuration both enable TLS.
/// The distinction (fall back to plaintext vs. fail when the server offers no TLS) is not yet
/// enforced here; tighten this mapping if that guarantee becomes required.
/// - Note: the driver gives a supplied configuration ``prefer`` semantics natively it upgrades to TLS only when the server advertises support,
/// and continues in plaintext otherwise so `prefer` is fully enforced. `require` maps to the same configuration and therefore currently
/// behaves like ``prefer``: the refusal when the server offers no TLS is not yet enforced.
var tlsConfiguration: TLSConfiguration? {
switch self {
case .off:
return nil
case .prefer, .require:
return .makeClientConfiguration()
case .off: nil
default: .makeClientConfiguration()
}
}
@@ -2,9 +2,9 @@ import HummingbirdFluent
/// A registrar declaring every migration against a `Fluent` service.
///
/// Built once around the application's `Fluent` service and called as a function `await migrate()`
/// during startup, before the migrations are applied.
public struct PrepareDB {
/// Built once around the application's `Fluent` service and called as a function `await migrate()` during startup, before the migrations are
/// applied.
public struct PrepareDB: Sendable {
// MARK: Initializers
@@ -15,9 +15,9 @@ public struct PrepareDB {
/// Registers every migration against the `Fluent` service, in order.
///
/// This is the single place migrations are declared: add each new migration here, in the order it must
/// run (migrations are applied in registration order and are append-only). Registering does not apply
/// them the caller runs `fluent.migrate()` (or the executable's migrate-and-exit mode) to do that.
/// This is the single place migrations are declared: add each new migration here, in the order it must run (migrations are applied in registration order
/// and are append-only). Registering does not apply them the caller runs `fluent.migrate()` (or the executable's migrate-and-exit mode) to do
/// that.
public func callAsFunction(
for fluent: Fluent
) async {
@@ -3,8 +3,8 @@ import SQLKit
/// A readiness probe reporting whether the database behind a `Fluent` service is reachable.
///
/// Built once around the application's `Fluent` service and called as a function whenever a fresh
/// answer is needed typically from a readiness endpoint: `let ready = await probe()`.
/// Built once around the application's `Fluent` service and called as a function whenever a fresh answer is needed typically from a readiness endpoint:
/// `let ready = await probe()`.
public struct Probe: Sendable {
// MARK: Properties
@@ -26,11 +26,10 @@ public struct Probe: Sendable {
/// Reports whether the database behind the `Fluent` service is reachable.
///
/// Runs a trivial `SELECT 1` against the default database the cheapest statement both the MySQL/MariaDB
/// and SQLite backends understand so a readiness check does not depend on any particular schema or model.
/// Any failure (connection refused, authentication error, pool exhausted) is reported as not reachable
/// rather than thrown, so callers can map it straight onto a readiness response. A default database that
/// is not an SQL database is likewise reported as not reachable.
/// Runs a trivial `SELECT 1` against the default database the cheapest statement both the MySQL/MariaDB and SQLite backends understand so
/// a readiness check does not depend on any particular schema or model. Any failure (connection refused, authentication error, pool exhausted) is
/// reported as not reachable rather than thrown, so callers can map it straight onto a readiness response. A default database that is not an SQL
/// database is likewise reported as not reachable.
/// - Returns: `true` when the database answers the probe, `false` otherwise.
public func callAsFunction() async -> Bool {
guard let database = fluent.db() as? any SQLDatabase else {
@@ -5,8 +5,7 @@ import Logging
/// A factory building the `Fluent` service the application persists through.
///
/// Built once around the driver the executable picks at startup and called as a function to produce
/// the configured service: `let fluent = service()`.
/// Built once around the driver the executable picks at startup and called as a function to produce the configured service: `let fluent = service()`.
public struct Service: Sendable {
// MARK: Properties
@@ -35,10 +34,9 @@ public struct Service: Sendable {
/// Builds a `Fluent` service configured for the driver.
///
/// The selected backend is registered as the *default* database, so repositories resolve it with a plain
/// `fluent.db()` and stay agnostic of which driver is in use. The returned service is not yet running; add
/// it to the application's service group (`app.addServices(_:)`) so it starts and shuts its connection pool
/// down alongside the server.
/// The selected backend is registered as the *default* database, so repositories resolve it with a plain `fluent.db()` and stay agnostic of which
/// driver is in use. The returned service is not yet running; add it to the application's service group (`app.addServices(_:)`) so it starts and shuts
/// its connection pool down alongside the server.
/// - Returns: the configured `Fluent` service, ready to be added to the service group.
public func callAsFunction() -> Fluent {
let fluent = Fluent(
@@ -63,8 +61,8 @@ public struct Service: Sendable {
isDefault: true
)
case .inMemory:
// A single connection keeps every query pointed at the same in-memory store,
// rather than each pooled connection getting its own private database.
// A single connection keeps every query pointed at the same in-memory store, rather than each pooled
// connection getting its own private database.
fluent.databases.use(
.sqlite(.memory, maxConnectionsPerEventLoop: 1),
as: .sqlite,
@@ -1,7 +1,6 @@
/// The connection parameters for the MySQL/MariaDB backend.
///
/// The executable builds this from its `database.*` configuration; the package itself reads no
/// configuration, so these values arrive as plain data.
/// The executable builds this from its `database.*` configuration; the package itself reads no configuration, so these values arrive as plain data.
public struct Configuration: Sendable {
// MARK: Properties