import Configuration import Hummingbird import HummingbirdCompression import Logging import Persistence import Infrastructure import WebsiteLibrary /// 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.Configuration, logLevel: Logger.Level, probe: Probe ) -> Router { // HEAD siblings are generated for every GET route, so uptime monitors and crawlers probing // with HEAD requests get the page's status and headers instead of a 404. let router = Router( context: AppRequestContext.self, options: .autoGenerateHeadEndpoints ) router.addMiddleware { LogRequestsMiddleware(logLevel) SecurityHeadersMiddleware( configuration: securityHeaders ) ResponseCompressionMiddleware( minimumResponseSizeToCompress: compressionMinResponseSize ) LocalizationMiddleware() NotFoundMiddleware() FileMiddleware( staticFilesPath, cacheControl: cacheControl ) } router.addController { RootController() HealthController( probe: probe ) } return router }