From 4f47f1aed8cf4671e1b4846215bf07d4ea68124a Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Tue, 14 Jan 2025 07:51:13 -0500 Subject: [PATCH] feat: Working on hummingbird app --- Package.resolved | 11 ++- Package.swift | 14 +++- Sources/DatabaseClientLive/Users.swift | 18 ++--- .../DatabaseClientLive/VendorBranches.swift | 52 +++++++----- Sources/DatabaseClientLive/Vendors.swift | 16 ++-- Sources/HApp/App+build.swift | 80 +++++++++++++++++++ Sources/HApp/App.swift | 29 +++++++ Sources/SharedModels/User.swift | 39 +++++---- Sources/SharedModels/Vendor.swift | 35 ++++---- Sources/SharedModels/VendorBranch.swift | 15 ++-- 10 files changed, 223 insertions(+), 86 deletions(-) create mode 100644 Sources/HApp/App+build.swift create mode 100644 Sources/HApp/App.swift diff --git a/Package.resolved b/Package.resolved index 75976f4..942fc23 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "5fd82be053af28c2638151b00890a2e86e39fa8f499795235ca85c4080aa9364", + "originHash" : "68600988594927fcb25434476f598e1a813cb64030b7d9d8bf5b480e13a11989", "pins" : [ { "identity" : "async-http-client", @@ -154,6 +154,15 @@ "version" : "1.2.0" } }, + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser.git", + "state" : { + "revision" : "41982a3656a71c768319979febd796c6fd111d5c", + "version" : "1.5.0" + } + }, { "identity" : "swift-asn1", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 52fad14..42f9447 100644 --- a/Package.swift +++ b/Package.swift @@ -7,6 +7,8 @@ let package = Package( .macOS(.v14) ], products: [ + .executable(name: "App", targets: ["App"]), + .executable(name: "HApp", targets: ["HApp"]), .library(name: "SharedModels", targets: ["SharedModels"]), .library(name: "DatabaseClient", targets: ["DatabaseClient"]), .library(name: "DatabaseClientLive", targets: ["DatabaseClientLive"]) @@ -23,7 +25,9 @@ let package = Package( // 🔵 Non-blocking, event-driven networking for Swift. Used for custom executors .package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"), .package(url: "https://github.com/pointfreeco/swift-dependencies.git", from: "1.6.3"), - .package(url: "https://github.com/hummingbird-project/hummingbird-auth.git", from: "2.0.2") + .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") ], targets: [ .executableTarget( @@ -49,6 +53,14 @@ let package = Package( ], swiftSettings: swiftSettings ), + .executableTarget( + name: "HApp", + dependencies: [ + .product(name: "Hummingbird", package: "hummingbird"), + .product(name: "ArgumentParser", package: "swift-argument-parser") + ], + swiftSettings: swiftSettings + ), .target( name: "DatabaseClient", dependencies: [ diff --git a/Sources/DatabaseClientLive/Users.swift b/Sources/DatabaseClientLive/Users.swift index c62b4aa..044fe42 100644 --- a/Sources/DatabaseClientLive/Users.swift +++ b/Sources/DatabaseClientLive/Users.swift @@ -10,16 +10,16 @@ public extension DatabaseClient.Users { .init { create in let model = try create.toModel() try await model.save(on: database) - return model.toDTO() + return try model.toDTO() } delete: { id in guard let model = try await UserModel.find(id, on: database) else { throw NotFoundError() } try await model.delete(on: database) } fetchAll: { - try await UserModel.query(on: database).all().map { $0.toDTO() } + try await UserModel.query(on: database).all().map { try $0.toDTO() } } get: { id in - try await UserModel.find(id, on: database).map { $0.toDTO() } + try await UserModel.find(id, on: database).map { try $0.toDTO() } } login: { login in try login.validate() @@ -168,13 +168,13 @@ final class UserModel: Model, @unchecked Sendable { self.passwordHash = passwordHash } - func toDTO() -> User { - .init( - id: id, - createdAt: createdAt, + func toDTO() throws -> User { + try .init( + id: requireID(), email: email, - updatedAt: updatedAt, - username: username + username: username, + createdAt: createdAt, + updatedAt: updatedAt ) } diff --git a/Sources/DatabaseClientLive/VendorBranches.swift b/Sources/DatabaseClientLive/VendorBranches.swift index 2f29e01..60dab83 100644 --- a/Sources/DatabaseClientLive/VendorBranches.swift +++ b/Sources/DatabaseClientLive/VendorBranches.swift @@ -9,7 +9,7 @@ public extension DatabaseClient.VendorBranches { .init { create in let model = try create.toModel() try await model.save(on: db) - return model.toDTO() + return try model.toDTO() } delete: { id in guard let model = try await VendorBranchModel.find(id, on: db) else { throw NotFoundError() @@ -29,22 +29,22 @@ public extension DatabaseClient.VendorBranches { .with(\.$branches) .first()? .branches - .map { $0.toDTO() } + .map { try $0.toDTO() } guard let branches else { throw NotFoundError() } return branches } - return try await query.all().map { $0.toDTO() } + return try await query.all().map { try $0.toDTO() } } get: { id in - try await VendorBranchModel.find(id, on: db).map { $0.toDTO() } + try await VendorBranchModel.find(id, on: db).map { try $0.toDTO() } } update: { id, updates in guard let model = try await VendorBranchModel.find(id, on: db) else { throw NotFoundError() } try model.applyUpdates(updates) try await model.save(on: db) - return model.toDTO() + return try model.toDTO() } } @@ -53,16 +53,19 @@ public extension DatabaseClient.VendorBranches { extension VendorBranch { struct Migrate: AsyncMigration { + let name = "CreateVendorBranch" + fileprivate typealias FieldKey = VendorBranch.Key.Field + func prepare(on database: any Database) async throws { try await database.schema(VendorBranchModel.schema) .id() - .field("name", .string, .required) - .field("vendor_id", .uuid, .required) - .field("created_at", .datetime) - .field("updated_at", .datetime) - .foreignKey("vendor_id", references: VendorModel.schema, "id", onDelete: .cascade) + .field(.string(FieldKey.name), .string, .required) + .field(.string(FieldKey.vendorID), .uuid, .required) + .field(.string(FieldKey.createdAt), .datetime) + .field(.string(FieldKey.updatedAt), .datetime) + .foreignKey(.string(FieldKey.vendorID), references: VendorModel.schema, "id", onDelete: .cascade) .create() } @@ -96,23 +99,36 @@ extension VendorBranch.Update { } } +private extension VendorBranch { + enum Key { + static let schema = "vendor_branch" + enum Field { + static let createdAt = "created_at" + static let name = "name" + static let updatedAt = "updated_at" + static let vendorID = "vendor_id" + } + } +} + final class VendorBranchModel: Model, @unchecked Sendable { - static let schema = "vendor_branch" + fileprivate typealias FieldKey = VendorBranch.Key.Field + static let schema = VendorBranch.Key.schema @ID(key: .id) var id: UUID? - @Field(key: "name") + @Field(key: .string(FieldKey.name)) var name: String - @Timestamp(key: "created_at", on: .create) + @Timestamp(key: .string(FieldKey.createdAt), on: .create) var createdAt: Date? - @Timestamp(key: "updated_at", on: .update) + @Timestamp(key: .string(FieldKey.updatedAt), on: .update) var updatedAt: Date? - @Parent(key: "vendor_id") + @Parent(key: .string(FieldKey.vendorID)) var vendor: VendorModel init() {} @@ -123,9 +139,9 @@ final class VendorBranchModel: Model, @unchecked Sendable { $vendor.id = vendorId } - func toDTO() -> VendorBranch { - .init( - id: id, + func toDTO() throws -> VendorBranch { + try .init( + id: requireID(), name: name, vendorID: $vendor.id, createdAt: createdAt, diff --git a/Sources/DatabaseClientLive/Vendors.swift b/Sources/DatabaseClientLive/Vendors.swift index f2838f3..3f381d2 100644 --- a/Sources/DatabaseClientLive/Vendors.swift +++ b/Sources/DatabaseClientLive/Vendors.swift @@ -9,7 +9,7 @@ public extension DatabaseClient.Vendors { .init { create in let model = try create.toModel() try await model.save(on: db) - return model.toDTO() + return try model.toDTO() } delete: { id in guard let model = try await VendorModel.find(id, on: db) else { throw NotFoundError() @@ -27,7 +27,7 @@ public extension DatabaseClient.Vendors { break } - return try await query.all().map { $0.toDTO(includeBranches: withBranches) } + return try await query.all().map { try $0.toDTO(includeBranches: withBranches) } } get: { id, request in var query = VendorModel.query(on: db).filter(\.$id == id) @@ -35,14 +35,14 @@ public extension DatabaseClient.Vendors { if withBranches { query = query.with(\.$branches) } - return try await query.first().map { $0.toDTO(includeBranches: withBranches) } + return try await query.first().map { try $0.toDTO(includeBranches: withBranches) } } update: { id, updates in guard let model = try await VendorModel.find(id, on: db) else { throw NotFoundError() } try model.applyUpdates(updates) try await model.save(on: db) - return model.toDTO() + return try model.toDTO() } } } @@ -119,12 +119,12 @@ final class VendorModel: Model, @unchecked Sendable { self.name = name } - func toDTO(includeBranches: Bool? = nil) -> Vendor { - .init( - id: id, + func toDTO(includeBranches: Bool? = nil) throws -> Vendor { + try .init( + id: requireID(), name: name, branches: ($branches.value != nil && $branches.value!.count > 0) - ? $branches.value!.map { $0.toDTO() } + ? $branches.value!.map { try $0.toDTO() } : [], createdAt: createdAt, updatedAt: updatedAt diff --git a/Sources/HApp/App+build.swift b/Sources/HApp/App+build.swift new file mode 100644 index 0000000..765e3a2 --- /dev/null +++ b/Sources/HApp/App+build.swift @@ -0,0 +1,80 @@ +import Hummingbird +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 + } +} + +/// Build application +/// - Parameter arguments: application arguments +public func buildApplication(_ arguments: AppConfiguration) async throws -> some ApplicationProtocol { + let environment = Environment() + + let logger = { + var logger = Logger(label: "Todos") + logger.logLevel = arguments.logLevel ?? + environment.get("LOG_LEVEL").map { Logger.Level(rawValue: $0) ?? .info } ?? + .info + + return logger + }() + + let router = Router() + + // Add middleware + + router.addMiddleware { + // logging middleware + LogRequestsMiddleware(.info) + } + + // Add health endpoint + + router.get("/health") { _, _ -> HTTPResponse.Status in + return .ok + } + + // let router = buildRouter() + // + let app = Application( + router: router, + + configuration: .init( + address: .hostname(arguments.hostname, port: arguments.port), + serverName: "Todos" + ), + logger: logger + ) + + return app +} + +/// Build router + +// func buildRouter() -> Router { +// let router = Router() +// +// // Add middleware +// +// router.addMiddleware { +// // logging middleware +// LogRequestsMiddleware(.info) +// } +// +// // Add health endpoint +// +// router.get("/health") { _, _ -> HTTPResponse.Status in +// return .ok +// } +// +// return router +// } diff --git a/Sources/HApp/App.swift b/Sources/HApp/App.swift new file mode 100644 index 0000000..386a8e3 --- /dev/null +++ b/Sources/HApp/App.swift @@ -0,0 +1,29 @@ +import ArgumentParser +import Hummingbird +import Logging + +@main +struct App: AsyncParsableCommand { + + @Option(name: .shortAndLong) + var hostname: String = "127.0.0.1" + + @Option(name: .shortAndLong) + var port: Int = 8080 + + @Option(name: .shortAndLong) + var logLevel: Logger.Level? + + func run() async throws { + let app = try await buildApplication(.init(hostname: hostname, port: port, logLevel: logLevel)) + try await app.runService() + } +} + +/// Extend `Logger.Level` so it can be used as an argument + +#if hasFeature(RetroactiveAttribute) + extension Logger.Level: @retroactive ExpressibleByArgument {} +#else + extension Logger.Level: ExpressibleByArgument {} +#endif diff --git a/Sources/SharedModels/User.swift b/Sources/SharedModels/User.swift index a28af0f..d3a958f 100644 --- a/Sources/SharedModels/User.swift +++ b/Sources/SharedModels/User.swift @@ -4,35 +4,32 @@ import Foundation public struct User: Codable, Equatable, Identifiable, Sendable { public var id: UUID - public var createdAt: Date public var email: String - public var updatedAt: Date public var username: String + public var createdAt: Date? + public var updatedAt: Date? public init( - id: UUID? = nil, - createdAt: Date? = nil, + id: UUID, email: String, - updatedAt: Date? = nil, - username: String + username: String, + createdAt: Date? = nil, + updatedAt: Date? = nil ) { - @Dependency(\.date) var date - @Dependency(\.uuid) var uuid - - self.id = id ?? uuid() - self.createdAt = createdAt ?? date.now + self.id = id + self.createdAt = createdAt self.email = email - self.updatedAt = updatedAt ?? date.now + self.updatedAt = updatedAt self.username = username } } -public extension User { - static var mocks: [Self] { - [ - .init(email: "blob@test.com", username: "blob"), - .init(email: "blob-jr@test.com", username: "blob-jr"), - .init(email: "blob-sr@test.com", username: "blob-sr") - ] - } -} +// public extension User { +// static var mocks: [Self] { +// [ +// .init(email: "blob@test.com", username: "blob"), +// .init(email: "blob-jr@test.com", username: "blob-jr"), +// .init(email: "blob-sr@test.com", username: "blob-sr") +// ] +// } +// } diff --git a/Sources/SharedModels/Vendor.swift b/Sources/SharedModels/Vendor.swift index 28bd93f..060fcd3 100644 --- a/Sources/SharedModels/Vendor.swift +++ b/Sources/SharedModels/Vendor.swift @@ -5,34 +5,31 @@ public struct Vendor: Codable, Equatable, Identifiable, Sendable { public var id: UUID public var name: String public var branches: [VendorBranch]? - public var createdAt: Date - public var updatedAt: Date + public var createdAt: Date? + public var updatedAt: Date? public init( - id: UUID? = nil, + id: UUID, name: String, branches: [VendorBranch]? = nil, createdAt: Date? = nil, updatedAt: Date? = nil ) { - @Dependency(\.date) var date - @Dependency(\.uuid) var uuid - - self.id = id ?? uuid() + self.id = id self.name = name self.branches = branches - self.createdAt = createdAt ?? date.now - self.updatedAt = updatedAt ?? date.now + self.createdAt = createdAt + self.updatedAt = updatedAt } } -public extension Vendor { - - static var mocks: [Self] { - [ - .init(name: "Corken"), - .init(name: "Johnstone"), - .init(name: "Winstel Controls") - ] - } -} +// public extension Vendor { +// +// static var mocks: [Self] { +// [ +// .init(name: "Corken"), +// .init(name: "Johnstone"), +// .init(name: "Winstel Controls") +// ] +// } +// } diff --git a/Sources/SharedModels/VendorBranch.swift b/Sources/SharedModels/VendorBranch.swift index 4fa2689..752d250 100644 --- a/Sources/SharedModels/VendorBranch.swift +++ b/Sources/SharedModels/VendorBranch.swift @@ -5,23 +5,20 @@ public struct VendorBranch: Codable, Equatable, Identifiable, Sendable { public var id: UUID public var name: String public var vendorID: Vendor.ID - public var createdAt: Date - public var updatedAt: Date + public var createdAt: Date? + public var updatedAt: Date? public init( - id: UUID? = nil, + id: UUID, name: String, vendorID: Vendor.ID, createdAt: Date? = nil, updatedAt: Date? = nil ) { - @Dependency(\.date) var date - @Dependency(\.uuid) var uuid - - self.id = id ?? uuid() + self.id = id self.name = name self.vendorID = vendorID - self.createdAt = createdAt ?? date.now - self.updatedAt = updatedAt ?? date.now + self.createdAt = createdAt + self.updatedAt = updatedAt } }