Project updates from Template (#1)
This PR contains the latest updates from the generic Website template, which have been added while working on #loud-amsterdam. Reviewed-on: #1 Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
This commit was merged in pull request #1.
This commit is contained in:
@@ -56,10 +56,15 @@ package extension ConfigReader {
|
||||
/// The `Cache-Control` policy applied to static files, grouped by media type.
|
||||
///
|
||||
/// The max-ages are read from the `cache.maxAge.asset`, `cache.maxAge.text`, `cache.maxAge.image`, and
|
||||
/// `cache.maxAge.default` keys. Stylesheets and scripts are referenced through fingerprinted URLs (see `FingerprintAssets`) and
|
||||
/// fonts are immutable subset files, so all three are served long-lived and `immutable` — a deploy busts them by changing the URL, never by
|
||||
/// revalidation. The remaining text files (e.g. `robots.txt`) keep their unversioned URLs and require revalidation once stale; images and
|
||||
/// everything else are served public with their max-age alone. The groups match in order, so the specific types precede the `text` category.
|
||||
/// `cache.maxAge.default` keys. Stylesheets, scripts, videos, JPEGs, and WebPs are referenced through fingerprinted URLs (see
|
||||
/// `FingerprintAssets`) and fonts are immutable subset files, so all of them are served long-lived and `immutable` — a deploy busts them by
|
||||
/// changing the URL, never by revalidation. The remaining text files (e.g. `robots.txt`) keep their unversioned URLs and require revalidation
|
||||
/// once stale; the remaining images cannot be `immutable` — the group covers the icons, and a browser fetches `/favicon.ico` unversioned
|
||||
/// whatever the markup says — and everything else is served public with its max-age alone. The groups match in order, so the specific types
|
||||
/// precede the `text` and `image` categories.
|
||||
///
|
||||
/// - Important: an image the markup references without a `?v=` token must be in neither JPEG nor WebP, or it is served immutable for a year and
|
||||
/// a deploy cannot dislodge it.
|
||||
var cacheControl: CacheControl {
|
||||
let maxAgeAsset = int(
|
||||
forKey: .Cache.maxAgeAsset,
|
||||
@@ -82,6 +87,9 @@ package extension ConfigReader {
|
||||
(.textCss, [.public, .maxAge(maxAgeAsset), .immutable]),
|
||||
(.textJavascript, [.public, .maxAge(maxAgeAsset), .immutable]),
|
||||
(.font, [.public, .maxAge(maxAgeAsset), .immutable]),
|
||||
(.videoMp4, [.public, .maxAge(maxAgeAsset), .immutable]),
|
||||
(.imageJpeg, [.public, .maxAge(maxAgeAsset), .immutable]),
|
||||
(.imageWebp, [.public, .maxAge(maxAgeAsset), .immutable]),
|
||||
(.text, [.public, .maxAge(maxAgeText), .mustRevalidate]),
|
||||
(.image, [.public, .maxAge(maxAgeImage)]),
|
||||
(.init(type: .any), [.public, .maxAge(maxAgeDefault)]),
|
||||
@@ -100,51 +108,70 @@ package extension ConfigReader {
|
||||
///
|
||||
/// When `database.driver` selects PostgreSQL, the connection parameters are assembled from the `database.host`, `database.port`,
|
||||
/// `database.name`, `database.username`, `database.password` (empty when unset), `database.tls`,
|
||||
/// `database.pool.maxPerEventLoop`, and `database.pool.timeout` keys. Any other driver value falls back to the in-memory database.
|
||||
/// `database.pool.maxPerEventLoop`, and `database.pool.timeout` keys. Any token but `inMemory` and `postgres` throws a
|
||||
/// ``ConfigError``: a mistyped driver fails the boot rather than running on the in-memory database and discarding every write on restart.
|
||||
var driver: Driver {
|
||||
switch string(
|
||||
forKey: .Database.driver,
|
||||
default: .Database.driver
|
||||
) {
|
||||
case .Database.driverPostgres:
|
||||
return .postgres(
|
||||
.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
|
||||
),
|
||||
poolTimeout: .seconds(int(
|
||||
forKey: .Database.poolTimeout,
|
||||
default: .Database.poolTimeout
|
||||
))
|
||||
get throws {
|
||||
switch string(
|
||||
forKey: .Database.driver,
|
||||
default: .Database.driver
|
||||
) {
|
||||
case .Database.driverInMemory:
|
||||
return .inMemory
|
||||
case .Database.driverPostgres:
|
||||
return .postgres(
|
||||
.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: try tls,
|
||||
maxConnectionsPerEventLoop: int(
|
||||
forKey: .Database.poolMaxPerEventLoop,
|
||||
default: .Database.poolMaxPerEventLoop
|
||||
),
|
||||
poolTimeout: .seconds(int(
|
||||
forKey: .Database.poolTimeout,
|
||||
default: .Database.poolTimeout
|
||||
))
|
||||
)
|
||||
)
|
||||
)
|
||||
default:
|
||||
return .inMemory
|
||||
case let token:
|
||||
throw ConfigError.unknownDatabaseDriver(token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The HTTPS redirect middleware configuration, built from the `https.trustForwardedProto` key and ``siteOrigin``.
|
||||
///
|
||||
/// Off by default, so a deployment without a proxy in front never redirects on a header its clients could have written themselves. The redirects
|
||||
/// point at ``siteOrigin`` — the same value the pages build their canonical URLs from, so the two cannot disagree.
|
||||
var httpsRedirect: HTTPSRedirectMiddleware<AppRequestContext>.Configuration {
|
||||
.init(
|
||||
origin: siteOrigin,
|
||||
trustForwardedProto: bool(
|
||||
forKey: .HTTPS.trustForwardedProto,
|
||||
default: false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
@@ -164,7 +191,7 @@ package extension ConfigReader {
|
||||
)
|
||||
}
|
||||
|
||||
/// The rate limit applied to the subscription endpoint, built from the `rateLimit.*` keys.
|
||||
/// The rate limit built from the `rateLimit.*` keys; the template applies it to no route yet.
|
||||
///
|
||||
/// `rateLimit.limit` requests are admitted per client per `rateLimit.window` seconds. When `rateLimit.trustForwardedFor` is set,
|
||||
/// clients are keyed by the first `X-Forwarded-For` entry — enable it only behind a reverse proxy that sets the header, since clients can forge it
|
||||
@@ -226,6 +253,17 @@ package extension ConfigReader {
|
||||
)
|
||||
}
|
||||
|
||||
/// The public origin the site is served at (scheme and host, no trailing slash), read from the `site.origin` key.
|
||||
///
|
||||
/// The redirects and any absolute links derive from it, so a staging deployment can point it at itself — or leave it unset — without the
|
||||
/// production origin leaking into its markup.
|
||||
var siteOrigin: String {
|
||||
string(
|
||||
forKey: .Site.origin,
|
||||
default: .Site.origin
|
||||
)
|
||||
}
|
||||
|
||||
/// The directory the static files are served from, read from the `path.staticFiles` key.
|
||||
var staticFilesPath: String {
|
||||
string(
|
||||
@@ -242,16 +280,45 @@ private extension ConfigReader {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The TLS posture for the PostgreSQL connection, mapped from the `database.tls` key: `off` and `require` map to their postures, and any
|
||||
/// other value falls back to `prefer`.
|
||||
/// The TLS posture for the PostgreSQL connection, mapped from the `database.tls` key.
|
||||
///
|
||||
/// Any token but `off`, `prefer`, and `require` throws a ``ConfigError``: a mistyped posture (`required`, say) fails the boot rather than
|
||||
/// falling back to `prefer`, which hands the password over in plaintext when the upgrade is stripped.
|
||||
var tls: TLS {
|
||||
switch string(
|
||||
forKey: .Database.tls,
|
||||
default: .Database.tls
|
||||
) {
|
||||
case .Database.tlsOff: .off
|
||||
case .Database.tlsRequire: .require
|
||||
default: .prefer
|
||||
get throws {
|
||||
switch string(
|
||||
forKey: .Database.tls,
|
||||
default: .Database.tls
|
||||
) {
|
||||
case .Database.tlsOff: .off
|
||||
case .Database.tlsPrefer: .prefer
|
||||
case .Database.tlsRequire: .require
|
||||
case let token: throw ConfigError.unknownDatabaseTLS(token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - ConfigError
|
||||
|
||||
/// A configuration value the executable refuses to boot with; ``description`` names the tokens the key accepts.
|
||||
package enum ConfigError: Error, Equatable, CustomStringConvertible {
|
||||
|
||||
/// The `database.driver` key holds an unrecognized token.
|
||||
case unknownDatabaseDriver(String)
|
||||
|
||||
/// The `database.tls` key holds an unrecognized token.
|
||||
case unknownDatabaseTLS(String)
|
||||
|
||||
// MARK: Computed
|
||||
|
||||
package var description: String {
|
||||
switch self {
|
||||
case .unknownDatabaseDriver(let token):
|
||||
"Unknown 'database.driver' value '\(token)': use '\(String.Database.driverInMemory)' or '\(String.Database.driverPostgres)'."
|
||||
case .unknownDatabaseTLS(let token):
|
||||
"Unknown 'database.tls' value '\(token)': use '\(String.Database.tlsOff)', '\(String.Database.tlsPrefer)', or '\(String.Database.tlsRequire)'."
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user