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

View File

@@ -1,5 +1,5 @@
{ {
"originHash" : "ed354a8e92f6b810403986d192b495a0e8e67cc9577e8ec24bce4ba275c0513d", "originHash" : "b6e6af1076a5bcce49e1231c44be25d770eaef278e2d1ce1c961446d49cb2d00",
"pins" : [ "pins" : [
{ {
"identity" : "async-http-client", "identity" : "async-http-client",
@@ -73,6 +73,15 @@
"version" : "1.53.0" "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", "identity" : "fluent-sqlite-driver",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
@@ -100,6 +109,24 @@
"version" : "0.14.0" "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", "identity" : "routing-kit",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",

View File

@@ -8,7 +8,7 @@ let package = Package(
.executable(name: "App", targets: ["App"]), .executable(name: "App", targets: ["App"]),
.library(name: "AuthClient", targets: ["AuthClient"]), .library(name: "AuthClient", targets: ["AuthClient"]),
.library(name: "DatabaseClient", targets: ["DatabaseClient"]), .library(name: "DatabaseClient", targets: ["DatabaseClient"]),
.library(name: "EnvClient", targets: ["EnvClient"]), .library(name: "EnvVars", targets: ["EnvVars"]),
.library(name: "FileClient", targets: ["FileClient"]), .library(name: "FileClient", targets: ["FileClient"]),
.library(name: "HTMLSnapshotTesting", targets: ["HTMLSnapshotTesting"]), .library(name: "HTMLSnapshotTesting", targets: ["HTMLSnapshotTesting"]),
.library(name: "PdfClient", targets: ["PdfClient"]), .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/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.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-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/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-dependencies", from: "1.0.0"),
.package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.12.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: "Dependencies", package: "swift-dependencies"),
.product(name: "Fluent", package: "fluent"), .product(name: "Fluent", package: "fluent"),
.product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"), .product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"),
.product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"),
.product(name: "Vapor", package: "vapor"), .product(name: "Vapor", package: "vapor"),
.product(name: "NIOCore", package: "swift-nio"), .product(name: "NIOCore", package: "swift-nio"),
.product(name: "NIOPosix", package: "swift-nio"), .product(name: "NIOPosix", package: "swift-nio"),
@@ -81,7 +83,7 @@ let package = Package(
] ]
), ),
.target( .target(
name: "EnvClient", name: "EnvVars",
dependencies: [ dependencies: [
.product(name: "Dependencies", package: "swift-dependencies"), .product(name: "Dependencies", package: "swift-dependencies"),
.product(name: "DependenciesMacros", package: "swift-dependencies"), .product(name: "DependenciesMacros", package: "swift-dependencies"),
@@ -105,7 +107,7 @@ let package = Package(
.target( .target(
name: "PdfClient", name: "PdfClient",
dependencies: [ dependencies: [
.target(name: "EnvClient"), .target(name: "EnvVars"),
.target(name: "FileClient"), .target(name: "FileClient"),
.target(name: "ManualDCore"), .target(name: "ManualDCore"),
.product(name: "Dependencies", package: "swift-dependencies"), .product(name: "Dependencies", package: "swift-dependencies"),

View File

@@ -7,12 +7,7 @@
'Noto Color Emoji'; 'Noto Color Emoji';
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
monospace; 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-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-200: oklch(92.8% 0.006 264.531);
--color-gray-400: oklch(70.7% 0.022 261.325); --color-gray-400: oklch(70.7% 0.022 261.325);
--color-black: #000; --color-black: #000;
@@ -30,7 +25,6 @@
--text-3xl--line-height: calc(2.25 / 1.875); --text-3xl--line-height: calc(2.25 / 1.875);
--font-weight-bold: 700; --font-weight-bold: 700;
--radius-sm: 0.25rem; --radius-sm: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem; --radius-lg: 0.5rem;
--ease-out: cubic-bezier(0, 0, 0.2, 1); --ease-out: cubic-bezier(0, 0, 0.2, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);

View File

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

View File

@@ -1,7 +1,9 @@
import DatabaseClient import DatabaseClient
import Dependencies import Dependencies
import Elementary import Elementary
import EnvVars
import Fluent import Fluent
import FluentPostgresDriver
import FluentSQLiteDriver import FluentSQLiteDriver
import ManualDCore import ManualDCore
import NIOSSL import NIOSSL
@@ -14,12 +16,15 @@ import ViewController
// configures your application // configures your application
public func configure( public func configure(
_ app: Application, _ app: Application,
in environment: EnvVars,
makeDatabaseClient: @escaping (any Database) -> DatabaseClient = { .live(database: $0) } makeDatabaseClient: @escaping (any Database) -> DatabaseClient = { .live(database: $0) }
) async throws { ) async throws {
// Setup the database client. // 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. // Add the global middlewares.
addMiddleware(to: app, database: databaseClient) addMiddleware(to: app, database: databaseClient, environment: environment)
#if DEBUG #if DEBUG
// Live reload of the application for development when launched with the `./swift-dev` command // Live reload of the application for development when launched with the `./swift-dev` command
// app.lifecycle.use(BrowserSyncHandler()) // app.lifecycle.use(BrowserSyncHandler())
@@ -33,7 +38,11 @@ public func configure(
addCommands(to: app) 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` // cors middleware should come before default error middleware using `at: .beginning`
let corsConfiguration = CORSMiddleware.Configuration( let corsConfiguration = CORSMiddleware.Configuration(
allowedOrigin: .all, 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(FileMiddleware(publicDirectory: app.directory.publicDirectory))
app.middleware.use(app.sessions.middleware) app.middleware.use(app.sessions.middleware)
app.middleware.use(DependenciesMiddleware(database: databaseClient)) app.middleware.use(DependenciesMiddleware(database: databaseClient, environment: environment))
} }
private func setupDatabase( private func setupDatabase(
on app: Application, on app: Application,
environment: EnvVars,
factory makeDatabaseClient: @escaping (any Database) -> DatabaseClient factory makeDatabaseClient: @escaping (any Database) -> DatabaseClient
) async throws -> DatabaseClient { ) async throws -> DatabaseClient {
switch app.environment { switch app.environment {
case .production, .development: case .production:
let dbFileName = Environment.get("SQLITE_FILENAME") ?? "db.sqlite" 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) app.databases.use(DatabaseConfigurationFactory.sqlite(.file(dbFileName)), as: .sqlite)
default: default:
app.databases.use(DatabaseConfigurationFactory.sqlite(.memory), as: .sqlite) app.databases.use(DatabaseConfigurationFactory.sqlite(.memory), as: .sqlite)
@@ -129,3 +142,30 @@ private func siteHandler(
return try await viewController.respond(route: route, request: request) 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 DatabaseClient
import Dependencies import Dependencies
import EnvVars
import Logging import Logging
import NIOCore import NIOCore
import NIOPosix 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. // 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. // 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. // If enabled, you should be careful about calling async functions before this point as it can cause assertion failures.
// let executorTakeoverSuccess = NIOSingletons.unsafeTryInstallSingletonPosixEventLoopGroupAsConcurrencyGlobalExecutor() let executorTakeoverSuccess =
// app.logger.debug("Tried to install SwiftNIO's EventLoopGroup as Swift's global concurrency executor", metadata: ["success": .stringConvertible(executorTakeoverSuccess)]) NIOSingletons.unsafeTryInstallSingletonPosixEventLoopGroupAsConcurrencyGlobalExecutor()
app.logger.debug(
"Tried to install SwiftNIO's EventLoopGroup as Swift's global concurrency executor",
metadata: ["success": .stringConvertible(executorTakeoverSuccess)]
)
do { do {
try await configure(app) try await configure(app, in: EnvVars.live())
} catch { } catch {
app.logger.report(error: error) app.logger.report(error: error)
try? await app.asyncShutdown() 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 Dependencies
import DependenciesMacros import DependenciesMacros
import Elementary import Elementary
import EnvClient import EnvVars
import FileClient import FileClient
import Foundation import Foundation
import ManualDCore import ManualDCore
@@ -46,9 +46,8 @@ extension PdfClient: DependencyKey {
}, },
generatePdf: { projectID, html in generatePdf: { projectID, html in
@Dependency(\.fileClient) var fileClient @Dependency(\.fileClient) var fileClient
@Dependency(\.env) var env @Dependency(\.environment) var environment
let envVars = try env()
let baseUrl = "/tmp/\(projectID)" let baseUrl = "/tmp/\(projectID)"
try await fileClient.writeFile(html.render(), "\(baseUrl).html") try await fileClient.writeFile(html.render(), "\(baseUrl).html")
@@ -57,10 +56,10 @@ extension PdfClient: DependencyKey {
let standardOutput = Pipe() let standardOutput = Pipe()
process.standardInput = standardInput process.standardInput = standardInput
process.standardOutput = standardOutput process.standardOutput = standardOutput
process.executableURL = URL(fileURLWithPath: envVars.pandocPath) process.executableURL = URL(fileURLWithPath: environment.pandocPath)
process.arguments = [ process.arguments = [
"\(baseUrl).html", "\(baseUrl).html",
"--pdf-engine=\(envVars.pdfEngine)", "--pdf-engine=\(environment.pdfEngine)",
"--from=html", "--from=html",
"--css=Public/css/pdf.css", "--css=Public/css/pdf.css",
"--output=\(baseUrl).pdf", "--output=\(baseUrl).pdf",

View File

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