From c8bcffa0b5059f559c3b269fa79106f64b4e231b Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Tue, 14 Jan 2025 09:19:04 -0500 Subject: [PATCH] feat: Minimal user api controller on hummingbird app, not sure there's big wins over vapor. --- Package.resolved | 11 ++- Package.swift | 8 ++- Sources/HApp/App+build.swift | 68 ++++++++++++------- Sources/HApp/App.swift | 20 ++++-- .../HApp/Controllers/Api/UserController.swift | 28 ++++++++ 5 files changed, 105 insertions(+), 30 deletions(-) create mode 100644 Sources/HApp/Controllers/Api/UserController.swift diff --git a/Package.resolved b/Package.resolved index 942fc23..cef9859 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "68600988594927fcb25434476f598e1a813cb64030b7d9d8bf5b480e13a11989", + "originHash" : "0c4aa955a400075a593dfba3804996f803c9e812e765f72d0026e12c182b751c", "pins" : [ { "identity" : "async-http-client", @@ -82,6 +82,15 @@ "version" : "2.0.2" } }, + { + "identity" : "hummingbird-fluent", + "kind" : "remoteSourceControl", + "location" : "https://github.com/hummingbird-project/hummingbird-fluent.git", + "state" : { + "revision" : "45459ea5b541c6a96b87d1be848e384593b7dde3", + "version" : "2.0.0" + } + }, { "identity" : "leaf", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 42f9447..53b0c67 100644 --- a/Package.swift +++ b/Package.swift @@ -27,7 +27,8 @@ let package = Package( .package(url: "https://github.com/pointfreeco/swift-dependencies.git", from: "1.6.3"), .package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0"), .package(url: "https://github.com/hummingbird-project/hummingbird-auth.git", from: "2.0.2"), - .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0") + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"), + .package(url: "https://github.com/hummingbird-project/hummingbird-fluent.git", from: "2.0.0") ], targets: [ .executableTarget( @@ -56,8 +57,11 @@ let package = Package( .executableTarget( name: "HApp", dependencies: [ + "DatabaseClientLive", .product(name: "Hummingbird", package: "hummingbird"), - .product(name: "ArgumentParser", package: "swift-argument-parser") + .product(name: "HummingbirdFluent", package: "hummingbird-fluent"), + .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver") ], swiftSettings: swiftSettings ), diff --git a/Sources/HApp/App+build.swift b/Sources/HApp/App+build.swift index 765e3a2..7406000 100644 --- a/Sources/HApp/App+build.swift +++ b/Sources/HApp/App+build.swift @@ -1,65 +1,87 @@ +import DatabaseClient +import DatabaseClientLive +import FluentSQLiteDriver import Hummingbird +import HummingbirdFluent import Logging -public struct AppConfiguration { - - public let hostname: String - public let port: Int - public let logLevel: Logger.Level? - - public init(hostname: String, port: Int, logLevel: Logger.Level? = nil) { - self.hostname = hostname - self.port = port - self.logLevel = logLevel - } +public protocol AppArguments { + var inMemoryDatabase: Bool { get } + var migrate: Bool { get } + var revert: Bool { get } + var hostname: String { get } + var port: Int { get } + var logLevel: Logger.Level? { get } } /// Build application /// - Parameter arguments: application arguments -public func buildApplication(_ arguments: AppConfiguration) async throws -> some ApplicationProtocol { +public func buildApplication(_ arguments: some AppArguments) async throws -> some ApplicationProtocol { let environment = Environment() let logger = { - var logger = Logger(label: "Todos") + var logger = Logger(label: "PurchaseOrders") logger.logLevel = arguments.logLevel ?? environment.get("LOG_LEVEL").map { Logger.Level(rawValue: $0) ?? .info } ?? .info return logger }() + let fluent = Fluent(logger: logger) + if arguments.inMemoryDatabase { + fluent.databases.use(.sqlite(.memory), as: .sqlite) + } else { + fluent.databases.use(.sqlite(.file("hdb.sqlite")), as: .sqlite) + } + let dbClient = DatabaseClient.live(database: fluent.db()) + try await fluent.migrations.add(dbClient.migrations()) + + if arguments.revert { + try await fluent.revert() + } + + if arguments.migrate || arguments.inMemoryDatabase { + try await fluent.migrate() + } + + let fluentPersist = await FluentPersistDriver(fluent: fluent) let router = Router() // Add middleware - router.addMiddleware { - // logging middleware - LogRequestsMiddleware(.info) - } + // logging middleware + router.add(middleware: LogRequestsMiddleware(.info)) + router.add(middleware: FileMiddleware(logger: logger)) + router.add(middleware: CORSMiddleware( + allowOrigin: .originBased, + allowHeaders: [.contentType], + allowMethods: [.get, .options, .post, .delete, .put, .patch] + )) // Add health endpoint - router.get("/health") { _, _ -> HTTPResponse.Status in return .ok } + UserController(db: dbClient.users).addRoutes(to: router.group("api/users")) + // let router = buildRouter() // - let app = Application( + var app = Application( router: router, - configuration: .init( address: .hostname(arguments.hostname, port: arguments.port), - serverName: "Todos" + serverName: "Purchase-Orders" ), logger: logger ) + app.addServices(fluent, fluentPersist) return app } -/// Build router - +// Build router // func buildRouter() -> Router { // let router = Router() // diff --git a/Sources/HApp/App.swift b/Sources/HApp/App.swift index 386a8e3..4be70f8 100644 --- a/Sources/HApp/App.swift +++ b/Sources/HApp/App.swift @@ -3,7 +3,7 @@ import Hummingbird import Logging @main -struct App: AsyncParsableCommand { +struct App: AsyncParsableCommand, AppArguments { @Option(name: .shortAndLong) var hostname: String = "127.0.0.1" @@ -14,16 +14,28 @@ struct App: AsyncParsableCommand { @Option(name: .shortAndLong) var logLevel: Logger.Level? + @Flag(name: .shortAndLong) + var migrate: Bool = false + + @Flag(name: .shortAndLong) + var revert: Bool = false + + @Flag(name: .shortAndLong) + var inMemoryDatabase: Bool = false + func run() async throws { - let app = try await buildApplication(.init(hostname: hostname, port: port, logLevel: logLevel)) + let app = try await buildApplication(self) try await app.runService() } } /// Extend `Logger.Level` so it can be used as an argument - #if hasFeature(RetroactiveAttribute) - extension Logger.Level: @retroactive ExpressibleByArgument {} + extension Logger.Level: @retroactive ExpressibleByArgument { + public init?(argument: String) { + self.init(rawValue: argument) + } + } #else extension Logger.Level: ExpressibleByArgument {} #endif diff --git a/Sources/HApp/Controllers/Api/UserController.swift b/Sources/HApp/Controllers/Api/UserController.swift new file mode 100644 index 0000000..4842b83 --- /dev/null +++ b/Sources/HApp/Controllers/Api/UserController.swift @@ -0,0 +1,28 @@ +import DatabaseClient +import Hummingbird +import HummingbirdFluent +import SharedModels + +struct UserController { + let db: DatabaseClient.Users + + func addRoutes(to group: RouterGroup) { + group.get(use: list) + .post(use: create) + } + + @Sendable + func list(_ request: Request, context: Context) async throws -> [User] { + try await db.fetchAll() + } + + @Sendable + func create(_ request: Request, context: Context) async throws -> User { + let create = try await request.decode(as: User.Create.self, context: context) + let user = try await db.create(create) + return user + } +} + +extension User.Create: ResponseCodable {} +extension User: ResponseCodable {}