diff --git a/Packages/Persistence/Sources/Public/Methods/Service.swift b/Packages/Persistence/Sources/Public/Methods/Service.swift index 67bf4de..bf2dec2 100644 --- a/Packages/Persistence/Sources/Public/Methods/Service.swift +++ b/Packages/Persistence/Sources/Public/Methods/Service.swift @@ -2,16 +2,25 @@ 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 persistence backend to register. - private let driver: Driver + /// The resolved persistence backend to register. + private let backend: Backend /// The logger the database emits through. private let logger: Logger @@ -19,14 +28,27 @@ public struct Service: Sendable { // 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 - ) { - self.driver = driver + ) throws { + switch driver { + case .postgres(let configuration): + self.backend = .postgres( + configuration, + try configuration.tls.postgresTLS() + ) + case .inMemory: + self.backend = .inMemory + } + self.logger = logger } @@ -37,15 +59,14 @@ public struct Service: Sendable { /// 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. - /// - Throws: an error when the TLS context for the PostgreSQL backend cannot be built. /// - Returns: the configured `Fluent` service, ready to be added to the service group. - public func callAsFunction() throws -> Fluent { + public func callAsFunction() -> Fluent { let fluent = Fluent( logger: logger ) - switch driver { - case .postgres(let configuration): + switch backend { + case .postgres(let configuration, let tls): fluent.databases.use( .postgres( configuration: .init( @@ -54,7 +75,7 @@ public struct Service: Sendable { username: configuration.username, password: configuration.password, database: configuration.name, - tls: try configuration.tls.postgresTLS() + tls: tls ), maxConnectionsPerEventLoop: configuration.maxConnectionsPerEventLoop ), diff --git a/Packages/Persistence/Tests/Cases/Public/Methods/ProbeTests.swift b/Packages/Persistence/Tests/Cases/Public/Methods/ProbeTests.swift index 64971b6..9029fcd 100644 --- a/Packages/Persistence/Tests/Cases/Public/Methods/ProbeTests.swift +++ b/Packages/Persistence/Tests/Cases/Public/Methods/ProbeTests.swift @@ -15,11 +15,11 @@ struct ProbeTests { @Test func `reports a reachable database`() async throws { - let service = Service( + let service = try Service( driver: .inMemory, logger: Logger(label: "test") ) - let fluent = try service() + let fluent = service() let probe = Probe(fluent: fluent) let isReachable = await probe() @@ -33,7 +33,7 @@ struct ProbeTests { func `reports an unreachable database`() async throws { // Port 1 on the loopback interface has nothing listening, so the connection is refused // immediately instead of timing out. - let service = Service( + let service = try Service( driver: .postgres( .init( host: "127.0.0.1", @@ -47,7 +47,7 @@ struct ProbeTests { ), logger: Logger(label: "test") ) - let fluent = try service() + let fluent = service() let probe = Probe(fluent: fluent) let isReachable = await probe() diff --git a/Packages/Persistence/Tests/Cases/Public/Methods/ServiceTests.swift b/Packages/Persistence/Tests/Cases/Public/Methods/ServiceTests.swift index 6b84904..8c7afd7 100644 --- a/Packages/Persistence/Tests/Cases/Public/Methods/ServiceTests.swift +++ b/Packages/Persistence/Tests/Cases/Public/Methods/ServiceTests.swift @@ -15,12 +15,12 @@ struct ServiceTests { @Test func `registers an SQLite database as the default for the in-memory driver`() async throws { - let service = Service( + let service = try Service( driver: .inMemory, logger: Logger(label: "test") ) - let fluent = try service() + let fluent = service() let database = fluent.db() as? any SQLDatabase try await fluent.shutdown() @@ -34,7 +34,7 @@ struct ServiceTests { func `registers a PostgreSQL database as the default for the postgres 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( + let service = try Service( driver: .postgres( .init( host: "127.0.0.1", @@ -49,7 +49,7 @@ struct ServiceTests { logger: Logger(label: "test") ) - let fluent = try service() + let fluent = service() let database = fluent.db() as? any SQLDatabase try await fluent.shutdown() @@ -61,12 +61,12 @@ struct ServiceTests { @Test func `builds a usable in-memory database`() async throws { - let service = Service( + let service = try Service( driver: .inMemory, logger: Logger(label: "test") ) - let fluent = try service() + let fluent = service() do { let database = try #require(fluent.db() as? any SQLDatabase) @@ -116,12 +116,12 @@ private extension ServiceTests { revertAfter: Bool = false ) async throws { let prepareDB = PrepareDB() - let service = Service( + let service = try Service( driver: driver, logger: Logger(label: "test") ) - let fluent = try service() + let fluent = service() do { await prepareDB(for: fluent) diff --git a/Services/Website/Sources/App/Extensions/App+Build.swift b/Services/Website/Sources/App/Extensions/App+Build.swift index 61c5894..56b5caa 100644 --- a/Services/Website/Sources/App/Extensions/App+Build.swift +++ b/Services/Website/Sources/App/Extensions/App+Build.swift @@ -34,11 +34,11 @@ func application( logger.warning("String Catalog is \(isCatalogMissing ? "missing" : "undecodable"); pages will serve raw localization keys") } - let persistence = Service( + let persistence = try Service( driver: reader.driver, logger: logger ) - let fluent = try persistence() + let fluent = persistence() let fingerprintAssets = FingerprintAssets(logger: logger) let prepareDB = PrepareDB() @@ -88,12 +88,12 @@ func migration( serverName: reader.serverName, logLevel: reader.logLevel ) - let service = Service( + let service = try Service( driver: reader.driver, logger: logger ) - let fluent = try service() + let fluent = service() let prepareDB = PrepareDB() await prepareDB(for: fluent) diff --git a/Services/Website/Tests/Library/Cases/Public/Controllers/HealthControllerTests.swift b/Services/Website/Tests/Library/Cases/Public/Controllers/HealthControllerTests.swift index 5c9d190..3a89f10 100644 --- a/Services/Website/Tests/Library/Cases/Public/Controllers/HealthControllerTests.swift +++ b/Services/Website/Tests/Library/Cases/Public/Controllers/HealthControllerTests.swift @@ -35,11 +35,11 @@ struct HealthControllerTests { @Test func `serves ready at the readiness path when the database is reachable`() async throws { - let service = Service( + let service = try Service( driver: .inMemory, logger: Logger(label: "test") ) - let fluent = try service() + let fluent = service() do { try await app( @@ -69,7 +69,7 @@ struct HealthControllerTests { func `serves unavailable at the readiness path when the database is unreachable`() async throws { // Port 1 on the loopback interface has nothing listening, so the probe's connection is refused // immediately instead of timing out. - let service = Service( + let service = try Service( driver: .postgres( .init( host: "127.0.0.1", @@ -83,7 +83,7 @@ struct HealthControllerTests { ), logger: Logger(label: "test") ) - let fluent = try service() + let fluent = service() do { try await app(