feat: Updates environment variables and datbase to allow postgres configuration for production environments.
All checks were successful
CI / Linux Tests (push) Successful in 6m39s

This commit is contained in:
2026-02-03 09:41:31 -05:00
parent bad4a49f41
commit 3cc7fe9926
11 changed files with 202 additions and 101 deletions

View File

@@ -2,6 +2,7 @@
import AuthClient
import DatabaseClient
import Dependencies
import EnvVars
import ManualDCore
import PdfClient
import Vapor
@@ -14,16 +15,19 @@ struct DependenciesMiddleware: AsyncMiddleware {
private let values: DependencyValues.Continuation
// private let apiController: ApiController
private let database: DatabaseClient
private let environment: EnvVars
private let viewController: ViewController
init(
database: DatabaseClient,
environment: EnvVars,
// apiController: ApiController = .liveValue,
viewController: ViewController = .liveValue
) {
self.values = withEscapedDependencies { $0 }
// self.apiController = apiController
self.database = database
self.environment = environment
self.viewController = viewController
}
@@ -33,6 +37,7 @@ struct DependenciesMiddleware: AsyncMiddleware {
// $0.apiController = apiController
$0.auth = .live(on: request)
$0.database = database
$0.environment = environment
// $0.dateFormatter = .liveValue
$0.viewController = viewController
$0.pdfClient = .liveValue

View File

@@ -1,7 +1,9 @@
import DatabaseClient
import Dependencies
import Elementary
import EnvVars
import Fluent
import FluentPostgresDriver
import FluentSQLiteDriver
import ManualDCore
import NIOSSL
@@ -14,12 +16,15 @@ import ViewController
// configures your application
public func configure(
_ app: Application,
in environment: EnvVars,
makeDatabaseClient: @escaping (any Database) -> DatabaseClient = { .live(database: $0) }
) async throws {
// Setup the database client.
let databaseClient = try await setupDatabase(on: app, factory: makeDatabaseClient)
let databaseClient = try await setupDatabase(
on: app, environment: environment, factory: makeDatabaseClient
)
// Add the global middlewares.
addMiddleware(to: app, database: databaseClient)
addMiddleware(to: app, database: databaseClient, environment: environment)
#if DEBUG
// Live reload of the application for development when launched with the `./swift-dev` command
// app.lifecycle.use(BrowserSyncHandler())
@@ -33,7 +38,11 @@ public func configure(
addCommands(to: app)
}
private func addMiddleware(to app: Application, database databaseClient: DatabaseClient) {
private func addMiddleware(
to app: Application,
database databaseClient: DatabaseClient,
environment: EnvVars
) {
// cors middleware should come before default error middleware using `at: .beginning`
let corsConfiguration = CORSMiddleware.Configuration(
allowedOrigin: .all,
@@ -48,16 +57,20 @@ private func addMiddleware(to app: Application, database databaseClient: Databas
app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
app.middleware.use(app.sessions.middleware)
app.middleware.use(DependenciesMiddleware(database: databaseClient))
app.middleware.use(DependenciesMiddleware(database: databaseClient, environment: environment))
}
private func setupDatabase(
on app: Application,
environment: EnvVars,
factory makeDatabaseClient: @escaping (any Database) -> DatabaseClient
) async throws -> DatabaseClient {
switch app.environment {
case .production, .development:
let dbFileName = Environment.get("SQLITE_FILENAME") ?? "db.sqlite"
case .production:
let configuration = try environment.postgresConfiguration()
app.databases.use(.postgres(configuration: configuration), as: .psql)
case .development:
let dbFileName = environment.sqlitePath ?? "db.sqlite"
app.databases.use(DatabaseConfigurationFactory.sqlite(.file(dbFileName)), as: .sqlite)
default:
app.databases.use(DatabaseConfigurationFactory.sqlite(.memory), as: .sqlite)
@@ -129,3 +142,30 @@ private func siteHandler(
return try await viewController.respond(route: route, request: request)
}
}
extension EnvVars {
func postgresConfiguration() throws -> SQLPostgresConfiguration {
guard let hostname = postgresHostname,
let username = postgresUsername,
let password = postgresPassword,
let database = postgresDatabase
else {
throw EnvError("Missing environment variables for postgres connection.")
}
return .init(
hostname: hostname,
username: username,
password: password,
database: database,
tls: .disable
)
}
}
struct EnvError: Error {
let reason: String
init(_ reason: String) {
self.reason = reason
}
}

View File

@@ -1,5 +1,6 @@
import DatabaseClient
import Dependencies
import EnvVars
import Logging
import NIOCore
import NIOPosix
@@ -17,11 +18,15 @@ enum Entrypoint {
// You can enable it if you'd like to reduce the amount of context switching between NIO and Swift Concurrency.
// Note: this has caused issues with some libraries that use `.wait()` and cleanly shutting down.
// If enabled, you should be careful about calling async functions before this point as it can cause assertion failures.
// let executorTakeoverSuccess = NIOSingletons.unsafeTryInstallSingletonPosixEventLoopGroupAsConcurrencyGlobalExecutor()
// app.logger.debug("Tried to install SwiftNIO's EventLoopGroup as Swift's global concurrency executor", metadata: ["success": .stringConvertible(executorTakeoverSuccess)])
let executorTakeoverSuccess =
NIOSingletons.unsafeTryInstallSingletonPosixEventLoopGroupAsConcurrencyGlobalExecutor()
app.logger.debug(
"Tried to install SwiftNIO's EventLoopGroup as Swift's global concurrency executor",
metadata: ["success": .stringConvertible(executorTakeoverSuccess)]
)
do {
try await configure(app)
try await configure(app, in: EnvVars.live())
} catch {
app.logger.report(error: error)
try? await app.asyncShutdown()

View File

@@ -1,74 +0,0 @@
import Dependencies
import DependenciesMacros
import Foundation
extension DependencyValues {
/// Holds values defined in the process environment that are needed.
///
/// These are generally loaded from a `.env` file, but also have default values,
/// if not found.
public var env: @Sendable () throws -> EnvVars {
get { self[EnvClient.self].env }
set { self[EnvClient.self].env = newValue }
}
}
@DependencyClient
struct EnvClient: Sendable {
public var env: @Sendable () throws -> EnvVars
}
/// Holds values defined in the process environment that are needed.
///
/// These are generally loaded from a `.env` file, but also have default values,
/// if not found.
public struct EnvVars: Codable, Equatable, Sendable {
/// The path to the pandoc executable on the system, used to generate pdf's.
public let pandocPath: String
/// The pdf engine to use with pandoc when creating pdf's.
public let pdfEngine: String
public init(
pandocPath: String = "/usr/bin/pandoc",
pdfEngine: String = "weasyprint"
) {
self.pandocPath = pandocPath
self.pdfEngine = pdfEngine
}
enum CodingKeys: String, CodingKey {
case pandocPath = "PANDOC_PATH"
case pdfEngine = "PDF_ENGINE"
}
}
extension EnvClient: DependencyKey {
static let testValue = Self()
static let liveValue = Self(env: {
// Convert default values into a dictionary.
let defaults =
(try? encoder.encode(EnvVars()))
.flatMap { try? decoder.decode([String: String].self, from: $0) }
?? [:]
// Merge the default values with values found in process environment.
let assigned = defaults.merging(ProcessInfo.processInfo.environment, uniquingKeysWith: { $1 })
return (try? JSONSerialization.data(withJSONObject: assigned))
.flatMap { try? decoder.decode(EnvVars.self, from: $0) }
?? .init()
})
}
private let encoder: JSONEncoder = {
JSONEncoder()
}()
private let decoder: JSONDecoder = {
JSONDecoder()
}()

View File

@@ -0,0 +1,100 @@
import Dependencies
import DependenciesMacros
import Foundation
extension DependencyValues {
/// Holds values defined in the process environment that are needed.
///
/// These are generally loaded from a `.env` file, but also have default values,
/// if not found.
public var environment: EnvVars {
get { self[EnvVars.self] }
set { self[EnvVars.self] = newValue }
}
}
/// Holds values defined in the process environment that are needed.
///
/// These are generally loaded from a `.env` file, but also have default values,
/// if not found.
public struct EnvVars: Codable, Equatable, Sendable {
/// The path to the pandoc executable on the system, used to generate pdf's.
public let pandocPath: String
/// The pdf engine to use with pandoc when creating pdf's.
public let pdfEngine: String
/// The postgres hostname, used for production database connection.
public let postgresHostname: String?
/// The postgres username, used for production database connection.
public let postgresUsername: String?
/// The postgres password, used for production database connection.
public let postgresPassword: String?
/// The postgres database, used for production database connection.
public let postgresDatabase: String?
/// The path to the sqlite database, used for development database connection.
public let sqlitePath: String?
public init(
pandocPath: String = "/usr/bin/pandoc",
pdfEngine: String = "weasyprint",
postgresHostname: String? = "localhost",
postgresUsername: String? = "vapor",
postgresPassword: String? = "super-secret",
postgresDatabase: String? = "vapor",
sqlitePath: String? = "db.sqlite"
) {
self.pandocPath = pandocPath
self.pdfEngine = pdfEngine
self.postgresHostname = postgresHostname
self.postgresUsername = postgresUsername
self.postgresPassword = postgresPassword
self.postgresDatabase = postgresDatabase
self.sqlitePath = sqlitePath
}
enum CodingKeys: String, CodingKey {
case pandocPath = "PANDOC_PATH"
case pdfEngine = "PDF_ENGINE"
case postgresHostname = "POSTGRES_HOSTNAME"
case postgresUsername = "POSTGRES_USERNAME"
case postgresPassword = "POSTGRES_PASSWORD"
case postgresDatabase = "POSTGRES_DATABASE"
case sqlitePath = "SQLITE_PATH"
}
public static func live(_ env: [String: String] = ProcessInfo.processInfo.environment) -> Self {
// Convert default values into a dictionary.
let defaults =
(try? encoder.encode(EnvVars()))
.flatMap { try? decoder.decode([String: String].self, from: $0) }
?? [:]
// Merge the default values with values found in process environment.
let assigned = defaults.merging(env, uniquingKeysWith: { $1 })
return (try? JSONSerialization.data(withJSONObject: assigned))
.flatMap { try? decoder.decode(EnvVars.self, from: $0) }
?? .init()
}
}
extension EnvVars: TestDependencyKey {
public static let testValue = Self()
}
private let encoder: JSONEncoder = {
JSONEncoder()
}()
private let decoder: JSONDecoder = {
JSONDecoder()
}()

View File

@@ -1,7 +1,7 @@
import Dependencies
import DependenciesMacros
import Elementary
import EnvClient
import EnvVars
import FileClient
import Foundation
import ManualDCore
@@ -46,9 +46,8 @@ extension PdfClient: DependencyKey {
},
generatePdf: { projectID, html in
@Dependency(\.fileClient) var fileClient
@Dependency(\.env) var env
@Dependency(\.environment) var environment
let envVars = try env()
let baseUrl = "/tmp/\(projectID)"
try await fileClient.writeFile(html.render(), "\(baseUrl).html")
@@ -57,10 +56,10 @@ extension PdfClient: DependencyKey {
let standardOutput = Pipe()
process.standardInput = standardInput
process.standardOutput = standardOutput
process.executableURL = URL(fileURLWithPath: envVars.pandocPath)
process.executableURL = URL(fileURLWithPath: environment.pandocPath)
process.arguments = [
"\(baseUrl).html",
"--pdf-engine=\(envVars.pdfEngine)",
"--pdf-engine=\(environment.pdfEngine)",
"--from=html",
"--css=Public/css/pdf.css",
"--output=\(baseUrl).pdf",