diff --git a/Package.resolved b/Package.resolved index 1be6d83..a4c6bec 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,6 +1,24 @@ { - "originHash" : "6db0ff1757d16de886ae50dadf070f0d2ada4d31b5765536ba4266e399ed7a67", + "originHash" : "f8ca659e4ec9041ea590b94c8dc267718be8941a4594eb74964b6396e987ca95", "pins" : [ + { + "identity" : "async-http-client", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swift-server/async-http-client.git", + "state" : { + "revision" : "5dd84c7bb48b348751d7bbe7ba94a17bafdcef37", + "version" : "1.30.2" + } + }, + { + "identity" : "async-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/async-kit.git", + "state" : { + "revision" : "6f3615ccf2ac3c2ae0c8087d527546e9544a43dd", + "version" : "1.21.0" + } + }, { "identity" : "combine-schedulers", "kind" : "remoteSourceControl", @@ -10,6 +28,51 @@ "version" : "1.1.0" } }, + { + "identity" : "console-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/console-kit.git", + "state" : { + "revision" : "742f624a998cba2a9e653d9b1e91ad3f3a5dff6b", + "version" : "4.15.2" + } + }, + { + "identity" : "fluent", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/fluent.git", + "state" : { + "revision" : "2fe9e36daf4bdb5edcf193e0d0806ba2074d2864", + "version" : "4.13.0" + } + }, + { + "identity" : "fluent-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/fluent-kit.git", + "state" : { + "revision" : "0272fdaf7cf6f482c2799026c0695f5fe40e3e8c", + "version" : "1.53.0" + } + }, + { + "identity" : "fluent-sqlite-driver", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/fluent-sqlite-driver.git", + "state" : { + "revision" : "73529a63ab11c7fe87da17b5a67a1b1f58c020f8", + "version" : "4.8.1" + } + }, + { + "identity" : "multipart-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/multipart-kit.git", + "state" : { + "revision" : "3498e60218e6003894ff95192d756e238c01f44e", + "version" : "4.7.1" + } + }, { "identity" : "opencombine", "kind" : "remoteSourceControl", @@ -19,6 +82,78 @@ "version" : "0.14.0" } }, + { + "identity" : "routing-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/routing-kit.git", + "state" : { + "revision" : "1a10ccea61e4248effd23b6e814999ce7bdf0ee0", + "version" : "4.9.3" + } + }, + { + "identity" : "sql-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/sql-kit.git", + "state" : { + "revision" : "c0ea243ffeb8b5ff9e20a281e44003c6abb8896f", + "version" : "3.34.0" + } + }, + { + "identity" : "sqlite-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/sqlite-kit.git", + "state" : { + "revision" : "f35a863ecc2da5d563b836a9a696b148b0f4169f", + "version" : "4.5.2" + } + }, + { + "identity" : "sqlite-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/sqlite-nio.git", + "state" : { + "revision" : "2ab61385b70da8ed74958ce62fa9ebf0359cb08b", + "version" : "1.12.2" + } + }, + { + "identity" : "swift-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-algorithms.git", + "state" : { + "revision" : "87e50f483c54e6efd60e885f7f5aa946cee68023", + "version" : "1.2.1" + } + }, + { + "identity" : "swift-asn1", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-asn1.git", + "state" : { + "revision" : "810496cf121e525d660cd0ea89a758740476b85f", + "version" : "1.5.1" + } + }, + { + "identity" : "swift-async-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-async-algorithms.git", + "state" : { + "revision" : "6c050d5ef8e1aa6342528460db614e9770d7f804", + "version" : "1.1.1" + } + }, + { + "identity" : "swift-atomics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-atomics.git", + "state" : { + "revision" : "b601256eab081c0f92f059e12818ac1d4f178ff7", + "version" : "1.3.0" + } + }, { "identity" : "swift-case-paths", "kind" : "remoteSourceControl", @@ -28,6 +163,15 @@ "version" : "1.7.2" } }, + { + "identity" : "swift-certificates", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-certificates.git", + "state" : { + "revision" : "133a347911b6ad0fc8fe3bf46ca90c66cff97130", + "version" : "1.17.0" + } + }, { "identity" : "swift-clocks", "kind" : "remoteSourceControl", @@ -55,6 +199,15 @@ "version" : "1.3.2" } }, + { + "identity" : "swift-crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-crypto.git", + "state" : { + "revision" : "6f70fa9eab24c1fd982af18c281c4525d05e3095", + "version" : "4.2.0" + } + }, { "identity" : "swift-dependencies", "kind" : "remoteSourceControl", @@ -64,6 +217,105 @@ "version" : "1.10.0" } }, + { + "identity" : "swift-distributed-tracing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-distributed-tracing.git", + "state" : { + "revision" : "baa932c1336f7894145cbaafcd34ce2dd0b77c97", + "version" : "1.3.1" + } + }, + { + "identity" : "swift-http-structured-headers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-structured-headers.git", + "state" : { + "revision" : "76d7627bd88b47bf5a0f8497dd244885960dde0b", + "version" : "1.6.0" + } + }, + { + "identity" : "swift-http-types", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-types.git", + "state" : { + "revision" : "45eb0224913ea070ec4fba17291b9e7ecf4749ca", + "version" : "1.5.1" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "bc386b95f2a16ccd0150a8235e7c69eab2b866ca", + "version" : "1.8.0" + } + }, + { + "identity" : "swift-metrics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-metrics.git", + "state" : { + "revision" : "0743a9364382629da3bf5677b46a2c4b1ce5d2a6", + "version" : "2.7.1" + } + }, + { + "identity" : "swift-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio.git", + "state" : { + "revision" : "a1605a3303a28e14d822dec8aaa53da8a9490461", + "version" : "2.92.0" + } + }, + { + "identity" : "swift-nio-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-extras.git", + "state" : { + "revision" : "1c90641b02b6ab47c6d0db2063a12198b04e83e2", + "version" : "1.31.2" + } + }, + { + "identity" : "swift-nio-http2", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-http2.git", + "state" : { + "revision" : "c2ba4cfbb83f307c66f5a6df6bb43e3c88dfbf80", + "version" : "1.39.0" + } + }, + { + "identity" : "swift-nio-ssl", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-ssl.git", + "state" : { + "revision" : "173cc69a058623525a58ae6710e2f5727c663793", + "version" : "2.36.0" + } + }, + { + "identity" : "swift-nio-transport-services", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-transport-services.git", + "state" : { + "revision" : "60c3e187154421171721c1a38e800b390680fb5d", + "version" : "1.26.0" + } + }, + { + "identity" : "swift-numerics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-numerics.git", + "state" : { + "revision" : "0c0290ff6b24942dadb83a929ffaaa1481df04a2", + "version" : "1.1.1" + } + }, { "identity" : "swift-parsing", "kind" : "remoteSourceControl", @@ -73,6 +325,24 @@ "version" : "0.14.1" } }, + { + "identity" : "swift-service-context", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-service-context.git", + "state" : { + "revision" : "1983448fefc717a2bc2ebde5490fe99873c5b8a6", + "version" : "1.2.1" + } + }, + { + "identity" : "swift-service-lifecycle", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swift-server/swift-service-lifecycle.git", + "state" : { + "revision" : "1de37290c0ab3c5a96028e0f02911b672fd42348", + "version" : "2.9.1" + } + }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl", @@ -82,6 +352,15 @@ "version" : "602.0.0" } }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system.git", + "state" : { + "revision" : "395a77f0aa927f0ff73941d7ac35f2b46d47c9db", + "version" : "1.6.3" + } + }, { "identity" : "swift-url-routing", "kind" : "remoteSourceControl", @@ -91,6 +370,24 @@ "version" : "0.6.2" } }, + { + "identity" : "vapor", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/vapor.git", + "state" : { + "revision" : "f7090db27390ebc4cadbff06d76fe8ce79d6ece6", + "version" : "4.120.0" + } + }, + { + "identity" : "websocket-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/websocket-kit.git", + "state" : { + "revision" : "8666c92dbbb3c8eefc8008c9c8dcf50bfd302167", + "version" : "2.16.1" + } + }, { "identity" : "xctest-dynamic-overlay", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 347dad2..b15ecd9 100644 --- a/Package.swift +++ b/Package.swift @@ -5,18 +5,32 @@ import PackageDescription let package = Package( name: "swift-manual-d", products: [ - .library(name: "swift-manual-d", targets: ["swift-manual-d"]), .library(name: "ManualDCore", targets: ["ManualDCore"]), .library(name: "ManualDClient", targets: ["ManualDClient"]), ], 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"), + // ðŸ”ĩ Non-blocking, event-driven networking 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", from: "1.0.0"), .package(url: "https://github.com/pointfreeco/swift-url-routing.git", from: "0.6.2"), .package(url: "https://github.com/pointfreeco/swift-case-paths.git", from: "1.6.0"), ], targets: [ .target( - name: "swift-manual-d" + name: "DatabaseClient", + dependencies: [ + .target(name: "ManualDCore"), + .product(name: "Dependencies", package: "swift-dependencies"), + .product(name: "DependenciesMacros", package: "swift-dependencies"), + .product(name: "Fluent", package: "fluent"), + .product(name: "Vapor", package: "vapor"), + ] ), .target( name: "ManualDCore", diff --git a/Sources/DatabaseClient/Errors.swift b/Sources/DatabaseClient/Errors.swift new file mode 100644 index 0000000..76e148e --- /dev/null +++ b/Sources/DatabaseClient/Errors.swift @@ -0,0 +1,11 @@ +import Foundation + +public struct ValidationError: Error { + public let message: String + + public init(_ message: String) { + self.message = message + } +} + +public struct NotFoundError: Error {} diff --git a/Sources/DatabaseClient/Interface.swift b/Sources/DatabaseClient/Interface.swift new file mode 100644 index 0000000..9b4db44 --- /dev/null +++ b/Sources/DatabaseClient/Interface.swift @@ -0,0 +1,35 @@ +import Dependencies +import DependenciesMacros +import FluentKit +import ManualDCore + +extension DependencyValues { + public var database: DatabaseClient { + get { self[DatabaseClient.self] } + set { self[DatabaseClient.self] = newValue } + } +} + +@DependencyClient +public struct DatabaseClient: Sendable { + public var migrations: Migrations + public var projects: Projects +} + +extension DatabaseClient { + @DependencyClient + public struct Migrations: Sendable { + public var run: @Sendable () async throws -> [any AsyncMigration] + } +} + +extension DatabaseClient: TestDependencyKey { + public static let testValue: DatabaseClient = Self( + migrations: .testValue, + projects: .testValue + ) +} + +extension DatabaseClient.Migrations: TestDependencyKey { + public static let testValue = Self() +} diff --git a/Sources/DatabaseClient/Projects.swift b/Sources/DatabaseClient/Projects.swift new file mode 100644 index 0000000..34902ff --- /dev/null +++ b/Sources/DatabaseClient/Projects.swift @@ -0,0 +1,161 @@ +import Dependencies +import DependenciesMacros +import Fluent +import Foundation +import ManualDCore + +extension DatabaseClient { + @DependencyClient + public struct Projects: Sendable { + public var create: @Sendable (Project.Create) async throws -> Project + public var delete: @Sendable (Project.ID) async throws -> Void + public var get: @Sendable (Project.ID) async throws -> Project? + } +} + +extension DatabaseClient.Projects: TestDependencyKey { + public static let testValue = Self() +} + +extension DatabaseClient.Projects { + public static func live(database: any Database) -> Self { + .init( + create: { request in + let model = try request.toModel() + try await model.save(on: database) + return try model.toDTO() + }, + delete: { id in + guard let model = ProjectModel.find(id, on: database) else { + throw NotFoundError() + } + try await model.delete(on: database) + }, + get: { id in + ProjectModel.find(id, on: database).map { try $0.toDTO() } + } + ) + } +} + +extension Project.Create { + + func toModel() throws -> ProjectModel { + try validate() + return .init( + name: name, + streetAddress: streetAddress, + city: city, + state: state, + zipCode: zipCode + ) + } + + func validate() throws(ValidationError) { + guard !name.isEmpty else { + throw ValidationError("Project name should not be empty.") + } + guard !streetAddress.isEmpty else { + throw ValidationError("Project street address should not be empty.") + } + guard !city.isEmpty else { + throw ValidationError("Project city should not be empty.") + } + guard !state.isEmpty else { + throw ValidationError("Project state should not be empty.") + } + guard !zipCode.isEmpty else { + throw ValidationError("Project zipCode should not be empty.") + } + } +} + +extension Project { + struct Migrate: AsyncMigration { + let name = "CreateProject" + + func prepare(on database: any Database) async throws { + try await database.schema(ProjectModel.schema) + .id() + .field("name", .string, .required) + .field("streetAddress", .string, .required) + .field("city", .string, .required) + .field("state", .string, .required) + .field("zipCode", .string, .required) + .field("createdAt", .datetime) + .field("updatedAt", .datetime) + .unique(on: "name") + .create() + } + + func revert(on database: any Database) async throws { + try await database.schema(ProjectModel.schema).delete() + } + } +} + +// The Database model. +final class ProjectModel: Model, @unchecked Sendable { + + static let schema = "project" + + @ID(key: .id) + var id: UUID? + + @Field(key: "name") + var name: String + + @Field(key: "streetAddress") + var streetAddress: String + + @Field(key: "city") + var city: String + + @Field(key: "state") + var state: String + + @Field(key: "zipCode") + var zipCode: String + + @Timestamp(key: "createdAt", on: .create, format: .iso8601) + var createdAt: Date? + + @Timestamp(key: "updatedAt", on: .update, format: .iso8601) + var updatedAt: Date? + + init() {} + + init( + id: UUID? = nil, + name: String, + streetAddress: String, + city: String, + state: String, + zipCode: String, + createdAt: Date? = nil, + updatedAt: Date? = nil + ) { + self.id = id + self.name = name + self.streetAddress = streetAddress + self.city = city + self.city = city + self.state = state + self.zipCode = zipCode + self.createdAt = createdAt + self.updatedAt = updatedAt + } + + func toDTO() throws -> Project { + try .init( + id: requireID(), + name: name, + streetAddress: streetAddress, + city: city, + state: state, + zipCode: zipCode, + createdAt: createdAt!, + updatedAt: updatedAt! + ) + } +} diff --git a/Sources/ManualDCore/Project.swift b/Sources/ManualDCore/Project.swift index 01ae942..0101215 100644 --- a/Sources/ManualDCore/Project.swift +++ b/Sources/ManualDCore/Project.swift @@ -8,6 +8,8 @@ public struct Project: Codable, Equatable, Identifiable, Sendable { public let city: String public let state: String public let zipCode: String + public let createdAt: Date + public let updatedAt: Date public init( id: UUID, @@ -15,7 +17,9 @@ public struct Project: Codable, Equatable, Identifiable, Sendable { streetAddress: String, city: String, state: String, - zipCode: String + zipCode: String, + createdAt: Date, + updatedAt: Date ) { self.id = id self.name = name @@ -23,6 +27,8 @@ public struct Project: Codable, Equatable, Identifiable, Sendable { self.city = city self.state = state self.zipCode = zipCode + self.createdAt = createdAt + self.updatedAt = updatedAt } } diff --git a/Sources/swift-manual-d/swift_manual_d.swift b/Sources/swift-manual-d/swift_manual_d.swift deleted file mode 100644 index 08b22b8..0000000 --- a/Sources/swift-manual-d/swift_manual_d.swift +++ /dev/null @@ -1,2 +0,0 @@ -// The Swift Programming Language -// https://docs.swift.org/swift-book