From 88ee71cb68734548b1d038e767ada4f6dfb959c1 Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Fri, 10 Jan 2025 13:31:56 -0500 Subject: [PATCH] feat: Begins breaking out database interfaces and api controllers into seperate items. --- .swiftlint.yml | 5 ++ .../Api/EmployeeApiController.swift | 64 ++++++++++++++++++ .../Api/PurchaseOrderApiController.swift | 62 ++++++++++++++++++ .../Controllers/Api/UserApiController.swift | 59 +++++++++++++++++ .../Controllers/Api/VendorApiController.swift | 52 +++++++++++++++ .../Api/VendorBranchApiController.swift | 64 ++++++++++++++++++ Sources/App/Controllers/ApiController.swift | 35 +++++++--- .../PurchaseOrderViewController.swift | 45 ++++++------- Sources/App/DB/EmployeeDB.swift | 49 ++++++++++++++ Sources/App/DB/PurchaseOrderDB.swift | 65 +++++++++++++++++++ Sources/App/DB/UserDB.swift | 36 ++++++++++ Sources/App/DB/VendorBranchDB.swift | 62 ++++++++++++++++++ Sources/App/DB/VendorDB.swift | 47 ++++++++++++++ .../Extensions/RouteBuilder+protected.swift | 15 +++++ 14 files changed, 624 insertions(+), 36 deletions(-) create mode 100644 Sources/App/Controllers/Api/EmployeeApiController.swift create mode 100644 Sources/App/Controllers/Api/PurchaseOrderApiController.swift create mode 100644 Sources/App/Controllers/Api/UserApiController.swift create mode 100644 Sources/App/Controllers/Api/VendorApiController.swift create mode 100644 Sources/App/Controllers/Api/VendorBranchApiController.swift create mode 100644 Sources/App/DB/EmployeeDB.swift create mode 100644 Sources/App/DB/PurchaseOrderDB.swift create mode 100644 Sources/App/DB/UserDB.swift create mode 100644 Sources/App/DB/VendorBranchDB.swift create mode 100644 Sources/App/DB/VendorDB.swift diff --git a/.swiftlint.yml b/.swiftlint.yml index 213129c..baad5e7 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -9,3 +9,8 @@ included: - Tests ignore_multiline_statement_conditions: true + +identifier_name: + excluded: + - "db" + - "id" diff --git a/Sources/App/Controllers/Api/EmployeeApiController.swift b/Sources/App/Controllers/Api/EmployeeApiController.swift new file mode 100644 index 0000000..215a577 --- /dev/null +++ b/Sources/App/Controllers/Api/EmployeeApiController.swift @@ -0,0 +1,64 @@ +import Fluent +import Vapor + +struct EmployeeApiController: RouteCollection { + + private let employeeDB = EmployeeDB() + + 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 employeeDB.fetchAll(active: params.active, on: req.db) + } + + @Sendable + func create(req: Request) async throws -> Employee.DTO { + try Employee.Create.validate(content: req) + let create = try req.content.decode(Employee.Create.self) + return try await employeeDB.create(create, on: req.db) + } + + @Sendable + func get(req: Request) async throws -> Employee.DTO { + guard let id = req.parameters.get("employeeID", as: Employee.IDValue.self), + let employee = try await employeeDB.get(id: id, on: req.db) + else { + throw Abort(.notFound) + } + return employee + } + + @Sendable + func update(req: Request) async throws -> Employee.DTO { + try Employee.Update.validate(content: req) + 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.content.decode(Employee.Update.self) + return try await employeeDB.update(id: employeeID, with: updates, on: req.db) + } + + @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 employeeDB.delete(id: employeeID, on: req.db) + return .ok + } +} + +struct EmployeesIndexQuery: Content { + let active: Bool? +} diff --git a/Sources/App/Controllers/Api/PurchaseOrderApiController.swift b/Sources/App/Controllers/Api/PurchaseOrderApiController.swift new file mode 100644 index 0000000..12d521e --- /dev/null +++ b/Sources/App/Controllers/Api/PurchaseOrderApiController.swift @@ -0,0 +1,62 @@ +import Fluent +import Vapor + +// TODO: Add update route. + +struct PurchaseOrderApiController: RouteCollection { + private let purchaseOrders = PurchaseOrderDB() + + 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(on: req.db) + } + + @Sendable + func create(req: Request) async throws -> PurchaseOrder.DTO { + try PurchaseOrder.Create.validate(content: req) + let model = try req.content.decode(PurchaseOrder.Create.self) + return try await purchaseOrders.create( + model, + createdById: req.auth.require(User.self).requireID(), + on: req.db + ) + } + + @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: id, on: req.db) + 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: id, on: req.db) + 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 new file mode 100644 index 0000000..0274994 --- /dev/null +++ b/Sources/App/Controllers/Api/UserApiController.swift @@ -0,0 +1,59 @@ +import Fluent +import Vapor + +// TODO: Add update and get by id. +struct UserApiController: RouteCollection { + let users = UserDB() + + func boot(routes: any RoutesBuilder) throws { + let unProtected = routes.apiUnprotected(route: "users") + let protected = routes.apiProtected(route: "users") + + unProtected.post(use: create(req:)) + protected.get(use: index(req:)) + protected.post("login", use: login(req:)) + protected.group(":id") { + $0.delete(use: delete(req:)) + } + } + + @Sendable + func index(req: Request) async throws -> [User.DTO] { + try await users.fetchAll(on: req.db) + } + + @Sendable + func create(req: Request) async throws -> User.DTO { + // Allow the first user to be created without authentication. + 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 model = try req.content.decode(User.Create.self) + return try await users.create(model, on: req.db) + } + + @Sendable + func login(req: Request) async throws -> UserToken { + let user = try req.auth.require(User.self) + return try await users.login(user: user, on: req.db) + } + + // @Sendable + // func get(req: Request) async throws -> User.DTO { + // guard let id = req.parameters.get("id", as: User.IDValue.self), + // let user = users. + // } + + @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") + } + try await users.delete(id: id, on: req.db) + return .ok + } +} diff --git a/Sources/App/Controllers/Api/VendorApiController.swift b/Sources/App/Controllers/Api/VendorApiController.swift new file mode 100644 index 0000000..304cb94 --- /dev/null +++ b/Sources/App/Controllers/Api/VendorApiController.swift @@ -0,0 +1,52 @@ +import Fluent +import Vapor + +struct VendorApiController: RouteCollection { + private let vendors = VendorDB() + + 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(withBranches: params.branches, on: req.db) + } + + @Sendable + func create(req: Request) async throws -> Vendor.DTO { + try Vendor.Create.validate(content: req) + let model = try req.content.decode(Vendor.Create.self) + return try await vendors.create(model, on: req.db) + } + + @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: id, with: updates, on: req.db) + } + + @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: id, on: req.db) + return .ok + } +} + +struct VendorsIndexQuery: Content { + let branches: Bool? +} diff --git a/Sources/App/Controllers/Api/VendorBranchApiController.swift b/Sources/App/Controllers/Api/VendorBranchApiController.swift new file mode 100644 index 0000000..458b431 --- /dev/null +++ b/Sources/App/Controllers/Api/VendorBranchApiController.swift @@ -0,0 +1,64 @@ +import Fluent +import Vapor + +struct VendorBranchApiController: RouteCollection { + private let vendorBranches = VendorBranchDB() + + 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(on: req.db) + } + + @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.fetch(for: id, on: req.db) + } + + @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.") + } + try VendorBranch.Create.validate(content: req) + let model = try req.content.decode(VendorBranch.Create.self) + return try await vendorBranches.create(model, for: id, on: req.db) + } + + @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: id, with: updates, on: req.db) + } + + @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: id, on: req.db) + return .ok + } + +} diff --git a/Sources/App/Controllers/ApiController.swift b/Sources/App/Controllers/ApiController.swift index a14ce75..632b20e 100644 --- a/Sources/App/Controllers/ApiController.swift +++ b/Sources/App/Controllers/ApiController.swift @@ -1,6 +1,7 @@ import Fluent import Vapor +// TODO: Use DB controllers. struct ApiController: RouteCollection { func boot(routes: RoutesBuilder) throws { let api = routes.grouped("api", "v1") @@ -29,6 +30,9 @@ struct ApiController: RouteCollection { 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:)) @@ -102,8 +106,6 @@ struct ApiController: RouteCollection { // MARK: - PurchaseOrders - // TODO: Add fetch by id. - // TODO: Add pagination and filters. @Sendable func purchaseOrdersIndex(req: Request) async throws -> [PurchaseOrder.DTO] { @@ -118,6 +120,27 @@ struct ApiController: RouteCollection { .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) @@ -287,11 +310,3 @@ struct ApiController: RouteCollection { return branch.toDTO() } } - -struct VendorsIndexQuery: Content { - let branches: Bool? -} - -struct EmployeesIndexQuery: Content { - let active: Bool? -} diff --git a/Sources/App/Controllers/PurchaseOrderViewController.swift b/Sources/App/Controllers/PurchaseOrderViewController.swift index f809988..1790fdf 100644 --- a/Sources/App/Controllers/PurchaseOrderViewController.swift +++ b/Sources/App/Controllers/PurchaseOrderViewController.swift @@ -3,6 +3,7 @@ import Vapor struct PurchaseOrderViewController: RouteCollection { private let api = ApiController() + private let api2 = PurchaseOrderDB() func boot(routes: any RoutesBuilder) throws { let pos = routes.protected.grouped("purchase-orders") @@ -11,9 +12,10 @@ struct PurchaseOrderViewController: RouteCollection { pos.post(use: create(req:)) } + // TODO: Use pageinated version. @Sendable func index(req: Request) async throws -> View { - let purchaseOrders = try await api.purchaseOrdersIndex(req: req) + 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) req.logger.debug("Branches: \(branches)") @@ -30,31 +32,9 @@ struct PurchaseOrderViewController: RouteCollection { 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) - - 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) - let loaded = try await PurchaseOrder.query(on: req.db) - .filter(\.$id == purchaseOrder.requireID()) - .with(\.$createdFor) - .with(\.$createdBy) - .with(\.$vendorBranch) { - $0.with(\.$vendor) - } - .first() - - return try await req.view.render("purchaseOrders/table-row", loaded) - - // let purchaseOrders = try await api.purchaseOrdersIndex(req: req) - // return try await req.view.render("purchaseOrders/table", ["purchaseOrders": purchaseOrders]) + let create = try req.content.decode(PurchaseOrder.FormCreate.self).toCreate() + let purchaseOrder = try await api2.create(create, createdById: createdById, on: req.db) + return try await req.view.render("purchaseOrders/table-row", purchaseOrder) } } @@ -113,6 +93,7 @@ private extension PurchaseOrder { let createdForID: Employee.IDValue let vendorBranchID: VendorBranch.IDValue + // TODO: Remove. func toModel(createdByID: User.IDValue) -> PurchaseOrder { .init( id: id, @@ -127,6 +108,18 @@ private extension PurchaseOrder { 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 + ) + } } } diff --git a/Sources/App/DB/EmployeeDB.swift b/Sources/App/DB/EmployeeDB.swift new file mode 100644 index 0000000..f5e3e59 --- /dev/null +++ b/Sources/App/DB/EmployeeDB.swift @@ -0,0 +1,49 @@ +import Fluent +import Vapor + +// An intermediate layer between our api and view controllers that interacts with the +// database model. +struct EmployeeDB { + + func create(_ model: Employee.Create, on db: any Database) async throws -> Employee.DTO { + let model = model.toModel() + try await model.save(on: db) + return model.toDTO() + } + + func fetchAll(active: Bool? = nil, on db: any Database) async throws -> [Employee.DTO] { + var query = Employee.query(on: db) + .sort(\.$lastName) + + if let active { + query = query.filter(\.$active == active) + } + + return try await query.all().map { $0.toDTO() } + } + + func get(id: Employee.IDValue, on db: any Database) async throws -> Employee.DTO? { + try await Employee.find(id, on: db).map { $0.toDTO() } + } + + func update( + id: Employee.IDValue, + with updates: Employee.Update, + on db: any Database + ) async throws -> Employee.DTO { + guard let employee = try await Employee.find(id, on: db) else { + throw Abort(.badRequest, reason: "Employee id not found.") + } + employee.applyUpdates(updates) + try await employee.save(on: db) + return employee.toDTO() + } + + func delete(id: Employee.IDValue, on db: any Database) async throws { + guard let employee = try await Employee.find(id, on: db) else { + throw Abort(.badRequest, reason: "Employee id not found.") + } + try await employee.delete(on: db) + } + +} diff --git a/Sources/App/DB/PurchaseOrderDB.swift b/Sources/App/DB/PurchaseOrderDB.swift new file mode 100644 index 0000000..5786874 --- /dev/null +++ b/Sources/App/DB/PurchaseOrderDB.swift @@ -0,0 +1,65 @@ +import Fluent +import Vapor + +// An intermediate between our api and view controllers that interacts with the +// database. +struct PurchaseOrderDB { + + func create( + _ model: PurchaseOrder.Create, + createdById: User.IDValue, + on db: any Database + ) async throws -> PurchaseOrder.DTO { + 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 get(id: purchaseOrder.requireID(), on: db) else { + return purchaseOrder.toDTO() + } + return loaded + } + + func fetchAll(on db: any Database) async throws -> [PurchaseOrder.DTO] { + try await PurchaseOrder.allQuery(on: db) + .sort(\.$id, .descending) + .all().map { $0.toDTO() } + } + + func fetchPage(_ page: Int, limit: Int, on db: any Database) async throws -> Page { + try await PurchaseOrder.allQuery(on: db) + .sort(\.$id, .descending) + .paginate(PageRequest(page: page, per: limit)) + .map { $0.toDTO() } + } + + func get(id: PurchaseOrder.IDValue, on db: any Database) async throws -> PurchaseOrder.DTO? { + try await PurchaseOrder.allQuery(on: db) + .filter(\.$id == id) + .first()?.toDTO() + } + + func delete(id: PurchaseOrder.IDValue, on db: any Database) async throws { + 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) + } + } +} diff --git a/Sources/App/DB/UserDB.swift b/Sources/App/DB/UserDB.swift new file mode 100644 index 0000000..1048d58 --- /dev/null +++ b/Sources/App/DB/UserDB.swift @@ -0,0 +1,36 @@ +import Fluent +import Vapor + +struct UserDB { + + func create(_ model: User.Create, on db: any Database) async throws -> User.DTO { + 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() + } + + func login(user: User, on db: any Database) async throws -> UserToken { + let token = try user.generateToken() + try await token.save(on: db) + return token + } + + func fetchAll(on db: any Database) async throws -> [User.DTO] { + try await User.query(on: db).all().map { $0.toDTO() } + } + + func delete(id: User.IDValue, on db: any Database) async throws { + guard let user = try await User.find(id, on: db) else { + throw Abort(.notFound) + } + try await user.delete(on: db) + } + +} diff --git a/Sources/App/DB/VendorBranchDB.swift b/Sources/App/DB/VendorBranchDB.swift new file mode 100644 index 0000000..4943c57 --- /dev/null +++ b/Sources/App/DB/VendorBranchDB.swift @@ -0,0 +1,62 @@ +import Fluent +import Vapor + +struct VendorBranchDB { + + func create( + _ model: VendorBranch.Create, + for vendorID: Vendor.IDValue, + on db: any Database + ) async throws -> VendorBranch.DTO { + 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() + } + + func fetchAll(withVendor: Bool? = nil, on db: any Database) async throws -> [VendorBranch.DTO] { + var query = VendorBranch.query(on: db) + if withVendor == true { + query = query.with(\.$vendor) + } + return try await query.all().map { $0.toDTO() } + } + + func fetch(for vendorID: Vendor.IDValue, on db: any Database) async throws -> [VendorBranch.DTO] { + guard let vendor = try await Vendor.query(on: db) + .filter(\.$id == vendorID) + .with(\.$branches) + .first() + else { + throw Abort(.notFound) + } + + return vendor.branches.map { $0.toDTO() } + } + + func get(id: VendorBranch.IDValue, on db: any Database) async throws -> VendorBranch.DTO? { + try await VendorBranch.find(id, on: db).map { $0.toDTO() } + } + + func update( + id: VendorBranch.IDValue, + with updates: VendorBranch.Update, + on db: any Database + ) async throws -> VendorBranch.DTO { + 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() + } + + func delete(id: VendorBranch.IDValue, on db: any Database) async throws { + guard let branch = try await VendorBranch.find(id, on: db) else { + throw Abort(.notFound) + } + try await branch.delete(on: db) + } +} diff --git a/Sources/App/DB/VendorDB.swift b/Sources/App/DB/VendorDB.swift new file mode 100644 index 0000000..7e74fda --- /dev/null +++ b/Sources/App/DB/VendorDB.swift @@ -0,0 +1,47 @@ +import Fluent +import Vapor + +struct VendorDB { + func create(_ model: Vendor.Create, on db: any Database) async throws -> Vendor.DTO { + let model = model.toModel() + try await model.save(on: db) + return model.toDTO() + } + + func fetchAll(withBranches: Bool? = nil, on db: any Database) async throws -> [Vendor.DTO] { + var query = Vendor.query(on: db).sort(\.$name, .ascending) + if withBranches == true { + query = query.with(\.$branches) + } + return try await query.all().map { $0.toDTO(includeBranches: withBranches) } + } + + func get(id: Vendor.IDValue, withBranches: Bool? = nil, on db: any Database) async throws -> Vendor.DTO? { + var query = Vendor.query(on: db).filter(\.$id == id) + + if withBranches == true { + query = query.with(\.$branches) + } + return try await query.first().map { $0.toDTO(includeBranches: withBranches) } + } + + func update( + id: Vendor.IDValue, + with updates: Vendor.Update, + on db: any Database + ) async throws -> Vendor.DTO { + guard let vendor = try await Vendor.find(id, on: db) else { + throw Abort(.notFound) + } + vendor.applyUpdates(updates) + return vendor.toDTO() + } + + func delete(id: Vendor.IDValue, on db: any Database) async throws { + guard let vendor = try await Vendor.find(id, on: db) else { + throw Abort(.notFound) + } + try await vendor.delete(on: db) + } + +} diff --git a/Sources/App/Extensions/RouteBuilder+protected.swift b/Sources/App/Extensions/RouteBuilder+protected.swift index 1a84d67..8fa9c93 100644 --- a/Sources/App/Extensions/RouteBuilder+protected.swift +++ b/Sources/App/Extensions/RouteBuilder+protected.swift @@ -12,4 +12,19 @@ extension RoutesBuilder { } ) } + + func apiUnprotected(route: PathComponent) -> any RoutesBuilder { + grouped("api", "v1", route) + } + + // Allows basic or token authentication for api routes and prefixes the + // given route with "/api/v1". + func apiProtected(route: PathComponent) -> any RoutesBuilder { + let prefixed = grouped("api", "v1", route) + return prefixed.grouped( + User.authenticator(), + UserToken.authenticator(), + User.guardMiddleware() + ) + } }