From ccf80f05a782eaa8e854c4f9a6e39dbe6ce6d2e5 Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Tue, 14 Jan 2025 11:50:06 -0500 Subject: [PATCH] feat: Begins integrating database client into vapor app. --- Package.resolved | 65 +-- Package.swift | 24 +- .../Api/EmployeeApiController.swift | 128 +++--- .../Api/PurchaseOrderApiController.swift | 122 +++--- .../Controllers/Api/UserApiController.swift | 25 +- .../Controllers/Api/VendorApiController.swift | 114 ++--- .../Api/VendorBranchApiController.swift | 134 +++--- Sources/App/Controllers/ApiController.swift | 8 +- .../View/EmployeeViewController.swift | 316 +++++++------- .../View/PurchaseOrderViewController.swift | 392 +++++++++--------- .../View/UsersViewController.swift | 242 +++++------ .../View/VendorViewController.swift | 216 +++++----- Sources/App/Controllers/ViewController.swift | 222 +++++----- Sources/App/DB/EmployeeDB.swift | 176 ++++---- Sources/App/DB/PurchaseOrderDB.swift | 174 ++++---- Sources/App/DB/UserDB.swift | 120 +++--- Sources/App/DB/VendorBranchDB.swift | 174 ++++---- Sources/App/DB/VendorDB.swift | 170 ++++---- .../Extensions/RouteBuilder+protected.swift | 47 ++- Sources/App/Models/Employee.swift | 330 +++++++-------- Sources/App/Models/PurchaseOrder.swift | 312 +++++++------- Sources/App/Models/User.swift | 242 +++++------ Sources/App/Models/UserToken.swift | 102 ++--- Sources/App/Models/Vendor.swift | 226 +++++----- Sources/App/Models/VendorBranch.swift | 238 +++++------ Sources/App/configure.swift | 30 +- Sources/App/routes.swift | 2 +- Sources/DatabaseClient/Employees.swift | 34 -- Sources/DatabaseClient/PurchaseOrders.swift | 32 -- Sources/DatabaseClient/Users.swift | 63 +-- Sources/DatabaseClient/VendorBranches.swift | 20 - Sources/DatabaseClient/Vendors.swift | 19 - Sources/DatabaseClientLive/Live.swift | 2 +- Sources/DatabaseClientLive/Users.swift | 67 ++- Sources/HApp/App+build.swift | 102 ----- Sources/HApp/App.swift | 41 -- .../HApp/Controllers/Api/UserController.swift | 28 -- Sources/SharedModels/Employee.swift | 33 ++ Sources/SharedModels/PurchaseOrder.swift | 32 ++ Sources/SharedModels/User.swift | 55 +++ Sources/SharedModels/Vendor.swift | 19 + Sources/SharedModels/VendorBranch.swift | 20 + 42 files changed, 2378 insertions(+), 2540 deletions(-) delete mode 100644 Sources/HApp/App+build.swift delete mode 100644 Sources/HApp/App.swift delete mode 100644 Sources/HApp/Controllers/Api/UserController.swift diff --git a/Package.resolved b/Package.resolved index cef9859..1e46b16 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "0c4aa955a400075a593dfba3804996f803c9e812e765f72d0026e12c182b751c", + "originHash" : "5e46aec5c52d22d116c033b012d49986e5f900c102b88ac80e054d2e6e64d458", "pins" : [ { "identity" : "async-http-client", @@ -64,33 +64,6 @@ "version" : "4.8.0" } }, - { - "identity" : "hummingbird", - "kind" : "remoteSourceControl", - "location" : "https://github.com/hummingbird-project/hummingbird.git", - "state" : { - "revision" : "7a41c20c25866064f22b2bfa2c8194083e7e1595", - "version" : "2.6.1" - } - }, - { - "identity" : "hummingbird-auth", - "kind" : "remoteSourceControl", - "location" : "https://github.com/hummingbird-project/hummingbird-auth.git", - "state" : { - "revision" : "8630a49acca3b38c50e29d61ab263cb7edf0b06d", - "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", @@ -163,15 +136,6 @@ "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", @@ -181,15 +145,6 @@ "version" : "1.3.0" } }, - { - "identity" : "swift-async-algorithms", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-async-algorithms.git", - "state" : { - "revision" : "4c3ea81f81f0a25d0470188459c6d4bf20cf2f97", - "version" : "1.0.3" - } - }, { "identity" : "swift-atomics", "kind" : "remoteSourceControl", @@ -253,15 +208,6 @@ "version" : "1.1.2" } }, - { - "identity" : "swift-extras-base64", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swift-extras/swift-extras-base64.git", - "state" : { - "revision" : "dc8121fdd2b444c97d6b0534e8ad4ddecbe0d5f4", - "version" : "1.0.0" - } - }, { "identity" : "swift-http-types", "kind" : "remoteSourceControl", @@ -352,15 +298,6 @@ "version" : "1.1.0" } }, - { - "identity" : "swift-service-lifecycle", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swift-server/swift-service-lifecycle.git", - "state" : { - "revision" : "c2e97cf6f81510f2d6b4a69453861db65d478560", - "version" : "2.6.3" - } - }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 53b0c67..75abb46 100644 --- a/Package.swift +++ b/Package.swift @@ -8,7 +8,6 @@ let package = Package( ], 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"]) @@ -24,16 +23,13 @@ let package = Package( .package(url: "https://github.com/vapor/leaf.git", from: "4.3.0"), // 🔵 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.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/hummingbird-project/hummingbird-fluent.git", from: "2.0.0") + .package(url: "https://github.com/pointfreeco/swift-dependencies.git", from: "1.6.3") ], targets: [ .executableTarget( name: "App", dependencies: [ + "DatabaseClientLive", .product(name: "Fluent", package: "fluent"), .product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"), .product(name: "Leaf", package: "leaf"), @@ -54,24 +50,14 @@ let package = Package( ], swiftSettings: swiftSettings ), - .executableTarget( - name: "HApp", - dependencies: [ - "DatabaseClientLive", - .product(name: "Hummingbird", package: "hummingbird"), - .product(name: "HummingbirdFluent", package: "hummingbird-fluent"), - .product(name: "ArgumentParser", package: "swift-argument-parser"), - .product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver") - ], - swiftSettings: swiftSettings - ), .target( name: "DatabaseClient", dependencies: [ "SharedModels", .product(name: "Dependencies", package: "swift-dependencies"), .product(name: "DependenciesMacros", package: "swift-dependencies"), - .product(name: "Fluent", package: "fluent") + .product(name: "Fluent", package: "fluent"), + .product(name: "Vapor", package: "vapor") ], swiftSettings: swiftSettings ), @@ -79,7 +65,7 @@ let package = Package( name: "DatabaseClientLive", dependencies: [ "DatabaseClient", - .product(name: "HummingbirdBcrypt", package: "hummingbird-auth") + .product(name: "Vapor", package: "vapor") ], swiftSettings: swiftSettings ), diff --git a/Sources/App/Controllers/Api/EmployeeApiController.swift b/Sources/App/Controllers/Api/EmployeeApiController.swift index d2f9f70..66b27b3 100644 --- a/Sources/App/Controllers/Api/EmployeeApiController.swift +++ b/Sources/App/Controllers/Api/EmployeeApiController.swift @@ -1,64 +1,64 @@ -import Dependencies -import Fluent -import Vapor - -struct EmployeeApiController: RouteCollection { - - @Dependency(\.employees) var employees - - func boot(routes: any RoutesBuilder) throws { - let protected = routes.apiProtected(route: "employees") - protected.get(use: index(req:)) - protected.post(use: create(req:)) - protected.group(":employeeID") { - $0.get(use: get(req:)) - $0.put(use: update(req:)) - $0.delete(use: delete(req:)) - } - } - - @Sendable - func index(req: Request) async throws -> [Employee.DTO] { - let params = try req.query.decode(EmployeesIndexQuery.self) - return try await employees.fetchAll(params.active == true ? .active : .default) - } - - @Sendable - func create(req: Request) async throws -> Employee.DTO { - try await employees.create( - req.ensureValidContent(Employee.Create.self) - ) - } - - @Sendable - func get(req: Request) async throws -> Employee.DTO { - guard let id = req.parameters.get("employeeID", as: Employee.IDValue.self), - let employee = try await employees.get(id) - else { - throw Abort(.notFound) - } - return employee - } - - @Sendable - func update(req: Request) async throws -> Employee.DTO { - guard let employeeID = req.parameters.get("employeeID", as: Employee.IDValue.self) else { - throw Abort(.badRequest, reason: "Employee id value not provided") - } - let updates = try req.ensureValidContent(Employee.Update.self) - return try await employees.update(employeeID, updates) - } - - @Sendable - func delete(req: Request) async throws -> HTTPStatus { - guard let employeeID = req.parameters.get("employeeID", as: Employee.IDValue.self) else { - throw Abort(.badRequest, reason: "Employee id value not provided") - } - try await employees.delete(employeeID) - return .ok - } -} - -struct EmployeesIndexQuery: Content { - let active: Bool? -} +// import Dependencies +// import Fluent +// import Vapor +// +// struct EmployeeApiController: RouteCollection { +// +// @Dependency(\.employees) var employees +// +// func boot(routes: any RoutesBuilder) throws { +// let protected = routes.apiProtected(route: "employees") +// protected.get(use: index(req:)) +// protected.post(use: create(req:)) +// protected.group(":employeeID") { +// $0.get(use: get(req:)) +// $0.put(use: update(req:)) +// $0.delete(use: delete(req:)) +// } +// } +// +// @Sendable +// func index(req: Request) async throws -> [Employee.DTO] { +// let params = try req.query.decode(EmployeesIndexQuery.self) +// return try await employees.fetchAll(params.active == true ? .active : .default) +// } +// +// @Sendable +// func create(req: Request) async throws -> Employee.DTO { +// try await employees.create( +// req.ensureValidContent(Employee.Create.self) +// ) +// } +// +// @Sendable +// func get(req: Request) async throws -> Employee.DTO { +// guard let id = req.parameters.get("employeeID", as: Employee.IDValue.self), +// let employee = try await employees.get(id) +// else { +// throw Abort(.notFound) +// } +// return employee +// } +// +// @Sendable +// func update(req: Request) async throws -> Employee.DTO { +// guard let employeeID = req.parameters.get("employeeID", as: Employee.IDValue.self) else { +// throw Abort(.badRequest, reason: "Employee id value not provided") +// } +// let updates = try req.ensureValidContent(Employee.Update.self) +// return try await employees.update(employeeID, updates) +// } +// +// @Sendable +// func delete(req: Request) async throws -> HTTPStatus { +// guard let employeeID = req.parameters.get("employeeID", as: Employee.IDValue.self) else { +// throw Abort(.badRequest, reason: "Employee id value not provided") +// } +// try await employees.delete(employeeID) +// return .ok +// } +// } +// +// struct EmployeesIndexQuery: Content { +// let active: Bool? +// } diff --git a/Sources/App/Controllers/Api/PurchaseOrderApiController.swift b/Sources/App/Controllers/Api/PurchaseOrderApiController.swift index c90bf34..b9b9b18 100644 --- a/Sources/App/Controllers/Api/PurchaseOrderApiController.swift +++ b/Sources/App/Controllers/Api/PurchaseOrderApiController.swift @@ -1,61 +1,61 @@ -import Dependencies -import Fluent -import Vapor - -// TODO: Add update route. - -struct PurchaseOrderApiController: RouteCollection { - - @Dependency(\.purchaseOrders) var purchaseOrders - - func boot(routes: any RoutesBuilder) throws { - let protected = routes.apiProtected(route: "purchase-orders") - protected.get(use: index(req:)) - protected.post(use: create(req:)) - protected.group(":id") { - $0.get(use: get(req:)) - $0.delete(use: delete(req:)) - } - } - - @Sendable - func index(req: Request) async throws -> [PurchaseOrder.DTO] { - try await purchaseOrders.fetchAll() - } - - @Sendable - func create(req: Request) async throws -> PurchaseOrder.DTO { - try await purchaseOrders.create( - req.ensureValidContent(PurchaseOrder.Create.self), - req.auth.require(User.self).requireID() - ) - } - - @Sendable - func get(req: Request) async throws -> PurchaseOrder.DTO { - guard let id = req.parameters.get("id", as: PurchaseOrder.IDValue.self), - let purchaseOrder = try await purchaseOrders.get(id) - else { - throw Abort(.notFound) - } - return purchaseOrder - } - - @Sendable - func delete(req: Request) async throws -> HTTPStatus { - guard let id = req.parameters.get("id", as: PurchaseOrder.IDValue.self) else { - throw Abort(.badRequest, reason: "Purchase order id not provided.") - } - try await purchaseOrders.delete(id) - return .ok - } - - // @Sendable - // func update(req: Request) async throws -> PurchaseOrder.DTO { - // guard let id = req.parameters.get("id", as: PurchaseOrder.IDValue.self) else { - // throw Abort(.badRequest, reason: "Purchase order id not provided.") - // } - // try await purchaseOrders.delete(id: id, on: req.db) - // return .ok - // } -} +// import Dependencies +// import Fluent +// import Vapor +// +// // TODO: Add update route. +// +// struct PurchaseOrderApiController: RouteCollection { +// +// @Dependency(\.purchaseOrders) var purchaseOrders +// +// func boot(routes: any RoutesBuilder) throws { +// let protected = routes.apiProtected(route: "purchase-orders") +// protected.get(use: index(req:)) +// protected.post(use: create(req:)) +// protected.group(":id") { +// $0.get(use: get(req:)) +// $0.delete(use: delete(req:)) +// } +// } +// +// @Sendable +// func index(req: Request) async throws -> [PurchaseOrder.DTO] { +// try await purchaseOrders.fetchAll() +// } +// +// @Sendable +// func create(req: Request) async throws -> PurchaseOrder.DTO { +// try await purchaseOrders.create( +// req.ensureValidContent(PurchaseOrder.Create.self), +// req.auth.require(User.self).requireID() +// ) +// } +// +// @Sendable +// func get(req: Request) async throws -> PurchaseOrder.DTO { +// guard let id = req.parameters.get("id", as: PurchaseOrder.IDValue.self), +// let purchaseOrder = try await purchaseOrders.get(id) +// else { +// throw Abort(.notFound) +// } +// return purchaseOrder +// } +// +// @Sendable +// func delete(req: Request) async throws -> HTTPStatus { +// guard let id = req.parameters.get("id", as: PurchaseOrder.IDValue.self) else { +// throw Abort(.badRequest, reason: "Purchase order id not provided.") +// } +// try await purchaseOrders.delete(id) +// return .ok +// } +// +// // @Sendable +// // func update(req: Request) async throws -> PurchaseOrder.DTO { +// // guard let id = req.parameters.get("id", as: PurchaseOrder.IDValue.self) else { +// // throw Abort(.badRequest, reason: "Purchase order id not provided.") +// // } +// // try await purchaseOrders.delete(id: id, on: req.db) +// // return .ok +// // } +// } diff --git a/Sources/App/Controllers/Api/UserApiController.swift b/Sources/App/Controllers/Api/UserApiController.swift index 50ac5bf..63cc6d7 100644 --- a/Sources/App/Controllers/Api/UserApiController.swift +++ b/Sources/App/Controllers/Api/UserApiController.swift @@ -1,11 +1,13 @@ +import DatabaseClient import Dependencies import Fluent +import SharedModels import Vapor // TODO: Add update and get by id. struct UserApiController: RouteCollection { - @Dependency(\.users) var users + @Dependency(\.database.users) var users func boot(routes: any RoutesBuilder) throws { let unProtected = routes.apiUnprotected(route: "users") @@ -20,26 +22,28 @@ struct UserApiController: RouteCollection { } @Sendable - func index(req: Request) async throws -> [User.DTO] { + func index(req: Request) async throws -> [User] { try await users.fetchAll() } @Sendable - func create(req: Request) async throws -> User.DTO { + func create(req: Request) async throws -> User { // Allow the first user to be created without authentication. - let count = try await User.query(on: req.db).count() + // let count = try await User.query(on: req.db).count() + let count = try await users.count() if count > 0 { guard req.auth.get(User.self) != nil else { throw Abort(.unauthorized) } } - return try await users.create(req.ensureValidContent(User.Create.self)) + return try await users.create(req.content.decode(User.Create.self)) } @Sendable - func login(req: Request) async throws -> UserToken { + func login(req: Request) async throws -> User { let user = try req.auth.require(User.self) - return try await users.login(user) + return user + // return try await users.login(user) } // @Sendable @@ -50,9 +54,10 @@ struct UserApiController: RouteCollection { @Sendable func delete(req: Request) async throws -> HTTPStatus { - guard let id = req.parameters.get("id", as: User.IDValue.self) else { - throw Abort(.badRequest, reason: "User id not provided") - } + // guard let id = req.parameters.get("id", as: User.IDValue.self) else { + // throw Abort(.badRequest, reason: "User id not provided") + // } + let id = try req.ensureIDPathComponent() try await users.delete(id) return .ok } diff --git a/Sources/App/Controllers/Api/VendorApiController.swift b/Sources/App/Controllers/Api/VendorApiController.swift index 6d7634e..d52c865 100644 --- a/Sources/App/Controllers/Api/VendorApiController.swift +++ b/Sources/App/Controllers/Api/VendorApiController.swift @@ -1,57 +1,57 @@ -import Dependencies -import Fluent -import Vapor - -struct VendorApiController: RouteCollection { - - @Dependency(\.vendors) var vendors - - func boot(routes: any RoutesBuilder) throws { - let protected = routes.apiProtected(route: "vendors") - protected.get(use: index(req:)) - protected.post(use: create(req:)) - protected.group(":id") { - $0.put(use: update(req:)) - $0.delete(use: delete(req:)) - } - } - - @Sendable - func index(req: Request) async throws -> [Vendor.DTO] { - let params = try req.query.decode(VendorsIndexQuery.self) - return try await vendors.fetchAll(params.fetchRequest) - } - - @Sendable - func create(req: Request) async throws -> Vendor.DTO { - try await vendors.create(req.ensureValidContent(Vendor.Create.self)) - } - - @Sendable - func update(req: Request) async throws -> Vendor.DTO { - guard let id = req.parameters.get("id", as: Vendor.IDValue.self) else { - throw Abort(.badRequest, reason: "Vendor id not provided.") - } - try Vendor.Update.validate(content: req) - let updates = try req.content.decode(Vendor.Update.self) - return try await vendors.update(id, updates) - } - - @Sendable - func delete(req: Request) async throws -> HTTPStatus { - guard let id = req.parameters.get("id", as: Vendor.IDValue.self) else { - throw Abort(.badRequest, reason: "Vendor id not provided.") - } - try await vendors.delete(id) - return .ok - } -} - -struct VendorsIndexQuery: Content { - let branches: Bool? - - var fetchRequest: VendorDB.FetchRequest { - if branches == true { return .withBranches } - return .default - } -} +// import Dependencies +// import Fluent +// import Vapor +// +// struct VendorApiController: RouteCollection { +// +// @Dependency(\.vendors) var vendors +// +// func boot(routes: any RoutesBuilder) throws { +// let protected = routes.apiProtected(route: "vendors") +// protected.get(use: index(req:)) +// protected.post(use: create(req:)) +// protected.group(":id") { +// $0.put(use: update(req:)) +// $0.delete(use: delete(req:)) +// } +// } +// +// @Sendable +// func index(req: Request) async throws -> [Vendor.DTO] { +// let params = try req.query.decode(VendorsIndexQuery.self) +// return try await vendors.fetchAll(params.fetchRequest) +// } +// +// @Sendable +// func create(req: Request) async throws -> Vendor.DTO { +// try await vendors.create(req.ensureValidContent(Vendor.Create.self)) +// } +// +// @Sendable +// func update(req: Request) async throws -> Vendor.DTO { +// guard let id = req.parameters.get("id", as: Vendor.IDValue.self) else { +// throw Abort(.badRequest, reason: "Vendor id not provided.") +// } +// try Vendor.Update.validate(content: req) +// let updates = try req.content.decode(Vendor.Update.self) +// return try await vendors.update(id, updates) +// } +// +// @Sendable +// func delete(req: Request) async throws -> HTTPStatus { +// guard let id = req.parameters.get("id", as: Vendor.IDValue.self) else { +// throw Abort(.badRequest, reason: "Vendor id not provided.") +// } +// try await vendors.delete(id) +// return .ok +// } +// } +// +// struct VendorsIndexQuery: Content { +// let branches: Bool? +// +// var fetchRequest: VendorDB.FetchRequest { +// if branches == true { return .withBranches } +// return .default +// } +// } diff --git a/Sources/App/Controllers/Api/VendorBranchApiController.swift b/Sources/App/Controllers/Api/VendorBranchApiController.swift index b362ac5..251fc60 100644 --- a/Sources/App/Controllers/Api/VendorBranchApiController.swift +++ b/Sources/App/Controllers/Api/VendorBranchApiController.swift @@ -1,67 +1,67 @@ -import Dependencies -import Fluent -import Vapor - -struct VendorBranchApiController: RouteCollection { - - @Dependency(\.vendorBranches) var vendorBranches - - func boot(routes: any RoutesBuilder) throws { - let prefix = routes.apiProtected(route: "vendors") - let root = prefix.grouped("branches") - root.get(use: index(req:)) - root.group(":id") { - $0.put(use: update(req:)) - $0.delete(use: delete(req:)) - } - - prefix.group(":vendorID", "branches") { - $0.get(use: indexForVendor(req:)) - $0.post(use: create(req:)) - } - } - - @Sendable - func index(req: Request) async throws -> [VendorBranch.DTO] { - try await vendorBranches.fetchAll() - } - - @Sendable - func indexForVendor(req: Request) async throws -> [VendorBranch.DTO] { - guard let id = req.parameters.get("vendorID", as: Vendor.IDValue.self) else { - throw Abort(.badRequest, reason: "Vendor id not provided.") - } - return try await vendorBranches.fetchAll(.for(vendorID: id)) - } - - @Sendable - func create(req: Request) async throws -> VendorBranch.DTO { - guard let id = req.parameters.get("vendorID", as: Vendor.IDValue.self) else { - throw Abort(.badRequest, reason: "Vendor id not provided.") - } - return try await vendorBranches.create( - req.ensureValidContent(VendorBranch.Create.self), - id - ) - } - - @Sendable - func update(req: Request) async throws -> VendorBranch.DTO { - guard let id = req.parameters.get("id", as: VendorBranch.IDValue.self) else { - throw Abort(.badRequest, reason: "Vendor branch id not provided.") - } - try VendorBranch.Update.validate(content: req) - let updates = try req.content.decode(VendorBranch.Update.self) - return try await vendorBranches.update(id, updates) - } - - @Sendable - func delete(req: Request) async throws -> HTTPStatus { - guard let id = req.parameters.get("id", as: VendorBranch.IDValue.self) else { - throw Abort(.badRequest, reason: "Vendor branch id not provided.") - } - try await vendorBranches.delete(id) - return .ok - } - -} +// import Dependencies +// import Fluent +// import Vapor +// +// struct VendorBranchApiController: RouteCollection { +// +// @Dependency(\.vendorBranches) var vendorBranches +// +// func boot(routes: any RoutesBuilder) throws { +// let prefix = routes.apiProtected(route: "vendors") +// let root = prefix.grouped("branches") +// root.get(use: index(req:)) +// root.group(":id") { +// $0.put(use: update(req:)) +// $0.delete(use: delete(req:)) +// } +// +// prefix.group(":vendorID", "branches") { +// $0.get(use: indexForVendor(req:)) +// $0.post(use: create(req:)) +// } +// } +// +// @Sendable +// func index(req: Request) async throws -> [VendorBranch.DTO] { +// try await vendorBranches.fetchAll() +// } +// +// @Sendable +// func indexForVendor(req: Request) async throws -> [VendorBranch.DTO] { +// guard let id = req.parameters.get("vendorID", as: Vendor.IDValue.self) else { +// throw Abort(.badRequest, reason: "Vendor id not provided.") +// } +// return try await vendorBranches.fetchAll(.for(vendorID: id)) +// } +// +// @Sendable +// func create(req: Request) async throws -> VendorBranch.DTO { +// guard let id = req.parameters.get("vendorID", as: Vendor.IDValue.self) else { +// throw Abort(.badRequest, reason: "Vendor id not provided.") +// } +// return try await vendorBranches.create( +// req.ensureValidContent(VendorBranch.Create.self), +// id +// ) +// } +// +// @Sendable +// func update(req: Request) async throws -> VendorBranch.DTO { +// guard let id = req.parameters.get("id", as: VendorBranch.IDValue.self) else { +// throw Abort(.badRequest, reason: "Vendor branch id not provided.") +// } +// try VendorBranch.Update.validate(content: req) +// let updates = try req.content.decode(VendorBranch.Update.self) +// return try await vendorBranches.update(id, updates) +// } +// +// @Sendable +// func delete(req: Request) async throws -> HTTPStatus { +// guard let id = req.parameters.get("id", as: VendorBranch.IDValue.self) else { +// throw Abort(.badRequest, reason: "Vendor branch id not provided.") +// } +// try await vendorBranches.delete(id) +// return .ok +// } +// +// } diff --git a/Sources/App/Controllers/ApiController.swift b/Sources/App/Controllers/ApiController.swift index bd49309..2e34aa8 100644 --- a/Sources/App/Controllers/ApiController.swift +++ b/Sources/App/Controllers/ApiController.swift @@ -3,10 +3,10 @@ import Vapor struct ApiController: RouteCollection { func boot(routes: any RoutesBuilder) throws { - try routes.register(collection: EmployeeApiController()) - try routes.register(collection: PurchaseOrderApiController()) + // try routes.register(collection: EmployeeApiController()) + // try routes.register(collection: PurchaseOrderApiController()) try routes.register(collection: UserApiController()) - try routes.register(collection: VendorApiController()) - try routes.register(collection: VendorBranchApiController()) + // try routes.register(collection: VendorApiController()) + // try routes.register(collection: VendorBranchApiController()) } } diff --git a/Sources/App/Controllers/View/EmployeeViewController.swift b/Sources/App/Controllers/View/EmployeeViewController.swift index d1dab79..ff37628 100644 --- a/Sources/App/Controllers/View/EmployeeViewController.swift +++ b/Sources/App/Controllers/View/EmployeeViewController.swift @@ -1,158 +1,158 @@ -import Dependencies -import Fluent -import Leaf -import Vapor - -struct EmployeeViewController: RouteCollection { - - @Dependency(\.employees) var employees - - func boot(routes: any RoutesBuilder) throws { - let protected = routes.protected.grouped("employees") - protected.get(use: index(req:)) - protected.get("form", use: employeeForm(req:)) - protected.post(use: create(req:)) - protected.group(":employeeID") { - $0.get(use: get(req:)) - $0.get("edit", use: edit(req:)) - $0.delete(use: delete(req:)) - $0.put(use: update(req:)) - $0.patch("toggle-active", use: toggleActive(req:)) - } - } - - @Sendable - func index(req: Request) async throws -> View { - return try await renderIndex(req) - } - - @Sendable - private func renderIndex( - _ req: Request, - _ employee: Employee.DTO? = nil, - _ form: EmployeeFormCTX? = nil - ) async throws -> View { - return try await req.view.render( - "employees/index", - EmployeesCTX(employee: employee, employees: employees.fetchAll(), form: form ?? .init()) - ) - } - - @Sendable - func create(req: Request) async throws -> View { - try Employee.Create.validate(content: req) - let employee = try await employees.create(req.content.decode(Employee.Create.self)) - return try await req.view.render("employees/table-row", employee) - } - - @Sendable - func get(req: Request) async throws -> View { - let employee = try await employees.get(req.ensureIDPathComponent(key: "employeeID")) - // Check if we've rendered the page yet. - guard req.isHtmxRequest else { - return try await renderIndex(req, employee) - } - return try await req.view.render("employees/detail", ["employee": employee]) - } - - @Sendable - func toggleActive(req: Request) async throws -> View { - guard let id = req.parameters.get("employeeID", as: Employee.IDValue.self) else { - throw Abort(.badRequest, reason: "Employee id not supplied.") - } - let employee = try await employees.toggleActive(id) - return try await req.view.render("employees/table-row", employee) - } - - // TODO: I think we can just return a response and remove the table-row, here. - @Sendable - func delete(req: Request) async throws -> View { - let id = try req.requireEmployeeID() - _ = try await employees.delete(id) - let employees = try await employees.fetchAll() - return try await req.view.render("employees/table", ["employees": employees]) - } - - @Sendable - func edit(req: Request) async throws -> View { - guard let employee = try await employees.get(req.parameters.get("employeeID")) else { - throw Abort(.notFound) - } - return try await req.view.render("employees/detail", EmployeeDetailCTX(editing: true, employee: employee)) - } - - @Sendable - func update(req: Request) async throws -> View { - let id = try req.requireEmployeeID() - try Employee.Update.validate(content: req) - let updates = try req.content.decode(Employee.Update.self) - req.logger.info("Employee updates: \(updates)") - let employee = try await employees.update(id, updates) - req.logger.info("Done updating employee: \(employee)") - return try await req.view.render("employees/table-row", employee) - } - - @Sendable - func employeeForm(req: Request) async throws -> View { - try await req.view.render("employees/form", EmployeeFormCTX()) - } - -} - -private extension Request { - func requireEmployeeID() throws -> Employee.IDValue { - guard let id = parameters.get("employeeID", as: Employee.IDValue.self) else { - throw Abort(.badRequest, reason: "Employee id not supplied") - } - return id - } -} - -private struct EmployeeDetailCTX: Content { - let editing: Bool - let employee: Employee.DTO? - - init(editing: Bool = false, employee: Employee.DTO? = nil) { - self.editing = editing - self.employee = employee - } -} - -private struct EmployeesCTX: Content { - let employee: Employee.DTO? - let employees: [Employee.DTO] - let form: EmployeeFormCTX - - init( - employee: Employee.DTO? = nil, - employees: [Employee.DTO], - form: EmployeeFormCTX? = nil - ) { - self.employee = employee - self.employees = employees - self.form = form ?? .init() - } -} - -private struct EmployeeFormCTX: Content { - - let htmxForm: HtmxFormCTX - - init(employee: Employee.DTO? = nil) { - self.htmxForm = .init( - formClass: "employee-form", - formId: "employee-form", - htmxTargetUrl: employee?.id == nil ? .post("/employees") : .put("/employees/\(employee!.id!)"), - htmxTarget: "#employee-table", - htmxPushUrl: false, - htmxResetAfterRequest: true, - htmxSwapOob: nil, - htmxSwap: employee == nil ? .outerHTML : nil, - context: .init(employee: employee) - ) - } - - struct Context: Content { - let employee: Employee.DTO? - } -} +// import Dependencies +// import Fluent +// import Leaf +// import Vapor +// +// struct EmployeeViewController: RouteCollection { +// +// @Dependency(\.employees) var employees +// +// func boot(routes: any RoutesBuilder) throws { +// let protected = routes.protected.grouped("employees") +// protected.get(use: index(req:)) +// protected.get("form", use: employeeForm(req:)) +// protected.post(use: create(req:)) +// protected.group(":employeeID") { +// $0.get(use: get(req:)) +// $0.get("edit", use: edit(req:)) +// $0.delete(use: delete(req:)) +// $0.put(use: update(req:)) +// $0.patch("toggle-active", use: toggleActive(req:)) +// } +// } +// +// @Sendable +// func index(req: Request) async throws -> View { +// return try await renderIndex(req) +// } +// +// @Sendable +// private func renderIndex( +// _ req: Request, +// _ employee: Employee.DTO? = nil, +// _ form: EmployeeFormCTX? = nil +// ) async throws -> View { +// return try await req.view.render( +// "employees/index", +// EmployeesCTX(employee: employee, employees: employees.fetchAll(), form: form ?? .init()) +// ) +// } +// +// @Sendable +// func create(req: Request) async throws -> View { +// try Employee.Create.validate(content: req) +// let employee = try await employees.create(req.content.decode(Employee.Create.self)) +// return try await req.view.render("employees/table-row", employee) +// } +// +// @Sendable +// func get(req: Request) async throws -> View { +// let employee = try await employees.get(req.ensureIDPathComponent(key: "employeeID")) +// // Check if we've rendered the page yet. +// guard req.isHtmxRequest else { +// return try await renderIndex(req, employee) +// } +// return try await req.view.render("employees/detail", ["employee": employee]) +// } +// +// @Sendable +// func toggleActive(req: Request) async throws -> View { +// guard let id = req.parameters.get("employeeID", as: Employee.IDValue.self) else { +// throw Abort(.badRequest, reason: "Employee id not supplied.") +// } +// let employee = try await employees.toggleActive(id) +// return try await req.view.render("employees/table-row", employee) +// } +// +// // TODO: I think we can just return a response and remove the table-row, here. +// @Sendable +// func delete(req: Request) async throws -> View { +// let id = try req.requireEmployeeID() +// _ = try await employees.delete(id) +// let employees = try await employees.fetchAll() +// return try await req.view.render("employees/table", ["employees": employees]) +// } +// +// @Sendable +// func edit(req: Request) async throws -> View { +// guard let employee = try await employees.get(req.parameters.get("employeeID")) else { +// throw Abort(.notFound) +// } +// return try await req.view.render("employees/detail", EmployeeDetailCTX(editing: true, employee: employee)) +// } +// +// @Sendable +// func update(req: Request) async throws -> View { +// let id = try req.requireEmployeeID() +// try Employee.Update.validate(content: req) +// let updates = try req.content.decode(Employee.Update.self) +// req.logger.info("Employee updates: \(updates)") +// let employee = try await employees.update(id, updates) +// req.logger.info("Done updating employee: \(employee)") +// return try await req.view.render("employees/table-row", employee) +// } +// +// @Sendable +// func employeeForm(req: Request) async throws -> View { +// try await req.view.render("employees/form", EmployeeFormCTX()) +// } +// +// } +// +// private extension Request { +// func requireEmployeeID() throws -> Employee.IDValue { +// guard let id = parameters.get("employeeID", as: Employee.IDValue.self) else { +// throw Abort(.badRequest, reason: "Employee id not supplied") +// } +// return id +// } +// } +// +// private struct EmployeeDetailCTX: Content { +// let editing: Bool +// let employee: Employee.DTO? +// +// init(editing: Bool = false, employee: Employee.DTO? = nil) { +// self.editing = editing +// self.employee = employee +// } +// } +// +// private struct EmployeesCTX: Content { +// let employee: Employee.DTO? +// let employees: [Employee.DTO] +// let form: EmployeeFormCTX +// +// init( +// employee: Employee.DTO? = nil, +// employees: [Employee.DTO], +// form: EmployeeFormCTX? = nil +// ) { +// self.employee = employee +// self.employees = employees +// self.form = form ?? .init() +// } +// } +// +// private struct EmployeeFormCTX: Content { +// +// let htmxForm: HtmxFormCTX +// +// init(employee: Employee.DTO? = nil) { +// self.htmxForm = .init( +// formClass: "employee-form", +// formId: "employee-form", +// htmxTargetUrl: employee?.id == nil ? .post("/employees") : .put("/employees/\(employee!.id!)"), +// htmxTarget: "#employee-table", +// htmxPushUrl: false, +// htmxResetAfterRequest: true, +// htmxSwapOob: nil, +// htmxSwap: employee == nil ? .outerHTML : nil, +// context: .init(employee: employee) +// ) +// } +// +// struct Context: Content { +// let employee: Employee.DTO? +// } +// } diff --git a/Sources/App/Controllers/View/PurchaseOrderViewController.swift b/Sources/App/Controllers/View/PurchaseOrderViewController.swift index c99311d..0c2fc19 100644 --- a/Sources/App/Controllers/View/PurchaseOrderViewController.swift +++ b/Sources/App/Controllers/View/PurchaseOrderViewController.swift @@ -1,196 +1,196 @@ -import Dependencies -import Fluent -import Vapor - -struct PurchaseOrderViewController: RouteCollection { - @Dependency(\.employees) var employees - @Dependency(\.purchaseOrders) var purchaseOrders - @Dependency(\.vendorBranches) var vendorBranches - - func boot(routes: any RoutesBuilder) throws { - let pos = routes.protected.grouped("purchase-orders") - - pos.get(use: index(req:)) - pos.group("details", "close") { - $0.get(use: detailClose(req:)) - } - pos.post(use: create(req:)) - pos.group(":id") { - $0.get(use: detail(req:)) - } - } - - @Sendable - func index(req: Request) async throws -> View { - let params = try? req.query.decode(PurchaseOrderIndex.self) - let purchaseOrdersPage = try await purchaseOrders.fetchPage( - .init(page: params?.page ?? 1, per: params?.limit ?? 50) - ) - let branches = try await vendorBranches.getBranches(req: req) - let employees = try await employees.fetchAll() - req.logger.debug("Branches: \(branches)") - return try await req.view.render( - "purchaseOrders/index", - PurchaseOrderCTX( - page: purchaseOrdersPage, - form: .create(branches: branches, employees: employees) - ) - ) - } - - @Sendable - func detail(req: Request) async throws -> View { - guard let id = req.parameters.get("id", as: PurchaseOrder.IDValue.self) else { - throw Abort(.badRequest, reason: "Id not supplied.") - } - let purchaseOrder = try await purchaseOrders.get(id) - return try await req.view.render("purchaseOrders/detail", ["purchaseOrderDetail": purchaseOrder]) - } - - @Sendable - func detailClose(req: Request) async throws -> View { - return try await req.view.render("purchaseOrders/detail") - } - - @Sendable - func create(req: Request) async throws -> View { - try PurchaseOrder.FormCreate.validate(content: req) - let createdById = try req.auth.require(User.self).requireID() - let create = try req.content.decode(PurchaseOrder.FormCreate.self).toCreate() - let purchaseOrder = try await purchaseOrders.create(create, createdById) - return try await req.view.render("purchaseOrders/table-row", purchaseOrder) - } -} - -private struct PurchaseOrderIndex: Content { - let page: Int? - let limit: Int? -} - -private struct PurchaseOrderCTX: Content { - let purchaseOrderDetail: PurchaseOrder.DTO? - let purchaseOrders: [PurchaseOrder.DTO] - let page: Int - let limit: Int - let hasNext: Bool - let hasPrevious: Bool - let form: PurchaseOrderFormCTX? - - init( - detail: PurchaseOrder.DTO? = nil, - page: Page, - form: PurchaseOrderFormCTX? - ) { - self.purchaseOrderDetail = detail - self.purchaseOrders = page.items - self.page = page.metadata.page - self.limit = page.metadata.per - self.hasNext = page.metadata.hasNext - self.hasPrevious = page.metadata.page > 1 - self.form = form - } -} - -private extension PageMetadata { - var hasNext: Bool { - total > (page * per) - } -} - -private struct PurchaseOrderFormCTX: Content { - - let htmxForm: HtmxFormCTX - - struct Context: Content { - let branches: [VendorBranch.FormDTO] - let employees: [Employee.DTO] - } - - static func create(branches: [VendorBranch.FormDTO], employees: [Employee.DTO]) -> Self { - .init(htmxForm: .init( - formClass: "po-form", - formId: "po-form", - htmxTargetUrl: .post("/purchase-orders"), - htmxTarget: "#po-table-body", - htmxPushUrl: false, - htmxResetAfterRequest: true, - htmxSwapOob: nil, - htmxSwap: .afterbegin, - context: .init(branches: branches, employees: employees) - )) - } -} - -extension VendorBranch { - struct FormDTO: Content { - let id: UUID - let name: String - let vendor: Vendor.DTO - } - - func toFormDTO() throws -> VendorBranch.FormDTO { - try .init( - id: requireID(), - name: name, - vendor: vendor.toDTO() - ) - } -} - -private extension PurchaseOrder { - struct FormCreate: Content { - let id: Int? - let workOrder: String? - let materials: String - let customer: String - let truckStock: Bool? - let createdForID: Employee.IDValue - let vendorBranchID: VendorBranch.IDValue - - // TODO: Remove. - func toModel(createdByID: User.IDValue) -> PurchaseOrder { - .init( - id: id, - workOrder: workOrder != nil ? (workOrder == "" ? nil : Int(workOrder!)) : nil, - materials: materials, - customer: customer, - truckStock: truckStock ?? false, - createdByID: createdByID, - createdForID: createdForID, - vendorBranchID: vendorBranchID, - createdAt: nil, - updatedAt: nil - ) - } - - func toCreate() -> PurchaseOrder.Create { - .init( - id: id, - workOrder: workOrder != nil ? (workOrder == "" ? nil : Int(workOrder!)) : nil, - materials: materials, - customer: customer, - truckStock: truckStock, - createdForID: createdForID, - vendorBranchID: vendorBranchID - ) - } - } -} - -private extension VendorBranchDB { - - func getBranches(req: Request) async throws -> [VendorBranch.FormDTO] { - try await VendorBranch.query(on: req.db) - .with(\.$vendor) - .all() - .map { try $0.toFormDTO() } - } -} - -extension PurchaseOrder.FormCreate: Validatable { - - static func validations(_ validations: inout Validations) { - validations.add("materials", as: String.self, is: !.empty) - validations.add("customer", as: String.self, is: !.empty) - } -} +// import Dependencies +// import Fluent +// import Vapor +// +// struct PurchaseOrderViewController: RouteCollection { +// @Dependency(\.employees) var employees +// @Dependency(\.purchaseOrders) var purchaseOrders +// @Dependency(\.vendorBranches) var vendorBranches +// +// func boot(routes: any RoutesBuilder) throws { +// let pos = routes.protected.grouped("purchase-orders") +// +// pos.get(use: index(req:)) +// pos.group("details", "close") { +// $0.get(use: detailClose(req:)) +// } +// pos.post(use: create(req:)) +// pos.group(":id") { +// $0.get(use: detail(req:)) +// } +// } +// +// @Sendable +// func index(req: Request) async throws -> View { +// let params = try? req.query.decode(PurchaseOrderIndex.self) +// let purchaseOrdersPage = try await purchaseOrders.fetchPage( +// .init(page: params?.page ?? 1, per: params?.limit ?? 50) +// ) +// let branches = try await vendorBranches.getBranches(req: req) +// let employees = try await employees.fetchAll() +// req.logger.debug("Branches: \(branches)") +// return try await req.view.render( +// "purchaseOrders/index", +// PurchaseOrderCTX( +// page: purchaseOrdersPage, +// form: .create(branches: branches, employees: employees) +// ) +// ) +// } +// +// @Sendable +// func detail(req: Request) async throws -> View { +// guard let id = req.parameters.get("id", as: PurchaseOrder.IDValue.self) else { +// throw Abort(.badRequest, reason: "Id not supplied.") +// } +// let purchaseOrder = try await purchaseOrders.get(id) +// return try await req.view.render("purchaseOrders/detail", ["purchaseOrderDetail": purchaseOrder]) +// } +// +// @Sendable +// func detailClose(req: Request) async throws -> View { +// return try await req.view.render("purchaseOrders/detail") +// } +// +// @Sendable +// func create(req: Request) async throws -> View { +// try PurchaseOrder.FormCreate.validate(content: req) +// let createdById = try req.auth.require(User.self).requireID() +// let create = try req.content.decode(PurchaseOrder.FormCreate.self).toCreate() +// let purchaseOrder = try await purchaseOrders.create(create, createdById) +// return try await req.view.render("purchaseOrders/table-row", purchaseOrder) +// } +// } +// +// private struct PurchaseOrderIndex: Content { +// let page: Int? +// let limit: Int? +// } +// +// private struct PurchaseOrderCTX: Content { +// let purchaseOrderDetail: PurchaseOrder.DTO? +// let purchaseOrders: [PurchaseOrder.DTO] +// let page: Int +// let limit: Int +// let hasNext: Bool +// let hasPrevious: Bool +// let form: PurchaseOrderFormCTX? +// +// init( +// detail: PurchaseOrder.DTO? = nil, +// page: Page, +// form: PurchaseOrderFormCTX? +// ) { +// self.purchaseOrderDetail = detail +// self.purchaseOrders = page.items +// self.page = page.metadata.page +// self.limit = page.metadata.per +// self.hasNext = page.metadata.hasNext +// self.hasPrevious = page.metadata.page > 1 +// self.form = form +// } +// } +// +// private extension PageMetadata { +// var hasNext: Bool { +// total > (page * per) +// } +// } +// +// private struct PurchaseOrderFormCTX: Content { +// +// let htmxForm: HtmxFormCTX +// +// struct Context: Content { +// let branches: [VendorBranch.FormDTO] +// let employees: [Employee.DTO] +// } +// +// static func create(branches: [VendorBranch.FormDTO], employees: [Employee.DTO]) -> Self { +// .init(htmxForm: .init( +// formClass: "po-form", +// formId: "po-form", +// htmxTargetUrl: .post("/purchase-orders"), +// htmxTarget: "#po-table-body", +// htmxPushUrl: false, +// htmxResetAfterRequest: true, +// htmxSwapOob: nil, +// htmxSwap: .afterbegin, +// context: .init(branches: branches, employees: employees) +// )) +// } +// } +// +// extension VendorBranch { +// struct FormDTO: Content { +// let id: UUID +// let name: String +// let vendor: Vendor.DTO +// } +// +// func toFormDTO() throws -> VendorBranch.FormDTO { +// try .init( +// id: requireID(), +// name: name, +// vendor: vendor.toDTO() +// ) +// } +// } +// +// private extension PurchaseOrder { +// struct FormCreate: Content { +// let id: Int? +// let workOrder: String? +// let materials: String +// let customer: String +// let truckStock: Bool? +// let createdForID: Employee.IDValue +// let vendorBranchID: VendorBranch.IDValue +// +// // TODO: Remove. +// func toModel(createdByID: User.IDValue) -> PurchaseOrder { +// .init( +// id: id, +// workOrder: workOrder != nil ? (workOrder == "" ? nil : Int(workOrder!)) : nil, +// materials: materials, +// customer: customer, +// truckStock: truckStock ?? false, +// createdByID: createdByID, +// createdForID: createdForID, +// vendorBranchID: vendorBranchID, +// createdAt: nil, +// updatedAt: nil +// ) +// } +// +// func toCreate() -> PurchaseOrder.Create { +// .init( +// id: id, +// workOrder: workOrder != nil ? (workOrder == "" ? nil : Int(workOrder!)) : nil, +// materials: materials, +// customer: customer, +// truckStock: truckStock, +// createdForID: createdForID, +// vendorBranchID: vendorBranchID +// ) +// } +// } +// } +// +// private extension VendorBranchDB { +// +// func getBranches(req: Request) async throws -> [VendorBranch.FormDTO] { +// try await VendorBranch.query(on: req.db) +// .with(\.$vendor) +// .all() +// .map { try $0.toFormDTO() } +// } +// } +// +// extension PurchaseOrder.FormCreate: Validatable { +// +// static func validations(_ validations: inout Validations) { +// validations.add("materials", as: String.self, is: !.empty) +// validations.add("customer", as: String.self, is: !.empty) +// } +// } diff --git a/Sources/App/Controllers/View/UsersViewController.swift b/Sources/App/Controllers/View/UsersViewController.swift index ecc44ef..9176574 100644 --- a/Sources/App/Controllers/View/UsersViewController.swift +++ b/Sources/App/Controllers/View/UsersViewController.swift @@ -1,121 +1,121 @@ -import Dependencies -import Fluent -import Vapor - -struct UserViewController: RouteCollection { - - @Dependency(\.users) var users - - private let api = UserApiController() - - func boot(routes: any RoutesBuilder) throws { - let users = routes.protected.grouped("users") - users.get(use: index(req:)) - users.post(use: create(req:)) - users.group(":id") { - $0.get(use: details(req:)) - $0.delete(use: delete(req:)) - } - } - - @Sendable - func index(req: Request) async throws -> View { - try await renderIndex(req) - } - - @Sendable - private func renderIndex(_ req: Request, _ user: User.DTO? = nil) async throws -> View { - let users = try await api.getSortedUsers(req: req) - return try await req.view.render("users/index", UsersCTX(user: user, users: users)) - } - - @Sendable - func create(req: Request) async throws -> View { - let user = try await api.create(req: req) - return try await req.view.render("users/table-row", user) - } - - @Sendable - func details(req: Request) async throws -> View { - let user = try await users.get(req.ensureIDPathComponent()) - // Check if the page has been rendered before. - guard req.isHtmxRequest else { - // Not an htmx-request, so render the whole page with the details. - return try await renderIndex(req, user) - } - // An htmx-request header was present, so just return the details, - return try await req.view.render("users/detail", ["user": user]) - } - - @Sendable - func delete(req: Request) async throws -> View { - _ = try await api.delete(req: req) - return try await req.view.render("users/table", ["users": api.getSortedUsers(req: req)]) - } -} - -struct UserFormCTX: Content { - let htmxForm: HtmxFormCTX - - struct Context: Content { - let showConfirmPassword: Bool - let showEmailInput: Bool - let buttonLabel: String - } - - static func signIn(next: String?) -> Self { - .init( - htmxForm: .init( - formClass: "user-form", - formId: "user-form", - htmxTargetUrl: .post("/login\((next != nil && next != "/") ? "?next=\(next!)" : "")"), - htmxTarget: "user-table", - htmxPushUrl: true, - htmxResetAfterRequest: true, - htmxSwapOob: nil, - htmxSwap: .afterbegin, - context: .init(showConfirmPassword: false, showEmailInput: false, buttonLabel: "Sign In") - ) - ) - } - - static func create() -> Self { - .init( - htmxForm: .init( - formClass: "user-form", - formId: "user-form", - htmxTargetUrl: .post("/users"), - htmxTarget: "#user-table", - htmxPushUrl: false, - htmxResetAfterRequest: true, - htmxSwapOob: nil, - htmxSwap: nil, - context: .init(showConfirmPassword: true, showEmailInput: true, buttonLabel: "Create") - ) - ) - } -} - -private struct UsersCTX: Content { - let user: User.DTO? - let users: [User.DTO] - let form: UserFormCTX - - init( - user: User.DTO? = nil, - users: [User.DTO], - form: UserFormCTX? = nil - ) { - self.user = user - self.users = users - self.form = form ?? .create() - } -} - -private extension UserApiController { - - func getSortedUsers(req: Request) async throws -> [User.DTO] { - try await index(req: req) - .sorted { ($0.username ?? "") < ($1.username ?? "") } - } -} +// import Dependencies +// import Fluent +// import Vapor +// +// struct UserViewController: RouteCollection { +// +// @Dependency(\.users) var users +// +// private let api = UserApiController() +// +// func boot(routes: any RoutesBuilder) throws { +// let users = routes.protected.grouped("users") +// users.get(use: index(req:)) +// users.post(use: create(req:)) +// users.group(":id") { +// $0.get(use: details(req:)) +// $0.delete(use: delete(req:)) +// } +// } +// +// @Sendable +// func index(req: Request) async throws -> View { +// try await renderIndex(req) +// } +// +// @Sendable +// private func renderIndex(_ req: Request, _ user: User.DTO? = nil) async throws -> View { +// let users = try await api.getSortedUsers(req: req) +// return try await req.view.render("users/index", UsersCTX(user: user, users: users)) +// } +// +// @Sendable +// func create(req: Request) async throws -> View { +// let user = try await api.create(req: req) +// return try await req.view.render("users/table-row", user) +// } +// +// @Sendable +// func details(req: Request) async throws -> View { +// let user = try await users.get(req.ensureIDPathComponent()) +// // Check if the page has been rendered before. +// guard req.isHtmxRequest else { +// // Not an htmx-request, so render the whole page with the details. +// return try await renderIndex(req, user) +// } +// // An htmx-request header was present, so just return the details, +// return try await req.view.render("users/detail", ["user": user]) +// } +// +// @Sendable +// func delete(req: Request) async throws -> View { +// _ = try await api.delete(req: req) +// return try await req.view.render("users/table", ["users": api.getSortedUsers(req: req)]) +// } +// } +// +// struct UserFormCTX: Content { +// let htmxForm: HtmxFormCTX +// +// struct Context: Content { +// let showConfirmPassword: Bool +// let showEmailInput: Bool +// let buttonLabel: String +// } +// +// static func signIn(next: String?) -> Self { +// .init( +// htmxForm: .init( +// formClass: "user-form", +// formId: "user-form", +// htmxTargetUrl: .post("/login\((next != nil && next != "/") ? "?next=\(next!)" : "")"), +// htmxTarget: "user-table", +// htmxPushUrl: true, +// htmxResetAfterRequest: true, +// htmxSwapOob: nil, +// htmxSwap: .afterbegin, +// context: .init(showConfirmPassword: false, showEmailInput: false, buttonLabel: "Sign In") +// ) +// ) +// } +// +// static func create() -> Self { +// .init( +// htmxForm: .init( +// formClass: "user-form", +// formId: "user-form", +// htmxTargetUrl: .post("/users"), +// htmxTarget: "#user-table", +// htmxPushUrl: false, +// htmxResetAfterRequest: true, +// htmxSwapOob: nil, +// htmxSwap: nil, +// context: .init(showConfirmPassword: true, showEmailInput: true, buttonLabel: "Create") +// ) +// ) +// } +// } +// +// private struct UsersCTX: Content { +// let user: User.DTO? +// let users: [User.DTO] +// let form: UserFormCTX +// +// init( +// user: User.DTO? = nil, +// users: [User.DTO], +// form: UserFormCTX? = nil +// ) { +// self.user = user +// self.users = users +// self.form = form ?? .create() +// } +// } +// +// private extension UserApiController { +// +// func getSortedUsers(req: Request) async throws -> [User.DTO] { +// try await index(req: req) +// .sorted { ($0.username ?? "") < ($1.username ?? "") } +// } +// } diff --git a/Sources/App/Controllers/View/VendorViewController.swift b/Sources/App/Controllers/View/VendorViewController.swift index 14aa0d1..dbb6c77 100644 --- a/Sources/App/Controllers/View/VendorViewController.swift +++ b/Sources/App/Controllers/View/VendorViewController.swift @@ -1,108 +1,108 @@ -import Fluent -import Vapor - -struct VendorViewController: RouteCollection { - private let api = VendorApiController() - - func boot(routes: any RoutesBuilder) throws { - let vendors = routes.protected.grouped("vendors") - - vendors.get(use: index(req:)) - vendors.post(use: create(req:)) - vendors.group(":vendorID") { - $0.delete(use: delete(req:)) - $0.put(use: update(req:)) - } - } - - @Sendable - func index(req: Request) async throws -> View { - return try await req.view.render("vendors/index", makeCtx(req: req)) - } - - @Sendable - func create(req: Request) async throws -> View { - let ctx = try req.content.decode(CreateVendorCTX.self) - req.logger.debug("CTX: \(ctx)") - let vendor = Vendor.Create(name: ctx.name).toModel() - try await vendor.save(on: req.db) - - if let branchString = ctx.branches { - let branches = branchString.split(separator: ",") - .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } - - for branch in branches { - try await vendor.$branches.create( - VendorBranch(name: String(branch), vendorId: vendor.requireID()), - on: req.db - ) - } - } - - return try await req.view.render("vendors/table", makeCtx(req: req)) - } - - @Sendable - func delete(req: Request) async throws -> HTTPStatus { - try await api.delete(req: req) - } - - @Sendable - func update(req: Request) async throws -> View { - _ = try await api.update(req: req) - return try await req.view.render("vendors/table", makeCtx(req: req, oob: true)) - } - - private func makeCtx(req: Request, vendor: Vendor? = nil, oob: Bool = false) async throws -> VendorsCTX { - let vendors = try await Vendor.query(on: req.db) - .with(\.$branches) - .sort(\.$name, .ascending) - .all() - .map { $0.toDTO() } - - return .init( - vendors: vendors, - form: .init(vendor: vendor, oob: oob) - ) - } -} - -struct VendorFormCTX: Content { - let htmxForm: HtmxFormCTX - - init(vendor: Vendor? = nil, oob: Bool = false) { - self.htmxForm = .init( - formClass: "vendor-form", - formId: "vendor-form", - htmxTargetUrl: vendor == nil ? .post("/vendors") : .put("/vendors"), - htmxTarget: "#vendor-table", - htmxPushUrl: false, - htmxResetAfterRequest: true, - htmxSwapOob: oob ? .outerHTML : nil, - htmxSwap: oob ? nil : .outerHTML, - context: .init(vendor: vendor) - ) - } - - struct Context: Content { - let vendor: Vendor? - let branches: String? - let buttonLabel: String - - init(vendor: Vendor? = nil) { - self.vendor = vendor - self.branches = vendor?.branches.map(\.name).joined(separator: ", ") - self.buttonLabel = vendor == nil ? "Create" : "Update" - } - } -} - -private struct VendorsCTX: Content { - let vendors: [Vendor.DTO] - let form: VendorFormCTX -} - -private struct CreateVendorCTX: Content { - let name: String - let branches: String? -} +// import Fluent +// import Vapor +// +// struct VendorViewController: RouteCollection { +// private let api = VendorApiController() +// +// func boot(routes: any RoutesBuilder) throws { +// let vendors = routes.protected.grouped("vendors") +// +// vendors.get(use: index(req:)) +// vendors.post(use: create(req:)) +// vendors.group(":vendorID") { +// $0.delete(use: delete(req:)) +// $0.put(use: update(req:)) +// } +// } +// +// @Sendable +// func index(req: Request) async throws -> View { +// return try await req.view.render("vendors/index", makeCtx(req: req)) +// } +// +// @Sendable +// func create(req: Request) async throws -> View { +// let ctx = try req.content.decode(CreateVendorCTX.self) +// req.logger.debug("CTX: \(ctx)") +// let vendor = Vendor.Create(name: ctx.name).toModel() +// try await vendor.save(on: req.db) +// +// if let branchString = ctx.branches { +// let branches = branchString.split(separator: ",") +// .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } +// +// for branch in branches { +// try await vendor.$branches.create( +// VendorBranch(name: String(branch), vendorId: vendor.requireID()), +// on: req.db +// ) +// } +// } +// +// return try await req.view.render("vendors/table", makeCtx(req: req)) +// } +// +// @Sendable +// func delete(req: Request) async throws -> HTTPStatus { +// try await api.delete(req: req) +// } +// +// @Sendable +// func update(req: Request) async throws -> View { +// _ = try await api.update(req: req) +// return try await req.view.render("vendors/table", makeCtx(req: req, oob: true)) +// } +// +// private func makeCtx(req: Request, vendor: Vendor? = nil, oob: Bool = false) async throws -> VendorsCTX { +// let vendors = try await Vendor.query(on: req.db) +// .with(\.$branches) +// .sort(\.$name, .ascending) +// .all() +// .map { $0.toDTO() } +// +// return .init( +// vendors: vendors, +// form: .init(vendor: vendor, oob: oob) +// ) +// } +// } +// +// struct VendorFormCTX: Content { +// let htmxForm: HtmxFormCTX +// +// init(vendor: Vendor? = nil, oob: Bool = false) { +// self.htmxForm = .init( +// formClass: "vendor-form", +// formId: "vendor-form", +// htmxTargetUrl: vendor == nil ? .post("/vendors") : .put("/vendors"), +// htmxTarget: "#vendor-table", +// htmxPushUrl: false, +// htmxResetAfterRequest: true, +// htmxSwapOob: oob ? .outerHTML : nil, +// htmxSwap: oob ? nil : .outerHTML, +// context: .init(vendor: vendor) +// ) +// } +// +// struct Context: Content { +// let vendor: Vendor? +// let branches: String? +// let buttonLabel: String +// +// init(vendor: Vendor? = nil) { +// self.vendor = vendor +// self.branches = vendor?.branches.map(\.name).joined(separator: ", ") +// self.buttonLabel = vendor == nil ? "Create" : "Update" +// } +// } +// } +// +// private struct VendorsCTX: Content { +// let vendors: [Vendor.DTO] +// let form: VendorFormCTX +// } +// +// private struct CreateVendorCTX: Content { +// let name: String +// let branches: String? +// } diff --git a/Sources/App/Controllers/ViewController.swift b/Sources/App/Controllers/ViewController.swift index 43c4be3..8a75fc0 100644 --- a/Sources/App/Controllers/ViewController.swift +++ b/Sources/App/Controllers/ViewController.swift @@ -1,111 +1,111 @@ -import Fluent -import Leaf -import Vapor - -struct ViewController: RouteCollection { - - private let api = ApiController() - private let employees = EmployeeViewController() - private let purchaseOrders = PurchaseOrderViewController() - private let users = UserViewController() - private let vendors = VendorViewController() - - func boot(routes: any RoutesBuilder) throws { - let protected = routes.protected - - // MARK: - Non-protected routes. - - // routes.get(use: index(req:)) - routes.get("login", use: getLogin(req:)) - routes.post("login", use: postLogin(req:)) - - // MARK: Protected routes. - - protected.get(use: home(req:)) - protected.get("**", use: catchAll(req:)) - protected.post("logout", use: logout(req:)) - // protected.get("users", use: users(req:)) - try routes.register(collection: employees) - try routes.register(collection: purchaseOrders) - try routes.register(collection: users) - try routes.register(collection: vendors) - } - - @Sendable - func getLogin(req: Request) async throws -> View { - req.logger.debug("Login Query: \(req.url.query ?? "n/a")") - let params = try? req.query.decode(LoginParameter.self) - return try await req.view.render( - "login", UserFormCTX.signIn(next: params?.next) - ) - } - - @Sendable - func postLogin(req: Request) async throws -> Response { - let content = try req.content.decode(UserForm.self) - guard let user = try await User.query(on: req.db) - .filter(\.$username == content.username) - .first() - else { - throw Abort(.badRequest, reason: "User not found.") - } - - guard try user.verify(password: content.password) else { - throw Abort(.unauthorized, reason: "Invalid password.") - } - req.auth.login(user) - - req.logger.debug("User logged in: \(user.toDTO())") - return try await home(req: req) - } - - @Sendable - func logout(req: Request) async throws -> View { - req.auth.logout(User.self) - return try await req.view.render("login") - } - - @Sendable - func home(req: Request) async throws -> Response { - if let loginParams = try? req.query.decode(LoginParameter.self) { - return req.redirect(to: loginParams.next) - } - return try await req.view.render("home").encodeResponse(for: req) - } - - @Sendable - func catchAll(req: Request) async throws -> View { - var route: HomeRoute? - - if let loginParams = try? req.query.decode(LoginParameter.self), - let next = loginParams.next.split(separator: "/").last - { - route = HomeRoute(rawValue: String(next)) - } else if let routeString = req.parameters.getCatchall().last { - route = HomeRoute(rawValue: routeString) - } - - return try await req.view.render("home", HomeCTX(route: route)) - } - -} - -private struct UserForm: Content { - let username: String - let password: String -} - -enum HomeRoute: String, Content { - case employees - case purchaseOrders - case users - case vendors -} - -struct HomeCTX: Content { - let route: HomeRoute? -} - -struct LoginParameter: Content { - let next: String -} +// import Fluent +// import Leaf +// import Vapor +// +// struct ViewController: RouteCollection { +// +// private let api = ApiController() +// private let employees = EmployeeViewController() +// private let purchaseOrders = PurchaseOrderViewController() +// private let users = UserViewController() +// private let vendors = VendorViewController() +// +// func boot(routes: any RoutesBuilder) throws { +// let protected = routes.protected +// +// // MARK: - Non-protected routes. +// +// // routes.get(use: index(req:)) +// routes.get("login", use: getLogin(req:)) +// routes.post("login", use: postLogin(req:)) +// +// // MARK: Protected routes. +// +// protected.get(use: home(req:)) +// protected.get("**", use: catchAll(req:)) +// protected.post("logout", use: logout(req:)) +// // protected.get("users", use: users(req:)) +// try routes.register(collection: employees) +// try routes.register(collection: purchaseOrders) +// try routes.register(collection: users) +// try routes.register(collection: vendors) +// } +// +// @Sendable +// func getLogin(req: Request) async throws -> View { +// req.logger.debug("Login Query: \(req.url.query ?? "n/a")") +// let params = try? req.query.decode(LoginParameter.self) +// return try await req.view.render( +// "login", UserFormCTX.signIn(next: params?.next) +// ) +// } +// +// @Sendable +// func postLogin(req: Request) async throws -> Response { +// let content = try req.content.decode(UserForm.self) +// guard let user = try await User.query(on: req.db) +// .filter(\.$username == content.username) +// .first() +// else { +// throw Abort(.badRequest, reason: "User not found.") +// } +// +// guard try user.verify(password: content.password) else { +// throw Abort(.unauthorized, reason: "Invalid password.") +// } +// req.auth.login(user) +// +// req.logger.debug("User logged in: \(user.toDTO())") +// return try await home(req: req) +// } +// +// @Sendable +// func logout(req: Request) async throws -> View { +// req.auth.logout(User.self) +// return try await req.view.render("login") +// } +// +// @Sendable +// func home(req: Request) async throws -> Response { +// if let loginParams = try? req.query.decode(LoginParameter.self) { +// return req.redirect(to: loginParams.next) +// } +// return try await req.view.render("home").encodeResponse(for: req) +// } +// +// @Sendable +// func catchAll(req: Request) async throws -> View { +// var route: HomeRoute? +// +// if let loginParams = try? req.query.decode(LoginParameter.self), +// let next = loginParams.next.split(separator: "/").last +// { +// route = HomeRoute(rawValue: String(next)) +// } else if let routeString = req.parameters.getCatchall().last { +// route = HomeRoute(rawValue: routeString) +// } +// +// return try await req.view.render("home", HomeCTX(route: route)) +// } +// +// } +// +// private struct UserForm: Content { +// let username: String +// let password: String +// } +// +// enum HomeRoute: String, Content { +// case employees +// case purchaseOrders +// case users +// case vendors +// } +// +// struct HomeCTX: Content { +// let route: HomeRoute? +// } +// +// struct LoginParameter: Content { +// let next: String +// } diff --git a/Sources/App/DB/EmployeeDB.swift b/Sources/App/DB/EmployeeDB.swift index 8e0a829..a44a7d5 100644 --- a/Sources/App/DB/EmployeeDB.swift +++ b/Sources/App/DB/EmployeeDB.swift @@ -1,88 +1,88 @@ -import Dependencies -import DependenciesMacros -import Fluent -import Vapor - -extension DependencyValues { - // An intermediate layer between our api and view controllers that interacts with the - // database model. - var employees: EmployeeDB { - get { self[EmployeeDB.self] } - set { self[EmployeeDB.self] = newValue } - } -} - -@DependencyClient -struct EmployeeDB: Sendable { - var create: @Sendable (Employee.Create) async throws -> Employee.DTO - var fetchAll: @Sendable (FetchRequest) async throws -> [Employee.DTO] - var get: @Sendable (Employee.IDValue) async throws -> Employee.DTO? - var update: @Sendable (Employee.IDValue, Employee.Update) async throws -> Employee.DTO - var delete: @Sendable (Employee.IDValue) async throws -> Void - var toggleActive: @Sendable (Employee.IDValue) async throws -> Employee.DTO - - enum FetchRequest { - case active - case `default` - } - - func fetchAll() async throws -> [Employee.DTO] { - try await fetchAll(.default) - } - - func get(_ id: String?) async throws -> Employee.DTO? { - guard let idString = id, let id = UUID(uuidString: idString) else { - throw Abort(.badRequest, reason: "Employee id not valid.") - } - return try await get(id) - } -} - -extension EmployeeDB: TestDependencyKey { - static let testValue: EmployeeDB = Self() - - static func live(database: any Database) -> Self { - .init( - create: { model in - let model = model.toModel() - try await model.save(on: database) - return model.toDTO() - }, - fetchAll: { request in - var query = Employee.query(on: database) - .sort(\.$lastName) - - if request == .active { - query = query.filter(\.$active == true) - } - - return try await query.all().map { $0.toDTO() } - }, - get: { id in - try await Employee.find(id, on: database).map { $0.toDTO() } - }, - update: { id, updates in - guard let employee = try await Employee.find(id, on: database) else { - throw Abort(.badRequest, reason: "Employee id not found.") - } - employee.applyUpdates(updates) - try await employee.save(on: database) - return employee.toDTO() - }, - delete: { id in - guard let employee = try await Employee.find(id, on: database) else { - throw Abort(.badRequest, reason: "Employee id not found.") - } - try await employee.delete(on: database) - }, - toggleActive: { id in - guard let employee = try await Employee.find(id, on: database) else { - throw Abort(.notFound) - } - employee.active.toggle() - try await employee.save(on: database) - return employee.toDTO() - } - ) - } -} +// import Dependencies +// import DependenciesMacros +// import Fluent +// import Vapor +// +// extension DependencyValues { +// // An intermediate layer between our api and view controllers that interacts with the +// // database model. +// var employees: EmployeeDB { +// get { self[EmployeeDB.self] } +// set { self[EmployeeDB.self] = newValue } +// } +// } +// +// @DependencyClient +// struct EmployeeDB: Sendable { +// var create: @Sendable (Employee.Create) async throws -> Employee.DTO +// var fetchAll: @Sendable (FetchRequest) async throws -> [Employee.DTO] +// var get: @Sendable (Employee.IDValue) async throws -> Employee.DTO? +// var update: @Sendable (Employee.IDValue, Employee.Update) async throws -> Employee.DTO +// var delete: @Sendable (Employee.IDValue) async throws -> Void +// var toggleActive: @Sendable (Employee.IDValue) async throws -> Employee.DTO +// +// enum FetchRequest { +// case active +// case `default` +// } +// +// func fetchAll() async throws -> [Employee.DTO] { +// try await fetchAll(.default) +// } +// +// func get(_ id: String?) async throws -> Employee.DTO? { +// guard let idString = id, let id = UUID(uuidString: idString) else { +// throw Abort(.badRequest, reason: "Employee id not valid.") +// } +// return try await get(id) +// } +// } +// +// extension EmployeeDB: TestDependencyKey { +// static let testValue: EmployeeDB = Self() +// +// static func live(database: any Database) -> Self { +// .init( +// create: { model in +// let model = model.toModel() +// try await model.save(on: database) +// return model.toDTO() +// }, +// fetchAll: { request in +// var query = Employee.query(on: database) +// .sort(\.$lastName) +// +// if request == .active { +// query = query.filter(\.$active == true) +// } +// +// return try await query.all().map { $0.toDTO() } +// }, +// get: { id in +// try await Employee.find(id, on: database).map { $0.toDTO() } +// }, +// update: { id, updates in +// guard let employee = try await Employee.find(id, on: database) else { +// throw Abort(.badRequest, reason: "Employee id not found.") +// } +// employee.applyUpdates(updates) +// try await employee.save(on: database) +// return employee.toDTO() +// }, +// delete: { id in +// guard let employee = try await Employee.find(id, on: database) else { +// throw Abort(.badRequest, reason: "Employee id not found.") +// } +// try await employee.delete(on: database) +// }, +// toggleActive: { id in +// guard let employee = try await Employee.find(id, on: database) else { +// throw Abort(.notFound) +// } +// employee.active.toggle() +// try await employee.save(on: database) +// return employee.toDTO() +// } +// ) +// } +// } diff --git a/Sources/App/DB/PurchaseOrderDB.swift b/Sources/App/DB/PurchaseOrderDB.swift index 99fa7bf..07d0d80 100644 --- a/Sources/App/DB/PurchaseOrderDB.swift +++ b/Sources/App/DB/PurchaseOrderDB.swift @@ -1,87 +1,87 @@ -import Dependencies -import DependenciesMacros -import Fluent -import Vapor - -extension DependencyValues { - // An intermediate between our api and view controllers that interacts with the - // database. - var purchaseOrders: PurchaseOrdersDB { - get { self[PurchaseOrdersDB.self] } - set { self[PurchaseOrdersDB.self] = newValue } - } -} - -@DependencyClient -struct PurchaseOrdersDB: Sendable { - var create: @Sendable (PurchaseOrder.Create, User.IDValue) async throws -> PurchaseOrder.DTO - var fetchAll: @Sendable () async throws -> [PurchaseOrder.DTO] - var fetchPage: @Sendable (PageRequest) async throws -> Page - var get: @Sendable (PurchaseOrder.IDValue) async throws -> PurchaseOrder.DTO? - // var update: @Sendable (PurchaseOrder.IDValue, PurchaseOrder.Update) async throws -> PurchaseOrder.DTO - var delete: @Sendable (PurchaseOrder.IDValue) async throws -> Void -} - -extension PurchaseOrdersDB: TestDependencyKey { - static let testValue: PurchaseOrdersDB = Self() - - static func live(database db: any Database) -> Self { - .init( - create: { model, createdById in - guard let employee = try await Employee.find(model.createdForID, on: db) else { - throw Abort(.notFound, reason: "Employee not found.") - } - - guard employee.active else { - throw Abort(.badRequest, reason: "Employee is not active, unable to generate a PO for in-active employees") - } - - let purchaseOrder = model.toModel(createdByID: createdById) - try await purchaseOrder.save(on: db) - guard let loaded = try await PurchaseOrder.get(purchaseOrder.requireID(), on: db) else { - return purchaseOrder.toDTO() - } - return loaded - - }, - fetchAll: { - try await PurchaseOrder.allQuery(on: db) - .sort(\.$id, .descending) - .all().map { $0.toDTO() } - }, - fetchPage: { request in - try await PurchaseOrder.allQuery(on: db) - .sort(\.$id, .descending) - .paginate(request) - .map { $0.toDTO() } - }, - get: { id in - try await PurchaseOrder.get(id, on: db) - }, - delete: { id in - guard let purchaseOrder = try await PurchaseOrder.find(id, on: db) else { - throw Abort(.notFound) - } - try await purchaseOrder.delete(on: db) - } - ) - } - -} - -private extension PurchaseOrder { - static func allQuery(on db: any Database) -> QueryBuilder { - PurchaseOrder.query(on: db) - .with(\.$createdBy) - .with(\.$createdFor) - .with(\.$vendorBranch) { branch in - branch.with(\.$vendor) - } - } - - static func get(_ id: PurchaseOrder.IDValue, on db: any Database) async throws -> PurchaseOrder.DTO? { - try await PurchaseOrder.allQuery(on: db) - .filter(\.$id == id) - .first()?.toDTO() - } -} +// import Dependencies +// import DependenciesMacros +// import Fluent +// import Vapor +// +// extension DependencyValues { +// // An intermediate between our api and view controllers that interacts with the +// // database. +// var purchaseOrders: PurchaseOrdersDB { +// get { self[PurchaseOrdersDB.self] } +// set { self[PurchaseOrdersDB.self] = newValue } +// } +// } +// +// @DependencyClient +// struct PurchaseOrdersDB: Sendable { +// var create: @Sendable (PurchaseOrder.Create, User.IDValue) async throws -> PurchaseOrder.DTO +// var fetchAll: @Sendable () async throws -> [PurchaseOrder.DTO] +// var fetchPage: @Sendable (PageRequest) async throws -> Page +// var get: @Sendable (PurchaseOrder.IDValue) async throws -> PurchaseOrder.DTO? +// // var update: @Sendable (PurchaseOrder.IDValue, PurchaseOrder.Update) async throws -> PurchaseOrder.DTO +// var delete: @Sendable (PurchaseOrder.IDValue) async throws -> Void +// } +// +// extension PurchaseOrdersDB: TestDependencyKey { +// static let testValue: PurchaseOrdersDB = Self() +// +// static func live(database db: any Database) -> Self { +// .init( +// create: { model, createdById in +// guard let employee = try await Employee.find(model.createdForID, on: db) else { +// throw Abort(.notFound, reason: "Employee not found.") +// } +// +// guard employee.active else { +// throw Abort(.badRequest, reason: "Employee is not active, unable to generate a PO for in-active employees") +// } +// +// let purchaseOrder = model.toModel(createdByID: createdById) +// try await purchaseOrder.save(on: db) +// guard let loaded = try await PurchaseOrder.get(purchaseOrder.requireID(), on: db) else { +// return purchaseOrder.toDTO() +// } +// return loaded +// +// }, +// fetchAll: { +// try await PurchaseOrder.allQuery(on: db) +// .sort(\.$id, .descending) +// .all().map { $0.toDTO() } +// }, +// fetchPage: { request in +// try await PurchaseOrder.allQuery(on: db) +// .sort(\.$id, .descending) +// .paginate(request) +// .map { $0.toDTO() } +// }, +// get: { id in +// try await PurchaseOrder.get(id, on: db) +// }, +// delete: { id in +// guard let purchaseOrder = try await PurchaseOrder.find(id, on: db) else { +// throw Abort(.notFound) +// } +// try await purchaseOrder.delete(on: db) +// } +// ) +// } +// +// } +// +// private extension PurchaseOrder { +// static func allQuery(on db: any Database) -> QueryBuilder { +// PurchaseOrder.query(on: db) +// .with(\.$createdBy) +// .with(\.$createdFor) +// .with(\.$vendorBranch) { branch in +// branch.with(\.$vendor) +// } +// } +// +// static func get(_ id: PurchaseOrder.IDValue, on db: any Database) async throws -> PurchaseOrder.DTO? { +// try await PurchaseOrder.allQuery(on: db) +// .filter(\.$id == id) +// .first()?.toDTO() +// } +// } diff --git a/Sources/App/DB/UserDB.swift b/Sources/App/DB/UserDB.swift index 1512ecf..114e09d 100644 --- a/Sources/App/DB/UserDB.swift +++ b/Sources/App/DB/UserDB.swift @@ -1,60 +1,60 @@ -import Dependencies -import DependenciesMacros -import Fluent -import Vapor - -extension DependencyValues { - var users: UserDB { - get { self[UserDB.self] } - set { self[UserDB.self] = newValue } - } -} - -@DependencyClient -struct UserDB: Sendable { - var create: @Sendable (User.Create) async throws -> User.DTO - var delete: @Sendable (User.IDValue) async throws -> Void - var fetchAll: @Sendable () async throws -> [User.DTO] - var get: @Sendable (User.IDValue) async throws -> User.DTO? - var login: @Sendable (User) async throws -> UserToken -} - -extension UserDB: TestDependencyKey { - static let testValue: UserDB = Self() - - static func live(database db: any Database) -> Self { - self.init( - create: { model in - guard model.password == model.confirmPassword else { - throw Abort(.badRequest, reason: "Passwords did not match.") - } - let user = try User( - username: model.username, - email: model.email, - passwordHash: Bcrypt.hash(model.password) - ) - try await user.save(on: db) - return user.toDTO() - - }, - delete: { id in - guard let user = try await User.find(id, on: db) else { - throw Abort(.notFound) - } - try await user.delete(on: db) - - }, - fetchAll: { - try await User.query(on: db).all().map { $0.toDTO() } - }, - get: { id in - try await User.find(id, on: db).map { $0.toDTO() } - }, - login: { user in - let token = try user.generateToken() - try await token.save(on: db) - return token - } - ) - } -} +// import Dependencies +// import DependenciesMacros +// import Fluent +// import Vapor +// +// extension DependencyValues { +// var users: UserDB { +// get { self[UserDB.self] } +// set { self[UserDB.self] = newValue } +// } +// } +// +// @DependencyClient +// struct UserDB: Sendable { +// var create: @Sendable (User.Create) async throws -> User.DTO +// var delete: @Sendable (User.IDValue) async throws -> Void +// var fetchAll: @Sendable () async throws -> [User.DTO] +// var get: @Sendable (User.IDValue) async throws -> User.DTO? +// var login: @Sendable (User) async throws -> UserToken +// } +// +// extension UserDB: TestDependencyKey { +// static let testValue: UserDB = Self() +// +// static func live(database db: any Database) -> Self { +// self.init( +// create: { model in +// guard model.password == model.confirmPassword else { +// throw Abort(.badRequest, reason: "Passwords did not match.") +// } +// let user = try User( +// username: model.username, +// email: model.email, +// passwordHash: Bcrypt.hash(model.password) +// ) +// try await user.save(on: db) +// return user.toDTO() +// +// }, +// delete: { id in +// guard let user = try await User.find(id, on: db) else { +// throw Abort(.notFound) +// } +// try await user.delete(on: db) +// +// }, +// fetchAll: { +// try await User.query(on: db).all().map { $0.toDTO() } +// }, +// get: { id in +// try await User.find(id, on: db).map { $0.toDTO() } +// }, +// login: { user in +// let token = try user.generateToken() +// try await token.save(on: db) +// return token +// } +// ) +// } +// } diff --git a/Sources/App/DB/VendorBranchDB.swift b/Sources/App/DB/VendorBranchDB.swift index 7ea01e6..96b553e 100644 --- a/Sources/App/DB/VendorBranchDB.swift +++ b/Sources/App/DB/VendorBranchDB.swift @@ -1,87 +1,87 @@ -import Dependencies -import DependenciesMacros -import Fluent -import Vapor - -public extension DependencyValues { - var vendorBranches: VendorBranchDB { - get { self[VendorBranchDB.self] } - set { self[VendorBranchDB.self] = newValue } - } -} - -@DependencyClient -public struct VendorBranchDB: Sendable { - var create: @Sendable (VendorBranch.Create, Vendor.IDValue) async throws -> VendorBranch.DTO - var delete: @Sendable (VendorBranch.IDValue) async throws -> Void - var fetchAll: @Sendable (FetchRequest) async throws -> [VendorBranch.DTO] - var get: @Sendable (VendorBranch.IDValue) async throws -> VendorBranch.DTO? - var update: @Sendable (VendorBranch.IDValue, VendorBranch.Update) async throws -> VendorBranch.DTO - - enum FetchRequest: Equatable { - case `default` - case `for`(vendorID: Vendor.IDValue) - case withVendor - } - - func fetchAll() async throws -> [VendorBranch.DTO] { - try await fetchAll(.default) - } -} - -extension VendorBranchDB: TestDependencyKey { - public static let testValue: VendorBranchDB = Self() - - static func live(database db: any Database) -> Self { - .init( - create: { model, vendorID in - let branch = model.toModel() - guard let vendor = try await Vendor.find(vendorID, on: db) else { - throw Abort(.badRequest, reason: "Vendor does not exist.") - } - try await vendor.$branches.create(branch, on: db) - return branch.toDTO() - }, - delete: { id in - guard let branch = try await VendorBranch.find(id, on: db) else { - throw Abort(.notFound) - } - try await branch.delete(on: db) - }, - fetchAll: { request in - var query = VendorBranch.query(on: db) - switch request { - case .withVendor: - query = query.with(\.$vendor) - - case let .for(vendorID: vendorID): - let branches = try await Vendor.query(on: db) - .filter(\.$id == vendorID) - .with(\.$branches) - .first()? - .branches - .map { $0.toDTO() } - - guard let branches else { throw Abort(.badGateway, reason: "Vendor id not found.") } - return branches - - case .default: - break - } - return try await query.all().map { $0.toDTO() } - - }, - get: { id in - try await VendorBranch.find(id, on: db).map { $0.toDTO() } - }, - update: { id, updates in - guard let branch = try await VendorBranch.find(id, on: db) else { - throw Abort(.notFound) - } - branch.applyUpdates(updates) - try await branch.save(on: db) - return branch.toDTO() - } - ) - } -} +// import Dependencies +// import DependenciesMacros +// import Fluent +// import Vapor +// +// public extension DependencyValues { +// var vendorBranches: VendorBranchDB { +// get { self[VendorBranchDB.self] } +// set { self[VendorBranchDB.self] = newValue } +// } +// } +// +// @DependencyClient +// public struct VendorBranchDB: Sendable { +// var create: @Sendable (VendorBranch.Create, Vendor.IDValue) async throws -> VendorBranch.DTO +// var delete: @Sendable (VendorBranch.IDValue) async throws -> Void +// var fetchAll: @Sendable (FetchRequest) async throws -> [VendorBranch.DTO] +// var get: @Sendable (VendorBranch.IDValue) async throws -> VendorBranch.DTO? +// var update: @Sendable (VendorBranch.IDValue, VendorBranch.Update) async throws -> VendorBranch.DTO +// +// enum FetchRequest: Equatable { +// case `default` +// case `for`(vendorID: Vendor.IDValue) +// case withVendor +// } +// +// func fetchAll() async throws -> [VendorBranch.DTO] { +// try await fetchAll(.default) +// } +// } +// +// extension VendorBranchDB: TestDependencyKey { +// public static let testValue: VendorBranchDB = Self() +// +// static func live(database db: any Database) -> Self { +// .init( +// create: { model, vendorID in +// let branch = model.toModel() +// guard let vendor = try await Vendor.find(vendorID, on: db) else { +// throw Abort(.badRequest, reason: "Vendor does not exist.") +// } +// try await vendor.$branches.create(branch, on: db) +// return branch.toDTO() +// }, +// delete: { id in +// guard let branch = try await VendorBranch.find(id, on: db) else { +// throw Abort(.notFound) +// } +// try await branch.delete(on: db) +// }, +// fetchAll: { request in +// var query = VendorBranch.query(on: db) +// switch request { +// case .withVendor: +// query = query.with(\.$vendor) +// +// case let .for(vendorID: vendorID): +// let branches = try await Vendor.query(on: db) +// .filter(\.$id == vendorID) +// .with(\.$branches) +// .first()? +// .branches +// .map { $0.toDTO() } +// +// guard let branches else { throw Abort(.badGateway, reason: "Vendor id not found.") } +// return branches +// +// case .default: +// break +// } +// return try await query.all().map { $0.toDTO() } +// +// }, +// get: { id in +// try await VendorBranch.find(id, on: db).map { $0.toDTO() } +// }, +// update: { id, updates in +// guard let branch = try await VendorBranch.find(id, on: db) else { +// throw Abort(.notFound) +// } +// branch.applyUpdates(updates) +// try await branch.save(on: db) +// return branch.toDTO() +// } +// ) +// } +// } diff --git a/Sources/App/DB/VendorDB.swift b/Sources/App/DB/VendorDB.swift index 2078574..5247765 100644 --- a/Sources/App/DB/VendorDB.swift +++ b/Sources/App/DB/VendorDB.swift @@ -1,85 +1,85 @@ -import Dependencies -import DependenciesMacros -import Fluent -import Vapor - -public extension DependencyValues { - var vendors: VendorDB { - get { self[VendorDB.self] } - set { self[VendorDB.self] = newValue } - } -} - -@DependencyClient -public struct VendorDB: Sendable { - var create: @Sendable (Vendor.Create) async throws -> Vendor.DTO - var delete: @Sendable (Vendor.IDValue) async throws -> Void - var fetchAll: @Sendable (FetchRequest) async throws -> [Vendor.DTO] - var get: @Sendable (Vendor.IDValue, GetRequest) async throws -> Vendor.DTO? - var update: @Sendable (Vendor.IDValue, Vendor.Update) async throws -> Vendor.DTO - - enum FetchRequest { - case `default` - case withBranches - } - - enum GetRequest { - case `default` - case withBranches - } - - func fetchAll() async throws -> [Vendor.DTO] { - try await fetchAll(.default) - } - - func get(_ id: Vendor.IDValue) async throws -> Vendor.DTO? { - try await get(id, .default) - } -} - -extension VendorDB: TestDependencyKey { - public static let testValue: VendorDB = Self() - - static func live(database db: any Database) -> Self { - .init( - create: { model in - let model = model.toModel() - try await model.save(on: db) - return model.toDTO() - - }, - delete: { id in - guard let vendor = try await Vendor.find(id, on: db) else { - throw Abort(.notFound) - } - try await vendor.delete(on: db) - - }, - fetchAll: { request in - var query = Vendor.query(on: db).sort(\.$name, .ascending) - let withBranches = request == .withBranches - if withBranches { - query = query.with(\.$branches) - } - return try await query.all().map { $0.toDTO(includeBranches: withBranches) } - - }, - get: { id, request in - var query = Vendor.query(on: db).filter(\.$id == id) - let withBranches = request == .withBranches - if withBranches { - query = query.with(\.$branches) - } - return try await query.first().map { $0.toDTO(includeBranches: withBranches) } - - }, - update: { id, updates in - guard let vendor = try await Vendor.find(id, on: db) else { - throw Abort(.notFound) - } - vendor.applyUpdates(updates) - return vendor.toDTO() - } - ) - } -} +// import Dependencies +// import DependenciesMacros +// import Fluent +// import Vapor +// +// public extension DependencyValues { +// var vendors: VendorDB { +// get { self[VendorDB.self] } +// set { self[VendorDB.self] = newValue } +// } +// } +// +// @DependencyClient +// public struct VendorDB: Sendable { +// var create: @Sendable (Vendor.Create) async throws -> Vendor.DTO +// var delete: @Sendable (Vendor.IDValue) async throws -> Void +// var fetchAll: @Sendable (FetchRequest) async throws -> [Vendor.DTO] +// var get: @Sendable (Vendor.IDValue, GetRequest) async throws -> Vendor.DTO? +// var update: @Sendable (Vendor.IDValue, Vendor.Update) async throws -> Vendor.DTO +// +// enum FetchRequest { +// case `default` +// case withBranches +// } +// +// enum GetRequest { +// case `default` +// case withBranches +// } +// +// func fetchAll() async throws -> [Vendor.DTO] { +// try await fetchAll(.default) +// } +// +// func get(_ id: Vendor.IDValue) async throws -> Vendor.DTO? { +// try await get(id, .default) +// } +// } +// +// extension VendorDB: TestDependencyKey { +// public static let testValue: VendorDB = Self() +// +// static func live(database db: any Database) -> Self { +// .init( +// create: { model in +// let model = model.toModel() +// try await model.save(on: db) +// return model.toDTO() +// +// }, +// delete: { id in +// guard let vendor = try await Vendor.find(id, on: db) else { +// throw Abort(.notFound) +// } +// try await vendor.delete(on: db) +// +// }, +// fetchAll: { request in +// var query = Vendor.query(on: db).sort(\.$name, .ascending) +// let withBranches = request == .withBranches +// if withBranches { +// query = query.with(\.$branches) +// } +// return try await query.all().map { $0.toDTO(includeBranches: withBranches) } +// +// }, +// get: { id, request in +// var query = Vendor.query(on: db).filter(\.$id == id) +// let withBranches = request == .withBranches +// if withBranches { +// query = query.with(\.$branches) +// } +// return try await query.first().map { $0.toDTO(includeBranches: withBranches) } +// +// }, +// update: { id, updates in +// guard let vendor = try await Vendor.find(id, on: db) else { +// throw Abort(.notFound) +// } +// vendor.applyUpdates(updates) +// return vendor.toDTO() +// } +// ) +// } +// } diff --git a/Sources/App/Extensions/RouteBuilder+protected.swift b/Sources/App/Extensions/RouteBuilder+protected.swift index f44065e..d608e3e 100644 --- a/Sources/App/Extensions/RouteBuilder+protected.swift +++ b/Sources/App/Extensions/RouteBuilder+protected.swift @@ -1,3 +1,5 @@ +import DatabaseClientLive +import SharedModels import Vapor extension RoutesBuilder { @@ -5,16 +7,19 @@ extension RoutesBuilder { // Used to ensure views are protected, redirects users to the login page if they're // not authenticated. var protected: any RoutesBuilder { - #if DEBUG - return self - #else - return grouped( - User.credentialsAuthenticator(), - User.redirectMiddleware { req in - "login?next=\(req.url)" - } - ) - #endif + // #if DEBUG + // return self + // #else + return grouped( + // User.credentialsAuthenticator(), + UserPasswordAuthenticator(), + UserTokenAuthenticator(), + UserSessionAuthenticator(), + User.redirectMiddleware { req in + "login?next=\(req.url)" + } + ) + // #endif } func apiUnprotected(route: PathComponent) -> any RoutesBuilder { @@ -24,15 +29,17 @@ extension RoutesBuilder { // Allows basic or token authentication for api routes and prefixes the // given route with "/api/v1". func apiProtected(route: PathComponent) -> any RoutesBuilder { - #if DEBUG - return apiUnprotected(route: route) - #else - let prefixed = grouped("api", "v1", route) - return prefixed.grouped( - User.authenticator(), - UserToken.authenticator(), - User.guardMiddleware() - ) - #endif + // #if DEBUG + // return apiUnprotected(route: route) + // #else + let prefixed = grouped("api", "v1", route) + return prefixed.grouped( + UserPasswordAuthenticator(), + UserTokenAuthenticator(), + // User.authenticator(), + // UserToken.authenticator(), + User.guardMiddleware() + ) + // #endif } } diff --git a/Sources/App/Models/Employee.swift b/Sources/App/Models/Employee.swift index 2bce85a..b9f6a12 100644 --- a/Sources/App/Models/Employee.swift +++ b/Sources/App/Models/Employee.swift @@ -1,165 +1,165 @@ -import Fluent -import struct Foundation.UUID -import Vapor - -// TODO: Add soft-delete?? - -/// The employee database model. -/// -/// An employee is someone that PO's can be generated for. They can be either a field -/// employee / technician, an office employee, or an administrator. -/// -/// # NOTE: Only `User` types can login and generate po's for employees. -/// -final class Employee: Model, @unchecked Sendable { - - static let schema = "employee" - - @ID(key: .id) - var id: UUID? - - @Field(key: "first_name") - var firstName: String - - @Field(key: "last_name") - var lastName: String - - @Field(key: "is_active") - var active: Bool - - @Timestamp(key: "created_at", on: .create) - var createdAt: Date? - - @Timestamp(key: "updated_at", on: .update) - var updatedAt: Date? - - init() {} - - init( - id: UUID? = nil, - firstName: String, - lastName: String, - active: Bool, - createdAt: Date? = nil, - updatedAt: Date? = nil - ) { - self.id = id - self.firstName = firstName - self.lastName = lastName - self.active = active - self.createdAt = createdAt - self.updatedAt = updatedAt - } - - func toDTO() -> DTO { - .init( - id: id, - firstName: $firstName.value, - lastName: $lastName.value, - active: $active.value, - createdAt: createdAt, - updatedAt: updatedAt - ) - } - - func applyUpdates(_ updates: Update) { - if let firstName = updates.firstName { - self.firstName = firstName - } - if let lastName = updates.lastName { - self.lastName = lastName - } - if let active = updates.active { - self.active = active - } - } -} - -// MARK: - Helpers - -extension Employee { - - struct Create: Content { - let firstName: String - let lastName: String - let active: Bool? - - func toModel() -> Employee { - .init( - firstName: firstName, - lastName: lastName, - active: active ?? true - ) - } - } - - struct DTO: Content { - - var id: UUID? - var firstName: String? - var lastName: String? - var active: Bool? - var createdAt: Date? - var updatedAt: Date? - - func toModel() -> Employee { - let model = Employee() - - model.id = id - if let firstName { - model.firstName = firstName - } - if let lastName { - model.lastName = lastName - } - if let active { - model.active = active - } - return model - } - } - - struct Migrate: AsyncMigration { - - let name = "CreateEmployee" - - func prepare(on database: any Database) async throws { - try await database.schema(Employee.schema) - .id() - .field("first_name", .string, .required) - .field("last_name", .string, .required) - .field("is_active", .bool, .required, .sql(.default(true))) - .field("created_at", .datetime) - .field("updated_at", .datetime) - .unique(on: "first_name", "last_name") - .create() - } - - func revert(on database: any Database) async throws { - try await database.schema(Employee.schema).delete() - } - - } - - struct Update: Content { - var firstName: String? - var lastName: String? - var active: Bool? - } -} - -// MARK: - Validations - -extension Employee.Create: Validatable { - static func validations(_ validations: inout Validations) { - validations.add("firstName", as: String.self, is: !.empty) - validations.add("lastName", as: String.self, is: !.empty) - } -} - -extension Employee.Update: Validatable { - static func validations(_ validations: inout Validations) { - validations.add("firstName", as: String?.self, is: .nil || !.empty, required: false) - validations.add("lastName", as: String?.self, is: .nil || !.empty, required: false) - } -} +// import Fluent +// import struct Foundation.UUID +// import Vapor +// +// // TODO: Add soft-delete?? +// +// /// The employee database model. +// /// +// /// An employee is someone that PO's can be generated for. They can be either a field +// /// employee / technician, an office employee, or an administrator. +// /// +// /// # NOTE: Only `User` types can login and generate po's for employees. +// /// +// final class Employee: Model, @unchecked Sendable { +// +// static let schema = "employee" +// +// @ID(key: .id) +// var id: UUID? +// +// @Field(key: "first_name") +// var firstName: String +// +// @Field(key: "last_name") +// var lastName: String +// +// @Field(key: "is_active") +// var active: Bool +// +// @Timestamp(key: "created_at", on: .create) +// var createdAt: Date? +// +// @Timestamp(key: "updated_at", on: .update) +// var updatedAt: Date? +// +// init() {} +// +// init( +// id: UUID? = nil, +// firstName: String, +// lastName: String, +// active: Bool, +// createdAt: Date? = nil, +// updatedAt: Date? = nil +// ) { +// self.id = id +// self.firstName = firstName +// self.lastName = lastName +// self.active = active +// self.createdAt = createdAt +// self.updatedAt = updatedAt +// } +// +// func toDTO() -> DTO { +// .init( +// id: id, +// firstName: $firstName.value, +// lastName: $lastName.value, +// active: $active.value, +// createdAt: createdAt, +// updatedAt: updatedAt +// ) +// } +// +// func applyUpdates(_ updates: Update) { +// if let firstName = updates.firstName { +// self.firstName = firstName +// } +// if let lastName = updates.lastName { +// self.lastName = lastName +// } +// if let active = updates.active { +// self.active = active +// } +// } +// } +// +// // MARK: - Helpers +// +// extension Employee { +// +// struct Create: Content { +// let firstName: String +// let lastName: String +// let active: Bool? +// +// func toModel() -> Employee { +// .init( +// firstName: firstName, +// lastName: lastName, +// active: active ?? true +// ) +// } +// } +// +// struct DTO: Content { +// +// var id: UUID? +// var firstName: String? +// var lastName: String? +// var active: Bool? +// var createdAt: Date? +// var updatedAt: Date? +// +// func toModel() -> Employee { +// let model = Employee() +// +// model.id = id +// if let firstName { +// model.firstName = firstName +// } +// if let lastName { +// model.lastName = lastName +// } +// if let active { +// model.active = active +// } +// return model +// } +// } +// +// struct Migrate: AsyncMigration { +// +// let name = "CreateEmployee" +// +// func prepare(on database: any Database) async throws { +// try await database.schema(Employee.schema) +// .id() +// .field("first_name", .string, .required) +// .field("last_name", .string, .required) +// .field("is_active", .bool, .required, .sql(.default(true))) +// .field("created_at", .datetime) +// .field("updated_at", .datetime) +// .unique(on: "first_name", "last_name") +// .create() +// } +// +// func revert(on database: any Database) async throws { +// try await database.schema(Employee.schema).delete() +// } +// +// } +// +// struct Update: Content { +// var firstName: String? +// var lastName: String? +// var active: Bool? +// } +// } +// +// // MARK: - Validations +// +// extension Employee.Create: Validatable { +// static func validations(_ validations: inout Validations) { +// validations.add("firstName", as: String.self, is: !.empty) +// validations.add("lastName", as: String.self, is: !.empty) +// } +// } +// +// extension Employee.Update: Validatable { +// static func validations(_ validations: inout Validations) { +// validations.add("firstName", as: String?.self, is: .nil || !.empty, required: false) +// validations.add("lastName", as: String?.self, is: .nil || !.empty, required: false) +// } +// } diff --git a/Sources/App/Models/PurchaseOrder.swift b/Sources/App/Models/PurchaseOrder.swift index 2a1462d..f66146c 100644 --- a/Sources/App/Models/PurchaseOrder.swift +++ b/Sources/App/Models/PurchaseOrder.swift @@ -1,156 +1,156 @@ -import Fluent -import Vapor - -/// The purchase order database model. -/// -/// # NOTE: An initial purchase order should be created with an `id` higher than our current PO -/// so that subsequent PO's are generated with higher values than our current system produces. -/// once the first one is set, the rest will auto-increment from there. -final class PurchaseOrder: Model, Content, @unchecked Sendable { - static let schema = "purchase_order" - - @ID(custom: "id", generatedBy: .database) - var id: Int? - - @Field(key: "work_order") - var workOrder: Int? - - @Field(key: "materials") - var materials: String - - @Field(key: "customer") - var customer: String - - @Field(key: "truck_stock") - var truckStock: Bool - - @Parent(key: "created_by_id") - var createdBy: User - - @Parent(key: "created_for_id") - var createdFor: Employee - - @Parent(key: "vendor_branch_id") - var vendorBranch: VendorBranch - - @Timestamp(key: "created_at", on: .create) - var createdAt: Date? - - @Timestamp(key: "updated_at", on: .update) - var updatedAt: Date? - - init() {} - - init( - id: Int? = nil, - workOrder: Int? = nil, - materials: String, - customer: String, - truckStock: Bool, - createdByID: User.IDValue, - createdForID: Employee.IDValue, - vendorBranchID: VendorBranch.IDValue, - createdAt: Date? = nil, - updatedAt: Date? = nil - ) { - self.id = id - self.workOrder = workOrder - self.materials = materials - self.customer = customer - self.truckStock = truckStock - $createdBy.id = createdByID - $createdFor.id = createdForID - $vendorBranch.id = vendorBranchID - self.createdAt = createdAt - self.updatedAt = updatedAt - } - - func toDTO() -> DTO { - .init( - id: id, - workOrder: workOrder, - materials: materials, - customer: customer, - truckStock: truckStock, - createdBy: $createdBy.value?.toDTO(), - createdFor: $createdFor.value?.toDTO(), - vendorBranch: $vendorBranch.value, - createdAt: createdAt, - updatedAt: updatedAt - ) - } - -} - -extension PurchaseOrder { - - struct Create: Content { - let id: Int? - let workOrder: Int? - let materials: String - let customer: String - let truckStock: Bool? - let createdForID: Employee.IDValue - let vendorBranchID: VendorBranch.IDValue - - func toModel(createdByID: User.IDValue) -> PurchaseOrder { - .init( - id: id, - workOrder: workOrder, - materials: materials, - customer: customer, - truckStock: truckStock ?? false, - createdByID: createdByID, - createdForID: createdForID, - vendorBranchID: vendorBranchID, - createdAt: nil, - updatedAt: nil - ) - } - } - - struct DTO: Content { - let id: Int? - let workOrder: Int? - let materials: String - let customer: String - let truckStock: Bool - let createdBy: User.DTO? - let createdFor: Employee.DTO? - let vendorBranch: VendorBranch? - let createdAt: Date? - let updatedAt: Date? - } - - struct Migrate: AsyncMigration { - - let name = "CreatePurchaseOrder" - - func prepare(on database: any Database) async throws { - try await database.schema(PurchaseOrder.schema) - .field("id", .int, .identifier(auto: true)) - .field("work_order", .int) - .field("customer", .string, .required) - .field("materials", .string, .required) - .field("truck_stock", .bool, .required) - .field("created_by_id", .uuid, .required, .references(User.schema, "id")) - .field("created_for_id", .uuid, .required, .references(Employee.schema, "id")) - .field("vendor_branch_id", .uuid, .required, .references(VendorBranch.schema, "id")) - .field("created_at", .datetime) - .field("updated_at", .datetime) - .create() - } - - func revert(on database: any Database) async throws { - try await database.schema(PurchaseOrder.schema).delete() - } - } -} - -extension PurchaseOrder.Create: Validatable { - - static func validations(_ validations: inout Validations) { - validations.add("materials", as: String.self, is: !.empty) - validations.add("customer", as: String.self, is: !.empty) - } -} +// import Fluent +// import Vapor +// +// /// The purchase order database model. +// /// +// /// # NOTE: An initial purchase order should be created with an `id` higher than our current PO +// /// so that subsequent PO's are generated with higher values than our current system produces. +// /// once the first one is set, the rest will auto-increment from there. +// final class PurchaseOrder: Model, Content, @unchecked Sendable { +// static let schema = "purchase_order" +// +// @ID(custom: "id", generatedBy: .database) +// var id: Int? +// +// @Field(key: "work_order") +// var workOrder: Int? +// +// @Field(key: "materials") +// var materials: String +// +// @Field(key: "customer") +// var customer: String +// +// @Field(key: "truck_stock") +// var truckStock: Bool +// +// @Parent(key: "created_by_id") +// var createdBy: User +// +// @Parent(key: "created_for_id") +// var createdFor: Employee +// +// @Parent(key: "vendor_branch_id") +// var vendorBranch: VendorBranch +// +// @Timestamp(key: "created_at", on: .create) +// var createdAt: Date? +// +// @Timestamp(key: "updated_at", on: .update) +// var updatedAt: Date? +// +// init() {} +// +// init( +// id: Int? = nil, +// workOrder: Int? = nil, +// materials: String, +// customer: String, +// truckStock: Bool, +// createdByID: User.IDValue, +// createdForID: Employee.IDValue, +// vendorBranchID: VendorBranch.IDValue, +// createdAt: Date? = nil, +// updatedAt: Date? = nil +// ) { +// self.id = id +// self.workOrder = workOrder +// self.materials = materials +// self.customer = customer +// self.truckStock = truckStock +// $createdBy.id = createdByID +// $createdFor.id = createdForID +// $vendorBranch.id = vendorBranchID +// self.createdAt = createdAt +// self.updatedAt = updatedAt +// } +// +// func toDTO() -> DTO { +// .init( +// id: id, +// workOrder: workOrder, +// materials: materials, +// customer: customer, +// truckStock: truckStock, +// createdBy: $createdBy.value?.toDTO(), +// createdFor: $createdFor.value?.toDTO(), +// vendorBranch: $vendorBranch.value, +// createdAt: createdAt, +// updatedAt: updatedAt +// ) +// } +// +// } +// +// extension PurchaseOrder { +// +// struct Create: Content { +// let id: Int? +// let workOrder: Int? +// let materials: String +// let customer: String +// let truckStock: Bool? +// let createdForID: Employee.IDValue +// let vendorBranchID: VendorBranch.IDValue +// +// func toModel(createdByID: User.IDValue) -> PurchaseOrder { +// .init( +// id: id, +// workOrder: workOrder, +// materials: materials, +// customer: customer, +// truckStock: truckStock ?? false, +// createdByID: createdByID, +// createdForID: createdForID, +// vendorBranchID: vendorBranchID, +// createdAt: nil, +// updatedAt: nil +// ) +// } +// } +// +// struct DTO: Content { +// let id: Int? +// let workOrder: Int? +// let materials: String +// let customer: String +// let truckStock: Bool +// let createdBy: User.DTO? +// let createdFor: Employee.DTO? +// let vendorBranch: VendorBranch? +// let createdAt: Date? +// let updatedAt: Date? +// } +// +// struct Migrate: AsyncMigration { +// +// let name = "CreatePurchaseOrder" +// +// func prepare(on database: any Database) async throws { +// try await database.schema(PurchaseOrder.schema) +// .field("id", .int, .identifier(auto: true)) +// .field("work_order", .int) +// .field("customer", .string, .required) +// .field("materials", .string, .required) +// .field("truck_stock", .bool, .required) +// .field("created_by_id", .uuid, .required, .references(User.schema, "id")) +// .field("created_for_id", .uuid, .required, .references(Employee.schema, "id")) +// .field("vendor_branch_id", .uuid, .required, .references(VendorBranch.schema, "id")) +// .field("created_at", .datetime) +// .field("updated_at", .datetime) +// .create() +// } +// +// func revert(on database: any Database) async throws { +// try await database.schema(PurchaseOrder.schema).delete() +// } +// } +// } +// +// extension PurchaseOrder.Create: Validatable { +// +// static func validations(_ validations: inout Validations) { +// validations.add("materials", as: String.self, is: !.empty) +// validations.add("customer", as: String.self, is: !.empty) +// } +// } diff --git a/Sources/App/Models/User.swift b/Sources/App/Models/User.swift index b2a9508..874d234 100644 --- a/Sources/App/Models/User.swift +++ b/Sources/App/Models/User.swift @@ -1,121 +1,121 @@ -import Fluent -import Vapor - -/// The user database model. -/// -/// A user is someone who is able to login and generate PO's for employees. Generally a user should also -/// have an employee profile, but not all employees are users. Users are generally restricted to office workers -/// and administrators. -/// -/// -final class User: Model, @unchecked Sendable { - static let schema = "user" - - @ID(key: .id) - var id: UUID? - - @Field(key: "username") - var username: String - - @Field(key: "email") - var email: String - - @Field(key: "password_hash") - var passwordHash: String - - @Timestamp(key: "created_at", on: .create) - var createdAt: Date? - - @Timestamp(key: "updated_at", on: .update) - var updatedAt: Date? - - init() {} - - init( - id: UUID? = nil, - username: String, - email: String, - passwordHash: String - ) { - self.id = id - self.username = username - self.email = email - self.passwordHash = passwordHash - } - - func toDTO() -> DTO { - .init( - id: id, - username: $username.value, - email: $email.value, - createdAt: createdAt, - updatedAt: updatedAt - ) - } - - func generateToken() throws -> UserToken { - try .init( - value: [UInt8].random(count: 16).base64, - userID: requireID() - ) - } - -} - -extension User { - - struct Create: Content { - var username: String - var email: String - var password: String - var confirmPassword: String - } - - struct DTO: Content { - let id: UUID? - let username: String? - let email: String? - let createdAt: Date? - let updatedAt: Date? - } - - struct Migrate: AsyncMigration { - let name = "CreateUser" - - func prepare(on database: any Database) async throws { - try await database.schema(User.schema) - .id() - .field("username", .string, .required) - .field("email", .string, .required) - .field("password_hash", .string, .required) - .field("created_at", .datetime) - .field("updated_at", .datetime) - .unique(on: "email", "username") - .create() - } - - func revert(on database: any Database) async throws { - try await database.schema(User.schema).delete() - } - } -} - -extension User: ModelAuthenticatable { - static let usernameKey = \User.$username - static let passwordHashKey = \User.$passwordHash - - func verify(password: String) throws -> Bool { - try Bcrypt.verify(password, created: passwordHash) - } -} - -extension User: ModelSessionAuthenticatable {} -extension User: ModelCredentialsAuthenticatable {} - -extension User.Create: Validatable { - static func validations(_ validations: inout Validations) { - validations.add("username", as: String.self, is: !.empty) - validations.add("email", as: String.self, is: .email) - validations.add("password", as: String.self, is: .count(8...)) - } -} +// import Fluent +// import Vapor +// +// /// The user database model. +// /// +// /// A user is someone who is able to login and generate PO's for employees. Generally a user should also +// /// have an employee profile, but not all employees are users. Users are generally restricted to office workers +// /// and administrators. +// /// +// /// +// final class User: Model, @unchecked Sendable { +// static let schema = "user" +// +// @ID(key: .id) +// var id: UUID? +// +// @Field(key: "username") +// var username: String +// +// @Field(key: "email") +// var email: String +// +// @Field(key: "password_hash") +// var passwordHash: String +// +// @Timestamp(key: "created_at", on: .create) +// var createdAt: Date? +// +// @Timestamp(key: "updated_at", on: .update) +// var updatedAt: Date? +// +// init() {} +// +// init( +// id: UUID? = nil, +// username: String, +// email: String, +// passwordHash: String +// ) { +// self.id = id +// self.username = username +// self.email = email +// self.passwordHash = passwordHash +// } +// +// func toDTO() -> DTO { +// .init( +// id: id, +// username: $username.value, +// email: $email.value, +// createdAt: createdAt, +// updatedAt: updatedAt +// ) +// } +// +// func generateToken() throws -> UserToken { +// try .init( +// value: [UInt8].random(count: 16).base64, +// userID: requireID() +// ) +// } +// +// } +// +// extension User { +// +// struct Create: Content { +// var username: String +// var email: String +// var password: String +// var confirmPassword: String +// } +// +// struct DTO: Content { +// let id: UUID? +// let username: String? +// let email: String? +// let createdAt: Date? +// let updatedAt: Date? +// } +// +// struct Migrate: AsyncMigration { +// let name = "CreateUser" +// +// func prepare(on database: any Database) async throws { +// try await database.schema(User.schema) +// .id() +// .field("username", .string, .required) +// .field("email", .string, .required) +// .field("password_hash", .string, .required) +// .field("created_at", .datetime) +// .field("updated_at", .datetime) +// .unique(on: "email", "username") +// .create() +// } +// +// func revert(on database: any Database) async throws { +// try await database.schema(User.schema).delete() +// } +// } +// } +// +// extension User: ModelAuthenticatable { +// static let usernameKey = \User.$username +// static let passwordHashKey = \User.$passwordHash +// +// func verify(password: String) throws -> Bool { +// try Bcrypt.verify(password, created: passwordHash) +// } +// } +// +// extension User: ModelSessionAuthenticatable {} +// extension User: ModelCredentialsAuthenticatable {} +// +// extension User.Create: Validatable { +// static func validations(_ validations: inout Validations) { +// validations.add("username", as: String.self, is: !.empty) +// validations.add("email", as: String.self, is: .email) +// validations.add("password", as: String.self, is: .count(8...)) +// } +// } diff --git a/Sources/App/Models/UserToken.swift b/Sources/App/Models/UserToken.swift index 5429044..5db2f47 100644 --- a/Sources/App/Models/UserToken.swift +++ b/Sources/App/Models/UserToken.swift @@ -1,51 +1,51 @@ -import Fluent -import Vapor - -final class UserToken: Model, Content, @unchecked Sendable { - - static let schema = "user_token" - - @ID(key: .id) - var id: UUID? - - @Field(key: "value") - var value: String - - @Parent(key: "user_id") - var user: User - - init() {} - - init(id: UUID? = nil, value: String, userID: User.IDValue) { - self.id = id - self.value = value - $user.id = userID - } -} - -extension UserToken { - - struct Migrate: AsyncMigration { - let name = "CreateUserToken" - - func prepare(on database: any Database) async throws { - try await database.schema(UserToken.schema) - .id() - .field("value", .string, .required) - .field("user_id", .uuid, .required, .references(User.schema, "id")) - .unique(on: "value") - .create() - } - - func revert(on database: any Database) async throws { - try await database.schema(UserToken.schema).delete() - } - } -} - -extension UserToken: ModelTokenAuthenticatable { - static let valueKey = \UserToken.$value - static let userKey = \UserToken.$user - - var isValid: Bool { true } -} +// import Fluent +// import Vapor +// +// final class UserToken: Model, Content, @unchecked Sendable { +// +// static let schema = "user_token" +// +// @ID(key: .id) +// var id: UUID? +// +// @Field(key: "value") +// var value: String +// +// @Parent(key: "user_id") +// var user: User +// +// init() {} +// +// init(id: UUID? = nil, value: String, userID: User.IDValue) { +// self.id = id +// self.value = value +// $user.id = userID +// } +// } +// +// extension UserToken { +// +// struct Migrate: AsyncMigration { +// let name = "CreateUserToken" +// +// func prepare(on database: any Database) async throws { +// try await database.schema(UserToken.schema) +// .id() +// .field("value", .string, .required) +// .field("user_id", .uuid, .required, .references(User.schema, "id")) +// .unique(on: "value") +// .create() +// } +// +// func revert(on database: any Database) async throws { +// try await database.schema(UserToken.schema).delete() +// } +// } +// } +// +// extension UserToken: ModelTokenAuthenticatable { +// static let valueKey = \UserToken.$value +// static let userKey = \UserToken.$user +// +// var isValid: Bool { true } +// } diff --git a/Sources/App/Models/Vendor.swift b/Sources/App/Models/Vendor.swift index 5ce69e9..5e0a5bc 100644 --- a/Sources/App/Models/Vendor.swift +++ b/Sources/App/Models/Vendor.swift @@ -1,113 +1,113 @@ -import Fluent -import struct Foundation.UUID -import Vapor - -// The primary database model. -final class Vendor: Model, @unchecked Sendable { - - static let schema = "vendor" - - @ID(key: .id) - var id: UUID? - - @Field(key: "name") - var name: String - - @Timestamp(key: "created_at", on: .create) - var createdAt: Date? - - @Timestamp(key: "updated_at", on: .update) - var updatedAt: Date? - - @Children(for: \.$vendor) - var branches: [VendorBranch] - - init() {} - - init(id: UUID? = nil, name: String) { - self.id = id - self.name = name - } - - func toDTO(includeBranches: Bool? = nil) -> DTO { - .init( - id: id, - name: $name.value, - branches: ($branches.value != nil && $branches.value!.count > 0) - ? $branches.value!.map { $0.toDTO() } - : (includeBranches == true) ? [] : nil, - createdAt: createdAt, - updatedAt: updatedAt - ) - } - - func applyUpdates(_ updates: Update) { - name = updates.name - } -} - -// MARK: - Helpers. - -extension Vendor { - struct Create: Content { - var name: String - - func toModel() -> Vendor { - .init(name: name) - } - } - - struct DTO: Content { - - var id: UUID? - var name: String? - var branches: [VendorBranch.DTO]? - let createdAt: Date? - let updatedAt: Date? - - func toModel() -> Vendor { - let model = Vendor() - model.id = id - if let name { - model.name = name - } - return model - } - } - - struct Migrate: AsyncMigration { - let name = "CreateVendor" - - func prepare(on database: any Database) async throws { - try await database.schema(Vendor.schema) - .id() - .field("name", .string, .required) - .field("created_at", .datetime) - .field("updated_at", .datetime) - .unique(on: "name") - .create() - } - - func revert(on database: any Database) async throws { - try await database.schema(Vendor.schema).delete() - } - } - - struct Update: Content { - var name: String - } -} - -// MARK: - Validations - -extension Vendor.Create: Validatable { - static func validations(_ validations: inout Validations) { - validations.add("name", as: String.self, is: !.empty) - } -} - -extension Vendor.Update: Validatable { - static func validations(_ validations: inout Validations) { - validations.add("name", as: String.self, is: !.empty) - } -} +// import Fluent +// import struct Foundation.UUID +// import Vapor +// +// // The primary database model. +// final class Vendor: Model, @unchecked Sendable { +// +// static let schema = "vendor" +// +// @ID(key: .id) +// var id: UUID? +// +// @Field(key: "name") +// var name: String +// +// @Timestamp(key: "created_at", on: .create) +// var createdAt: Date? +// +// @Timestamp(key: "updated_at", on: .update) +// var updatedAt: Date? +// +// @Children(for: \.$vendor) +// var branches: [VendorBranch] +// +// init() {} +// +// init(id: UUID? = nil, name: String) { +// self.id = id +// self.name = name +// } +// +// func toDTO(includeBranches: Bool? = nil) -> DTO { +// .init( +// id: id, +// name: $name.value, +// branches: ($branches.value != nil && $branches.value!.count > 0) +// ? $branches.value!.map { $0.toDTO() } +// : (includeBranches == true) ? [] : nil, +// createdAt: createdAt, +// updatedAt: updatedAt +// ) +// } +// +// func applyUpdates(_ updates: Update) { +// name = updates.name +// } +// } +// +// // MARK: - Helpers. +// +// extension Vendor { +// struct Create: Content { +// var name: String +// +// func toModel() -> Vendor { +// .init(name: name) +// } +// } +// +// struct DTO: Content { +// +// var id: UUID? +// var name: String? +// var branches: [VendorBranch.DTO]? +// let createdAt: Date? +// let updatedAt: Date? +// +// func toModel() -> Vendor { +// let model = Vendor() +// model.id = id +// if let name { +// model.name = name +// } +// return model +// } +// } +// +// struct Migrate: AsyncMigration { +// let name = "CreateVendor" +// +// func prepare(on database: any Database) async throws { +// try await database.schema(Vendor.schema) +// .id() +// .field("name", .string, .required) +// .field("created_at", .datetime) +// .field("updated_at", .datetime) +// .unique(on: "name") +// .create() +// } +// +// func revert(on database: any Database) async throws { +// try await database.schema(Vendor.schema).delete() +// } +// } +// +// struct Update: Content { +// var name: String +// } +// } +// +// // MARK: - Validations +// +// extension Vendor.Create: Validatable { +// static func validations(_ validations: inout Validations) { +// validations.add("name", as: String.self, is: !.empty) +// } +// } +// +// extension Vendor.Update: Validatable { +// static func validations(_ validations: inout Validations) { +// validations.add("name", as: String.self, is: !.empty) +// } +// } diff --git a/Sources/App/Models/VendorBranch.swift b/Sources/App/Models/VendorBranch.swift index dd36eee..f7d7448 100644 --- a/Sources/App/Models/VendorBranch.swift +++ b/Sources/App/Models/VendorBranch.swift @@ -1,119 +1,119 @@ -import Fluent -import struct Foundation.UUID -import Vapor - -final class VendorBranch: Model, @unchecked Sendable { - - static let schema = "vendor_branch" - - @ID(key: .id) - var id: UUID? - - @Field(key: "name") - var name: String - - @Timestamp(key: "created_at", on: .create) - var createdAt: Date? - - @Timestamp(key: "updated_at", on: .update) - var updatedAt: Date? - - @Parent(key: "vendor_id") - var vendor: Vendor - - init() {} - - init(id: UUID? = nil, name: String, vendorId: Vendor.IDValue) { - self.id = id - self.name = name - $vendor.id = vendorId - } - - func toDTO() -> DTO { - .init( - id: id, - name: $name.value, - vendorId: $vendor.id, - createdAt: createdAt, - updatedAt: updatedAt - ) - } - - func applyUpdates(_ updates: Update) { - name = updates.name - } - -} - -// MARK: - Helpers - -extension VendorBranch { - struct Create: Content { - var name: String - - func toModel() -> VendorBranch { - let model = VendorBranch() - model.name = name - return model - } - } - - struct DTO: Content { - var id: UUID? - var name: String? - var vendorId: Vendor.IDValue? - let createdAt: Date? - let updatedAt: Date? - - func toModel() -> VendorBranch { - let model = VendorBranch() - - model.id = id - if let name { - model.name = name - } - if let vendorId { - model.$vendor.id = vendorId - } - return model - } - - } - - struct Migrate: AsyncMigration { - let name = "CreateVendorBranch" - - func prepare(on database: any Database) async throws { - try await database.schema(VendorBranch.schema) - .id() - .field("name", .string, .required) - .field("vendor_id", .uuid, .required) - .field("created_at", .datetime) - .field("updated_at", .datetime) - .foreignKey("vendor_id", references: Vendor.schema, "id", onDelete: .cascade) - .create() - } - - func revert(on database: any Database) async throws { - try await database.schema(VendorBranch.schema).delete() - } - } - - struct Update: Content { - var name: String - } -} - -// MARK: - Validations - -extension VendorBranch.Create: Validatable { - static func validations(_ validations: inout Validations) { - validations.add("name", as: String.self, is: !.empty) - } -} - -extension VendorBranch.Update: Validatable { - static func validations(_ validations: inout Validations) { - validations.add("name", as: String.self, is: !.empty) - } -} +// import Fluent +// import struct Foundation.UUID +// import Vapor +// +// final class VendorBranch: Model, @unchecked Sendable { +// +// static let schema = "vendor_branch" +// +// @ID(key: .id) +// var id: UUID? +// +// @Field(key: "name") +// var name: String +// +// @Timestamp(key: "created_at", on: .create) +// var createdAt: Date? +// +// @Timestamp(key: "updated_at", on: .update) +// var updatedAt: Date? +// +// @Parent(key: "vendor_id") +// var vendor: Vendor +// +// init() {} +// +// init(id: UUID? = nil, name: String, vendorId: Vendor.IDValue) { +// self.id = id +// self.name = name +// $vendor.id = vendorId +// } +// +// func toDTO() -> DTO { +// .init( +// id: id, +// name: $name.value, +// vendorId: $vendor.id, +// createdAt: createdAt, +// updatedAt: updatedAt +// ) +// } +// +// func applyUpdates(_ updates: Update) { +// name = updates.name +// } +// +// } +// +// // MARK: - Helpers +// +// extension VendorBranch { +// struct Create: Content { +// var name: String +// +// func toModel() -> VendorBranch { +// let model = VendorBranch() +// model.name = name +// return model +// } +// } +// +// struct DTO: Content { +// var id: UUID? +// var name: String? +// var vendorId: Vendor.IDValue? +// let createdAt: Date? +// let updatedAt: Date? +// +// func toModel() -> VendorBranch { +// let model = VendorBranch() +// +// model.id = id +// if let name { +// model.name = name +// } +// if let vendorId { +// model.$vendor.id = vendorId +// } +// return model +// } +// +// } +// +// struct Migrate: AsyncMigration { +// let name = "CreateVendorBranch" +// +// func prepare(on database: any Database) async throws { +// try await database.schema(VendorBranch.schema) +// .id() +// .field("name", .string, .required) +// .field("vendor_id", .uuid, .required) +// .field("created_at", .datetime) +// .field("updated_at", .datetime) +// .foreignKey("vendor_id", references: Vendor.schema, "id", onDelete: .cascade) +// .create() +// } +// +// func revert(on database: any Database) async throws { +// try await database.schema(VendorBranch.schema).delete() +// } +// } +// +// struct Update: Content { +// var name: String +// } +// } +// +// // MARK: - Validations +// +// extension VendorBranch.Create: Validatable { +// static func validations(_ validations: inout Validations) { +// validations.add("name", as: String.self, is: !.empty) +// } +// } +// +// extension VendorBranch.Update: Validatable { +// static func validations(_ validations: inout Validations) { +// validations.add("name", as: String.self, is: !.empty) +// } +// } diff --git a/Sources/App/configure.swift b/Sources/App/configure.swift index 4788530..7215301 100644 --- a/Sources/App/configure.swift +++ b/Sources/App/configure.swift @@ -1,8 +1,10 @@ +import DatabaseClientLive import Dependencies import Fluent import FluentSQLiteDriver import Leaf import NIOSSL +import SharedModels import Vapor // configures your application @@ -10,7 +12,7 @@ public func configure(_ app: Application) async throws { // uncomment to serve files from /Public folder app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory)) app.middleware.use(app.sessions.middleware) - app.middleware.use(User.sessionAuthenticator()) + // app.middleware.use(User.sessionAuthenticator()) #if DEBUG app.lifecycle.use(BrowserSyncHandler()) @@ -23,21 +25,25 @@ public func configure(_ app: Application) async throws { app.databases.use(DatabaseConfigurationFactory.sqlite(.memory), as: .sqlite) } - app.migrations.add(Vendor.Migrate()) - app.migrations.add(VendorBranch.Migrate()) - app.migrations.add(Employee.Migrate()) - app.migrations.add(User.Migrate()) - app.migrations.add(UserToken.Migrate()) - app.migrations.add(PurchaseOrder.Migrate()) + let databaseClient = DatabaseClient.live(database: app.db) + try await app.migrations.add(databaseClient.migrations()) + + // app.migrations.add(Vendor.Migrate()) + // app.migrations.add(VendorBranch.Migrate()) + // app.migrations.add(Employee.Migrate()) + // app.migrations.add(User.Migrate()) + // app.migrations.add(UserToken.Migrate()) + // app.migrations.add(PurchaseOrder.Migrate()) app.views.use(.leaf) try withDependencies { - $0.employees = .live(database: app.db(.sqlite)) - $0.purchaseOrders = .live(database: app.db(.sqlite)) - $0.users = .live(database: app.db(.sqlite)) - $0.vendorBranches = .live(database: app.db(.sqlite)) - $0.vendors = .live(database: app.db(.sqlite)) + $0.database = databaseClient + // $0.employees = .live(database: app.db(.sqlite)) + // $0.purchaseOrders = .live(database: app.db(.sqlite)) + // $0.users = .live(database: app.db(.sqlite)) + // $0.vendorBranches = .live(database: app.db(.sqlite)) + // $0.vendors = .live(database: app.db(.sqlite)) } operation: { // register routes try routes(app) diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift index 4d2e87f..55f9179 100644 --- a/Sources/App/routes.swift +++ b/Sources/App/routes.swift @@ -3,5 +3,5 @@ import Vapor func routes(_ app: Application) throws { try app.register(collection: ApiController()) - try app.register(collection: ViewController()) + // try app.register(collection: ViewController()) } diff --git a/Sources/DatabaseClient/Employees.swift b/Sources/DatabaseClient/Employees.swift index 32fb10b..14744ba 100644 --- a/Sources/DatabaseClient/Employees.swift +++ b/Sources/DatabaseClient/Employees.swift @@ -27,37 +27,3 @@ public extension DatabaseClient { extension DatabaseClient.Employees: TestDependencyKey { public static let testValue = Self() } - -public extension Employee { - struct Create: Codable, Sendable { - public let firstName: String - public let lastName: String - public let active: Bool? - - public init( - firstName: String, - lastName: String, - active: Bool? = nil - ) { - self.firstName = firstName - self.lastName = lastName - self.active = active - } - } - - struct Update: Codable, Sendable { - public let firstName: String? - public let lastName: String? - public let active: Bool? - - public init( - firstName: String? = nil, - lastName: String? = nil, - active: Bool? = nil - ) { - self.firstName = firstName - self.lastName = lastName - self.active = active - } - } -} diff --git a/Sources/DatabaseClient/PurchaseOrders.swift b/Sources/DatabaseClient/PurchaseOrders.swift index 84e33fb..c31e65a 100644 --- a/Sources/DatabaseClient/PurchaseOrders.swift +++ b/Sources/DatabaseClient/PurchaseOrders.swift @@ -18,35 +18,3 @@ public extension DatabaseClient { extension DatabaseClient.PurchaseOrders: TestDependencyKey { public static let testValue: DatabaseClient.PurchaseOrders = Self() } - -public extension PurchaseOrder { - struct Create: Codable, Sendable { - - public let id: Int? - public let workOrder: Int? - public let materials: String - public let customer: String - public let truckStock: Bool? - public let createdForID: Employee.ID - public let vendorBranchID: VendorBranch.ID - - public init( - id: Int? = nil, - workOrder: Int? = nil, - materials: String, - customer: String, - truckStock: Bool? = nil, - createdForID: Employee.ID, - vendorBranchID: VendorBranch.ID - ) { - self.id = id - self.workOrder = workOrder - self.materials = materials - self.customer = customer - self.truckStock = truckStock - self.createdForID = createdForID - self.vendorBranchID = vendorBranchID - } - } - -} diff --git a/Sources/DatabaseClient/Users.swift b/Sources/DatabaseClient/Users.swift index a3e7e5c..f62951f 100644 --- a/Sources/DatabaseClient/Users.swift +++ b/Sources/DatabaseClient/Users.swift @@ -2,11 +2,13 @@ import Dependencies import DependenciesMacros import Foundation import SharedModels +import Vapor public extension DatabaseClient { @DependencyClient struct Users: Sendable { + public var count: @Sendable () async throws -> Int public var create: @Sendable (User.Create) async throws -> User public var delete: @Sendable (User.ID) async throws -> Void public var fetchAll: @Sendable () async throws -> [User] @@ -16,61 +18,12 @@ public extension DatabaseClient { } } +public extension DatabaseClient.Users { + enum AuthRequest { + case basic(BasicAuthorization) + } +} + extension DatabaseClient.Users: TestDependencyKey { public static let testValue: DatabaseClient.Users = Self() } - -public extension User { - - struct Create: Codable, Sendable { - public let username: String - public let email: String - public let password: String - public let confirmPassword: String - - public init( - username: String, - email: String, - password: String, - confirmPassword: String - ) { - self.username = username - self.email = email - self.password = password - self.confirmPassword = confirmPassword - } - } - - struct Login: Codable, Sendable { - public let username: String? - public let email: String? - public let password: String - - public init( - username: String?, - email: String? = nil, - password: String - ) { - self.username = username - self.email = email - self.password = password - } - } - - struct Token: Codable, Equatable, Identifiable, Sendable { - public let id: UUID - public let userID: User.ID - public let value: String - - public init( - id: UUID, - userID: User.ID, - value: String - ) { - self.id = id - self.userID = userID - self.value = value - } - } - -} diff --git a/Sources/DatabaseClient/VendorBranches.swift b/Sources/DatabaseClient/VendorBranches.swift index 8c4a062..150a85f 100644 --- a/Sources/DatabaseClient/VendorBranches.swift +++ b/Sources/DatabaseClient/VendorBranches.swift @@ -26,23 +26,3 @@ public extension DatabaseClient { extension DatabaseClient.VendorBranches: TestDependencyKey { public static let testValue: DatabaseClient.VendorBranches = Self() } - -public extension VendorBranch { - struct Create: Codable, Sendable { - public let name: String - public let vendorID: Vendor.ID - - public init(name: String, vendorID: Vendor.ID) { - self.name = name - self.vendorID = vendorID - } - } - - struct Update: Codable, Sendable { - public let name: String? - - public init(name: String?) { - self.name = name - } - } -} diff --git a/Sources/DatabaseClient/Vendors.swift b/Sources/DatabaseClient/Vendors.swift index 2d60a84..a354e45 100644 --- a/Sources/DatabaseClient/Vendors.swift +++ b/Sources/DatabaseClient/Vendors.swift @@ -34,22 +34,3 @@ public extension DatabaseClient { extension DatabaseClient.Vendors: TestDependencyKey { public static let testValue: DatabaseClient.Vendors = Self() } - -public extension Vendor { - - struct Create: Codable, Sendable { - public let name: String - - public init(name: String) { - self.name = name - } - } - - struct Update: Codable, Sendable { - public let name: String? - - public init(name: String?) { - self.name = name - } - } -} diff --git a/Sources/DatabaseClientLive/Live.swift b/Sources/DatabaseClientLive/Live.swift index 360aec0..8cee29d 100644 --- a/Sources/DatabaseClientLive/Live.swift +++ b/Sources/DatabaseClientLive/Live.swift @@ -1,4 +1,4 @@ -import DatabaseClient +@_exported import DatabaseClient import FluentKit import SharedModels diff --git a/Sources/DatabaseClientLive/Users.swift b/Sources/DatabaseClientLive/Users.swift index 044fe42..d7063ca 100644 --- a/Sources/DatabaseClientLive/Users.swift +++ b/Sources/DatabaseClientLive/Users.swift @@ -1,13 +1,15 @@ import DatabaseClient import FluentKit import Foundation -import HummingbirdBcrypt import SharedModels +import Vapor public extension DatabaseClient.Users { static func live(database: any Database) -> Self { - .init { create in + .init { + try await UserModel.query(on: database).count() + } create: { create in let model = try create.toModel() try await model.save(on: database) return try model.toDTO() @@ -98,7 +100,7 @@ extension User.Create { func toModel() throws -> UserModel { try validate() - return .init(username: username, email: email, passwordHash: Bcrypt.hash(password, cost: 12)) + return try .init(username: username, email: email, passwordHash: Bcrypt.hash(password, cost: 12)) } func validate() throws { @@ -209,3 +211,62 @@ final class UserTokenModel: Model, Codable, @unchecked Sendable { } } + +// MARK: - Authentication + +extension User: Authenticatable {} +extension User: SessionAuthenticatable { + public var sessionID: String { username } +} + +extension User: Content {} + +public struct UserPasswordAuthenticator: AsyncBasicAuthenticator { + public typealias User = SharedModels.User + + public init() {} + + public func authenticate(basic: BasicAuthorization, for request: Request) async throws { + guard let user = try await UserModel.query(on: request.db) + .filter(\.$username == basic.username) + .first(), + try Bcrypt.verify(basic.password, created: user.passwordHash) + else { + throw Abort(.unauthorized) + } + try request.auth.login(user.toDTO()) + } +} + +public struct UserTokenAuthenticator: AsyncBearerAuthenticator { + public typealias User = SharedModels.User + + public init() {} + + public func authenticate(bearer: BearerAuthorization, for request: Request) async throws { + guard let token = try await UserTokenModel.query(on: request.db) + .filter(\.$value == bearer.token) + .with(\.$user) + .first() + else { + throw Abort(.unauthorized) + } + try request.auth.login(token.user.toDTO()) + } +} + +public struct UserSessionAuthenticator: AsyncSessionAuthenticator { + public typealias User = SharedModels.User + + public init() {} + + public func authenticate(sessionID: User.SessionID, for request: Request) async throws { + guard let user = try await UserModel.query(on: request.db) + .filter(\.$username == sessionID) + .first() + else { + throw Abort(.unauthorized) + } + try request.auth.login(user.toDTO()) + } +} diff --git a/Sources/HApp/App+build.swift b/Sources/HApp/App+build.swift deleted file mode 100644 index 7406000..0000000 --- a/Sources/HApp/App+build.swift +++ /dev/null @@ -1,102 +0,0 @@ -import DatabaseClient -import DatabaseClientLive -import FluentSQLiteDriver -import Hummingbird -import HummingbirdFluent -import Logging - -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: some AppArguments) async throws -> some ApplicationProtocol { - let environment = Environment() - - let logger = { - 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 - - // 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() - // - var app = Application( - router: router, - configuration: .init( - address: .hostname(arguments.hostname, port: arguments.port), - serverName: "Purchase-Orders" - ), - logger: logger - ) - - app.addServices(fluent, fluentPersist) - 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 deleted file mode 100644 index 4be70f8..0000000 --- a/Sources/HApp/App.swift +++ /dev/null @@ -1,41 +0,0 @@ -import ArgumentParser -import Hummingbird -import Logging - -@main -struct App: AsyncParsableCommand, AppArguments { - - @Option(name: .shortAndLong) - var hostname: String = "127.0.0.1" - - @Option(name: .shortAndLong) - var port: Int = 8080 - - @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(self) - try await app.runService() - } -} - -/// Extend `Logger.Level` so it can be used as an argument -#if hasFeature(RetroactiveAttribute) - 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 deleted file mode 100644 index 4842b83..0000000 --- a/Sources/HApp/Controllers/Api/UserController.swift +++ /dev/null @@ -1,28 +0,0 @@ -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 {} diff --git a/Sources/SharedModels/Employee.swift b/Sources/SharedModels/Employee.swift index c325130..db192a4 100644 --- a/Sources/SharedModels/Employee.swift +++ b/Sources/SharedModels/Employee.swift @@ -24,7 +24,40 @@ public struct Employee: Codable, Equatable, Identifiable, Sendable { self.lastName = lastName self.updatedAt = updatedAt } +} +public extension Employee { + struct Create: Codable, Sendable { + public let firstName: String + public let lastName: String + public let active: Bool? + + public init( + firstName: String, + lastName: String, + active: Bool? = nil + ) { + self.firstName = firstName + self.lastName = lastName + self.active = active + } + } + + struct Update: Codable, Sendable { + public let firstName: String? + public let lastName: String? + public let active: Bool? + + public init( + firstName: String? = nil, + lastName: String? = nil, + active: Bool? = nil + ) { + self.firstName = firstName + self.lastName = lastName + self.active = active + } + } } // public extension Employee { diff --git a/Sources/SharedModels/PurchaseOrder.swift b/Sources/SharedModels/PurchaseOrder.swift index 0ccb163..686835d 100644 --- a/Sources/SharedModels/PurchaseOrder.swift +++ b/Sources/SharedModels/PurchaseOrder.swift @@ -38,3 +38,35 @@ public struct PurchaseOrder: Codable, Equatable, Identifiable, Sendable { self.updatedAt = updatedAt } } + +public extension PurchaseOrder { + struct Create: Codable, Sendable { + + public let id: Int? + public let workOrder: Int? + public let materials: String + public let customer: String + public let truckStock: Bool? + public let createdForID: Employee.ID + public let vendorBranchID: VendorBranch.ID + + public init( + id: Int? = nil, + workOrder: Int? = nil, + materials: String, + customer: String, + truckStock: Bool? = nil, + createdForID: Employee.ID, + vendorBranchID: VendorBranch.ID + ) { + self.id = id + self.workOrder = workOrder + self.materials = materials + self.customer = customer + self.truckStock = truckStock + self.createdForID = createdForID + self.vendorBranchID = vendorBranchID + } + } + +} diff --git a/Sources/SharedModels/User.swift b/Sources/SharedModels/User.swift index d3a958f..f1a2c31 100644 --- a/Sources/SharedModels/User.swift +++ b/Sources/SharedModels/User.swift @@ -24,6 +24,61 @@ public struct User: Codable, Equatable, Identifiable, Sendable { } } +public extension User { + + struct Create: Codable, Sendable { + public let username: String + public let email: String + public let password: String + public let confirmPassword: String + + public init( + username: String, + email: String, + password: String, + confirmPassword: String + ) { + self.username = username + self.email = email + self.password = password + self.confirmPassword = confirmPassword + } + } + + struct Login: Codable, Sendable { + public let username: String? + public let email: String? + public let password: String + + public init( + username: String?, + email: String? = nil, + password: String + ) { + self.username = username + self.email = email + self.password = password + } + } + + struct Token: Codable, Equatable, Identifiable, Sendable { + public let id: UUID + public let userID: User.ID + public let value: String + + public init( + id: UUID, + userID: User.ID, + value: String + ) { + self.id = id + self.userID = userID + self.value = value + } + } + +} + // public extension User { // static var mocks: [Self] { // [ diff --git a/Sources/SharedModels/Vendor.swift b/Sources/SharedModels/Vendor.swift index 060fcd3..a57f511 100644 --- a/Sources/SharedModels/Vendor.swift +++ b/Sources/SharedModels/Vendor.swift @@ -23,6 +23,25 @@ public struct Vendor: Codable, Equatable, Identifiable, Sendable { } } +public extension Vendor { + + struct Create: Codable, Sendable { + public let name: String + + public init(name: String) { + self.name = name + } + } + + struct Update: Codable, Sendable { + public let name: String? + + public init(name: String?) { + self.name = name + } + } +} + // public extension Vendor { // // static var mocks: [Self] { diff --git a/Sources/SharedModels/VendorBranch.swift b/Sources/SharedModels/VendorBranch.swift index 752d250..5710a9a 100644 --- a/Sources/SharedModels/VendorBranch.swift +++ b/Sources/SharedModels/VendorBranch.swift @@ -22,3 +22,23 @@ public struct VendorBranch: Codable, Equatable, Identifiable, Sendable { self.updatedAt = updatedAt } } + +public extension VendorBranch { + struct Create: Codable, Sendable { + public let name: String + public let vendorID: Vendor.ID + + public init(name: String, vendorID: Vendor.ID) { + self.name = name + self.vendorID = vendorID + } + } + + struct Update: Codable, Sendable { + public let name: String? + + public init(name: String?) { + self.name = name + } + } +}