Merge branch 'setup' into template
This commit is contained in:
@@ -42,7 +42,7 @@ struct App {
|
||||
return
|
||||
}
|
||||
|
||||
let app = await application(
|
||||
let app = try await application(
|
||||
reader: reader
|
||||
)
|
||||
|
||||
|
||||
@@ -12,13 +12,14 @@ import WebsiteLibrary
|
||||
/// 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 warns when the localization catalog cannot be read, since pages would serve raw localization keys.
|
||||
/// 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
|
||||
/// and stops alongside the HTTP server; the ephemeral in-memory backend is migrated on startup, while a PostgreSQL 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.
|
||||
/// - Throws: an error when the persistence service cannot be built (e.g. its TLS context fails to build).
|
||||
func application(
|
||||
reader: ConfigReader
|
||||
) async -> some ApplicationProtocol {
|
||||
) async throws -> some ApplicationProtocol {
|
||||
let languages = LanguageList()
|
||||
let logger = logger(
|
||||
serverName: reader.serverName,
|
||||
@@ -33,7 +34,7 @@ func application(
|
||||
logger.warning("String Catalog is \(isCatalogMissing ? "missing" : "undecodable"); pages will serve raw localization keys")
|
||||
}
|
||||
|
||||
let persistence = Service(
|
||||
let persistence = try Service(
|
||||
driver: reader.driver,
|
||||
logger: logger
|
||||
)
|
||||
@@ -63,7 +64,7 @@ func application(
|
||||
|
||||
app.addServices(fluent)
|
||||
|
||||
// The in-memory backend is recreated on every launch, so it is migrated on startup. The MySQL/MariaDB backend is
|
||||
// The in-memory backend is recreated on every launch, so it is migrated on startup. The PostgreSQL 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 {
|
||||
@@ -77,7 +78,7 @@ func application(
|
||||
/// 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
|
||||
/// migrations, and shuts the database down — so a shared PostgreSQL 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(
|
||||
@@ -87,7 +88,7 @@ func migration(
|
||||
serverName: reader.serverName,
|
||||
logLevel: reader.logLevel
|
||||
)
|
||||
let service = Service(
|
||||
let service = try Service(
|
||||
driver: reader.driver,
|
||||
logger: logger
|
||||
)
|
||||
|
||||
@@ -14,6 +14,43 @@ package extension ConfigReader {
|
||||
|
||||
// MARK: Computed
|
||||
|
||||
/// The analytics tracker the landing page embeds, built from the `analytics.*` keys, or `nil` when `analytics.websiteID` resolves
|
||||
/// empty — a deployment disables analytics entirely by clearing the identifier.
|
||||
///
|
||||
/// The script URL is not configurable: its origin is single-sourced in `String.Analytics`, so the tracker tag and the
|
||||
/// `Content-Security-Policy` that must allow it derive from one constant and cannot drift apart.
|
||||
///
|
||||
/// The `analytics.domains` filter must name the host the pages are served from — i.e. the host of ``siteOrigin``. The two keys are
|
||||
/// independent, so a deployment that overrides `site.origin` without matching `analytics.domains` reports from a host it no longer
|
||||
/// serves and records nothing; change them together.
|
||||
///
|
||||
/// Recorder mode is on by default — the pages embed the session recorder script alongside the tracker — and the `analytics.recorder`
|
||||
/// flag turns it off for a deployment. The recorder loads from the same origin as the tracker, so the `Content-Security-Policy` needs
|
||||
/// no extra allowance.
|
||||
var analytics: Analytics? {
|
||||
let websiteID = string(
|
||||
forKey: .Analytics.websiteID,
|
||||
default: .Analytics.websiteID
|
||||
)
|
||||
|
||||
guard !websiteID.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return .init(
|
||||
scriptURL: .Analytics.scriptURL,
|
||||
websiteID: websiteID,
|
||||
domains: string(
|
||||
forKey: .Analytics.domains,
|
||||
default: .Analytics.domains
|
||||
),
|
||||
recorder: bool(
|
||||
forKey: .Analytics.recorder,
|
||||
default: true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/// 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
|
||||
@@ -59,16 +96,16 @@ package extension ConfigReader {
|
||||
|
||||
/// 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.
|
||||
/// 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.
|
||||
var driver: Driver {
|
||||
switch string(
|
||||
forKey: .Database.driver,
|
||||
default: .Database.driver
|
||||
) {
|
||||
case .Database.driverMySQL:
|
||||
return .mysql(
|
||||
case .Database.driverPostgres:
|
||||
return .postgres(
|
||||
.init(
|
||||
host: string(
|
||||
forKey: .Database.host,
|
||||
@@ -94,7 +131,11 @@ package extension ConfigReader {
|
||||
maxConnectionsPerEventLoop: int(
|
||||
forKey: .Database.poolMaxPerEventLoop,
|
||||
default: .Database.poolMaxPerEventLoop
|
||||
)
|
||||
),
|
||||
poolTimeout: .seconds(int(
|
||||
forKey: .Database.poolTimeout,
|
||||
default: .Database.poolTimeout
|
||||
))
|
||||
)
|
||||
)
|
||||
default:
|
||||
@@ -199,7 +240,7 @@ 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
|
||||
/// 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`.
|
||||
var tls: TLS {
|
||||
switch string(
|
||||
|
||||
+13
-2
@@ -1,6 +1,15 @@
|
||||
import Configuration
|
||||
|
||||
extension AbsoluteConfigKey {
|
||||
/// A namespace for the analytics configuration keys, as absolute keys.
|
||||
public enum Analytics {
|
||||
/// The absolute configuration key for the analytics website identifier.
|
||||
public static let websiteID: AbsoluteConfigKey = .init(.Analytics.websiteID)
|
||||
/// The absolute configuration key for the comma-delimited domains the tracker reports from.
|
||||
public static let domains: AbsoluteConfigKey = .init(.Analytics.domains)
|
||||
/// The absolute configuration key for recorder mode, loading the session recorder script alongside the tracker.
|
||||
public static let recorder: AbsoluteConfigKey = .init(.Analytics.recorder)
|
||||
}
|
||||
/// A namespace for the static files cache configuration keys, as absolute keys.
|
||||
public enum Cache {
|
||||
/// The absolute configuration key for the max-age, in seconds, applied to fingerprinted assets and fonts.
|
||||
@@ -23,9 +32,9 @@ extension AbsoluteConfigKey {
|
||||
public static let migrate: AbsoluteConfigKey = .init(.Database.migrate)
|
||||
/// The absolute configuration key for the persistence driver.
|
||||
public static let driver: AbsoluteConfigKey = .init(.Database.driver)
|
||||
/// The absolute configuration key for the MySQL/MariaDB host.
|
||||
/// The absolute configuration key for the PostgreSQL host.
|
||||
public static let host: AbsoluteConfigKey = .init(.Database.host)
|
||||
/// The absolute configuration key for the MySQL/MariaDB port.
|
||||
/// The absolute configuration key for the PostgreSQL port.
|
||||
public static let port: AbsoluteConfigKey = .init(.Database.port)
|
||||
/// The absolute configuration key for the database name.
|
||||
public static let name: AbsoluteConfigKey = .init(.Database.name)
|
||||
@@ -37,6 +46,8 @@ extension AbsoluteConfigKey {
|
||||
public static let tls: AbsoluteConfigKey = .init(.Database.tls)
|
||||
/// The absolute configuration key for the maximum pooled connections per event loop.
|
||||
public static let poolMaxPerEventLoop: AbsoluteConfigKey = .init(.Database.poolMaxPerEventLoop)
|
||||
/// The absolute configuration key for the longest wait, in seconds, for a pooled connection to become available.
|
||||
public static let poolTimeout: AbsoluteConfigKey = .init(.Database.poolTimeout)
|
||||
}
|
||||
/// A namespace for the HTTP server configuration keys, as absolute keys.
|
||||
public enum HTTP {
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import Configuration
|
||||
|
||||
extension ConfigKey {
|
||||
/// A namespace for the analytics configuration keys.
|
||||
public enum Analytics {
|
||||
/// The configuration key for the analytics website identifier (cleared to disable analytics).
|
||||
public static let websiteID: ConfigKey = "analytics.websiteID"
|
||||
/// The configuration key for the comma-delimited domains the tracker reports from.
|
||||
public static let domains: ConfigKey = "analytics.domains"
|
||||
/// The configuration key for recorder mode, loading the session recorder script alongside the tracker (set to `false` to disable).
|
||||
public static let recorder: ConfigKey = "analytics.recorder"
|
||||
}
|
||||
/// A namespace for the static files cache configuration keys.
|
||||
public enum Cache {
|
||||
/// The configuration key for the max-age, in seconds, applied to fingerprinted assets (CSS, JavaScript) and fonts.
|
||||
@@ -21,11 +30,11 @@ extension ConfigKey {
|
||||
public enum Database {
|
||||
/// The configuration key selecting migrate-and-exit mode (run migrations, then exit) instead of serving.
|
||||
public static let migrate: ConfigKey = "database.migrate"
|
||||
/// The configuration key for the persistence driver (`inMemory` or `mysql`).
|
||||
/// The configuration key for the persistence driver (`inMemory` or `postgres`).
|
||||
public static let driver: ConfigKey = "database.driver"
|
||||
/// The configuration key for the MySQL/MariaDB host.
|
||||
/// The configuration key for the PostgreSQL host.
|
||||
public static let host: ConfigKey = "database.host"
|
||||
/// The configuration key for the MySQL/MariaDB port.
|
||||
/// The configuration key for the PostgreSQL port.
|
||||
public static let port: ConfigKey = "database.port"
|
||||
/// The configuration key for the database name.
|
||||
public static let name: ConfigKey = "database.name"
|
||||
@@ -37,6 +46,8 @@ extension ConfigKey {
|
||||
public static let tls: ConfigKey = "database.tls"
|
||||
/// The configuration key for the maximum pooled connections per event loop.
|
||||
public static let poolMaxPerEventLoop: ConfigKey = "database.pool.maxPerEventLoop"
|
||||
/// The configuration key for the longest wait, in seconds, for a pooled connection to become available.
|
||||
public static let poolTimeout: ConfigKey = "database.pool.timeout"
|
||||
}
|
||||
/// A namespace for the HTTP server configuration keys.
|
||||
public enum HTTP {
|
||||
|
||||
@@ -17,9 +17,11 @@ extension Int {
|
||||
}
|
||||
/// A namespace for the persistence's default configuration values.
|
||||
public enum Database {
|
||||
/// The default MySQL/MariaDB port.
|
||||
public static let port = 3_306
|
||||
/// The default PostgreSQL port.
|
||||
public static let port = 5_432
|
||||
/// The default maximum pooled connections per event loop.
|
||||
public static let poolMaxPerEventLoop = 4
|
||||
/// The default longest wait, in seconds, for a pooled connection to become available (the driver's own default).
|
||||
public static let poolTimeout = 10
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,28 @@
|
||||
extension String {
|
||||
/// A namespace for the analytics default configuration values.
|
||||
public enum Analytics {
|
||||
/// The origin the analytics scripts are loaded from and their beacons are sent to (scheme and host, no trailing slash).
|
||||
///
|
||||
/// Single-sourced here: both ``scriptURL`` and the session recorder script the pages embed in recorder mode derive from this
|
||||
/// constant, and the site's `Content-Security-Policy` must allow it.
|
||||
public static let origin = "https://analytics.rock-n-code.com"
|
||||
/// The URL the analytics tracker script is loaded from.
|
||||
public static let scriptURL = "\(origin)/script"
|
||||
/// The default analytics website identifier the tracker reports as.
|
||||
public static let websiteID = "f28681d6-20e8-43f3-9c3b-5d6a0f8e0591"
|
||||
/// The default comma-delimited domains the tracker reports from; visits from any other host are ignored.
|
||||
///
|
||||
/// Keep it paired with the host the pages are served at: a deployment that serves from another host without overriding
|
||||
/// `analytics.domains` to match reports from a host it no longer serves, so analytics silently records nothing.
|
||||
public static let domains = "loud.amsterdam"
|
||||
}
|
||||
/// A namespace for the persistence's default configuration values and recognized tokens.
|
||||
public enum Database {
|
||||
/// The default persistence driver: in-memory SQLite, which needs no external infrastructure.
|
||||
public static let driver = "inMemory"
|
||||
/// The driver token selecting the MySQL/MariaDB backend.
|
||||
public static let driverMySQL = "mysql"
|
||||
/// The default MySQL/MariaDB host.
|
||||
/// The driver token selecting the PostgreSQL backend.
|
||||
public static let driverPostgres = "postgres"
|
||||
/// The default PostgreSQL host.
|
||||
public static let host = "localhost"
|
||||
/// The default database name.
|
||||
public static let name = "site"
|
||||
|
||||
Reference in New Issue
Block a user