feat: Adds component pressure loss to database client and api routes.

This commit is contained in:
2025-12-29 17:04:25 -05:00
parent a2514853a6
commit 6eedb7396d
4 changed files with 249 additions and 3 deletions

View File

@@ -0,0 +1,146 @@
import Dependencies
import DependenciesMacros
import Fluent
import Foundation
import ManualDCore
extension DatabaseClient {
@DependencyClient
public struct ComponentLoss: Sendable {
public var create:
@Sendable (ComponentPressureLoss.Create) async throws -> ComponentPressureLoss
public var delete: @Sendable (ComponentPressureLoss.ID) async throws -> Void
public var fetch: @Sendable (Project.ID) async throws -> ComponentPressureLoss
public var get: @Sendable (ComponentPressureLoss.ID) async throws -> ComponentPressureLoss?
}
}
extension DatabaseClient.ComponentLoss: TestDependencyKey {
public static let testValue = Self()
}
extension DatabaseClient.ComponentLoss {
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 ComponentLossModel.find(id, on: database) else {
throw NotFoundError()
}
try await model.delete(on: database)
},
fetch: { projectID in
guard
let model = try await ComponentLossModel.query(on: database)
.filter("projectID", .equal, projectID)
.first()
else {
throw NotFoundError()
}
return try model.toDTO()
},
get: { id in
try await ComponentLossModel.find(id, on: database).map { try $0.toDTO() }
}
)
}
}
extension ComponentPressureLoss.Create {
func toModel() throws(ValidationError) -> ComponentLossModel {
try validate()
return .init(name: name, value: value, projectID: projectID)
}
func validate() throws(ValidationError) {
guard !name.isEmpty else {
throw ValidationError("Component loss name should not be empty.")
}
guard value > 0 else {
throw ValidationError("Component loss value should be greater than 0.")
}
guard value < 1.0 else {
throw ValidationError("Component loss value should be less than 1.0.")
}
}
}
extension ComponentPressureLoss {
struct Migrate: AsyncMigration {
let name = "CreateComponentLoss"
func prepare(on database: any Database) async throws {
try await database.schema(ComponentLossModel.schema)
.id()
.field("name", .string, .required)
.field("value", .double, .required)
.field("createdAt", .datetime)
.field("updatedAt", .datetime)
.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(ComponentLossModel.schema).delete()
}
}
}
final class ComponentLossModel: Model, @unchecked Sendable {
static let schema = "component_loss"
@ID(key: .id)
var id: UUID?
@Field(key: "name")
var name: String
@Field(key: "value")
var value: Double
@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,
value: Double,
createdAt: Date? = nil,
updatedAt: Date? = nil,
projectID: Project.ID
) {
self.id = id
self.name = name
self.value = value
self.createdAt = createdAt
self.updatedAt = updatedAt
$project.id = projectID
}
func toDTO() throws -> ComponentPressureLoss {
try .init(
id: requireID(),
projectID: $project.id,
name: name,
value: value,
createdAt: createdAt!,
updatedAt: updatedAt!
)
}
}

View File

@@ -16,6 +16,7 @@ public struct DatabaseClient: Sendable {
public var projects: Projects
public var rooms: Rooms
public var equipment: Equipment
public var componentLoss: ComponentLoss
}
extension DatabaseClient: TestDependencyKey {
@@ -23,7 +24,8 @@ extension DatabaseClient: TestDependencyKey {
migrations: .testValue,
projects: .testValue,
rooms: .testValue,
equipment: .testValue
equipment: .testValue,
componentLoss: .testValue
)
public static func live(database: any Database) -> Self {
@@ -31,7 +33,8 @@ extension DatabaseClient: TestDependencyKey {
migrations: .liveValue,
projects: .live(database: database),
rooms: .live(database: database),
equipment: .live(database: database)
equipment: .live(database: database),
componentLoss: .live(database: database)
)
}
}
@@ -51,6 +54,7 @@ extension DatabaseClient.Migrations: DependencyKey {
public static let liveValue = Self(
run: {
[
ComponentPressureLoss.Migrate(),
EquipmentInfo.Migrate(),
Project.Migrate(),
Room.Migrate(),

View File

@@ -1,5 +1,50 @@
import Foundation
public struct ComponentPressureLoss: Codable, Equatable, Identifiable, Sendable {
public let id: UUID
public let projectID: Project.ID
public let name: String
public let value: Double
public let createdAt: Date
public let updatedAt: Date
public init(
id: UUID,
projectID: Project.ID,
name: String,
value: Double,
createdAt: Date,
updatedAt: Date
) {
self.id = id
self.projectID = projectID
self.name = name
self.value = value
self.createdAt = createdAt
self.updatedAt = updatedAt
}
}
extension ComponentPressureLoss {
public struct Create: Codable, Equatable, Sendable {
public let projectID: Project.ID
public let name: String
public let value: Double
public init(
projectID: Project.ID,
name: String,
value: Double,
) {
self.projectID = projectID
self.name = name
self.value = value
}
}
}
public typealias ComponentPressureLosses = [String: Double]
#if DEBUG

View File

@@ -10,6 +10,8 @@ extension SiteRoute {
case project(Self.ProjectRoute)
case room(Self.RoomRoute)
case equipment(Self.EquipmentRoute)
case componentLoss(Self.ComponentLossRoute)
public static let rootPath = Path {
"api"
@@ -25,6 +27,14 @@ extension SiteRoute {
rootPath
RoomRoute.router
}
Route(.case(Self.equipment)) {
rootPath
EquipmentRoute.router
}
Route(.case(Self.componentLoss)) {
rootPath
ComponentLossRoute.router
}
}
}
@@ -109,7 +119,7 @@ extension SiteRoute.Api {
case fetch(projectID: Project.ID)
case get(id: EquipmentInfo.ID)
static let rootPath = "rooms"
static let rootPath = "equipment"
public static let router = OneOf {
Route(.case(Self.create)) {
@@ -141,3 +151,44 @@ extension SiteRoute.Api {
}
}
}
extension SiteRoute.Api {
public enum ComponentLossRoute: Sendable, Equatable {
case create(ComponentPressureLoss.Create)
case delete(id: ComponentPressureLoss.ID)
case fetch(projectID: Project.ID)
case get(id: ComponentPressureLoss.ID)
static let rootPath = "componentLoss"
public static let router = OneOf {
Route(.case(Self.create)) {
Path { rootPath }
Method.post
Body(.json(ComponentPressureLoss.Create.self))
}
Route(.case(Self.delete(id:))) {
Path {
rootPath
ComponentPressureLoss.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
ComponentPressureLoss.ID.parser()
}
Method.get
}
}
}
}