From b23dc6bf076770deaad78a146b2d30da603fdcc7 Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Sun, 19 Jan 2025 13:33:01 -0500 Subject: [PATCH] feat: Cleans up routes. --- .../Api/EmployeeApiController.swift | 70 --- .../Api/PurchaseOrderApiController.swift | 59 -- .../Controllers/Api/UserApiController.swift | 52 -- .../Controllers/Api/VendorApiController.swift | 51 -- .../Api/VendorBranchApiController.swift | 65 --- Sources/App/Controllers/ApiController.swift | 141 ++++- .../View/EmployeeViewController.swift | 65 --- .../PurchaseOrderSearchViewController.swift | 73 --- .../View/PurchaseOrderViewController.swift | 133 ----- .../View/UsersViewController.swift | 79 --- .../View/UtilsViewController.swift | 54 -- .../View/VendorViewController.swift | 98 ---- Sources/App/Controllers/ViewController.swift | 383 ++++++++++++ .../App/Views/Employees/EmployeeForm.swift | 6 +- .../App/Views/Employees/EmployeeTable.swift | 4 +- .../PurchaseOrders/PurchaseOrderForm.swift | 2 +- .../PurchaseOrders/PurchaseOrderSearch.swift | 15 +- .../PurchaseOrders/PurchaseOrderTable.swift | 6 +- Sources/App/Views/Users/UserDetail.swift | 4 +- Sources/App/Views/Users/UserForm.swift | 17 +- Sources/App/Views/Users/UserTable.swift | 2 +- Sources/App/Views/Vendors/VendorDetail.swift | 7 +- Sources/App/Views/Vendors/VendorForm.swift | 4 +- Sources/App/Views/Vendors/VendorTable.swift | 2 +- Sources/App/Views/ViewRoute.swift | 101 +--- Sources/App/routes.swift | 550 ------------------ Sources/DatabaseClientLive/Vendors.swift | 10 +- Sources/SharedModels/Routes/ApiRoute.swift | 144 ++++- .../SharedModels/Routes/SharedRoutes.swift | 165 ------ Sources/SharedModels/Routes/ViewRoute.swift | 375 ++++++++---- Sources/SharedModels/Vendor.swift | 4 +- justfile | 3 + 32 files changed, 958 insertions(+), 1786 deletions(-) delete mode 100644 Sources/App/Controllers/Api/EmployeeApiController.swift delete mode 100644 Sources/App/Controllers/Api/PurchaseOrderApiController.swift delete mode 100644 Sources/App/Controllers/Api/UserApiController.swift delete mode 100644 Sources/App/Controllers/Api/VendorApiController.swift delete mode 100644 Sources/App/Controllers/Api/VendorBranchApiController.swift delete mode 100644 Sources/App/Controllers/View/EmployeeViewController.swift delete mode 100644 Sources/App/Controllers/View/PurchaseOrderSearchViewController.swift delete mode 100644 Sources/App/Controllers/View/PurchaseOrderViewController.swift delete mode 100644 Sources/App/Controllers/View/UsersViewController.swift delete mode 100644 Sources/App/Controllers/View/UtilsViewController.swift delete mode 100644 Sources/App/Controllers/View/VendorViewController.swift create mode 100644 Sources/App/Controllers/ViewController.swift delete mode 100644 Sources/SharedModels/Routes/SharedRoutes.swift diff --git a/Sources/App/Controllers/Api/EmployeeApiController.swift b/Sources/App/Controllers/Api/EmployeeApiController.swift deleted file mode 100644 index 4c479f7..0000000 --- a/Sources/App/Controllers/Api/EmployeeApiController.swift +++ /dev/null @@ -1,70 +0,0 @@ -import DatabaseClient -import Dependencies -import SharedModels -import Vapor - -struct EmployeeApiController: RouteCollection { - - @Dependency(\.database.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(":id") { - $0.get(use: get(req:)) - $0.put(use: update(req:)) - $0.delete(use: delete(req:)) - } - } - - @Sendable - func index(req: Request) async throws -> [Employee] { - let params = try req.query.decode(EmployeesIndexQuery.self) - return try await employees.fetchAll(params.request) - } - - @Sendable - func create(req: Request) async throws -> Employee { - try await employees.create( - req.content.decode(Employee.Create.self) - ) - } - - @Sendable - func get(req: Request) async throws -> Employee { - guard let employee = try await employees.get(req.ensureIDPathComponent()) else { - throw Abort(.notFound) - } - return employee - } - - @Sendable - func update(req: Request) async throws -> Employee { - return try await employees.update( - req.ensureIDPathComponent(), - req.content.decode(Employee.Update.self) - ) - } - - @Sendable - func delete(req: Request) async throws -> HTTPStatus { - try await employees.delete(req.ensureIDPathComponent()) - return .ok - } -} - -struct EmployeesIndexQuery: Content { - let active: Bool? - - var request: DatabaseClient.Employees.FetchRequest { - switch active { - case .none: - return .all - case .some(true): - return .active - case .some(false): - return .inactive - } - } -} diff --git a/Sources/App/Controllers/Api/PurchaseOrderApiController.swift b/Sources/App/Controllers/Api/PurchaseOrderApiController.swift deleted file mode 100644 index c5c628b..0000000 --- a/Sources/App/Controllers/Api/PurchaseOrderApiController.swift +++ /dev/null @@ -1,59 +0,0 @@ -import DatabaseClient -import Dependencies -import Fluent -import SharedModels -import Vapor - -// TODO: Add update route. - -struct PurchaseOrderApiController: RouteCollection { - - @Dependency(\.database.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] { - try await purchaseOrders.fetchAll() - } - - @Sendable - func create(req: Request) async throws -> PurchaseOrder { - try await purchaseOrders.create( - req.content.decode(PurchaseOrder.CreateIntermediate.self) - .toCreate(createdByID: req.auth.require(User.self).id) - ) - } - - @Sendable - func get(req: Request) async throws -> PurchaseOrder { - guard let purchaseOrder = try await purchaseOrders.get(req.ensureIDPathComponent(as: Int.self)) - else { - throw Abort(.notFound) - } - return purchaseOrder - } - - @Sendable - func delete(req: Request) async throws -> HTTPStatus { - try await purchaseOrders.delete(req.ensureIDPathComponent(as: Int.self)) - return .ok - } - - // @Sendable - // func update(req: Request) async throws -> PurchaseOrder.DTO { - // guard let id = req.parameters.get("id", as: PurchaseOrder.ID.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 deleted file mode 100644 index 8cb1f6c..0000000 --- a/Sources/App/Controllers/Api/UserApiController.swift +++ /dev/null @@ -1,52 +0,0 @@ -import DatabaseClient -import Dependencies -import Fluent -import SharedModels -import Vapor - -// TODO: Add update and get by id. -struct UserApiController: RouteCollection { - - @Dependency(\.database.users) var users - - 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.get("login", use: login(req:)) - protected.group(":id") { - $0.delete(use: delete(req:)) - } - } - - @Sendable - func index(req: Request) async throws -> [User] { - try await users.fetchAll() - } - - @Sendable - func create(req: Request) async throws -> User { - // Allow the first user to be created without authentication. - 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.content.decode(User.Create.self)) - } - - @Sendable - func login(req: Request) async throws -> User.Token { - let user = try req.auth.require(User.self) - return try await users.token(user.id) - } - - @Sendable - func delete(req: Request) async throws -> HTTPStatus { - try await users.delete(req.ensureIDPathComponent()) - return .ok - } -} diff --git a/Sources/App/Controllers/Api/VendorApiController.swift b/Sources/App/Controllers/Api/VendorApiController.swift deleted file mode 100644 index fc5aad9..0000000 --- a/Sources/App/Controllers/Api/VendorApiController.swift +++ /dev/null @@ -1,51 +0,0 @@ -import DatabaseClient -import Dependencies -import Fluent -import SharedModels -import Vapor - -struct VendorApiController: RouteCollection { - - @Dependency(\.database.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] { - let params = try req.query.decode(VendorsIndexQuery.self) - return try await vendors.fetchAll(params.request) - } - - @Sendable - func create(req: Request) async throws -> Vendor { - try await vendors.create(req.content.decode(Vendor.Create.self)) - } - - @Sendable - func update(req: Request) async throws -> Vendor { - return try await vendors.update(req.ensureIDPathComponent(), with: req.content.decode(Vendor.Update.self)) - } - - @Sendable - func delete(req: Request) async throws -> HTTPStatus { - try await vendors.delete(req.ensureIDPathComponent()) - return .ok - } -} - -struct VendorsIndexQuery: Content { - let branches: Bool? - - var request: DatabaseClient.Vendors.FetchRequest { - if branches == true { return .withBranches } - return .all - } -} diff --git a/Sources/App/Controllers/Api/VendorBranchApiController.swift b/Sources/App/Controllers/Api/VendorBranchApiController.swift deleted file mode 100644 index bde2a62..0000000 --- a/Sources/App/Controllers/Api/VendorBranchApiController.swift +++ /dev/null @@ -1,65 +0,0 @@ -import DatabaseClient -import Dependencies -import Fluent -import SharedModels -import Vapor - -struct VendorBranchApiController: RouteCollection { - - @Dependency(\.database.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] { - try await vendorBranches.fetchAll() - } - - @Sendable - func indexForVendor(req: Request) async throws -> [VendorBranch] { - guard let id = req.parameters.get("vendorID", as: Vendor.ID.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 { - let id = try req.ensureIDPathComponent(key: "vendorID") - let content = try req.content.decode(BranchCreateRequest.self) - return try await vendorBranches.create( - .init(name: content.name, vendorID: id) - ) - } - - @Sendable - func update(req: Request) async throws -> VendorBranch { - return try await vendorBranches.update( - req.ensureIDPathComponent(), - req.content.decode(VendorBranch.Update.self) - ) - } - - @Sendable - func delete(req: Request) async throws -> HTTPStatus { - try await vendorBranches.delete(req.ensureIDPathComponent()) - return .ok - } - -} - -private struct BranchCreateRequest: Content { - let name: String -} diff --git a/Sources/App/Controllers/ApiController.swift b/Sources/App/Controllers/ApiController.swift index c27798c..4942efb 100644 --- a/Sources/App/Controllers/ApiController.swift +++ b/Sources/App/Controllers/ApiController.swift @@ -1,14 +1,141 @@ +import DatabaseClient import Dependencies import Fluent import SharedModels 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: UserApiController()) - try routes.register(collection: VendorApiController()) - try routes.register(collection: VendorBranchApiController()) +extension ApiRoute { + func handle(request: Request) async throws -> any AsyncResponseEncodable { + switch self { + case let .employee(route): + return try await route.handle(request: request) + case let .purchaseOrder(route): + return try await route.handle(request: request) + case let .user(route): + return try await route.handle(request: request) + case let .vendor(route): + return try await route.handle(request: request) + case let .vendorBranch(route): + return try await route.handle(request: request) + } + } +} + +extension ApiRoute.EmployeeApiRoute { + func handle(request: Request) async throws -> any AsyncResponseEncodable { + @Dependency(\.database) var database + switch self { + case let .create(employee): + return try await database.employees.create(employee) + case let .delete(id: id): + try await database.employees.delete(id) + return HTTPStatus.ok + case .index: + return try await database.employees.fetchAll() + case let .get(id: id): + guard let employee = try await database.employees.get(id) else { + throw Abort(.badRequest, reason: "Employee not found") + } + return employee + case let .update(id: id, updates: updates): + return try await database.employees.update(id, updates) + } + } +} + +extension ApiRoute.PurchaseOrderApiRoute { + func handle(request: Request) async throws -> any AsyncResponseEncodable { + @Dependency(\.database.purchaseOrders) var purchaseOrders + switch self { + case .index: + return try await purchaseOrders.fetchAll() + case let .create(purchaseOrder): + return try await purchaseOrders.create(purchaseOrder) + case let .delete(id: id): + try await purchaseOrders.delete(id) + return HTTPStatus.ok + case let .get(id: id): + guard let output = try await purchaseOrders.get(id) else { + throw Abort(.badRequest, reason: "Purchase order not found.") + } + return output + case let .page(page: page, limit: limit): + return try await purchaseOrders.fetchPage(.init(page: page, per: limit)) + } + } +} + +// TODO: Add Login. +extension ApiRoute.UserApiRoute { + func handle(request: Request) async throws -> any AsyncResponseEncodable { + @Dependency(\.database.users) var users + switch self { + case let .create(user): + return try await users.create(user) + case let .delete(id: id): + try await users.delete(id) + return HTTPStatus.ok + case .index: + return try await users.fetchAll() + case let .get(id: id): + guard let user = try await users.get(id) else { + throw Abort(.badRequest, reason: "Employee not found") + } + return user + // case let .login(user): + // return try await users.login(user) + case let .update(id: id, updates: updates): + return try await users.update(id, updates) + } + } +} + +extension ApiRoute.VendorApiRoute { + func handle(request: Request) async throws -> any AsyncResponseEncodable { + @Dependency(\.database.vendors) var vendors + switch self { + case let .create(vendor): + return try await vendors.create(vendor) + case let .delete(id: id): + try await vendors.delete(id) + return HTTPStatus.ok + case let .get(id: id): + guard let vendor = try await vendors.get(id) else { + throw Abort(.badRequest, reason: "Employee not found") + } + return vendor + case let .update(id: id, updates: updates): + return try await vendors.update(id, with: updates) + case let .index(withBranches: withBranches): + guard withBranches == true else { + return try await vendors.fetchAll() + } + return try await vendors.fetchAll(.withBranches) + } + } +} + +extension ApiRoute.VendorBranchApiRoute { + func handle(request: Request) async throws -> any AsyncResponseEncodable { + @Dependency(\.database.vendorBranches) var vendorBranches + switch self { + case let .create(branch): + return try await vendorBranches.create(branch) + case let .delete(id: id): + try await vendorBranches.delete(id) + return HTTPStatus.ok + case let .index(for: optionalVendorID): + guard let vendorID = optionalVendorID else { + return try await vendorBranches.fetchAll() + } + return try await vendorBranches.fetchAll(.for(vendorID: vendorID)) + case let .get(id: id): + guard let branch = try await vendorBranches.get(id) else { + throw Abort(.badRequest, reason: "Employee not found") + } + return branch + case let .update(id: id, updates: updates): + return try await vendorBranches.update(id, updates) + } } } diff --git a/Sources/App/Controllers/View/EmployeeViewController.swift b/Sources/App/Controllers/View/EmployeeViewController.swift deleted file mode 100644 index dbe52cb..0000000 --- a/Sources/App/Controllers/View/EmployeeViewController.swift +++ /dev/null @@ -1,65 +0,0 @@ -import DatabaseClient -import Dependencies -import Elementary -import SharedModels -import Vapor -import VaporElementary - -struct EmployeeViewController: RouteCollection { - - @Dependency(\.database.employees) var employees - - func boot(routes: any RoutesBuilder) throws { - let route = routes.protected.grouped("employees") - route.get(use: index) - route.get("create", use: form) - route.post(use: create) - route.group(":id") { - $0.get(use: get) - $0.put(use: update) - } - } - - @Sendable - func index(req: Request) async throws -> HTMLResponse { - try await req.render { try await mainPage(EmployeeForm()) } - } - - @Sendable - func form(req: Request) async throws -> HTMLResponse { - await req.render { EmployeeForm(shouldShow: true) } - } - - @Sendable - func create(req: Request) async throws -> HTMLResponse { - let employee = try await employees.create(req.content.decode(Employee.Create.self)) - return await req.render { EmployeeTable.Row(employee: employee) } - } - - @Sendable - func get(req: Request) async throws -> HTMLResponse { - guard let employee = try await employees.get(req.ensureIDPathComponent()) else { - throw Abort(.badRequest, reason: "Employee not found.") - } - guard req.isHtmxRequest else { - return try await req.render { try await mainPage(EmployeeForm(employee: employee)) } - } - return await req.render { EmployeeForm(employee: employee) } - } - - @Sendable - func update(req: Request) async throws -> HTMLResponse { - let employee = try await employees.update(req.ensureIDPathComponent(), req.content.decode(Employee.Update.self)) - return await req.render { EmployeeTable.Row(employee: employee) } - } - - private func mainPage(_ html: C) async throws -> some SendableHTMLDocument where C: Sendable { - let employees = try await self.employees.fetchAll() - return MainPage(displayNav: true, route: .employees) { - div(.class("container")) { - html - EmployeeTable(employees: employees) - } - } - } -} diff --git a/Sources/App/Controllers/View/PurchaseOrderSearchViewController.swift b/Sources/App/Controllers/View/PurchaseOrderSearchViewController.swift deleted file mode 100644 index fe2dcf5..0000000 --- a/Sources/App/Controllers/View/PurchaseOrderSearchViewController.swift +++ /dev/null @@ -1,73 +0,0 @@ -// import Dependencies -// import Elementary -// import Fluent -// import SharedModels -// import Vapor -// import VaporElementary -// -// struct PurchaseOrderSearchViewController: RouteCollection { -// @Dependency(\.database.employees) var employees -// @Dependency(\.database.vendorBranches) var vendorBranches -// @Dependency(\.database.purchaseOrders) var purchaseOrders -// -// func boot(routes: any RoutesBuilder) throws { -// let route = routes.protected.grouped("purchase-orders", "search") -// route.get(use: index) -// route.post(use: post) -// } -// -// @Sendable -// func index(req: Request) async throws -> HTMLResponse { -// let query = try? req.query.decode(FormQuery.self) -// let html = PurchaseOrderSearch(context: query?.context) -// if query?.table == true || !req.isHtmxRequest { -// return await req.render { mainPage(search: html) } -// } -// return await req.render { html } -// } -// -// @Sendable -// func post(req: Request) async throws -> HTMLResponse { -// let context = try req.content.decode(PurchaseOrderSearchContent.self) -// let results = try await purchaseOrders.search(context.toDatabaseQuery(), .init(page: 1, per: 25)) -// return await req.render { PurchaseOrderTable(page: results, context: .search, searchContext: nil) } -// } -// -// func mainPage(search: PurchaseOrderSearch = .init()) -> some SendableHTMLDocument { -// MainPage(displayNav: true, route: .purchaseOrders) { -// div(.class("container"), .id("purchase-order-content")) { -// search -// PurchaseOrderTable(page: .init(items: [], metadata: .init(page: 0, per: 50, total: 0))) -// } -// } -// } -// -// } -// -// extension PurchaseOrderSearchContent { -// -// func toDatabaseQuery() throws -> PurchaseOrder.SearchContext { -// switch context { -// case .employee: -// guard let createdForID else { -// throw Abort(.badRequest, reason: "Employee id not provided") -// } -// return .employee(createdForID) -// case .customer: -// guard let search, !search.isEmpty else { -// throw Abort(.badRequest, reason: "Customer search string is empty.") -// } -// return .customer(search) -// case .vendor: -// guard let vendorBranchID else { -// throw Abort(.badRequest, reason: "Vendor branch id not provided.") -// } -// return .vendor(vendorBranchID) -// } -// } -// } -// -// private struct FormQuery: Content { -// let context: PurchaseOrderSearchContext -// let table: Bool? -// } diff --git a/Sources/App/Controllers/View/PurchaseOrderViewController.swift b/Sources/App/Controllers/View/PurchaseOrderViewController.swift deleted file mode 100644 index c748e88..0000000 --- a/Sources/App/Controllers/View/PurchaseOrderViewController.swift +++ /dev/null @@ -1,133 +0,0 @@ -import Dependencies -import Elementary -import Fluent -import SharedModels -import Vapor -import VaporElementary - -struct PurchaseOrderViewController: RouteCollection { - @Dependency(\.database.employees) var employees - @Dependency(\.database.vendorBranches) var vendorBranches - @Dependency(\.database.purchaseOrders) var purchaseOrders - - func boot(routes: any RoutesBuilder) throws { - let route = routes.protected.grouped("purchase-orders") - route.get(use: index) - route.get("next", use: nextPage) - route.post(use: create(req:)) - route.group("create") { - $0.get(use: form) - } - route.group(":id") { - $0.get(use: get) - } - } - - @Sendable - func index(req: Request) async throws -> HTMLResponse { - let page = try? req.query.decode(IndexQuery.self) - return try await req.render { try await mainPage(PurchaseOrderForm(), page: page ?? .default) } - } - - @Sendable - func nextPage(req: Request) async throws -> HTMLResponse { - let query = try req.query.decode(IndexQuery.self) - let page = try await purchaseOrders.fetchPage(.init(page: query.page, per: query.limit)) - return await req.render { PurchaseOrderTable.Rows(page: page) } - } - - @Sendable - func form(req: Request) async throws -> HTMLResponse { - guard req.isHtmxRequest else { - return try await req.render { - try await mainPage(PurchaseOrderForm(shouldShow: true), page: .default) - } - } - return await req.render { PurchaseOrderForm(shouldShow: true) } - } - - @Sendable - func get(req: Request) async throws -> HTMLResponse { - let purchaseOrder = try await purchaseOrders.get(req.ensureIDPathComponent(as: Int.self)) - let form = PurchaseOrderForm(purchaseOrder: purchaseOrder, shouldShow: true) - guard req.isHtmxRequest else { - return try await req.render { - try await mainPage(form, page: .default) - } - } - return await req.render { form } - } - - @Sendable - func create(req: Request) async throws -> HTMLResponse { - let create = try req.content.decode(CreateContext.self).toIntermediate() - let user = try req.auth.require(User.self) - let purchaseOrder = try await purchaseOrders.create(create.toCreate(createdByID: user.id)) - return await req.render { PurchaseOrderTable.Row(purchaseOrder: purchaseOrder) } - } - - // @Sendable - // func postSearch(req: Request) async throws -> HTMLResponse { - // let context = try req.content.decode(PurchaseOrderSearchContent.self) - // let results = try await purchaseOrders.search(context.toDatabaseQuery(), .init(page: 1, per: 25)) - // return await req.render { PurchaseOrderTable(page: results, context: .search, searchContext: nil) } - // } - - // Show the form to generate a search query. - // @Sendable - // func getSearch(req: Request) async throws -> HTMLResponse { - // // TODO: Need to handle updating the form. - // return await req.render { - // MainPage(displayNav: true, route: .purchaseOrders) { - // div(.class("container"), .id("purchase-order-content")) { - // PurchaseOrderSearch() - // PurchaseOrderTable(page: .init(items: [], metadata: .init(page: 0, per: 50, total: 0))) - // } - // } - // } - // } - - private func mainPage( - _ html: C, - page: IndexQuery - ) async throws -> some SendableHTMLDocument where C: Sendable { - let page = try await purchaseOrders.fetchPage(.init(page: page.page, per: page.limit)) - return MainPage(displayNav: true, route: .purchaseOrders) { - div(.class("container"), .id("purchase-order-content")) { - html - PurchaseOrderTable(page: page) - } - } - } -} - -struct IndexQuery: Content { - let page: Int - let limit: Int - - static var `default`: Self { - .init(page: 1, limit: 25) - } -} - -private struct CreateContext: Content { - let id: Int? - let workOrder: String - let materials: String - let customer: String - let truckStock: Bool? - let createdForID: Employee.ID - let vendorBranchID: VendorBranch.ID - - func toIntermediate() -> PurchaseOrder.CreateIntermediate { - .init( - id: id, - workOrder: workOrder.isEmpty ? nil : Int(workOrder), - materials: materials, - customer: customer, - truckStock: truckStock, - createdForID: createdForID, - vendorBranchID: vendorBranchID - ) - } -} diff --git a/Sources/App/Controllers/View/UsersViewController.swift b/Sources/App/Controllers/View/UsersViewController.swift deleted file mode 100644 index 5ec3766..0000000 --- a/Sources/App/Controllers/View/UsersViewController.swift +++ /dev/null @@ -1,79 +0,0 @@ -import DatabaseClient -import Dependencies -import Elementary -import SharedModels -import Vapor -import VaporElementary - -struct UserViewController: RouteCollection { - - @Dependency(\.database.users) var users - - func boot(routes: any RoutesBuilder) throws { - let users = routes.protected.grouped("users") - // let users = routes.grouped("users") - users.get(use: index) - users.post(use: create) - users.get("create", use: form) - users.group(":id") { - $0.post(use: update) - $0.get(use: get) - $0.delete(use: delete) - } - } - - @Sendable - func index(req: Request) async throws -> HTMLResponse { - try await req.render { - try await mainPage(UserDetail(user: nil)) - } - } - - @Sendable - func get(req: Request) async throws -> HTMLResponse { - let user = try await users.get(req.ensureIDPathComponent()) - let detail = UserDetail(user: user) - - guard req.isHtmxRequest else { - return try await req.render { try await mainPage(detail) } - } - return await req.render { UserDetail(user: user) } - } - - @Sendable - func create(req: Request) async throws -> HTMLResponse { - _ = try await users.create(req.content.decode(User.Create.self)) - let users = try await users.fetchAll() - // return req.redirect(to: "/users") - return await req.render { UserTable(users: users) } - } - - @Sendable - func delete(req: Request) async throws -> HTTPStatus { - try await users.delete(req.ensureIDPathComponent()) - return .ok - } - - @Sendable - func form(req: Request) async throws -> HTMLResponse { - await req.render { UserForm(context: .create) } - } - - @Sendable - func update(req: Request) async throws -> HTMLResponse { - let updates = try req.content.decode(User.Update.self) - req.logger.info("\(updates)") - let user = try await users.update(req.ensureIDPathComponent(), updates) - return await req.render { UserTable.Row(user: user) } - } - - private func mainPage(_ html: C) async throws -> some SendableHTMLDocument where C: Sendable { - let users = try await users.fetchAll() - return MainPage(displayNav: true, route: .users) { - div(.class("container")) { - html - UserTable(users: users) - } - } - } -} diff --git a/Sources/App/Controllers/View/UtilsViewController.swift b/Sources/App/Controllers/View/UtilsViewController.swift deleted file mode 100644 index 1e07824..0000000 --- a/Sources/App/Controllers/View/UtilsViewController.swift +++ /dev/null @@ -1,54 +0,0 @@ -import DatabaseClient -import Dependencies -import SharedModels -import Vapor -import VaporElementary - -struct UtilsViewController: RouteCollection { - @Dependency(\.database) var database - - func boot(routes: any RoutesBuilder) throws { - let route = routes.protected - route.group("select") { - $0.get("employee", use: employeeSelect(req:)) - $0.get("vendor-branches", use: vendorBranchSelect(req:)) - } - } - - @Sendable - func employeeSelect(req: Request) async throws -> HTMLResponse { - let context = try req.query.decode(SelectQueryContext.self) - let employees = try await database.employees.fetchAll() - return await req.render { context.toHTML(employees: employees) } - } - - @Sendable - func vendorBranchSelect(req: Request) async throws -> HTMLResponse { - let context = try req.query.decode(SelectQueryContext.self) - let branches = try await database.vendorBranches.fetchAllWithDetail() - return await req.render { context.toHTML(branches: branches) } - } -} - -private struct SelectQueryContext: Content { - - let context: SelectContext - - func toHTML(employees: [Employee]) -> EmployeeSelect { - switch context { - case .purchaseOrderForm: - return .purchaseOrderForm(employees: employees) - case .purchaseOrderSearch: - return .purchaseOrderSearch(employees: employees) - } - } - - func toHTML(branches: [VendorBranch.Detail]) -> VendorBranchSelect { - switch context { - case .purchaseOrderForm: - return .purchaseOrderForm(branches: branches) - case .purchaseOrderSearch: - return .purchaseOrderSearch(branches: branches) - } - } -} diff --git a/Sources/App/Controllers/View/VendorViewController.swift b/Sources/App/Controllers/View/VendorViewController.swift deleted file mode 100644 index 6d797a3..0000000 --- a/Sources/App/Controllers/View/VendorViewController.swift +++ /dev/null @@ -1,98 +0,0 @@ -import DatabaseClient -import Dependencies -import Elementary -import SharedModels -import Vapor -import VaporElementary - -struct VendorViewController: RouteCollection { - - @Dependency(\.database.vendors) var vendors - @Dependency(\.database.vendorBranches) var vendorBranches - - func boot(routes: any RoutesBuilder) throws { - let route = routes.protected.grouped("vendors") - route.get(use: index) - route.post(use: create) - route.get("create", use: form) - route.group(":id") { - $0.get(use: detail) - $0.put(use: update) - $0.post("branches", use: createBranch(req:)) - } - } - - @Sendable - func index(req: Request) async throws -> HTMLResponse { - try await req.render { - try await mainPage(VendorForm()) - } - } - - @Sendable - func create(req: Request) async throws -> HTMLResponse { - let vendor = try await vendors.create(req.content.decode(Vendor.Create.self)) - let vendors = try await vendors.fetchAll(.withBranches) - return await req.render { - div(.class("container"), .id("content")) { - VendorDetail(vendor: vendor) - VendorTable(vendors: vendors) - } - } - } - - @Sendable - func form(req: Request) async throws -> HTMLResponse { - await req.render { VendorForm(.float(shouldShow: true)) } - } - - @Sendable - func detail(req: Request) async throws -> HTMLResponse { - guard let vendor = try await vendors.get(req.ensureIDPathComponent(), .withBranches) else { - throw Abort(.badRequest, reason: "Vendor does not exist.") - } - let html = VendorDetail(vendor: vendor) - guard req.isHtmxRequest else { - return try await req.render { try await mainPage(html) } - } - return await req.render { html } - } - - @Sendable - func update(req: Request) async throws -> HTMLResponse { - let vendor = try await vendors.update( - req.ensureIDPathComponent(), - with: req.content.decode(Vendor.Update.self), - returnWithBranches: true - ) - return await req.render { VendorDetail(vendor: vendor) } - } - - @Sendable - func createBranch(req: Request) async throws -> HTMLResponse { - let vendorID = try req.ensureIDPathComponent() - let create = try req.content.decode(CreateBranch.self) - let branch = try await vendorBranches.create(.init(name: create.name, vendorID: vendorID)) - return await req.render { VendorDetail.BranchRow(branch: branch) } - } - - @Sendable - func deleteBranch(req: Request) async throws -> HTTPStatus { - try await vendorBranches.delete(req.ensureIDPathComponent(key: "branchID")) - return .ok - } - - private func mainPage(_ html: C) async throws -> some SendableHTMLDocument where C: Sendable { - let vendors = try await vendors.fetchAll(.withBranches) - return MainPage(displayNav: true, route: .vendors) { - div(.class("container"), .id("content")) { - html - VendorTable(vendors: vendors) - } - } - } -} - -struct CreateBranch: Content { - let name: String -} diff --git a/Sources/App/Controllers/ViewController.swift b/Sources/App/Controllers/ViewController.swift new file mode 100644 index 0000000..04873cf --- /dev/null +++ b/Sources/App/Controllers/ViewController.swift @@ -0,0 +1,383 @@ +import DatabaseClient +import Dependencies +import Elementary +import SharedModels +import Vapor + +extension SharedModels.ViewRoute { + func handle(request: Request) async throws -> any AsyncResponseEncodable { + @Dependency(\.database.users) var users + switch self { + case let .employee(route): + return try await route.handle(request: request) + + case let .login(route): + switch route { + case let .index(next: next): + return await request.render { + MainPage(displayNav: false, route: .login) { + UserForm(context: .login(next: next)) + } + } + case let .post(login): + let token = try await users.login(.init(username: login.username, password: login.password)) + let user = try await users.get(token.userID)! + request.session.authenticate(user) + return await request.render { + MainPage(displayNav: true, route: .purchaseOrders) { + div( + .hx.get(login.next ?? "/purchase-orders"), + .hx.pushURL(true), + .hx.target("body"), + .hx.trigger(.event(.revealed)), + .hx.indicator(".hx-indicator") + ) { + Img.spinner().attributes(.class("hx-indicator")) + } + } + } + } + + case let .purchaseOrder(route): + return try await route.handle(request: request) + + case let .user(route): + return try await route.handle(request: request) + + case let .vendor(route): + return try await route.handle(request: request) + + case let .vendorBranch(route): + return try await route.handle(request: request) + } + } +} + +extension SharedModels.ViewRoute.EmployeeRoute { + + private func mainPage( + _ html: C + ) async throws -> some SendableHTMLDocument where C: Sendable { + @Dependency(\.database) var database + let employees = try await database.employees.fetchAll() + return MainPage(displayNav: true, route: .employees) { + div(.class("container")) { + html + EmployeeTable(employees: employees) + } + } + } + + func handle(request: Request) async throws -> any AsyncResponseEncodable { + @Dependency(\.database.employees) var employees + + switch self { + case .form: + return try await request.render(mainPage: mainPage) { + EmployeeForm(shouldShow: true) + } + + case let .select(context: context): + return try await request.render { + try await context.toHTML(employees: employees.fetchAll()) + } + + case .index: + return try await request.render { try await mainPage(EmployeeForm()) } + + case let .get(id: id): + guard let employee = try await employees.get(id) else { + throw Abort(.badRequest, reason: "Employee id not found.") + } + return try await request.render(mainPage: mainPage) { + EmployeeForm(employee: employee) + } + + case let .create(employee): + return try await request.render { + try await EmployeeTable.Row(employee: employees.create(employee)) + } + + case let .delete(id: id): + try await employees.delete(id) + return HTTPStatus.ok + + case let .update(id: id, updates: updates): + return try await request.render { + try await EmployeeTable.Row(employee: employees.update(id, updates)) + } + } + } + +} + +extension SharedModels.ViewRoute.PurchaseOrderRoute { + private func mainPage( + _ html: C, + page: Int, + limit: Int + ) async throws -> some SendableHTMLDocument where C: Sendable { + @Dependency(\.database.purchaseOrders) var purchaseOrders + let page = try await purchaseOrders.fetchPage(.init(page: page, per: limit)) + return MainPage(displayNav: true, route: .purchaseOrders) { + div(.class("container"), .id("purchase-order-content")) { + html + PurchaseOrderTable(page: page) + } + } + } + + private func mainPage( + _ html: C + ) async throws -> some SendableHTMLDocument where C: Sendable { + try await mainPage(html, page: 1, limit: 25) + } + + func handle(request: Vapor.Request) async throws -> any AsyncResponseEncodable { + @Dependency(\.database.purchaseOrders) var purchaseOrders + switch self { + case .form: + return try await request.render(mainPage: mainPage) { + PurchaseOrderForm(shouldShow: true) + } + + case let .search(route): + return try await route.handle(request: request) + + case let .create(purchaseOrder): + return try await request.render { + try await PurchaseOrderTable.Row(purchaseOrder: purchaseOrders.create(purchaseOrder)) + } + + case let .delete(id: id): + try await purchaseOrders.delete(id) + return HTTPStatus.ok + + case .index: + return try await request.render { + try await mainPage(PurchaseOrderForm()) + } + + case let .get(id: id): + guard let purchaseOrder = try await purchaseOrders.get(id) else { + throw Abort(.badRequest, reason: "Purchase order not found.") + } + return try await request.render(mainPage: mainPage) { + PurchaseOrderForm(purchaseOrder: purchaseOrder, shouldShow: true) + } + + case let .page(page: page, limit: limit): + return try await request.render { + try await PurchaseOrderTable.Rows( + page: purchaseOrders.fetchPage(.init(page: page, per: limit)) + ) + } + } + } + +} + +extension SharedModels.ViewRoute.PurchaseOrderRoute.Search { + + func mainPage(search: PurchaseOrderSearch = .init()) -> some SendableHTMLDocument { + MainPage(displayNav: true, route: .purchaseOrders) { + div(.class("container"), .id("purchase-order-content")) { + search + PurchaseOrderTable(page: .init(items: [], metadata: .init(page: 0, per: 50, total: 0))) + } + } + } + + func handle(request: Vapor.Request) async throws -> any AsyncResponseEncodable { + @Dependency(\.database) var database + switch self { + case let .index(context: context, table: table): + let html = PurchaseOrderSearch(context: context) + if table == true || !request.isHtmxRequest { + return await request.render { mainPage(search: html) } + } + return await request.render { html } + + case let .search(context): + let results = try await database.purchaseOrders.search(context.toDatabaseQuery(), .init(page: 1, per: 25)) + return await request.render { + PurchaseOrderTable(page: results, context: .search) + } + } + } +} + +extension SharedModels.ViewRoute.UserRoute { + + private func mainPage(_ html: C) async throws -> some SendableHTMLDocument where C: Sendable { + @Dependency(\.database) var database + let users = try await database.users.fetchAll() + return MainPage(displayNav: true, route: .users) { + div(.class("container")) { + html + UserTable(users: users) + } + } + } + + func handle(request: Request) async throws -> any AsyncResponseEncodable { + @Dependency(\.database.users) var users + switch self { + case .form: + return try await request.render(mainPage: mainPage) { + UserForm(context: .create) + } + + case let .create(user): + return try await request.render { + try await UserTable.Row(user: users.create(user)) + } + + case let .delete(id: id): + try await users.delete(id) + return HTTPStatus.ok + + case .index: + return try await request.render { + try await mainPage(UserDetail(user: nil)) + } + + case let .get(id: id): + guard let user = try await users.get(id) else { + throw Abort(.badRequest, reason: "User not found.") + } + return try await request.render(mainPage: mainPage) { + UserDetail(user: user) + } + + case let .update(id: id, updates: updates): + return try await request.render { + try await UserTable.Row(user: users.update(id, updates)) + } + } + } + +} + +extension SharedModels.ViewRoute.VendorRoute { + private func mainPage(_ html: C) async throws -> some SendableHTMLDocument where C: Sendable { + @Dependency(\.database) var database + let vendors = try await database.vendors.fetchAll(.withBranches) + return MainPage(displayNav: true, route: .vendors) { + div(.class("container"), .id("content")) { + html + VendorTable(vendors: vendors) + } + } + } + + func handle(request: Request) async throws -> any AsyncResponseEncodable { + @Dependency(\.database) var database + + switch self { + case .form: + return try await request.render(mainPage: mainPage) { + VendorForm(.float(shouldShow: true)) + } + + case .index: + return try await request.render { + try await mainPage(VendorForm()) + } + + case let .create(vendor): + let vendor = try await database.vendors.create(vendor) + return await request.render { + div(.class("container"), .id("content")) { + VendorDetail(vendor: vendor) + try await VendorTable(vendors: database.vendors.fetchAll(.withBranches)) + } + } + + case let .delete(id: id): + try await database.vendors.delete(id) + return HTTPStatus.ok + + case let .get(id: id): + guard let vendor = try await database.vendors.get(id, .withBranches) else { + throw Abort(.badRequest, reason: "Vendor not found.") + } + return try await request.render(mainPage: mainPage) { + VendorDetail(vendor: vendor) + } + + case let .update(id: id, updates: updates): + return try await request.render { + try await VendorDetail( + vendor: database.vendors.update(id, with: updates, returnWithBranches: true) + ) + } + } + } +} + +extension SharedModels.ViewRoute.VendorBranchRoute { + + func handle(request: Request) async throws -> any AsyncResponseEncodable { + @Dependency(\.database) var database + + switch self { + case let .select(context: context): + return try await request.render { + try await context.toHTML(branches: database.vendorBranches.fetchAllWithDetail()) + } + + case let .create(branch): + return try await request.render { + try await VendorDetail.BranchRow(branch: database.vendorBranches.create(branch)) + } + + case let .delete(id: id): + try await database.vendorBranches.delete(id) + return HTTPStatus.ok + } + } +} + +extension SharedModels.ViewRoute.PurchaseOrderRoute.Search.Request { + + func toDatabaseQuery() throws -> PurchaseOrder.SearchContext { + switch context { + case .employee: + guard let createdForID else { + throw Abort(.badRequest, reason: "Employee id not provided") + } + return .employee(createdForID) + case .customer: + guard let customerSearch, !customerSearch.isEmpty else { + throw Abort(.badRequest, reason: "Customer search string is empty.") + } + return .customer(customerSearch) + case .vendor: + guard let vendorBranchID else { + throw Abort(.badRequest, reason: "Vendor branch id not provided.") + } + return .vendor(vendorBranchID) + } + } +} + +extension SharedModels.ViewRoute.SelectContext { + func toHTML(employees: [Employee]) -> EmployeeSelect { + switch self { + case .purchaseOrderForm: + return .purchaseOrderForm(employees: employees) + case .purchaseOrderSearch: + return .purchaseOrderSearch(employees: employees) + } + } + + func toHTML(branches: [VendorBranch.Detail]) -> VendorBranchSelect { + switch self { + case .purchaseOrderForm: + return .purchaseOrderForm(branches: branches) + case .purchaseOrderSearch: + return .purchaseOrderSearch(branches: branches) + } + } +} diff --git a/Sources/App/Views/Employees/EmployeeForm.swift b/Sources/App/Views/Employees/EmployeeForm.swift index a5ee0ef..0f8072b 100644 --- a/Sources/App/Views/Employees/EmployeeForm.swift +++ b/Sources/App/Views/Employees/EmployeeForm.swift @@ -51,8 +51,8 @@ struct EmployeeForm: HTML { "Delete" } .attributes( - .hx.delete(route: .employee(.shared(.delete(id: employee.id)))), .hx.confirm("Are you sure you want to delete this employee?"), + .hx.delete(route: .employee(.delete(id: employee.id))), .hx.target("#employee_\(employee.id)"), .hx.swap(.outerHTML.transition(true).swap("1s")) ) @@ -75,7 +75,7 @@ struct EmployeeForm: HTML { } private var targetURL: SharedModels.ViewRoute { - guard let employee else { return .employee(.shared(.index)) } - return .employee(.shared(.get(id: employee.id))) + guard let employee else { return .employee(.index) } + return .employee(.get(id: employee.id)) } } diff --git a/Sources/App/Views/Employees/EmployeeTable.swift b/Sources/App/Views/Employees/EmployeeTable.swift index 687e584..2066452 100644 --- a/Sources/App/Views/Employees/EmployeeTable.swift +++ b/Sources/App/Views/Employees/EmployeeTable.swift @@ -14,7 +14,7 @@ struct EmployeeTable: HTML { Button.add() .attributes( .style("padding: 0px 10px;"), - .hx.get(route: .employee(.shared(.index))), + .hx.get(route: .employee(.form)), .hx.target(.float), .hx.swap(.outerHTML.transition(true).swap("0.5s")) ) @@ -39,7 +39,7 @@ struct EmployeeTable: HTML { Button.detail() .attributes( .style("padding-left: 15px;"), - .hx.get(route: .employee(.shared(.get(id: employee.id)))), + .hx.get(route: .employee(.get(id: employee.id))), .hx.target(.float), .hx.pushURL(true), .hx.swap(.outerHTML.transition(true).swap("0.5s")) diff --git a/Sources/App/Views/PurchaseOrders/PurchaseOrderForm.swift b/Sources/App/Views/PurchaseOrders/PurchaseOrderForm.swift index e53c090..1df16c9 100644 --- a/Sources/App/Views/PurchaseOrders/PurchaseOrderForm.swift +++ b/Sources/App/Views/PurchaseOrders/PurchaseOrderForm.swift @@ -27,7 +27,7 @@ struct PurchaseOrderForm: HTML { } } form( - .hx.post(route: .purchaseOrder(.shared(.index))), + .hx.post(route: .purchaseOrder(.index)), .hx.target(.purchaseOrders(.table)), .hx.swap(.afterBegin), .customToggleFloatAfterRequest diff --git a/Sources/App/Views/PurchaseOrders/PurchaseOrderSearch.swift b/Sources/App/Views/PurchaseOrders/PurchaseOrderSearch.swift index 52dc73e..3efefab 100644 --- a/Sources/App/Views/PurchaseOrders/PurchaseOrderSearch.swift +++ b/Sources/App/Views/PurchaseOrders/PurchaseOrderSearch.swift @@ -23,7 +23,7 @@ struct PurchaseOrderSearch: HTML { div(.class("btn-row")) { button( .class("btn-secondary"), .style("position: absolute; top: 80px; right: 20px;"), - .hx.get(route: .purchaseOrder(.shared(.index))), .hx.pushURL(true), .hx.target("body") + .hx.get(route: .purchaseOrder(.index)), .hx.pushURL(true), .hx.target("body") ) { "x" } } @@ -61,16 +61,3 @@ struct PurchaseOrderSearch: HTML { } } - -// enum PurchaseOrderSearchContext: String, Codable, Content, CaseIterable { -// case employee -// case customer -// case vendor -// } - -// struct PurchaseOrderSearchContent: Content { -// let context: PurchaseOrderSearchContext -// let createdForID: Employee.ID? -// let search: String? -// let vendorBranchID: VendorBranch.ID? -// } diff --git a/Sources/App/Views/PurchaseOrders/PurchaseOrderTable.swift b/Sources/App/Views/PurchaseOrders/PurchaseOrderTable.swift index edfd6bd..5b79af8 100644 --- a/Sources/App/Views/PurchaseOrders/PurchaseOrderTable.swift +++ b/Sources/App/Views/PurchaseOrders/PurchaseOrderTable.swift @@ -45,7 +45,7 @@ struct PurchaseOrderTable: HTML { if context != .search { Button.add() .attributes( - .hx.get(route: .purchaseOrder(.shared(.index))), .hx.target(.float), + .hx.get(route: .purchaseOrder(.index)), .hx.target(.float), .hx.swap(.outerHTML), .hx.pushURL(true) ) } @@ -82,7 +82,7 @@ struct PurchaseOrderTable: HTML { if page.metadata.pageCount > page.metadata.page { tr( // .hx.get("/purchase-orders/next?page=\(page.metadata.page + 1)&limit=\(page.metadata.per)"), - .hx.get(route: .purchaseOrder(.shared(.page(page: page.metadata.page + 1, limit: page.metadata.per)))), + .hx.get(route: .purchaseOrder(.page(page: page.metadata.page + 1, limit: page.metadata.per))), .hx.trigger(.event(.revealed)), .hx.swap(.outerHTML.transition(true).swap("1s")), .hx.target(.this), @@ -111,7 +111,7 @@ struct PurchaseOrderTable: HTML { td { Button.detail() .attributes( - .hx.get(route: .purchaseOrder(.shared(.get(id: purchaseOrder.id)))), + .hx.get(route: .purchaseOrder(.get(id: purchaseOrder.id))), .hx.target("#float"), .hx.swap(.outerHTML.transition(true).swap("0.5s")), .hx.pushURL(true) diff --git a/Sources/App/Views/Users/UserDetail.swift b/Sources/App/Views/Users/UserDetail.swift index 8d59290..3fbf982 100644 --- a/Sources/App/Views/Users/UserDetail.swift +++ b/Sources/App/Views/Users/UserDetail.swift @@ -12,7 +12,7 @@ struct UserDetail: HTML, Sendable { Float(shouldDisplay: user != nil, resetURL: "/users") { if let user { form( - .hx.post(route: .user(.shared(.get(id: user.id)))), + .hx.post(route: .user(.get(id: user.id))), .hx.swap(.outerHTML), .hx.target(.user(.row(id: user.id))), .custom(name: "hx-on::after-request", value: "toggleContent('float'); window.location.href='/users';") @@ -36,7 +36,7 @@ struct UserDetail: HTML, Sendable { ) { "Update" } Button.danger { "Delete" } .attributes( - .hx.delete(route: .user(.shared(.get(id: user.id)))), + .hx.delete(route: .user(.get(id: user.id))), .hx.trigger(.event(.click)), .hx.swap(.outerHTML), .hx.target(.user(.row(id: user.id))), diff --git a/Sources/App/Views/Users/UserForm.swift b/Sources/App/Views/Users/UserForm.swift index 42b0e62..fe87783 100644 --- a/Sources/App/Views/Users/UserForm.swift +++ b/Sources/App/Views/Users/UserForm.swift @@ -1,5 +1,6 @@ import Elementary import ElementaryHTMX +import SharedModels // Form used to login or create a new user. struct UserForm: HTML, Sendable { @@ -28,6 +29,9 @@ struct UserForm: HTML, Sendable { value: "if(event.detail.successful) this.reset(); toggleContent('float');" ) ) { + if case let .login(next) = context, let next { + input(.type(.hidden), .name("next"), .value(next)) + } div(.class("row")) { input(.type(.text), .id("username"), .name("username"), .placeholder("Username"), .autofocus, .required) } @@ -104,12 +108,13 @@ struct UserForm: HTML, Sendable { switch self { case .create: return "/users" - case let .login(next: next): - let path = "/login" - if let next { - return "\(path)?next=\(next)" - } - return path + case .login: + return "/login" + // let path = "/login" + // if let next { + // return "\(path)?next=\(next)" + // } + // return path } } } diff --git a/Sources/App/Views/Users/UserTable.swift b/Sources/App/Views/Users/UserTable.swift index 592008d..e42c91e 100644 --- a/Sources/App/Views/Users/UserTable.swift +++ b/Sources/App/Views/Users/UserTable.swift @@ -45,7 +45,7 @@ struct UserTable: HTML { td { user.email } td { Button.detail().attributes( - .hx.get(route: .user(.shared(.get(id: user.id)))), + .hx.get(route: .user(.get(id: user.id))), .hx.target(.float), .hx.swap(.outerHTML), .hx.pushURL(true) diff --git a/Sources/App/Views/Vendors/VendorDetail.swift b/Sources/App/Views/Vendors/VendorDetail.swift index 1ee57a6..841bc52 100644 --- a/Sources/App/Views/Vendors/VendorDetail.swift +++ b/Sources/App/Views/Vendors/VendorDetail.swift @@ -2,6 +2,7 @@ import Elementary import ElementaryHTMX import SharedModels +// TODO: Lazy Load branches when view appears. struct VendorDetail: HTML { let vendor: Vendor @@ -15,7 +16,7 @@ struct VendorDetail: HTML { } closeButton: { Button.close(id: "float") .attributes( - .hx.get(route: .vendor(.shared(.index(withBranches: true)))), + .hx.get(route: .vendor(.index)), .hx.pushURL(true), .hx.target(.body), .hx.swap(.outerHTML) @@ -25,6 +26,7 @@ struct VendorDetail: HTML { // TODO: What route for here?? var branchForm: some HTML { + // TODO: Add hidden input field with vendor id. form( .id("branch-form"), .hx.post("/vendors/\(vendor.id)/branches"), @@ -34,7 +36,8 @@ struct VendorDetail: HTML { ) { input( .type(.text), .class("col-9"), .name("name"), .placeholder("Add branch..."), .required, - .hx.post(route: .vendorBranch(.index(for: vendor.id))), + // FIX: route + // .hx.post(route: .vendorBranch(.index(for: vendor.id))), .hx.trigger(.event(.keyup).changed().delay("800ms")), .hx.target("#branches"), .hx.swap(.beforeEnd) // , diff --git a/Sources/App/Views/Vendors/VendorForm.swift b/Sources/App/Views/Vendors/VendorForm.swift index 4fbc50c..1b8d8b9 100644 --- a/Sources/App/Views/Vendors/VendorForm.swift +++ b/Sources/App/Views/Vendors/VendorForm.swift @@ -86,7 +86,7 @@ struct VendorForm: HTML { } var targetURL: SharedModels.ViewRoute { - guard let vendor else { return .vendor(.shared(.index(withBranches: true))) } - return .vendor(.shared(.get(id: vendor.id))) + guard let vendor else { return .vendor(.index) } + return .vendor(.get(id: vendor.id)) } } diff --git a/Sources/App/Views/Vendors/VendorTable.swift b/Sources/App/Views/Vendors/VendorTable.swift index c8854c8..989e4d9 100644 --- a/Sources/App/Views/Vendors/VendorTable.swift +++ b/Sources/App/Views/Vendors/VendorTable.swift @@ -41,7 +41,7 @@ struct VendorTable: HTML { Button.detail() .attributes( .style("padding-left: 15px;"), - .hx.get(route: .vendor(.shared(.get(id: vendor.id)))), + .hx.get(route: .vendor(.get(id: vendor.id))), .hx.target("#float"), .hx.pushURL(true), .hx.swap(.outerHTML) diff --git a/Sources/App/Views/ViewRoute.swift b/Sources/App/Views/ViewRoute.swift index 5b22660..d4af274 100644 --- a/Sources/App/Views/ViewRoute.swift +++ b/Sources/App/Views/ViewRoute.swift @@ -19,23 +19,6 @@ extension HTMLAttribute.hx { static func delete(route: SharedModels.ViewRoute) -> HTMLAttribute { delete(SharedModels.ViewRoute.router.path(for: route)) } - - // static func get(route: SharedModels.ApiRoute) -> HTMLAttribute { - // get(route: .shared(route)) - // } - // - // static func post(route: SharedModels.ApiRoute) -> HTMLAttribute { - // post(SharedModels.ApiRoute.router.path(for: route)) - // } - // - // static func put(route: SharedModels.ApiRoute) -> HTMLAttribute { - // put(SharedModels.ApiRoute.router.path(for: route)) - // } - // - // static func delete(route: SharedModels.ApiRoute) -> HTMLAttribute { - // delete(SharedModels.ApiRoute.router.path(for: route)) - // } - } extension HTMLAttribute.hx { @@ -51,89 +34,6 @@ extension HTMLAttribute where Tag: HTMLTrait.Attributes.Global { } } -// TODO: Remove. -enum RouteKey { - case employees(EmployeeRoute? = nil) - case purchaseOrders(PurchaseOrderRoute? = nil) - case users(UserRoute? = nil) - - var url: String { - switch self { - case let .employees(employees): - let path = "/employees" - guard let employees else { return path } - return "\(path)/\(employees.path)" - - case let .purchaseOrders(route): - let path = "/purchase-orders" - guard let route else { return path } - return "\(path)/\(route.path)" - - case let .users(route): - let path = "/users" - guard let route else { return path } - return "\(path)/\(route.path)" - } - } - - enum EmployeeRoute { - case create - case id(Employee.ID) - - var path: String { - switch self { - case .create: return "create" - case let .id(id): return id.uuidString - } - } - } - - enum PurchaseOrderRoute { - case create - case nextPage(PageMetadata) - // case search(SearchQuery? = nil) - // - var path: String { - switch self { - case .create: - return "create" - - case let .nextPage(currentPage): - return "next?page=\(currentPage.page + 1)&limit\(currentPage.per)" - // case let .search(query): - // guard let query else { return "search" } - // return "search?\(query.query)" - } - } - - // enum SearchQuery { - // case context(PurchaseOrderSearchContext, table: Bool? = nil) - // - // var query: String { - // switch self { - // case let .context(context, table): - // let query = "context=\(context.rawValue)" - // guard let table else { return query } - // return "\(query)&table=\(table)" - // } - // } - // } - } - - enum UserRoute { - case create - case id(User.ID) - - var path: String { - switch self { - case .create: return "create" - case let .id(id): return id.uuidString - } - } - } - -} - enum HXTarget { case body case employee(EmployeeKey) @@ -212,6 +112,7 @@ enum HXTarget { } } +// TODO: Move to MainPage enum ViewRoute: String { case employees diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift index 7e43a9b..c130981 100644 --- a/Sources/App/routes.swift +++ b/Sources/App/routes.swift @@ -10,16 +10,8 @@ import VaporElementary import VaporRouting func routes(_ app: Application) throws { - // try app.register(collection: ApiController()) app.mount(SiteRoute.router, use: siteHandler) - // try app.register(collection: UserViewController()) - // try app.register(collection: VendorViewController()) - // try app.register(collection: EmployeeViewController()) - // try app.register(collection: PurchaseOrderViewController()) - // try app.register(collection: PurchaseOrderSearchViewController()) - // try app.register(collection: UtilsViewController()) - // app.get { _ in HTMLResponse { MainPage(displayNav: false, route: .purchaseOrders) { @@ -80,545 +72,3 @@ func siteHandler( return try await route.handle(request: request) } } - -extension ApiRoute { - func handle(request: Request) async throws -> any AsyncResponseEncodable { - switch self { - case let .employee(route): - return try await route.handle(request: request) - case let .purchaseOrder(route): - return try await route.handle(request: request) - case let .user(route): - return try await route.handle(request: request) - case let .vendor(route): - return try await route.handle(request: request) - case let .vendorBranch(route): - return try await route.handle(request: request) - } - } -} - -extension SharedModels.ViewRoute { - func handle(request: Request) async throws -> any AsyncResponseEncodable { - switch self { - case let .employee(route): - return try await route.handle(request: request) - - case .login: - // TODO: Needs to have login context. - return await request.render { - MainPage(displayNav: false, route: .login) { - UserForm(context: .login(next: nil)) - } - } - - case let .purchaseOrder(route): - return try await route.handle(request: request) - - case let .select(route): - return try await route.handle(request: request) - - case let .user(route): - return try await route.handle(request: request) - - case let .vendor(route): - return try await route.handle(request: request) - } - } -} - -extension ApiRoute.EmployeeApiRoute { - func handle(request: Request) async throws -> any AsyncResponseEncodable { - @Dependency(\.database) var database - switch self { - case let .create(employee): - return try await database.employees.create(employee) - case let .delete(id: id): - try await database.employees.delete(id) - return HTTPStatus.ok - case .index: - return try await database.employees.fetchAll() - case let .get(id: id): - guard let employee = try await database.employees.get(id) else { - throw Abort(.badRequest, reason: "Employee not found") - } - return employee - case let .update(id: id, updates: updates): - return try await database.employees.update(id, updates) - } - } -} - -extension ApiRoute.PurchaseOrderApiRoute { - func handle(request: Request) async throws -> any AsyncResponseEncodable { - @Dependency(\.database.purchaseOrders) var purchaseOrders - switch self { - case .index: - return try await purchaseOrders.fetchAll() - case let .create(purchaseOrder): - return try await purchaseOrders.create(purchaseOrder) - case let .delete(id: id): - try await purchaseOrders.delete(id) - return HTTPStatus.ok - case let .get(id: id): - guard let output = try await purchaseOrders.get(id) else { - throw Abort(.badRequest, reason: "Purchase order not found.") - } - return output - case let .page(page: page, limit: limit): - return try await purchaseOrders.fetchPage(.init(page: page, per: limit)) - } - } -} - -extension ApiRoute.UserApiRoute { - func handle(request: Request) async throws -> any AsyncResponseEncodable { - @Dependency(\.database.users) var users - switch self { - case let .create(user): - return try await users.create(user) - case let .delete(id: id): - try await users.delete(id) - return HTTPStatus.ok - case .index: - return try await users.fetchAll() - case let .get(id: id): - guard let user = try await users.get(id) else { - throw Abort(.badRequest, reason: "Employee not found") - } - return user - case let .login(user): - return try await users.login(user) - case let .update(id: id, updates: updates): - return try await users.update(id, updates) - } - } -} - -extension ApiRoute.VendorApiRoute { - func handle(request: Request) async throws -> any AsyncResponseEncodable { - @Dependency(\.database.vendors) var vendors - switch self { - case let .create(vendor): - return try await vendors.create(vendor) - case let .delete(id: id): - try await vendors.delete(id) - return HTTPStatus.ok - case let .index(withBranches: withBranches): - guard withBranches == true else { - return try await vendors.fetchAll() - } - return try await vendors.fetchAll(.withBranches) - case let .get(id: id): - guard let vendor = try await vendors.get(id) else { - throw Abort(.badRequest, reason: "Employee not found") - } - return vendor - case let .update(id: id, updates: updates): - return try await vendors.update(id, with: updates) - } - } -} - -extension ApiRoute.VendorBranchApiRoute { - func handle(request: Request) async throws -> any AsyncResponseEncodable { - @Dependency(\.database.vendorBranches) var vendorBranches - switch self { - case let .create(branch): - return try await vendorBranches.create(branch) - case let .delete(id: id): - try await vendorBranches.delete(id) - return HTTPStatus.ok - case let .index(for: optionalVendorID): - guard let vendorID = optionalVendorID else { - return try await vendorBranches.fetchAll() - } - return try await vendorBranches.fetchAll(.for(vendorID: vendorID)) - case let .get(id: id): - guard let branch = try await vendorBranches.get(id) else { - throw Abort(.badRequest, reason: "Employee not found") - } - return branch - case let .update(id: id, updates: updates): - return try await vendorBranches.update(id, updates) - } - } -} - -extension SharedModels.ViewRoute.EmployeeRoute { - - private func mainPage( - _ html: C - ) async throws -> some SendableHTMLDocument where C: Sendable { - @Dependency(\.database) var database - let employees = try await database.employees.fetchAll() - return MainPage(displayNav: true, route: .employees) { - div(.class("container")) { - html - EmployeeTable(employees: employees) - } - } - } - - func handle(request: Request) async throws -> any AsyncResponseEncodable { - @Dependency(\.database.employees) var employees - - switch self { - case .form: - return try await request.render(mainPage: mainPage) { - EmployeeForm(shouldShow: true) - } - case let .shared(route): - switch route { - case .index: - return try await request.render { try await mainPage(EmployeeForm()) } - - case let .get(id: id): - guard let employee = try await employees.get(id) else { - throw Abort(.badRequest, reason: "Employee id not found.") - } - return try await request.render(mainPage: mainPage) { - EmployeeForm(employee: employee) - } - - case let .create(employee): - return try await request.render { - try await EmployeeTable.Row(employee: employees.create(employee)) - } - - case let .delete(id: id): - try await employees.delete(id) - return HTTPStatus.ok - - case let .update(id: id, updates: updates): - return try await request.render { - try await EmployeeTable.Row(employee: employees.update(id, updates)) - } - } - } - } - -} - -extension SharedModels.ViewRoute.PurchaseOrderRoute { - private func mainPage( - _ html: C, - page: Int, - limit: Int - ) async throws -> some SendableHTMLDocument where C: Sendable { - @Dependency(\.database.purchaseOrders) var purchaseOrders - let page = try await purchaseOrders.fetchPage(.init(page: page, per: limit)) - return MainPage(displayNav: true, route: .purchaseOrders) { - div(.class("container"), .id("purchase-order-content")) { - html - PurchaseOrderTable(page: page) - } - } - } - - private func mainPage( - _ html: C - ) async throws -> some SendableHTMLDocument where C: Sendable { - try await mainPage(html, page: 1, limit: 25) - } - - func handle(request: Vapor.Request) async throws -> any AsyncResponseEncodable { - @Dependency(\.database.purchaseOrders) var purchaseOrders - switch self { - case .form: - return try await request.render(mainPage: mainPage) { - PurchaseOrderForm(shouldShow: true) - } - - case let .search(route): - return try await route.handle(request: request) - - case let .shared(route): - switch route { - case let .create(purchaseOrder): - return try await request.render { - try await PurchaseOrderTable.Row(purchaseOrder: purchaseOrders.create(purchaseOrder)) - } - - case let .delete(id: id): - try await purchaseOrders.delete(id) - return HTTPStatus.ok - - case .index: - return try await request.render { - try await mainPage(PurchaseOrderForm()) - } - - case let .get(id: id): - guard let purchaseOrder = try await purchaseOrders.get(id) else { - throw Abort(.badRequest, reason: "Purchase order not found.") - } - return try await request.render(mainPage: mainPage) { - PurchaseOrderForm(purchaseOrder: purchaseOrder, shouldShow: true) - } - - case let .page(page: page, limit: limit): - return try await request.render { - try await PurchaseOrderTable.Rows( - page: purchaseOrders.fetchPage(.init(page: page, per: limit)) - ) - } - } - } - } - -} - -extension SharedModels.ViewRoute.PurchaseOrderRoute.Search { - - func mainPage(search: PurchaseOrderSearch = .init()) -> some SendableHTMLDocument { - MainPage(displayNav: true, route: .purchaseOrders) { - div(.class("container"), .id("purchase-order-content")) { - search - PurchaseOrderTable(page: .init(items: [], metadata: .init(page: 0, per: 50, total: 0))) - } - } - } - - func handle(request: Vapor.Request) async throws -> any AsyncResponseEncodable { - @Dependency(\.database) var database - switch self { - case let .index(context: context, table: table): - let html = PurchaseOrderSearch(context: context) - if table == true || !request.isHtmxRequest { - return await request.render { mainPage(search: html) } - } - return await request.render { html } - - case let .search(context): - let results = try await database.purchaseOrders.search(context.toDatabaseQuery(), .init(page: 1, per: 25)) - return await request.render { - PurchaseOrderTable(page: results, context: .search) - } - } - } -} - -extension SharedModels.ViewRoute.UserRoute { - - private func mainPage(_ html: C) async throws -> some SendableHTMLDocument where C: Sendable { - @Dependency(\.database) var database - let users = try await database.users.fetchAll() - return MainPage(displayNav: true, route: .users) { - div(.class("container")) { - html - UserTable(users: users) - } - } - } - - func handle(request: Request) async throws -> any AsyncResponseEncodable { - @Dependency(\.database.users) var users - switch self { - case .form: - return try await request.render(mainPage: mainPage) { - UserForm(context: .create) - } - case let .shared(route): - switch route { - case let .create(user): - return try await request.render { - try await UserTable.Row(user: users.create(user)) - } - - case let .delete(id: id): - try await users.delete(id) - return HTTPStatus.ok - - case .index: - return try await request.render { - try await mainPage(UserDetail(user: nil)) - } - - case let .get(id: id): - guard let user = try await users.get(id) else { - throw Abort(.badRequest, reason: "User not found.") - } - return try await request.render(mainPage: mainPage) { - UserDetail(user: user) - } - - case let .login(login): - let token = try await users.login(login) - let user = try await users.get(token.userID)! - request.session.authenticate(user) - return await request.render { - MainPage(displayNav: true, route: .purchaseOrders) { - div( - .hx.get("/purchase-orders"), - .hx.pushURL(true), - .hx.target("body"), - .hx.trigger(.event(.revealed)), - .hx.indicator(".hx-indicator") - ) { - Img.spinner().attributes(.class("hx-indicator")) - } - } - } - - case let .update(id: id, updates: updates): - return try await request.render { - try await UserTable.Row(user: users.update(id, updates)) - } - } - } - } - -} - -extension SharedModels.ViewRoute.VendorRoute { - private func mainPage(_ html: C) async throws -> some SendableHTMLDocument where C: Sendable { - @Dependency(\.database) var database - let vendors = try await database.vendors.fetchAll(.withBranches) - return MainPage(displayNav: true, route: .vendors) { - div(.class("container"), .id("content")) { - html - VendorTable(vendors: vendors) - } - } - } - - func handle(request: Request) async throws -> any AsyncResponseEncodable { - @Dependency(\.database) var database - - switch self { - case .form: - return try await request.render(mainPage: mainPage) { - VendorForm(.float(shouldShow: true)) - } - - case let .shared(route): - switch route { - case let .create(vendor): - let vendor = try await database.vendors.create(vendor) - return await request.render { - div(.class("container"), .id("content")) { - VendorDetail(vendor: vendor) - try await VendorTable(vendors: database.vendors.fetchAll(.withBranches)) - } - } - - case let .delete(id: id): - try await database.vendors.delete(id) - return HTTPStatus.ok - - case let .get(id: id): - guard let vendor = try await database.vendors.get(id, .withBranches) else { - throw Abort(.badRequest, reason: "Vendor not found.") - } - return try await request.render(mainPage: mainPage) { - VendorDetail(vendor: vendor) - } - - case .index: - return try await request.render { - try await mainPage(VendorForm()) - } - - case let .update(id: id, updates: updates): - return try await request.render { - try await VendorDetail( - vendor: database.vendors.update(id, with: updates, returnWithBranches: true) - ) - } - } - } - } -} - -extension SharedModels.ViewRoute.VendorBranchRoute { - - func handle(request: Request) async throws -> any AsyncResponseEncodable { - @Dependency(\.database) var database - - switch self { - case let .shared(route): - switch route { - case let .create(branch): - return try await request.render { - try await VendorDetail.BranchRow(branch: database.vendorBranches.create(branch)) - } - - case let .delete(id: id): - try await database.vendorBranches.delete(id) - return HTTPStatus.ok - - // FIX: - case let .get(id: id): - fatalError() - - case let .index(for: vendorID): - fatalError() - - case let .update(id: id, updates: updates): - fatalError() - } - } - } -} - -extension SharedModels.ViewRoute.PurchaseOrderRoute.Search.Request { - - func toDatabaseQuery() throws -> PurchaseOrder.SearchContext { - switch context { - case .employee: - guard let createdForID else { - throw Abort(.badRequest, reason: "Employee id not provided") - } - return .employee(createdForID) - case .customer: - guard let customerSearch, !customerSearch.isEmpty else { - throw Abort(.badRequest, reason: "Customer search string is empty.") - } - return .customer(customerSearch) - case .vendor: - guard let vendorBranchID else { - throw Abort(.badRequest, reason: "Vendor branch id not provided.") - } - return .vendor(vendorBranchID) - } - } -} - -extension SharedModels.ViewRoute.SelectRoute { - - func handle(request: Request) async throws -> any AsyncResponseEncodable { - @Dependency(\.database) var database - - switch self { - case let .employee(context: context): - return try await request.render { - try await context.toHTML(employees: database.employees.fetchAll()) - } - case let .vendorBranches(context: context): - return try await request.render { - try await context.toHTML(branches: database.vendorBranches.fetchAllWithDetail()) - } - } - } -} - -extension SharedModels.ViewRoute.SelectRoute.Context { - func toHTML(employees: [Employee]) -> EmployeeSelect { - switch self { - case .purchaseOrderForm: - return .purchaseOrderForm(employees: employees) - case .purchaseOrderSearch: - return .purchaseOrderSearch(employees: employees) - } - } - - func toHTML(branches: [VendorBranch.Detail]) -> VendorBranchSelect { - switch self { - case .purchaseOrderForm: - return .purchaseOrderForm(branches: branches) - case .purchaseOrderSearch: - return .purchaseOrderSearch(branches: branches) - } - } -} diff --git a/Sources/DatabaseClientLive/Vendors.swift b/Sources/DatabaseClientLive/Vendors.swift index 1854b1f..098a0ae 100644 --- a/Sources/DatabaseClientLive/Vendors.swift +++ b/Sources/DatabaseClientLive/Vendors.swift @@ -92,10 +92,8 @@ extension Vendor.Create { extension Vendor.Update { func validate() throws { - if let name { - guard !name.isEmpty else { - throw ValidationError(message: "Vendor name should not be empty.") - } + guard !name.isEmpty else { + throw ValidationError(message: "Vendor name should not be empty.") } } } @@ -141,8 +139,6 @@ final class VendorModel: Model, @unchecked Sendable { func applyUpdates(_ updates: Vendor.Update) throws { try updates.validate() - if let name = updates.name { - self.name = name - } + name = updates.name } } diff --git a/Sources/SharedModels/Routes/ApiRoute.swift b/Sources/SharedModels/Routes/ApiRoute.swift index bf81fd3..6de7af3 100644 --- a/Sources/SharedModels/Routes/ApiRoute.swift +++ b/Sources/SharedModels/Routes/ApiRoute.swift @@ -38,37 +38,137 @@ public enum ApiRoute: Sendable { } public enum EmployeeApiRoute: Sendable { - case shared(SharedEmployeeRoute) + case create(Employee.Create) + case delete(id: Employee.ID) + case get(id: Employee.ID) + case index + case update(id: Employee.ID, updates: Employee.Update) - public static let router = Route(.case(Self.shared)) { - SharedEmployeeRoute.router + static let rootPath = "employees" + + public static let router = OneOf { + Route(.case(Self.create)) { + Path { rootPath } + Method.post + Body(.json(Employee.Create.self)) + } + Route(.case(Self.index)) { + Path { rootPath } + Method.get + } + Route(.case(Self.delete(id:))) { + Path { rootPath; UUID.parser() } + Method.delete + } + Route(.case(Self.get(id:))) { + Path { rootPath; UUID.parser() } + Method.get + } + Route(.case(Self.update(id:updates:))) { + Path { rootPath; UUID.parser() } + Method.put + Body(.json(Employee.Update.self)) + } } } public enum PurchaseOrderApiRoute: Sendable { - case shared(SharedPurchaseOrderRoute) + case create(PurchaseOrder.Create) + case delete(id: PurchaseOrder.ID) + case get(id: PurchaseOrder.ID) + case index + case page(page: Int, limit: Int) - public static let router = Route(.case(Self.shared)) { - SharedPurchaseOrderRoute.router + static let rootPath = "purchase-orders" + + public static let router = OneOf { + Route(.case(Self.create)) { + Path { rootPath } + Method.post + Body(.json(PurchaseOrder.Create.self)) + } + Route(.case(Self.delete(id:))) { + Path { rootPath; Digits() } + Method.delete + } + Route(.case(Self.get(id:))) { + Path { rootPath; Digits() } + Method.get + } + Route(.case(Self.index)) { + Path { rootPath } + Method.get + } + Route(.case(Self.page(page:limit:))) { + Path { rootPath; "next" } + Method.get + Query { + Field("page", default: 1) { Digits() } + Field("limit", default: 25) { Digits() } + } + } } } - // TODO: Add logout. + // TODO: Add login / logout. public enum UserApiRoute: Sendable { - case shared(SharedUserRoute) + case create(User.Create) + case delete(id: User.ID) + case get(id: User.ID) + case index + case update(id: User.ID, updates: User.Update) - public static let router = Route(.case(Self.shared)) { - SharedUserRoute.router + static let rootPath = "users" + + public static let router = OneOf { + Route(.case(Self.create)) { + Path { rootPath } + Method.post + Body(.json(User.Create.self)) + } + Route(.case(Self.delete(id:))) { + Path { rootPath; User.ID.parser() } + Method.delete + } + Route(.case(Self.get(id:))) { + Path { rootPath; User.ID.parser() } + Method.get + } + Route(.case(Self.index)) { + Path { rootPath } + Method.get + } + Route(.case(Self.update(id:updates:))) { + Path { rootPath; User.ID.parser() } + Method.patch + Body(.json(User.Update.self)) + } } } public enum VendorApiRoute: Sendable { case index(withBranches: Bool?) - case shared(SharedVendorRoute) + case create(Vendor.Create) + case delete(id: Vendor.ID) + case get(id: Vendor.ID) + case update(id: Vendor.ID, updates: Vendor.Update) static let rootPath = "vendors" public static let router = OneOf { + Route(.case(Self.create)) { + Path { rootPath } + Method.post + Body(.json(Vendor.Create.self)) + } + Route(.case(Self.delete(id:))) { + Path { rootPath; Vendor.ID.parser() } + Method.delete + } + Route(.case(Self.get(id:))) { + Path { rootPath; Vendor.ID.parser() } + Method.get + } Route(.case(Self.index(withBranches:))) { Path { rootPath } Method.get @@ -78,19 +178,32 @@ public enum ApiRoute: Sendable { } } } - Route(.case(Self.shared)) { - SharedVendorRoute.router + Route(.case(Self.update(id:updates:))) { + Path { rootPath; Vendor.ID.parser() } + Method.put + Body(.json(Vendor.Update.self)) } } } public enum VendorBranchApiRoute: Sendable { + case create(VendorBranch.Create) + case delete(id: VendorBranch.ID) case get(id: VendorBranch.ID) case index(for: Vendor.ID? = nil) - case shared(SharedVendorBranchRoute) case update(id: VendorBranch.ID, updates: VendorBranch.Update) public static let router = OneOf { + Route(.case(Self.create)) { + Path { "vendors"; "branches" } + Method.post + Body(.json(VendorBranch.Create.self)) + } + Route(.case(Self.delete(id:))) { + Path { "vendors"; "branches"; VendorBranch.ID.parser() } + Method.delete + } + Route(.case(Self.get(id:))) { Path { "vendors"; "branches"; VendorBranch.ID.parser() } Method.get @@ -102,9 +215,6 @@ public enum ApiRoute: Sendable { Field("vendorID", default: nil) { Optionally { VendorBranch.ID.parser() } } } } - Route(.case(Self.shared)) { - SharedVendorBranchRoute.router - } Route(.case(Self.update(id:updates:))) { Path { "vendors"; "branches"; VendorBranch.ID.parser() } Method.put diff --git a/Sources/SharedModels/Routes/SharedRoutes.swift b/Sources/SharedModels/Routes/SharedRoutes.swift deleted file mode 100644 index 7b85f60..0000000 --- a/Sources/SharedModels/Routes/SharedRoutes.swift +++ /dev/null @@ -1,165 +0,0 @@ -import CasePathsCore -import Foundation -@preconcurrency import URLRouting - -public enum SharedEmployeeRoute: Sendable { - case create(Employee.Create) - case delete(id: Employee.ID) - case get(id: Employee.ID) - case index - case update(id: Employee.ID, updates: Employee.Update) - - static let rootPath = "employees" - - public static let router = OneOf { - Route(.case(Self.create)) { - Path { rootPath } - Method.post - Body(.json(Employee.Create.self)) - } - Route(.case(Self.index)) { - Path { rootPath } - Method.get - } - Route(.case(Self.delete(id:))) { - Path { rootPath; UUID.parser() } - Method.delete - } - Route(.case(Self.get(id:))) { - Path { rootPath; UUID.parser() } - Method.get - } - Route(.case(Self.update(id:updates:))) { - Path { rootPath; UUID.parser() } - Method.put - Body(.json(Employee.Update.self)) - } - } -} - -public enum SharedPurchaseOrderRoute: Sendable { - case create(PurchaseOrder.Create) - case delete(id: PurchaseOrder.ID) - case get(id: PurchaseOrder.ID) - case index - case page(page: Int, limit: Int) - - static let rootPath = "purchase-orders" - - public static let router = OneOf { - Route(.case(Self.create)) { - Path { rootPath } - Method.post - Body(.json(PurchaseOrder.Create.self)) - } - Route(.case(Self.delete(id:))) { - Path { rootPath; Digits() } - Method.delete - } - Route(.case(Self.get(id:))) { - Path { rootPath; Digits() } - Method.get - } - Route(.case(Self.index)) { - Path { rootPath } - Method.get - } - Route(.case(Self.page(page:limit:))) { - Path { rootPath; "next" } - Method.get - Query { - Field("page", default: 1) { Digits() } - Field("limit", default: 25) { Digits() } - } - } - } -} - -public enum SharedUserRoute: Sendable { - case create(User.Create) - case delete(id: User.ID) - case get(id: User.ID) - case index - case login(User.Login) - case update(id: User.ID, updates: User.Update) - - static let rootPath = "users" - - public static let router = OneOf { - Route(.case(Self.create)) { - Path { rootPath } - Method.post - Body(.json(User.Create.self)) - } - Route(.case(Self.delete(id:))) { - Path { rootPath; User.ID.parser() } - Method.delete - } - Route(.case(Self.get(id:))) { - Path { rootPath; User.ID.parser() } - Method.get - } - Route(.case(Self.index)) { - Path { rootPath } - Method.get - } - Route(.case(Self.login)) { - Path { rootPath } - Method.post - Body(.json(User.Login.self)) - } - Route(.case(Self.update(id:updates:))) { - Path { rootPath; User.ID.parser() } - // TODO: Use put or patch. - Method.post - Body(.json(User.Update.self)) - } - } -} - -public enum SharedVendorRoute: Sendable { - case create(Vendor.Create) - case delete(id: Vendor.ID) - case get(id: Vendor.ID) - case update(id: Vendor.ID, updates: Vendor.Update) - - static let rootPath = "vendors" - - public static let router = OneOf { - Route(.case(Self.create)) { - Path { rootPath } - Method.post - Body(.json(Vendor.Create.self)) - } - Route(.case(Self.delete(id:))) { - Path { rootPath; Vendor.ID.parser() } - Method.delete - } - Route(.case(Self.get(id:))) { - Path { rootPath; Vendor.ID.parser() } - Method.get - } - Route(.case(Self.update(id:updates:))) { - Path { rootPath; Vendor.ID.parser() } - Method.put - Body(.json(Vendor.Update.self)) - } - } -} - -public enum SharedVendorBranchRoute: Sendable { - case create(VendorBranch.Create) - case delete(id: VendorBranch.ID) - - public static let router = OneOf { - Route(.case(Self.create)) { - Path { "vendors"; "branches" } - Method.post - Body(.json(VendorBranch.Create.self)) - } - Route(.case(Self.delete(id:))) { - Path { "vendors"; "branches"; VendorBranch.ID.parser() } - Method.delete - } - } -} diff --git a/Sources/SharedModels/Routes/ViewRoute.swift b/Sources/SharedModels/Routes/ViewRoute.swift index 4747a91..fb5dc36 100644 --- a/Sources/SharedModels/Routes/ViewRoute.swift +++ b/Sources/SharedModels/Routes/ViewRoute.swift @@ -5,71 +5,184 @@ import Foundation public enum ViewRoute: Sendable { case employee(EmployeeRoute) - case login + case login(LoginRoute) case purchaseOrder(PurchaseOrderRoute) - case select(SelectRoute) case user(UserRoute) case vendor(VendorRoute) case vendorBranch(VendorBranchRoute) public static let router = OneOf { Route(.case(Self.employee)) { EmployeeRoute.router } - Route(.case(Self.login)) { - Path { "login" } - Method.get - } + Route(.case(Self.login)) { LoginRoute.router } Route(.case(Self.purchaseOrder)) { PurchaseOrderRoute.router } - Route(.case(Self.select)) { SelectRoute.router } Route(.case(Self.user)) { UserRoute.router } Route(.case(Self.vendor)) { VendorRoute.router } Route(.case(Self.vendorBranch)) { VendorBranchRoute.router } } - public enum EmployeeRoute: Sendable { +} + +public extension ViewRoute { + + enum EmployeeRoute: Sendable { + case create(Employee.Create) + case delete(id: Employee.ID) case form - case shared(SharedEmployeeRoute) - - public static func delete(id: Employee.ID) -> Self { - .shared(.delete(id: id)) - } - - public static func get(id: Employee.ID) -> Self { - .shared(.get(id: id)) - } - - public static var index: Self { .shared(.index) } + case get(id: Employee.ID) + case index + case select(context: SelectContext) + case update(id: Employee.ID, updates: Employee.Update) static let rootPath = "employees" public static let router = OneOf { - Route(.case(Self.form)) { - Path { rootPath; "create" } + Route(.case(Self.create)) { + Path { rootPath } + Method.post + Body { + FormData { + Field("firstName", .string) + Field("lastName", .string) + Field("active") { Optionally { Bool.parser() } } + } + .map(.memberwise(Employee.Create.init)) + } + } + Route(.case(Self.index)) { + Path { rootPath } Method.get } - Route(.case(Self.shared)) { - SharedEmployeeRoute.router + Route(.case(Self.delete(id:))) { + Path { rootPath; Employee.ID.parser() } + Method.delete + } + Route(.case(Self.get(id:))) { + Path { rootPath; Employee.ID.parser() } + Method.get + } + Route(.case(Self.update(id:updates:))) { + Path { rootPath; Employee.ID.parser() } + Method.put + Body { + FormData { + Field("firstName") { Optionally { CharacterSet.alphanumerics.map(.string) } } + Field("lastName") { Optionally { CharacterSet.alphanumerics.map(.string) } } + Field("active") { Optionally { Bool.parser() } } + } + .map(.memberwise(Employee.Update.init)) + } + } + Route(.case(Self.select(context:))) { + Path { rootPath; "select" } + Method.get + Query { + Field("context") { SelectContext.parser() } + } } } } +} - public enum PurchaseOrderRoute: Sendable { +public extension ViewRoute { + + enum LoginRoute: Sendable { + case index(next: String? = nil) + case post(Request) + + static let rootPath = "login" + + public static let router = OneOf { + Route(.case(Self.index)) { + Method.get + Path { rootPath } + Query { + Field("next", default: nil) { + Optionally { CharacterSet.urlPathAllowed.map(.string) } + } + } + } + Route(.case(Self.post)) { + Path { rootPath } + Method.post + Body { + FormData { + Field("username", .string) + Field("password", .string) + Field("next", default: nil) { + Optionally { CharacterSet.urlPathAllowed.map(.string) } + } + } + .map(.memberwise(Request.init)) + } + } + } + + public struct Request: Codable, Equatable, Sendable { + public let username: String + public let password: String + public let next: String? + } + } +} + +public extension ViewRoute { + enum PurchaseOrderRoute: Sendable { + case create(PurchaseOrder.Create) + case delete(id: PurchaseOrder.ID) case form + case get(id: PurchaseOrder.ID) + case index + case page(page: Int, limit: Int) case search(Search) - case shared(SharedPurchaseOrderRoute) static let rootPath = "purchase-orders" public static let router = OneOf { + Route(.case(Self.create)) { + Path { rootPath } + Method.post + Body { + FormData { + Field("id") { Optionally { PurchaseOrder.ID.parser() } } + Field("workOrder") { Optionally { Int.parser() } } + Field("materials", .string) + Field("customer", .string) + Field("truckStock") { Optionally { Bool.parser() } } + Field("createdByID") { User.ID.parser() } + Field("createdForID") { Employee.ID.parser() } + Field("vendorBranchID") { VendorBranch.ID.parser() } + } + .map(.memberwise(PurchaseOrder.Create.init)) + } + } + Route(.case(Self.delete(id:))) { + Path { rootPath; Digits() } + Method.delete + } + Route(.case(Self.form)) { Path { rootPath; "create" } Method.get } + Route(.case(Self.get(id:))) { + Path { rootPath; Digits() } + Method.get + } + Route(.case(Self.index)) { + Path { rootPath } + Method.get + } + Route(.case(Self.page(page:limit:))) { + Path { rootPath; "next" } + Method.get + Query { + Field("page", default: 1) { Digits() } + Field("limit", default: 25) { Digits() } + } + } Route(.case(Self.search)) { Search.router } - Route(.case(Self.shared)) { - SharedPurchaseOrderRoute.router - } } public enum Search: Sendable { @@ -121,98 +234,156 @@ public enum ViewRoute: Sendable { } } +} - // TODO: Move into respective view routes. - public enum SelectRoute: Sendable { - case employee(context: Context) - case vendorBranches(context: Context) +public extension ViewRoute { - public enum Context: String, Codable, Sendable, CaseIterable { - case purchaseOrderForm - case purchaseOrderSearch - } - - static let rootPath = "select" - - public static let router = OneOf { - Route(.case(Self.employee(context:))) { - Path { rootPath; "employee" } - Method.get - Query { - Field("context") { Context.parser() } - } - } - Route(.case(Self.vendorBranches(context:))) { - Path { rootPath; "vendor-branches" } - Method.get - Query { - Field("context") { Context.parser() } - } - } - } + enum SelectContext: String, Codable, Sendable, CaseIterable { + case purchaseOrderForm + case purchaseOrderSearch } - public enum UserRoute: Sendable { +} + +public extension ViewRoute { + enum UserRoute: Sendable { + case create(User.Create) + case delete(id: User.ID) case form - case shared(SharedUserRoute) + case get(id: User.ID) + case index + case update(id: User.ID, updates: User.Update) static let rootPath = "users" - public static func delete(id: User.ID) -> Self { - .shared(.delete(id: id)) - } - - public static var index: Self { .shared(.index) } - public static let router = OneOf { + Route(.case(Self.create)) { + Path { rootPath } + Method.post + Body(.json(User.Create.self)) + } + Route(.case(Self.delete(id:))) { + Path { rootPath; User.ID.parser() } + Method.delete + } Route(.case(Self.form)) { Path { rootPath; "create" } Method.get } - Route(.case(Self.shared)) { - SharedUserRoute.router - } - } - } - - public enum VendorRoute: Sendable { - case form - case shared(SharedVendorRoute) - - static let rootPath = "vendors" - - public static let router = OneOf { - Route(.case(Self.form)) { - Path { rootPath; "create" } + Route(.case(Self.get(id:))) { + Path { rootPath; User.ID.parser() } Method.get } - Route(.case(Self.shared)) { - SharedVendorRoute.router - } - } - } - - // TODO: Add Select - public enum VendorBranchRoute: Sendable { - - case index(vendorID: Vendor.ID) - case shared(SharedVendorBranchRoute) - - public static func delete(id: VendorBranch.ID) -> Self { - .shared(.delete(id: id)) - } - - public static let router = OneOf { - Route(.case(Self.index(vendorID:))) { - Path { "vendors"; "branches" } + Route(.case(Self.index)) { + Path { rootPath } Method.get - Query { - Field("vendorID") { Vendor.ID.parser() } + } + Route(.case(Self.update(id:updates:))) { + Path { rootPath; User.ID.parser() } + Method.patch + Body { + FormData { + Field("username") { + Optionally { CharacterSet.alphanumerics.map(.string) } + } + Field("email") { + Optionally { + // TODO: Not sure if this is correct. + Rest().map(.string) + } + } + } + .map(.memberwise(User.Update.init)) + } + } + } + } +} + +public extension ViewRoute { + + enum VendorRoute: Sendable { + case create(Vendor.Create) + case delete(id: Vendor.ID) + case form + case get(id: Vendor.ID) + case index + case update(id: Vendor.ID, updates: Vendor.Update) + + static let rootPath = "vendors" + + public static let router = OneOf { + Route(.case(Self.create)) { + Path { rootPath } + Method.post + Body { + FormData { + Field("name", .string) + } + .map(.memberwise(Vendor.Create.init)) + } + } + Route(.case(Self.delete(id:))) { + Path { rootPath; Vendor.ID.parser() } + Method.delete + } + Route(.case(Self.get(id:))) { + Path { rootPath; Vendor.ID.parser() } + Method.get + } + + Route(.case(Self.form)) { + Path { rootPath; "create" } + Method.get + } + Route(.case(Self.index)) { + Path { rootPath } + Method.get + } + Route(.case(Self.update(id:updates:))) { + Path { rootPath; Vendor.ID.parser() } + Method.put + Body { + FormData { + Field("name", .string) + } + .map(.memberwise(Vendor.Update.init)) + } + } + } + } +} + +public extension ViewRoute { + + enum VendorBranchRoute: Sendable { + case create(VendorBranch.Create) + case delete(id: VendorBranch.ID) + case select(context: ViewRoute.SelectContext) + + public static let router = OneOf { + Route(.case(Self.create)) { + Path { "vendors"; "branches" } + Method.post + Body { + FormData { + Field("name", .string) + Field("vendorID") { Vendor.ID.parser() } + } + .map(.memberwise(VendorBranch.Create.init)) + } + } + Route(.case(Self.delete(id:))) { + Path { "vendors"; "branches"; VendorBranch.ID.parser() } + Method.delete + } + Route(.case(Self.select(context:))) { + Path { "vendors"; "branches"; "select" } + Method.get + Query { + Field("context") { SelectContext.parser() } } } - Route(.case(Self.shared)) { - SharedVendorBranchRoute.router - } } } } diff --git a/Sources/SharedModels/Vendor.swift b/Sources/SharedModels/Vendor.swift index 4cf0167..d02c3e9 100644 --- a/Sources/SharedModels/Vendor.swift +++ b/Sources/SharedModels/Vendor.swift @@ -34,9 +34,9 @@ public extension Vendor { } struct Update: Codable, Sendable { - public let name: String? + public let name: String - public init(name: String?) { + public init(name: String) { self.name = name } } diff --git a/justfile b/justfile index e3448b8..88356f2 100644 --- a/justfile +++ b/justfile @@ -12,3 +12,6 @@ rm-seed: run: ./swift-dev + +clean: + rm -rf .build