diff --git a/Sources/DatabaseClient/Equipment.swift b/Sources/DatabaseClient/Equipment.swift new file mode 100644 index 0000000..1367836 --- /dev/null +++ b/Sources/DatabaseClient/Equipment.swift @@ -0,0 +1,162 @@ +import Dependencies +import DependenciesMacros +import Fluent +import Foundation +import ManualDCore + +extension DatabaseClient { + @DependencyClient + public struct Equipment: Sendable { + public var create: @Sendable (EquipmentInfo.Create) async throws -> EquipmentInfo + public var delete: @Sendable (EquipmentInfo.ID) async throws -> Void + public var fetch: @Sendable (Project.ID) async throws -> EquipmentInfo? + public var get: @Sendable (EquipmentInfo.ID) async throws -> EquipmentInfo? + } +} + +extension DatabaseClient.Equipment: TestDependencyKey { + public static let testValue = Self() +} + +extension DatabaseClient.Equipment { + 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 EquipmentModel.find(id, on: database) else { + throw NotFoundError() + } + try await model.delete(on: database) + }, + fetch: { projectId in + guard + let model = try await EquipmentModel.query(on: database) + .filter("projectID", .equal, projectId) + .first() + else { + throw NotFoundError() + } + return try model.toDTO() + }, + get: { id in + try await EquipmentModel.find(id, on: database).map { try $0.toDTO() } + } + ) + } +} + +extension EquipmentInfo.Create { + + func toModel() throws(ValidationError) -> EquipmentModel { + try validate() + return .init( + staticPressure: staticPressure, + heatingCFM: heatingCFM, + coolingCFM: coolingCFM, + projectID: projectID + ) + } + + func validate() throws(ValidationError) { + guard staticPressure >= 0 else { + throw ValidationError("Equipment info static pressure should be greater than 0.") + } + guard staticPressure <= 1.0 else { + throw ValidationError("Equipment info static pressure should be less than 1.0.") + } + guard heatingCFM >= 0 else { + throw ValidationError("Equipment info heating CFM should be greater than 0.") + } + guard coolingCFM >= 0 else { + throw ValidationError("Equipment info heating CFM should be greater than 0.") + } + } +} + +extension EquipmentInfo { + + struct Migrate: AsyncMigration { + + let name = "CreateEquipment" + + func prepare(on database: any Database) async throws { + try await database.schema(EquipmentModel.schema) + .id() + .field("staticPressure", .double, .required) + .field("heatingCFM", .int16, .required) + .field("coolingCFM", .int16, .required) + .field("createdAt", .datetime) + .field("updatedAt", .datetime) + .foreignKey("projectID", references: ProjectModel.schema, "id", onDelete: .cascade) + .unique(on: "projectID") + .create() + } + + func revert(on database: any Database) async throws { + try await database.schema(EquipmentModel.schema).delete() + } + + } +} + +final class EquipmentModel: Model, @unchecked Sendable { + + static let schema = "equipment" + + @ID(key: .id) + var id: UUID? + + @Field(key: "staticPressure") + var staticPressure: Double + + @Field(key: "heatingCFM") + var heatingCFM: Int + + @Field(key: "coolingCFM") + var coolingCFM: 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, + staticPressure: Double, + heatingCFM: Int, + coolingCFM: Int, + createdAt: Date? = nil, + updatedAt: Date? = nil, + projectID: Project.ID + ) { + self.id = id + self.staticPressure = staticPressure + self.heatingCFM = heatingCFM + self.coolingCFM = coolingCFM + self.createdAt = createdAt + self.updatedAt = updatedAt + $project.id = projectID + } + + func toDTO() throws -> EquipmentInfo { + try .init( + id: requireID(), + projectID: $project.id, + staticPressure: staticPressure, + heatingCFM: heatingCFM, + coolingCFM: coolingCFM, + createdAt: createdAt!, + updatedAt: updatedAt! + ) + } +} diff --git a/Sources/DatabaseClient/Interface.swift b/Sources/DatabaseClient/Interface.swift index ad06e59..91ffff9 100644 --- a/Sources/DatabaseClient/Interface.swift +++ b/Sources/DatabaseClient/Interface.swift @@ -15,20 +15,23 @@ public struct DatabaseClient: Sendable { public var migrations: Migrations public var projects: Projects public var rooms: Rooms + public var equipment: Equipment } extension DatabaseClient: TestDependencyKey { public static let testValue: DatabaseClient = Self( migrations: .testValue, projects: .testValue, - rooms: .testValue + rooms: .testValue, + equipment: .testValue ) public static func live(database: any Database) -> Self { .init( migrations: .liveValue, projects: .live(database: database), - rooms: .live(database: database) + rooms: .live(database: database), + equipment: .live(database: database) ) } } @@ -48,6 +51,7 @@ extension DatabaseClient.Migrations: DependencyKey { public static let liveValue = Self( run: { [ + EquipmentInfo.Migrate(), Project.Migrate(), Room.Migrate(), ] diff --git a/Sources/ManualDCore/EquipmentInfo.swift b/Sources/ManualDCore/EquipmentInfo.swift index 9da06bf..686721d 100644 --- a/Sources/ManualDCore/EquipmentInfo.swift +++ b/Sources/ManualDCore/EquipmentInfo.swift @@ -1,17 +1,52 @@ import Foundation -public struct EquipmentInfo: Codable, Equatable, Sendable { +public struct EquipmentInfo: Codable, Equatable, Identifiable, Sendable { + public let id: UUID + public let projectID: Project.ID public let staticPressure: Double public let heatingCFM: Int public let coolingCFM: Int + public let createdAt: Date + public let updatedAt: Date public init( + id: UUID, + projectID: Project.ID, staticPressure: Double = 0.5, heatingCFM: Int, - coolingCFM: Int + coolingCFM: Int, + createdAt: Date, + updatedAt: Date ) { + self.id = id + self.projectID = projectID self.staticPressure = staticPressure self.heatingCFM = heatingCFM self.coolingCFM = coolingCFM + self.createdAt = createdAt + self.updatedAt = updatedAt + } +} + +extension EquipmentInfo { + + // TODO: Remove projectID and use dependency to lookup current project ?? + public struct Create: Codable, Equatable, Sendable { + public let projectID: Project.ID + public let staticPressure: Double + public let heatingCFM: Int + public let coolingCFM: Int + + public init( + projectID: Project.ID, + staticPressure: Double = 0.5, + heatingCFM: Int, + coolingCFM: Int + ) { + self.projectID = projectID + self.staticPressure = staticPressure + self.heatingCFM = heatingCFM + self.coolingCFM = coolingCFM + } } } diff --git a/Sources/ManualDCore/Routes/ApiRoute.swift b/Sources/ManualDCore/Routes/ApiRoute.swift index a7927cd..1f3e7ed 100644 --- a/Sources/ManualDCore/Routes/ApiRoute.swift +++ b/Sources/ManualDCore/Routes/ApiRoute.swift @@ -100,3 +100,44 @@ extension SiteRoute.Api { } } } + +extension SiteRoute.Api { + + public enum EquipmentRoute: Sendable, Equatable { + case create(EquipmentInfo.Create) + case delete(id: EquipmentInfo.ID) + case fetch(projectID: Project.ID) + case get(id: EquipmentInfo.ID) + + static let rootPath = "rooms" + + public static let router = OneOf { + Route(.case(Self.create)) { + Path { rootPath } + Method.post + Body(.json(EquipmentInfo.Create.self)) + } + Route(.case(Self.delete(id:))) { + Path { + rootPath + EquipmentInfo.ID.parser() + } + Method.delete + } + Route(.case(Self.fetch(projectID:))) { + Path { rootPath } + Method.get + Query { + Field("projectID") { Project.ID.parser() } + } + } + Route(.case(Self.get(id:))) { + Path { + rootPath + EquipmentInfo.ID.parser() + } + Method.get + } + } + } +}