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,5 +1,5 @@
{
"originHash" : "0c4aa955a400075a593dfba3804996f803c9e812e765f72d0026e12c182b751c",
"originHash" : "5e46aec5c52d22d116c033b012d49986e5f900c102b88ac80e054d2e6e64d458",
"pins" : [
{
"identity" : "async-http-client",
@@ -64,33 +64,6 @@
"version" : "4.8.0"
}
},
{
"identity" : "hummingbird",
"kind" : "remoteSourceControl",
"location" : "https://github.com/hummingbird-project/hummingbird.git",
"state" : {
"revision" : "7a41c20c25866064f22b2bfa2c8194083e7e1595",
"version" : "2.6.1"
}
},
{
"identity" : "hummingbird-auth",
"kind" : "remoteSourceControl",
"location" : "https://github.com/hummingbird-project/hummingbird-auth.git",
"state" : {
"revision" : "8630a49acca3b38c50e29d61ab263cb7edf0b06d",
"version" : "2.0.2"
}
},
{
"identity" : "hummingbird-fluent",
"kind" : "remoteSourceControl",
"location" : "https://github.com/hummingbird-project/hummingbird-fluent.git",
"state" : {
"revision" : "45459ea5b541c6a96b87d1be848e384593b7dde3",
"version" : "2.0.0"
}
},
{
"identity" : "leaf",
"kind" : "remoteSourceControl",
@@ -163,15 +136,6 @@
"version" : "1.2.0"
}
},
{
"identity" : "swift-argument-parser",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser.git",
"state" : {
"revision" : "41982a3656a71c768319979febd796c6fd111d5c",
"version" : "1.5.0"
}
},
{
"identity" : "swift-asn1",
"kind" : "remoteSourceControl",
@@ -181,15 +145,6 @@
"version" : "1.3.0"
}
},
{
"identity" : "swift-async-algorithms",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-async-algorithms.git",
"state" : {
"revision" : "4c3ea81f81f0a25d0470188459c6d4bf20cf2f97",
"version" : "1.0.3"
}
},
{
"identity" : "swift-atomics",
"kind" : "remoteSourceControl",
@@ -253,15 +208,6 @@
"version" : "1.1.2"
}
},
{
"identity" : "swift-extras-base64",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swift-extras/swift-extras-base64.git",
"state" : {
"revision" : "dc8121fdd2b444c97d6b0534e8ad4ddecbe0d5f4",
"version" : "1.0.0"
}
},
{
"identity" : "swift-http-types",
"kind" : "remoteSourceControl",
@@ -352,15 +298,6 @@
"version" : "1.1.0"
}
},
{
"identity" : "swift-service-lifecycle",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swift-server/swift-service-lifecycle.git",
"state" : {
"revision" : "c2e97cf6f81510f2d6b4a69453861db65d478560",
"version" : "2.6.3"
}
},
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",

View File

@@ -8,7 +8,6 @@ let package = Package(
],
products: [
.executable(name: "App", targets: ["App"]),
.executable(name: "HApp", targets: ["HApp"]),
.library(name: "SharedModels", targets: ["SharedModels"]),
.library(name: "DatabaseClient", targets: ["DatabaseClient"]),
.library(name: "DatabaseClientLive", targets: ["DatabaseClientLive"])
@@ -24,16 +23,13 @@ let package = Package(
.package(url: "https://github.com/vapor/leaf.git", from: "4.3.0"),
// 🔵 Non-blocking, event-driven networking for Swift. Used for custom executors
.package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"),
.package(url: "https://github.com/pointfreeco/swift-dependencies.git", from: "1.6.3"),
.package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0"),
.package(url: "https://github.com/hummingbird-project/hummingbird-auth.git", from: "2.0.2"),
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"),
.package(url: "https://github.com/hummingbird-project/hummingbird-fluent.git", from: "2.0.0")
.package(url: "https://github.com/pointfreeco/swift-dependencies.git", from: "1.6.3")
],
targets: [
.executableTarget(
name: "App",
dependencies: [
"DatabaseClientLive",
.product(name: "Fluent", package: "fluent"),
.product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"),
.product(name: "Leaf", package: "leaf"),
@@ -54,24 +50,14 @@ let package = Package(
],
swiftSettings: swiftSettings
),
.executableTarget(
name: "HApp",
dependencies: [
"DatabaseClientLive",
.product(name: "Hummingbird", package: "hummingbird"),
.product(name: "HummingbirdFluent", package: "hummingbird-fluent"),
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver")
],
swiftSettings: swiftSettings
),
.target(
name: "DatabaseClient",
dependencies: [
"SharedModels",
.product(name: "Dependencies", package: "swift-dependencies"),
.product(name: "DependenciesMacros", package: "swift-dependencies"),
.product(name: "Fluent", package: "fluent")
.product(name: "Fluent", package: "fluent"),
.product(name: "Vapor", package: "vapor")
],
swiftSettings: swiftSettings
),
@@ -79,7 +65,7 @@ let package = Package(
name: "DatabaseClientLive",
dependencies: [
"DatabaseClient",
.product(name: "HummingbirdBcrypt", package: "hummingbird-auth")
.product(name: "Vapor", package: "vapor")
],
swiftSettings: swiftSettings
),

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
}
// 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 update(req: Request) async throws -> PurchaseOrder.DTO {
// 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: id, on: req.db)
// 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
// #if DEBUG
// return self
// #else
return grouped(
User.credentialsAuthenticator(),
// User.credentialsAuthenticator(),
UserPasswordAuthenticator(),
UserTokenAuthenticator(),
UserSessionAuthenticator(),
User.redirectMiddleware { req in
"login?next=\(req.url)"
}
)
#endif
// #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
// #if DEBUG
// return apiUnprotected(route: route)
// #else
let prefixed = grouped("api", "v1", route)
return prefixed.grouped(
User.authenticator(),
UserToken.authenticator(),
UserPasswordAuthenticator(),
UserTokenAuthenticator(),
// User.authenticator(),
// UserToken.authenticator(),
User.guardMiddleware()
)
#endif
// #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())
}

View File

@@ -27,37 +27,3 @@ public extension DatabaseClient {
extension DatabaseClient.Employees: TestDependencyKey {
public static let testValue = Self()
}
public extension Employee {
struct Create: Codable, Sendable {
public let firstName: String
public let lastName: String
public let active: Bool?
public init(
firstName: String,
lastName: String,
active: Bool? = nil
) {
self.firstName = firstName
self.lastName = lastName
self.active = active
}
}
struct Update: Codable, Sendable {
public let firstName: String?
public let lastName: String?
public let active: Bool?
public init(
firstName: String? = nil,
lastName: String? = nil,
active: Bool? = nil
) {
self.firstName = firstName
self.lastName = lastName
self.active = active
}
}
}

View File

@@ -18,35 +18,3 @@ public extension DatabaseClient {
extension DatabaseClient.PurchaseOrders: TestDependencyKey {
public static let testValue: DatabaseClient.PurchaseOrders = Self()
}
public extension PurchaseOrder {
struct Create: Codable, Sendable {
public let id: Int?
public let workOrder: Int?
public let materials: String
public let customer: String
public let truckStock: Bool?
public let createdForID: Employee.ID
public let vendorBranchID: VendorBranch.ID
public init(
id: Int? = nil,
workOrder: Int? = nil,
materials: String,
customer: String,
truckStock: Bool? = nil,
createdForID: Employee.ID,
vendorBranchID: VendorBranch.ID
) {
self.id = id
self.workOrder = workOrder
self.materials = materials
self.customer = customer
self.truckStock = truckStock
self.createdForID = createdForID
self.vendorBranchID = vendorBranchID
}
}
}

View File

@@ -2,11 +2,13 @@ import Dependencies
import DependenciesMacros
import Foundation
import SharedModels
import Vapor
public extension DatabaseClient {
@DependencyClient
struct Users: Sendable {
public var count: @Sendable () async throws -> Int
public var create: @Sendable (User.Create) async throws -> User
public var delete: @Sendable (User.ID) async throws -> Void
public var fetchAll: @Sendable () async throws -> [User]
@@ -16,61 +18,12 @@ public extension DatabaseClient {
}
}
public extension DatabaseClient.Users {
enum AuthRequest {
case basic(BasicAuthorization)
}
}
extension DatabaseClient.Users: TestDependencyKey {
public static let testValue: DatabaseClient.Users = Self()
}
public extension User {
struct Create: Codable, Sendable {
public let username: String
public let email: String
public let password: String
public let confirmPassword: String
public init(
username: String,
email: String,
password: String,
confirmPassword: String
) {
self.username = username
self.email = email
self.password = password
self.confirmPassword = confirmPassword
}
}
struct Login: Codable, Sendable {
public let username: String?
public let email: String?
public let password: String
public init(
username: String?,
email: String? = nil,
password: String
) {
self.username = username
self.email = email
self.password = password
}
}
struct Token: Codable, Equatable, Identifiable, Sendable {
public let id: UUID
public let userID: User.ID
public let value: String
public init(
id: UUID,
userID: User.ID,
value: String
) {
self.id = id
self.userID = userID
self.value = value
}
}
}

View File

@@ -26,23 +26,3 @@ public extension DatabaseClient {
extension DatabaseClient.VendorBranches: TestDependencyKey {
public static let testValue: DatabaseClient.VendorBranches = Self()
}
public extension VendorBranch {
struct Create: Codable, Sendable {
public let name: String
public let vendorID: Vendor.ID
public init(name: String, vendorID: Vendor.ID) {
self.name = name
self.vendorID = vendorID
}
}
struct Update: Codable, Sendable {
public let name: String?
public init(name: String?) {
self.name = name
}
}
}

View File

@@ -34,22 +34,3 @@ public extension DatabaseClient {
extension DatabaseClient.Vendors: TestDependencyKey {
public static let testValue: DatabaseClient.Vendors = Self()
}
public extension Vendor {
struct Create: Codable, Sendable {
public let name: String
public init(name: String) {
self.name = name
}
}
struct Update: Codable, Sendable {
public let name: String?
public init(name: String?) {
self.name = name
}
}
}

View File

@@ -1,4 +1,4 @@
import DatabaseClient
@_exported import DatabaseClient
import FluentKit
import SharedModels

View File

@@ -1,13 +1,15 @@
import DatabaseClient
import FluentKit
import Foundation
import HummingbirdBcrypt
import SharedModels
import Vapor
public extension DatabaseClient.Users {
static func live(database: any Database) -> Self {
.init { create in
.init {
try await UserModel.query(on: database).count()
} create: { create in
let model = try create.toModel()
try await model.save(on: database)
return try model.toDTO()
@@ -98,7 +100,7 @@ extension User.Create {
func toModel() throws -> UserModel {
try validate()
return .init(username: username, email: email, passwordHash: Bcrypt.hash(password, cost: 12))
return try .init(username: username, email: email, passwordHash: Bcrypt.hash(password, cost: 12))
}
func validate() throws {
@@ -209,3 +211,62 @@ final class UserTokenModel: Model, Codable, @unchecked Sendable {
}
}
// MARK: - Authentication
extension User: Authenticatable {}
extension User: SessionAuthenticatable {
public var sessionID: String { username }
}
extension User: Content {}
public struct UserPasswordAuthenticator: AsyncBasicAuthenticator {
public typealias User = SharedModels.User
public init() {}
public func authenticate(basic: BasicAuthorization, for request: Request) async throws {
guard let user = try await UserModel.query(on: request.db)
.filter(\.$username == basic.username)
.first(),
try Bcrypt.verify(basic.password, created: user.passwordHash)
else {
throw Abort(.unauthorized)
}
try request.auth.login(user.toDTO())
}
}
public struct UserTokenAuthenticator: AsyncBearerAuthenticator {
public typealias User = SharedModels.User
public init() {}
public func authenticate(bearer: BearerAuthorization, for request: Request) async throws {
guard let token = try await UserTokenModel.query(on: request.db)
.filter(\.$value == bearer.token)
.with(\.$user)
.first()
else {
throw Abort(.unauthorized)
}
try request.auth.login(token.user.toDTO())
}
}
public struct UserSessionAuthenticator: AsyncSessionAuthenticator {
public typealias User = SharedModels.User
public init() {}
public func authenticate(sessionID: User.SessionID, for request: Request) async throws {
guard let user = try await UserModel.query(on: request.db)
.filter(\.$username == sessionID)
.first()
else {
throw Abort(.unauthorized)
}
try request.auth.login(user.toDTO())
}
}

View File

@@ -1,102 +0,0 @@
import DatabaseClient
import DatabaseClientLive
import FluentSQLiteDriver
import Hummingbird
import HummingbirdFluent
import Logging
public protocol AppArguments {
var inMemoryDatabase: Bool { get }
var migrate: Bool { get }
var revert: Bool { get }
var hostname: String { get }
var port: Int { get }
var logLevel: Logger.Level? { get }
}
/// Build application
/// - Parameter arguments: application arguments
public func buildApplication(_ arguments: some AppArguments) async throws -> some ApplicationProtocol {
let environment = Environment()
let logger = {
var logger = Logger(label: "PurchaseOrders")
logger.logLevel = arguments.logLevel ??
environment.get("LOG_LEVEL").map { Logger.Level(rawValue: $0) ?? .info } ??
.info
return logger
}()
let fluent = Fluent(logger: logger)
if arguments.inMemoryDatabase {
fluent.databases.use(.sqlite(.memory), as: .sqlite)
} else {
fluent.databases.use(.sqlite(.file("hdb.sqlite")), as: .sqlite)
}
let dbClient = DatabaseClient.live(database: fluent.db())
try await fluent.migrations.add(dbClient.migrations())
if arguments.revert {
try await fluent.revert()
}
if arguments.migrate || arguments.inMemoryDatabase {
try await fluent.migrate()
}
let fluentPersist = await FluentPersistDriver(fluent: fluent)
let router = Router()
// Add middleware
// logging middleware
router.add(middleware: LogRequestsMiddleware(.info))
router.add(middleware: FileMiddleware(logger: logger))
router.add(middleware: CORSMiddleware(
allowOrigin: .originBased,
allowHeaders: [.contentType],
allowMethods: [.get, .options, .post, .delete, .put, .patch]
))
// Add health endpoint
router.get("/health") { _, _ -> HTTPResponse.Status in
return .ok
}
UserController(db: dbClient.users).addRoutes(to: router.group("api/users"))
// let router = buildRouter()
//
var app = Application(
router: router,
configuration: .init(
address: .hostname(arguments.hostname, port: arguments.port),
serverName: "Purchase-Orders"
),
logger: logger
)
app.addServices(fluent, fluentPersist)
return app
}
// Build router
// func buildRouter() -> Router<AppRequestContext> {
// let router = Router()
//
// // Add middleware
//
// router.addMiddleware {
// // logging middleware
// LogRequestsMiddleware(.info)
// }
//
// // Add health endpoint
//
// router.get("/health") { _, _ -> HTTPResponse.Status in
// return .ok
// }
//
// return router
// }

View File

@@ -1,41 +0,0 @@
import ArgumentParser
import Hummingbird
import Logging
@main
struct App: AsyncParsableCommand, AppArguments {
@Option(name: .shortAndLong)
var hostname: String = "127.0.0.1"
@Option(name: .shortAndLong)
var port: Int = 8080
@Option(name: .shortAndLong)
var logLevel: Logger.Level?
@Flag(name: .shortAndLong)
var migrate: Bool = false
@Flag(name: .shortAndLong)
var revert: Bool = false
@Flag(name: .shortAndLong)
var inMemoryDatabase: Bool = false
func run() async throws {
let app = try await buildApplication(self)
try await app.runService()
}
}
/// Extend `Logger.Level` so it can be used as an argument
#if hasFeature(RetroactiveAttribute)
extension Logger.Level: @retroactive ExpressibleByArgument {
public init?(argument: String) {
self.init(rawValue: argument)
}
}
#else
extension Logger.Level: ExpressibleByArgument {}
#endif

View File

@@ -1,28 +0,0 @@
import DatabaseClient
import Hummingbird
import HummingbirdFluent
import SharedModels
struct UserController<Context: RequestContext> {
let db: DatabaseClient.Users
func addRoutes(to group: RouterGroup<Context>) {
group.get(use: list)
.post(use: create)
}
@Sendable
func list(_ request: Request, context: Context) async throws -> [User] {
try await db.fetchAll()
}
@Sendable
func create(_ request: Request, context: Context) async throws -> User {
let create = try await request.decode(as: User.Create.self, context: context)
let user = try await db.create(create)
return user
}
}
extension User.Create: ResponseCodable {}
extension User: ResponseCodable {}

View File

@@ -24,7 +24,40 @@ public struct Employee: Codable, Equatable, Identifiable, Sendable {
self.lastName = lastName
self.updatedAt = updatedAt
}
}
public extension Employee {
struct Create: Codable, Sendable {
public let firstName: String
public let lastName: String
public let active: Bool?
public init(
firstName: String,
lastName: String,
active: Bool? = nil
) {
self.firstName = firstName
self.lastName = lastName
self.active = active
}
}
struct Update: Codable, Sendable {
public let firstName: String?
public let lastName: String?
public let active: Bool?
public init(
firstName: String? = nil,
lastName: String? = nil,
active: Bool? = nil
) {
self.firstName = firstName
self.lastName = lastName
self.active = active
}
}
}
// public extension Employee {

View File

@@ -38,3 +38,35 @@ public struct PurchaseOrder: Codable, Equatable, Identifiable, Sendable {
self.updatedAt = updatedAt
}
}
public extension PurchaseOrder {
struct Create: Codable, Sendable {
public let id: Int?
public let workOrder: Int?
public let materials: String
public let customer: String
public let truckStock: Bool?
public let createdForID: Employee.ID
public let vendorBranchID: VendorBranch.ID
public init(
id: Int? = nil,
workOrder: Int? = nil,
materials: String,
customer: String,
truckStock: Bool? = nil,
createdForID: Employee.ID,
vendorBranchID: VendorBranch.ID
) {
self.id = id
self.workOrder = workOrder
self.materials = materials
self.customer = customer
self.truckStock = truckStock
self.createdForID = createdForID
self.vendorBranchID = vendorBranchID
}
}
}

View File

@@ -24,6 +24,61 @@ public struct User: Codable, Equatable, Identifiable, Sendable {
}
}
public extension User {
struct Create: Codable, Sendable {
public let username: String
public let email: String
public let password: String
public let confirmPassword: String
public init(
username: String,
email: String,
password: String,
confirmPassword: String
) {
self.username = username
self.email = email
self.password = password
self.confirmPassword = confirmPassword
}
}
struct Login: Codable, Sendable {
public let username: String?
public let email: String?
public let password: String
public init(
username: String?,
email: String? = nil,
password: String
) {
self.username = username
self.email = email
self.password = password
}
}
struct Token: Codable, Equatable, Identifiable, Sendable {
public let id: UUID
public let userID: User.ID
public let value: String
public init(
id: UUID,
userID: User.ID,
value: String
) {
self.id = id
self.userID = userID
self.value = value
}
}
}
// public extension User {
// static var mocks: [Self] {
// [

View File

@@ -23,6 +23,25 @@ public struct Vendor: Codable, Equatable, Identifiable, Sendable {
}
}
public extension Vendor {
struct Create: Codable, Sendable {
public let name: String
public init(name: String) {
self.name = name
}
}
struct Update: Codable, Sendable {
public let name: String?
public init(name: String?) {
self.name = name
}
}
}
// public extension Vendor {
//
// static var mocks: [Self] {

View File

@@ -22,3 +22,23 @@ public struct VendorBranch: Codable, Equatable, Identifiable, Sendable {
self.updatedAt = updatedAt
}
}
public extension VendorBranch {
struct Create: Codable, Sendable {
public let name: String
public let vendorID: Vendor.ID
public init(name: String, vendorID: Vendor.ID) {
self.name = name
self.vendorID = vendorID
}
}
struct Update: Codable, Sendable {
public let name: String?
public init(name: String?) {
self.name = name
}
}
}