feat: Begins integrating database client into vapor app.
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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
|
||||
),
|
||||
|
||||
@@ -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?
|
||||
// }
|
||||
|
||||
@@ -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
|
||||
// // }
|
||||
// }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -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
|
||||
// }
|
||||
//
|
||||
// }
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -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)
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -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 ?? "") }
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -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?
|
||||
// }
|
||||
|
||||
@@ -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
|
||||
// }
|
||||
|
||||
@@ -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()
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -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()
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -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
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -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()
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -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()
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -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)
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -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...))
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -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 }
|
||||
// }
|
||||
|
||||
@@ -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)
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -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)
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import DatabaseClient
|
||||
@_exported import DatabaseClient
|
||||
import FluentKit
|
||||
import SharedModels
|
||||
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
// }
|
||||
@@ -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
|
||||
@@ -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 {}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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] {
|
||||
// [
|
||||
|
||||
@@ -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] {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user