import DatabaseClientLive import Dependencies import Elementary import Fluent import FluentSQLiteDriver import NIOSSL import SharedModels import Vapor import VaporElementary @preconcurrency import VaporRouting import ViewControllerLive // configures your application public func configure( _ app: Application, makeDatabaseClient: @escaping (any Database) -> DatabaseClient = { .live(database: $0) } ) async throws { // cors middleware should come before default error middleware using `at: .beginning` let corsConfiguration = CORSMiddleware.Configuration( allowedOrigin: .all, allowedMethods: [.GET, .POST, .PUT, .OPTIONS, .DELETE, .PATCH], allowedHeaders: [.accept, .authorization, .contentType, .origin, .xRequestedWith, .userAgent, .accessControlAllowOrigin] ) let cors = CORSMiddleware(configuration: corsConfiguration) app.middleware.use(cors, at: .beginning) app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory)) app.middleware.use(app.sessions.middleware) #if DEBUG app.lifecycle.use(BrowserSyncHandler()) #endif switch app.environment { case .production, .development: let dbFileName = Environment.get("SQLITE_FILENAME") ?? "db.sqlite" app.databases.use(DatabaseConfigurationFactory.sqlite(.file(dbFileName)), as: .sqlite) default: app.databases.use(DatabaseConfigurationFactory.sqlite(.memory), as: .sqlite) } let databaseClient = makeDatabaseClient(app.db) if app.environment != .testing { try await app.migrations.add(databaseClient.migrations()) } app.middleware.use(DependenciesMiddleware(database: databaseClient)) app.mount( SiteRoute.router, middleware: { if app.environment == .testing { return nil } else { return $0.middleware() } }, use: siteHandler ) if app.environment != .testing { try await app.autoMigrate() } #if DEBUG app.asyncCommands.use(SeedCommand(), as: "seed") #endif app.asyncCommands.use(GenerateAdminUserCommand(), as: "generate-admin") } extension SiteRoute { func middleware() -> [any Middleware]? { switch self { case let .api(route): return route.middleware case .health: return nil case let .view(route): // return nil return route.middleware } } } @Sendable func siteHandler( request: Request, route: SiteRoute ) async throws -> any AsyncResponseEncodable { switch route { case let .api(route): return try await route.handle(request: request) case .health: return HTTPStatus.ok case let .view(route): return try await route.respond(request: request) // return try await route.handle(request: request) } } extension ViewRoute { func respond(request: Request) async throws -> any AsyncResponseEncodable { if self == .index { return request.redirect(to: ViewRoute.router.path(for: .purchaseOrder(.index))) } else { let html = try await view(isHtmxRequest: request.isHtmxRequest, authenticate: { request.auth.login($0) }) // Delete routes return nil, but are valid routes. guard let html else { return HTTPStatus.ok } return AnyHTMLResponse(value: html) } } } struct AnyHTMLResponse: AsyncResponseEncodable { public var chunkSize: Int public var headers: HTTPHeaders = ["Content-Type": "text/html; charset=utf-8"] var value: _SendableAnyHTMLBox init(chunkSize: Int = 1024, additionalHeaders: HTTPHeaders = [:], value: any HTML & Sendable) { self.chunkSize = chunkSize if additionalHeaders.contains(name: .contentType) { self.headers = additionalHeaders } else { headers.add(contentsOf: additionalHeaders) } self.value = .init(value) } func encodeResponse(for request: Request) async throws -> Response { Response( status: .ok, headers: headers, body: .init(asyncStream: { [value, chunkSize] writer in guard let html = value.tryTake() else { assertionFailure("Non-sendable HTML value consumed more than once") request.logger.error("Non-sendable HTML value consumed more than once") throw Abort(.internalServerError) } try await writer.writeHTML(html, chunkSize: chunkSize) try await writer.write(.end) }) ) } }