Initial commit.
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user