feat: Begins migrating views from leaf to elementary

This commit is contained in:
2025-01-14 17:11:57 -05:00
parent 31c6b51371
commit 8842957cf3
23 changed files with 294 additions and 1136 deletions

View File

@@ -1,5 +1,5 @@
{
"originHash" : "5e46aec5c52d22d116c033b012d49986e5f900c102b88ac80e054d2e6e64d458",
"originHash" : "b5426b686e9548f4fc69224d4e0f13d10ce55816d5b71622f5f446b12d66140a",
"pins" : [
{
"identity" : "async-http-client",
@@ -37,6 +37,24 @@
"version" : "4.15.1"
}
},
{
"identity" : "elementary",
"kind" : "remoteSourceControl",
"location" : "https://github.com/sliemeobn/elementary.git",
"state" : {
"revision" : "9830c5e1b6740d367b28d416797ff4e6b007e541",
"version" : "0.4.4"
}
},
{
"identity" : "elementary-htmx",
"kind" : "remoteSourceControl",
"location" : "https://github.com/sliemeobn/elementary-htmx.git",
"state" : {
"revision" : "6e8430d24a6dc2de9d16270a6af985eb37a190ad",
"version" : "0.4.0"
}
},
{
"identity" : "fluent",
"kind" : "remoteSourceControl",
@@ -325,6 +343,15 @@
"version" : "4.111.0"
}
},
{
"identity" : "vapor-elementary",
"kind" : "remoteSourceControl",
"location" : "https://github.com/vapor-community/vapor-elementary.git",
"state" : {
"revision" : "a14bc5d995f06fa2c398fa7a6bb6f98f1fd54446",
"version" : "0.2.1"
}
},
{
"identity" : "websocket-kit",
"kind" : "remoteSourceControl",

View File

@@ -23,7 +23,10 @@ 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/pointfreeco/swift-dependencies.git", from: "1.6.3"),
.package(url: "https://github.com/sliemeobn/elementary.git", from: "0.3.2"),
.package(url: "https://github.com/sliemeobn/elementary-htmx.git", from: "0.4.0"),
.package(url: "https://github.com/vapor-community/vapor-elementary.git", from: "0.1.0")
],
targets: [
.executableTarget(
@@ -37,7 +40,10 @@ let package = Package(
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "NIOPosix", package: "swift-nio"),
.product(name: "Dependencies", package: "swift-dependencies"),
.product(name: "DependenciesMacros", package: "swift-dependencies")
.product(name: "DependenciesMacros", package: "swift-dependencies"),
.product(name: "Elementary", package: "elementary"),
.product(name: "VaporElementary", package: "vapor-elementary"),
.product(name: "ElementaryHTMX", package: "elementary-htmx")
],
swiftSettings: swiftSettings

1
Public/images/menu.svg Normal file
View File

@@ -0,0 +1 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <g id="Menu / Hamburger_MD"> <path id="Vector" d="M5 17H19M5 12H19M5 7H19" stroke="#929292" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </g> </g></svg>

After

Width:  |  Height:  |  Size: 409 B

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,23 @@
import DatabaseClientLive
import Dependencies
import Vapor
struct DependenciesMiddleware: AsyncMiddleware {
private let values: DependencyValues.Continuation
init() {
self.values = withEscapedDependencies { $0 }
}
func respond(to request: Request, chainingTo next: any AsyncResponder) async throws -> Response {
try await values.yield {
try await withDependencies {
$0.database = .live(database: request.db)
} operation: {
try await next.respond(to: request)
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,9 @@
import Elementary
struct ToggleFormButton: HTML {
var content: some HTML<HTMLTag.a> {
a(.href("javascript:void(0)"), .on(.click, "toggleContent('form')"), .class("btn-add")) {
"+"
}
}
}

View File

@@ -0,0 +1,43 @@
import Elementary
import ElementaryHTMX
struct MainPage<Inner: HTML>: HTMLDocument {
var title: String { "Purchase Orders" }
let inner: Inner
let displayNav: Bool
init(displayNav: Bool = false, _ inner: () -> Inner) {
self.displayNav = displayNav
self.inner = inner()
}
var head: some HTML {
meta(.charset(.utf8))
script(.src("https://unpkg.com/htmx.org@2.0.4")) {}
script(.src("/js/main.js")) {}
link(.rel(.stylesheet), .href("/css/main.css"))
}
var body: some HTML {
header {
Logo()
if displayNav {
Navbar()
}
}
inner
}
}
extension MainPage: Sendable where Inner: Sendable {}
struct Logo: HTML, Sendable {
var content: some HTML {
div(.id("logo")) {
"HHE - Purchase Orders"
}
}
}

View File

@@ -0,0 +1,31 @@
import Elementary
import ElementaryHTMX
struct Navbar: HTML {
var content: some HTML {
div(.class("sidepanel"), .id("sidepanel")) {
a(.href("javascript:void(0)"), .class("closebtn"), .on(.click, "closeSidepanel()")) {
"x"
}
a(.hx.get("/purchase-orders?page=1&limit=50"), .hx.target("body"), .hx.pushURL(true)) {
"Purchae Orders"
}
a(.hx.get("/users"), .hx.target("body"), .hx.pushURL(true)) {
"Users"
}
a(.hx.get("/employees"), .hx.target("body"), .hx.pushURL(true)) {
"Employees"
}
a(.hx.get("/vendors"), .hx.target("body"), .hx.pushURL(true)) {
"Vendors"
}
div(.style("border-bottom: 1px solid grey; margin-bottom: 5px;")) {}
a(.hx.post("/logout"), .hx.target("#content"), .hx.swap(.outerHTML), .hx.trigger(.event(.click))) {
"Logout"
}
}
button(.class("openbtn"), .on(.click, "openSidepanel()")) {
img(.src("/images/menu.svg"), .style("width: 30px;, height: 30px;"))
}
}
}

View File

@@ -0,0 +1,78 @@
import Elementary
import ElementaryHTMX
struct UserForm: HTML, Sendable {
let context: Context
var content: some HTML {
form(
.id("user-form"),
.class("user-form"),
.hx.post(context.targetURL),
.hx.pushURL(context.pushURL),
.custom(name: "hx-on::after-request", value: "if(event.detail.successful) this.reset(); toggleContent('form');")
) {
input(.type(.text), .id("username"), .name("username"), .placeholder("Username"), .autofocus, .required)
br()
if context.showEmailInput {
input(.type(.email), .id("email"), .name("email"), .placeholder("Email"), .required)
br()
}
input(.type(.password), .id("password"), .name("password"), .placeholder("Password"), .required)
br()
if context.showConfirmPassword {
input(.type(.password), .id("confirmPassword"), .name("confirmPassword"), .required)
br()
}
input(.type(.submit), .value(context.buttonLabel))
}
}
enum Context {
case create
case login(next: String?)
var showConfirmPassword: Bool {
switch self {
case .create: return true
case .login: return false
}
}
var showEmailInput: Bool {
switch self {
case .create: return true
case .login: return false
}
}
var pushURL: Bool {
switch self {
case .create: return false
case .login: return true
}
}
var buttonLabel: String {
switch self {
case .create:
return "Create"
case .login:
return "Login"
}
}
var targetURL: String {
switch self {
case .create:
return "/users"
case let .login(next: next):
let path = "/login"
if let next {
return "\(path)?next=\(next)"
}
return path
}
}
}
}

View File

@@ -0,0 +1,40 @@
import DatabaseClient
import Dependencies
import Elementary
import ElementaryHTMX
import SharedModels
struct UserTable: HTML {
@Dependency(\.database.users.fetchAll) var fetchAll
var content: some HTML {
table(.id("user-table")) {
thead {
tr {
th { "Username" }
th { "Email" }
th { ToggleFormButton() }
}
}
tbody {
let users = try await fetchAll()
for user in users {
Row(user: user)
}
}
}
}
struct Row: HTML {
let user: User
var content: some HTML<HTMLTag.tr> {
tr {
td { user.username }
td { user.email }
td { "Fix me." }
}
}
}
}

View File

@@ -22,6 +22,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(DependenciesMiddleware())
#if DEBUG
app.lifecycle.use(BrowserSyncHandler())
@@ -42,7 +43,6 @@ public func configure(_ app: Application) async throws {
try withDependencies {
$0.database = databaseClient
} operation: {
// register routes
try routes(app)
}

View File

@@ -1,3 +1,5 @@
import DatabaseClientLive
import Dependencies
import Logging
import NIOCore
import NIOPosix

View File

@@ -1,7 +1,37 @@
import DatabaseClientLive
import Dependencies
import Elementary
import Fluent
import Vapor
import VaporElementary
func routes(_ app: Application) throws {
try app.register(collection: ApiController())
// try app.register(collection: ViewController())
app.get("test") { _ in
HTMLResponse {
MainPage(displayNav: false) {
div(.class("container")) {
h1 { "iT WORKS" }
}
}
}
}
app.get("login") { _ in
HTMLResponse {
MainPage(displayNav: false) {
UserForm(context: .login(next: nil))
}
}
}
app.get("users") { _ in
HTMLResponse {
MainPage(displayNav: false) {
UserTable()
}
}
}
}