Database setup for the Website service (#13)
This PR contains the work done to introduce a _Fluent_-based persistence layer for the Website service, selectable at runtime alongside the existing in-memory default, plus the local dev tooling and docs to support it. To provide further details about the work: * Persistence package * The `Driver` and `TLS` enumerations * The `Configuration` type * The `Service` factory that builds the service * `PrepareDB` for migrations registration * The `Probe` for readiness checks. * App integration * Builds the driver, registers migrations, and attaches `Fluent` to the service lifecycle so it starts/stops with the HTTP server. * Migrate-on-boot is gated to the in-memory backend; MySQL/MariaDB is migrated out of band via --database-migrate so shared databases never race on startup. * The `ConfigReader+Properties` extension maps database.* config keys onto the driver. * Library * Added database configuration constants. * The `HealthController` controller gains a readiness probe: `GET /health/ready` checks whether the database is reachable, separate from the existing liveness check. * Others * Updated the `docker-compose` files to support a database service behind a database profile, and hardened for local development * New database targets on the `Makefile` file and overall documentation updated * Updated the `.env.local`, `Dockerfile`, and `README` files to document the persistence workflow, config keys, and local DB commands Reviewed-on: rock-n-code/loud-amsterdam#13 Co-authored-by: Javier Cicchelli <javier@rock-n-code.com> Co-committed-by: Javier Cicchelli <javier@rock-n-code.com>
This commit is contained in:
@@ -1,203 +0,0 @@
|
||||
import Configuration
|
||||
import Hummingbird
|
||||
import HummingbirdCompression
|
||||
import Logging
|
||||
import WebsiteCore
|
||||
|
||||
/// Builds the website application.
|
||||
///
|
||||
/// Reads the log level, server name, static files location, minimum response size to compress, and
|
||||
/// security headers from the configuration, then assembles the router, server configuration, and logger.
|
||||
/// - Parameter reader: the configuration reader the values are read from.
|
||||
/// - Returns: the configured application, ready to run as a service.
|
||||
func application(
|
||||
reader: ConfigReader
|
||||
) async -> some ApplicationProtocol {
|
||||
let cacheControl = cacheControl(
|
||||
textMaxAge: reader.int(
|
||||
forKey: .Cache.maxAgeText,
|
||||
default: .Cache.maxAgeText
|
||||
),
|
||||
imageMaxAge: reader.int(
|
||||
forKey: .Cache.maxAgeImage,
|
||||
default: .Cache.maxAgeImage
|
||||
),
|
||||
defaultMaxAge: reader.int(
|
||||
forKey: .Cache.maxAgeDefault,
|
||||
default: .Cache.maxAgeDefault
|
||||
)
|
||||
)
|
||||
let compressionMinResponseSize = reader.int(
|
||||
forKey: .Compression.minResponseSize,
|
||||
default: .Compression.minResponseSize
|
||||
)
|
||||
let logLevel = reader.string(
|
||||
forKey: .Log.level,
|
||||
as: Logger.Level.self,
|
||||
default: .info
|
||||
)
|
||||
let serverName = reader.string(
|
||||
forKey: .HTTP.serverName,
|
||||
default: .Server.name
|
||||
)
|
||||
let staticFilesPath = reader.string(
|
||||
forKey: .Path.staticFiles,
|
||||
default: .Path.staticResources
|
||||
)
|
||||
let securityHeaders = securityHeaders(
|
||||
reader: reader
|
||||
)
|
||||
|
||||
return Application(
|
||||
router: router(
|
||||
staticFilesPath: staticFilesPath,
|
||||
cacheControl: cacheControl,
|
||||
compressionMinResponseSize: compressionMinResponseSize,
|
||||
securityHeaders: securityHeaders,
|
||||
logLevel: logLevel
|
||||
),
|
||||
configuration: ApplicationConfiguration(
|
||||
reader: reader.scoped(to: "http")
|
||||
),
|
||||
logger: logger(
|
||||
serverName: serverName,
|
||||
logLevel: logLevel
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
// Request context used by application
|
||||
private typealias AppRequestContext = WebsiteRequestContext
|
||||
|
||||
/// Builds the cache-control policy applied to the served static files.
|
||||
///
|
||||
/// Static files are public and validated by `FileMiddleware` through their `ETag` and
|
||||
/// `Last-Modified` headers, so each media type is given a `max-age` after which the browser
|
||||
/// revalidates. Text-based assets (CSS, JavaScript) are additionally marked `must-revalidate`
|
||||
/// since they change between deployments while keeping their filenames.
|
||||
/// - Parameters:
|
||||
/// - textMaxAge: the max-age, in seconds, applied to text-based static files (CSS, JavaScript, plain text).
|
||||
/// - imageMaxAge: the max-age, in seconds, applied to image static files (ICO, PNG, SVG).
|
||||
/// - defaultMaxAge: the max-age, in seconds, applied to all other static files (e.g. the web manifest).
|
||||
/// - Returns: the configured cache-control policy.
|
||||
private func cacheControl(
|
||||
textMaxAge: Int,
|
||||
imageMaxAge: Int,
|
||||
defaultMaxAge: Int
|
||||
) -> CacheControl {
|
||||
.init([
|
||||
(.text, [.public, .maxAge(textMaxAge), .mustRevalidate]),
|
||||
(.image, [.public, .maxAge(imageMaxAge)]),
|
||||
(.init(type: .any), [.public, .maxAge(defaultMaxAge)]),
|
||||
])
|
||||
}
|
||||
|
||||
/// Builds the security-headers configuration applied to every response.
|
||||
///
|
||||
/// Each header value falls back to the hardened default in `String.Security` when the matching
|
||||
/// configuration key is unset. `Strict-Transport-Security` has no default: it is read as an optional
|
||||
/// and omitted entirely unless explicitly configured, so it stays off in plain-HTTP development and
|
||||
/// is enabled only behind TLS in production.
|
||||
/// - Parameter reader: the configuration reader the header values are read from.
|
||||
/// - Returns: the configured security-headers configuration.
|
||||
private func securityHeaders(
|
||||
reader: ConfigReader
|
||||
) -> SecurityHeadersMiddleware<AppRequestContext>.Configuration {
|
||||
.init(
|
||||
contentSecurityPolicy: reader.string(
|
||||
forKey: .Security.contentSecurityPolicy,
|
||||
default: .Security.contentSecurityPolicy
|
||||
),
|
||||
contentTypeOptions: reader.string(
|
||||
forKey: .Security.contentTypeOptions,
|
||||
default: .Security.contentTypeOptions
|
||||
),
|
||||
frameOptions: reader.string(
|
||||
forKey: .Security.frameOptions,
|
||||
default: .Security.frameOptions
|
||||
),
|
||||
referrerPolicy: reader.string(
|
||||
forKey: .Security.referrerPolicy,
|
||||
default: .Security.referrerPolicy
|
||||
),
|
||||
permissionsPolicy: reader.string(
|
||||
forKey: .Security.permissionsPolicy,
|
||||
default: .Security.permissionsPolicy
|
||||
),
|
||||
strictTransportSecurity: reader.string(
|
||||
forKey: .Security.strictTransportSecurity
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/// Builds the application's logger.
|
||||
/// - Parameters:
|
||||
/// - serverName: the label applied to the logger.
|
||||
/// - logLevel: the minimum level the logger emits.
|
||||
/// - Returns: the configured logger.
|
||||
private func logger(
|
||||
serverName: String,
|
||||
logLevel: Logger.Level
|
||||
) -> Logger {
|
||||
var logger = Logger(label: serverName)
|
||||
|
||||
logger.logLevel = logLevel
|
||||
|
||||
return logger
|
||||
}
|
||||
|
||||
/// Builds the application's router.
|
||||
///
|
||||
/// Registers the request-logging middleware, the security-headers middleware that stamps the given
|
||||
/// `securityHeaders` onto every response, the response-compression middleware that compresses
|
||||
/// responses larger than `minimumResponseSizeToCompress` when the client advertises support, the
|
||||
/// localization middleware that negotiates the request's language from its `Accept-Language` header,
|
||||
/// the not-found middleware that serves the error page, and the static file middleware that serves the
|
||||
/// contents of `staticFilesPath` (tagging responses with the given `cacheControl` directives), then
|
||||
/// adds the `RootController` routes that render the landing page and the `HealthController` routes
|
||||
/// that serve the health check.
|
||||
///
|
||||
/// The security-headers middleware sits just inside request logging so it covers every response that
|
||||
/// reaches a client — the landing page, the compressed responses, the rendered error page, and the
|
||||
/// served static files.
|
||||
/// - Parameters:
|
||||
/// - staticFilesPath: the folder, relative to the working directory, the static files are served from.
|
||||
/// - cacheControl: the cache-control directives applied to the served static files.
|
||||
/// - compressionMinResponseSize: the minimum response body size, in bytes, before compression is applied.
|
||||
/// - securityHeaders: the security headers applied to every response.
|
||||
/// - logLevel: the level the request-logging middleware logs at.
|
||||
/// - Returns: the configured router.
|
||||
private func router(
|
||||
staticFilesPath: String,
|
||||
cacheControl: CacheControl,
|
||||
compressionMinResponseSize: Int,
|
||||
securityHeaders: SecurityHeadersMiddleware<AppRequestContext>.Configuration,
|
||||
logLevel: Logger.Level
|
||||
) -> Router<AppRequestContext> {
|
||||
let router = Router(context: AppRequestContext.self)
|
||||
|
||||
router.addMiddleware {
|
||||
LogRequestsMiddleware(logLevel)
|
||||
SecurityHeadersMiddleware(
|
||||
configuration: securityHeaders
|
||||
)
|
||||
ResponseCompressionMiddleware(
|
||||
minimumResponseSizeToCompress: compressionMinResponseSize
|
||||
)
|
||||
LocalizationMiddleware()
|
||||
NotFoundMiddleware()
|
||||
FileMiddleware(
|
||||
staticFilesPath,
|
||||
cacheControl: cacheControl
|
||||
)
|
||||
}
|
||||
|
||||
router.addRoutes {
|
||||
RootController<AppRequestContext>().routes
|
||||
HealthController<AppRequestContext>().routes
|
||||
}
|
||||
|
||||
return router
|
||||
}
|
||||
@@ -2,8 +2,17 @@ import Configuration
|
||||
import Hummingbird
|
||||
import Logging
|
||||
|
||||
/// The entry point of the website executable.
|
||||
///
|
||||
/// Loads the configuration, then either serves the website or — when the `database.migrate` flag is set — runs the registered migrations against the
|
||||
/// configured backend and exits.
|
||||
@main
|
||||
struct App {
|
||||
|
||||
/// Loads the configuration and runs the mode it selects.
|
||||
///
|
||||
/// The configuration is read from the providers in precedence order: command-line arguments first, then process environment variables, then a
|
||||
/// `.env` file when one is present, and finally the in-memory defaults (currently just the server name).
|
||||
static func main() async throws {
|
||||
let reader = try await ConfigReader(
|
||||
providers: [
|
||||
@@ -19,10 +28,22 @@ struct App {
|
||||
]
|
||||
)
|
||||
|
||||
// Migrate-and-exit mode runs the registered migrations against the configured backend and returns,
|
||||
// so a shared database is migrated by a single deliberate invocation (`--database-migrate`) rather
|
||||
// than by every booting instance.
|
||||
guard !reader.migrate else {
|
||||
try await migration(
|
||||
reader: reader
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let app = await application(
|
||||
reader: reader
|
||||
)
|
||||
|
||||
try await app.runService()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import Configuration
|
||||
import Hummingbird
|
||||
import HummingbirdCompression
|
||||
import Logging
|
||||
import Persistence
|
||||
import WebsiteCore
|
||||
|
||||
/// Builds the website application.
|
||||
///
|
||||
/// Reads the log level, server name, static files location, minimum response size to compress, and security headers from the configuration, then assembles
|
||||
/// the router, server configuration, and logger. It also builds the persistence driver, registers its migrations, and attaches the `Fluent` service so it starts
|
||||
/// and stops alongside the HTTP server; the ephemeral in-memory backend is migrated on startup, while a MySQL/MariaDB backend is migrated out of
|
||||
/// band (so a shared database is never migrated on boot).
|
||||
/// - Parameter reader: the configuration reader the values are read from.
|
||||
/// - Returns: the configured application, ready to run as a service.
|
||||
func application(
|
||||
reader: ConfigReader
|
||||
) async -> some ApplicationProtocol {
|
||||
let logger = logger(
|
||||
serverName: reader.serverName,
|
||||
logLevel: reader.logLevel
|
||||
)
|
||||
let persistence = Service(
|
||||
driver: reader.driver,
|
||||
logger: logger
|
||||
)
|
||||
let fluent = persistence()
|
||||
let prepareDB = PrepareDB()
|
||||
|
||||
await prepareDB(for: fluent)
|
||||
|
||||
var app = Application(
|
||||
router: router(
|
||||
staticFilesPath: reader.staticFilesPath,
|
||||
cacheControl: reader.cacheControl,
|
||||
compressionMinResponseSize: reader.compressionMinResponseSize,
|
||||
securityHeaders: reader.securityHeaders,
|
||||
logLevel: reader.logLevel,
|
||||
probe: Probe(fluent: fluent)
|
||||
),
|
||||
configuration: ApplicationConfiguration(
|
||||
reader: reader.scoped(to: "http")
|
||||
),
|
||||
logger: logger
|
||||
)
|
||||
|
||||
app.addServices(fluent)
|
||||
|
||||
// The in-memory backend is recreated on every launch, so it is migrated on startup. The MySQL/MariaDB
|
||||
// backend is left untouched here: a shared database is migrated out of band to avoid multi-instance races.
|
||||
if case .inMemory = reader.driver {
|
||||
app.beforeServerStarts {
|
||||
try await fluent.migrate()
|
||||
}
|
||||
}
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
/// Runs every registered migration against the configured backend, then exits.
|
||||
///
|
||||
/// This is the out-of-band migration path selected by the `database.migrate` flag: it builds the same driver the service would run against, applies the
|
||||
/// migrations, and shuts the database down — so a shared MySQL/MariaDB database is migrated by a single deliberate invocation rather than by every
|
||||
/// booting instance.
|
||||
/// - Parameter reader: the configuration reader the values are read from.
|
||||
func migration(
|
||||
reader: ConfigReader
|
||||
) async throws {
|
||||
let logger = logger(
|
||||
serverName: reader.serverName,
|
||||
logLevel: reader.logLevel
|
||||
)
|
||||
let service = Service(
|
||||
driver: reader.driver,
|
||||
logger: logger
|
||||
)
|
||||
|
||||
let fluent = service()
|
||||
let prepareDB = PrepareDB()
|
||||
|
||||
await prepareDB(for: fluent)
|
||||
|
||||
do {
|
||||
try await fluent.migrate()
|
||||
}
|
||||
catch {
|
||||
try? await fluent.shutdown()
|
||||
|
||||
throw error
|
||||
}
|
||||
|
||||
try await fluent.shutdown()
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
/// The request context type the application serves its routes with.
|
||||
private typealias AppRequestContext = WebsiteRequestContext
|
||||
|
||||
/// Builds the application's logger.
|
||||
/// - Parameters:
|
||||
/// - serverName: the label applied to the logger.
|
||||
/// - logLevel: the minimum level the logger emits.
|
||||
/// - Returns: the configured logger.
|
||||
private func logger(
|
||||
serverName: String,
|
||||
logLevel: Logger.Level
|
||||
) -> Logger {
|
||||
var logger = Logger(label: serverName)
|
||||
|
||||
logger.logLevel = logLevel
|
||||
|
||||
return logger
|
||||
}
|
||||
|
||||
/// Builds the application's router.
|
||||
///
|
||||
/// Registers the request-logging middleware, the security-headers middleware that stamps the given `securityHeaders` onto every response, the
|
||||
/// response-compression middleware that compresses responses larger than `minimumResponseSizeToCompress` when the client advertises support,
|
||||
/// the localization middleware that negotiates the request's language from its `Accept-Language` header, the not-found middleware that serves the
|
||||
/// error page, and the static file middleware that serves the contents of `staticFilesPath` (tagging responses with the given `cacheControl`
|
||||
/// directives), then adds the `RootController` routes that render the landing page and the `HealthController` routes that serve the health check.
|
||||
///
|
||||
/// The security-headers middleware sits just inside request logging so it covers every response that reaches a client — the landing page, the compressed
|
||||
/// responses, the rendered error page, and the served static files.
|
||||
/// - Parameters:
|
||||
/// - staticFilesPath: the folder, relative to the working directory, the static files are served from.
|
||||
/// - cacheControl: the cache-control directives applied to the served static files.
|
||||
/// - compressionMinResponseSize: the minimum response body size, in bytes, before compression is applied.
|
||||
/// - securityHeaders: the security headers applied to every response.
|
||||
/// - logLevel: the level the request-logging middleware logs at.
|
||||
/// - probe: the probe consulted by the `HealthController` readiness route.
|
||||
/// - Returns: the configured router.
|
||||
private func router(
|
||||
staticFilesPath: String,
|
||||
cacheControl: CacheControl,
|
||||
compressionMinResponseSize: Int,
|
||||
securityHeaders: SecurityHeadersMiddleware<AppRequestContext>.Configuration,
|
||||
logLevel: Logger.Level,
|
||||
probe: Probe
|
||||
) -> Router<AppRequestContext> {
|
||||
let router = Router(context: AppRequestContext.self)
|
||||
|
||||
router.addMiddleware {
|
||||
LogRequestsMiddleware(logLevel)
|
||||
SecurityHeadersMiddleware(
|
||||
configuration: securityHeaders
|
||||
)
|
||||
ResponseCompressionMiddleware(
|
||||
minimumResponseSizeToCompress: compressionMinResponseSize
|
||||
)
|
||||
LocalizationMiddleware()
|
||||
NotFoundMiddleware()
|
||||
FileMiddleware(
|
||||
staticFilesPath,
|
||||
cacheControl: cacheControl
|
||||
)
|
||||
}
|
||||
|
||||
router.addRoutes {
|
||||
RootController<AppRequestContext>().routes
|
||||
HealthController<AppRequestContext>(probe: probe).routes
|
||||
}
|
||||
|
||||
return router
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
import Configuration
|
||||
import Hummingbird
|
||||
import Logging
|
||||
import Persistence
|
||||
import WebsiteCore
|
||||
|
||||
package extension ConfigReader {
|
||||
|
||||
// MARK: Type aliases
|
||||
|
||||
/// The request context type the application serves its routes with; the security headers configuration is generic over it.
|
||||
typealias AppRequestContext = WebsiteRequestContext
|
||||
|
||||
// MARK: Computed
|
||||
|
||||
/// The `Cache-Control` policy applied to static files, grouped by media type.
|
||||
///
|
||||
/// The max-ages are read from the `cache.maxAge.text`, `cache.maxAge.image`, and `cache.maxAge.default` keys. Text files (CSS,
|
||||
/// JavaScript, plain text) additionally require revalidation once stale; images and everything else are served public with their max-age alone.
|
||||
var cacheControl: CacheControl {
|
||||
let maxAgeDefault = int(
|
||||
forKey: .Cache.maxAgeDefault,
|
||||
default: .Cache.maxAgeDefault
|
||||
)
|
||||
let maxAgeImage = int(
|
||||
forKey: .Cache.maxAgeImage,
|
||||
default: .Cache.maxAgeImage
|
||||
)
|
||||
let maxAgeText = int(
|
||||
forKey: .Cache.maxAgeText,
|
||||
default: .Cache.maxAgeText
|
||||
)
|
||||
|
||||
return .init([
|
||||
(.text, [.public, .maxAge(maxAgeText), .mustRevalidate]),
|
||||
(.image, [.public, .maxAge(maxAgeImage)]),
|
||||
(.init(type: .any), [.public, .maxAge(maxAgeDefault)]),
|
||||
])
|
||||
}
|
||||
|
||||
/// The minimum response body size, in bytes, before a response is compressed — read from the `compression.minimumResponseSize` key.
|
||||
var compressionMinResponseSize: Int {
|
||||
int(
|
||||
forKey: .Compression.minResponseSize,
|
||||
default: .Compression.minResponseSize
|
||||
)
|
||||
}
|
||||
|
||||
/// The persistence backend the service runs against, derived from the `database.*` keys.
|
||||
///
|
||||
/// When `database.driver` selects MySQL, the connection parameters are assembled from the `database.host`, `database.port`,
|
||||
/// `database.name`, `database.username`, `database.password` (empty when unset), `database.tls`, and
|
||||
/// `database.pool.maxPerEventLoop` keys. Any other driver value falls back to the in-memory database.
|
||||
var driver: Driver {
|
||||
switch string(
|
||||
forKey: .Database.driver,
|
||||
default: .Database.driver
|
||||
) {
|
||||
case .Database.driverMySQL:
|
||||
return .mysql(
|
||||
.init(
|
||||
host: string(
|
||||
forKey: .Database.host,
|
||||
default: .Database.host
|
||||
),
|
||||
port: int(
|
||||
forKey: .Database.port,
|
||||
default: .Database.port
|
||||
),
|
||||
name: string(
|
||||
forKey: .Database.name,
|
||||
default: .Database.name
|
||||
),
|
||||
username: string(
|
||||
forKey: .Database.username,
|
||||
default: .Database.username
|
||||
),
|
||||
password: string(
|
||||
forKey: .Database.password,
|
||||
default: ""
|
||||
),
|
||||
tls: tls,
|
||||
maxConnectionsPerEventLoop: int(
|
||||
forKey: .Database.poolMaxPerEventLoop,
|
||||
default: .Database.poolMaxPerEventLoop
|
||||
)
|
||||
)
|
||||
)
|
||||
default:
|
||||
return .inMemory
|
||||
}
|
||||
}
|
||||
|
||||
/// The minimum log level the application emits at, read from the `log.level` key.
|
||||
///
|
||||
/// Falls back to `.info` when the key is unset or its value names no `Logger.Level` case.
|
||||
var logLevel: Logger.Level {
|
||||
string(
|
||||
forKey: .Log.level,
|
||||
as: Logger.Level.self,
|
||||
default: .info
|
||||
)
|
||||
}
|
||||
|
||||
/// Whether the executable runs in migrate-and-exit mode instead of serving, read from the `database.migrate` flag; off by default.
|
||||
var migrate: Bool {
|
||||
bool(
|
||||
forKey: .Database.migrate,
|
||||
default: false
|
||||
)
|
||||
}
|
||||
|
||||
/// The security headers middleware configuration, built from the `security.*` keys.
|
||||
///
|
||||
/// Every header value has a default except `Strict-Transport-Security`, which is only sent when `security.strictTransportSecurity`
|
||||
/// is set — the header is a commitment browsers cache, so it must be opted into for deployments actually served over HTTPS.
|
||||
var securityHeaders: SecurityHeadersMiddleware<AppRequestContext>.Configuration {
|
||||
.init(
|
||||
contentSecurityPolicy: string(
|
||||
forKey: .Security.contentSecurityPolicy,
|
||||
default: .Security.contentSecurityPolicy
|
||||
),
|
||||
contentTypeOptions: string(
|
||||
forKey: .Security.contentTypeOptions,
|
||||
default: .Security.contentTypeOptions
|
||||
),
|
||||
frameOptions: string(
|
||||
forKey: .Security.frameOptions,
|
||||
default: .Security.frameOptions
|
||||
),
|
||||
referrerPolicy: string(
|
||||
forKey: .Security.referrerPolicy,
|
||||
default: .Security.referrerPolicy
|
||||
),
|
||||
permissionsPolicy: string(
|
||||
forKey: .Security.permissionsPolicy,
|
||||
default: .Security.permissionsPolicy
|
||||
),
|
||||
strictTransportSecurity: string(
|
||||
forKey: .Security.strictTransportSecurity
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/// The name the server reports in its `Server` response header, read from the `http.serverName` key.
|
||||
var serverName: String {
|
||||
string(
|
||||
forKey: .HTTP.serverName,
|
||||
default: .Server.name
|
||||
)
|
||||
}
|
||||
|
||||
/// The directory the static files are served from, read from the `path.staticFiles` key.
|
||||
var staticFilesPath: String {
|
||||
string(
|
||||
forKey: .Path.staticFiles,
|
||||
default: .Path.staticResources
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private extension ConfigReader {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The TLS posture for the MySQL connection, mapped from the `database.tls` key: `off` and `require` map to their postures, and any
|
||||
/// other value falls back to `prefer`.
|
||||
var tls: TLS {
|
||||
switch string(
|
||||
forKey: .Database.tls,
|
||||
default: .Database.tls
|
||||
) {
|
||||
case .Database.tlsOff: .off
|
||||
case .Database.tlsRequire: .require
|
||||
default: .prefer
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user