diff --git a/Sources/DatabaseClient/ComponentPressureLoss.swift b/Sources/DatabaseClient/ComponentPressureLoss.swift new file mode 100644 index 0000000..adb0100 --- /dev/null +++ b/Sources/DatabaseClient/ComponentPressureLoss.swift @@ -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! + ) + } +} diff --git a/Sources/DatabaseClient/Interface.swift b/Sources/DatabaseClient/Interface.swift index 91ffff9..57040d9 100644 --- a/Sources/DatabaseClient/Interface.swift +++ b/Sources/DatabaseClient/Interface.swift @@ -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(), diff --git a/Sources/ManualDCore/ComponentPressureLosses.swift b/Sources/ManualDCore/ComponentPressureLosses.swift index 9128b24..d7b1a21 100644 --- a/Sources/ManualDCore/ComponentPressureLosses.swift +++ b/Sources/ManualDCore/ComponentPressureLosses.swift @@ -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 diff --git a/Sources/ManualDCore/Routes/ApiRoute.swift b/Sources/ManualDCore/Routes/ApiRoute.swift index 1f3e7ed..762fe11 100644 --- a/Sources/ManualDCore/Routes/ApiRoute.swift +++ b/Sources/ManualDCore/Routes/ApiRoute.swift @@ -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 + } + } + } +}