Initial commit.

This commit is contained in:
2026-08-19 23:19:08 +02:00
commit 22e737d9c2
153 changed files with 11680 additions and 0 deletions
@@ -0,0 +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.
public enum Driver: Sendable {
/// A PostgreSQL server, reached with the given connection parameters.
///
/// - Parameter configuration: the host, credentials, TLS posture, and pooling limits the connection is opened with.
case postgres(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.
case inMemory
}
@@ -0,0 +1,41 @@
import NIOSSL
import PostgresNIO
/// 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 PostgreSQL driver
/// receives the resulting connection TLS mode through ``postgresTLS()``.
public enum TLS: Sendable {
/// Connect without TLS, in plaintext.
case off
/// Connect over TLS when the server offers it, falling back to plaintext otherwise.
case prefer
/// Connect only over TLS, refusing the connection when the server offers none.
case require
}
// MARK: - Methods
extension TLS {
/// The connection TLS mode passed to the PostgreSQL driver for this posture.
///
/// Returns `.disable` for ``off`` (connect in plaintext) and the default client configuration for ``prefer`` and ``require``. The driver enforces
/// both semantics natively: `prefer` upgrades to TLS only when the server advertises support and continues in plaintext otherwise, while `require`
/// refuses the connection when the server offers no TLS.
///
/// - Throws: an error when the TLS context cannot be built from the default client configuration.
/// - Returns: the connection TLS mode for this posture.
func postgresTLS() throws -> PostgresConnection.Configuration.TLS {
switch self {
case .off: .disable
case .prefer: .prefer(try NIOSSLContext(configuration: .makeClientConfiguration()))
case .require: .require(try NIOSSLContext(configuration: .makeClientConfiguration()))
}
}
}
@@ -0,0 +1,29 @@
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: Sendable {
// MARK: Initializers
/// Creates a registrar for the migrations for a `Fluent` service.
public init() {}
// MARK: Methods
/// 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.
public func callAsFunction(
for fluent: Fluent
) async {
await fluent.migrations.add([
CreateExampleRecord()
])
}
}
@@ -0,0 +1,77 @@
import HummingbirdFluent
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()`.
public struct Probe: Sendable {
// MARK: Properties
/// The `Fluent` service whose default database is probed.
private let fluent: Fluent
/// The longest the probe waits for the database's answer before reporting it as not reachable.
private let timeout: Duration
// MARK: Initializers
/// Creates a probe for the default database of the given `Fluent` service.
/// - Parameters:
/// - fluent: the `Fluent` service whose default database is probed.
/// - timeout: the longest the probe waits for the database's answer before reporting it as not reachable.
public init(
fluent: Fluent,
timeout: Duration = .seconds(2)
) {
self.fluent = fluent
self.timeout = timeout
}
// MARK: Methods
/// Reports whether the database behind the `Fluent` service is reachable.
///
/// Runs a trivial `SELECT 1` against the default database the cheapest statement both the PostgreSQL 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, and an answer that does not arrive within the timeout is likewise reported as not reachable so
/// a database that hangs yields a prompt "not ready" instead of a hanging readiness endpoint. A default database that is not an SQL database is
/// also reported as not reachable.
/// - Returns: `true` when the database answers the probe in time, `false` otherwise.
public func callAsFunction() async -> Bool {
guard let database = fluent.db() as? any SQLDatabase else {
return false
}
// The query is raced against the deadline from unstructured tasks whose first answer wins: a structured group
// would await the query child before returning, so a database that hangs mid-query the very failure the
// deadline exists for would hang the probe with it. The loser is cancelled and abandoned; a late answer lands
// in a finished stream and is dropped.
let (answers, continuation) = AsyncStream.makeStream(of: Bool.self)
let query = Task {
do {
try await database
.raw("SELECT 1")
.run()
continuation.yield(true)
} catch {
continuation.yield(false)
}
}
let deadline = Task {
try? await Task.sleep(for: timeout)
continuation.yield(false)
}
var answer = answers.makeAsyncIterator()
let isReachable = await answer.next() ?? false
query.cancel()
deadline.cancel()
return isReachable
}
}
@@ -0,0 +1,99 @@
import FluentPostgresDriver
import FluentSQLiteDriver
import HummingbirdFluent
import Logging
import PostgresNIO
/// 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()`.
public struct Service: Sendable {
// MARK: Enumerations
/// The persistence backend resolved at construction, with the PostgreSQL TLS mode already built.
private enum Backend {
case postgres(Configuration, PostgresConnection.Configuration.TLS)
case inMemory
}
// MARK: Properties
/// The resolved persistence backend to register.
private let backend: Backend
/// The logger the database emits through.
private let logger: Logger
// MARK: Initializers
/// Creates a factory for a `Fluent` service backed by the given driver.
///
/// The TLS context for the PostgreSQL backend is built here, once the factory holds only resolved configuration, so producing the
/// service afterwards cannot fail.
/// - Parameters:
/// - driver: the persistence backend to register.
/// - logger: the logger the database emits through.
/// - Throws: an error when the TLS context for the PostgreSQL backend cannot be built.
public init(
driver: Driver,
logger: Logger
) throws {
switch driver {
case .postgres(let configuration):
self.backend = .postgres(
configuration,
try configuration.tls.postgresTLS()
)
case .inMemory:
self.backend = .inMemory
}
self.logger = logger
}
// MARK: Methods
/// 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.
/// - Returns: the configured `Fluent` service, ready to be added to the service group.
public func callAsFunction() -> Fluent {
let fluent = Fluent(
logger: logger
)
switch backend {
case .postgres(let configuration, let tls):
fluent.databases.use(
.postgres(
configuration: .init(
hostname: configuration.host,
port: configuration.port,
username: configuration.username,
password: configuration.password,
database: configuration.name,
tls: tls
),
maxConnectionsPerEventLoop: configuration.maxConnectionsPerEventLoop,
connectionPoolTimeout: .init(configuration.poolTimeout)
),
as: .psql,
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.
fluent.databases.use(
.sqlite(.memory, maxConnectionsPerEventLoop: 1),
as: .sqlite,
isDefault: true
)
}
return fluent
}
}
@@ -0,0 +1,65 @@
/// The connection parameters for the PostgreSQL backend.
///
/// 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
/// The host the database server is reached at.
let host: String
/// The maximum number of pooled connections opened per event loop.
let maxConnectionsPerEventLoop: Int
/// The name of the database to open.
let name: String
/// The password the connection authenticates with.
let password: String
/// The longest a query waits for a pooled connection to become available before failing.
let poolTimeout: Duration
/// The port the database server listens on.
let port: Int
/// The TLS posture used when connecting.
let tls: TLS
/// The username the connection authenticates as.
let username: String
// MARK: Initializers
/// Creates a set of PostgreSQL connection parameters.
/// - Parameters:
/// - host: the host the database server is reached at.
/// - port: the port the database server listens on.
/// - name: the name of the database to open.
/// - username: the username the connection authenticates as.
/// - password: the password the connection authenticates with.
/// - tls: the TLS posture used when connecting.
/// - maxConnectionsPerEventLoop: the maximum number of pooled connections opened per event loop.
/// - poolTimeout: the longest a query waits for a pooled connection to become available before failing.
public init(
host: String,
port: Int,
name: String,
username: String,
password: String,
tls: TLS,
maxConnectionsPerEventLoop: Int,
poolTimeout: Duration
) {
self.host = host
self.maxConnectionsPerEventLoop = maxConnectionsPerEventLoop
self.name = name
self.password = password
self.poolTimeout = poolTimeout
self.port = port
self.tls = tls
self.username = username
}
}