diff --git a/Package.resolved b/Package.resolved index d445755..aa4bd68 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "c59e2480163811f4420fdf7da7e204df5b85f36e4d4106acb2ef4eedb9a90e2e", + "originHash" : "8af359d8d0bd04e1f21c9125fce09eb7d6a61a45a372b7485f4ecf4068fb7a53", "pins" : [ { "identity" : "async-http-client", @@ -19,6 +19,15 @@ "version" : "1.20.0" } }, + { + "identity" : "combine-schedulers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/combine-schedulers", + "state" : { + "revision" : "5928286acce13def418ec36d05a001a9641086f2", + "version" : "1.0.3" + } + }, { "identity" : "console-kit", "kind" : "remoteSourceControl", @@ -145,6 +154,15 @@ "version" : "1.2.0" } }, + { + "identity" : "swift-clocks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-clocks", + "state" : { + "revision" : "cc46202b53476d64e824e0b6612da09d84ffde8e", + "version" : "1.0.6" + } + }, { "identity" : "swift-collections", "kind" : "remoteSourceControl", @@ -154,6 +172,15 @@ "version" : "1.1.4" } }, + { + "identity" : "swift-concurrency-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-concurrency-extras", + "state" : { + "revision" : "82a4ae7170d98d8538ec77238b7eb8e7199ef2e8", + "version" : "1.3.1" + } + }, { "identity" : "swift-crypto", "kind" : "remoteSourceControl", @@ -163,6 +190,15 @@ "version" : "3.10.0" } }, + { + "identity" : "swift-dependencies", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-dependencies.git", + "state" : { + "revision" : "85f89f5d0ce5a18945f65371d40ca997da85a41a", + "version" : "1.6.3" + } + }, { "identity" : "swift-distributed-tracing", "kind" : "remoteSourceControl", @@ -262,6 +298,15 @@ "version" : "1.1.0" } }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax", + "state" : { + "revision" : "0687f71944021d616d34d922343dcef086855920", + "version" : "600.0.1" + } + }, { "identity" : "swift-system", "kind" : "remoteSourceControl", @@ -288,6 +333,15 @@ "revision" : "4232d34efa49f633ba61afde365d3896fc7f8740", "version" : "2.15.0" } + }, + { + "identity" : "xctest-dynamic-overlay", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state" : { + "revision" : "a3f634d1a409c7979cabc0a71b3f26ffa9fc8af1", + "version" : "1.4.3" + } } ], "version" : 3 diff --git a/Package.swift b/Package.swift index 80466ae..b0b7166 100644 --- a/Package.swift +++ b/Package.swift @@ -2,48 +2,52 @@ import PackageDescription let package = Package( - name: "hhe-po", - platforms: [ - .macOS(.v13) - ], - dependencies: [ - // 💧 A server-side Swift web framework. - .package(url: "https://github.com/vapor/vapor.git", from: "4.110.1"), - // 🗄 An ORM for SQL and NoSQL databases. - .package(url: "https://github.com/vapor/fluent.git", from: "4.9.0"), - // ðŸŠķ Fluent driver for SQLite. - .package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.6.0"), - // 🍃 An expressive, performant, and extensible templating language built for Swift. - .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"), - ], - targets: [ - .executableTarget( - name: "App", - dependencies: [ - .product(name: "Fluent", package: "fluent"), - .product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"), - .product(name: "Leaf", package: "leaf"), - .product(name: "Vapor", package: "vapor"), - .product(name: "NIOCore", package: "swift-nio"), - .product(name: "NIOPosix", package: "swift-nio"), - ], - swiftSettings: swiftSettings - ), - .testTarget( - name: "AppTests", - dependencies: [ - .target(name: "App"), - .product(name: "VaporTesting", package: "vapor"), - ], - swiftSettings: swiftSettings - ) - ], - swiftLanguageModes: [.v5] + name: "hhe-po", + platforms: [ + .macOS(.v13) + ], + dependencies: [ + // 💧 A server-side Swift web framework. + .package(url: "https://github.com/vapor/vapor.git", from: "4.110.1"), + // 🗄 An ORM for SQL and NoSQL databases. + .package(url: "https://github.com/vapor/fluent.git", from: "4.9.0"), + // ðŸŠķ Fluent driver for SQLite. + .package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.6.0"), + // 🍃 An expressive, performant, and extensible templating language built for Swift. + .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") + ], + targets: [ + .executableTarget( + name: "App", + dependencies: [ + .product(name: "Fluent", package: "fluent"), + .product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"), + .product(name: "Leaf", package: "leaf"), + .product(name: "Vapor", package: "vapor"), + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "NIOPosix", package: "swift-nio"), + .product(name: "Dependencies", package: "swift-dependencies"), + .product(name: "DependenciesMacros", package: "swift-dependencies") + + ], + swiftSettings: swiftSettings + ), + .testTarget( + name: "AppTests", + dependencies: [ + .target(name: "App"), + .product(name: "VaporTesting", package: "vapor") + ], + swiftSettings: swiftSettings + ) + ], + swiftLanguageModes: [.v5] ) var swiftSettings: [SwiftSetting] { [ - .enableUpcomingFeature("DisableOutwardActorInference"), - .enableExperimentalFeature("StrictConcurrency"), + .enableUpcomingFeature("DisableOutwardActorInference"), + .enableExperimentalFeature("StrictConcurrency") ] } diff --git a/Sources/App/Controllers/Api/EmployeeApiController.swift b/Sources/App/Controllers/Api/EmployeeApiController.swift index 215a577..dd4692a 100644 --- a/Sources/App/Controllers/Api/EmployeeApiController.swift +++ b/Sources/App/Controllers/Api/EmployeeApiController.swift @@ -1,9 +1,10 @@ +import Dependencies import Fluent import Vapor struct EmployeeApiController: RouteCollection { - private let employeeDB = EmployeeDB() + @Dependency(\.employees) var employees func boot(routes: any RoutesBuilder) throws { let protected = routes.apiProtected(route: "employees") @@ -19,20 +20,20 @@ struct EmployeeApiController: RouteCollection { @Sendable func index(req: Request) async throws -> [Employee.DTO] { let params = try req.query.decode(EmployeesIndexQuery.self) - return try await employeeDB.fetchAll(active: params.active, on: req.db) + return try await employees.fetchAll(params.active ?? false) } @Sendable func create(req: Request) async throws -> Employee.DTO { try Employee.Create.validate(content: req) let create = try req.content.decode(Employee.Create.self) - return try await employeeDB.create(create, on: req.db) + return try await employees.create(create) } @Sendable func get(req: Request) async throws -> Employee.DTO { guard let id = req.parameters.get("employeeID", as: Employee.IDValue.self), - let employee = try await employeeDB.get(id: id, on: req.db) + let employee = try await employees.get(id) else { throw Abort(.notFound) } @@ -46,7 +47,7 @@ struct EmployeeApiController: RouteCollection { throw Abort(.badRequest, reason: "Employee id value not provided") } let updates = try req.content.decode(Employee.Update.self) - return try await employeeDB.update(id: employeeID, with: updates, on: req.db) + return try await employees.update(employeeID, updates) } @Sendable @@ -54,7 +55,7 @@ struct EmployeeApiController: RouteCollection { guard let employeeID = req.parameters.get("employeeID", as: Employee.IDValue.self) else { throw Abort(.badRequest, reason: "Employee id value not provided") } - try await employeeDB.delete(id: employeeID, on: req.db) + try await employees.delete(employeeID) return .ok } } diff --git a/Sources/App/Controllers/DB/EmployeeDB.swift b/Sources/App/Controllers/DB/EmployeeDB.swift index f5e3e59..930038d 100644 --- a/Sources/App/Controllers/DB/EmployeeDB.swift +++ b/Sources/App/Controllers/DB/EmployeeDB.swift @@ -1,49 +1,114 @@ +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 (Bool) 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 + + func fetchAll() async throws -> [Employee.DTO] { + try await fetchAll(false) + } +} + +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: { active in + var query = Employee.query(on: database) + .sort(\.$lastName) + + if active { + query = query.filter(\.$active == active) + } + + 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) + } + ) + } +} + // An intermediate layer between our api and view controllers that interacts with the // database model. -struct EmployeeDB { - - func create(_ model: Employee.Create, on db: any Database) async throws -> Employee.DTO { - let model = model.toModel() - try await model.save(on: db) - return model.toDTO() - } - - func fetchAll(active: Bool? = nil, on db: any Database) async throws -> [Employee.DTO] { - var query = Employee.query(on: db) - .sort(\.$lastName) - - if let active { - query = query.filter(\.$active == active) - } - - return try await query.all().map { $0.toDTO() } - } - - func get(id: Employee.IDValue, on db: any Database) async throws -> Employee.DTO? { - try await Employee.find(id, on: db).map { $0.toDTO() } - } - - func update( - id: Employee.IDValue, - with updates: Employee.Update, - on db: any Database - ) async throws -> Employee.DTO { - guard let employee = try await Employee.find(id, on: db) else { - throw Abort(.badRequest, reason: "Employee id not found.") - } - employee.applyUpdates(updates) - try await employee.save(on: db) - return employee.toDTO() - } - - func delete(id: Employee.IDValue, on db: any Database) async throws { - guard let employee = try await Employee.find(id, on: db) else { - throw Abort(.badRequest, reason: "Employee id not found.") - } - try await employee.delete(on: db) - } - -} +// struct EmployeeDB { +// +// func create(_ model: Employee.Create, on db: any Database) async throws -> Employee.DTO { +// let model = model.toModel() +// try await model.save(on: db) +// return model.toDTO() +// } +// +// func fetchAll(active: Bool? = nil, on db: any Database) async throws -> [Employee.DTO] { +// var query = Employee.query(on: db) +// .sort(\.$lastName) +// +// if let active { +// query = query.filter(\.$active == active) +// } +// +// return try await query.all().map { $0.toDTO() } +// } +// +// func get(id: Employee.IDValue, on db: any Database) async throws -> Employee.DTO? { +// try await Employee.find(id, on: db).map { $0.toDTO() } +// } +// +// func update( +// id: Employee.IDValue, +// with updates: Employee.Update, +// on db: any Database +// ) async throws -> Employee.DTO { +// guard let employee = try await Employee.find(id, on: db) else { +// throw Abort(.badRequest, reason: "Employee id not found.") +// } +// employee.applyUpdates(updates) +// try await employee.save(on: db) +// return employee.toDTO() +// } +// +// func delete(id: Employee.IDValue, on db: any Database) async throws { +// guard let employee = try await Employee.find(id, on: db) else { +// throw Abort(.badRequest, reason: "Employee id not found.") +// } +// try await employee.delete(on: db) +// } +// +// } diff --git a/Sources/App/Controllers/View/EmployeeViewController.swift b/Sources/App/Controllers/View/EmployeeViewController.swift index dd1ebbe..2f680ba 100644 --- a/Sources/App/Controllers/View/EmployeeViewController.swift +++ b/Sources/App/Controllers/View/EmployeeViewController.swift @@ -1,10 +1,11 @@ +import Dependencies import Fluent import Leaf import Vapor struct EmployeeViewController: RouteCollection { - private let employees = EmployeeDB() + @Dependency(\.employees) var employees private let api = EmployeeApiController() func boot(routes: any RoutesBuilder) throws { @@ -22,16 +23,15 @@ struct EmployeeViewController: RouteCollection { @Sendable func index(req: Request) async throws -> View { - return try await req.view.render("employees/index", EmployeesCTX(db: employees, req: req)) + return try await req.view.render("employees/index", EmployeesCTX()) } @Sendable func create(req: Request) async throws -> View { try Employee.Create.validate(content: req) let model = try req.content.decode(Employee.Create.self) - _ = try await employees.create(model, on: req.db) - // _ = try await db.createEmployee(req: req) - return try await req.view.render("employees/index", EmployeesCTX(oob: true, db: employees, req: req)) + _ = try await employees.create(model) + return try await req.view.render("employees/index", EmployeesCTX(oob: true)) } @Sendable @@ -41,14 +41,14 @@ struct EmployeeViewController: RouteCollection { } employee.active.toggle() try await employee.save(on: req.db) - let employees = try await employees.fetchAll(on: req.db) + let employees = try await employees.fetchAll() return try await req.view.render("employees/table", ["employees": employees]) } @Sendable func delete(req: Request) async throws -> View { _ = try await api.delete(req: req) - let employees = try await employees.fetchAll(on: req.db) + let employees = try await employees.fetchAll() return try await req.view.render("employees/table", ["employees": employees]) } @@ -63,7 +63,7 @@ struct EmployeeViewController: RouteCollection { @Sendable func update(req: Request) async throws -> View { _ = try await api.update(req: req) - return try await req.view.render("employees/index", EmployeesCTX(oob: true, db: employees, req: req)) + return try await req.view.render("employees/index", EmployeesCTX(oob: true)) } @Sendable @@ -80,12 +80,11 @@ private struct EmployeesCTX: Content { init( oob: Bool = false, - employee: Employee? = nil, - db: EmployeeDB, - req: Request + employee: Employee? = nil ) async throws { + @Dependency(\.employees) var employees self.oob = oob - self.employees = try await db.fetchAll(on: req.db) + self.employees = try await employees.fetchAll() self.form = .init(employee: employee.map { $0.toDTO() }) } } diff --git a/Sources/App/configure.swift b/Sources/App/configure.swift index 5360513..5b1e77b 100644 --- a/Sources/App/configure.swift +++ b/Sources/App/configure.swift @@ -1,3 +1,4 @@ +import Dependencies import Fluent import FluentSQLiteDriver import Leaf @@ -31,8 +32,12 @@ public func configure(_ app: Application) async throws { app.views.use(.leaf) - // register routes - try routes(app) + try withDependencies { + $0.employees = .live(database: app.db(.sqlite)) + } operation: { + // register routes + try routes(app) + } if app.environment != .production { try await app.autoMigrate() diff --git a/Sources/App/entrypoint.swift b/Sources/App/entrypoint.swift index 846c5b3..6b67cec 100644 --- a/Sources/App/entrypoint.swift +++ b/Sources/App/entrypoint.swift @@ -25,6 +25,7 @@ enum Entrypoint { try? await app.asyncShutdown() throw error } + try await app.execute() try await app.asyncShutdown() }