diff --git a/Sources/DatabaseClient/EffectiveLength.swift b/Sources/DatabaseClient/EffectiveLength.swift new file mode 100644 index 0000000..cf72d10 --- /dev/null +++ b/Sources/DatabaseClient/EffectiveLength.swift @@ -0,0 +1,158 @@ +import Dependencies +import DependenciesMacros +import Fluent +import Foundation +import ManualDCore + +extension DatabaseClient { + @DependencyClient + public struct EffectiveLengthClient: Sendable { + public var create: @Sendable (EffectiveLength.Create) async throws -> EffectiveLength + public var delete: @Sendable (EffectiveLength.ID) async throws -> Void + public var fetch: @Sendable (Project.ID) async throws -> EffectiveLength? + public var get: @Sendable (EffectiveLength.ID) async throws -> EffectiveLength? + } +} + +extension DatabaseClient.EffectiveLengthClient: TestDependencyKey { + public static let testValue = Self() + + 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 EffectiveLengthModel.find(id, on: database) else { + throw NotFoundError() + } + try await model.delete(on: database) + }, + fetch: { projectID in + guard + let model = try await EffectiveLengthModel.query(on: database) + .filter("projectID", .equal, projectID) + .first() + else { + throw NotFoundError() + } + + return try model.toDTO() + }, + get: { id in + try await EffectiveLengthModel.find(id, on: database).map { try $0.toDTO() } + } + ) + } +} + +extension EffectiveLength.Create { + + func toModel() throws -> EffectiveLengthModel { + try validate() + return try .init( + name: name, + type: type.rawValue, + straightLengths: straightLengths, + groups: JSONEncoder().encode(groups), + projectID: projectID + ) + } + + func validate() throws(ValidationError) { + guard !name.isEmpty else { + throw ValidationError("Effective length name can not be empty.") + } + } +} + +extension EffectiveLength { + + struct Migrate: AsyncMigration { + let name = "CreateEffectiveLength" + + func prepare(on database: any Database) async throws { + try await database.schema(EffectiveLengthModel.schema) + .id() + .field("name", .string, .required) + .field("type", .string, .required) + .field("straightLengths", .array(of: .int)) + .field("groups", .data) + .field("createdAt", .datetime) + .field("updatedAt", .datetime) + .field("projectID", .uuid, .required, .references(ProjectModel.schema, "id")) + .unique(on: "projectID", "name", "type") + .create() + } + + func revert(on database: any Database) async throws { + try await database.schema(EffectiveLengthModel.schema).delete() + } + } +} + +final class EffectiveLengthModel: Model, @unchecked Sendable { + + static let schema = "effective_length" + + @ID(key: .id) + var id: UUID? + + @Field(key: "name") + var name: String + + @Field(key: "type") + var type: String + + @Field(key: "straightLengths") + var straightLengths: [Int] + + @Field(key: "groups") + var groups: Data + + @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, + type: String, + straightLengths: [Int], + groups: Data, + createdAt: Date? = nil, + updatedAt: Date? = nil, + projectID: Project.ID + ) { + self.id = id + self.name = name + self.type = type + self.straightLengths = straightLengths + self.groups = groups + self.createdAt = createdAt + self.updatedAt = updatedAt + $project.id = projectID + } + + func toDTO() throws -> EffectiveLength { + try .init( + id: requireID(), + projectID: $project.id, + name: name, + type: .init(rawValue: type)!, + straightLengths: straightLengths, + groups: JSONDecoder().decode([EffectiveLength.Group].self, from: groups), + createdAt: createdAt!, + updatedAt: updatedAt! + ) + } +} diff --git a/Sources/DatabaseClient/Interface.swift b/Sources/DatabaseClient/Interface.swift index ea6a11e..afff9cf 100644 --- a/Sources/DatabaseClient/Interface.swift +++ b/Sources/DatabaseClient/Interface.swift @@ -17,6 +17,7 @@ public struct DatabaseClient: Sendable { public var rooms: Rooms public var equipment: Equipment public var componentLoss: ComponentLoss + public var effectiveLength: EffectiveLengthClient } extension DatabaseClient: TestDependencyKey { @@ -25,7 +26,8 @@ extension DatabaseClient: TestDependencyKey { projects: .testValue, rooms: .testValue, equipment: .testValue, - componentLoss: .testValue + componentLoss: .testValue, + effectiveLength: .testValue ) public static func live(database: any Database) -> Self { @@ -34,7 +36,8 @@ extension DatabaseClient: TestDependencyKey { projects: .live(database: database), rooms: .live(database: database), equipment: .live(database: database), - componentLoss: .live(database: database) + componentLoss: .live(database: database), + effectiveLength: .live(database: database) ) } } @@ -58,6 +61,7 @@ extension DatabaseClient.Migrations: DependencyKey { ComponentPressureLoss.Migrate(), EquipmentInfo.Migrate(), Room.Migrate(), + EffectiveLength.Migrate(), ] } ) diff --git a/Sources/ManualDCore/EffectiveLength.swift b/Sources/ManualDCore/EffectiveLength.swift new file mode 100644 index 0000000..379d4cb --- /dev/null +++ b/Sources/ManualDCore/EffectiveLength.swift @@ -0,0 +1,87 @@ +import Foundation + +// TODO: Not sure how to model effective length groups in the database. +// thinking perhaps just have a 'data' field that encoded / decodes +// to swift types?? +public struct EffectiveLength: Codable, Equatable, Identifiable, Sendable { + + public let id: UUID + public let projectID: Project.ID + public let name: String + public let type: EffectiveLengthType + public let straightLengths: [Int] + public let groups: [Group] + public let createdAt: Date + public let updatedAt: Date + + public init( + id: UUID, + projectID: Project.ID, + name: String, + type: EffectiveLength.EffectiveLengthType, + straightLengths: [Int], + groups: [EffectiveLength.Group], + createdAt: Date, + updatedAt: Date + ) { + self.id = id + self.projectID = projectID + self.name = name + self.type = type + self.straightLengths = straightLengths + self.groups = groups + self.createdAt = createdAt + self.updatedAt = updatedAt + } +} + +extension EffectiveLength { + + public struct Create: Codable, Equatable, Sendable { + + public let projectID: Project.ID + public let name: String + public let type: EffectiveLengthType + public let straightLengths: [Int] + public let groups: [Group] + + public init( + projectID: Project.ID, + name: String, + type: EffectiveLength.EffectiveLengthType, + straightLengths: [Int], + groups: [EffectiveLength.Group] + ) { + self.projectID = projectID + self.name = name + self.type = type + self.straightLengths = straightLengths + self.groups = groups + } + } + + public enum EffectiveLengthType: String, CaseIterable, Codable, Sendable { + case `return` + case supply + } + + public struct Group: Codable, Equatable, Sendable { + + public let group: Int + public let letter: String + public let value: Double + public let quantity: Int + + public init( + group: Int, + letter: String, + value: Double, + quantity: Int = 1 + ) { + self.group = group + self.letter = letter + self.value = value + self.quantity = quantity + } + } +} diff --git a/Sources/ManualDCore/Routes/ApiRoute.swift b/Sources/ManualDCore/Routes/ApiRoute.swift index 762fe11..35f53b2 100644 --- a/Sources/ManualDCore/Routes/ApiRoute.swift +++ b/Sources/ManualDCore/Routes/ApiRoute.swift @@ -192,3 +192,48 @@ extension SiteRoute.Api { } } } + +extension SiteRoute.Api { + public enum EffectiveLengthRoute: Equatable, Sendable { + case create(EffectiveLength.Create) + case delete(id: EffectiveLength.ID) + case fetch(projectID: Project.ID) + case get(id: EffectiveLength.ID) + + static let rootPath = "effectiveLength" + + public static let router = OneOf { + Route(.case(Self.create)) { + Path { + rootPath + "create" + } + Method.post + Body(.json(EffectiveLength.Create.self)) + } + Route(.case(Self.delete(id:))) { + Path { + rootPath + EffectiveLength.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 + EffectiveLength.ID.parser() + } + Method.get + } + } + } +}