7 Commits

Author SHA1 Message Date
18a5ef06d3 feat: Rename items in database client for consistency.
All checks were successful
CI / Linux Tests (push) Successful in 5m24s
2026-01-29 15:47:24 -05:00
6723f7a410 feat: Adds some documentation strings in ManualDCore module. 2026-01-29 15:16:26 -05:00
5440024038 feat: Renames EffectiveLength to EquivalentLength
All checks were successful
CI / Linux Tests (push) Successful in 9m34s
2026-01-29 11:25:20 -05:00
f005b43936 feat: Try to build image in ci instead of relying on just.
All checks were successful
CI / Linux Tests (push) Successful in 5m46s
2026-01-29 10:52:07 -05:00
f44b35ab3d feat: Try extractions/setup-just
Some checks failed
CI / Linux Tests (push) Failing after 7s
2026-01-29 10:46:00 -05:00
bbf9a8b390 feat: Setup just directly in ci workflow
Some checks failed
CI / Linux Tests (push) Failing after 10s
2026-01-29 10:43:49 -05:00
c52cee212f feat: Adds ci workflow.
Some checks failed
CI / Linux Tests (push) Failing after 5s
2026-01-29 10:36:29 -05:00
51 changed files with 762 additions and 710 deletions

31
.gitea/workflows/ci.yaml Normal file
View File

@@ -0,0 +1,31 @@
name: CI
on:
push:
branches:
- main
- dev
pull_request:
workflow_dispatch:
jobs:
ubuntu:
name: Linux Tests
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Setup QEMU
uses: docker/setup-qemu-action@v3
- name: Setup buildx
uses: docker/setup-buildx-action@v3
- name: Build test image
uses: docker/build-push-action@v6
with:
context: .
file: docker/Dockerfile.test
push: false
load: true
tags: michael/ductcalc:test
- name: Run Tests
run: |
docker run --rm michael/ductcalc:test swift test

View File

@@ -120,11 +120,10 @@ let package = Package(
.target(name: "HTMLSnapshotTesting"), .target(name: "HTMLSnapshotTesting"),
.target(name: "PdfClient"), .target(name: "PdfClient"),
.product(name: "SnapshotTesting", package: "swift-snapshot-testing"), .product(name: "SnapshotTesting", package: "swift-snapshot-testing"),
],
resources: [
.copy("__Snapshots__")
] ]
// ,
// resources: [
// .copy("__Snapshots__")
// ]
), ),
.target( .target(
name: "ProjectClient", name: "ProjectClient",

View File

@@ -104,14 +104,14 @@ extension SiteRoute.Api.ComponentLossRoute {
switch self { switch self {
case .create(let request): case .create(let request):
return try await database.componentLoss.create(request) return try await database.componentLosses.create(request)
case .delete(let id): case .delete(let id):
try await database.componentLoss.delete(id) try await database.componentLosses.delete(id)
return nil return nil
case .fetch(let projectID): case .fetch(let projectID):
return try await database.componentLoss.fetch(projectID) return try await database.componentLosses.fetch(projectID)
case .get(let id): case .get(let id):
guard let room = try await database.componentLoss.get(id) else { guard let room = try await database.componentLosses.get(id) else {
logger.error("Component loss not found for id: \(id)") logger.error("Component loss not found for id: \(id)")
throw ApiError("Component loss not found.") throw ApiError("Component loss not found.")
} }

View File

@@ -16,11 +16,108 @@ public struct DatabaseClient: Sendable {
public var projects: Projects public var projects: Projects
public var rooms: Rooms public var rooms: Rooms
public var equipment: Equipment public var equipment: Equipment
public var componentLoss: ComponentLoss public var componentLosses: ComponentLosses
public var effectiveLength: EffectiveLengthClient public var equivalentLengths: EquivalentLengths
public var users: Users public var users: Users
public var userProfile: UserProfile public var userProfiles: UserProfiles
public var trunkSizes: TrunkSizes public var trunkSizes: TrunkSizes
@DependencyClient
public struct ComponentLosses: 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?
public var update:
@Sendable (ComponentPressureLoss.ID, ComponentPressureLoss.Update) async throws ->
ComponentPressureLoss
}
@DependencyClient
public struct EquivalentLengths: Sendable {
public var create: @Sendable (EquivalentLength.Create) async throws -> EquivalentLength
public var delete: @Sendable (EquivalentLength.ID) async throws -> Void
public var fetch: @Sendable (Project.ID) async throws -> [EquivalentLength]
public var fetchMax: @Sendable (Project.ID) async throws -> EquivalentLength.MaxContainer
public var get: @Sendable (EquivalentLength.ID) async throws -> EquivalentLength?
public var update:
@Sendable (EquivalentLength.ID, EquivalentLength.Update) async throws -> EquivalentLength
}
@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?
public var update:
@Sendable (EquipmentInfo.ID, EquipmentInfo.Update) async throws -> EquipmentInfo
}
@DependencyClient
public struct Migrations: Sendable {
public var all: @Sendable () async throws -> [any AsyncMigration]
public func callAsFunction() async throws -> [any AsyncMigration] {
try await self.all()
}
}
@DependencyClient
public struct Projects: Sendable {
public var create: @Sendable (User.ID, Project.Create) async throws -> Project
public var delete: @Sendable (Project.ID) async throws -> Void
public var detail: @Sendable (Project.ID) async throws -> Project.Detail?
public var get: @Sendable (Project.ID) async throws -> Project?
public var getCompletedSteps: @Sendable (Project.ID) async throws -> Project.CompletedSteps
public var getSensibleHeatRatio: @Sendable (Project.ID) async throws -> Double?
public var fetch: @Sendable (User.ID, PageRequest) async throws -> Page<Project>
public var update: @Sendable (Project.ID, Project.Update) async throws -> Project
}
@DependencyClient
public struct Rooms: Sendable {
public var create: @Sendable (Room.Create) async throws -> Room
public var delete: @Sendable (Room.ID) async throws -> Void
public var deleteRectangularSize:
@Sendable (Room.ID, Room.RectangularSize.ID) async throws -> Room
public var get: @Sendable (Room.ID) async throws -> Room?
public var fetch: @Sendable (Project.ID) async throws -> [Room]
public var update: @Sendable (Room.ID, Room.Update) async throws -> Room
public var updateRectangularSize: @Sendable (Room.ID, Room.RectangularSize) async throws -> Room
}
@DependencyClient
public struct TrunkSizes: Sendable {
public var create: @Sendable (TrunkSize.Create) async throws -> TrunkSize
public var delete: @Sendable (TrunkSize.ID) async throws -> Void
public var fetch: @Sendable (Project.ID) async throws -> [TrunkSize]
public var get: @Sendable (TrunkSize.ID) async throws -> TrunkSize?
public var update:
@Sendable (TrunkSize.ID, TrunkSize.Update) async throws ->
TrunkSize
}
@DependencyClient
public struct UserProfiles: Sendable {
public var create: @Sendable (User.Profile.Create) async throws -> User.Profile
public var delete: @Sendable (User.Profile.ID) async throws -> Void
public var fetch: @Sendable (User.ID) async throws -> User.Profile?
public var get: @Sendable (User.Profile.ID) async throws -> User.Profile?
public var update: @Sendable (User.Profile.ID, User.Profile.Update) async throws -> User.Profile
}
@DependencyClient
public struct Users: Sendable {
public var create: @Sendable (User.Create) async throws -> User
public var delete: @Sendable (User.ID) async throws -> Void
public var get: @Sendable (User.ID) async throws -> User?
public var login: @Sendable (User.Login) async throws -> User.Token
public var logout: @Sendable (User.Token.ID) async throws -> Void
// public var token: @Sendable (User.ID) async throws -> User.Token
}
} }
extension DatabaseClient: TestDependencyKey { extension DatabaseClient: TestDependencyKey {
@@ -29,10 +126,10 @@ extension DatabaseClient: TestDependencyKey {
projects: .testValue, projects: .testValue,
rooms: .testValue, rooms: .testValue,
equipment: .testValue, equipment: .testValue,
componentLoss: .testValue, componentLosses: .testValue,
effectiveLength: .testValue, equivalentLengths: .testValue,
users: .testValue, users: .testValue,
userProfile: .testValue, userProfiles: .testValue,
trunkSizes: .testValue trunkSizes: .testValue
) )
@@ -42,33 +139,20 @@ extension DatabaseClient: TestDependencyKey {
projects: .live(database: database), projects: .live(database: database),
rooms: .live(database: database), rooms: .live(database: database),
equipment: .live(database: database), equipment: .live(database: database),
componentLoss: .live(database: database), componentLosses: .live(database: database),
effectiveLength: .live(database: database), equivalentLengths: .live(database: database),
users: .live(database: database), users: .live(database: database),
userProfile: .live(database: database), userProfiles: .live(database: database),
trunkSizes: .live(database: database) trunkSizes: .live(database: database)
) )
} }
} }
extension DatabaseClient {
@DependencyClient
public struct Migrations: Sendable {
public var run: @Sendable () async throws -> [any AsyncMigration]
public func callAsFunction() async throws -> [any AsyncMigration] {
try await self.run()
}
}
}
extension DatabaseClient.Migrations: TestDependencyKey {
public static let testValue = Self()
}
extension DatabaseClient.Migrations: DependencyKey { extension DatabaseClient.Migrations: DependencyKey {
public static let testValue = Self()
public static let liveValue = Self( public static let liveValue = Self(
run: { all: {
[ [
Project.Migrate(), Project.Migrate(),
User.Migrate(), User.Migrate(),
@@ -77,7 +161,7 @@ extension DatabaseClient.Migrations: DependencyKey {
ComponentPressureLoss.Migrate(), ComponentPressureLoss.Migrate(),
EquipmentInfo.Migrate(), EquipmentInfo.Migrate(),
Room.Migrate(), Room.Migrate(),
EffectiveLength.Migrate(), EquivalentLength.Migrate(),
TrunkSize.Migrate(), TrunkSize.Migrate(),
] ]
} }

View File

@@ -4,25 +4,11 @@ import Fluent
import Foundation import Foundation
import ManualDCore import ManualDCore
extension DatabaseClient { extension DatabaseClient.ComponentLosses: TestDependencyKey {
@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?
public var update:
@Sendable (ComponentPressureLoss.ID, ComponentPressureLoss.Update) async throws ->
ComponentPressureLoss
}
}
extension DatabaseClient.ComponentLoss: TestDependencyKey {
public static let testValue = Self() public static let testValue = Self()
} }
extension DatabaseClient.ComponentLoss { extension DatabaseClient.ComponentLosses {
public static func live(database: any Database) -> Self { public static func live(database: any Database) -> Self {
.init( .init(
create: { request in create: { request in

View File

@@ -4,20 +4,7 @@ import Fluent
import Foundation import Foundation
import ManualDCore import ManualDCore
extension DatabaseClient { extension DatabaseClient.EquivalentLengths: TestDependencyKey {
@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 fetchMax: @Sendable (Project.ID) async throws -> EffectiveLength.MaxContainer
public var get: @Sendable (EffectiveLength.ID) async throws -> EffectiveLength?
public var update:
@Sendable (EffectiveLength.ID, EffectiveLength.Update) async throws -> EffectiveLength
}
}
extension DatabaseClient.EffectiveLengthClient: TestDependencyKey {
public static let testValue = Self() public static let testValue = Self()
public static func live(database: any Database) -> Self { public static func live(database: any Database) -> Self {
@@ -74,7 +61,7 @@ extension DatabaseClient.EffectiveLengthClient: TestDependencyKey {
} }
} }
extension EffectiveLength.Create { extension EquivalentLength.Create {
func toModel() throws -> EffectiveLengthModel { func toModel() throws -> EffectiveLengthModel {
try validate() try validate()
@@ -94,7 +81,7 @@ extension EffectiveLength.Create {
} }
} }
extension EffectiveLength { extension EquivalentLength {
struct Migrate: AsyncMigration { struct Migrate: AsyncMigration {
let name = "CreateEffectiveLength" let name = "CreateEffectiveLength"
@@ -173,20 +160,20 @@ final class EffectiveLengthModel: Model, @unchecked Sendable {
$project.id = projectID $project.id = projectID
} }
func toDTO() throws -> EffectiveLength { func toDTO() throws -> EquivalentLength {
try .init( try .init(
id: requireID(), id: requireID(),
projectID: $project.id, projectID: $project.id,
name: name, name: name,
type: .init(rawValue: type)!, type: .init(rawValue: type)!,
straightLengths: straightLengths, straightLengths: straightLengths,
groups: JSONDecoder().decode([EffectiveLength.Group].self, from: groups), groups: JSONDecoder().decode([EquivalentLength.FittingGroup].self, from: groups),
createdAt: createdAt!, createdAt: createdAt!,
updatedAt: updatedAt! updatedAt: updatedAt!
) )
} }
func applyUpdates(_ updates: EffectiveLength.Update) throws { func applyUpdates(_ updates: EquivalentLength.Update) throws {
if let name = updates.name, name != self.name { if let name = updates.name, name != self.name {
self.name = name self.name = name
} }

View File

@@ -4,23 +4,9 @@ import Fluent
import Foundation import Foundation
import ManualDCore 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?
public var update:
@Sendable (EquipmentInfo.ID, EquipmentInfo.Update) async throws -> EquipmentInfo
}
}
extension DatabaseClient.Equipment: TestDependencyKey { extension DatabaseClient.Equipment: TestDependencyKey {
public static let testValue = Self() public static let testValue = Self()
}
extension DatabaseClient.Equipment {
public static func live(database: any Database) -> Self { public static func live(database: any Database) -> Self {
.init( .init(
create: { request in create: { request in

View File

@@ -4,20 +4,6 @@ import Fluent
import Foundation import Foundation
import ManualDCore import ManualDCore
extension DatabaseClient {
@DependencyClient
public struct Projects: Sendable {
public var create: @Sendable (User.ID, Project.Create) async throws -> Project
public var delete: @Sendable (Project.ID) async throws -> Void
public var detail: @Sendable (Project.ID) async throws -> Project.Detail?
public var get: @Sendable (Project.ID) async throws -> Project?
public var getCompletedSteps: @Sendable (Project.ID) async throws -> Project.CompletedSteps
public var getSensibleHeatRatio: @Sendable (Project.ID) async throws -> Double?
public var fetch: @Sendable (User.ID, PageRequest) async throws -> Page<Project>
public var update: @Sendable (Project.ID, Project.Update) async throws -> Project
}
}
extension DatabaseClient.Projects: TestDependencyKey { extension DatabaseClient.Projects: TestDependencyKey {
public static let testValue = Self() public static let testValue = Self()

View File

@@ -4,20 +4,6 @@ import Fluent
import Foundation import Foundation
import ManualDCore import ManualDCore
extension DatabaseClient {
@DependencyClient
public struct Rooms: Sendable {
public var create: @Sendable (Room.Create) async throws -> Room
public var delete: @Sendable (Room.ID) async throws -> Void
public var deleteRectangularSize:
@Sendable (Room.ID, Room.RectangularSize.ID) async throws -> Room
public var get: @Sendable (Room.ID) async throws -> Room?
public var fetch: @Sendable (Project.ID) async throws -> [Room]
public var update: @Sendable (Room.ID, Room.Update) async throws -> Room
public var updateRectangularSize: @Sendable (Room.ID, Room.RectangularSize) async throws -> Room
}
}
extension DatabaseClient.Rooms: TestDependencyKey { extension DatabaseClient.Rooms: TestDependencyKey {
public static let testValue = Self() public static let testValue = Self()

View File

@@ -4,19 +4,6 @@ import Fluent
import Foundation import Foundation
import ManualDCore import ManualDCore
extension DatabaseClient {
@DependencyClient
public struct TrunkSizes: Sendable {
public var create: @Sendable (TrunkSize.Create) async throws -> TrunkSize
public var delete: @Sendable (TrunkSize.ID) async throws -> Void
public var fetch: @Sendable (Project.ID) async throws -> [TrunkSize]
public var get: @Sendable (TrunkSize.ID) async throws -> TrunkSize?
public var update:
@Sendable (TrunkSize.ID, TrunkSize.Update) async throws ->
TrunkSize
}
}
extension DatabaseClient.TrunkSizes: TestDependencyKey { extension DatabaseClient.TrunkSizes: TestDependencyKey {
public static let testValue = Self() public static let testValue = Self()

View File

@@ -4,18 +4,7 @@ import Fluent
import ManualDCore import ManualDCore
import Vapor import Vapor
extension DatabaseClient { extension DatabaseClient.UserProfiles: TestDependencyKey {
@DependencyClient
public struct UserProfile: Sendable {
public var create: @Sendable (User.Profile.Create) async throws -> User.Profile
public var delete: @Sendable (User.Profile.ID) async throws -> Void
public var fetch: @Sendable (User.ID) async throws -> User.Profile?
public var get: @Sendable (User.Profile.ID) async throws -> User.Profile?
public var update: @Sendable (User.Profile.ID, User.Profile.Update) async throws -> User.Profile
}
}
extension DatabaseClient.UserProfile: TestDependencyKey {
public static let testValue = Self() public static let testValue = Self()

View File

@@ -4,19 +4,6 @@ import Fluent
import ManualDCore import ManualDCore
import Vapor import Vapor
extension DatabaseClient {
@DependencyClient
public struct Users: Sendable {
public var create: @Sendable (User.Create) async throws -> User
public var delete: @Sendable (User.ID) async throws -> Void
public var get: @Sendable (User.ID) async throws -> User?
public var login: @Sendable (User.Login) async throws -> User.Token
public var logout: @Sendable (User.Token.ID) async throws -> Void
// public var token: @Sendable (User.ID) async throws -> User.Token
}
}
extension DatabaseClient.Users: TestDependencyKey { extension DatabaseClient.Users: TestDependencyKey {
public static let testValue = Self() public static let testValue = Self()

View File

@@ -1,175 +0,0 @@
// import Dependencies
// import DependenciesMacros
// import Fluent
// import Foundation
// import ManualDCore
//
// extension DatabaseClient {
// @DependencyClient
// public struct RectangularDuct: Sendable {
// public var create:
// @Sendable (DuctSizing.RectangularDuct.Create) async throws -> DuctSizing.RectangularDuct
// public var delete: @Sendable (DuctSizing.RectangularDuct.ID) async throws -> Void
// public var fetch: @Sendable (Room.ID) async throws -> [DuctSizing.RectangularDuct]
// public var get:
// @Sendable (DuctSizing.RectangularDuct.ID) async throws -> DuctSizing.RectangularDuct?
// public var update:
// @Sendable (DuctSizing.RectangularDuct.ID, DuctSizing.RectangularDuct.Update) async throws ->
// DuctSizing.RectangularDuct
// }
// }
//
// extension DatabaseClient.RectangularDuct: TestDependencyKey {
// public static let testValue = Self()
//
// public static func live(database: any Database) -> Self {
// .init(
// create: { request in
// try request.validate()
// let model = request.toModel()
// try await model.save(on: database)
// return try model.toDTO()
// },
// delete: { id in
// guard let model = try await RectangularDuctModel.find(id, on: database) else {
// throw NotFoundError()
// }
// try await model.delete(on: database)
// },
// fetch: { roomID in
// try await RectangularDuctModel.query(on: database)
// .with(\.$room)
// .filter(\.$room.$id == roomID)
// .all()
// .map { try $0.toDTO() }
// },
// get: { id in
// try await RectangularDuctModel.find(id, on: database)
// .map { try $0.toDTO() }
// },
// update: { id, updates in
// guard let model = try await RectangularDuctModel.find(id, on: database) else {
// throw NotFoundError()
// }
// try updates.validate()
// model.applyUpdates(updates)
// if model.hasChanges {
// try await model.save(on: database)
// }
// return try model.toDTO()
// }
// )
// }
// }
//
// extension DuctSizing.RectangularDuct.Create {
//
// func validate() throws(ValidationError) {
// guard height > 0 else {
// throw ValidationError("Rectangular duct size height should be greater than 0.")
// }
// if let register {
// guard register > 0 else {
// throw ValidationError("Rectangular duct size register should be greater than 0.")
// }
// }
// }
//
// func toModel() -> RectangularDuctModel {
// .init(roomID: roomID, height: height)
// }
// }
//
// extension DuctSizing.RectangularDuct.Update {
//
// func validate() throws(ValidationError) {
// if let height {
// guard height > 0 else {
// throw ValidationError("Rectangular duct size height should be greater than 0.")
// }
// }
// if let register {
// guard register > 0 else {
// throw ValidationError("Rectangular duct size register should be greater than 0.")
// }
// }
// }
// }
//
// extension DuctSizing.RectangularDuct {
// struct Migrate: AsyncMigration {
// let name = "CreateRectangularDuct"
//
// func prepare(on database: any Database) async throws {
// try await database.schema(RectangularDuctModel.schema)
// .id()
// .field("register", .int8)
// .field("height", .int8, .required)
// .field("roomID", .uuid, .required, .references(RoomModel.schema, "id", onDelete: .cascade))
// .field("createdAt", .datetime)
// .field("updatedAt", .datetime)
// .create()
// }
//
// func revert(on database: any Database) async throws {
// try await database.schema(RectangularDuctModel.schema).delete()
// }
// }
// }
//
// final class RectangularDuctModel: Model, @unchecked Sendable {
//
// static let schema = "rectangularDuct"
//
// @ID(key: .id)
// var id: UUID?
//
// @Parent(key: "roomID")
// var room: RoomModel
//
// @Field(key: "height")
// var height: Int
//
// @Field(key: "register")
// var register: Int?
//
// @Timestamp(key: "createdAt", on: .create, format: .iso8601)
// var createdAt: Date?
//
// @Timestamp(key: "updatedAt", on: .update, format: .iso8601)
// var updatedAt: Date?
//
// init() {}
//
// init(
// id: UUID? = nil,
// roomID: Room.ID,
// register: Int? = nil,
// height: Int
// ) {
// self.id = id
// $room.id = roomID
// self.register = register
// self.height = height
// }
//
// func toDTO() throws -> DuctSizing.RectangularDuct {
// return try .init(
// id: requireID(),
// roomID: $room.id,
// register: register,
// height: height,
// createdAt: createdAt!,
// updatedAt: updatedAt!
// )
// }
//
// func applyUpdates(_ updates: DuctSizing.RectangularDuct.Update) {
// if let height = updates.height, height != self.height {
// self.height = height
// }
// if let register = updates.register, register != self.register {
// self.register = register
// }
// }
// }

View File

@@ -46,10 +46,6 @@ extension TrunkSize {
} }
} }
extension ComponentPressureLosses {
var totalLosses: Double { values.reduce(0) { $0 + $1 } }
}
extension Array where Element == EffectiveLengthGroup { extension Array where Element == EffectiveLengthGroup {
var totalEffectiveLength: Int { var totalEffectiveLength: Int {
reduce(0) { $0 + $1.effectiveLength } reduce(0) { $0 + $1.effectiveLength }

View File

@@ -1,6 +1,10 @@
import Dependencies import Dependencies
import Foundation import Foundation
/// Represents component pressure losses used in the friction rate worksheet.
///
/// These are items such as filter, evaporator-coils, balance-dampers, etc. that
/// need to be overcome by the system fan.
public struct ComponentPressureLoss: Codable, Equatable, Identifiable, Sendable { public struct ComponentPressureLoss: Codable, Equatable, Identifiable, Sendable {
public let id: UUID public let id: UUID
@@ -44,7 +48,7 @@ extension ComponentPressureLoss {
self.value = value self.value = value
} }
// Return's commonly used default component pressure losses. /// Commonly used default component pressure losses.
public static func `default`(projectID: Project.ID) -> [Self] { public static func `default`(projectID: Project.ID) -> [Self] {
[ [
.init(projectID: projectID, name: "supply-outlet", value: 0.03), .init(projectID: projectID, name: "supply-outlet", value: 0.03),
@@ -75,20 +79,7 @@ extension Array where Element == ComponentPressureLoss {
} }
} }
public typealias ComponentPressureLosses = [String: Double]
#if DEBUG #if DEBUG
extension ComponentPressureLosses {
public static var mock: Self {
[
"evaporator-coil": 0.2,
"filter": 0.1,
"supply-outlet": 0.03,
"return-grille": 0.03,
"balancing-damper": 0.03,
]
}
}
extension Array where Element == ComponentPressureLoss { extension Array where Element == ComponentPressureLoss {
public static func mock(projectID: Project.ID) -> Self { public static func mock(projectID: Project.ID) -> Self {

View File

@@ -1,215 +0,0 @@
import Dependencies
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 struct Update: Codable, Equatable, Sendable {
public let name: String?
public let type: EffectiveLengthType?
public let straightLengths: [Int]?
public let groups: [Group]?
public init(
name: String? = nil,
type: EffectiveLength.EffectiveLengthType? = nil,
straightLengths: [Int]? = nil,
groups: [EffectiveLength.Group]? = nil
) {
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
}
}
public struct MaxContainer: Codable, Equatable, Sendable {
public let supply: EffectiveLength?
public let `return`: EffectiveLength?
public var total: Double? {
guard let supply else { return nil }
guard let `return` else { return nil }
return supply.totalEquivalentLength + `return`.totalEquivalentLength
}
public init(supply: EffectiveLength? = nil, return: EffectiveLength? = nil) {
self.supply = supply
self.return = `return`
}
}
}
extension EffectiveLength {
public var totalEquivalentLength: Double {
straightLengths.reduce(into: 0.0) { $0 += Double($1) }
+ groups.totalEquivalentLength
}
}
extension Array where Element == EffectiveLength.Group {
public var totalEquivalentLength: Double {
reduce(into: 0.0) {
$0 += ($1.value * Double($1.quantity))
}
}
}
#if DEBUG
extension EffectiveLength {
public static func mock(projectID: Project.ID) -> [Self] {
@Dependency(\.uuid) var uuid
@Dependency(\.date.now) var now
return [
.init(
id: uuid(),
projectID: projectID,
name: "Supply - 1",
type: .supply,
straightLengths: [10, 25],
groups: [
.init(group: 1, letter: "a", value: 20),
.init(group: 2, letter: "b", value: 30, quantity: 1),
.init(group: 3, letter: "a", value: 10, quantity: 1),
.init(group: 12, letter: "a", value: 10, quantity: 1),
],
createdAt: now,
updatedAt: now
),
.init(
id: uuid(),
projectID: projectID,
name: "Return - 1",
type: .return,
straightLengths: [10, 20, 5],
groups: [
.init(group: 5, letter: "a", value: 10),
.init(group: 6, letter: "a", value: 15, quantity: 1),
.init(group: 7, letter: "a", value: 20, quantity: 1),
],
createdAt: now,
updatedAt: now
),
]
}
public static let mocks: [Self] = [
.init(
id: UUID(0),
projectID: UUID(0),
name: "Test Supply - 1",
type: .supply,
straightLengths: [10, 20, 25],
groups: [
.init(group: 1, letter: "a", value: 20),
.init(group: 2, letter: "b", value: 15, quantity: 2),
.init(group: 3, letter: "c", value: 10, quantity: 1),
],
createdAt: Date(),
updatedAt: Date()
),
.init(
id: UUID(1),
projectID: UUID(0),
name: "Test Return - 1",
type: .return,
straightLengths: [10, 20, 25],
groups: [
.init(group: 1, letter: "a", value: 20),
.init(group: 2, letter: "b", value: 15, quantity: 2),
.init(group: 3, letter: "c", value: 10, quantity: 1),
],
createdAt: Date(),
updatedAt: Date()
),
]
}
#endif

View File

@@ -1,5 +1,7 @@
import Foundation import Foundation
// TODO: These are not used, should they be removed??
// TODO: Add other description / label for items that have same group & letter, but // TODO: Add other description / label for items that have same group & letter, but
// different effective length. // different effective length.
public struct EffectiveLengthGroup: Codable, Equatable, Sendable { public struct EffectiveLengthGroup: Codable, Equatable, Sendable {

View File

@@ -1,6 +1,10 @@
import Dependencies import Dependencies
import Foundation import Foundation
/// Represents the equipment information for a project.
///
/// This is used in the friction rate worksheet and sizing ducts. It holds on to items
/// such as the target static pressure, cooling CFM, and heating CFM for the project.
public struct EquipmentInfo: Codable, Equatable, Identifiable, Sendable { public struct EquipmentInfo: Codable, Equatable, Identifiable, Sendable {
public let id: UUID public let id: UUID
public let projectID: Project.ID public let projectID: Project.ID

View File

@@ -0,0 +1,239 @@
import Dependencies
import Foundation
/// Represents the equivalent length of a single duct path.
///
/// These consist of both straight lengths of duct / trunks, as well as the
/// equivalent length of duct fittings. They are used to determine the worst
/// case total equivalent length of duct that the system fan has to move air
/// through.
///
/// There can be many equivalent lengths saved for a project, however the only
/// ones that matter in most calculations are the longest supply path and the
/// the longest return path.
///
/// It is required that project has at least one equivalent length saved for
/// the supply and one saved for return, otherwise duct sizes can not be calculated.
public struct EquivalentLength: Codable, Equatable, Identifiable, Sendable {
/// The id of the equivalent length.
public let id: UUID
/// The project that this equivalent length is associated with.
public let projectID: Project.ID
/// A unique name / label for this equivalent length.
public let name: String
/// The type (supply or return) of the equivalent length.
public let type: EffectiveLengthType
/// The straight lengths of duct for this equivalent length.
public let straightLengths: [Int]
/// The fitting groups associated with this equivalent length.
public let groups: [FittingGroup]
/// When this equivalent length was created in the database.
public let createdAt: Date
/// When this equivalent length was updated in the database.
public let updatedAt: Date
public init(
id: UUID,
projectID: Project.ID,
name: String,
type: EquivalentLength.EffectiveLengthType,
straightLengths: [Int],
groups: [EquivalentLength.FittingGroup],
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 EquivalentLength {
/// Represents the data needed to create a new ``EquivalentLength`` in the database.
public struct Create: Codable, Equatable, Sendable {
/// The project that this equivalent length is associated with.
public let projectID: Project.ID
/// A unique name / label for this equivalent length.
public let name: String
/// The type (supply or return) of the equivalent length.
public let type: EffectiveLengthType
/// The straight lengths of duct for this equivalent length.
public let straightLengths: [Int]
/// The fitting groups associated with this equivalent length.
public let groups: [FittingGroup]
public init(
projectID: Project.ID,
name: String,
type: EquivalentLength.EffectiveLengthType,
straightLengths: [Int],
groups: [EquivalentLength.FittingGroup]
) {
self.projectID = projectID
self.name = name
self.type = type
self.straightLengths = straightLengths
self.groups = groups
}
}
/// Represents the data needed to update an ``EquivalentLength`` in the database.
///
/// Only the supplied fields are updated.
public struct Update: Codable, Equatable, Sendable {
/// A unique name / label for this equivalent length.
public let name: String?
/// The type (supply or return) of the equivalent length.
public let type: EffectiveLengthType?
/// The straight lengths of duct for this equivalent length.
public let straightLengths: [Int]?
/// The fitting groups associated with this equivalent length.
public let groups: [FittingGroup]?
public init(
name: String? = nil,
type: EquivalentLength.EffectiveLengthType? = nil,
straightLengths: [Int]? = nil,
groups: [EquivalentLength.FittingGroup]? = nil
) {
self.name = name
self.type = type
self.straightLengths = straightLengths
self.groups = groups
}
}
/// Represents the type of equivalent length, either supply or return.
public enum EffectiveLengthType: String, CaseIterable, Codable, Sendable {
case `return`
case supply
}
/// Represents a Manual-D fitting group.
///
/// These are defined by Manual-D and convert different types of fittings into
/// an equivalent length of straight duct.
public struct FittingGroup: Codable, Equatable, Sendable {
/// The fitting group number.
public let group: Int
/// The fitting group letter.
public let letter: String
/// The equivalent length of the fitting.
public let value: Double
/// The quantity of the fittings in the path.
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
}
}
// TODO: Should these not be optional and we just throw an error or return nil from
// a database query.
/// Represents the max ``EquivalentLength``'s for a project.
///
/// Calculating the duct sizes for a project requires there to be a max supply
/// and a max return equivalent length, so this container represents those values
/// when they exist in the database.
public struct MaxContainer: Codable, Equatable, Sendable {
/// The longest supply equivalent length.
public let supply: EquivalentLength?
/// The longest return equivalent length.
public let `return`: EquivalentLength?
public var totalEquivalentLength: Double? {
guard let supply else { return nil }
guard let `return` else { return nil }
return supply.totalEquivalentLength + `return`.totalEquivalentLength
}
public init(supply: EquivalentLength? = nil, return: EquivalentLength? = nil) {
self.supply = supply
self.return = `return`
}
}
}
extension EquivalentLength {
/// The calculated total equivalent length.
///
/// This is the sum of all the straigth lengths and fitting groups (with quantities).
public var totalEquivalentLength: Double {
straightLengths.reduce(into: 0.0) { $0 += Double($1) }
+ groups.totalEquivalentLength
}
}
extension Array where Element == EquivalentLength.FittingGroup {
/// The calculated total equivalent length for the fitting groups.
public var totalEquivalentLength: Double {
reduce(into: 0.0) {
$0 += ($1.value * Double($1.quantity))
}
}
}
#if DEBUG
extension EquivalentLength {
public static func mock(projectID: Project.ID) -> [Self] {
@Dependency(\.uuid) var uuid
@Dependency(\.date.now) var now
return [
.init(
id: uuid(),
projectID: projectID,
name: "Supply - 1",
type: .supply,
straightLengths: [10, 25],
groups: [
.init(group: 1, letter: "a", value: 20),
.init(group: 2, letter: "b", value: 30, quantity: 1),
.init(group: 3, letter: "a", value: 10, quantity: 1),
.init(group: 12, letter: "a", value: 10, quantity: 1),
],
createdAt: now,
updatedAt: now
),
.init(
id: uuid(),
projectID: projectID,
name: "Return - 1",
type: .return,
straightLengths: [10, 20, 5],
groups: [
.init(group: 5, letter: "a", value: 10),
.init(group: 6, letter: "a", value: 15, quantity: 1),
.init(group: 7, letter: "a", value: 20, quantity: 1),
],
createdAt: now,
updatedAt: now
),
]
}
}
#endif

View File

@@ -1,8 +1,14 @@
/// Holds onto values returned when calculating the design /// Holds onto values returned when calculating the design
/// friction rate for a project. /// friction rate for a project.
///
/// **NOTE:** This is not stored in the database, it is calculated on the fly.
public struct FrictionRate: Codable, Equatable, Sendable { public struct FrictionRate: Codable, Equatable, Sendable {
/// The available static pressure is the equipment's design static pressure
/// minus the ``ComponentPressureLoss``es for the project.
public let availableStaticPressure: Double public let availableStaticPressure: Double
/// The calculated design friction rate value.
public let value: Double public let value: Double
/// Whether the design friction rate is within a valid range.
public var hasErrors: Bool { error != nil } public var hasErrors: Bool { error != nil }
public init( public init(
@@ -13,6 +19,7 @@ public struct FrictionRate: Codable, Equatable, Sendable {
self.value = value self.value = value
} }
/// The error if the design friction rate is out of a valid range.
public var error: FrictionRateError? { public var error: FrictionRateError? {
if value >= 0.18 { if value >= 0.18 {
return .init( return .init(
@@ -37,8 +44,13 @@ public struct FrictionRate: Codable, Equatable, Sendable {
} }
} }
/// Represents an error when the ``FrictionRate`` is out of a valid range.
///
/// This holds onto the reason for the error as well as possible resolutions.
public struct FrictionRateError: Error, Equatable, Sendable { public struct FrictionRateError: Error, Equatable, Sendable {
/// The reason for the error.
public let reason: String public let reason: String
/// The possible resolutions to the error.
public let resolutions: [String] public let resolutions: [String]
public init( public init(

View File

@@ -1,16 +1,29 @@
import Dependencies import Dependencies
import Foundation import Foundation
/// Represents a single duct design project / system.
///
/// Holds items such as project name and address.
public struct Project: Codable, Equatable, Identifiable, Sendable { public struct Project: Codable, Equatable, Identifiable, Sendable {
/// The unique ID of the project.
public let id: UUID public let id: UUID
/// The name of the project.
public let name: String public let name: String
/// The street address of the project.
public let streetAddress: String public let streetAddress: String
/// The city of the project.
public let city: String public let city: String
/// The state of the project.
public let state: String public let state: String
/// The zip code of the project.
public let zipCode: String public let zipCode: String
/// The global sensible heat ratio for the project.
///
/// **NOTE:** This is used for calculating the sensible cooling load for rooms.
public let sensibleHeatRatio: Double? public let sensibleHeatRatio: Double?
/// When the project was created in the database.
public let createdAt: Date public let createdAt: Date
/// When the project was updated in the database.
public let updatedAt: Date public let updatedAt: Date
public init( public init(
@@ -37,14 +50,20 @@ public struct Project: Codable, Equatable, Identifiable, Sendable {
} }
extension Project { extension Project {
/// Represents the data needed to create a new project.
public struct Create: Codable, Equatable, Sendable { public struct Create: Codable, Equatable, Sendable {
/// The name of the project.
public let name: String public let name: String
/// The street address of the project.
public let streetAddress: String public let streetAddress: String
/// The city of the project.
public let city: String public let city: String
/// The state of the project.
public let state: String public let state: String
/// The zip code of the project.
public let zipCode: String public let zipCode: String
/// The global sensible heat ratio for the project.
public let sensibleHeatRatio: Double? public let sensibleHeatRatio: Double?
public init( public init(
@@ -64,11 +83,19 @@ extension Project {
} }
} }
/// Represents steps that are completed in order to calculate the duct sizes
/// for a project.
///
/// This is primarily used on the web pages to display errors or color of the
/// different steps of a project.
public struct CompletedSteps: Codable, Equatable, Sendable { public struct CompletedSteps: Codable, Equatable, Sendable {
/// Whether there is ``EquipmentInfo`` for a project.
public let equipmentInfo: Bool public let equipmentInfo: Bool
/// Whether there are ``Room``'s for a project.
public let rooms: Bool public let rooms: Bool
/// Whether there are ``EquivalentLength``'s for a project.
public let equivalentLength: Bool public let equivalentLength: Bool
/// Whether there is a ``FrictionRate`` for a project.
public let frictionRate: Bool public let frictionRate: Bool
public init( public init(
@@ -84,20 +111,30 @@ extension Project {
} }
} }
/// Represents project details loaded from the database.
///
/// This is generally used to perform duct sizing calculations for the
/// project, once all the steps have been completed.
public struct Detail: Codable, Equatable, Sendable { public struct Detail: Codable, Equatable, Sendable {
/// The project.
public let project: Project public let project: Project
/// The component pressure losses for the project.
public let componentLosses: [ComponentPressureLoss] public let componentLosses: [ComponentPressureLoss]
/// The equipment info for the project.
public let equipmentInfo: EquipmentInfo public let equipmentInfo: EquipmentInfo
public let equivalentLengths: [EffectiveLength] /// The equivalent lengths for the project.
public let equivalentLengths: [EquivalentLength]
/// The rooms in the project.
public let rooms: [Room] public let rooms: [Room]
/// The trunk sizes in the project.
public let trunks: [TrunkSize] public let trunks: [TrunkSize]
public init( public init(
project: Project, project: Project,
componentLosses: [ComponentPressureLoss], componentLosses: [ComponentPressureLoss],
equipmentInfo: EquipmentInfo, equipmentInfo: EquipmentInfo,
equivalentLengths: [EffectiveLength], equivalentLengths: [EquivalentLength],
rooms: [Room], rooms: [Room],
trunks: [TrunkSize] trunks: [TrunkSize]
) { ) {
@@ -110,13 +147,22 @@ extension Project {
} }
} }
/// Represents fields that can be updated for a project that has already been created.
///
/// Only fields that are supplied get updated in the database.
public struct Update: Codable, Equatable, Sendable { public struct Update: Codable, Equatable, Sendable {
/// The name of the project.
public let name: String? public let name: String?
/// The street address of the project.
public let streetAddress: String? public let streetAddress: String?
/// The city of the project.
public let city: String? public let city: String?
/// The state of the project.
public let state: String? public let state: String?
/// The zip code of the project.
public let zipCode: String? public let zipCode: String?
/// The global sensible heat ratio for the project.
public let sensibleHeatRatio: Double? public let sensibleHeatRatio: Double?
public init( public init(

View File

@@ -1,16 +1,37 @@
import Dependencies import Dependencies
import Foundation import Foundation
/// Represents a room in a project.
///
/// This contains data such as the heating and cooling load for the
/// room, the number of registers in the room, and any rectangular
/// duct size calculations stored for the room.
public struct Room: Codable, Equatable, Identifiable, Sendable { public struct Room: Codable, Equatable, Identifiable, Sendable {
/// The unique id of the room.
public let id: UUID public let id: UUID
/// The project this room is associated with.
public let projectID: Project.ID public let projectID: Project.ID
/// A unique name for the room in the project.
public let name: String public let name: String
/// The heating load required for the room (from Manual-J).
public let heatingLoad: Double public let heatingLoad: Double
/// The total cooling load required for the room (from Manual-J).
public let coolingTotal: Double public let coolingTotal: Double
/// An optional sensible cooling load for the room.
///
/// **NOTE:** This is generally not set, but calculated from the project wide
/// sensible heat ratio.
public let coolingSensible: Double? public let coolingSensible: Double?
/// The number of registers for the room.
public let registerCount: Int public let registerCount: Int
/// The rectangular duct size calculations for a room.
///
/// **NOTE:** These are optionally set after the round sizes have been calculate
/// for a room.
public let rectangularSizes: [RectangularSize]? public let rectangularSizes: [RectangularSize]?
/// When the room was created in the database.
public let createdAt: Date public let createdAt: Date
/// When the room was updated in the database.
public let updatedAt: Date public let updatedAt: Date
public init( public init(
@@ -39,13 +60,19 @@ public struct Room: Codable, Equatable, Identifiable, Sendable {
} }
extension Room { extension Room {
/// Represents the data required to create a new room for a project.
public struct Create: Codable, Equatable, Sendable { public struct Create: Codable, Equatable, Sendable {
/// The project this room is associated with.
public let projectID: Project.ID public let projectID: Project.ID
/// A unique name for the room in the project.
public let name: String public let name: String
/// The heating load required for the room (from Manual-J).
public let heatingLoad: Double public let heatingLoad: Double
/// The total cooling load required for the room (from Manual-J).
public let coolingTotal: Double public let coolingTotal: Double
/// An optional sensible cooling load for the room.
public let coolingSensible: Double? public let coolingSensible: Double?
/// The number of registers for the room.
public let registerCount: Int public let registerCount: Int
public init( public init(
@@ -65,10 +92,17 @@ extension Room {
} }
} }
/// Represents a rectangular size calculation that is stored in the
/// database for a given room.
///
/// These are done after the round duct sizes have been calculated and
/// can be used to calculate the equivalent rectangular size for a given run.
public struct RectangularSize: Codable, Equatable, Identifiable, Sendable { public struct RectangularSize: Codable, Equatable, Identifiable, Sendable {
/// The unique id of the rectangular size.
public let id: UUID public let id: UUID
/// The register the rectangular size is associated with.
public let register: Int? public let register: Int?
/// The height of the rectangular size, the width gets calculated.
public let height: Int public let height: Int
public init( public init(
@@ -82,12 +116,21 @@ extension Room {
} }
} }
/// Represents field that can be updated on a room after it's been created in the database.
///
/// Only fields that are supplied get updated.
public struct Update: Codable, Equatable, Sendable { public struct Update: Codable, Equatable, Sendable {
/// A unique name for the room in the project.
public let name: String? public let name: String?
/// The heating load required for the room (from Manual-J).
public let heatingLoad: Double? public let heatingLoad: Double?
/// The total cooling load required for the room (from Manual-J).
public let coolingTotal: Double? public let coolingTotal: Double?
/// An optional sensible cooling load for the room.
public let coolingSensible: Double? public let coolingSensible: Double?
/// The number of registers for the room.
public let registerCount: Int? public let registerCount: Int?
/// The rectangular duct size calculations for a room.
public let rectangularSizes: [RectangularSize]? public let rectangularSizes: [RectangularSize]?
public init( public init(
@@ -120,14 +163,20 @@ extension Room {
extension Array where Element == Room { extension Array where Element == Room {
/// The sum of heating loads for an array of rooms.
public var totalHeatingLoad: Double { public var totalHeatingLoad: Double {
reduce(into: 0) { $0 += $1.heatingLoad } reduce(into: 0) { $0 += $1.heatingLoad }
} }
/// The sum of total cooling loads for an array of rooms.
public var totalCoolingLoad: Double { public var totalCoolingLoad: Double {
reduce(into: 0) { $0 += $1.coolingTotal } reduce(into: 0) { $0 += $1.coolingTotal }
} }
/// The sum of sensible cooling loads for an array of rooms.
///
/// - Parameters:
/// - shr: The project wide sensible heat ratio.
public func totalCoolingSensible(shr: Double) -> Double { public func totalCoolingSensible(shr: Double) -> Double {
reduce(into: 0) { reduce(into: 0) {
let sensible = $1.coolingSensible ?? ($1.coolingTotal * shr) let sensible = $1.coolingSensible ?? ($1.coolingTotal * shr)
@@ -139,38 +188,6 @@ extension Array where Element == Room {
#if DEBUG #if DEBUG
extension Room { extension Room {
public static let mocks = [
Room(
id: UUID(0),
projectID: UUID(0),
name: "Kitchen",
heatingLoad: 12345,
coolingTotal: 1234,
registerCount: 2,
createdAt: Date(),
updatedAt: Date()
),
Room(
id: UUID(1),
projectID: UUID(1),
name: "Bedroom - 1",
heatingLoad: 12345,
coolingTotal: 1456,
registerCount: 1,
createdAt: Date(),
updatedAt: Date()
),
Room(
id: UUID(2),
projectID: UUID(2),
name: "Family Room",
heatingLoad: 12345,
coolingTotal: 1673,
registerCount: 3,
createdAt: Date(),
updatedAt: Date()
),
]
public static func mock(projectID: Project.ID) -> [Self] { public static func mock(projectID: Project.ID) -> [Self] {
@Dependency(\.uuid) var uuid @Dependency(\.uuid) var uuid

View File

@@ -226,10 +226,10 @@ extension SiteRoute.Api {
extension SiteRoute.Api { extension SiteRoute.Api {
public enum EffectiveLengthRoute: Equatable, Sendable { public enum EffectiveLengthRoute: Equatable, Sendable {
case create(EffectiveLength.Create) case create(EquivalentLength.Create)
case delete(id: EffectiveLength.ID) case delete(id: EquivalentLength.ID)
case fetch(projectID: Project.ID) case fetch(projectID: Project.ID)
case get(id: EffectiveLength.ID) case get(id: EquivalentLength.ID)
static let rootPath = "effectiveLength" static let rootPath = "effectiveLength"
@@ -240,12 +240,12 @@ extension SiteRoute.Api {
"create" "create"
} }
Method.post Method.post
Body(.json(EffectiveLength.Create.self)) Body(.json(EquivalentLength.Create.self))
} }
Route(.case(Self.delete(id:))) { Route(.case(Self.delete(id:))) {
Path { Path {
rootPath rootPath
EffectiveLength.ID.parser() EquivalentLength.ID.parser()
} }
Method.delete Method.delete
} }
@@ -261,7 +261,7 @@ extension SiteRoute.Api {
Route(.case(Self.get(id:))) { Route(.case(Self.get(id:))) {
Path { Path {
rootPath rootPath
EffectiveLength.ID.parser() EquivalentLength.ID.parser()
} }
Method.get Method.get
} }

View File

@@ -394,11 +394,11 @@ extension SiteRoute.View.ProjectRoute {
} }
public enum EquivalentLengthRoute: Equatable, Sendable { public enum EquivalentLengthRoute: Equatable, Sendable {
case delete(id: EffectiveLength.ID) case delete(id: EquivalentLength.ID)
case field(FieldType, style: EffectiveLength.EffectiveLengthType? = nil) case field(FieldType, style: EquivalentLength.EffectiveLengthType? = nil)
case index case index
case submit(FormStep) case submit(FormStep)
case update(EffectiveLength.ID, StepThree) case update(EquivalentLength.ID, StepThree)
static let rootPath = "effective-lengths" static let rootPath = "effective-lengths"
@@ -406,7 +406,7 @@ extension SiteRoute.View.ProjectRoute {
Route(.case(Self.delete(id:))) { Route(.case(Self.delete(id:))) {
Path { Path {
rootPath rootPath
EffectiveLength.ID.parser() EquivalentLength.ID.parser()
} }
Method.delete Method.delete
} }
@@ -424,7 +424,7 @@ extension SiteRoute.View.ProjectRoute {
Field("type") { FieldType.parser() } Field("type") { FieldType.parser() }
Optionally { Optionally {
Field("style", default: nil) { Field("style", default: nil) {
EffectiveLength.EffectiveLengthType.parser() EquivalentLength.EffectiveLengthType.parser()
} }
} }
} }
@@ -437,16 +437,16 @@ extension SiteRoute.View.ProjectRoute {
Route(.case(Self.update)) { Route(.case(Self.update)) {
Path { Path {
rootPath rootPath
EffectiveLength.ID.parser() EquivalentLength.ID.parser()
} }
Method.patch Method.patch
Body { Body {
FormData { FormData {
Optionally { Optionally {
Field("id", default: nil) { EffectiveLength.ID.parser() } Field("id", default: nil) { EquivalentLength.ID.parser() }
} }
Field("name", .string) Field("name", .string)
Field("type") { EffectiveLength.EffectiveLengthType.parser() } Field("type") { EquivalentLength.EffectiveLengthType.parser() }
Many { Many {
Field("straightLengths") { Field("straightLengths") {
Int.parser() Int.parser()
@@ -490,10 +490,10 @@ extension SiteRoute.View.ProjectRoute {
Body { Body {
FormData { FormData {
Optionally { Optionally {
Field("id", default: nil) { EffectiveLength.ID.parser() } Field("id", default: nil) { EquivalentLength.ID.parser() }
} }
Field("name", .string) Field("name", .string)
Field("type") { EffectiveLength.EffectiveLengthType.parser() } Field("type") { EquivalentLength.EffectiveLengthType.parser() }
} }
.map(.memberwise(StepOne.init)) .map(.memberwise(StepOne.init))
} }
@@ -505,10 +505,10 @@ extension SiteRoute.View.ProjectRoute {
Body { Body {
FormData { FormData {
Optionally { Optionally {
Field("id", default: nil) { EffectiveLength.ID.parser() } Field("id", default: nil) { EquivalentLength.ID.parser() }
} }
Field("name", .string) Field("name", .string)
Field("type") { EffectiveLength.EffectiveLengthType.parser() } Field("type") { EquivalentLength.EffectiveLengthType.parser() }
Many { Many {
Field("straightLengths") { Field("straightLengths") {
Int.parser() Int.parser()
@@ -525,10 +525,10 @@ extension SiteRoute.View.ProjectRoute {
Body { Body {
FormData { FormData {
Optionally { Optionally {
Field("id", default: nil) { EffectiveLength.ID.parser() } Field("id", default: nil) { EquivalentLength.ID.parser() }
} }
Field("name", .string) Field("name", .string)
Field("type") { EffectiveLength.EffectiveLengthType.parser() } Field("type") { EquivalentLength.EffectiveLengthType.parser() }
Many { Many {
Field("straightLengths") { Field("straightLengths") {
Int.parser() Int.parser()
@@ -567,22 +567,22 @@ extension SiteRoute.View.ProjectRoute {
} }
public struct StepOne: Codable, Equatable, Sendable { public struct StepOne: Codable, Equatable, Sendable {
public let id: EffectiveLength.ID? public let id: EquivalentLength.ID?
public let name: String public let name: String
public let type: EffectiveLength.EffectiveLengthType public let type: EquivalentLength.EffectiveLengthType
} }
public struct StepTwo: Codable, Equatable, Sendable { public struct StepTwo: Codable, Equatable, Sendable {
public let id: EffectiveLength.ID? public let id: EquivalentLength.ID?
public let name: String public let name: String
public let type: EffectiveLength.EffectiveLengthType public let type: EquivalentLength.EffectiveLengthType
public let straightLengths: [Int] public let straightLengths: [Int]
public init( public init(
id: EffectiveLength.ID? = nil, id: EquivalentLength.ID? = nil,
name: String, name: String,
type: EffectiveLength.EffectiveLengthType, type: EquivalentLength.EffectiveLengthType,
straightLengths: [Int] straightLengths: [Int]
) { ) {
self.id = id self.id = id
@@ -593,9 +593,9 @@ extension SiteRoute.View.ProjectRoute {
} }
public struct StepThree: Codable, Equatable, Sendable { public struct StepThree: Codable, Equatable, Sendable {
public let id: EffectiveLength.ID? public let id: EquivalentLength.ID?
public let name: String public let name: String
public let type: EffectiveLength.EffectiveLengthType public let type: EquivalentLength.EffectiveLengthType
public let straightLengths: [Int] public let straightLengths: [Int]
public let groupGroups: [Int] public let groupGroups: [Int]
public let groupLetters: [String] public let groupLetters: [String]

View File

@@ -1,5 +1,6 @@
import Foundation import Foundation
/// Represents supported color themes for the website.
public enum Theme: String, CaseIterable, Codable, Equatable, Sendable { public enum Theme: String, CaseIterable, Codable, Equatable, Sendable {
case aqua case aqua
case cupcake case cupcake
@@ -13,6 +14,7 @@ public enum Theme: String, CaseIterable, Codable, Equatable, Sendable {
case retro case retro
case synthwave case synthwave
/// Represents dark color themes.
public static let darkThemes = [ public static let darkThemes = [
Self.aqua, Self.aqua,
Self.cyberpunk, Self.cyberpunk,
@@ -22,6 +24,7 @@ public enum Theme: String, CaseIterable, Codable, Equatable, Sendable {
Self.synthwave, Self.synthwave,
] ]
/// Represents light color themes.
public static let lightThemes = [ public static let lightThemes = [
Self.cupcake, Self.cupcake,
Self.light, Self.light,

View File

@@ -1,14 +1,23 @@
import Dependencies import Dependencies
import Foundation import Foundation
// Represents the database model. /// Represents trunk calculations for a project.
///
/// These are used to size trunk ducts / runs for multiple rooms or registers.
public struct TrunkSize: Codable, Equatable, Identifiable, Sendable { public struct TrunkSize: Codable, Equatable, Identifiable, Sendable {
/// The unique identifier of the trunk size.
public let id: UUID public let id: UUID
/// The project the trunk size is for.
public let projectID: Project.ID public let projectID: Project.ID
/// The type of the trunk size (supply or return).
public let type: TrunkType public let type: TrunkType
/// The rooms / registers associated with the trunk size.
public let rooms: [RoomProxy] public let rooms: [RoomProxy]
/// An optional rectangular height used to calculate the equivalent
/// rectangular size of the trunk.
public let height: Int? public let height: Int?
/// An optional name / label used for identifying the trunk.
public let name: String? public let name: String?
public init( public init(
@@ -29,12 +38,19 @@ public struct TrunkSize: Codable, Equatable, Identifiable, Sendable {
} }
extension TrunkSize { extension TrunkSize {
/// Represents the data needed to create a new ``TrunkSize`` in the database.
public struct Create: Codable, Equatable, Sendable { public struct Create: Codable, Equatable, Sendable {
/// The project the trunk size is for.
public let projectID: Project.ID public let projectID: Project.ID
/// The type of the trunk size (supply or return).
public let type: TrunkType public let type: TrunkType
/// The rooms / registers associated with the trunk size.
public let rooms: [Room.ID: [Int]] public let rooms: [Room.ID: [Int]]
/// An optional rectangular height used to calculate the equivalent
/// rectangular size of the trunk.
public let height: Int? public let height: Int?
/// An optional name / label used for identifying the trunk.
public let name: String? public let name: String?
public init( public init(
@@ -52,11 +68,19 @@ extension TrunkSize {
} }
} }
/// Represents the fields that can be updated on a ``TrunkSize`` in the database.
///
/// Only supplied fields are updated.
public struct Update: Codable, Equatable, Sendable { public struct Update: Codable, Equatable, Sendable {
/// The type of the trunk size (supply or return).
public let type: TrunkType? public let type: TrunkType?
/// The rooms / registers associated with the trunk size.
public let rooms: [Room.ID: [Int]]? public let rooms: [Room.ID: [Int]]?
/// An optional rectangular height used to calculate the equivalent
/// rectangular size of the trunk.
public let height: Int? public let height: Int?
/// An optional name / label used for identifying the trunk.
public let name: String? public let name: String?
public init( public init(
@@ -72,18 +96,29 @@ extension TrunkSize {
} }
} }
public struct RoomProxy: Codable, Equatable, Identifiable, Sendable { /// A container / wrapper around a ``Room`` that is used with a ``TrunkSize``.
///
/// This is needed because a room can have multiple registers and it is possible
/// that a trunk does not serve all registers in that room.
@dynamicMemberLookup
public struct RoomProxy: Codable, Equatable, Sendable {
public var id: Room.ID { room.id } /// The room associated with the ``TrunkSize``.
public let room: Room public let room: Room
/// The specific room registers associated with the ``TrunkSize``.
public let registers: [Int] public let registers: [Int]
public init(room: Room, registers: [Int]) { public init(room: Room, registers: [Int]) {
self.room = room self.room = room
self.registers = registers self.registers = registers
} }
public subscript<T>(dynamicMember keyPath: KeyPath<Room, T>) -> T {
room[keyPath: keyPath]
}
} }
/// Represents the type of a ``TrunkSize``, either supply or return.
public enum TrunkType: String, CaseIterable, Codable, Equatable, Sendable { public enum TrunkType: String, CaseIterable, Codable, Equatable, Sendable {
case `return` case `return`
case supply case supply

View File

@@ -1,12 +1,17 @@
import Dependencies import Dependencies
import Foundation import Foundation
// FIX: Remove username. /// Represents a user of the site.
///
public struct User: Codable, Equatable, Identifiable, Sendable { public struct User: Codable, Equatable, Identifiable, Sendable {
/// The unique id of the user.
public let id: UUID public let id: UUID
/// The user's email address.
public let email: String public let email: String
/// When the user was created in the database.
public let createdAt: Date public let createdAt: Date
/// When the user was updated in the database.
public let updatedAt: Date public let updatedAt: Date
public init( public init(
@@ -23,10 +28,14 @@ public struct User: Codable, Equatable, Identifiable, Sendable {
} }
extension User { extension User {
/// Represents the data required to create a new user.
public struct Create: Codable, Equatable, Sendable { public struct Create: Codable, Equatable, Sendable {
/// The user's email address.
public let email: String public let email: String
/// The password for the user.
public let password: String public let password: String
/// The password confirmation, must match the password.
public let confirmPassword: String public let confirmPassword: String
public init( public init(
@@ -40,9 +49,13 @@ extension User {
} }
} }
/// Represents data required to login a user.
public struct Login: Codable, Equatable, Sendable { public struct Login: Codable, Equatable, Sendable {
/// The user's email address.
public let email: String public let email: String
/// The password for the user.
public let password: String public let password: String
/// An optional page / route to navigate to after logging in the user.
public let next: String? public let next: String?
public init(email: String, password: String, next: String? = nil) { public init(email: String, password: String, next: String? = nil) {
@@ -52,10 +65,13 @@ extension User {
} }
} }
/// Represents a user session token, for a logged in user.
public struct Token: Codable, Equatable, Identifiable, Sendable { public struct Token: Codable, Equatable, Identifiable, Sendable {
/// The unique id of the token.
public let id: UUID public let id: UUID
/// The user id the token is for.
public let userID: User.ID public let userID: User.ID
/// The token value.
public let value: String public let value: String
public init(id: UUID, userID: User.ID, value: String) { public init(id: UUID, userID: User.ID, value: String) {

View File

@@ -2,19 +2,32 @@ import Dependencies
import Foundation import Foundation
extension User { extension User {
/// Represents a user's profile. Which contains extra information about a user of the site.
public struct Profile: Codable, Equatable, Identifiable, Sendable { public struct Profile: Codable, Equatable, Identifiable, Sendable {
/// The unique id of the profile
public let id: UUID public let id: UUID
/// The user id the profile is for.
public let userID: User.ID public let userID: User.ID
/// The user's first name.
public let firstName: String public let firstName: String
/// The user's last name.
public let lastName: String public let lastName: String
/// The user's company name.
public let companyName: String public let companyName: String
/// The user's street address.
public let streetAddress: String public let streetAddress: String
/// The user's city.
public let city: String public let city: String
/// The user's state.
public let state: String public let state: String
/// The user's zip code.
public let zipCode: String public let zipCode: String
/// An optional theme that the user prefers.
public let theme: Theme? public let theme: Theme?
/// When the profile was created in the database.
public let createdAt: Date public let createdAt: Date
/// When the profile was updated in the database.
public let updatedAt: Date public let updatedAt: Date
public init( public init(
@@ -49,15 +62,25 @@ extension User {
extension User.Profile { extension User.Profile {
/// Represents the data required to create a user profile.
public struct Create: Codable, Equatable, Sendable { public struct Create: Codable, Equatable, Sendable {
/// The user id the profile is for.
public let userID: User.ID public let userID: User.ID
/// The user's first name.
public let firstName: String public let firstName: String
/// The user's last name.
public let lastName: String public let lastName: String
/// The user's company name.
public let companyName: String public let companyName: String
/// The user's street address.
public let streetAddress: String public let streetAddress: String
/// The user's city.
public let city: String public let city: String
/// The user's state.
public let state: String public let state: String
/// The user's zip code.
public let zipCode: String public let zipCode: String
/// An optional theme that the user prefers.
public let theme: Theme? public let theme: Theme?
public init( public init(
@@ -83,14 +106,25 @@ extension User.Profile {
} }
} }
/// Represents the fields that can be updated on a user's profile.
///
/// Only fields that are supplied get updated.
public struct Update: Codable, Equatable, Sendable { public struct Update: Codable, Equatable, Sendable {
/// The user's first name.
public let firstName: String? public let firstName: String?
/// The user's last name.
public let lastName: String? public let lastName: String?
/// The user's company name.
public let companyName: String? public let companyName: String?
/// The user's street address.
public let streetAddress: String? public let streetAddress: String?
/// The user's city.
public let city: String? public let city: String?
/// The user's state.
public let state: String? public let state: String?
/// The user's zip code.
public let zipCode: String? public let zipCode: String?
/// An optional theme that the user prefers.
public let theme: Theme? public let theme: Theme?
public init( public init(

View File

@@ -84,8 +84,8 @@ extension PdfClient {
public let componentLosses: [ComponentPressureLoss] public let componentLosses: [ComponentPressureLoss]
public let ductSizes: DuctSizes public let ductSizes: DuctSizes
public let equipmentInfo: EquipmentInfo public let equipmentInfo: EquipmentInfo
public let maxSupplyTEL: EffectiveLength public let maxSupplyTEL: EquivalentLength
public let maxReturnTEL: EffectiveLength public let maxReturnTEL: EquivalentLength
public let frictionRate: FrictionRate public let frictionRate: FrictionRate
public let projectSHR: Double public let projectSHR: Double
@@ -99,8 +99,8 @@ extension PdfClient {
componentLosses: [ComponentPressureLoss], componentLosses: [ComponentPressureLoss],
ductSizes: DuctSizes, ductSizes: DuctSizes,
equipmentInfo: EquipmentInfo, equipmentInfo: EquipmentInfo,
maxSupplyTEL: EffectiveLength, maxSupplyTEL: EquivalentLength,
maxReturnTEL: EffectiveLength, maxReturnTEL: EquivalentLength,
frictionRate: FrictionRate, frictionRate: FrictionRate,
projectSHR: Double projectSHR: Double
) { ) {
@@ -134,7 +134,7 @@ extension PdfClient {
let rooms = Room.mock(projectID: project.id) let rooms = Room.mock(projectID: project.id)
let trunks = TrunkSize.mock(projectID: project.id, rooms: rooms) let trunks = TrunkSize.mock(projectID: project.id, rooms: rooms)
let equipmentInfo = EquipmentInfo.mock(projectID: project.id) let equipmentInfo = EquipmentInfo.mock(projectID: project.id)
let equivalentLengths = EffectiveLength.mock(projectID: project.id) let equivalentLengths = EquivalentLength.mock(projectID: project.id)
return .init( return .init(
project: project, project: project,

View File

@@ -2,7 +2,7 @@ import Elementary
import ManualDCore import ManualDCore
struct EffectiveLengthsTable: HTML, Sendable { struct EffectiveLengthsTable: HTML, Sendable {
let effectiveLengths: [EffectiveLength] let effectiveLengths: [EquivalentLength]
var body: some HTML<HTMLTag.table> { var body: some HTML<HTMLTag.table> {
table { table {
@@ -41,7 +41,7 @@ struct EffectiveLengthsTable: HTML, Sendable {
} }
struct EffectiveLengthGroupTable: HTML, Sendable { struct EffectiveLengthGroupTable: HTML, Sendable {
let groups: [EffectiveLength.Group] let groups: [EquivalentLength.FittingGroup]
var body: some HTML<HTMLTag.table> { var body: some HTML<HTMLTag.table> {
table { table {

View File

@@ -60,12 +60,12 @@ extension ProjectClient {
public struct FrictionRateResponse: Codable, Equatable, Sendable { public struct FrictionRateResponse: Codable, Equatable, Sendable {
public let componentLosses: [ComponentPressureLoss] public let componentLosses: [ComponentPressureLoss]
public let equivalentLengths: EffectiveLength.MaxContainer public let equivalentLengths: EquivalentLength.MaxContainer
public let frictionRate: FrictionRate? public let frictionRate: FrictionRate?
public init( public init(
componentLosses: [ComponentPressureLoss], componentLosses: [ComponentPressureLoss],
equivalentLengths: EffectiveLength.MaxContainer, equivalentLengths: EquivalentLength.MaxContainer,
frictionRate: FrictionRate? = nil frictionRate: FrictionRate? = nil
) { ) {
self.componentLosses = componentLosses self.componentLosses = componentLosses

View File

@@ -1,7 +1,7 @@
import DatabaseClient import DatabaseClient
import ManualDCore import ManualDCore
extension DatabaseClient.ComponentLoss { extension DatabaseClient.ComponentLosses {
func createDefaults(projectID: Project.ID) async throws { func createDefaults(projectID: Project.ID) async throws {
let defaults = ComponentPressureLoss.Create.default(projectID: projectID) let defaults = ComponentPressureLoss.Create.default(projectID: projectID)

View File

@@ -144,11 +144,11 @@ extension DatabaseClient {
// Internal container. // Internal container.
struct DesignFrictionRateResponse: Equatable, Sendable { struct DesignFrictionRateResponse: Equatable, Sendable {
typealias EnsuredTEL = (supply: EffectiveLength, return: EffectiveLength) typealias EnsuredTEL = (supply: EquivalentLength, return: EquivalentLength)
let designFrictionRate: Double let designFrictionRate: Double
let equipmentInfo: EquipmentInfo let equipmentInfo: EquipmentInfo
let telMaxContainer: EffectiveLength.MaxContainer let telMaxContainer: EquivalentLength.MaxContainer
func ensureMaxContainer() throws -> EnsuredTEL { func ensureMaxContainer() throws -> EnsuredTEL {
@@ -167,9 +167,9 @@ extension DatabaseClient {
func designFrictionRate( func designFrictionRate(
componentLosses: [ComponentPressureLoss], componentLosses: [ComponentPressureLoss],
equipmentInfo: EquipmentInfo, equipmentInfo: EquipmentInfo,
equivalentLengths: EffectiveLength.MaxContainer equivalentLengths: EquivalentLength.MaxContainer
) -> DesignFrictionRateResponse? { ) -> DesignFrictionRateResponse? {
guard let tel = equivalentLengths.total, guard let tel = equivalentLengths.totalEquivalentLength,
componentLosses.count > 0 componentLosses.count > 0
else { return nil } else { return nil }
@@ -192,9 +192,9 @@ extension DatabaseClient {
} }
return try await designFrictionRate( return try await designFrictionRate(
componentLosses: componentLoss.fetch(projectID), componentLosses: componentLosses.fetch(projectID),
equipmentInfo: equipmentInfo, equipmentInfo: equipmentInfo,
equivalentLengths: effectiveLength.fetchMax(projectID) equivalentLengths: equivalentLengths.fetchMax(projectID)
) )
} }
} }

View File

@@ -4,8 +4,8 @@ import ManualDCore
struct DuctSizeSharedRequest { struct DuctSizeSharedRequest {
let equipmentInfo: EquipmentInfo let equipmentInfo: EquipmentInfo
let maxSupplyLength: EffectiveLength let maxSupplyLength: EquivalentLength
let maxReturnLenght: EffectiveLength let maxReturnLenght: EquivalentLength
let designFrictionRate: Double let designFrictionRate: Double
let projectSHR: Double let projectSHR: Double
} }

View File

@@ -8,7 +8,7 @@ extension ManualDClient {
func frictionRate(details: Project.Detail) async throws -> ProjectClient.FrictionRateResponse { func frictionRate(details: Project.Detail) async throws -> ProjectClient.FrictionRateResponse {
let maxContainer = details.maxContainer let maxContainer = details.maxContainer
guard let totalEquivalentLength = maxContainer.total else { guard let totalEquivalentLength = maxContainer.totalEquivalentLength else {
return .init(componentLosses: details.componentLosses, equivalentLengths: maxContainer) return .init(componentLosses: details.componentLosses, equivalentLengths: maxContainer)
} }
@@ -28,15 +28,15 @@ extension ManualDClient {
func frictionRate(projectID: Project.ID) async throws -> ProjectClient.FrictionRateResponse { func frictionRate(projectID: Project.ID) async throws -> ProjectClient.FrictionRateResponse {
@Dependency(\.database) var database @Dependency(\.database) var database
let componentLosses = try await database.componentLoss.fetch(projectID) let componentLosses = try await database.componentLosses.fetch(projectID)
let lengths = try await database.effectiveLength.fetchMax(projectID) let lengths = try await database.equivalentLengths.fetchMax(projectID)
let equipmentInfo = try await database.equipment.fetch(projectID) let equipmentInfo = try await database.equipment.fetch(projectID)
guard let staticPressure = equipmentInfo?.staticPressure else { guard let staticPressure = equipmentInfo?.staticPressure else {
return .init(componentLosses: componentLosses, equivalentLengths: lengths) return .init(componentLosses: componentLosses, equivalentLengths: lengths)
} }
guard let totalEquivalentLength = lengths.total else { guard let totalEquivalentLength = lengths.totalEquivalentLength else {
return .init(componentLosses: componentLosses, equivalentLengths: lengths) return .init(componentLosses: componentLosses, equivalentLengths: lengths)
} }
@@ -46,7 +46,7 @@ extension ManualDClient {
frictionRate: frictionRate( frictionRate: frictionRate(
.init( .init(
externalStaticPressure: staticPressure, externalStaticPressure: staticPressure,
componentPressureLosses: database.componentLoss.fetch(projectID), componentPressureLosses: componentLosses,
totalEffectiveLength: Int(totalEquivalentLength) totalEffectiveLength: Int(totalEquivalentLength)
) )
) )

View File

@@ -1,7 +1,7 @@
import ManualDCore import ManualDCore
extension Project.Detail { extension Project.Detail {
var maxContainer: EffectiveLength.MaxContainer { var maxContainer: EquivalentLength.MaxContainer {
.init( .init(
supply: equivalentLengths.filter({ $0.type == .supply }) supply: equivalentLengths.filter({ $0.type == .supply })
.sorted(by: { $0.totalEquivalentLength > $1.totalEquivalentLength }) .sorted(by: { $0.totalEquivalentLength > $1.totalEquivalentLength })

View File

@@ -26,7 +26,7 @@ extension ProjectClient: DependencyKey {
}, },
createProject: { userID, request in createProject: { userID, request in
let project = try await database.projects.create(userID, request) let project = try await database.projects.create(userID, request)
try await database.componentLoss.createDefaults(projectID: project.id) try await database.componentLosses.createDefaults(projectID: project.id)
return try await .init( return try await .init(
projectID: project.id, projectID: project.id,
rooms: database.rooms.fetch(project.id), rooms: database.rooms.fetch(project.id),

View File

@@ -12,8 +12,8 @@ extension SiteRoute.View.ProjectRoute.EquivalentLengthRoute.StepThree {
} }
} }
var groups: [EffectiveLength.Group] { var groups: [EquivalentLength.FittingGroup] {
var groups = [EffectiveLength.Group]() var groups = [EquivalentLength.FittingGroup]()
for (n, group) in groupGroups.enumerated() { for (n, group) in groupGroups.enumerated() {
groups.append( groups.append(
.init( .init(
@@ -29,7 +29,7 @@ extension SiteRoute.View.ProjectRoute.EquivalentLengthRoute.StepThree {
} }
} }
extension EffectiveLength.Create { extension EquivalentLength.Create {
init( init(
form: SiteRoute.View.ProjectRoute.EquivalentLengthRoute.StepThree, form: SiteRoute.View.ProjectRoute.EquivalentLengthRoute.StepThree,
@@ -45,7 +45,7 @@ extension EffectiveLength.Create {
} }
} }
extension EffectiveLength.Update { extension EquivalentLength.Update {
init( init(
form: SiteRoute.View.ProjectRoute.EquivalentLengthRoute.StepThree, form: SiteRoute.View.ProjectRoute.EquivalentLengthRoute.StepThree,
projectID: Project.ID projectID: Project.ID

View File

@@ -70,7 +70,7 @@ extension ViewController.Request {
case .submitProfile(let profile): case .submitProfile(let profile):
return await view { return await view {
await ResultView { await ResultView {
_ = try await database.userProfile.create(profile) _ = try await database.userProfiles.create(profile)
let userID = profile.userID let userID = profile.userID
// let user = try currentUser() // let user = try currentUser()
return ( return (
@@ -108,7 +108,7 @@ extension ViewController.Request {
get async { get async {
@Dependency(\.database) var database @Dependency(\.database) var database
guard let user = try? currentUser() else { return nil } guard let user = try? currentUser() else { return nil }
return try? await database.userProfile.fetch(user.id)?.theme return try? await database.userProfiles.fetch(user.id)?.theme
} }
} }
@@ -366,8 +366,8 @@ extension SiteRoute.View.ProjectRoute.FrictionRateRoute {
return await request.view { return await request.view {
await ResultView { await ResultView {
let equipment = try await database.equipment.fetch(projectID) let equipment = try await database.equipment.fetch(projectID)
let componentLosses = try await database.componentLoss.fetch(projectID) let componentLosses = try await database.componentLosses.fetch(projectID)
let lengths = try await database.effectiveLength.fetchMax(projectID) let lengths = try await database.equivalentLengths.fetchMax(projectID)
return ( return (
try await database.projects.getCompletedSteps(projectID), try await database.projects.getCompletedSteps(projectID),
@@ -407,16 +407,16 @@ extension SiteRoute.View.ProjectRoute.ComponentLossRoute {
return EmptyHTML() return EmptyHTML()
case .delete(let id): case .delete(let id):
return await view(on: request, projectID: projectID) { return await view(on: request, projectID: projectID) {
_ = try await database.componentLoss.delete(id) _ = try await database.componentLosses.delete(id)
} }
case .submit(let form): case .submit(let form):
return await view(on: request, projectID: projectID) { return await view(on: request, projectID: projectID) {
_ = try await database.componentLoss.create(form) _ = try await database.componentLosses.create(form)
} }
case .update(let id, let form): case .update(let id, let form):
return await view(on: request, projectID: projectID) { return await view(on: request, projectID: projectID) {
_ = try await database.componentLoss.update(id, form) _ = try await database.componentLosses.update(id, form)
} }
} }
} }
@@ -464,7 +464,7 @@ extension SiteRoute.View.ProjectRoute.EquivalentLengthRoute {
case .delete(let id): case .delete(let id):
return await ResultView { return await ResultView {
try await database.effectiveLength.delete(id) try await database.equivalentLengths.delete(id)
} }
case .index: case .index:
@@ -481,16 +481,16 @@ extension SiteRoute.View.ProjectRoute.EquivalentLengthRoute {
case .update(let id, let form): case .update(let id, let form):
return await view(on: request, projectID: projectID) { return await view(on: request, projectID: projectID) {
_ = try await database.effectiveLength.update(id, .init(form: form, projectID: projectID)) _ = try await database.equivalentLengths.update(id, .init(form: form, projectID: projectID))
} }
case .submit(let step): case .submit(let step):
switch step { switch step {
case .one(let stepOne): case .one(let stepOne):
return await ResultView { return await ResultView {
var effectiveLength: EffectiveLength? = nil var effectiveLength: EquivalentLength? = nil
if let id = stepOne.id { if let id = stepOne.id {
effectiveLength = try await database.effectiveLength.get(id) effectiveLength = try await database.equivalentLengths.get(id)
} }
return effectiveLength return effectiveLength
} onSuccess: { effectiveLength in } onSuccess: { effectiveLength in
@@ -503,9 +503,9 @@ extension SiteRoute.View.ProjectRoute.EquivalentLengthRoute {
case .two(let stepTwo): case .two(let stepTwo):
return await ResultView { return await ResultView {
request.logger.debug("ViewController: Got step two...") request.logger.debug("ViewController: Got step two...")
var effectiveLength: EffectiveLength? = nil var effectiveLength: EquivalentLength? = nil
if let id = stepTwo.id { if let id = stepTwo.id {
effectiveLength = try await database.effectiveLength.get(id) effectiveLength = try await database.equivalentLengths.get(id)
} }
return effectiveLength return effectiveLength
} onSuccess: { effectiveLength in } onSuccess: { effectiveLength in
@@ -515,7 +515,7 @@ extension SiteRoute.View.ProjectRoute.EquivalentLengthRoute {
} }
case .three(let stepThree): case .three(let stepThree):
return await view(on: request, projectID: projectID) { return await view(on: request, projectID: projectID) {
_ = try await database.effectiveLength.create( _ = try await database.equivalentLengths.create(
.init(form: stepThree, projectID: projectID) .init(form: stepThree, projectID: projectID)
) )
} }
@@ -536,7 +536,7 @@ extension SiteRoute.View.ProjectRoute.EquivalentLengthRoute {
try await catching() try await catching()
return ( return (
try await database.projects.getCompletedSteps(projectID), try await database.projects.getCompletedSteps(projectID),
try await database.effectiveLength.fetch(projectID) try await database.equivalentLengths.fetch(projectID)
) )
} onSuccess: { (steps, equivalentLengths) in } onSuccess: { (steps, equivalentLengths) in
ProjectView(projectID: projectID, activeTab: .equivalentLength, completedSteps: steps) { ProjectView(projectID: projectID, activeTab: .equivalentLength, completedSteps: steps) {
@@ -652,11 +652,11 @@ extension SiteRoute.View.UserRoute.Profile {
return await view(on: request) return await view(on: request)
case .submit(let form): case .submit(let form):
return await view(on: request) { return await view(on: request) {
_ = try await database.userProfile.create(form) _ = try await database.userProfiles.create(form)
} }
case .update(let id, let updates): case .update(let id, let updates):
return await view(on: request) { return await view(on: request) {
_ = try await database.userProfile.update(id, updates) _ = try await database.userProfiles.update(id, updates)
} }
} }
} }
@@ -673,7 +673,7 @@ extension SiteRoute.View.UserRoute.Profile {
let user = try request.currentUser() let user = try request.currentUser()
return ( return (
user, user,
try await database.userProfile.fetch(user.id) try await database.userProfiles.fetch(user.id)
) )
} onSuccess: { (user, profile) in } onSuccess: { (user, profile) in
UserView(user: user, profile: profile) UserView(user: user, profile: profile)

View File

@@ -7,7 +7,7 @@ import Styleguide
struct EffectiveLengthForm: HTML, Sendable { struct EffectiveLengthForm: HTML, Sendable {
static func id(_ equivalentLength: EffectiveLength?) -> String { static func id(_ equivalentLength: EquivalentLength?) -> String {
let base = "equivalentLengthForm" let base = "equivalentLengthForm"
guard let equivalentLength else { return base } guard let equivalentLength else { return base }
return "\(base)_\(equivalentLength.id.uuidString.replacing("-", with: ""))" return "\(base)_\(equivalentLength.id.uuidString.replacing("-", with: ""))"
@@ -15,15 +15,15 @@ struct EffectiveLengthForm: HTML, Sendable {
let projectID: Project.ID let projectID: Project.ID
let dismiss: Bool let dismiss: Bool
let type: EffectiveLength.EffectiveLengthType let type: EquivalentLength.EffectiveLengthType
let effectiveLength: EffectiveLength? let effectiveLength: EquivalentLength?
var id: String { Self.id(effectiveLength) } var id: String { Self.id(effectiveLength) }
init( init(
projectID: Project.ID, projectID: Project.ID,
dismiss: Bool, dismiss: Bool,
type: EffectiveLength.EffectiveLengthType = .supply type: EquivalentLength.EffectiveLengthType = .supply
) { ) {
self.projectID = projectID self.projectID = projectID
self.dismiss = dismiss self.dismiss = dismiss
@@ -32,7 +32,7 @@ struct EffectiveLengthForm: HTML, Sendable {
} }
init( init(
effectiveLength: EffectiveLength effectiveLength: EquivalentLength
) { ) {
self.dismiss = true self.dismiss = true
self.type = effectiveLength.type self.type = effectiveLength.type
@@ -55,7 +55,7 @@ struct EffectiveLengthForm: HTML, Sendable {
struct StepOne: HTML, Sendable { struct StepOne: HTML, Sendable {
let projectID: Project.ID let projectID: Project.ID
let effectiveLength: EffectiveLength? let effectiveLength: EquivalentLength?
var route: String { var route: String {
let baseRoute = SiteRoute.View.router.path( let baseRoute = SiteRoute.View.router.path(
@@ -97,7 +97,7 @@ struct EffectiveLengthForm: HTML, Sendable {
struct StepTwo: HTML, Sendable { struct StepTwo: HTML, Sendable {
let projectID: Project.ID let projectID: Project.ID
let stepOne: SiteRoute.View.ProjectRoute.EquivalentLengthRoute.StepOne let stepOne: SiteRoute.View.ProjectRoute.EquivalentLengthRoute.StepOne
let effectiveLength: EffectiveLength? let effectiveLength: EquivalentLength?
var route: String { var route: String {
let baseRoute = SiteRoute.View.router.path( let baseRoute = SiteRoute.View.router.path(
@@ -152,7 +152,7 @@ struct EffectiveLengthForm: HTML, Sendable {
struct StepThree: HTML, Sendable { struct StepThree: HTML, Sendable {
let projectID: Project.ID let projectID: Project.ID
let effectiveLength: EffectiveLength? let effectiveLength: EquivalentLength?
let stepTwo: SiteRoute.View.ProjectRoute.EquivalentLengthRoute.StepTwo let stepTwo: SiteRoute.View.ProjectRoute.EquivalentLengthRoute.StepTwo
var route: String { var route: String {
@@ -254,10 +254,10 @@ struct StraightLengthField: HTML, Sendable {
struct GroupField: HTML, Sendable { struct GroupField: HTML, Sendable {
let style: EffectiveLength.EffectiveLengthType let style: EquivalentLength.EffectiveLengthType
let group: EffectiveLength.Group? let group: EquivalentLength.FittingGroup?
init(style: EffectiveLength.EffectiveLengthType, group: EffectiveLength.Group? = nil) { init(style: EquivalentLength.EffectiveLengthType, group: EquivalentLength.FittingGroup? = nil) {
self.style = style self.style = style
self.group = group self.group = group
} }
@@ -307,7 +307,7 @@ struct GroupField: HTML, Sendable {
struct GroupSelect: HTML, Sendable { struct GroupSelect: HTML, Sendable {
let style: EffectiveLength.EffectiveLengthType let style: EquivalentLength.EffectiveLengthType
var body: some HTML { var body: some HTML {
label(.class("select")) { label(.class("select")) {
@@ -328,13 +328,13 @@ struct GroupSelect: HTML, Sendable {
struct GroupTypeSelect: HTML, Sendable { struct GroupTypeSelect: HTML, Sendable {
let projectID: Project.ID let projectID: Project.ID
let selected: EffectiveLength.EffectiveLengthType let selected: EquivalentLength.EffectiveLengthType
var body: some HTML<HTMLTag.label> { var body: some HTML<HTMLTag.label> {
label(.class("select w-full")) { label(.class("select w-full")) {
span(.class("label")) { "Type" } span(.class("label")) { "Type" }
select(.name("type"), .id("type")) { select(.name("type"), .id("type")) {
for value in EffectiveLength.EffectiveLengthType.allCases { for value in EquivalentLength.EffectiveLengthType.allCases {
option( option(
.value("\(value.rawValue)"), .value("\(value.rawValue)"),
) { value.title } ) { value.title }
@@ -345,7 +345,7 @@ struct GroupTypeSelect: HTML, Sendable {
} }
} }
extension EffectiveLength.EffectiveLengthType { extension EquivalentLength.EffectiveLengthType {
var title: String { rawValue.capitalized } var title: String { rawValue.capitalized }

View File

@@ -5,9 +5,9 @@ import Styleguide
struct EffectiveLengthsTable: HTML, Sendable { struct EffectiveLengthsTable: HTML, Sendable {
let effectiveLengths: [EffectiveLength] let effectiveLengths: [EquivalentLength]
private var sortedLengths: [EffectiveLength] { private var sortedLengths: [EquivalentLength] {
effectiveLengths.sorted { effectiveLengths.sorted {
$0.totalEquivalentLength > $1.totalEquivalentLength $0.totalEquivalentLength > $1.totalEquivalentLength
} }
@@ -55,7 +55,7 @@ struct EffectiveLengthsTable: HTML, Sendable {
struct EffectiveLenghtRow: HTML, Sendable { struct EffectiveLenghtRow: HTML, Sendable {
let effectiveLength: EffectiveLength let effectiveLength: EquivalentLength
private var deleteRoute: SiteRoute.View { private var deleteRoute: SiteRoute.View {
.project( .project(

View File

@@ -7,14 +7,14 @@ struct EffectiveLengthsView: HTML, Sendable {
@Environment(ProjectViewValue.$projectID) var projectID @Environment(ProjectViewValue.$projectID) var projectID
let effectiveLengths: [EffectiveLength] let effectiveLengths: [EquivalentLength]
var supplies: [EffectiveLength] { var supplies: [EquivalentLength] {
effectiveLengths.filter({ $0.type == .supply }) effectiveLengths.filter({ $0.type == .supply })
.sorted { $0.totalEquivalentLength > $1.totalEquivalentLength } .sorted { $0.totalEquivalentLength > $1.totalEquivalentLength }
} }
var returns: [EffectiveLength] { var returns: [EquivalentLength] {
effectiveLengths.filter({ $0.type == .return }) effectiveLengths.filter({ $0.type == .return })
.sorted { $0.totalEquivalentLength > $1.totalEquivalentLength } .sorted { $0.totalEquivalentLength > $1.totalEquivalentLength }
} }

View File

@@ -8,7 +8,7 @@ struct FrictionRateView: HTML, Sendable {
@Environment(ProjectViewValue.$projectID) var projectID @Environment(ProjectViewValue.$projectID) var projectID
let componentLosses: [ComponentPressureLoss] let componentLosses: [ComponentPressureLoss]
let equivalentLengths: EffectiveLength.MaxContainer let equivalentLengths: EquivalentLength.MaxContainer
let frictionRate: FrictionRate? let frictionRate: FrictionRate?
private var availableStaticPressure: Double? { private var availableStaticPressure: Double? {

View File

@@ -233,12 +233,12 @@ extension ManualDClient {
func frictionRate( func frictionRate(
equipmentInfo: EquipmentInfo?, equipmentInfo: EquipmentInfo?,
componentLosses: [ComponentPressureLoss], componentLosses: [ComponentPressureLoss],
effectiveLength: EffectiveLength.MaxContainer effectiveLength: EquivalentLength.MaxContainer
) async throws -> FrictionRate? { ) async throws -> FrictionRate? {
guard let staticPressure = equipmentInfo?.staticPressure else { guard let staticPressure = equipmentInfo?.staticPressure else {
return nil return nil
} }
guard let totalEquivalentLength = effectiveLength.total else { guard let totalEquivalentLength = effectiveLength.totalEquivalentLength else {
return nil return nil
} }
return try await self.frictionRate( return try await self.frictionRate(

View File

@@ -1,7 +1,7 @@
# TODO's # TODO's
- [x] Fix theme not working when selected upon signup. - [x] Fix theme not working when selected upon signup.
- [ ] Pdf generation - [x] Pdf generation
- [ ] Add postgres / mysql support - [ ] Add postgres / mysql support
- [ ] Opensource / license ?? - [ ] Opensource / license ??
- [ ] Figure out domain to host (currently thinking ductcalc.pro) - [ ] Figure out domain to host (currently thinking ductcalc.pro)

View File

@@ -78,10 +78,10 @@ struct ProjectRouteTests {
let p = Body { let p = Body {
FormData { FormData {
Optionally { Optionally {
Field("id", default: nil) { EffectiveLength.ID.parser() } Field("id", default: nil) { EquivalentLength.ID.parser() }
} }
Field("name", .string) Field("name", .string)
Field("type") { EffectiveLength.EffectiveLengthType.parser() } Field("type") { EquivalentLength.EffectiveLengthType.parser() }
Many { Many {
Field("straightLengths") { Field("straightLengths") {
Int.parser() Int.parser()

View File

@@ -86,7 +86,7 @@ struct ViewControllerTests {
let project = Project.mock let project = Project.mock
let rooms = Room.mock(projectID: project.id) let rooms = Room.mock(projectID: project.id)
let equipment = EquipmentInfo.mock(projectID: project.id) let equipment = EquipmentInfo.mock(projectID: project.id)
let tels = EffectiveLength.mock(projectID: project.id) let tels = EquivalentLength.mock(projectID: project.id)
let componentLosses = ComponentPressureLoss.mock(projectID: project.id) let componentLosses = ComponentPressureLoss.mock(projectID: project.id)
let trunks = TrunkSize.mock(projectID: project.id, rooms: rooms) let trunks = TrunkSize.mock(projectID: project.id, rooms: rooms)
@@ -108,11 +108,11 @@ struct ViewControllerTests {
$0.database.projects.getSensibleHeatRatio = { _ in 0.83 } $0.database.projects.getSensibleHeatRatio = { _ in 0.83 }
$0.database.rooms.fetch = { _ in rooms } $0.database.rooms.fetch = { _ in rooms }
$0.database.equipment.fetch = { _ in equipment } $0.database.equipment.fetch = { _ in equipment }
$0.database.effectiveLength.fetch = { _ in tels } $0.database.equivalentLengths.fetch = { _ in tels }
$0.database.effectiveLength.fetchMax = { _ in $0.database.equivalentLengths.fetchMax = { _ in
.init(supply: tels.first, return: tels.last) .init(supply: tels.first, return: tels.last)
} }
$0.database.componentLoss.fetch = { _ in componentLosses } $0.database.componentLosses.fetch = { _ in componentLosses }
$0.projectClient.calculateDuctSizes = { _ in $0.projectClient.calculateDuctSizes = { _ in
.mock(equipmentInfo: equipment, rooms: rooms, trunks: trunks) .mock(equipmentInfo: equipment, rooms: rooms, trunks: trunks)
} }
@@ -164,7 +164,7 @@ struct ViewControllerTests {
return try await withDependencies { return try await withDependencies {
$0.viewController = .liveValue $0.viewController = .liveValue
$0.authClient.currentUser = { user } $0.authClient.currentUser = { user }
$0.database.userProfile.fetch = { _ in profile } $0.database.userProfiles.fetch = { _ in profile }
$0.manualD = .liveValue $0.manualD = .liveValue
try await updateDependencies(&$0) try await updateDependencies(&$0)
} operation: { } operation: {

36
docker/Dockerfile.test Normal file
View File

@@ -0,0 +1,36 @@
FROM docker.io/swift:6.2-noble
# Make sure all system packages are up to date, and install only essential packages.
RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \
&& apt-get -q update \
&& apt-get -q dist-upgrade -y \
&& apt-get -q install -y \
libjemalloc2 \
ca-certificates \
tzdata \
# If your app or its dependencies import FoundationNetworking, also install `libcurl4`.
libcurl4 \
# If your app or its dependencies import FoundationXML, also install `libxml2`.
# libxml2 \
sqlite3 \
&& rm -r /var/lib/apt/lists/*
# Set up a build area
WORKDIR /app
# First just resolve dependencies.
# This creates a cached layer that can be reused
# as long as your Package.swift/Package.resolved
# files do not change.
COPY ./Package.* ./
RUN swift package resolve \
$([ -f ./Package.resolved ] && echo "--force-resolved-versions" || true)
# Copy entire repo into container
COPY . .
ENV SWIFT_BACKTRACE=enable=no
ENV LOG_LEVEL=debug
CMD ["swift", "test"]

View File

@@ -14,7 +14,10 @@ run:
@swift run App serve --log debug @swift run App serve --log debug
build-docker file="docker/Dockerfile": build-docker file="docker/Dockerfile":
@podman build -f {{file}} -t {{docker_image}}:{{docker_tag}} . @docker build -f {{file}} -t {{docker_image}}:{{docker_tag}} .
run-docker: run-docker:
@podman run -it --rm -v $PWD:/app -p 8080:8080 {{docker_image}}:{{docker_tag}} @docker run -it --rm -v $PWD:/app -p 8080:8080 {{docker_image}}:{{docker_tag}}
test-docker: (build-docker "docker/Dockerfile.test")
@docker run --rm {{docker_image}}:{{docker_tag}} swift test