feat: Begins integrating database client into vapor app.

This commit is contained in:
2025-01-14 11:50:06 -05:00
parent c8bcffa0b5
commit ccf80f05a7
42 changed files with 2378 additions and 2540 deletions

View File

@@ -1,64 +1,64 @@
import Dependencies
import Fluent
import Vapor
struct EmployeeApiController: RouteCollection {
@Dependency(\.employees) var employees
func boot(routes: any RoutesBuilder) throws {
let protected = routes.apiProtected(route: "employees")
protected.get(use: index(req:))
protected.post(use: create(req:))
protected.group(":employeeID") {
$0.get(use: get(req:))
$0.put(use: update(req:))
$0.delete(use: delete(req:))
}
}
@Sendable
func index(req: Request) async throws -> [Employee.DTO] {
let params = try req.query.decode(EmployeesIndexQuery.self)
return try await employees.fetchAll(params.active == true ? .active : .default)
}
@Sendable
func create(req: Request) async throws -> Employee.DTO {
try await employees.create(
req.ensureValidContent(Employee.Create.self)
)
}
@Sendable
func get(req: Request) async throws -> Employee.DTO {
guard let id = req.parameters.get("employeeID", as: Employee.IDValue.self),
let employee = try await employees.get(id)
else {
throw Abort(.notFound)
}
return employee
}
@Sendable
func update(req: Request) async throws -> Employee.DTO {
guard let employeeID = req.parameters.get("employeeID", as: Employee.IDValue.self) else {
throw Abort(.badRequest, reason: "Employee id value not provided")
}
let updates = try req.ensureValidContent(Employee.Update.self)
return try await employees.update(employeeID, updates)
}
@Sendable
func delete(req: Request) async throws -> HTTPStatus {
guard let employeeID = req.parameters.get("employeeID", as: Employee.IDValue.self) else {
throw Abort(.badRequest, reason: "Employee id value not provided")
}
try await employees.delete(employeeID)
return .ok
}
}
struct EmployeesIndexQuery: Content {
let active: Bool?
}
// import Dependencies
// import Fluent
// import Vapor
//
// struct EmployeeApiController: RouteCollection {
//
// @Dependency(\.employees) var employees
//
// func boot(routes: any RoutesBuilder) throws {
// let protected = routes.apiProtected(route: "employees")
// protected.get(use: index(req:))
// protected.post(use: create(req:))
// protected.group(":employeeID") {
// $0.get(use: get(req:))
// $0.put(use: update(req:))
// $0.delete(use: delete(req:))
// }
// }
//
// @Sendable
// func index(req: Request) async throws -> [Employee.DTO] {
// let params = try req.query.decode(EmployeesIndexQuery.self)
// return try await employees.fetchAll(params.active == true ? .active : .default)
// }
//
// @Sendable
// func create(req: Request) async throws -> Employee.DTO {
// try await employees.create(
// req.ensureValidContent(Employee.Create.self)
// )
// }
//
// @Sendable
// func get(req: Request) async throws -> Employee.DTO {
// guard let id = req.parameters.get("employeeID", as: Employee.IDValue.self),
// let employee = try await employees.get(id)
// else {
// throw Abort(.notFound)
// }
// return employee
// }
//
// @Sendable
// func update(req: Request) async throws -> Employee.DTO {
// guard let employeeID = req.parameters.get("employeeID", as: Employee.IDValue.self) else {
// throw Abort(.badRequest, reason: "Employee id value not provided")
// }
// let updates = try req.ensureValidContent(Employee.Update.self)
// return try await employees.update(employeeID, updates)
// }
//
// @Sendable
// func delete(req: Request) async throws -> HTTPStatus {
// guard let employeeID = req.parameters.get("employeeID", as: Employee.IDValue.self) else {
// throw Abort(.badRequest, reason: "Employee id value not provided")
// }
// try await employees.delete(employeeID)
// return .ok
// }
// }
//
// struct EmployeesIndexQuery: Content {
// let active: Bool?
// }

View File

@@ -1,61 +1,61 @@
import Dependencies
import Fluent
import Vapor
// TODO: Add update route.
struct PurchaseOrderApiController: RouteCollection {
@Dependency(\.purchaseOrders) var purchaseOrders
func boot(routes: any RoutesBuilder) throws {
let protected = routes.apiProtected(route: "purchase-orders")
protected.get(use: index(req:))
protected.post(use: create(req:))
protected.group(":id") {
$0.get(use: get(req:))
$0.delete(use: delete(req:))
}
}
@Sendable
func index(req: Request) async throws -> [PurchaseOrder.DTO] {
try await purchaseOrders.fetchAll()
}
@Sendable
func create(req: Request) async throws -> PurchaseOrder.DTO {
try await purchaseOrders.create(
req.ensureValidContent(PurchaseOrder.Create.self),
req.auth.require(User.self).requireID()
)
}
@Sendable
func get(req: Request) async throws -> PurchaseOrder.DTO {
guard let id = req.parameters.get("id", as: PurchaseOrder.IDValue.self),
let purchaseOrder = try await purchaseOrders.get(id)
else {
throw Abort(.notFound)
}
return purchaseOrder
}
@Sendable
func delete(req: Request) async throws -> HTTPStatus {
guard let id = req.parameters.get("id", as: PurchaseOrder.IDValue.self) else {
throw Abort(.badRequest, reason: "Purchase order id not provided.")
}
try await purchaseOrders.delete(id)
return .ok
}
// @Sendable
// func update(req: Request) async throws -> PurchaseOrder.DTO {
// guard let id = req.parameters.get("id", as: PurchaseOrder.IDValue.self) else {
// throw Abort(.badRequest, reason: "Purchase order id not provided.")
// }
// try await purchaseOrders.delete(id: id, on: req.db)
// return .ok
// }
}
// import Dependencies
// import Fluent
// import Vapor
//
// // TODO: Add update route.
//
// struct PurchaseOrderApiController: RouteCollection {
//
// @Dependency(\.purchaseOrders) var purchaseOrders
//
// func boot(routes: any RoutesBuilder) throws {
// let protected = routes.apiProtected(route: "purchase-orders")
// protected.get(use: index(req:))
// protected.post(use: create(req:))
// protected.group(":id") {
// $0.get(use: get(req:))
// $0.delete(use: delete(req:))
// }
// }
//
// @Sendable
// func index(req: Request) async throws -> [PurchaseOrder.DTO] {
// try await purchaseOrders.fetchAll()
// }
//
// @Sendable
// func create(req: Request) async throws -> PurchaseOrder.DTO {
// try await purchaseOrders.create(
// req.ensureValidContent(PurchaseOrder.Create.self),
// req.auth.require(User.self).requireID()
// )
// }
//
// @Sendable
// func get(req: Request) async throws -> PurchaseOrder.DTO {
// guard let id = req.parameters.get("id", as: PurchaseOrder.IDValue.self),
// let purchaseOrder = try await purchaseOrders.get(id)
// else {
// throw Abort(.notFound)
// }
// return purchaseOrder
// }
//
// @Sendable
// func delete(req: Request) async throws -> HTTPStatus {
// guard let id = req.parameters.get("id", as: PurchaseOrder.IDValue.self) else {
// throw Abort(.badRequest, reason: "Purchase order id not provided.")
// }
// try await purchaseOrders.delete(id)
// return .ok
// }
//
// // @Sendable
// // func update(req: Request) async throws -> PurchaseOrder.DTO {
// // guard let id = req.parameters.get("id", as: PurchaseOrder.IDValue.self) else {
// // throw Abort(.badRequest, reason: "Purchase order id not provided.")
// // }
// // try await purchaseOrders.delete(id: id, on: req.db)
// // return .ok
// // }
// }

View File

@@ -1,11 +1,13 @@
import DatabaseClient
import Dependencies
import Fluent
import SharedModels
import Vapor
// TODO: Add update and get by id.
struct UserApiController: RouteCollection {
@Dependency(\.users) var users
@Dependency(\.database.users) var users
func boot(routes: any RoutesBuilder) throws {
let unProtected = routes.apiUnprotected(route: "users")
@@ -20,26 +22,28 @@ struct UserApiController: RouteCollection {
}
@Sendable
func index(req: Request) async throws -> [User.DTO] {
func index(req: Request) async throws -> [User] {
try await users.fetchAll()
}
@Sendable
func create(req: Request) async throws -> User.DTO {
func create(req: Request) async throws -> User {
// Allow the first user to be created without authentication.
let count = try await User.query(on: req.db).count()
// let count = try await User.query(on: req.db).count()
let count = try await users.count()
if count > 0 {
guard req.auth.get(User.self) != nil else {
throw Abort(.unauthorized)
}
}
return try await users.create(req.ensureValidContent(User.Create.self))
return try await users.create(req.content.decode(User.Create.self))
}
@Sendable
func login(req: Request) async throws -> UserToken {
func login(req: Request) async throws -> User {
let user = try req.auth.require(User.self)
return try await users.login(user)
return user
// return try await users.login(user)
}
// @Sendable
@@ -50,9 +54,10 @@ struct UserApiController: RouteCollection {
@Sendable
func delete(req: Request) async throws -> HTTPStatus {
guard let id = req.parameters.get("id", as: User.IDValue.self) else {
throw Abort(.badRequest, reason: "User id not provided")
}
// guard let id = req.parameters.get("id", as: User.IDValue.self) else {
// throw Abort(.badRequest, reason: "User id not provided")
// }
let id = try req.ensureIDPathComponent()
try await users.delete(id)
return .ok
}

View File

@@ -1,57 +1,57 @@
import Dependencies
import Fluent
import Vapor
struct VendorApiController: RouteCollection {
@Dependency(\.vendors) var vendors
func boot(routes: any RoutesBuilder) throws {
let protected = routes.apiProtected(route: "vendors")
protected.get(use: index(req:))
protected.post(use: create(req:))
protected.group(":id") {
$0.put(use: update(req:))
$0.delete(use: delete(req:))
}
}
@Sendable
func index(req: Request) async throws -> [Vendor.DTO] {
let params = try req.query.decode(VendorsIndexQuery.self)
return try await vendors.fetchAll(params.fetchRequest)
}
@Sendable
func create(req: Request) async throws -> Vendor.DTO {
try await vendors.create(req.ensureValidContent(Vendor.Create.self))
}
@Sendable
func update(req: Request) async throws -> Vendor.DTO {
guard let id = req.parameters.get("id", as: Vendor.IDValue.self) else {
throw Abort(.badRequest, reason: "Vendor id not provided.")
}
try Vendor.Update.validate(content: req)
let updates = try req.content.decode(Vendor.Update.self)
return try await vendors.update(id, updates)
}
@Sendable
func delete(req: Request) async throws -> HTTPStatus {
guard let id = req.parameters.get("id", as: Vendor.IDValue.self) else {
throw Abort(.badRequest, reason: "Vendor id not provided.")
}
try await vendors.delete(id)
return .ok
}
}
struct VendorsIndexQuery: Content {
let branches: Bool?
var fetchRequest: VendorDB.FetchRequest {
if branches == true { return .withBranches }
return .default
}
}
// import Dependencies
// import Fluent
// import Vapor
//
// struct VendorApiController: RouteCollection {
//
// @Dependency(\.vendors) var vendors
//
// func boot(routes: any RoutesBuilder) throws {
// let protected = routes.apiProtected(route: "vendors")
// protected.get(use: index(req:))
// protected.post(use: create(req:))
// protected.group(":id") {
// $0.put(use: update(req:))
// $0.delete(use: delete(req:))
// }
// }
//
// @Sendable
// func index(req: Request) async throws -> [Vendor.DTO] {
// let params = try req.query.decode(VendorsIndexQuery.self)
// return try await vendors.fetchAll(params.fetchRequest)
// }
//
// @Sendable
// func create(req: Request) async throws -> Vendor.DTO {
// try await vendors.create(req.ensureValidContent(Vendor.Create.self))
// }
//
// @Sendable
// func update(req: Request) async throws -> Vendor.DTO {
// guard let id = req.parameters.get("id", as: Vendor.IDValue.self) else {
// throw Abort(.badRequest, reason: "Vendor id not provided.")
// }
// try Vendor.Update.validate(content: req)
// let updates = try req.content.decode(Vendor.Update.self)
// return try await vendors.update(id, updates)
// }
//
// @Sendable
// func delete(req: Request) async throws -> HTTPStatus {
// guard let id = req.parameters.get("id", as: Vendor.IDValue.self) else {
// throw Abort(.badRequest, reason: "Vendor id not provided.")
// }
// try await vendors.delete(id)
// return .ok
// }
// }
//
// struct VendorsIndexQuery: Content {
// let branches: Bool?
//
// var fetchRequest: VendorDB.FetchRequest {
// if branches == true { return .withBranches }
// return .default
// }
// }

View File

@@ -1,67 +1,67 @@
import Dependencies
import Fluent
import Vapor
struct VendorBranchApiController: RouteCollection {
@Dependency(\.vendorBranches) var vendorBranches
func boot(routes: any RoutesBuilder) throws {
let prefix = routes.apiProtected(route: "vendors")
let root = prefix.grouped("branches")
root.get(use: index(req:))
root.group(":id") {
$0.put(use: update(req:))
$0.delete(use: delete(req:))
}
prefix.group(":vendorID", "branches") {
$0.get(use: indexForVendor(req:))
$0.post(use: create(req:))
}
}
@Sendable
func index(req: Request) async throws -> [VendorBranch.DTO] {
try await vendorBranches.fetchAll()
}
@Sendable
func indexForVendor(req: Request) async throws -> [VendorBranch.DTO] {
guard let id = req.parameters.get("vendorID", as: Vendor.IDValue.self) else {
throw Abort(.badRequest, reason: "Vendor id not provided.")
}
return try await vendorBranches.fetchAll(.for(vendorID: id))
}
@Sendable
func create(req: Request) async throws -> VendorBranch.DTO {
guard let id = req.parameters.get("vendorID", as: Vendor.IDValue.self) else {
throw Abort(.badRequest, reason: "Vendor id not provided.")
}
return try await vendorBranches.create(
req.ensureValidContent(VendorBranch.Create.self),
id
)
}
@Sendable
func update(req: Request) async throws -> VendorBranch.DTO {
guard let id = req.parameters.get("id", as: VendorBranch.IDValue.self) else {
throw Abort(.badRequest, reason: "Vendor branch id not provided.")
}
try VendorBranch.Update.validate(content: req)
let updates = try req.content.decode(VendorBranch.Update.self)
return try await vendorBranches.update(id, updates)
}
@Sendable
func delete(req: Request) async throws -> HTTPStatus {
guard let id = req.parameters.get("id", as: VendorBranch.IDValue.self) else {
throw Abort(.badRequest, reason: "Vendor branch id not provided.")
}
try await vendorBranches.delete(id)
return .ok
}
}
// import Dependencies
// import Fluent
// import Vapor
//
// struct VendorBranchApiController: RouteCollection {
//
// @Dependency(\.vendorBranches) var vendorBranches
//
// func boot(routes: any RoutesBuilder) throws {
// let prefix = routes.apiProtected(route: "vendors")
// let root = prefix.grouped("branches")
// root.get(use: index(req:))
// root.group(":id") {
// $0.put(use: update(req:))
// $0.delete(use: delete(req:))
// }
//
// prefix.group(":vendorID", "branches") {
// $0.get(use: indexForVendor(req:))
// $0.post(use: create(req:))
// }
// }
//
// @Sendable
// func index(req: Request) async throws -> [VendorBranch.DTO] {
// try await vendorBranches.fetchAll()
// }
//
// @Sendable
// func indexForVendor(req: Request) async throws -> [VendorBranch.DTO] {
// guard let id = req.parameters.get("vendorID", as: Vendor.IDValue.self) else {
// throw Abort(.badRequest, reason: "Vendor id not provided.")
// }
// return try await vendorBranches.fetchAll(.for(vendorID: id))
// }
//
// @Sendable
// func create(req: Request) async throws -> VendorBranch.DTO {
// guard let id = req.parameters.get("vendorID", as: Vendor.IDValue.self) else {
// throw Abort(.badRequest, reason: "Vendor id not provided.")
// }
// return try await vendorBranches.create(
// req.ensureValidContent(VendorBranch.Create.self),
// id
// )
// }
//
// @Sendable
// func update(req: Request) async throws -> VendorBranch.DTO {
// guard let id = req.parameters.get("id", as: VendorBranch.IDValue.self) else {
// throw Abort(.badRequest, reason: "Vendor branch id not provided.")
// }
// try VendorBranch.Update.validate(content: req)
// let updates = try req.content.decode(VendorBranch.Update.self)
// return try await vendorBranches.update(id, updates)
// }
//
// @Sendable
// func delete(req: Request) async throws -> HTTPStatus {
// guard let id = req.parameters.get("id", as: VendorBranch.IDValue.self) else {
// throw Abort(.badRequest, reason: "Vendor branch id not provided.")
// }
// try await vendorBranches.delete(id)
// return .ok
// }
//
// }

View File

@@ -3,10 +3,10 @@ import Vapor
struct ApiController: RouteCollection {
func boot(routes: any RoutesBuilder) throws {
try routes.register(collection: EmployeeApiController())
try routes.register(collection: PurchaseOrderApiController())
// try routes.register(collection: EmployeeApiController())
// try routes.register(collection: PurchaseOrderApiController())
try routes.register(collection: UserApiController())
try routes.register(collection: VendorApiController())
try routes.register(collection: VendorBranchApiController())
// try routes.register(collection: VendorApiController())
// try routes.register(collection: VendorBranchApiController())
}
}

View File

@@ -1,158 +1,158 @@
import Dependencies
import Fluent
import Leaf
import Vapor
struct EmployeeViewController: RouteCollection {
@Dependency(\.employees) var employees
func boot(routes: any RoutesBuilder) throws {
let protected = routes.protected.grouped("employees")
protected.get(use: index(req:))
protected.get("form", use: employeeForm(req:))
protected.post(use: create(req:))
protected.group(":employeeID") {
$0.get(use: get(req:))
$0.get("edit", use: edit(req:))
$0.delete(use: delete(req:))
$0.put(use: update(req:))
$0.patch("toggle-active", use: toggleActive(req:))
}
}
@Sendable
func index(req: Request) async throws -> View {
return try await renderIndex(req)
}
@Sendable
private func renderIndex(
_ req: Request,
_ employee: Employee.DTO? = nil,
_ form: EmployeeFormCTX? = nil
) async throws -> View {
return try await req.view.render(
"employees/index",
EmployeesCTX(employee: employee, employees: employees.fetchAll(), form: form ?? .init())
)
}
@Sendable
func create(req: Request) async throws -> View {
try Employee.Create.validate(content: req)
let employee = try await employees.create(req.content.decode(Employee.Create.self))
return try await req.view.render("employees/table-row", employee)
}
@Sendable
func get(req: Request) async throws -> View {
let employee = try await employees.get(req.ensureIDPathComponent(key: "employeeID"))
// Check if we've rendered the page yet.
guard req.isHtmxRequest else {
return try await renderIndex(req, employee)
}
return try await req.view.render("employees/detail", ["employee": employee])
}
@Sendable
func toggleActive(req: Request) async throws -> View {
guard let id = req.parameters.get("employeeID", as: Employee.IDValue.self) else {
throw Abort(.badRequest, reason: "Employee id not supplied.")
}
let employee = try await employees.toggleActive(id)
return try await req.view.render("employees/table-row", employee)
}
// TODO: I think we can just return a response and remove the table-row, here.
@Sendable
func delete(req: Request) async throws -> View {
let id = try req.requireEmployeeID()
_ = try await employees.delete(id)
let employees = try await employees.fetchAll()
return try await req.view.render("employees/table", ["employees": employees])
}
@Sendable
func edit(req: Request) async throws -> View {
guard let employee = try await employees.get(req.parameters.get("employeeID")) else {
throw Abort(.notFound)
}
return try await req.view.render("employees/detail", EmployeeDetailCTX(editing: true, employee: employee))
}
@Sendable
func update(req: Request) async throws -> View {
let id = try req.requireEmployeeID()
try Employee.Update.validate(content: req)
let updates = try req.content.decode(Employee.Update.self)
req.logger.info("Employee updates: \(updates)")
let employee = try await employees.update(id, updates)
req.logger.info("Done updating employee: \(employee)")
return try await req.view.render("employees/table-row", employee)
}
@Sendable
func employeeForm(req: Request) async throws -> View {
try await req.view.render("employees/form", EmployeeFormCTX())
}
}
private extension Request {
func requireEmployeeID() throws -> Employee.IDValue {
guard let id = parameters.get("employeeID", as: Employee.IDValue.self) else {
throw Abort(.badRequest, reason: "Employee id not supplied")
}
return id
}
}
private struct EmployeeDetailCTX: Content {
let editing: Bool
let employee: Employee.DTO?
init(editing: Bool = false, employee: Employee.DTO? = nil) {
self.editing = editing
self.employee = employee
}
}
private struct EmployeesCTX: Content {
let employee: Employee.DTO?
let employees: [Employee.DTO]
let form: EmployeeFormCTX
init(
employee: Employee.DTO? = nil,
employees: [Employee.DTO],
form: EmployeeFormCTX? = nil
) {
self.employee = employee
self.employees = employees
self.form = form ?? .init()
}
}
private struct EmployeeFormCTX: Content {
let htmxForm: HtmxFormCTX<Context>
init(employee: Employee.DTO? = nil) {
self.htmxForm = .init(
formClass: "employee-form",
formId: "employee-form",
htmxTargetUrl: employee?.id == nil ? .post("/employees") : .put("/employees/\(employee!.id!)"),
htmxTarget: "#employee-table",
htmxPushUrl: false,
htmxResetAfterRequest: true,
htmxSwapOob: nil,
htmxSwap: employee == nil ? .outerHTML : nil,
context: .init(employee: employee)
)
}
struct Context: Content {
let employee: Employee.DTO?
}
}
// import Dependencies
// import Fluent
// import Leaf
// import Vapor
//
// struct EmployeeViewController: RouteCollection {
//
// @Dependency(\.employees) var employees
//
// func boot(routes: any RoutesBuilder) throws {
// let protected = routes.protected.grouped("employees")
// protected.get(use: index(req:))
// protected.get("form", use: employeeForm(req:))
// protected.post(use: create(req:))
// protected.group(":employeeID") {
// $0.get(use: get(req:))
// $0.get("edit", use: edit(req:))
// $0.delete(use: delete(req:))
// $0.put(use: update(req:))
// $0.patch("toggle-active", use: toggleActive(req:))
// }
// }
//
// @Sendable
// func index(req: Request) async throws -> View {
// return try await renderIndex(req)
// }
//
// @Sendable
// private func renderIndex(
// _ req: Request,
// _ employee: Employee.DTO? = nil,
// _ form: EmployeeFormCTX? = nil
// ) async throws -> View {
// return try await req.view.render(
// "employees/index",
// EmployeesCTX(employee: employee, employees: employees.fetchAll(), form: form ?? .init())
// )
// }
//
// @Sendable
// func create(req: Request) async throws -> View {
// try Employee.Create.validate(content: req)
// let employee = try await employees.create(req.content.decode(Employee.Create.self))
// return try await req.view.render("employees/table-row", employee)
// }
//
// @Sendable
// func get(req: Request) async throws -> View {
// let employee = try await employees.get(req.ensureIDPathComponent(key: "employeeID"))
// // Check if we've rendered the page yet.
// guard req.isHtmxRequest else {
// return try await renderIndex(req, employee)
// }
// return try await req.view.render("employees/detail", ["employee": employee])
// }
//
// @Sendable
// func toggleActive(req: Request) async throws -> View {
// guard let id = req.parameters.get("employeeID", as: Employee.IDValue.self) else {
// throw Abort(.badRequest, reason: "Employee id not supplied.")
// }
// let employee = try await employees.toggleActive(id)
// return try await req.view.render("employees/table-row", employee)
// }
//
// // TODO: I think we can just return a response and remove the table-row, here.
// @Sendable
// func delete(req: Request) async throws -> View {
// let id = try req.requireEmployeeID()
// _ = try await employees.delete(id)
// let employees = try await employees.fetchAll()
// return try await req.view.render("employees/table", ["employees": employees])
// }
//
// @Sendable
// func edit(req: Request) async throws -> View {
// guard let employee = try await employees.get(req.parameters.get("employeeID")) else {
// throw Abort(.notFound)
// }
// return try await req.view.render("employees/detail", EmployeeDetailCTX(editing: true, employee: employee))
// }
//
// @Sendable
// func update(req: Request) async throws -> View {
// let id = try req.requireEmployeeID()
// try Employee.Update.validate(content: req)
// let updates = try req.content.decode(Employee.Update.self)
// req.logger.info("Employee updates: \(updates)")
// let employee = try await employees.update(id, updates)
// req.logger.info("Done updating employee: \(employee)")
// return try await req.view.render("employees/table-row", employee)
// }
//
// @Sendable
// func employeeForm(req: Request) async throws -> View {
// try await req.view.render("employees/form", EmployeeFormCTX())
// }
//
// }
//
// private extension Request {
// func requireEmployeeID() throws -> Employee.IDValue {
// guard let id = parameters.get("employeeID", as: Employee.IDValue.self) else {
// throw Abort(.badRequest, reason: "Employee id not supplied")
// }
// return id
// }
// }
//
// private struct EmployeeDetailCTX: Content {
// let editing: Bool
// let employee: Employee.DTO?
//
// init(editing: Bool = false, employee: Employee.DTO? = nil) {
// self.editing = editing
// self.employee = employee
// }
// }
//
// private struct EmployeesCTX: Content {
// let employee: Employee.DTO?
// let employees: [Employee.DTO]
// let form: EmployeeFormCTX
//
// init(
// employee: Employee.DTO? = nil,
// employees: [Employee.DTO],
// form: EmployeeFormCTX? = nil
// ) {
// self.employee = employee
// self.employees = employees
// self.form = form ?? .init()
// }
// }
//
// private struct EmployeeFormCTX: Content {
//
// let htmxForm: HtmxFormCTX<Context>
//
// init(employee: Employee.DTO? = nil) {
// self.htmxForm = .init(
// formClass: "employee-form",
// formId: "employee-form",
// htmxTargetUrl: employee?.id == nil ? .post("/employees") : .put("/employees/\(employee!.id!)"),
// htmxTarget: "#employee-table",
// htmxPushUrl: false,
// htmxResetAfterRequest: true,
// htmxSwapOob: nil,
// htmxSwap: employee == nil ? .outerHTML : nil,
// context: .init(employee: employee)
// )
// }
//
// struct Context: Content {
// let employee: Employee.DTO?
// }
// }

View File

@@ -1,196 +1,196 @@
import Dependencies
import Fluent
import Vapor
struct PurchaseOrderViewController: RouteCollection {
@Dependency(\.employees) var employees
@Dependency(\.purchaseOrders) var purchaseOrders
@Dependency(\.vendorBranches) var vendorBranches
func boot(routes: any RoutesBuilder) throws {
let pos = routes.protected.grouped("purchase-orders")
pos.get(use: index(req:))
pos.group("details", "close") {
$0.get(use: detailClose(req:))
}
pos.post(use: create(req:))
pos.group(":id") {
$0.get(use: detail(req:))
}
}
@Sendable
func index(req: Request) async throws -> View {
let params = try? req.query.decode(PurchaseOrderIndex.self)
let purchaseOrdersPage = try await purchaseOrders.fetchPage(
.init(page: params?.page ?? 1, per: params?.limit ?? 50)
)
let branches = try await vendorBranches.getBranches(req: req)
let employees = try await employees.fetchAll()
req.logger.debug("Branches: \(branches)")
return try await req.view.render(
"purchaseOrders/index",
PurchaseOrderCTX(
page: purchaseOrdersPage,
form: .create(branches: branches, employees: employees)
)
)
}
@Sendable
func detail(req: Request) async throws -> View {
guard let id = req.parameters.get("id", as: PurchaseOrder.IDValue.self) else {
throw Abort(.badRequest, reason: "Id not supplied.")
}
let purchaseOrder = try await purchaseOrders.get(id)
return try await req.view.render("purchaseOrders/detail", ["purchaseOrderDetail": purchaseOrder])
}
@Sendable
func detailClose(req: Request) async throws -> View {
return try await req.view.render("purchaseOrders/detail")
}
@Sendable
func create(req: Request) async throws -> View {
try PurchaseOrder.FormCreate.validate(content: req)
let createdById = try req.auth.require(User.self).requireID()
let create = try req.content.decode(PurchaseOrder.FormCreate.self).toCreate()
let purchaseOrder = try await purchaseOrders.create(create, createdById)
return try await req.view.render("purchaseOrders/table-row", purchaseOrder)
}
}
private struct PurchaseOrderIndex: Content {
let page: Int?
let limit: Int?
}
private struct PurchaseOrderCTX: Content {
let purchaseOrderDetail: PurchaseOrder.DTO?
let purchaseOrders: [PurchaseOrder.DTO]
let page: Int
let limit: Int
let hasNext: Bool
let hasPrevious: Bool
let form: PurchaseOrderFormCTX?
init(
detail: PurchaseOrder.DTO? = nil,
page: Page<PurchaseOrder.DTO>,
form: PurchaseOrderFormCTX?
) {
self.purchaseOrderDetail = detail
self.purchaseOrders = page.items
self.page = page.metadata.page
self.limit = page.metadata.per
self.hasNext = page.metadata.hasNext
self.hasPrevious = page.metadata.page > 1
self.form = form
}
}
private extension PageMetadata {
var hasNext: Bool {
total > (page * per)
}
}
private struct PurchaseOrderFormCTX: Content {
let htmxForm: HtmxFormCTX<Context>
struct Context: Content {
let branches: [VendorBranch.FormDTO]
let employees: [Employee.DTO]
}
static func create(branches: [VendorBranch.FormDTO], employees: [Employee.DTO]) -> Self {
.init(htmxForm: .init(
formClass: "po-form",
formId: "po-form",
htmxTargetUrl: .post("/purchase-orders"),
htmxTarget: "#po-table-body",
htmxPushUrl: false,
htmxResetAfterRequest: true,
htmxSwapOob: nil,
htmxSwap: .afterbegin,
context: .init(branches: branches, employees: employees)
))
}
}
extension VendorBranch {
struct FormDTO: Content {
let id: UUID
let name: String
let vendor: Vendor.DTO
}
func toFormDTO() throws -> VendorBranch.FormDTO {
try .init(
id: requireID(),
name: name,
vendor: vendor.toDTO()
)
}
}
private extension PurchaseOrder {
struct FormCreate: Content {
let id: Int?
let workOrder: String?
let materials: String
let customer: String
let truckStock: Bool?
let createdForID: Employee.IDValue
let vendorBranchID: VendorBranch.IDValue
// TODO: Remove.
func toModel(createdByID: User.IDValue) -> PurchaseOrder {
.init(
id: id,
workOrder: workOrder != nil ? (workOrder == "" ? nil : Int(workOrder!)) : nil,
materials: materials,
customer: customer,
truckStock: truckStock ?? false,
createdByID: createdByID,
createdForID: createdForID,
vendorBranchID: vendorBranchID,
createdAt: nil,
updatedAt: nil
)
}
func toCreate() -> PurchaseOrder.Create {
.init(
id: id,
workOrder: workOrder != nil ? (workOrder == "" ? nil : Int(workOrder!)) : nil,
materials: materials,
customer: customer,
truckStock: truckStock,
createdForID: createdForID,
vendorBranchID: vendorBranchID
)
}
}
}
private extension VendorBranchDB {
func getBranches(req: Request) async throws -> [VendorBranch.FormDTO] {
try await VendorBranch.query(on: req.db)
.with(\.$vendor)
.all()
.map { try $0.toFormDTO() }
}
}
extension PurchaseOrder.FormCreate: Validatable {
static func validations(_ validations: inout Validations) {
validations.add("materials", as: String.self, is: !.empty)
validations.add("customer", as: String.self, is: !.empty)
}
}
// import Dependencies
// import Fluent
// import Vapor
//
// struct PurchaseOrderViewController: RouteCollection {
// @Dependency(\.employees) var employees
// @Dependency(\.purchaseOrders) var purchaseOrders
// @Dependency(\.vendorBranches) var vendorBranches
//
// func boot(routes: any RoutesBuilder) throws {
// let pos = routes.protected.grouped("purchase-orders")
//
// pos.get(use: index(req:))
// pos.group("details", "close") {
// $0.get(use: detailClose(req:))
// }
// pos.post(use: create(req:))
// pos.group(":id") {
// $0.get(use: detail(req:))
// }
// }
//
// @Sendable
// func index(req: Request) async throws -> View {
// let params = try? req.query.decode(PurchaseOrderIndex.self)
// let purchaseOrdersPage = try await purchaseOrders.fetchPage(
// .init(page: params?.page ?? 1, per: params?.limit ?? 50)
// )
// let branches = try await vendorBranches.getBranches(req: req)
// let employees = try await employees.fetchAll()
// req.logger.debug("Branches: \(branches)")
// return try await req.view.render(
// "purchaseOrders/index",
// PurchaseOrderCTX(
// page: purchaseOrdersPage,
// form: .create(branches: branches, employees: employees)
// )
// )
// }
//
// @Sendable
// func detail(req: Request) async throws -> View {
// guard let id = req.parameters.get("id", as: PurchaseOrder.IDValue.self) else {
// throw Abort(.badRequest, reason: "Id not supplied.")
// }
// let purchaseOrder = try await purchaseOrders.get(id)
// return try await req.view.render("purchaseOrders/detail", ["purchaseOrderDetail": purchaseOrder])
// }
//
// @Sendable
// func detailClose(req: Request) async throws -> View {
// return try await req.view.render("purchaseOrders/detail")
// }
//
// @Sendable
// func create(req: Request) async throws -> View {
// try PurchaseOrder.FormCreate.validate(content: req)
// let createdById = try req.auth.require(User.self).requireID()
// let create = try req.content.decode(PurchaseOrder.FormCreate.self).toCreate()
// let purchaseOrder = try await purchaseOrders.create(create, createdById)
// return try await req.view.render("purchaseOrders/table-row", purchaseOrder)
// }
// }
//
// private struct PurchaseOrderIndex: Content {
// let page: Int?
// let limit: Int?
// }
//
// private struct PurchaseOrderCTX: Content {
// let purchaseOrderDetail: PurchaseOrder.DTO?
// let purchaseOrders: [PurchaseOrder.DTO]
// let page: Int
// let limit: Int
// let hasNext: Bool
// let hasPrevious: Bool
// let form: PurchaseOrderFormCTX?
//
// init(
// detail: PurchaseOrder.DTO? = nil,
// page: Page<PurchaseOrder.DTO>,
// form: PurchaseOrderFormCTX?
// ) {
// self.purchaseOrderDetail = detail
// self.purchaseOrders = page.items
// self.page = page.metadata.page
// self.limit = page.metadata.per
// self.hasNext = page.metadata.hasNext
// self.hasPrevious = page.metadata.page > 1
// self.form = form
// }
// }
//
// private extension PageMetadata {
// var hasNext: Bool {
// total > (page * per)
// }
// }
//
// private struct PurchaseOrderFormCTX: Content {
//
// let htmxForm: HtmxFormCTX<Context>
//
// struct Context: Content {
// let branches: [VendorBranch.FormDTO]
// let employees: [Employee.DTO]
// }
//
// static func create(branches: [VendorBranch.FormDTO], employees: [Employee.DTO]) -> Self {
// .init(htmxForm: .init(
// formClass: "po-form",
// formId: "po-form",
// htmxTargetUrl: .post("/purchase-orders"),
// htmxTarget: "#po-table-body",
// htmxPushUrl: false,
// htmxResetAfterRequest: true,
// htmxSwapOob: nil,
// htmxSwap: .afterbegin,
// context: .init(branches: branches, employees: employees)
// ))
// }
// }
//
// extension VendorBranch {
// struct FormDTO: Content {
// let id: UUID
// let name: String
// let vendor: Vendor.DTO
// }
//
// func toFormDTO() throws -> VendorBranch.FormDTO {
// try .init(
// id: requireID(),
// name: name,
// vendor: vendor.toDTO()
// )
// }
// }
//
// private extension PurchaseOrder {
// struct FormCreate: Content {
// let id: Int?
// let workOrder: String?
// let materials: String
// let customer: String
// let truckStock: Bool?
// let createdForID: Employee.IDValue
// let vendorBranchID: VendorBranch.IDValue
//
// // TODO: Remove.
// func toModel(createdByID: User.IDValue) -> PurchaseOrder {
// .init(
// id: id,
// workOrder: workOrder != nil ? (workOrder == "" ? nil : Int(workOrder!)) : nil,
// materials: materials,
// customer: customer,
// truckStock: truckStock ?? false,
// createdByID: createdByID,
// createdForID: createdForID,
// vendorBranchID: vendorBranchID,
// createdAt: nil,
// updatedAt: nil
// )
// }
//
// func toCreate() -> PurchaseOrder.Create {
// .init(
// id: id,
// workOrder: workOrder != nil ? (workOrder == "" ? nil : Int(workOrder!)) : nil,
// materials: materials,
// customer: customer,
// truckStock: truckStock,
// createdForID: createdForID,
// vendorBranchID: vendorBranchID
// )
// }
// }
// }
//
// private extension VendorBranchDB {
//
// func getBranches(req: Request) async throws -> [VendorBranch.FormDTO] {
// try await VendorBranch.query(on: req.db)
// .with(\.$vendor)
// .all()
// .map { try $0.toFormDTO() }
// }
// }
//
// extension PurchaseOrder.FormCreate: Validatable {
//
// static func validations(_ validations: inout Validations) {
// validations.add("materials", as: String.self, is: !.empty)
// validations.add("customer", as: String.self, is: !.empty)
// }
// }

View File

@@ -1,121 +1,121 @@
import Dependencies
import Fluent
import Vapor
struct UserViewController: RouteCollection {
@Dependency(\.users) var users
private let api = UserApiController()
func boot(routes: any RoutesBuilder) throws {
let users = routes.protected.grouped("users")
users.get(use: index(req:))
users.post(use: create(req:))
users.group(":id") {
$0.get(use: details(req:))
$0.delete(use: delete(req:))
}
}
@Sendable
func index(req: Request) async throws -> View {
try await renderIndex(req)
}
@Sendable
private func renderIndex(_ req: Request, _ user: User.DTO? = nil) async throws -> View {
let users = try await api.getSortedUsers(req: req)
return try await req.view.render("users/index", UsersCTX(user: user, users: users))
}
@Sendable
func create(req: Request) async throws -> View {
let user = try await api.create(req: req)
return try await req.view.render("users/table-row", user)
}
@Sendable
func details(req: Request) async throws -> View {
let user = try await users.get(req.ensureIDPathComponent())
// Check if the page has been rendered before.
guard req.isHtmxRequest else {
// Not an htmx-request, so render the whole page with the details.
return try await renderIndex(req, user)
}
// An htmx-request header was present, so just return the details,
return try await req.view.render("users/detail", ["user": user])
}
@Sendable
func delete(req: Request) async throws -> View {
_ = try await api.delete(req: req)
return try await req.view.render("users/table", ["users": api.getSortedUsers(req: req)])
}
}
struct UserFormCTX: Content {
let htmxForm: HtmxFormCTX<Context>
struct Context: Content {
let showConfirmPassword: Bool
let showEmailInput: Bool
let buttonLabel: String
}
static func signIn(next: String?) -> Self {
.init(
htmxForm: .init(
formClass: "user-form",
formId: "user-form",
htmxTargetUrl: .post("/login\((next != nil && next != "/") ? "?next=\(next!)" : "")"),
htmxTarget: "user-table",
htmxPushUrl: true,
htmxResetAfterRequest: true,
htmxSwapOob: nil,
htmxSwap: .afterbegin,
context: .init(showConfirmPassword: false, showEmailInput: false, buttonLabel: "Sign In")
)
)
}
static func create() -> Self {
.init(
htmxForm: .init(
formClass: "user-form",
formId: "user-form",
htmxTargetUrl: .post("/users"),
htmxTarget: "#user-table",
htmxPushUrl: false,
htmxResetAfterRequest: true,
htmxSwapOob: nil,
htmxSwap: nil,
context: .init(showConfirmPassword: true, showEmailInput: true, buttonLabel: "Create")
)
)
}
}
private struct UsersCTX: Content {
let user: User.DTO?
let users: [User.DTO]
let form: UserFormCTX
init(
user: User.DTO? = nil,
users: [User.DTO],
form: UserFormCTX? = nil
) {
self.user = user
self.users = users
self.form = form ?? .create()
}
}
private extension UserApiController {
func getSortedUsers(req: Request) async throws -> [User.DTO] {
try await index(req: req)
.sorted { ($0.username ?? "") < ($1.username ?? "") }
}
}
// import Dependencies
// import Fluent
// import Vapor
//
// struct UserViewController: RouteCollection {
//
// @Dependency(\.users) var users
//
// private let api = UserApiController()
//
// func boot(routes: any RoutesBuilder) throws {
// let users = routes.protected.grouped("users")
// users.get(use: index(req:))
// users.post(use: create(req:))
// users.group(":id") {
// $0.get(use: details(req:))
// $0.delete(use: delete(req:))
// }
// }
//
// @Sendable
// func index(req: Request) async throws -> View {
// try await renderIndex(req)
// }
//
// @Sendable
// private func renderIndex(_ req: Request, _ user: User.DTO? = nil) async throws -> View {
// let users = try await api.getSortedUsers(req: req)
// return try await req.view.render("users/index", UsersCTX(user: user, users: users))
// }
//
// @Sendable
// func create(req: Request) async throws -> View {
// let user = try await api.create(req: req)
// return try await req.view.render("users/table-row", user)
// }
//
// @Sendable
// func details(req: Request) async throws -> View {
// let user = try await users.get(req.ensureIDPathComponent())
// // Check if the page has been rendered before.
// guard req.isHtmxRequest else {
// // Not an htmx-request, so render the whole page with the details.
// return try await renderIndex(req, user)
// }
// // An htmx-request header was present, so just return the details,
// return try await req.view.render("users/detail", ["user": user])
// }
//
// @Sendable
// func delete(req: Request) async throws -> View {
// _ = try await api.delete(req: req)
// return try await req.view.render("users/table", ["users": api.getSortedUsers(req: req)])
// }
// }
//
// struct UserFormCTX: Content {
// let htmxForm: HtmxFormCTX<Context>
//
// struct Context: Content {
// let showConfirmPassword: Bool
// let showEmailInput: Bool
// let buttonLabel: String
// }
//
// static func signIn(next: String?) -> Self {
// .init(
// htmxForm: .init(
// formClass: "user-form",
// formId: "user-form",
// htmxTargetUrl: .post("/login\((next != nil && next != "/") ? "?next=\(next!)" : "")"),
// htmxTarget: "user-table",
// htmxPushUrl: true,
// htmxResetAfterRequest: true,
// htmxSwapOob: nil,
// htmxSwap: .afterbegin,
// context: .init(showConfirmPassword: false, showEmailInput: false, buttonLabel: "Sign In")
// )
// )
// }
//
// static func create() -> Self {
// .init(
// htmxForm: .init(
// formClass: "user-form",
// formId: "user-form",
// htmxTargetUrl: .post("/users"),
// htmxTarget: "#user-table",
// htmxPushUrl: false,
// htmxResetAfterRequest: true,
// htmxSwapOob: nil,
// htmxSwap: nil,
// context: .init(showConfirmPassword: true, showEmailInput: true, buttonLabel: "Create")
// )
// )
// }
// }
//
// private struct UsersCTX: Content {
// let user: User.DTO?
// let users: [User.DTO]
// let form: UserFormCTX
//
// init(
// user: User.DTO? = nil,
// users: [User.DTO],
// form: UserFormCTX? = nil
// ) {
// self.user = user
// self.users = users
// self.form = form ?? .create()
// }
// }
//
// private extension UserApiController {
//
// func getSortedUsers(req: Request) async throws -> [User.DTO] {
// try await index(req: req)
// .sorted { ($0.username ?? "") < ($1.username ?? "") }
// }
// }

View File

@@ -1,108 +1,108 @@
import Fluent
import Vapor
struct VendorViewController: RouteCollection {
private let api = VendorApiController()
func boot(routes: any RoutesBuilder) throws {
let vendors = routes.protected.grouped("vendors")
vendors.get(use: index(req:))
vendors.post(use: create(req:))
vendors.group(":vendorID") {
$0.delete(use: delete(req:))
$0.put(use: update(req:))
}
}
@Sendable
func index(req: Request) async throws -> View {
return try await req.view.render("vendors/index", makeCtx(req: req))
}
@Sendable
func create(req: Request) async throws -> View {
let ctx = try req.content.decode(CreateVendorCTX.self)
req.logger.debug("CTX: \(ctx)")
let vendor = Vendor.Create(name: ctx.name).toModel()
try await vendor.save(on: req.db)
if let branchString = ctx.branches {
let branches = branchString.split(separator: ",")
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
for branch in branches {
try await vendor.$branches.create(
VendorBranch(name: String(branch), vendorId: vendor.requireID()),
on: req.db
)
}
}
return try await req.view.render("vendors/table", makeCtx(req: req))
}
@Sendable
func delete(req: Request) async throws -> HTTPStatus {
try await api.delete(req: req)
}
@Sendable
func update(req: Request) async throws -> View {
_ = try await api.update(req: req)
return try await req.view.render("vendors/table", makeCtx(req: req, oob: true))
}
private func makeCtx(req: Request, vendor: Vendor? = nil, oob: Bool = false) async throws -> VendorsCTX {
let vendors = try await Vendor.query(on: req.db)
.with(\.$branches)
.sort(\.$name, .ascending)
.all()
.map { $0.toDTO() }
return .init(
vendors: vendors,
form: .init(vendor: vendor, oob: oob)
)
}
}
struct VendorFormCTX: Content {
let htmxForm: HtmxFormCTX<Context>
init(vendor: Vendor? = nil, oob: Bool = false) {
self.htmxForm = .init(
formClass: "vendor-form",
formId: "vendor-form",
htmxTargetUrl: vendor == nil ? .post("/vendors") : .put("/vendors"),
htmxTarget: "#vendor-table",
htmxPushUrl: false,
htmxResetAfterRequest: true,
htmxSwapOob: oob ? .outerHTML : nil,
htmxSwap: oob ? nil : .outerHTML,
context: .init(vendor: vendor)
)
}
struct Context: Content {
let vendor: Vendor?
let branches: String?
let buttonLabel: String
init(vendor: Vendor? = nil) {
self.vendor = vendor
self.branches = vendor?.branches.map(\.name).joined(separator: ", ")
self.buttonLabel = vendor == nil ? "Create" : "Update"
}
}
}
private struct VendorsCTX: Content {
let vendors: [Vendor.DTO]
let form: VendorFormCTX
}
private struct CreateVendorCTX: Content {
let name: String
let branches: String?
}
// import Fluent
// import Vapor
//
// struct VendorViewController: RouteCollection {
// private let api = VendorApiController()
//
// func boot(routes: any RoutesBuilder) throws {
// let vendors = routes.protected.grouped("vendors")
//
// vendors.get(use: index(req:))
// vendors.post(use: create(req:))
// vendors.group(":vendorID") {
// $0.delete(use: delete(req:))
// $0.put(use: update(req:))
// }
// }
//
// @Sendable
// func index(req: Request) async throws -> View {
// return try await req.view.render("vendors/index", makeCtx(req: req))
// }
//
// @Sendable
// func create(req: Request) async throws -> View {
// let ctx = try req.content.decode(CreateVendorCTX.self)
// req.logger.debug("CTX: \(ctx)")
// let vendor = Vendor.Create(name: ctx.name).toModel()
// try await vendor.save(on: req.db)
//
// if let branchString = ctx.branches {
// let branches = branchString.split(separator: ",")
// .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
//
// for branch in branches {
// try await vendor.$branches.create(
// VendorBranch(name: String(branch), vendorId: vendor.requireID()),
// on: req.db
// )
// }
// }
//
// return try await req.view.render("vendors/table", makeCtx(req: req))
// }
//
// @Sendable
// func delete(req: Request) async throws -> HTTPStatus {
// try await api.delete(req: req)
// }
//
// @Sendable
// func update(req: Request) async throws -> View {
// _ = try await api.update(req: req)
// return try await req.view.render("vendors/table", makeCtx(req: req, oob: true))
// }
//
// private func makeCtx(req: Request, vendor: Vendor? = nil, oob: Bool = false) async throws -> VendorsCTX {
// let vendors = try await Vendor.query(on: req.db)
// .with(\.$branches)
// .sort(\.$name, .ascending)
// .all()
// .map { $0.toDTO() }
//
// return .init(
// vendors: vendors,
// form: .init(vendor: vendor, oob: oob)
// )
// }
// }
//
// struct VendorFormCTX: Content {
// let htmxForm: HtmxFormCTX<Context>
//
// init(vendor: Vendor? = nil, oob: Bool = false) {
// self.htmxForm = .init(
// formClass: "vendor-form",
// formId: "vendor-form",
// htmxTargetUrl: vendor == nil ? .post("/vendors") : .put("/vendors"),
// htmxTarget: "#vendor-table",
// htmxPushUrl: false,
// htmxResetAfterRequest: true,
// htmxSwapOob: oob ? .outerHTML : nil,
// htmxSwap: oob ? nil : .outerHTML,
// context: .init(vendor: vendor)
// )
// }
//
// struct Context: Content {
// let vendor: Vendor?
// let branches: String?
// let buttonLabel: String
//
// init(vendor: Vendor? = nil) {
// self.vendor = vendor
// self.branches = vendor?.branches.map(\.name).joined(separator: ", ")
// self.buttonLabel = vendor == nil ? "Create" : "Update"
// }
// }
// }
//
// private struct VendorsCTX: Content {
// let vendors: [Vendor.DTO]
// let form: VendorFormCTX
// }
//
// private struct CreateVendorCTX: Content {
// let name: String
// let branches: String?
// }

View File

@@ -1,111 +1,111 @@
import Fluent
import Leaf
import Vapor
struct ViewController: RouteCollection {
private let api = ApiController()
private let employees = EmployeeViewController()
private let purchaseOrders = PurchaseOrderViewController()
private let users = UserViewController()
private let vendors = VendorViewController()
func boot(routes: any RoutesBuilder) throws {
let protected = routes.protected
// MARK: - Non-protected routes.
// routes.get(use: index(req:))
routes.get("login", use: getLogin(req:))
routes.post("login", use: postLogin(req:))
// MARK: Protected routes.
protected.get(use: home(req:))
protected.get("**", use: catchAll(req:))
protected.post("logout", use: logout(req:))
// protected.get("users", use: users(req:))
try routes.register(collection: employees)
try routes.register(collection: purchaseOrders)
try routes.register(collection: users)
try routes.register(collection: vendors)
}
@Sendable
func getLogin(req: Request) async throws -> View {
req.logger.debug("Login Query: \(req.url.query ?? "n/a")")
let params = try? req.query.decode(LoginParameter.self)
return try await req.view.render(
"login", UserFormCTX.signIn(next: params?.next)
)
}
@Sendable
func postLogin(req: Request) async throws -> Response {
let content = try req.content.decode(UserForm.self)
guard let user = try await User.query(on: req.db)
.filter(\.$username == content.username)
.first()
else {
throw Abort(.badRequest, reason: "User not found.")
}
guard try user.verify(password: content.password) else {
throw Abort(.unauthorized, reason: "Invalid password.")
}
req.auth.login(user)
req.logger.debug("User logged in: \(user.toDTO())")
return try await home(req: req)
}
@Sendable
func logout(req: Request) async throws -> View {
req.auth.logout(User.self)
return try await req.view.render("login")
}
@Sendable
func home(req: Request) async throws -> Response {
if let loginParams = try? req.query.decode(LoginParameter.self) {
return req.redirect(to: loginParams.next)
}
return try await req.view.render("home").encodeResponse(for: req)
}
@Sendable
func catchAll(req: Request) async throws -> View {
var route: HomeRoute?
if let loginParams = try? req.query.decode(LoginParameter.self),
let next = loginParams.next.split(separator: "/").last
{
route = HomeRoute(rawValue: String(next))
} else if let routeString = req.parameters.getCatchall().last {
route = HomeRoute(rawValue: routeString)
}
return try await req.view.render("home", HomeCTX(route: route))
}
}
private struct UserForm: Content {
let username: String
let password: String
}
enum HomeRoute: String, Content {
case employees
case purchaseOrders
case users
case vendors
}
struct HomeCTX: Content {
let route: HomeRoute?
}
struct LoginParameter: Content {
let next: String
}
// import Fluent
// import Leaf
// import Vapor
//
// struct ViewController: RouteCollection {
//
// private let api = ApiController()
// private let employees = EmployeeViewController()
// private let purchaseOrders = PurchaseOrderViewController()
// private let users = UserViewController()
// private let vendors = VendorViewController()
//
// func boot(routes: any RoutesBuilder) throws {
// let protected = routes.protected
//
// // MARK: - Non-protected routes.
//
// // routes.get(use: index(req:))
// routes.get("login", use: getLogin(req:))
// routes.post("login", use: postLogin(req:))
//
// // MARK: Protected routes.
//
// protected.get(use: home(req:))
// protected.get("**", use: catchAll(req:))
// protected.post("logout", use: logout(req:))
// // protected.get("users", use: users(req:))
// try routes.register(collection: employees)
// try routes.register(collection: purchaseOrders)
// try routes.register(collection: users)
// try routes.register(collection: vendors)
// }
//
// @Sendable
// func getLogin(req: Request) async throws -> View {
// req.logger.debug("Login Query: \(req.url.query ?? "n/a")")
// let params = try? req.query.decode(LoginParameter.self)
// return try await req.view.render(
// "login", UserFormCTX.signIn(next: params?.next)
// )
// }
//
// @Sendable
// func postLogin(req: Request) async throws -> Response {
// let content = try req.content.decode(UserForm.self)
// guard let user = try await User.query(on: req.db)
// .filter(\.$username == content.username)
// .first()
// else {
// throw Abort(.badRequest, reason: "User not found.")
// }
//
// guard try user.verify(password: content.password) else {
// throw Abort(.unauthorized, reason: "Invalid password.")
// }
// req.auth.login(user)
//
// req.logger.debug("User logged in: \(user.toDTO())")
// return try await home(req: req)
// }
//
// @Sendable
// func logout(req: Request) async throws -> View {
// req.auth.logout(User.self)
// return try await req.view.render("login")
// }
//
// @Sendable
// func home(req: Request) async throws -> Response {
// if let loginParams = try? req.query.decode(LoginParameter.self) {
// return req.redirect(to: loginParams.next)
// }
// return try await req.view.render("home").encodeResponse(for: req)
// }
//
// @Sendable
// func catchAll(req: Request) async throws -> View {
// var route: HomeRoute?
//
// if let loginParams = try? req.query.decode(LoginParameter.self),
// let next = loginParams.next.split(separator: "/").last
// {
// route = HomeRoute(rawValue: String(next))
// } else if let routeString = req.parameters.getCatchall().last {
// route = HomeRoute(rawValue: routeString)
// }
//
// return try await req.view.render("home", HomeCTX(route: route))
// }
//
// }
//
// private struct UserForm: Content {
// let username: String
// let password: String
// }
//
// enum HomeRoute: String, Content {
// case employees
// case purchaseOrders
// case users
// case vendors
// }
//
// struct HomeCTX: Content {
// let route: HomeRoute?
// }
//
// struct LoginParameter: Content {
// let next: String
// }

View File

@@ -1,88 +1,88 @@
import Dependencies
import DependenciesMacros
import Fluent
import Vapor
extension DependencyValues {
// An intermediate layer between our api and view controllers that interacts with the
// database model.
var employees: EmployeeDB {
get { self[EmployeeDB.self] }
set { self[EmployeeDB.self] = newValue }
}
}
@DependencyClient
struct EmployeeDB: Sendable {
var create: @Sendable (Employee.Create) async throws -> Employee.DTO
var fetchAll: @Sendable (FetchRequest) async throws -> [Employee.DTO]
var get: @Sendable (Employee.IDValue) async throws -> Employee.DTO?
var update: @Sendable (Employee.IDValue, Employee.Update) async throws -> Employee.DTO
var delete: @Sendable (Employee.IDValue) async throws -> Void
var toggleActive: @Sendable (Employee.IDValue) async throws -> Employee.DTO
enum FetchRequest {
case active
case `default`
}
func fetchAll() async throws -> [Employee.DTO] {
try await fetchAll(.default)
}
func get(_ id: String?) async throws -> Employee.DTO? {
guard let idString = id, let id = UUID(uuidString: idString) else {
throw Abort(.badRequest, reason: "Employee id not valid.")
}
return try await get(id)
}
}
extension EmployeeDB: TestDependencyKey {
static let testValue: EmployeeDB = Self()
static func live(database: any Database) -> Self {
.init(
create: { model in
let model = model.toModel()
try await model.save(on: database)
return model.toDTO()
},
fetchAll: { request in
var query = Employee.query(on: database)
.sort(\.$lastName)
if request == .active {
query = query.filter(\.$active == true)
}
return try await query.all().map { $0.toDTO() }
},
get: { id in
try await Employee.find(id, on: database).map { $0.toDTO() }
},
update: { id, updates in
guard let employee = try await Employee.find(id, on: database) else {
throw Abort(.badRequest, reason: "Employee id not found.")
}
employee.applyUpdates(updates)
try await employee.save(on: database)
return employee.toDTO()
},
delete: { id in
guard let employee = try await Employee.find(id, on: database) else {
throw Abort(.badRequest, reason: "Employee id not found.")
}
try await employee.delete(on: database)
},
toggleActive: { id in
guard let employee = try await Employee.find(id, on: database) else {
throw Abort(.notFound)
}
employee.active.toggle()
try await employee.save(on: database)
return employee.toDTO()
}
)
}
}
// import Dependencies
// import DependenciesMacros
// import Fluent
// import Vapor
//
// extension DependencyValues {
// // An intermediate layer between our api and view controllers that interacts with the
// // database model.
// var employees: EmployeeDB {
// get { self[EmployeeDB.self] }
// set { self[EmployeeDB.self] = newValue }
// }
// }
//
// @DependencyClient
// struct EmployeeDB: Sendable {
// var create: @Sendable (Employee.Create) async throws -> Employee.DTO
// var fetchAll: @Sendable (FetchRequest) async throws -> [Employee.DTO]
// var get: @Sendable (Employee.IDValue) async throws -> Employee.DTO?
// var update: @Sendable (Employee.IDValue, Employee.Update) async throws -> Employee.DTO
// var delete: @Sendable (Employee.IDValue) async throws -> Void
// var toggleActive: @Sendable (Employee.IDValue) async throws -> Employee.DTO
//
// enum FetchRequest {
// case active
// case `default`
// }
//
// func fetchAll() async throws -> [Employee.DTO] {
// try await fetchAll(.default)
// }
//
// func get(_ id: String?) async throws -> Employee.DTO? {
// guard let idString = id, let id = UUID(uuidString: idString) else {
// throw Abort(.badRequest, reason: "Employee id not valid.")
// }
// return try await get(id)
// }
// }
//
// extension EmployeeDB: TestDependencyKey {
// static let testValue: EmployeeDB = Self()
//
// static func live(database: any Database) -> Self {
// .init(
// create: { model in
// let model = model.toModel()
// try await model.save(on: database)
// return model.toDTO()
// },
// fetchAll: { request in
// var query = Employee.query(on: database)
// .sort(\.$lastName)
//
// if request == .active {
// query = query.filter(\.$active == true)
// }
//
// return try await query.all().map { $0.toDTO() }
// },
// get: { id in
// try await Employee.find(id, on: database).map { $0.toDTO() }
// },
// update: { id, updates in
// guard let employee = try await Employee.find(id, on: database) else {
// throw Abort(.badRequest, reason: "Employee id not found.")
// }
// employee.applyUpdates(updates)
// try await employee.save(on: database)
// return employee.toDTO()
// },
// delete: { id in
// guard let employee = try await Employee.find(id, on: database) else {
// throw Abort(.badRequest, reason: "Employee id not found.")
// }
// try await employee.delete(on: database)
// },
// toggleActive: { id in
// guard let employee = try await Employee.find(id, on: database) else {
// throw Abort(.notFound)
// }
// employee.active.toggle()
// try await employee.save(on: database)
// return employee.toDTO()
// }
// )
// }
// }

View File

@@ -1,87 +1,87 @@
import Dependencies
import DependenciesMacros
import Fluent
import Vapor
extension DependencyValues {
// An intermediate between our api and view controllers that interacts with the
// database.
var purchaseOrders: PurchaseOrdersDB {
get { self[PurchaseOrdersDB.self] }
set { self[PurchaseOrdersDB.self] = newValue }
}
}
@DependencyClient
struct PurchaseOrdersDB: Sendable {
var create: @Sendable (PurchaseOrder.Create, User.IDValue) async throws -> PurchaseOrder.DTO
var fetchAll: @Sendable () async throws -> [PurchaseOrder.DTO]
var fetchPage: @Sendable (PageRequest) async throws -> Page<PurchaseOrder.DTO>
var get: @Sendable (PurchaseOrder.IDValue) async throws -> PurchaseOrder.DTO?
// var update: @Sendable (PurchaseOrder.IDValue, PurchaseOrder.Update) async throws -> PurchaseOrder.DTO
var delete: @Sendable (PurchaseOrder.IDValue) async throws -> Void
}
extension PurchaseOrdersDB: TestDependencyKey {
static let testValue: PurchaseOrdersDB = Self()
static func live(database db: any Database) -> Self {
.init(
create: { model, createdById in
guard let employee = try await Employee.find(model.createdForID, on: db) else {
throw Abort(.notFound, reason: "Employee not found.")
}
guard employee.active else {
throw Abort(.badRequest, reason: "Employee is not active, unable to generate a PO for in-active employees")
}
let purchaseOrder = model.toModel(createdByID: createdById)
try await purchaseOrder.save(on: db)
guard let loaded = try await PurchaseOrder.get(purchaseOrder.requireID(), on: db) else {
return purchaseOrder.toDTO()
}
return loaded
},
fetchAll: {
try await PurchaseOrder.allQuery(on: db)
.sort(\.$id, .descending)
.all().map { $0.toDTO() }
},
fetchPage: { request in
try await PurchaseOrder.allQuery(on: db)
.sort(\.$id, .descending)
.paginate(request)
.map { $0.toDTO() }
},
get: { id in
try await PurchaseOrder.get(id, on: db)
},
delete: { id in
guard let purchaseOrder = try await PurchaseOrder.find(id, on: db) else {
throw Abort(.notFound)
}
try await purchaseOrder.delete(on: db)
}
)
}
}
private extension PurchaseOrder {
static func allQuery(on db: any Database) -> QueryBuilder<PurchaseOrder> {
PurchaseOrder.query(on: db)
.with(\.$createdBy)
.with(\.$createdFor)
.with(\.$vendorBranch) { branch in
branch.with(\.$vendor)
}
}
static func get(_ id: PurchaseOrder.IDValue, on db: any Database) async throws -> PurchaseOrder.DTO? {
try await PurchaseOrder.allQuery(on: db)
.filter(\.$id == id)
.first()?.toDTO()
}
}
// import Dependencies
// import DependenciesMacros
// import Fluent
// import Vapor
//
// extension DependencyValues {
// // An intermediate between our api and view controllers that interacts with the
// // database.
// var purchaseOrders: PurchaseOrdersDB {
// get { self[PurchaseOrdersDB.self] }
// set { self[PurchaseOrdersDB.self] = newValue }
// }
// }
//
// @DependencyClient
// struct PurchaseOrdersDB: Sendable {
// var create: @Sendable (PurchaseOrder.Create, User.IDValue) async throws -> PurchaseOrder.DTO
// var fetchAll: @Sendable () async throws -> [PurchaseOrder.DTO]
// var fetchPage: @Sendable (PageRequest) async throws -> Page<PurchaseOrder.DTO>
// var get: @Sendable (PurchaseOrder.IDValue) async throws -> PurchaseOrder.DTO?
// // var update: @Sendable (PurchaseOrder.IDValue, PurchaseOrder.Update) async throws -> PurchaseOrder.DTO
// var delete: @Sendable (PurchaseOrder.IDValue) async throws -> Void
// }
//
// extension PurchaseOrdersDB: TestDependencyKey {
// static let testValue: PurchaseOrdersDB = Self()
//
// static func live(database db: any Database) -> Self {
// .init(
// create: { model, createdById in
// guard let employee = try await Employee.find(model.createdForID, on: db) else {
// throw Abort(.notFound, reason: "Employee not found.")
// }
//
// guard employee.active else {
// throw Abort(.badRequest, reason: "Employee is not active, unable to generate a PO for in-active employees")
// }
//
// let purchaseOrder = model.toModel(createdByID: createdById)
// try await purchaseOrder.save(on: db)
// guard let loaded = try await PurchaseOrder.get(purchaseOrder.requireID(), on: db) else {
// return purchaseOrder.toDTO()
// }
// return loaded
//
// },
// fetchAll: {
// try await PurchaseOrder.allQuery(on: db)
// .sort(\.$id, .descending)
// .all().map { $0.toDTO() }
// },
// fetchPage: { request in
// try await PurchaseOrder.allQuery(on: db)
// .sort(\.$id, .descending)
// .paginate(request)
// .map { $0.toDTO() }
// },
// get: { id in
// try await PurchaseOrder.get(id, on: db)
// },
// delete: { id in
// guard let purchaseOrder = try await PurchaseOrder.find(id, on: db) else {
// throw Abort(.notFound)
// }
// try await purchaseOrder.delete(on: db)
// }
// )
// }
//
// }
//
// private extension PurchaseOrder {
// static func allQuery(on db: any Database) -> QueryBuilder<PurchaseOrder> {
// PurchaseOrder.query(on: db)
// .with(\.$createdBy)
// .with(\.$createdFor)
// .with(\.$vendorBranch) { branch in
// branch.with(\.$vendor)
// }
// }
//
// static func get(_ id: PurchaseOrder.IDValue, on db: any Database) async throws -> PurchaseOrder.DTO? {
// try await PurchaseOrder.allQuery(on: db)
// .filter(\.$id == id)
// .first()?.toDTO()
// }
// }

View File

@@ -1,60 +1,60 @@
import Dependencies
import DependenciesMacros
import Fluent
import Vapor
extension DependencyValues {
var users: UserDB {
get { self[UserDB.self] }
set { self[UserDB.self] = newValue }
}
}
@DependencyClient
struct UserDB: Sendable {
var create: @Sendable (User.Create) async throws -> User.DTO
var delete: @Sendable (User.IDValue) async throws -> Void
var fetchAll: @Sendable () async throws -> [User.DTO]
var get: @Sendable (User.IDValue) async throws -> User.DTO?
var login: @Sendable (User) async throws -> UserToken
}
extension UserDB: TestDependencyKey {
static let testValue: UserDB = Self()
static func live(database db: any Database) -> Self {
self.init(
create: { model in
guard model.password == model.confirmPassword else {
throw Abort(.badRequest, reason: "Passwords did not match.")
}
let user = try User(
username: model.username,
email: model.email,
passwordHash: Bcrypt.hash(model.password)
)
try await user.save(on: db)
return user.toDTO()
},
delete: { id in
guard let user = try await User.find(id, on: db) else {
throw Abort(.notFound)
}
try await user.delete(on: db)
},
fetchAll: {
try await User.query(on: db).all().map { $0.toDTO() }
},
get: { id in
try await User.find(id, on: db).map { $0.toDTO() }
},
login: { user in
let token = try user.generateToken()
try await token.save(on: db)
return token
}
)
}
}
// import Dependencies
// import DependenciesMacros
// import Fluent
// import Vapor
//
// extension DependencyValues {
// var users: UserDB {
// get { self[UserDB.self] }
// set { self[UserDB.self] = newValue }
// }
// }
//
// @DependencyClient
// struct UserDB: Sendable {
// var create: @Sendable (User.Create) async throws -> User.DTO
// var delete: @Sendable (User.IDValue) async throws -> Void
// var fetchAll: @Sendable () async throws -> [User.DTO]
// var get: @Sendable (User.IDValue) async throws -> User.DTO?
// var login: @Sendable (User) async throws -> UserToken
// }
//
// extension UserDB: TestDependencyKey {
// static let testValue: UserDB = Self()
//
// static func live(database db: any Database) -> Self {
// self.init(
// create: { model in
// guard model.password == model.confirmPassword else {
// throw Abort(.badRequest, reason: "Passwords did not match.")
// }
// let user = try User(
// username: model.username,
// email: model.email,
// passwordHash: Bcrypt.hash(model.password)
// )
// try await user.save(on: db)
// return user.toDTO()
//
// },
// delete: { id in
// guard let user = try await User.find(id, on: db) else {
// throw Abort(.notFound)
// }
// try await user.delete(on: db)
//
// },
// fetchAll: {
// try await User.query(on: db).all().map { $0.toDTO() }
// },
// get: { id in
// try await User.find(id, on: db).map { $0.toDTO() }
// },
// login: { user in
// let token = try user.generateToken()
// try await token.save(on: db)
// return token
// }
// )
// }
// }

View File

@@ -1,87 +1,87 @@
import Dependencies
import DependenciesMacros
import Fluent
import Vapor
public extension DependencyValues {
var vendorBranches: VendorBranchDB {
get { self[VendorBranchDB.self] }
set { self[VendorBranchDB.self] = newValue }
}
}
@DependencyClient
public struct VendorBranchDB: Sendable {
var create: @Sendable (VendorBranch.Create, Vendor.IDValue) async throws -> VendorBranch.DTO
var delete: @Sendable (VendorBranch.IDValue) async throws -> Void
var fetchAll: @Sendable (FetchRequest) async throws -> [VendorBranch.DTO]
var get: @Sendable (VendorBranch.IDValue) async throws -> VendorBranch.DTO?
var update: @Sendable (VendorBranch.IDValue, VendorBranch.Update) async throws -> VendorBranch.DTO
enum FetchRequest: Equatable {
case `default`
case `for`(vendorID: Vendor.IDValue)
case withVendor
}
func fetchAll() async throws -> [VendorBranch.DTO] {
try await fetchAll(.default)
}
}
extension VendorBranchDB: TestDependencyKey {
public static let testValue: VendorBranchDB = Self()
static func live(database db: any Database) -> Self {
.init(
create: { model, vendorID in
let branch = model.toModel()
guard let vendor = try await Vendor.find(vendorID, on: db) else {
throw Abort(.badRequest, reason: "Vendor does not exist.")
}
try await vendor.$branches.create(branch, on: db)
return branch.toDTO()
},
delete: { id in
guard let branch = try await VendorBranch.find(id, on: db) else {
throw Abort(.notFound)
}
try await branch.delete(on: db)
},
fetchAll: { request in
var query = VendorBranch.query(on: db)
switch request {
case .withVendor:
query = query.with(\.$vendor)
case let .for(vendorID: vendorID):
let branches = try await Vendor.query(on: db)
.filter(\.$id == vendorID)
.with(\.$branches)
.first()?
.branches
.map { $0.toDTO() }
guard let branches else { throw Abort(.badGateway, reason: "Vendor id not found.") }
return branches
case .default:
break
}
return try await query.all().map { $0.toDTO() }
},
get: { id in
try await VendorBranch.find(id, on: db).map { $0.toDTO() }
},
update: { id, updates in
guard let branch = try await VendorBranch.find(id, on: db) else {
throw Abort(.notFound)
}
branch.applyUpdates(updates)
try await branch.save(on: db)
return branch.toDTO()
}
)
}
}
// import Dependencies
// import DependenciesMacros
// import Fluent
// import Vapor
//
// public extension DependencyValues {
// var vendorBranches: VendorBranchDB {
// get { self[VendorBranchDB.self] }
// set { self[VendorBranchDB.self] = newValue }
// }
// }
//
// @DependencyClient
// public struct VendorBranchDB: Sendable {
// var create: @Sendable (VendorBranch.Create, Vendor.IDValue) async throws -> VendorBranch.DTO
// var delete: @Sendable (VendorBranch.IDValue) async throws -> Void
// var fetchAll: @Sendable (FetchRequest) async throws -> [VendorBranch.DTO]
// var get: @Sendable (VendorBranch.IDValue) async throws -> VendorBranch.DTO?
// var update: @Sendable (VendorBranch.IDValue, VendorBranch.Update) async throws -> VendorBranch.DTO
//
// enum FetchRequest: Equatable {
// case `default`
// case `for`(vendorID: Vendor.IDValue)
// case withVendor
// }
//
// func fetchAll() async throws -> [VendorBranch.DTO] {
// try await fetchAll(.default)
// }
// }
//
// extension VendorBranchDB: TestDependencyKey {
// public static let testValue: VendorBranchDB = Self()
//
// static func live(database db: any Database) -> Self {
// .init(
// create: { model, vendorID in
// let branch = model.toModel()
// guard let vendor = try await Vendor.find(vendorID, on: db) else {
// throw Abort(.badRequest, reason: "Vendor does not exist.")
// }
// try await vendor.$branches.create(branch, on: db)
// return branch.toDTO()
// },
// delete: { id in
// guard let branch = try await VendorBranch.find(id, on: db) else {
// throw Abort(.notFound)
// }
// try await branch.delete(on: db)
// },
// fetchAll: { request in
// var query = VendorBranch.query(on: db)
// switch request {
// case .withVendor:
// query = query.with(\.$vendor)
//
// case let .for(vendorID: vendorID):
// let branches = try await Vendor.query(on: db)
// .filter(\.$id == vendorID)
// .with(\.$branches)
// .first()?
// .branches
// .map { $0.toDTO() }
//
// guard let branches else { throw Abort(.badGateway, reason: "Vendor id not found.") }
// return branches
//
// case .default:
// break
// }
// return try await query.all().map { $0.toDTO() }
//
// },
// get: { id in
// try await VendorBranch.find(id, on: db).map { $0.toDTO() }
// },
// update: { id, updates in
// guard let branch = try await VendorBranch.find(id, on: db) else {
// throw Abort(.notFound)
// }
// branch.applyUpdates(updates)
// try await branch.save(on: db)
// return branch.toDTO()
// }
// )
// }
// }

View File

@@ -1,85 +1,85 @@
import Dependencies
import DependenciesMacros
import Fluent
import Vapor
public extension DependencyValues {
var vendors: VendorDB {
get { self[VendorDB.self] }
set { self[VendorDB.self] = newValue }
}
}
@DependencyClient
public struct VendorDB: Sendable {
var create: @Sendable (Vendor.Create) async throws -> Vendor.DTO
var delete: @Sendable (Vendor.IDValue) async throws -> Void
var fetchAll: @Sendable (FetchRequest) async throws -> [Vendor.DTO]
var get: @Sendable (Vendor.IDValue, GetRequest) async throws -> Vendor.DTO?
var update: @Sendable (Vendor.IDValue, Vendor.Update) async throws -> Vendor.DTO
enum FetchRequest {
case `default`
case withBranches
}
enum GetRequest {
case `default`
case withBranches
}
func fetchAll() async throws -> [Vendor.DTO] {
try await fetchAll(.default)
}
func get(_ id: Vendor.IDValue) async throws -> Vendor.DTO? {
try await get(id, .default)
}
}
extension VendorDB: TestDependencyKey {
public static let testValue: VendorDB = Self()
static func live(database db: any Database) -> Self {
.init(
create: { model in
let model = model.toModel()
try await model.save(on: db)
return model.toDTO()
},
delete: { id in
guard let vendor = try await Vendor.find(id, on: db) else {
throw Abort(.notFound)
}
try await vendor.delete(on: db)
},
fetchAll: { request in
var query = Vendor.query(on: db).sort(\.$name, .ascending)
let withBranches = request == .withBranches
if withBranches {
query = query.with(\.$branches)
}
return try await query.all().map { $0.toDTO(includeBranches: withBranches) }
},
get: { id, request in
var query = Vendor.query(on: db).filter(\.$id == id)
let withBranches = request == .withBranches
if withBranches {
query = query.with(\.$branches)
}
return try await query.first().map { $0.toDTO(includeBranches: withBranches) }
},
update: { id, updates in
guard let vendor = try await Vendor.find(id, on: db) else {
throw Abort(.notFound)
}
vendor.applyUpdates(updates)
return vendor.toDTO()
}
)
}
}
// import Dependencies
// import DependenciesMacros
// import Fluent
// import Vapor
//
// public extension DependencyValues {
// var vendors: VendorDB {
// get { self[VendorDB.self] }
// set { self[VendorDB.self] = newValue }
// }
// }
//
// @DependencyClient
// public struct VendorDB: Sendable {
// var create: @Sendable (Vendor.Create) async throws -> Vendor.DTO
// var delete: @Sendable (Vendor.IDValue) async throws -> Void
// var fetchAll: @Sendable (FetchRequest) async throws -> [Vendor.DTO]
// var get: @Sendable (Vendor.IDValue, GetRequest) async throws -> Vendor.DTO?
// var update: @Sendable (Vendor.IDValue, Vendor.Update) async throws -> Vendor.DTO
//
// enum FetchRequest {
// case `default`
// case withBranches
// }
//
// enum GetRequest {
// case `default`
// case withBranches
// }
//
// func fetchAll() async throws -> [Vendor.DTO] {
// try await fetchAll(.default)
// }
//
// func get(_ id: Vendor.IDValue) async throws -> Vendor.DTO? {
// try await get(id, .default)
// }
// }
//
// extension VendorDB: TestDependencyKey {
// public static let testValue: VendorDB = Self()
//
// static func live(database db: any Database) -> Self {
// .init(
// create: { model in
// let model = model.toModel()
// try await model.save(on: db)
// return model.toDTO()
//
// },
// delete: { id in
// guard let vendor = try await Vendor.find(id, on: db) else {
// throw Abort(.notFound)
// }
// try await vendor.delete(on: db)
//
// },
// fetchAll: { request in
// var query = Vendor.query(on: db).sort(\.$name, .ascending)
// let withBranches = request == .withBranches
// if withBranches {
// query = query.with(\.$branches)
// }
// return try await query.all().map { $0.toDTO(includeBranches: withBranches) }
//
// },
// get: { id, request in
// var query = Vendor.query(on: db).filter(\.$id == id)
// let withBranches = request == .withBranches
// if withBranches {
// query = query.with(\.$branches)
// }
// return try await query.first().map { $0.toDTO(includeBranches: withBranches) }
//
// },
// update: { id, updates in
// guard let vendor = try await Vendor.find(id, on: db) else {
// throw Abort(.notFound)
// }
// vendor.applyUpdates(updates)
// return vendor.toDTO()
// }
// )
// }
// }

View File

@@ -1,3 +1,5 @@
import DatabaseClientLive
import SharedModels
import Vapor
extension RoutesBuilder {
@@ -5,16 +7,19 @@ extension RoutesBuilder {
// Used to ensure views are protected, redirects users to the login page if they're
// not authenticated.
var protected: any RoutesBuilder {
#if DEBUG
return self
#else
return grouped(
User.credentialsAuthenticator(),
User.redirectMiddleware { req in
"login?next=\(req.url)"
}
)
#endif
// #if DEBUG
// return self
// #else
return grouped(
// User.credentialsAuthenticator(),
UserPasswordAuthenticator(),
UserTokenAuthenticator(),
UserSessionAuthenticator(),
User.redirectMiddleware { req in
"login?next=\(req.url)"
}
)
// #endif
}
func apiUnprotected(route: PathComponent) -> any RoutesBuilder {
@@ -24,15 +29,17 @@ extension RoutesBuilder {
// Allows basic or token authentication for api routes and prefixes the
// given route with "/api/v1".
func apiProtected(route: PathComponent) -> any RoutesBuilder {
#if DEBUG
return apiUnprotected(route: route)
#else
let prefixed = grouped("api", "v1", route)
return prefixed.grouped(
User.authenticator(),
UserToken.authenticator(),
User.guardMiddleware()
)
#endif
// #if DEBUG
// return apiUnprotected(route: route)
// #else
let prefixed = grouped("api", "v1", route)
return prefixed.grouped(
UserPasswordAuthenticator(),
UserTokenAuthenticator(),
// User.authenticator(),
// UserToken.authenticator(),
User.guardMiddleware()
)
// #endif
}
}

View File

@@ -1,165 +1,165 @@
import Fluent
import struct Foundation.UUID
import Vapor
// TODO: Add soft-delete??
/// The employee database model.
///
/// An employee is someone that PO's can be generated for. They can be either a field
/// employee / technician, an office employee, or an administrator.
///
/// # NOTE: Only `User` types can login and generate po's for employees.
///
final class Employee: Model, @unchecked Sendable {
static let schema = "employee"
@ID(key: .id)
var id: UUID?
@Field(key: "first_name")
var firstName: String
@Field(key: "last_name")
var lastName: String
@Field(key: "is_active")
var active: Bool
@Timestamp(key: "created_at", on: .create)
var createdAt: Date?
@Timestamp(key: "updated_at", on: .update)
var updatedAt: Date?
init() {}
init(
id: UUID? = nil,
firstName: String,
lastName: String,
active: Bool,
createdAt: Date? = nil,
updatedAt: Date? = nil
) {
self.id = id
self.firstName = firstName
self.lastName = lastName
self.active = active
self.createdAt = createdAt
self.updatedAt = updatedAt
}
func toDTO() -> DTO {
.init(
id: id,
firstName: $firstName.value,
lastName: $lastName.value,
active: $active.value,
createdAt: createdAt,
updatedAt: updatedAt
)
}
func applyUpdates(_ updates: Update) {
if let firstName = updates.firstName {
self.firstName = firstName
}
if let lastName = updates.lastName {
self.lastName = lastName
}
if let active = updates.active {
self.active = active
}
}
}
// MARK: - Helpers
extension Employee {
struct Create: Content {
let firstName: String
let lastName: String
let active: Bool?
func toModel() -> Employee {
.init(
firstName: firstName,
lastName: lastName,
active: active ?? true
)
}
}
struct DTO: Content {
var id: UUID?
var firstName: String?
var lastName: String?
var active: Bool?
var createdAt: Date?
var updatedAt: Date?
func toModel() -> Employee {
let model = Employee()
model.id = id
if let firstName {
model.firstName = firstName
}
if let lastName {
model.lastName = lastName
}
if let active {
model.active = active
}
return model
}
}
struct Migrate: AsyncMigration {
let name = "CreateEmployee"
func prepare(on database: any Database) async throws {
try await database.schema(Employee.schema)
.id()
.field("first_name", .string, .required)
.field("last_name", .string, .required)
.field("is_active", .bool, .required, .sql(.default(true)))
.field("created_at", .datetime)
.field("updated_at", .datetime)
.unique(on: "first_name", "last_name")
.create()
}
func revert(on database: any Database) async throws {
try await database.schema(Employee.schema).delete()
}
}
struct Update: Content {
var firstName: String?
var lastName: String?
var active: Bool?
}
}
// MARK: - Validations
extension Employee.Create: Validatable {
static func validations(_ validations: inout Validations) {
validations.add("firstName", as: String.self, is: !.empty)
validations.add("lastName", as: String.self, is: !.empty)
}
}
extension Employee.Update: Validatable {
static func validations(_ validations: inout Validations) {
validations.add("firstName", as: String?.self, is: .nil || !.empty, required: false)
validations.add("lastName", as: String?.self, is: .nil || !.empty, required: false)
}
}
// import Fluent
// import struct Foundation.UUID
// import Vapor
//
// // TODO: Add soft-delete??
//
// /// The employee database model.
// ///
// /// An employee is someone that PO's can be generated for. They can be either a field
// /// employee / technician, an office employee, or an administrator.
// ///
// /// # NOTE: Only `User` types can login and generate po's for employees.
// ///
// final class Employee: Model, @unchecked Sendable {
//
// static let schema = "employee"
//
// @ID(key: .id)
// var id: UUID?
//
// @Field(key: "first_name")
// var firstName: String
//
// @Field(key: "last_name")
// var lastName: String
//
// @Field(key: "is_active")
// var active: Bool
//
// @Timestamp(key: "created_at", on: .create)
// var createdAt: Date?
//
// @Timestamp(key: "updated_at", on: .update)
// var updatedAt: Date?
//
// init() {}
//
// init(
// id: UUID? = nil,
// firstName: String,
// lastName: String,
// active: Bool,
// createdAt: Date? = nil,
// updatedAt: Date? = nil
// ) {
// self.id = id
// self.firstName = firstName
// self.lastName = lastName
// self.active = active
// self.createdAt = createdAt
// self.updatedAt = updatedAt
// }
//
// func toDTO() -> DTO {
// .init(
// id: id,
// firstName: $firstName.value,
// lastName: $lastName.value,
// active: $active.value,
// createdAt: createdAt,
// updatedAt: updatedAt
// )
// }
//
// func applyUpdates(_ updates: Update) {
// if let firstName = updates.firstName {
// self.firstName = firstName
// }
// if let lastName = updates.lastName {
// self.lastName = lastName
// }
// if let active = updates.active {
// self.active = active
// }
// }
// }
//
// // MARK: - Helpers
//
// extension Employee {
//
// struct Create: Content {
// let firstName: String
// let lastName: String
// let active: Bool?
//
// func toModel() -> Employee {
// .init(
// firstName: firstName,
// lastName: lastName,
// active: active ?? true
// )
// }
// }
//
// struct DTO: Content {
//
// var id: UUID?
// var firstName: String?
// var lastName: String?
// var active: Bool?
// var createdAt: Date?
// var updatedAt: Date?
//
// func toModel() -> Employee {
// let model = Employee()
//
// model.id = id
// if let firstName {
// model.firstName = firstName
// }
// if let lastName {
// model.lastName = lastName
// }
// if let active {
// model.active = active
// }
// return model
// }
// }
//
// struct Migrate: AsyncMigration {
//
// let name = "CreateEmployee"
//
// func prepare(on database: any Database) async throws {
// try await database.schema(Employee.schema)
// .id()
// .field("first_name", .string, .required)
// .field("last_name", .string, .required)
// .field("is_active", .bool, .required, .sql(.default(true)))
// .field("created_at", .datetime)
// .field("updated_at", .datetime)
// .unique(on: "first_name", "last_name")
// .create()
// }
//
// func revert(on database: any Database) async throws {
// try await database.schema(Employee.schema).delete()
// }
//
// }
//
// struct Update: Content {
// var firstName: String?
// var lastName: String?
// var active: Bool?
// }
// }
//
// // MARK: - Validations
//
// extension Employee.Create: Validatable {
// static func validations(_ validations: inout Validations) {
// validations.add("firstName", as: String.self, is: !.empty)
// validations.add("lastName", as: String.self, is: !.empty)
// }
// }
//
// extension Employee.Update: Validatable {
// static func validations(_ validations: inout Validations) {
// validations.add("firstName", as: String?.self, is: .nil || !.empty, required: false)
// validations.add("lastName", as: String?.self, is: .nil || !.empty, required: false)
// }
// }

View File

@@ -1,156 +1,156 @@
import Fluent
import Vapor
/// The purchase order database model.
///
/// # NOTE: An initial purchase order should be created with an `id` higher than our current PO
/// so that subsequent PO's are generated with higher values than our current system produces.
/// once the first one is set, the rest will auto-increment from there.
final class PurchaseOrder: Model, Content, @unchecked Sendable {
static let schema = "purchase_order"
@ID(custom: "id", generatedBy: .database)
var id: Int?
@Field(key: "work_order")
var workOrder: Int?
@Field(key: "materials")
var materials: String
@Field(key: "customer")
var customer: String
@Field(key: "truck_stock")
var truckStock: Bool
@Parent(key: "created_by_id")
var createdBy: User
@Parent(key: "created_for_id")
var createdFor: Employee
@Parent(key: "vendor_branch_id")
var vendorBranch: VendorBranch
@Timestamp(key: "created_at", on: .create)
var createdAt: Date?
@Timestamp(key: "updated_at", on: .update)
var updatedAt: Date?
init() {}
init(
id: Int? = nil,
workOrder: Int? = nil,
materials: String,
customer: String,
truckStock: Bool,
createdByID: User.IDValue,
createdForID: Employee.IDValue,
vendorBranchID: VendorBranch.IDValue,
createdAt: Date? = nil,
updatedAt: Date? = nil
) {
self.id = id
self.workOrder = workOrder
self.materials = materials
self.customer = customer
self.truckStock = truckStock
$createdBy.id = createdByID
$createdFor.id = createdForID
$vendorBranch.id = vendorBranchID
self.createdAt = createdAt
self.updatedAt = updatedAt
}
func toDTO() -> DTO {
.init(
id: id,
workOrder: workOrder,
materials: materials,
customer: customer,
truckStock: truckStock,
createdBy: $createdBy.value?.toDTO(),
createdFor: $createdFor.value?.toDTO(),
vendorBranch: $vendorBranch.value,
createdAt: createdAt,
updatedAt: updatedAt
)
}
}
extension PurchaseOrder {
struct Create: Content {
let id: Int?
let workOrder: Int?
let materials: String
let customer: String
let truckStock: Bool?
let createdForID: Employee.IDValue
let vendorBranchID: VendorBranch.IDValue
func toModel(createdByID: User.IDValue) -> PurchaseOrder {
.init(
id: id,
workOrder: workOrder,
materials: materials,
customer: customer,
truckStock: truckStock ?? false,
createdByID: createdByID,
createdForID: createdForID,
vendorBranchID: vendorBranchID,
createdAt: nil,
updatedAt: nil
)
}
}
struct DTO: Content {
let id: Int?
let workOrder: Int?
let materials: String
let customer: String
let truckStock: Bool
let createdBy: User.DTO?
let createdFor: Employee.DTO?
let vendorBranch: VendorBranch?
let createdAt: Date?
let updatedAt: Date?
}
struct Migrate: AsyncMigration {
let name = "CreatePurchaseOrder"
func prepare(on database: any Database) async throws {
try await database.schema(PurchaseOrder.schema)
.field("id", .int, .identifier(auto: true))
.field("work_order", .int)
.field("customer", .string, .required)
.field("materials", .string, .required)
.field("truck_stock", .bool, .required)
.field("created_by_id", .uuid, .required, .references(User.schema, "id"))
.field("created_for_id", .uuid, .required, .references(Employee.schema, "id"))
.field("vendor_branch_id", .uuid, .required, .references(VendorBranch.schema, "id"))
.field("created_at", .datetime)
.field("updated_at", .datetime)
.create()
}
func revert(on database: any Database) async throws {
try await database.schema(PurchaseOrder.schema).delete()
}
}
}
extension PurchaseOrder.Create: Validatable {
static func validations(_ validations: inout Validations) {
validations.add("materials", as: String.self, is: !.empty)
validations.add("customer", as: String.self, is: !.empty)
}
}
// import Fluent
// import Vapor
//
// /// The purchase order database model.
// ///
// /// # NOTE: An initial purchase order should be created with an `id` higher than our current PO
// /// so that subsequent PO's are generated with higher values than our current system produces.
// /// once the first one is set, the rest will auto-increment from there.
// final class PurchaseOrder: Model, Content, @unchecked Sendable {
// static let schema = "purchase_order"
//
// @ID(custom: "id", generatedBy: .database)
// var id: Int?
//
// @Field(key: "work_order")
// var workOrder: Int?
//
// @Field(key: "materials")
// var materials: String
//
// @Field(key: "customer")
// var customer: String
//
// @Field(key: "truck_stock")
// var truckStock: Bool
//
// @Parent(key: "created_by_id")
// var createdBy: User
//
// @Parent(key: "created_for_id")
// var createdFor: Employee
//
// @Parent(key: "vendor_branch_id")
// var vendorBranch: VendorBranch
//
// @Timestamp(key: "created_at", on: .create)
// var createdAt: Date?
//
// @Timestamp(key: "updated_at", on: .update)
// var updatedAt: Date?
//
// init() {}
//
// init(
// id: Int? = nil,
// workOrder: Int? = nil,
// materials: String,
// customer: String,
// truckStock: Bool,
// createdByID: User.IDValue,
// createdForID: Employee.IDValue,
// vendorBranchID: VendorBranch.IDValue,
// createdAt: Date? = nil,
// updatedAt: Date? = nil
// ) {
// self.id = id
// self.workOrder = workOrder
// self.materials = materials
// self.customer = customer
// self.truckStock = truckStock
// $createdBy.id = createdByID
// $createdFor.id = createdForID
// $vendorBranch.id = vendorBranchID
// self.createdAt = createdAt
// self.updatedAt = updatedAt
// }
//
// func toDTO() -> DTO {
// .init(
// id: id,
// workOrder: workOrder,
// materials: materials,
// customer: customer,
// truckStock: truckStock,
// createdBy: $createdBy.value?.toDTO(),
// createdFor: $createdFor.value?.toDTO(),
// vendorBranch: $vendorBranch.value,
// createdAt: createdAt,
// updatedAt: updatedAt
// )
// }
//
// }
//
// extension PurchaseOrder {
//
// struct Create: Content {
// let id: Int?
// let workOrder: Int?
// let materials: String
// let customer: String
// let truckStock: Bool?
// let createdForID: Employee.IDValue
// let vendorBranchID: VendorBranch.IDValue
//
// func toModel(createdByID: User.IDValue) -> PurchaseOrder {
// .init(
// id: id,
// workOrder: workOrder,
// materials: materials,
// customer: customer,
// truckStock: truckStock ?? false,
// createdByID: createdByID,
// createdForID: createdForID,
// vendorBranchID: vendorBranchID,
// createdAt: nil,
// updatedAt: nil
// )
// }
// }
//
// struct DTO: Content {
// let id: Int?
// let workOrder: Int?
// let materials: String
// let customer: String
// let truckStock: Bool
// let createdBy: User.DTO?
// let createdFor: Employee.DTO?
// let vendorBranch: VendorBranch?
// let createdAt: Date?
// let updatedAt: Date?
// }
//
// struct Migrate: AsyncMigration {
//
// let name = "CreatePurchaseOrder"
//
// func prepare(on database: any Database) async throws {
// try await database.schema(PurchaseOrder.schema)
// .field("id", .int, .identifier(auto: true))
// .field("work_order", .int)
// .field("customer", .string, .required)
// .field("materials", .string, .required)
// .field("truck_stock", .bool, .required)
// .field("created_by_id", .uuid, .required, .references(User.schema, "id"))
// .field("created_for_id", .uuid, .required, .references(Employee.schema, "id"))
// .field("vendor_branch_id", .uuid, .required, .references(VendorBranch.schema, "id"))
// .field("created_at", .datetime)
// .field("updated_at", .datetime)
// .create()
// }
//
// func revert(on database: any Database) async throws {
// try await database.schema(PurchaseOrder.schema).delete()
// }
// }
// }
//
// extension PurchaseOrder.Create: Validatable {
//
// static func validations(_ validations: inout Validations) {
// validations.add("materials", as: String.self, is: !.empty)
// validations.add("customer", as: String.self, is: !.empty)
// }
// }

View File

@@ -1,121 +1,121 @@
import Fluent
import Vapor
/// The user database model.
///
/// A user is someone who is able to login and generate PO's for employees. Generally a user should also
/// have an employee profile, but not all employees are users. Users are generally restricted to office workers
/// and administrators.
///
///
final class User: Model, @unchecked Sendable {
static let schema = "user"
@ID(key: .id)
var id: UUID?
@Field(key: "username")
var username: String
@Field(key: "email")
var email: String
@Field(key: "password_hash")
var passwordHash: String
@Timestamp(key: "created_at", on: .create)
var createdAt: Date?
@Timestamp(key: "updated_at", on: .update)
var updatedAt: Date?
init() {}
init(
id: UUID? = nil,
username: String,
email: String,
passwordHash: String
) {
self.id = id
self.username = username
self.email = email
self.passwordHash = passwordHash
}
func toDTO() -> DTO {
.init(
id: id,
username: $username.value,
email: $email.value,
createdAt: createdAt,
updatedAt: updatedAt
)
}
func generateToken() throws -> UserToken {
try .init(
value: [UInt8].random(count: 16).base64,
userID: requireID()
)
}
}
extension User {
struct Create: Content {
var username: String
var email: String
var password: String
var confirmPassword: String
}
struct DTO: Content {
let id: UUID?
let username: String?
let email: String?
let createdAt: Date?
let updatedAt: Date?
}
struct Migrate: AsyncMigration {
let name = "CreateUser"
func prepare(on database: any Database) async throws {
try await database.schema(User.schema)
.id()
.field("username", .string, .required)
.field("email", .string, .required)
.field("password_hash", .string, .required)
.field("created_at", .datetime)
.field("updated_at", .datetime)
.unique(on: "email", "username")
.create()
}
func revert(on database: any Database) async throws {
try await database.schema(User.schema).delete()
}
}
}
extension User: ModelAuthenticatable {
static let usernameKey = \User.$username
static let passwordHashKey = \User.$passwordHash
func verify(password: String) throws -> Bool {
try Bcrypt.verify(password, created: passwordHash)
}
}
extension User: ModelSessionAuthenticatable {}
extension User: ModelCredentialsAuthenticatable {}
extension User.Create: Validatable {
static func validations(_ validations: inout Validations) {
validations.add("username", as: String.self, is: !.empty)
validations.add("email", as: String.self, is: .email)
validations.add("password", as: String.self, is: .count(8...))
}
}
// import Fluent
// import Vapor
//
// /// The user database model.
// ///
// /// A user is someone who is able to login and generate PO's for employees. Generally a user should also
// /// have an employee profile, but not all employees are users. Users are generally restricted to office workers
// /// and administrators.
// ///
// ///
// final class User: Model, @unchecked Sendable {
// static let schema = "user"
//
// @ID(key: .id)
// var id: UUID?
//
// @Field(key: "username")
// var username: String
//
// @Field(key: "email")
// var email: String
//
// @Field(key: "password_hash")
// var passwordHash: String
//
// @Timestamp(key: "created_at", on: .create)
// var createdAt: Date?
//
// @Timestamp(key: "updated_at", on: .update)
// var updatedAt: Date?
//
// init() {}
//
// init(
// id: UUID? = nil,
// username: String,
// email: String,
// passwordHash: String
// ) {
// self.id = id
// self.username = username
// self.email = email
// self.passwordHash = passwordHash
// }
//
// func toDTO() -> DTO {
// .init(
// id: id,
// username: $username.value,
// email: $email.value,
// createdAt: createdAt,
// updatedAt: updatedAt
// )
// }
//
// func generateToken() throws -> UserToken {
// try .init(
// value: [UInt8].random(count: 16).base64,
// userID: requireID()
// )
// }
//
// }
//
// extension User {
//
// struct Create: Content {
// var username: String
// var email: String
// var password: String
// var confirmPassword: String
// }
//
// struct DTO: Content {
// let id: UUID?
// let username: String?
// let email: String?
// let createdAt: Date?
// let updatedAt: Date?
// }
//
// struct Migrate: AsyncMigration {
// let name = "CreateUser"
//
// func prepare(on database: any Database) async throws {
// try await database.schema(User.schema)
// .id()
// .field("username", .string, .required)
// .field("email", .string, .required)
// .field("password_hash", .string, .required)
// .field("created_at", .datetime)
// .field("updated_at", .datetime)
// .unique(on: "email", "username")
// .create()
// }
//
// func revert(on database: any Database) async throws {
// try await database.schema(User.schema).delete()
// }
// }
// }
//
// extension User: ModelAuthenticatable {
// static let usernameKey = \User.$username
// static let passwordHashKey = \User.$passwordHash
//
// func verify(password: String) throws -> Bool {
// try Bcrypt.verify(password, created: passwordHash)
// }
// }
//
// extension User: ModelSessionAuthenticatable {}
// extension User: ModelCredentialsAuthenticatable {}
//
// extension User.Create: Validatable {
// static func validations(_ validations: inout Validations) {
// validations.add("username", as: String.self, is: !.empty)
// validations.add("email", as: String.self, is: .email)
// validations.add("password", as: String.self, is: .count(8...))
// }
// }

View File

@@ -1,51 +1,51 @@
import Fluent
import Vapor
final class UserToken: Model, Content, @unchecked Sendable {
static let schema = "user_token"
@ID(key: .id)
var id: UUID?
@Field(key: "value")
var value: String
@Parent(key: "user_id")
var user: User
init() {}
init(id: UUID? = nil, value: String, userID: User.IDValue) {
self.id = id
self.value = value
$user.id = userID
}
}
extension UserToken {
struct Migrate: AsyncMigration {
let name = "CreateUserToken"
func prepare(on database: any Database) async throws {
try await database.schema(UserToken.schema)
.id()
.field("value", .string, .required)
.field("user_id", .uuid, .required, .references(User.schema, "id"))
.unique(on: "value")
.create()
}
func revert(on database: any Database) async throws {
try await database.schema(UserToken.schema).delete()
}
}
}
extension UserToken: ModelTokenAuthenticatable {
static let valueKey = \UserToken.$value
static let userKey = \UserToken.$user
var isValid: Bool { true }
}
// import Fluent
// import Vapor
//
// final class UserToken: Model, Content, @unchecked Sendable {
//
// static let schema = "user_token"
//
// @ID(key: .id)
// var id: UUID?
//
// @Field(key: "value")
// var value: String
//
// @Parent(key: "user_id")
// var user: User
//
// init() {}
//
// init(id: UUID? = nil, value: String, userID: User.IDValue) {
// self.id = id
// self.value = value
// $user.id = userID
// }
// }
//
// extension UserToken {
//
// struct Migrate: AsyncMigration {
// let name = "CreateUserToken"
//
// func prepare(on database: any Database) async throws {
// try await database.schema(UserToken.schema)
// .id()
// .field("value", .string, .required)
// .field("user_id", .uuid, .required, .references(User.schema, "id"))
// .unique(on: "value")
// .create()
// }
//
// func revert(on database: any Database) async throws {
// try await database.schema(UserToken.schema).delete()
// }
// }
// }
//
// extension UserToken: ModelTokenAuthenticatable {
// static let valueKey = \UserToken.$value
// static let userKey = \UserToken.$user
//
// var isValid: Bool { true }
// }

View File

@@ -1,113 +1,113 @@
import Fluent
import struct Foundation.UUID
import Vapor
// The primary database model.
final class Vendor: Model, @unchecked Sendable {
static let schema = "vendor"
@ID(key: .id)
var id: UUID?
@Field(key: "name")
var name: String
@Timestamp(key: "created_at", on: .create)
var createdAt: Date?
@Timestamp(key: "updated_at", on: .update)
var updatedAt: Date?
@Children(for: \.$vendor)
var branches: [VendorBranch]
init() {}
init(id: UUID? = nil, name: String) {
self.id = id
self.name = name
}
func toDTO(includeBranches: Bool? = nil) -> DTO {
.init(
id: id,
name: $name.value,
branches: ($branches.value != nil && $branches.value!.count > 0)
? $branches.value!.map { $0.toDTO() }
: (includeBranches == true) ? [] : nil,
createdAt: createdAt,
updatedAt: updatedAt
)
}
func applyUpdates(_ updates: Update) {
name = updates.name
}
}
// MARK: - Helpers.
extension Vendor {
struct Create: Content {
var name: String
func toModel() -> Vendor {
.init(name: name)
}
}
struct DTO: Content {
var id: UUID?
var name: String?
var branches: [VendorBranch.DTO]?
let createdAt: Date?
let updatedAt: Date?
func toModel() -> Vendor {
let model = Vendor()
model.id = id
if let name {
model.name = name
}
return model
}
}
struct Migrate: AsyncMigration {
let name = "CreateVendor"
func prepare(on database: any Database) async throws {
try await database.schema(Vendor.schema)
.id()
.field("name", .string, .required)
.field("created_at", .datetime)
.field("updated_at", .datetime)
.unique(on: "name")
.create()
}
func revert(on database: any Database) async throws {
try await database.schema(Vendor.schema).delete()
}
}
struct Update: Content {
var name: String
}
}
// MARK: - Validations
extension Vendor.Create: Validatable {
static func validations(_ validations: inout Validations) {
validations.add("name", as: String.self, is: !.empty)
}
}
extension Vendor.Update: Validatable {
static func validations(_ validations: inout Validations) {
validations.add("name", as: String.self, is: !.empty)
}
}
// import Fluent
// import struct Foundation.UUID
// import Vapor
//
// // The primary database model.
// final class Vendor: Model, @unchecked Sendable {
//
// static let schema = "vendor"
//
// @ID(key: .id)
// var id: UUID?
//
// @Field(key: "name")
// var name: String
//
// @Timestamp(key: "created_at", on: .create)
// var createdAt: Date?
//
// @Timestamp(key: "updated_at", on: .update)
// var updatedAt: Date?
//
// @Children(for: \.$vendor)
// var branches: [VendorBranch]
//
// init() {}
//
// init(id: UUID? = nil, name: String) {
// self.id = id
// self.name = name
// }
//
// func toDTO(includeBranches: Bool? = nil) -> DTO {
// .init(
// id: id,
// name: $name.value,
// branches: ($branches.value != nil && $branches.value!.count > 0)
// ? $branches.value!.map { $0.toDTO() }
// : (includeBranches == true) ? [] : nil,
// createdAt: createdAt,
// updatedAt: updatedAt
// )
// }
//
// func applyUpdates(_ updates: Update) {
// name = updates.name
// }
// }
//
// // MARK: - Helpers.
//
// extension Vendor {
// struct Create: Content {
// var name: String
//
// func toModel() -> Vendor {
// .init(name: name)
// }
// }
//
// struct DTO: Content {
//
// var id: UUID?
// var name: String?
// var branches: [VendorBranch.DTO]?
// let createdAt: Date?
// let updatedAt: Date?
//
// func toModel() -> Vendor {
// let model = Vendor()
// model.id = id
// if let name {
// model.name = name
// }
// return model
// }
// }
//
// struct Migrate: AsyncMigration {
// let name = "CreateVendor"
//
// func prepare(on database: any Database) async throws {
// try await database.schema(Vendor.schema)
// .id()
// .field("name", .string, .required)
// .field("created_at", .datetime)
// .field("updated_at", .datetime)
// .unique(on: "name")
// .create()
// }
//
// func revert(on database: any Database) async throws {
// try await database.schema(Vendor.schema).delete()
// }
// }
//
// struct Update: Content {
// var name: String
// }
// }
//
// // MARK: - Validations
//
// extension Vendor.Create: Validatable {
// static func validations(_ validations: inout Validations) {
// validations.add("name", as: String.self, is: !.empty)
// }
// }
//
// extension Vendor.Update: Validatable {
// static func validations(_ validations: inout Validations) {
// validations.add("name", as: String.self, is: !.empty)
// }
// }

View File

@@ -1,119 +1,119 @@
import Fluent
import struct Foundation.UUID
import Vapor
final class VendorBranch: Model, @unchecked Sendable {
static let schema = "vendor_branch"
@ID(key: .id)
var id: UUID?
@Field(key: "name")
var name: String
@Timestamp(key: "created_at", on: .create)
var createdAt: Date?
@Timestamp(key: "updated_at", on: .update)
var updatedAt: Date?
@Parent(key: "vendor_id")
var vendor: Vendor
init() {}
init(id: UUID? = nil, name: String, vendorId: Vendor.IDValue) {
self.id = id
self.name = name
$vendor.id = vendorId
}
func toDTO() -> DTO {
.init(
id: id,
name: $name.value,
vendorId: $vendor.id,
createdAt: createdAt,
updatedAt: updatedAt
)
}
func applyUpdates(_ updates: Update) {
name = updates.name
}
}
// MARK: - Helpers
extension VendorBranch {
struct Create: Content {
var name: String
func toModel() -> VendorBranch {
let model = VendorBranch()
model.name = name
return model
}
}
struct DTO: Content {
var id: UUID?
var name: String?
var vendorId: Vendor.IDValue?
let createdAt: Date?
let updatedAt: Date?
func toModel() -> VendorBranch {
let model = VendorBranch()
model.id = id
if let name {
model.name = name
}
if let vendorId {
model.$vendor.id = vendorId
}
return model
}
}
struct Migrate: AsyncMigration {
let name = "CreateVendorBranch"
func prepare(on database: any Database) async throws {
try await database.schema(VendorBranch.schema)
.id()
.field("name", .string, .required)
.field("vendor_id", .uuid, .required)
.field("created_at", .datetime)
.field("updated_at", .datetime)
.foreignKey("vendor_id", references: Vendor.schema, "id", onDelete: .cascade)
.create()
}
func revert(on database: any Database) async throws {
try await database.schema(VendorBranch.schema).delete()
}
}
struct Update: Content {
var name: String
}
}
// MARK: - Validations
extension VendorBranch.Create: Validatable {
static func validations(_ validations: inout Validations) {
validations.add("name", as: String.self, is: !.empty)
}
}
extension VendorBranch.Update: Validatable {
static func validations(_ validations: inout Validations) {
validations.add("name", as: String.self, is: !.empty)
}
}
// import Fluent
// import struct Foundation.UUID
// import Vapor
//
// final class VendorBranch: Model, @unchecked Sendable {
//
// static let schema = "vendor_branch"
//
// @ID(key: .id)
// var id: UUID?
//
// @Field(key: "name")
// var name: String
//
// @Timestamp(key: "created_at", on: .create)
// var createdAt: Date?
//
// @Timestamp(key: "updated_at", on: .update)
// var updatedAt: Date?
//
// @Parent(key: "vendor_id")
// var vendor: Vendor
//
// init() {}
//
// init(id: UUID? = nil, name: String, vendorId: Vendor.IDValue) {
// self.id = id
// self.name = name
// $vendor.id = vendorId
// }
//
// func toDTO() -> DTO {
// .init(
// id: id,
// name: $name.value,
// vendorId: $vendor.id,
// createdAt: createdAt,
// updatedAt: updatedAt
// )
// }
//
// func applyUpdates(_ updates: Update) {
// name = updates.name
// }
//
// }
//
// // MARK: - Helpers
//
// extension VendorBranch {
// struct Create: Content {
// var name: String
//
// func toModel() -> VendorBranch {
// let model = VendorBranch()
// model.name = name
// return model
// }
// }
//
// struct DTO: Content {
// var id: UUID?
// var name: String?
// var vendorId: Vendor.IDValue?
// let createdAt: Date?
// let updatedAt: Date?
//
// func toModel() -> VendorBranch {
// let model = VendorBranch()
//
// model.id = id
// if let name {
// model.name = name
// }
// if let vendorId {
// model.$vendor.id = vendorId
// }
// return model
// }
//
// }
//
// struct Migrate: AsyncMigration {
// let name = "CreateVendorBranch"
//
// func prepare(on database: any Database) async throws {
// try await database.schema(VendorBranch.schema)
// .id()
// .field("name", .string, .required)
// .field("vendor_id", .uuid, .required)
// .field("created_at", .datetime)
// .field("updated_at", .datetime)
// .foreignKey("vendor_id", references: Vendor.schema, "id", onDelete: .cascade)
// .create()
// }
//
// func revert(on database: any Database) async throws {
// try await database.schema(VendorBranch.schema).delete()
// }
// }
//
// struct Update: Content {
// var name: String
// }
// }
//
// // MARK: - Validations
//
// extension VendorBranch.Create: Validatable {
// static func validations(_ validations: inout Validations) {
// validations.add("name", as: String.self, is: !.empty)
// }
// }
//
// extension VendorBranch.Update: Validatable {
// static func validations(_ validations: inout Validations) {
// validations.add("name", as: String.self, is: !.empty)
// }
// }

View File

@@ -1,8 +1,10 @@
import DatabaseClientLive
import Dependencies
import Fluent
import FluentSQLiteDriver
import Leaf
import NIOSSL
import SharedModels
import Vapor
// configures your application
@@ -10,7 +12,7 @@ public func configure(_ app: Application) async throws {
// uncomment to serve files from /Public folder
app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
app.middleware.use(app.sessions.middleware)
app.middleware.use(User.sessionAuthenticator())
// app.middleware.use(User.sessionAuthenticator())
#if DEBUG
app.lifecycle.use(BrowserSyncHandler())
@@ -23,21 +25,25 @@ public func configure(_ app: Application) async throws {
app.databases.use(DatabaseConfigurationFactory.sqlite(.memory), as: .sqlite)
}
app.migrations.add(Vendor.Migrate())
app.migrations.add(VendorBranch.Migrate())
app.migrations.add(Employee.Migrate())
app.migrations.add(User.Migrate())
app.migrations.add(UserToken.Migrate())
app.migrations.add(PurchaseOrder.Migrate())
let databaseClient = DatabaseClient.live(database: app.db)
try await app.migrations.add(databaseClient.migrations())
// app.migrations.add(Vendor.Migrate())
// app.migrations.add(VendorBranch.Migrate())
// app.migrations.add(Employee.Migrate())
// app.migrations.add(User.Migrate())
// app.migrations.add(UserToken.Migrate())
// app.migrations.add(PurchaseOrder.Migrate())
app.views.use(.leaf)
try withDependencies {
$0.employees = .live(database: app.db(.sqlite))
$0.purchaseOrders = .live(database: app.db(.sqlite))
$0.users = .live(database: app.db(.sqlite))
$0.vendorBranches = .live(database: app.db(.sqlite))
$0.vendors = .live(database: app.db(.sqlite))
$0.database = databaseClient
// $0.employees = .live(database: app.db(.sqlite))
// $0.purchaseOrders = .live(database: app.db(.sqlite))
// $0.users = .live(database: app.db(.sqlite))
// $0.vendorBranches = .live(database: app.db(.sqlite))
// $0.vendors = .live(database: app.db(.sqlite))
} operation: {
// register routes
try routes(app)

View File

@@ -3,5 +3,5 @@ import Vapor
func routes(_ app: Application) throws {
try app.register(collection: ApiController())
try app.register(collection: ViewController())
// try app.register(collection: ViewController())
}