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,7 +2,7 @@
"name": "swift-manual-d-dev",
"image": "git.housh.dev/michael/swift-dev-container:latest",
"features": {
"ghcr.io/devcontainers/features/sshd:1": {},
//"ghcr.io/devcontainers/features/sshd:1": {},
"ghcr.io/devcontainers/features/git:1": {
"version": "os-provided",
"ppa": "false"
@@ -14,7 +14,7 @@
"ghcr.io/rocker-org/devcontainer-features/pandoc:1": {},
//"ghcr.io/devcontainers/features/docker-in-docker:2": {},
"ghcr.io/wxw-matt/devcontainer-features/apt:latest": {
"packages": "weasyprint gnupg2"
"packages": "weasyprint gnupg2 tmux"
}
},
"runArgs": [
@@ -22,6 +22,9 @@
"--security-opt",
"seccomp=unconfined"
],
"remoteEnv": {
"TERM": "xterm-256color"
},
"remoteUser": "swift",
"forwardPorts": [8080],
}

View File

@@ -1,5 +1,5 @@
{
"originHash" : "ed354a8e92f6b810403986d192b495a0e8e67cc9577e8ec24bce4ba275c0513d",
"originHash" : "b6e6af1076a5bcce49e1231c44be25d770eaef278e2d1ce1c961446d49cb2d00",
"pins" : [
{
"identity" : "async-http-client",
@@ -73,6 +73,15 @@
"version" : "1.53.0"
}
},
{
"identity" : "fluent-postgres-driver",
"kind" : "remoteSourceControl",
"location" : "https://github.com/vapor/fluent-postgres-driver.git",
"state" : {
"revision" : "59bff45a41d1ece1950bb8a6e0006d88c1fb6e69",
"version" : "2.12.0"
}
},
{
"identity" : "fluent-sqlite-driver",
"kind" : "remoteSourceControl",
@@ -100,6 +109,24 @@
"version" : "0.14.0"
}
},
{
"identity" : "postgres-kit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/vapor/postgres-kit.git",
"state" : {
"revision" : "7c079553e9cda74811e627775bf22e40a9405ad9",
"version" : "2.15.1"
}
},
{
"identity" : "postgres-nio",
"kind" : "remoteSourceControl",
"location" : "https://github.com/vapor/postgres-nio.git",
"state" : {
"revision" : "d578b86fb2c8321b114d97cd70831d1a3e9531a6",
"version" : "1.30.1"
}
},
{
"identity" : "routing-kit",
"kind" : "remoteSourceControl",

View File

@@ -8,7 +8,7 @@ let package = Package(
.executable(name: "App", targets: ["App"]),
.library(name: "AuthClient", targets: ["AuthClient"]),
.library(name: "DatabaseClient", targets: ["DatabaseClient"]),
.library(name: "EnvClient", targets: ["EnvClient"]),
.library(name: "EnvVars", targets: ["EnvVars"]),
.library(name: "FileClient", targets: ["FileClient"]),
.library(name: "HTMLSnapshotTesting", targets: ["HTMLSnapshotTesting"]),
.library(name: "PdfClient", targets: ["PdfClient"]),
@@ -22,6 +22,7 @@ let package = Package(
.package(url: "https://github.com/vapor/vapor.git", from: "4.110.1"),
.package(url: "https://github.com/vapor/fluent.git", from: "4.9.0"),
.package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.6.0"),
.package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.0.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"),
.package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.0.0"),
.package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.12.0"),
@@ -44,6 +45,7 @@ let package = Package(
.product(name: "Dependencies", package: "swift-dependencies"),
.product(name: "Fluent", package: "fluent"),
.product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"),
.product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"),
.product(name: "Vapor", package: "vapor"),
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "NIOPosix", package: "swift-nio"),
@@ -81,7 +83,7 @@ let package = Package(
]
),
.target(
name: "EnvClient",
name: "EnvVars",
dependencies: [
.product(name: "Dependencies", package: "swift-dependencies"),
.product(name: "DependenciesMacros", package: "swift-dependencies"),
@@ -105,7 +107,7 @@ let package = Package(
.target(
name: "PdfClient",
dependencies: [
.target(name: "EnvClient"),
.target(name: "EnvVars"),
.target(name: "FileClient"),
.target(name: "ManualDCore"),
.product(name: "Dependencies", package: "swift-dependencies"),

View File

@@ -7,12 +7,7 @@
'Noto Color Emoji';
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
monospace;
--color-red-500: oklch(63.7% 0.237 25.331);
--color-red-600: oklch(57.7% 0.245 27.325);
--color-green-400: oklch(79.2% 0.209 151.711);
--color-indigo-600: oklch(51.1% 0.262 276.966);
--color-slate-300: oklch(86.9% 0.022 252.894);
--color-slate-900: oklch(20.8% 0.042 265.755);
--color-gray-200: oklch(92.8% 0.006 264.531);
--color-gray-400: oklch(70.7% 0.022 261.325);
--color-black: #000;
@@ -30,7 +25,6 @@
--text-3xl--line-height: calc(2.25 / 1.875);
--font-weight-bold: 700;
--radius-sm: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
--ease-out: cubic-bezier(0, 0, 0.2, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);

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",

View File

@@ -15,7 +15,7 @@ func withDatabase(
) async throws {
let app = try await Application.make(.testing)
do {
try await configure(app)
try await configure(app, in: .live())
let database = app.db
try await app.autoMigrate()