diff --git a/Sources/App/Controllers/Api/UserApiController.swift b/Sources/App/Controllers/Api/UserApiController.swift index 0274994..7f21f99 100644 --- a/Sources/App/Controllers/Api/UserApiController.swift +++ b/Sources/App/Controllers/Api/UserApiController.swift @@ -11,7 +11,7 @@ struct UserApiController: RouteCollection { unProtected.post(use: create(req:)) protected.get(use: index(req:)) - protected.post("login", use: login(req:)) + protected.get("login", use: login(req:)) protected.group(":id") { $0.delete(use: delete(req:)) } diff --git a/Sources/App/Controllers/ApiController.swift b/Sources/App/Controllers/ApiController.swift index 632b20e..81575b2 100644 --- a/Sources/App/Controllers/ApiController.swift +++ b/Sources/App/Controllers/ApiController.swift @@ -1,312 +1,12 @@ import Fluent import Vapor -// TODO: Use DB controllers. struct ApiController: RouteCollection { func boot(routes: RoutesBuilder) throws { - let api = routes.grouped("api", "v1") - - // Allows basic or token authentication. - let protected = api.grouped( - User.authenticator(), - UserToken.authenticator(), - User.guardMiddleware() - ) - - let employees = protected.grouped("employees") - let purchaseOrders = protected.grouped("purchase-orders") - - // TODO: Need to handle the intial creation of users somehow. - let users = protected.grouped("users") - let vendors = protected.grouped("vendors") - let vendorBranches = vendors.grouped(":vendorID", "branches") - - employees.get(use: employeesIndex(req:)) - employees.post(use: createEmployee(req:)) - employees.group(":employeeID") { - $0.delete(use: self.deleteEmployee(req:)) - $0.put(use: self.updateEmployee(req:)) - } - - purchaseOrders.get(use: purchaseOrdersIndex(req:)) - purchaseOrders.post(use: createPurchaseOrder(req:)) - purchaseOrders.group(":purchaseOrderID") { - $0.get(use: getPurchaseOrder(req:)) - } - - users.get(use: usersIndex(req:)) - api.post("users", use: createUser(req:)) - users.group("login") { - $0.get(use: self.login(req:)) - } - users.group(":userID") { - $0.delete(use: self.deleteUser(req:)) - } - - vendors.get(use: vendorsIndex) - vendors.post(use: createVendor) - vendors.group(":vendorID") { - $0.delete(use: self.deleteVendor) - $0.put(use: self.updateVendor(req:)) - } - - vendorBranches.get(use: branchesIndex(req:)) - vendorBranches.post(use: createBranch(req:)) - - vendorBranches.group(":branchID") { - $0.delete(use: self.deleteBranch(req:)) - $0.put(use: self.updateBranch(req:)) - } - } - - // MARK: - Employees - - @Sendable - func employeesIndex(req: Request) async throws -> [Employee.DTO] { - var dbQuery = Employee - .query(on: req.db) - .sort(\.$lastName) - - let params = try req.query.decode(EmployeesIndexQuery.self) - if params.active == true { - dbQuery = dbQuery.filter(\.$active == true) - } - - return try await dbQuery.all().map { $0.toDTO() } - } - - @Sendable - func createEmployee(req: Request) async throws -> Employee.DTO { - try Employee.Create.validate(content: req) - let employee = try req.content.decode(Employee.Create.self).toModel() - try await employee.save(on: req.db) - return employee.toDTO() - } - - @Sendable - func deleteEmployee(req: Request) async throws -> HTTPStatus { - guard let employee = try await Employee.find(req.parameters.get("employeeID"), on: req.db) else { - throw Abort(.notFound) - } - try await employee.delete(on: req.db) - return .ok - } - - @Sendable - func updateEmployee(req: Request) async throws -> Employee.DTO { - try Employee.Update.validate(content: req) - guard let employee = try await Employee.find(req.parameters.get("employeeID"), on: req.db) else { - throw Abort(.notFound) - } - let updates = try req.content.decode(Employee.Update.self) - employee.applyUpdates(updates) - try await employee.save(on: req.db) - return employee.toDTO() - } - - // MARK: - PurchaseOrders - - // TODO: Add pagination and filters. - @Sendable - func purchaseOrdersIndex(req: Request) async throws -> [PurchaseOrder.DTO] { - try await PurchaseOrder.query(on: req.db) - .with(\.$createdBy) - .with(\.$createdFor) - .with(\.$vendorBranch) { - $0.with(\.$vendor) - } - .sort(\.$id, .descending) - .all() - .map { $0.toDTO() } - } - - @Sendable - func getPurchaseOrder(req: Request) async throws -> PurchaseOrder.DTO { - guard let id = req.parameters.get("purchaseOrderID", as: Int.self) else { - throw Abort(.badRequest) - } - - guard let purchaseOrder = try await PurchaseOrder.query(on: req.db) - .filter(\.$id == id) - .with(\.$createdBy) - .with(\.$createdFor) - .with(\.$vendorBranch, { - $0.with(\.$vendor) - }) - .first() - else { - throw Abort(.notFound) - } - - return purchaseOrder.toDTO() - } - - @Sendable - func createPurchaseOrder(req: Request) async throws -> PurchaseOrder.DTO { - try PurchaseOrder.Create.validate(content: req) - let createdById = try req.auth.require(User.self).requireID() - let create = try req.content.decode(PurchaseOrder.Create.self) - - guard let employee = try await Employee.find(create.createdForID, on: req.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 = create.toModel(createdByID: createdById) - try await purchaseOrder.save(on: req.db) - - guard let loaded = try await PurchaseOrder.query(on: req.db) - .filter(\.$id == purchaseOrder.requireID()) - .with(\.$createdBy) - .with(\.$createdFor) - .with(\.$vendorBranch, { branch in - branch.with(\.$vendor) - }) - .first() - else { - // This should really never happen, since we just created the purchase order. - throw Abort(.noContent, reason: "Failed to load purchase order after creation") - } - return loaded.toDTO() - } - - // MARK: - Users - - @Sendable - func createUser(req: Request) async throws -> User.DTO { - let count = try await User.query(on: req.db).count() - if count > 0 { - guard req.auth.get(User.self) != nil else { - throw Abort(.unauthorized) - } - } - - try User.Create.validate(content: req) - let create = try req.content.decode(User.Create.self) - guard create.password == create.confirmPassword else { - throw Abort(.badRequest, reason: "Passwords did not match.") - } - let user = try User( - username: create.username, - email: create.email, - passwordHash: Bcrypt.hash(create.password) - ) - try await user.save(on: req.db) - return user.toDTO() - } - - @Sendable - func login(req: Request) async throws -> UserToken { - let user = try req.auth.require(User.self) - let token = try user.generateToken() - try await token.save(on: req.db) - return token - } - - @Sendable - func usersIndex(req: Request) async throws -> [User.DTO] { - try await User.query(on: req.db).all().map { $0.toDTO() } - } - - @Sendable - func deleteUser(req: Request) async throws -> HTTPStatus { - guard let user = try await User.find(req.parameters.get("userID"), on: req.db) else { - throw Abort(.notFound) - } - - try await user.delete(on: req.db) - return .ok - } - - // MARK: - Vendors - - @Sendable - func vendorsIndex(req: Request) async throws -> [Vendor.DTO] { - var dbQuery = Vendor.query(on: req.db) - let params = try req.query.decode(VendorsIndexQuery.self) - - if params.branches == true { - dbQuery = dbQuery.with(\.$branches) - } - - return try await dbQuery - .sort(\.$name, .ascending) - .all() - .map { $0.toDTO(includeBranches: params.branches) } - } - - @Sendable - func createVendor(req: Request) async throws -> Vendor.DTO { - try Vendor.Create.validate(content: req) - let vendor = try req.content.decode(Vendor.Create.self).toModel() - try await vendor.save(on: req.db) - return vendor.toDTO() - } - - @Sendable - func updateVendor(req: Request) async throws -> Vendor.DTO { - try Vendor.Update.validate(content: req) - let updates = try req.content.decode(Vendor.Update.self) - guard let vendor = try await Vendor.find(req.parameters.get("vendorID"), on: req.db) else { - throw Abort(.notFound) - } - vendor.applyUpdates(updates) - try await vendor.save(on: req.db) - return vendor.toDTO() - } - - @Sendable - func deleteVendor(req: Request) async throws -> HTTPStatus { - guard let vendor = try await Vendor.find(req.parameters.get("vendorID"), on: req.db) else { - throw Abort(.notFound) - } - - try await vendor.delete(on: req.db) - return .ok - } - - // MARK: - VendorBranch - - @Sendable - func branchesIndex(req: Request) async throws -> [VendorBranch.DTO] { - guard let vendor = try await Vendor.find(req.parameters.get("vendorID"), on: req.db) else { - throw Abort(.notFound) - } - return try await vendor.$branches.get(on: req.db).map { $0.toDTO() } - } - - @Sendable - func createBranch(req: Request) async throws -> VendorBranch.DTO { - try VendorBranch.Create.validate(content: req) - let branch = try req.content.decode(VendorBranch.Create.self).toModel() - guard let vendor = try await Vendor.find(req.parameters.get("vendorID"), on: req.db) else { - throw Abort(.notFound) - } - try await vendor.$branches.create(branch, on: req.db) - return branch.toDTO() - } - - @Sendable - func deleteBranch(req: Request) async throws -> HTTPStatus { - guard let branch = try await VendorBranch.find(req.parameters.get("branchID"), on: req.db) else { - throw Abort(.notFound) - } - try await branch.delete(on: req.db) - return .ok - } - - @Sendable - func updateBranch(req: Request) async throws -> VendorBranch.DTO { - try VendorBranch.Update.validate(content: req) - let updates = try req.content.decode(VendorBranch.Update.self) - guard let branch = try await VendorBranch.find(req.parameters.get("branchID"), on: req.db) else { - throw Abort(.notFound) - } - branch.applyUpdates(updates) - try await branch.save(on: req.db) - return branch.toDTO() + 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()) } } diff --git a/Sources/App/Controllers/EmployeeViewController.swift b/Sources/App/Controllers/EmployeeViewController.swift index c17d643..dd1ebbe 100644 --- a/Sources/App/Controllers/EmployeeViewController.swift +++ b/Sources/App/Controllers/EmployeeViewController.swift @@ -4,14 +4,15 @@ import Vapor struct EmployeeViewController: RouteCollection { - private let api = ApiController() + private let employees = EmployeeDB() + private let api = EmployeeApiController() func boot(routes: any RoutesBuilder) throws { - let employees = routes.protected.grouped("employees") - employees.get(use: index(req:)) - employees.get("form", use: employeeForm(req:)) - employees.post(use: create(req:)) - employees.group(":employeeID") { + 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: edit(req:)) $0.delete(use: delete(req:)) $0.put(use: update(req:)) @@ -21,13 +22,16 @@ struct EmployeeViewController: RouteCollection { @Sendable func index(req: Request) async throws -> View { - return try await req.view.render("employees/index", EmployeesCTX(api: api, req: req)) + return try await req.view.render("employees/index", EmployeesCTX(db: employees, req: req)) } @Sendable func create(req: Request) async throws -> View { - _ = try await api.createEmployee(req: req) - return try await req.view.render("employees/index", EmployeesCTX(oob: true, api: api, req: req)) + try Employee.Create.validate(content: req) + let model = try req.content.decode(Employee.Create.self) + _ = try await employees.create(model, on: req.db) + // _ = try await db.createEmployee(req: req) + return try await req.view.render("employees/index", EmployeesCTX(oob: true, db: employees, req: req)) } @Sendable @@ -37,14 +41,14 @@ struct EmployeeViewController: RouteCollection { } employee.active.toggle() try await employee.save(on: req.db) - let employees = try await api.getSortedEmployees(req: req) + let employees = try await employees.fetchAll(on: req.db) return try await req.view.render("employees/table", ["employees": employees]) } @Sendable func delete(req: Request) async throws -> View { - _ = try await api.deleteEmployee(req: req) - let employees = try await api.getSortedEmployees(req: req) + _ = try await api.delete(req: req) + let employees = try await employees.fetchAll(on: req.db) return try await req.view.render("employees/table", ["employees": employees]) } @@ -58,8 +62,8 @@ struct EmployeeViewController: RouteCollection { @Sendable func update(req: Request) async throws -> View { - _ = try await api.updateEmployee(req: req) - return try await req.view.render("employees/index", EmployeesCTX(oob: true, api: api, req: req)) + _ = try await api.update(req: req) + return try await req.view.render("employees/index", EmployeesCTX(oob: true, db: employees, req: req)) } @Sendable @@ -77,11 +81,11 @@ private struct EmployeesCTX: Content { init( oob: Bool = false, employee: Employee? = nil, - api: ApiController, + db: EmployeeDB, req: Request ) async throws { self.oob = oob - self.employees = try await api.getSortedEmployees(req: req) + self.employees = try await db.fetchAll(on: req.db) self.form = .init(employee: employee.map { $0.toDTO() }) } } @@ -108,13 +112,3 @@ private struct EmployeeFormCTX: Content { let employee: Employee.DTO? } } - -private extension ApiController { - - func getSortedEmployees(req: Request) async throws -> [Employee.DTO] { - var employees = try await employeesIndex(req: req) - employees.sort { ($0.active ?? false) && !($1.active ?? false) } - employees.sort { ($0.lastName ?? "") < ($1.lastName ?? "") } - return employees - } -} diff --git a/Sources/App/Controllers/PurchaseOrderViewController.swift b/Sources/App/Controllers/PurchaseOrderViewController.swift index 1790fdf..d6b826f 100644 --- a/Sources/App/Controllers/PurchaseOrderViewController.swift +++ b/Sources/App/Controllers/PurchaseOrderViewController.swift @@ -2,6 +2,8 @@ import Fluent import Vapor struct PurchaseOrderViewController: RouteCollection { + private let employeesApi = EmployeeApiController() + private let branches = VendorBranchDB() private let api = ApiController() private let api2 = PurchaseOrderDB() @@ -16,8 +18,8 @@ struct PurchaseOrderViewController: RouteCollection { @Sendable func index(req: Request) async throws -> View { let purchaseOrders = try await api2.fetchAll(on: req.db) - let branches = try await api.getBranches(req: req) - let employees = try await api.employeesIndex(req: req) + let branches = try await self.branches.getBranches(req: req) + let employees = try await employeesApi.index(req: req) req.logger.debug("Branches: \(branches)") return try await req.view.render( "purchaseOrders/index", @@ -74,7 +76,7 @@ extension VendorBranch { let vendor: Vendor.DTO } - func toFormDTO() throws -> FormDTO { + func toFormDTO() throws -> VendorBranch.FormDTO { try .init( id: requireID(), name: name, @@ -123,12 +125,11 @@ private extension PurchaseOrder { } } -private extension ApiController { +private extension VendorBranchDB { func getBranches(req: Request) async throws -> [VendorBranch.FormDTO] { try await VendorBranch.query(on: req.db) .with(\.$vendor) - // .sort(Vendor.self, \.$name) .all() .map { try $0.toFormDTO() } } diff --git a/Sources/App/Controllers/UsersViewController.swift b/Sources/App/Controllers/UsersViewController.swift index 88b75d9..32a262a 100644 --- a/Sources/App/Controllers/UsersViewController.swift +++ b/Sources/App/Controllers/UsersViewController.swift @@ -3,7 +3,7 @@ import Vapor struct UserViewController: RouteCollection { - private let api = ApiController() + private let api = UserApiController() func boot(routes: any RoutesBuilder) throws { let users = routes.protected.grouped("users") @@ -24,13 +24,13 @@ struct UserViewController: RouteCollection { @Sendable func create(req: Request) async throws -> View { - _ = try await api.createUser(req: req) + _ = try await api.create(req: req) return try await req.view.render("users/table", ["users": api.getSortedUsers(req: req)]) } @Sendable func delete(req: Request) async throws -> View { - _ = try await api.deleteUser(req: req) + _ = try await api.delete(req: req) return try await req.view.render("users/table", ["users": api.getSortedUsers(req: req)]) } } @@ -87,10 +87,10 @@ private struct UsersCTX: Content { } } -private extension ApiController { +private extension UserApiController { func getSortedUsers(req: Request) async throws -> [User.DTO] { - try await usersIndex(req: req) + try await index(req: req) .sorted { ($0.username ?? "") < ($1.username ?? "") } } } diff --git a/Sources/App/Controllers/VendorViewController.swift b/Sources/App/Controllers/VendorViewController.swift index e354f36..14aa0d1 100644 --- a/Sources/App/Controllers/VendorViewController.swift +++ b/Sources/App/Controllers/VendorViewController.swift @@ -2,7 +2,7 @@ import Fluent import Vapor struct VendorViewController: RouteCollection { - private let api = ApiController() + private let api = VendorApiController() func boot(routes: any RoutesBuilder) throws { let vendors = routes.protected.grouped("vendors") @@ -44,12 +44,12 @@ struct VendorViewController: RouteCollection { @Sendable func delete(req: Request) async throws -> HTTPStatus { - try await api.deleteVendor(req: req) + try await api.delete(req: req) } @Sendable func update(req: Request) async throws -> View { - _ = try await api.updateVendor(req: req) + _ = try await api.update(req: req) return try await req.view.render("vendors/table", makeCtx(req: req, oob: true)) }