feat: Adds equipment info to database and api routes.

This commit is contained in:
2025-12-29 16:31:57 -05:00
parent 31930cd399
commit a2514853a6
4 changed files with 246 additions and 4 deletions

View File

@@ -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!
)
}
}

View File

@@ -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(),
]

View File

@@ -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
}
}
}

View File

@@ -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
}
}
}
}