Files
ccn/Services/Website/Sources/App/Extensions/ConfigReader+Properties.swift
T
javier dc6b22e648 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>
2026-07-11 09:15:58 +00:00

183 lines
6.3 KiB
Swift

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
}
}
}