diff --git a/Sources/DatabaseClient/Interface.swift b/Sources/DatabaseClient/Interface.swift index 9b4db44..ad06e59 100644 --- a/Sources/DatabaseClient/Interface.swift +++ b/Sources/DatabaseClient/Interface.swift @@ -14,6 +14,23 @@ extension DependencyValues { public struct DatabaseClient: Sendable { public var migrations: Migrations public var projects: Projects + public var rooms: Rooms +} + +extension DatabaseClient: TestDependencyKey { + public static let testValue: DatabaseClient = Self( + migrations: .testValue, + projects: .testValue, + rooms: .testValue + ) + + public static func live(database: any Database) -> Self { + .init( + migrations: .liveValue, + projects: .live(database: database), + rooms: .live(database: database) + ) + } } extension DatabaseClient { @@ -23,13 +40,17 @@ extension DatabaseClient { } } -extension DatabaseClient: TestDependencyKey { - public static let testValue: DatabaseClient = Self( - migrations: .testValue, - projects: .testValue - ) -} - extension DatabaseClient.Migrations: TestDependencyKey { public static let testValue = Self() } + +extension DatabaseClient.Migrations: DependencyKey { + public static let liveValue = Self( + run: { + [ + Project.Migrate(), + Room.Migrate(), + ] + } + ) +} diff --git a/Sources/DatabaseClient/Projects.swift b/Sources/DatabaseClient/Projects.swift index 34902ff..079cafa 100644 --- a/Sources/DatabaseClient/Projects.swift +++ b/Sources/DatabaseClient/Projects.swift @@ -26,13 +26,13 @@ extension DatabaseClient.Projects { return try model.toDTO() }, delete: { id in - guard let model = ProjectModel.find(id, on: database) else { + guard let model = try await 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() } + try await ProjectModel.find(id, on: database).map { try $0.toDTO() } } ) } diff --git a/Sources/DatabaseClient/Rooms.swift b/Sources/DatabaseClient/Rooms.swift new file mode 100644 index 0000000..590abde --- /dev/null +++ b/Sources/DatabaseClient/Rooms.swift @@ -0,0 +1,165 @@ +import Dependencies +import DependenciesMacros +import Fluent +import Foundation +import ManualDCore + +extension DatabaseClient { + @DependencyClient + public struct Rooms: Sendable { + public var create: @Sendable (Room.Create) async throws -> Room + public var delete: @Sendable (Room.ID) async throws -> Void + public var get: @Sendable (Room.ID) async throws -> Room? + } +} + +extension DatabaseClient.Rooms: TestDependencyKey { + public static let testValue = Self() +} + +extension DatabaseClient.Rooms { + 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 = try await RoomModel.find(id, on: database) else { + throw NotFoundError() + } + try await model.delete(on: database) + }, + get: { id in + try await RoomModel.find(id, on: database).map { try $0.toDTO() } + } + ) + } +} + +extension Room.Create { + + func toModel() throws(ValidationError) -> RoomModel { + try validate() + return .init( + name: name, + heatingLoad: heatingLoad, + coolingTotal: coolingTotal, + coolingSensible: coolingSensible, + registerCount: registerCount, + projectID: projectID + ) + } + + func validate() throws(ValidationError) { + guard !name.isEmpty else { + throw ValidationError("Room name should not be empty.") + } + guard heatingLoad >= 0 else { + throw ValidationError("Room heating load should not be less than 0.") + } + guard coolingTotal >= 0 else { + throw ValidationError("Room cooling total should not be less than 0.") + } + guard coolingSensible >= 0 else { + throw ValidationError("Room cooling sensible should not be less than 0.") + } + guard registerCount >= 1 else { + throw ValidationError("Room cooling sensible should not be less than 1.") + } + } +} + +extension Room { + struct Migrate: AsyncMigration { + let name = "CreateRoom" + + func prepare(on database: any Database) async throws { + try await database.schema(RoomModel.schema) + .id() + .field("name", .string, .required) + .field("heatingLoad", .double, .required) + .field("coolingTotal", .double, .required) + .field("coolingSensible", .double, .required) + .field("registerCount", .int8, .required) + .foreignKey("projectID", references: ProjectModel.schema, "id", onDelete: .cascade) + .unique(on: "projectID", "name") + .create() + } + + func revert(on database: any Database) async throws { + try await database.schema(RoomModel.schema).delete() + } + } +} + +final class RoomModel: Model, @unchecked Sendable { + + static let schema = "room" + + @ID(key: .id) + var id: UUID? + + @Field(key: "name") + var name: String + + @Field(key: "heatingLoad") + var heatingLoad: Double + + @Field(key: "coolingTotal") + var coolingTotal: Double + + @Field(key: "coolingSensible") + var coolingSensible: Double + + @Field(key: "registerCount") + var registerCount: Int + + @Timestamp(key: "createdAt", on: .create, format: .iso8601) + var createdAt: Date? + + @Timestamp(key: "updatedAt", on: .update, format: .iso8601) + var updatedAt: Date? + + @Parent(key: "projectID") + var project: ProjectModel + + init() {} + + init( + id: UUID? = nil, + name: String, + heatingLoad: Double, + coolingTotal: Double, + coolingSensible: Double, + registerCount: Int, + createdAt: Date? = nil, + updatedAt: Date? = nil, + projectID: Project.ID + ) { + self.id = id + self.name = name + self.heatingLoad = heatingLoad + self.coolingTotal = coolingTotal + self.coolingSensible = coolingSensible + self.registerCount = registerCount + self.createdAt = createdAt + self.updatedAt = updatedAt + $project.id = projectID + } + + func toDTO() throws -> Room { + try .init( + id: requireID(), + projectID: $project.id, + name: name, + heatingLoad: heatingLoad, + coolingLoad: .init(total: coolingTotal, sensible: coolingSensible), + registerCount: registerCount, + createdAt: createdAt!, + updatedAt: updatedAt! + ) + + } +} diff --git a/Sources/ManualDCore/Room.swift b/Sources/ManualDCore/Room.swift index 201f46c..8f4fd56 100644 --- a/Sources/ManualDCore/Room.swift +++ b/Sources/ManualDCore/Room.swift @@ -1,20 +1,61 @@ import Foundation -public struct Room: Codable, Equatable, Sendable { +public struct Room: Codable, Equatable, Identifiable, Sendable { + public let id: UUID + public let projectID: Project.ID public let name: String public let heatingLoad: Double public let coolingLoad: CoolingLoad public let registerCount: Int + public let createdAt: Date + public let updatedAt: Date public init( + id: UUID, + projectID: Project.ID, name: String, heatingLoad: Double, coolingLoad: CoolingLoad, - registerCount: Int = 1 + registerCount: Int = 1, + createdAt: Date, + updatedAt: Date ) { + self.id = id + self.projectID = projectID self.name = name self.heatingLoad = heatingLoad self.coolingLoad = coolingLoad self.registerCount = registerCount + self.createdAt = createdAt + self.updatedAt = updatedAt + } +} + +extension Room { + + // TODO: Maybe remove project ID, and make dependencies that retrieves current project id?? + public struct Create: Codable, Equatable, Sendable { + public let projectID: Project.ID + public let name: String + public let heatingLoad: Double + public let coolingTotal: Double + public let coolingSensible: Double + public let registerCount: Int + + public init( + projectID: Project.ID, + name: String, + heatingLoad: Double, + coolingTotal: Double, + coolingSensible: Double, + registerCount: Int = 1 + ) { + self.projectID = projectID + self.name = name + self.heatingLoad = heatingLoad + self.coolingTotal = coolingTotal + self.coolingSensible = coolingSensible + self.registerCount = registerCount + } } } diff --git a/Sources/ManualDCore/Routes/ApiRoute.swift b/Sources/ManualDCore/Routes/ApiRoute.swift index 65bf19d..a7927cd 100644 --- a/Sources/ManualDCore/Routes/ApiRoute.swift +++ b/Sources/ManualDCore/Routes/ApiRoute.swift @@ -9,6 +9,7 @@ extension SiteRoute { public enum Api: Sendable, Equatable { case project(Self.ProjectRoute) + case room(Self.RoomRoute) public static let rootPath = Path { "api" @@ -20,6 +21,10 @@ extension SiteRoute { rootPath ProjectRoute.router } + Route(.case(Self.room)) { + rootPath + RoomRoute.router + } } } @@ -61,5 +66,37 @@ extension SiteRoute.Api { } } } - +} + +extension SiteRoute.Api { + + public enum RoomRoute: Sendable, Equatable { + case create(Room.Create) + case delete(id: Room.ID) + case get(id: Room.ID) + + static let rootPath = "rooms" + + public static let router = OneOf { + Route(.case(Self.create)) { + Path { rootPath } + Method.post + Body(.json(Room.Create.self)) + } + Route(.case(Self.delete(id:))) { + Path { + rootPath + Room.ID.parser() + } + Method.delete + } + Route(.case(Self.get(id:))) { + Path { + rootPath + Room.ID.parser() + } + Method.get + } + } + } }