import Fluent import Vapor 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:)) 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 fetch by id. // 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 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() } } struct VendorsIndexQuery: Content { let branches: Bool? } struct EmployeesIndexQuery: Content { let active: Bool? }