Compare commits
27 Commits
main
...
57766c990e
| Author | SHA1 | Date | |
|---|---|---|---|
|
57766c990e
|
|||
|
b2b5e32535
|
|||
|
881737978d
|
|||
|
6a764ade2b
|
|||
|
5f03056534
|
|||
|
10dd0dac82
|
|||
|
3cc7fe9926
|
|||
|
bad4a49f41
|
|||
|
9276f88426
|
|||
|
a3fb87f86e
|
|||
|
b359a3317f
|
|||
|
e0ec15b91e
|
|||
|
44a0964181
|
|||
|
0b78950d14
|
|||
|
754019eac4
|
|||
|
a51e1b34d0
|
|||
|
c32ffcff8c
|
|||
|
4f3cc2c7ea
|
|||
|
9b618d55fa
|
|||
|
9379774fae
|
|||
|
18a5ef06d3
|
|||
|
6723f7a410
|
|||
|
5440024038
|
|||
|
f005b43936
|
|||
|
f44b35ab3d
|
|||
|
bbf9a8b390
|
|||
|
c52cee212f
|
40
.devcontainer/devcontainer.json
Normal file
40
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "swift-manual-d-dev",
|
||||
"image": "git.housh.dev/michael/swift-dev-container:latest",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/git:1": {
|
||||
"version": "os-provided",
|
||||
"ppa": "false"
|
||||
},
|
||||
"ghcr.io/jsburckhardt/devcontainer-features/just:1": {},
|
||||
"ghcr.io/rocker-org/devcontainer-features/pandoc:1": {},
|
||||
//"ghcr.io/devcontainers/features/docker-in-docker:2": {},
|
||||
"ghcr.io/wxw-matt/devcontainer-features/apt:latest": {
|
||||
"packages": "weasyprint gnupg2 tmux"
|
||||
},
|
||||
"ghcr.io/swift-server-community/swift-devcontainer-features/jemalloc:1": { },
|
||||
|
||||
},
|
||||
"runArgs": [
|
||||
"--cap-add=SYS_PTRACE",
|
||||
"--security-opt",
|
||||
"seccomp=unconfined"
|
||||
],
|
||||
"remoteEnv": {
|
||||
// Set TERM, prevents problems when "xterm-ghostty" get's passed from host,
|
||||
// which causes weird typing and display problems.
|
||||
"TERM": "xterm-256color",
|
||||
// Less backtrace error warnings from swift.
|
||||
"SWIFT_BACKTRACE": "enable=no"
|
||||
},
|
||||
"remoteUser": "swift",
|
||||
"forwardPorts": [8080],
|
||||
"mounts": [
|
||||
{
|
||||
"type": "bind",
|
||||
"source": "${localEnv:HOME}/.local/share/nvim",
|
||||
"target": "/home/swift/.local/share/nvim"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
31
.gitea/workflows/ci.yaml
Normal file
31
.gitea/workflows/ci.yaml
Normal 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
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -13,3 +13,5 @@ tailwindcss
|
||||
*.pdf
|
||||
.env
|
||||
.env*
|
||||
default.profraw
|
||||
rooms.csv
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"originHash" : "c3efcfd33bc1490f59ae406e4e5292027b2d01cafee9fc625652213505df50fb",
|
||||
"originHash" : "b6e6af1076a5bcce49e1231c44be25d770eaef278e2d1ce1c961446d49cb2d00",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "async-http-client",
|
||||
@@ -73,6 +73,15 @@
|
||||
"version" : "1.53.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "fluent-postgres-driver",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/vapor/fluent-postgres-driver.git",
|
||||
"state" : {
|
||||
"revision" : "59bff45a41d1ece1950bb8a6e0006d88c1fb6e69",
|
||||
"version" : "2.12.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "fluent-sqlite-driver",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -100,6 +109,24 @@
|
||||
"version" : "0.14.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "postgres-kit",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/vapor/postgres-kit.git",
|
||||
"state" : {
|
||||
"revision" : "7c079553e9cda74811e627775bf22e40a9405ad9",
|
||||
"version" : "2.15.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "postgres-nio",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/vapor/postgres-nio.git",
|
||||
"state" : {
|
||||
"revision" : "d578b86fb2c8321b114d97cd70831d1a3e9531a6",
|
||||
"version" : "1.30.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "routing-kit",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -397,6 +424,15 @@
|
||||
"version" : "1.6.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-tagged",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-tagged",
|
||||
"state" : {
|
||||
"revision" : "3907a9438f5b57d317001dc99f3f11b46882272b",
|
||||
"version" : "0.10.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-url-routing",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -406,6 +442,15 @@
|
||||
"version" : "0.6.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-validations",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/m-housh/swift-validations.git",
|
||||
"state" : {
|
||||
"revision" : "95ea5d267e37f6cdb9f91c5c8a01e718b9299db6",
|
||||
"version" : "0.3.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "vapor",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
||||
@@ -6,10 +6,10 @@ let package = Package(
|
||||
name: "swift-manual-d",
|
||||
products: [
|
||||
.executable(name: "App", targets: ["App"]),
|
||||
.library(name: "ApiController", targets: ["ApiController"]),
|
||||
.library(name: "AuthClient", targets: ["AuthClient"]),
|
||||
.library(name: "CSVParser", targets: ["CSVParser"]),
|
||||
.library(name: "DatabaseClient", targets: ["DatabaseClient"]),
|
||||
.library(name: "EnvClient", targets: ["EnvClient"]),
|
||||
.library(name: "EnvVars", targets: ["EnvVars"]),
|
||||
.library(name: "FileClient", targets: ["FileClient"]),
|
||||
.library(name: "HTMLSnapshotTesting", targets: ["HTMLSnapshotTesting"]),
|
||||
.library(name: "PdfClient", targets: ["PdfClient"]),
|
||||
@@ -23,27 +23,30 @@ let package = Package(
|
||||
.package(url: "https://github.com/vapor/vapor.git", from: "4.110.1"),
|
||||
.package(url: "https://github.com/vapor/fluent.git", from: "4.9.0"),
|
||||
.package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.6.0"),
|
||||
.package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.0.0"),
|
||||
.package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"),
|
||||
.package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.0.0"),
|
||||
.package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.12.0"),
|
||||
.package(url: "https://github.com/pointfreeco/swift-tagged", from: "0.6.0"),
|
||||
.package(url: "https://github.com/pointfreeco/swift-url-routing.git", from: "0.6.2"),
|
||||
.package(url: "https://github.com/pointfreeco/vapor-routing.git", from: "0.1.3"),
|
||||
.package(url: "https://github.com/pointfreeco/swift-case-paths.git", from: "1.6.0"),
|
||||
.package(url: "https://github.com/elementary-swift/elementary.git", from: "0.6.0"),
|
||||
.package(url: "https://github.com/elementary-swift/elementary-htmx.git", from: "0.5.0"),
|
||||
.package(url: "https://github.com/vapor-community/vapor-elementary.git", from: "0.1.0"),
|
||||
.package(url: "https://github.com/m-housh/swift-validations.git", from: "0.1.0"),
|
||||
],
|
||||
targets: [
|
||||
.executableTarget(
|
||||
name: "App",
|
||||
dependencies: [
|
||||
.target(name: "ApiController"),
|
||||
.target(name: "AuthClient"),
|
||||
.target(name: "DatabaseClient"),
|
||||
.target(name: "ViewController"),
|
||||
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||
.product(name: "Fluent", package: "fluent"),
|
||||
.product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"),
|
||||
.product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"),
|
||||
.product(name: "Vapor", package: "vapor"),
|
||||
.product(name: "NIOCore", package: "swift-nio"),
|
||||
.product(name: "NIOPosix", package: "swift-nio"),
|
||||
@@ -51,16 +54,6 @@ let package = Package(
|
||||
.product(name: "VaporRouting", package: "vapor-routing"),
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "ApiController",
|
||||
dependencies: [
|
||||
.target(name: "DatabaseClient"),
|
||||
.target(name: "ManualDCore"),
|
||||
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||
.product(name: "DependenciesMacros", package: "swift-dependencies"),
|
||||
.product(name: "Vapor", package: "vapor"),
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "AuthClient",
|
||||
dependencies: [
|
||||
@@ -70,6 +63,20 @@ let package = Package(
|
||||
.product(name: "DependenciesMacros", package: "swift-dependencies"),
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "CSVParser",
|
||||
dependencies: [
|
||||
.target(name: "ManualDCore"),
|
||||
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||
.product(name: "DependenciesMacros", package: "swift-dependencies"),
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "CSVParsingTests",
|
||||
dependencies: [
|
||||
.target(name: "CSVParser")
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "DatabaseClient",
|
||||
dependencies: [
|
||||
@@ -78,11 +85,20 @@ let package = Package(
|
||||
.product(name: "DependenciesMacros", package: "swift-dependencies"),
|
||||
.product(name: "Fluent", package: "fluent"),
|
||||
.product(name: "Vapor", package: "vapor"),
|
||||
.product(name: "Validations", package: "swift-validations"),
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "DatabaseClientTests",
|
||||
dependencies: [
|
||||
.target(name: "App"),
|
||||
.target(name: "DatabaseClient"),
|
||||
.product(name: "DependenciesTestSupport", package: "swift-dependencies"),
|
||||
.product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"),
|
||||
]
|
||||
),
|
||||
|
||||
.target(
|
||||
name: "EnvClient",
|
||||
name: "EnvVars",
|
||||
dependencies: [
|
||||
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||
.product(name: "DependenciesMacros", package: "swift-dependencies"),
|
||||
@@ -106,7 +122,7 @@ let package = Package(
|
||||
.target(
|
||||
name: "PdfClient",
|
||||
dependencies: [
|
||||
.target(name: "EnvClient"),
|
||||
.target(name: "EnvVars"),
|
||||
.target(name: "FileClient"),
|
||||
.target(name: "ManualDCore"),
|
||||
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||
@@ -120,11 +136,10 @@ let package = Package(
|
||||
.target(name: "HTMLSnapshotTesting"),
|
||||
.target(name: "PdfClient"),
|
||||
.product(name: "SnapshotTesting", package: "swift-snapshot-testing"),
|
||||
],
|
||||
resources: [
|
||||
.copy("__Snapshots__")
|
||||
]
|
||||
// ,
|
||||
// resources: [
|
||||
// .copy("__Snapshots__")
|
||||
// ]
|
||||
),
|
||||
.target(
|
||||
name: "ProjectClient",
|
||||
@@ -138,24 +153,19 @@ let package = Package(
|
||||
.target(
|
||||
name: "ManualDCore",
|
||||
dependencies: [
|
||||
.product(name: "CasePaths", package: "swift-case-paths"),
|
||||
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||
.product(name: "Fluent", package: "fluent"),
|
||||
.product(name: "URLRouting", package: "swift-url-routing"),
|
||||
.product(name: "CasePaths", package: "swift-case-paths"),
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "ApiRouteTests",
|
||||
dependencies: [
|
||||
.target(name: "ManualDCore")
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "ManualDClient",
|
||||
dependencies: [
|
||||
"ManualDCore",
|
||||
.target(name: "ManualDCore"),
|
||||
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||
.product(name: "DependenciesMacros", package: "swift-dependencies"),
|
||||
.product(name: "Tagged", package: "swift-tagged"),
|
||||
]
|
||||
),
|
||||
.target(
|
||||
@@ -177,6 +187,7 @@ let package = Package(
|
||||
name: "ViewController",
|
||||
dependencies: [
|
||||
.target(name: "AuthClient"),
|
||||
.target(name: "CSVParser"),
|
||||
.target(name: "DatabaseClient"),
|
||||
.target(name: "PdfClient"),
|
||||
.target(name: "ProjectClient"),
|
||||
|
||||
@@ -7,12 +7,7 @@
|
||||
'Noto Color Emoji';
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
||||
monospace;
|
||||
--color-red-500: oklch(63.7% 0.237 25.331);
|
||||
--color-red-600: oklch(57.7% 0.245 27.325);
|
||||
--color-green-400: oklch(79.2% 0.209 151.711);
|
||||
--color-indigo-600: oklch(51.1% 0.262 276.966);
|
||||
--color-slate-300: oklch(86.9% 0.022 252.894);
|
||||
--color-slate-900: oklch(20.8% 0.042 265.755);
|
||||
--color-gray-200: oklch(92.8% 0.006 264.531);
|
||||
--color-gray-400: oklch(70.7% 0.022 261.325);
|
||||
--color-black: #000;
|
||||
@@ -30,7 +25,6 @@
|
||||
--text-3xl--line-height: calc(2.25 / 1.875);
|
||||
--font-weight-bold: 700;
|
||||
--radius-sm: 0.25rem;
|
||||
--radius-md: 0.375rem;
|
||||
--radius-lg: 0.5rem;
|
||||
--ease-out: cubic-bezier(0, 0, 0.2, 1);
|
||||
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
@@ -6218,6 +6212,9 @@
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
.inline {
|
||||
display: inline;
|
||||
}
|
||||
.inline-block {
|
||||
display: inline-block;
|
||||
}
|
||||
@@ -6982,9 +6979,6 @@
|
||||
.rounded-lg {
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
.rounded-md {
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
.rounded-selector {
|
||||
border-radius: var(--radius-selector);
|
||||
}
|
||||
@@ -7449,15 +7443,9 @@
|
||||
.bg-error {
|
||||
background-color: var(--color-error);
|
||||
}
|
||||
.bg-red-500 {
|
||||
background-color: var(--color-red-500);
|
||||
}
|
||||
.bg-secondary {
|
||||
background-color: var(--color-secondary);
|
||||
}
|
||||
.bg-white {
|
||||
background-color: var(--color-white);
|
||||
}
|
||||
.divider-accent {
|
||||
@layer daisyui.l1.l2 {
|
||||
&:before, &:after {
|
||||
@@ -7866,15 +7854,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.px-3 {
|
||||
padding-inline: calc(var(--spacing) * 3);
|
||||
}
|
||||
.px-4 {
|
||||
padding-inline: calc(var(--spacing) * 4);
|
||||
}
|
||||
.py-1\.5 {
|
||||
padding-block: calc(var(--spacing) * 1.5);
|
||||
}
|
||||
.py-2 {
|
||||
padding-block: calc(var(--spacing) * 2);
|
||||
}
|
||||
@@ -8530,15 +8512,9 @@
|
||||
.text-info {
|
||||
color: var(--color-info);
|
||||
}
|
||||
.text-slate-900 {
|
||||
color: var(--color-slate-900);
|
||||
}
|
||||
.text-success {
|
||||
color: var(--color-success);
|
||||
}
|
||||
.text-white {
|
||||
color: var(--color-white);
|
||||
}
|
||||
.lowercase {
|
||||
text-transform: lowercase;
|
||||
}
|
||||
@@ -8607,10 +8583,6 @@
|
||||
--tw-shadow: 0 25px 50px -12px var(--tw-shadow-color, rgb(0 0 0 / 0.25));
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
.shadow-lg {
|
||||
--tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
.shadow-sm {
|
||||
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
@@ -8619,13 +8591,6 @@
|
||||
outline-style: var(--tw-outline-style);
|
||||
outline-width: 1px;
|
||||
}
|
||||
.outline-1 {
|
||||
outline-style: var(--tw-outline-style);
|
||||
outline-width: 1px;
|
||||
}
|
||||
.-outline-offset-1 {
|
||||
outline-offset: calc(1px * -1);
|
||||
}
|
||||
.btn-ghost {
|
||||
@layer daisyui.l1 {
|
||||
&:not(.btn-active, :hover, :active:focus, :focus-visible, input:checked:not(.filter .btn)) {
|
||||
@@ -8650,9 +8615,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.outline-slate-300 {
|
||||
outline-color: var(--color-slate-300);
|
||||
}
|
||||
.filter {
|
||||
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
|
||||
}
|
||||
@@ -9406,16 +9368,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.invalid\:border-red-500 {
|
||||
&:invalid {
|
||||
border-color: var(--color-red-500);
|
||||
}
|
||||
}
|
||||
.out-of-range\:border-red-500 {
|
||||
&:out-of-range {
|
||||
border-color: var(--color-red-500);
|
||||
}
|
||||
}
|
||||
.hover\:bg-neutral {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
@@ -9423,13 +9375,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:bg-red-600 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
background-color: var(--color-red-600);
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:text-white {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
@@ -9437,22 +9382,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.focus\:outline {
|
||||
&:focus {
|
||||
outline-style: var(--tw-outline-style);
|
||||
outline-width: 1px;
|
||||
}
|
||||
}
|
||||
.focus\:-outline-offset-2 {
|
||||
&:focus {
|
||||
outline-offset: calc(2px * -1);
|
||||
}
|
||||
}
|
||||
.focus\:outline-indigo-600 {
|
||||
&:focus {
|
||||
outline-color: var(--color-indigo-600);
|
||||
}
|
||||
}
|
||||
.data-active\:bg-neutral {
|
||||
&[data-active] {
|
||||
background-color: var(--color-neutral);
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import Dependencies
|
||||
import DependenciesMacros
|
||||
import Logging
|
||||
import ManualDCore
|
||||
|
||||
extension DependencyValues {
|
||||
public var apiController: ApiController {
|
||||
get { self[ApiController.self] }
|
||||
set { self[ApiController.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
@DependencyClient
|
||||
public struct ApiController: Sendable {
|
||||
public var json: @Sendable (Request) async throws -> (any Encodable)?
|
||||
}
|
||||
|
||||
extension ApiController {
|
||||
public struct Request: Sendable {
|
||||
public let route: SiteRoute.Api
|
||||
public let logger: Logger
|
||||
|
||||
public init(route: SiteRoute.Api, logger: Logger) {
|
||||
self.route = route
|
||||
self.logger = logger
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ApiController: DependencyKey {
|
||||
public static let testValue = Self()
|
||||
|
||||
public static let liveValue = Self(
|
||||
json: { request in
|
||||
try await request.route.respond(logger: request.logger)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
import DatabaseClient
|
||||
import Dependencies
|
||||
import Logging
|
||||
import ManualDCore
|
||||
|
||||
extension SiteRoute.Api {
|
||||
func respond(logger: Logger) async throws -> (any Encodable)? {
|
||||
switch self {
|
||||
case .project(let route):
|
||||
return try await route.respond(logger: logger)
|
||||
case .room(let route):
|
||||
return try await route.respond(logger: logger)
|
||||
case .equipment(let route):
|
||||
return try await route.respond(logger: logger)
|
||||
case .componentLoss(let route):
|
||||
return try await route.respond(logger: logger)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SiteRoute.Api.ProjectRoute {
|
||||
|
||||
func respond(logger: Logger) async throws -> (any Encodable)? {
|
||||
@Dependency(\.database) var database
|
||||
|
||||
switch self {
|
||||
case .create(let request):
|
||||
// return try await database.projects.create(request)
|
||||
// FIX:
|
||||
fatalError()
|
||||
case .delete(let id):
|
||||
try await database.projects.delete(id)
|
||||
return nil
|
||||
case .detail(let id, let route):
|
||||
switch route {
|
||||
case .index:
|
||||
return try await database.projects.detail(id)
|
||||
case .completedSteps:
|
||||
// FIX:
|
||||
fatalError()
|
||||
|
||||
}
|
||||
case .get(let id):
|
||||
guard let project = try await database.projects.get(id) else {
|
||||
logger.error("Project not found for id: \(id)")
|
||||
throw ApiError("Project not found.")
|
||||
}
|
||||
return project
|
||||
case .index:
|
||||
// FIX: Fix to return projects.
|
||||
return [Project]()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SiteRoute.Api.RoomRoute {
|
||||
|
||||
func respond(logger: Logger) async throws -> (any Encodable)? {
|
||||
@Dependency(\.database) var database
|
||||
|
||||
switch self {
|
||||
case .create(let request):
|
||||
return try await database.rooms.create(request)
|
||||
case .delete(let id):
|
||||
try await database.rooms.delete(id)
|
||||
return nil
|
||||
case .get(let id):
|
||||
guard let room = try await database.rooms.get(id) else {
|
||||
logger.error("Room not found for id: \(id)")
|
||||
throw ApiError("Room not found.")
|
||||
}
|
||||
return room
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SiteRoute.Api.EquipmentRoute {
|
||||
|
||||
func respond(logger: Logger) async throws -> (any Encodable)? {
|
||||
@Dependency(\.database) var database
|
||||
|
||||
switch self {
|
||||
case .create(let request):
|
||||
return try await database.equipment.create(request)
|
||||
case .delete(let id):
|
||||
try await database.equipment.delete(id)
|
||||
return nil
|
||||
case .fetch(let projectID):
|
||||
return try await database.equipment.fetch(projectID)
|
||||
case .get(let id):
|
||||
guard let room = try await database.equipment.get(id) else {
|
||||
logger.error("Equipment not found for id: \(id)")
|
||||
throw ApiError("Equipment not found.")
|
||||
}
|
||||
return room
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SiteRoute.Api.ComponentLossRoute {
|
||||
|
||||
func respond(logger: Logger) async throws -> (any Encodable)? {
|
||||
@Dependency(\.database) var database
|
||||
|
||||
switch self {
|
||||
case .create(let request):
|
||||
return try await database.componentLoss.create(request)
|
||||
case .delete(let id):
|
||||
try await database.componentLoss.delete(id)
|
||||
return nil
|
||||
case .fetch(let projectID):
|
||||
return try await database.componentLoss.fetch(projectID)
|
||||
case .get(let id):
|
||||
guard let room = try await database.componentLoss.get(id) else {
|
||||
logger.error("Component loss not found for id: \(id)")
|
||||
throw ApiError("Component loss not found.")
|
||||
}
|
||||
return room
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct ApiError: Error {
|
||||
let message: String
|
||||
|
||||
init(_ message: String) {
|
||||
self.message = message
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,37 @@
|
||||
import ApiController
|
||||
import ManualDCore
|
||||
import Vapor
|
||||
|
||||
extension ApiController {
|
||||
|
||||
func respond(_ route: SiteRoute.Api, request: Vapor.Request) async throws
|
||||
-> any AsyncResponseEncodable
|
||||
{
|
||||
guard let encodable = try await json(.init(route: route, logger: request.logger)) else {
|
||||
return HTTPStatus.ok
|
||||
}
|
||||
return AnyJSONResponse(value: encodable)
|
||||
}
|
||||
}
|
||||
|
||||
struct AnyJSONResponse: AsyncResponseEncodable {
|
||||
public var headers: HTTPHeaders = ["Content-Type": "application/json"]
|
||||
let value: any Encodable
|
||||
|
||||
init(additionalHeaders: HTTPHeaders = [:], value: any Encodable) {
|
||||
if additionalHeaders.contains(name: .contentType) {
|
||||
self.headers = additionalHeaders
|
||||
} else {
|
||||
headers.add(contentsOf: additionalHeaders)
|
||||
}
|
||||
self.value = value
|
||||
}
|
||||
|
||||
func encodeResponse(for request: Request) async throws -> Response {
|
||||
try Response(
|
||||
status: .ok,
|
||||
headers: headers,
|
||||
body: .init(data: JSONEncoder().encode(value))
|
||||
)
|
||||
}
|
||||
}
|
||||
// import ApiController
|
||||
// import ManualDCore
|
||||
// import Vapor
|
||||
//
|
||||
// extension ApiController {
|
||||
//
|
||||
// func respond(_ route: SiteRoute.Api, request: Vapor.Request) async throws
|
||||
// -> any AsyncResponseEncodable
|
||||
// {
|
||||
// guard let encodable = try await json(.init(route: route, logger: request.logger)) else {
|
||||
// return HTTPStatus.ok
|
||||
// }
|
||||
// return AnyJSONResponse(value: encodable)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// struct AnyJSONResponse: AsyncResponseEncodable {
|
||||
// public var headers: HTTPHeaders = ["Content-Type": "application/json"]
|
||||
// let value: any Encodable
|
||||
//
|
||||
// init(additionalHeaders: HTTPHeaders = [:], value: any Encodable) {
|
||||
// if additionalHeaders.contains(name: .contentType) {
|
||||
// self.headers = additionalHeaders
|
||||
// } else {
|
||||
// headers.add(contentsOf: additionalHeaders)
|
||||
// }
|
||||
// self.value = value
|
||||
// }
|
||||
//
|
||||
// func encodeResponse(for request: Request) async throws -> Response {
|
||||
// try Response(
|
||||
// status: .ok,
|
||||
// headers: headers,
|
||||
// body: .init(data: JSONEncoder().encode(value))
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import ApiController
|
||||
// import ApiController
|
||||
import AuthClient
|
||||
import DatabaseClient
|
||||
import Dependencies
|
||||
import EnvVars
|
||||
import ManualDCore
|
||||
import PdfClient
|
||||
import Vapor
|
||||
@@ -12,27 +13,31 @@ import ViewController
|
||||
struct DependenciesMiddleware: AsyncMiddleware {
|
||||
|
||||
private let values: DependencyValues.Continuation
|
||||
private let apiController: ApiController
|
||||
// private let apiController: ApiController
|
||||
private let database: DatabaseClient
|
||||
private let environment: EnvVars
|
||||
private let viewController: ViewController
|
||||
|
||||
init(
|
||||
database: DatabaseClient,
|
||||
apiController: ApiController = .liveValue,
|
||||
environment: EnvVars,
|
||||
// apiController: ApiController = .liveValue,
|
||||
viewController: ViewController = .liveValue
|
||||
) {
|
||||
self.values = withEscapedDependencies { $0 }
|
||||
self.apiController = apiController
|
||||
// self.apiController = apiController
|
||||
self.database = database
|
||||
self.environment = environment
|
||||
self.viewController = viewController
|
||||
}
|
||||
|
||||
func respond(to request: Request, chainingTo next: any AsyncResponder) async throws -> Response {
|
||||
try await values.yield {
|
||||
try await withDependencies {
|
||||
$0.apiController = apiController
|
||||
$0.authClient = .live(on: request)
|
||||
// $0.apiController = apiController
|
||||
$0.auth = .live(on: request)
|
||||
$0.database = database
|
||||
$0.environment = environment
|
||||
// $0.dateFormatter = .liveValue
|
||||
$0.viewController = viewController
|
||||
$0.pdfClient = .liveValue
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import DatabaseClient
|
||||
import Dependencies
|
||||
import Elementary
|
||||
import EnvVars
|
||||
import Fluent
|
||||
import FluentPostgresDriver
|
||||
import FluentSQLiteDriver
|
||||
import ManualDCore
|
||||
import NIOSSL
|
||||
@@ -14,12 +16,15 @@ import ViewController
|
||||
// configures your application
|
||||
public func configure(
|
||||
_ app: Application,
|
||||
in environment: EnvVars,
|
||||
makeDatabaseClient: @escaping (any Database) -> DatabaseClient = { .live(database: $0) }
|
||||
) async throws {
|
||||
// Setup the database client.
|
||||
let databaseClient = try await setupDatabase(on: app, factory: makeDatabaseClient)
|
||||
let databaseClient = try await setupDatabase(
|
||||
on: app, environment: environment, factory: makeDatabaseClient
|
||||
)
|
||||
// Add the global middlewares.
|
||||
addMiddleware(to: app, database: databaseClient)
|
||||
addMiddleware(to: app, database: databaseClient, environment: environment)
|
||||
#if DEBUG
|
||||
// Live reload of the application for development when launched with the `./swift-dev` command
|
||||
// app.lifecycle.use(BrowserSyncHandler())
|
||||
@@ -33,7 +38,11 @@ public func configure(
|
||||
addCommands(to: app)
|
||||
}
|
||||
|
||||
private func addMiddleware(to app: Application, database databaseClient: DatabaseClient) {
|
||||
private func addMiddleware(
|
||||
to app: Application,
|
||||
database databaseClient: DatabaseClient,
|
||||
environment: EnvVars
|
||||
) {
|
||||
// cors middleware should come before default error middleware using `at: .beginning`
|
||||
let corsConfiguration = CORSMiddleware.Configuration(
|
||||
allowedOrigin: .all,
|
||||
@@ -48,16 +57,20 @@ private func addMiddleware(to app: Application, database databaseClient: Databas
|
||||
|
||||
app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
|
||||
app.middleware.use(app.sessions.middleware)
|
||||
app.middleware.use(DependenciesMiddleware(database: databaseClient))
|
||||
app.middleware.use(DependenciesMiddleware(database: databaseClient, environment: environment))
|
||||
}
|
||||
|
||||
private func setupDatabase(
|
||||
on app: Application,
|
||||
environment: EnvVars,
|
||||
factory makeDatabaseClient: @escaping (any Database) -> DatabaseClient
|
||||
) async throws -> DatabaseClient {
|
||||
switch app.environment {
|
||||
case .production, .development:
|
||||
let dbFileName = Environment.get("SQLITE_FILENAME") ?? "db.sqlite"
|
||||
case .production:
|
||||
let configuration = try environment.postgresConfiguration()
|
||||
app.databases.use(.postgres(configuration: configuration), as: .psql)
|
||||
case .development:
|
||||
let dbFileName = environment.sqlitePath ?? "db.sqlite"
|
||||
app.databases.use(DatabaseConfigurationFactory.sqlite(.file(dbFileName)), as: .sqlite)
|
||||
default:
|
||||
app.databases.use(DatabaseConfigurationFactory.sqlite(.memory), as: .sqlite)
|
||||
@@ -65,9 +78,7 @@ private func setupDatabase(
|
||||
|
||||
let databaseClient = makeDatabaseClient(app.db)
|
||||
|
||||
if app.environment != .testing {
|
||||
try await app.migrations.add(databaseClient.migrations())
|
||||
}
|
||||
try await app.migrations.add(databaseClient.migrations())
|
||||
|
||||
return databaseClient
|
||||
}
|
||||
@@ -102,8 +113,6 @@ extension SiteRoute {
|
||||
|
||||
fileprivate func middleware() -> [any Middleware]? {
|
||||
switch self {
|
||||
case .api:
|
||||
return nil
|
||||
case .health:
|
||||
return nil
|
||||
case .view(let route):
|
||||
@@ -119,13 +128,10 @@ private func siteHandler(
|
||||
request: Request,
|
||||
route: SiteRoute
|
||||
) async throws -> any AsyncResponseEncodable {
|
||||
@Dependency(\.apiController) var apiController
|
||||
@Dependency(\.viewController) var viewController
|
||||
@Dependency(\.projectClient) var projectClient
|
||||
|
||||
switch route {
|
||||
case .api(let route):
|
||||
return try await apiController.respond(route, request: request)
|
||||
case .health:
|
||||
return HTTPStatus.ok
|
||||
// Generating a pdf return's a `Response` instead of `HTML` like other views, so we
|
||||
@@ -136,3 +142,30 @@ private func siteHandler(
|
||||
return try await viewController.respond(route: route, request: request)
|
||||
}
|
||||
}
|
||||
|
||||
extension EnvVars {
|
||||
func postgresConfiguration() throws -> SQLPostgresConfiguration {
|
||||
guard let hostname = postgresHostname,
|
||||
let username = postgresUsername,
|
||||
let password = postgresPassword,
|
||||
let database = postgresDatabase
|
||||
else {
|
||||
throw EnvError("Missing environment variables for postgres connection.")
|
||||
}
|
||||
return .init(
|
||||
hostname: hostname,
|
||||
username: username,
|
||||
password: password,
|
||||
database: database,
|
||||
tls: .disable
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct EnvError: Error {
|
||||
let reason: String
|
||||
|
||||
init(_ reason: String) {
|
||||
self.reason = reason
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import DatabaseClient
|
||||
import Dependencies
|
||||
import EnvVars
|
||||
import Logging
|
||||
import NIOCore
|
||||
import NIOPosix
|
||||
@@ -17,11 +18,15 @@ enum Entrypoint {
|
||||
// You can enable it if you'd like to reduce the amount of context switching between NIO and Swift Concurrency.
|
||||
// Note: this has caused issues with some libraries that use `.wait()` and cleanly shutting down.
|
||||
// If enabled, you should be careful about calling async functions before this point as it can cause assertion failures.
|
||||
// let executorTakeoverSuccess = NIOSingletons.unsafeTryInstallSingletonPosixEventLoopGroupAsConcurrencyGlobalExecutor()
|
||||
// app.logger.debug("Tried to install SwiftNIO's EventLoopGroup as Swift's global concurrency executor", metadata: ["success": .stringConvertible(executorTakeoverSuccess)])
|
||||
let executorTakeoverSuccess =
|
||||
NIOSingletons.unsafeTryInstallSingletonPosixEventLoopGroupAsConcurrencyGlobalExecutor()
|
||||
app.logger.debug(
|
||||
"Tried to install SwiftNIO's EventLoopGroup as Swift's global concurrency executor",
|
||||
metadata: ["success": .stringConvertible(executorTakeoverSuccess)]
|
||||
)
|
||||
|
||||
do {
|
||||
try await configure(app)
|
||||
try await configure(app, in: EnvVars.live())
|
||||
} catch {
|
||||
app.logger.report(error: error)
|
||||
try? await app.asyncShutdown()
|
||||
|
||||
@@ -5,17 +5,23 @@ import ManualDCore
|
||||
import Vapor
|
||||
|
||||
extension DependencyValues {
|
||||
public var authClient: AuthClient {
|
||||
/// Authentication dependency, for handling authentication tasks.
|
||||
public var auth: AuthClient {
|
||||
get { self[AuthClient.self] }
|
||||
set { self[AuthClient.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents authentication tasks that are used in the application.
|
||||
@DependencyClient
|
||||
public struct AuthClient: Sendable {
|
||||
/// Create a new user and log them in.
|
||||
public var createAndLogin: @Sendable (User.Create) async throws -> User
|
||||
/// Get the current user.
|
||||
public var currentUser: @Sendable () throws -> User
|
||||
/// Login a user.
|
||||
public var login: @Sendable (User.Login) async throws -> User
|
||||
/// Logout a user.
|
||||
public var logout: @Sendable () throws -> Void
|
||||
}
|
||||
|
||||
|
||||
43
Sources/CSVParser/Interface.swift
Normal file
43
Sources/CSVParser/Interface.swift
Normal file
@@ -0,0 +1,43 @@
|
||||
import Dependencies
|
||||
import DependenciesMacros
|
||||
import ManualDCore
|
||||
import Parsing
|
||||
|
||||
extension DependencyValues {
|
||||
public var csvParser: CSVParser {
|
||||
get { self[CSVParser.self] }
|
||||
set { self[CSVParser.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
@DependencyClient
|
||||
public struct CSVParser: Sendable {
|
||||
public var parseRooms: @Sendable (Room.CSV) async throws -> [Room.Create]
|
||||
}
|
||||
|
||||
extension CSVParser: DependencyKey {
|
||||
public static let testValue = Self()
|
||||
|
||||
public static let liveValue = Self(
|
||||
parseRooms: { csv in
|
||||
guard let string = String(data: csv.file, encoding: .utf8) else {
|
||||
throw CSVParsingError("Unreadable file data")
|
||||
}
|
||||
let rows = try RoomCSVParser().parse(string[...].utf8)
|
||||
let rooms = rows.reduce(into: [Room.Create]()) {
|
||||
if case .room(let room) = $1 {
|
||||
$0.append(room)
|
||||
}
|
||||
}
|
||||
return rooms
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
public struct CSVParsingError: Error {
|
||||
let reason: String
|
||||
|
||||
public init(_ reason: String) {
|
||||
self.reason = reason
|
||||
}
|
||||
}
|
||||
51
Sources/CSVParser/Internal/Room+parsing.swift
Normal file
51
Sources/CSVParser/Internal/Room+parsing.swift
Normal file
@@ -0,0 +1,51 @@
|
||||
import ManualDCore
|
||||
import Parsing
|
||||
|
||||
struct RoomCSVParser: Parser {
|
||||
var body: some Parser<Substring.UTF8View, [RoomRowType]> {
|
||||
Many {
|
||||
RoomRowParser()
|
||||
} separator: {
|
||||
"\n".utf8
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RoomRowParser: Parser {
|
||||
|
||||
var body: some Parser<Substring.UTF8View, RoomRowType> {
|
||||
OneOf {
|
||||
RoomCreateParser().map { RoomRowType.room($0) }
|
||||
Prefix { $0 != UInt8(ascii: "\n") }
|
||||
.map(.string)
|
||||
.map { RoomRowType.header($0) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum RoomRowType {
|
||||
case header(String)
|
||||
case room(Room.Create)
|
||||
}
|
||||
|
||||
struct RoomCreateParser: ParserPrinter {
|
||||
|
||||
var body: some ParserPrinter<Substring.UTF8View, Room.Create> {
|
||||
ParsePrint {
|
||||
Prefix { $0 != UInt8(ascii: ",") }.map(.string)
|
||||
",".utf8
|
||||
Double.parser()
|
||||
",".utf8
|
||||
Optionally {
|
||||
Double.parser()
|
||||
}
|
||||
",".utf8
|
||||
Optionally {
|
||||
Double.parser()
|
||||
}
|
||||
",".utf8
|
||||
Int.parser()
|
||||
}
|
||||
.map(.memberwise(Room.Create.init))
|
||||
}
|
||||
}
|
||||
@@ -4,23 +4,132 @@ import FluentKit
|
||||
import ManualDCore
|
||||
|
||||
extension DependencyValues {
|
||||
/// The database dependency.
|
||||
public var database: DatabaseClient {
|
||||
get { self[DatabaseClient.self] }
|
||||
set { self[DatabaseClient.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the database interactions used by the application.
|
||||
@DependencyClient
|
||||
public struct DatabaseClient: Sendable {
|
||||
/// Database migrations.
|
||||
public var migrations: Migrations
|
||||
/// Interactions with the projects table.
|
||||
public var projects: Projects
|
||||
/// Interactions with the rooms table.
|
||||
public var rooms: Rooms
|
||||
/// Interactions with the equipment table.
|
||||
public var equipment: Equipment
|
||||
public var componentLoss: ComponentLoss
|
||||
public var effectiveLength: EffectiveLengthClient
|
||||
/// Interactions with the component losses table.
|
||||
public var componentLosses: ComponentLosses
|
||||
/// Interactions with the equivalent lengths table.
|
||||
public var equivalentLengths: EquivalentLengths
|
||||
/// Interactions with the users table.
|
||||
public var users: Users
|
||||
public var userProfile: UserProfile
|
||||
/// Interactions with the user profiles table.
|
||||
public var userProfiles: UserProfiles
|
||||
/// Interactions with the trunk sizes table.
|
||||
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 (Project.ID, Room.Create) async throws -> Room
|
||||
public var createMany: @Sendable (Project.ID, [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 {
|
||||
@@ -29,10 +138,10 @@ extension DatabaseClient: TestDependencyKey {
|
||||
projects: .testValue,
|
||||
rooms: .testValue,
|
||||
equipment: .testValue,
|
||||
componentLoss: .testValue,
|
||||
effectiveLength: .testValue,
|
||||
componentLosses: .testValue,
|
||||
equivalentLengths: .testValue,
|
||||
users: .testValue,
|
||||
userProfile: .testValue,
|
||||
userProfiles: .testValue,
|
||||
trunkSizes: .testValue
|
||||
)
|
||||
|
||||
@@ -42,44 +151,11 @@ extension DatabaseClient: TestDependencyKey {
|
||||
projects: .live(database: database),
|
||||
rooms: .live(database: database),
|
||||
equipment: .live(database: database),
|
||||
componentLoss: .live(database: database),
|
||||
effectiveLength: .live(database: database),
|
||||
componentLosses: .live(database: database),
|
||||
equivalentLengths: .live(database: database),
|
||||
users: .live(database: database),
|
||||
userProfile: .live(database: database),
|
||||
userProfiles: .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 {
|
||||
public static let liveValue = Self(
|
||||
run: {
|
||||
[
|
||||
Project.Migrate(),
|
||||
User.Migrate(),
|
||||
User.Token.Migrate(),
|
||||
User.Profile.Migrate(),
|
||||
ComponentPressureLoss.Migrate(),
|
||||
EquipmentInfo.Migrate(),
|
||||
Room.Migrate(),
|
||||
EffectiveLength.Migrate(),
|
||||
TrunkSize.Migrate(),
|
||||
]
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
48
Sources/DatabaseClient/Internal/Array+validator.swift
Normal file
48
Sources/DatabaseClient/Internal/Array+validator.swift
Normal file
@@ -0,0 +1,48 @@
|
||||
import Validations
|
||||
|
||||
extension Validator {
|
||||
|
||||
static func validate<Child: Validatable>(
|
||||
_ toChild: KeyPath<Value, [Child]>
|
||||
)
|
||||
-> Self
|
||||
{
|
||||
self.mapValue({ $0[keyPath: toChild] }, with: ArrayValidator())
|
||||
}
|
||||
|
||||
static func validate<Child: Validatable>(
|
||||
_ toChild: KeyPath<Value, [Child]?>
|
||||
)
|
||||
-> Self
|
||||
{
|
||||
self.mapValue({ $0[keyPath: toChild] }, with: ArrayValidator().optional())
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element: Validatable {
|
||||
static func validator() -> some Validation<Self> {
|
||||
ArrayValidator<Element>()
|
||||
}
|
||||
}
|
||||
|
||||
struct ArrayValidator<Element>: Validation where Element: Validatable {
|
||||
func validate(_ value: [Element]) throws {
|
||||
for item in value {
|
||||
try item.validate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ForEachValidator<T, E>: Validation where T: Validation, T.Value == E {
|
||||
let validator: T
|
||||
|
||||
init(@ValidationBuilder<E> builder: () -> T) {
|
||||
self.validator = builder()
|
||||
}
|
||||
|
||||
func validate(_ value: [E]) throws {
|
||||
for item in value {
|
||||
try validator.validate(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,31 +3,19 @@ import DependenciesMacros
|
||||
import Fluent
|
||||
import Foundation
|
||||
import ManualDCore
|
||||
import SQLKit
|
||||
import Validations
|
||||
|
||||
extension DatabaseClient {
|
||||
@DependencyClient
|
||||
public struct ComponentLoss: Sendable {
|
||||
public var create:
|
||||
@Sendable (ComponentPressureLoss.Create) async throws -> ComponentPressureLoss
|
||||
public var delete: @Sendable (ComponentPressureLoss.ID) async throws -> Void
|
||||
public var fetch: @Sendable (Project.ID) async throws -> [ComponentPressureLoss]
|
||||
public var get: @Sendable (ComponentPressureLoss.ID) async throws -> ComponentPressureLoss?
|
||||
public var update:
|
||||
@Sendable (ComponentPressureLoss.ID, ComponentPressureLoss.Update) async throws ->
|
||||
ComponentPressureLoss
|
||||
}
|
||||
}
|
||||
|
||||
extension DatabaseClient.ComponentLoss: TestDependencyKey {
|
||||
extension DatabaseClient.ComponentLosses: TestDependencyKey {
|
||||
public static let testValue = Self()
|
||||
}
|
||||
|
||||
extension DatabaseClient.ComponentLoss {
|
||||
extension DatabaseClient.ComponentLosses {
|
||||
public static func live(database: any Database) -> Self {
|
||||
.init(
|
||||
create: { request in
|
||||
let model = try request.toModel()
|
||||
try await model.save(on: database)
|
||||
let model = request.toModel()
|
||||
try await model.validateAndSave(on: database)
|
||||
return try model.toDTO()
|
||||
},
|
||||
delete: { id in
|
||||
@@ -48,13 +36,13 @@ extension DatabaseClient.ComponentLoss {
|
||||
try await ComponentLossModel.find(id, on: database).map { try $0.toDTO() }
|
||||
},
|
||||
update: { id, updates in
|
||||
try updates.validate()
|
||||
// try updates.validate()
|
||||
guard let model = try await ComponentLossModel.find(id, on: database) else {
|
||||
throw NotFoundError()
|
||||
}
|
||||
model.applyUpdates(updates)
|
||||
if model.hasChanges {
|
||||
try await model.save(on: database)
|
||||
try await model.validateAndSave(on: database)
|
||||
}
|
||||
return try model.toDTO()
|
||||
}
|
||||
@@ -64,40 +52,9 @@ extension DatabaseClient.ComponentLoss {
|
||||
|
||||
extension ComponentPressureLoss.Create {
|
||||
|
||||
func toModel() throws(ValidationError) -> ComponentLossModel {
|
||||
try validate()
|
||||
func toModel() -> ComponentLossModel {
|
||||
return .init(name: name, value: value, projectID: projectID)
|
||||
}
|
||||
|
||||
func validate() throws(ValidationError) {
|
||||
guard !name.isEmpty else {
|
||||
throw ValidationError("Component loss name should not be empty.")
|
||||
}
|
||||
guard value > 0 else {
|
||||
throw ValidationError("Component loss value should be greater than 0.")
|
||||
}
|
||||
guard value < 1.0 else {
|
||||
throw ValidationError("Component loss value should be less than 1.0.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ComponentPressureLoss.Update {
|
||||
func validate() throws(ValidationError) {
|
||||
if let name {
|
||||
guard !name.isEmpty else {
|
||||
throw ValidationError("Component loss name should not be empty.")
|
||||
}
|
||||
}
|
||||
if let value {
|
||||
guard value > 0 else {
|
||||
throw ValidationError("Component loss value should be greater than 0.")
|
||||
}
|
||||
guard value < 1.0 else {
|
||||
throw ValidationError("Component loss value should be less than 1.0.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ComponentPressureLoss {
|
||||
@@ -184,3 +141,19 @@ final class ComponentLossModel: Model, @unchecked Sendable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ComponentLossModel: Validatable {
|
||||
|
||||
var body: some Validation<ComponentLossModel> {
|
||||
Validator.accumulating {
|
||||
Validator.validate(\.name, with: .notEmpty())
|
||||
.errorLabel("Name", inline: true)
|
||||
|
||||
Validator.validate(\.value) {
|
||||
Double.greaterThan(0.0)
|
||||
Double.lessThanOrEquals(1.0)
|
||||
}
|
||||
.errorLabel("Value", inline: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,29 +3,16 @@ import DependenciesMacros
|
||||
import Fluent
|
||||
import Foundation
|
||||
import ManualDCore
|
||||
|
||||
extension DatabaseClient {
|
||||
@DependencyClient
|
||||
public struct Equipment: Sendable {
|
||||
public var create: @Sendable (EquipmentInfo.Create) async throws -> EquipmentInfo
|
||||
public var delete: @Sendable (EquipmentInfo.ID) async throws -> Void
|
||||
public var fetch: @Sendable (Project.ID) async throws -> EquipmentInfo?
|
||||
public var get: @Sendable (EquipmentInfo.ID) async throws -> EquipmentInfo?
|
||||
public var update:
|
||||
@Sendable (EquipmentInfo.ID, EquipmentInfo.Update) async throws -> EquipmentInfo
|
||||
}
|
||||
}
|
||||
import Validations
|
||||
|
||||
extension DatabaseClient.Equipment: TestDependencyKey {
|
||||
public static let testValue = Self()
|
||||
}
|
||||
|
||||
extension DatabaseClient.Equipment {
|
||||
public static func live(database: any Database) -> Self {
|
||||
.init(
|
||||
create: { request in
|
||||
let model = try request.toModel()
|
||||
try await model.save(on: database)
|
||||
let model = request.toModel()
|
||||
try await model.validateAndSave(on: database)
|
||||
return try model.toDTO()
|
||||
},
|
||||
delete: { id in
|
||||
@@ -51,10 +38,9 @@ extension DatabaseClient.Equipment {
|
||||
guard let model = try await EquipmentModel.find(id, on: database) else {
|
||||
throw NotFoundError()
|
||||
}
|
||||
try updates.validate()
|
||||
model.applyUpdates(updates)
|
||||
if model.hasChanges {
|
||||
try await model.save(on: database)
|
||||
try await model.validateAndSave(on: database)
|
||||
}
|
||||
return try model.toDTO()
|
||||
}
|
||||
@@ -64,8 +50,7 @@ extension DatabaseClient.Equipment {
|
||||
|
||||
extension EquipmentInfo.Create {
|
||||
|
||||
func toModel() throws(ValidationError) -> EquipmentModel {
|
||||
try validate()
|
||||
func toModel() -> EquipmentModel {
|
||||
return .init(
|
||||
staticPressure: staticPressure,
|
||||
heatingCFM: heatingCFM,
|
||||
@@ -74,44 +59,6 @@ extension EquipmentInfo.Create {
|
||||
)
|
||||
}
|
||||
|
||||
func validate() throws(ValidationError) {
|
||||
guard staticPressure >= 0 else {
|
||||
throw ValidationError("Equipment info static pressure should be greater than 0.")
|
||||
}
|
||||
guard staticPressure <= 1.0 else {
|
||||
throw ValidationError("Equipment info static pressure should be less than 1.0.")
|
||||
}
|
||||
guard heatingCFM >= 0 else {
|
||||
throw ValidationError("Equipment info heating CFM should be greater than 0.")
|
||||
}
|
||||
guard coolingCFM >= 0 else {
|
||||
throw ValidationError("Equipment info heating CFM should be greater than 0.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension EquipmentInfo.Update {
|
||||
var hasUpdates: Bool {
|
||||
staticPressure != nil || heatingCFM != nil || coolingCFM != nil
|
||||
}
|
||||
|
||||
func validate() throws(ValidationError) {
|
||||
if let staticPressure {
|
||||
guard staticPressure >= 0 else {
|
||||
throw ValidationError("Equipment info static pressure should be greater than 0.")
|
||||
}
|
||||
}
|
||||
if let heatingCFM {
|
||||
guard heatingCFM >= 0 else {
|
||||
throw ValidationError("Equipment info heating CFM should be greater than 0.")
|
||||
}
|
||||
}
|
||||
if let coolingCFM {
|
||||
guard coolingCFM >= 0 else {
|
||||
throw ValidationError("Equipment info heating CFM should be greater than 0.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension EquipmentInfo {
|
||||
@@ -211,3 +158,22 @@ final class EquipmentModel: Model, @unchecked Sendable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension EquipmentModel: Validatable {
|
||||
|
||||
var body: some Validation<EquipmentModel> {
|
||||
Validator.accumulating {
|
||||
Validator.validate(\.staticPressure) {
|
||||
Double.greaterThan(0.0)
|
||||
Double.lessThan(1.0)
|
||||
}
|
||||
.errorLabel("Static Pressure", inline: true)
|
||||
|
||||
Validator.validate(\.heatingCFM, with: .greaterThan(0))
|
||||
.errorLabel("Heating CFM", inline: true)
|
||||
|
||||
Validator.validate(\.coolingCFM, with: .greaterThan(0))
|
||||
.errorLabel("Cooling CFM", inline: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,28 +3,16 @@ import DependenciesMacros
|
||||
import Fluent
|
||||
import Foundation
|
||||
import ManualDCore
|
||||
import Validations
|
||||
|
||||
extension DatabaseClient {
|
||||
@DependencyClient
|
||||
public struct EffectiveLengthClient: Sendable {
|
||||
public var create: @Sendable (EffectiveLength.Create) async throws -> EffectiveLength
|
||||
public var delete: @Sendable (EffectiveLength.ID) async throws -> Void
|
||||
public var fetch: @Sendable (Project.ID) async throws -> [EffectiveLength]
|
||||
public var 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 {
|
||||
extension DatabaseClient.EquivalentLengths: TestDependencyKey {
|
||||
public static let testValue = Self()
|
||||
|
||||
public static func live(database: any Database) -> Self {
|
||||
.init(
|
||||
create: { request in
|
||||
let model = try request.toModel()
|
||||
try await model.save(on: database)
|
||||
try await model.validateAndSave(on: database)
|
||||
return try model.toDTO()
|
||||
},
|
||||
delete: { id in
|
||||
@@ -66,7 +54,7 @@ extension DatabaseClient.EffectiveLengthClient: TestDependencyKey {
|
||||
}
|
||||
try model.applyUpdates(updates)
|
||||
if model.hasChanges {
|
||||
try await model.save(on: database)
|
||||
try await model.validateAndSave(on: database)
|
||||
}
|
||||
return try model.toDTO()
|
||||
}
|
||||
@@ -74,10 +62,12 @@ extension DatabaseClient.EffectiveLengthClient: TestDependencyKey {
|
||||
}
|
||||
}
|
||||
|
||||
extension EffectiveLength.Create {
|
||||
extension EquivalentLength.Create {
|
||||
|
||||
func toModel() throws -> EffectiveLengthModel {
|
||||
try validate()
|
||||
if groups.count > 0 {
|
||||
try [EquivalentLength.FittingGroup].validator().validate(groups)
|
||||
}
|
||||
return try .init(
|
||||
name: name,
|
||||
type: type.rawValue,
|
||||
@@ -86,15 +76,9 @@ extension EffectiveLength.Create {
|
||||
projectID: projectID
|
||||
)
|
||||
}
|
||||
|
||||
func validate() throws(ValidationError) {
|
||||
guard !name.isEmpty else {
|
||||
throw ValidationError("Effective length name can not be empty.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension EffectiveLength {
|
||||
extension EquivalentLength {
|
||||
|
||||
struct Migrate: AsyncMigration {
|
||||
let name = "CreateEffectiveLength"
|
||||
@@ -173,20 +157,20 @@ final class EffectiveLengthModel: Model, @unchecked Sendable {
|
||||
$project.id = projectID
|
||||
}
|
||||
|
||||
func toDTO() throws -> EffectiveLength {
|
||||
func toDTO() throws -> EquivalentLength {
|
||||
try .init(
|
||||
id: requireID(),
|
||||
projectID: $project.id,
|
||||
name: name,
|
||||
type: .init(rawValue: type)!,
|
||||
straightLengths: straightLengths,
|
||||
groups: JSONDecoder().decode([EffectiveLength.Group].self, from: groups),
|
||||
groups: JSONDecoder().decode([EquivalentLength.FittingGroup].self, from: groups),
|
||||
createdAt: createdAt!,
|
||||
updatedAt: updatedAt!
|
||||
)
|
||||
}
|
||||
|
||||
func applyUpdates(_ updates: EffectiveLength.Update) throws {
|
||||
func applyUpdates(_ updates: EquivalentLength.Update) throws {
|
||||
if let name = updates.name, name != self.name {
|
||||
self.name = name
|
||||
}
|
||||
@@ -197,7 +181,51 @@ final class EffectiveLengthModel: Model, @unchecked Sendable {
|
||||
self.straightLengths = straightLengths
|
||||
}
|
||||
if let groups = updates.groups {
|
||||
if groups.count > 0 {
|
||||
try [EquivalentLength.FittingGroup].validator().validate(groups)
|
||||
}
|
||||
self.groups = try JSONEncoder().encode(groups)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension EffectiveLengthModel: Validatable {
|
||||
|
||||
var body: some Validation<EffectiveLengthModel> {
|
||||
Validator.accumulating {
|
||||
Validator.validate(\.name, with: .notEmpty())
|
||||
.errorLabel("Name", inline: true)
|
||||
|
||||
Validator.validate(
|
||||
\.straightLengths,
|
||||
with: [Int].empty().or(
|
||||
ForEachValidator {
|
||||
Int.greaterThan(0)
|
||||
})
|
||||
)
|
||||
.errorLabel("Straight Lengths", inline: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension EquivalentLength.FittingGroup: Validatable {
|
||||
|
||||
public var body: some Validation<Self> {
|
||||
Validator.accumulating {
|
||||
Validator.validate(\.group) {
|
||||
Int.greaterThanOrEquals(1)
|
||||
Int.lessThanOrEquals(12)
|
||||
}
|
||||
.errorLabel("Group", inline: true)
|
||||
|
||||
Validator.validate(\.letter, with: .regex(matching: "[a-zA-Z]"))
|
||||
.errorLabel("Letter", inline: true)
|
||||
|
||||
Validator.validate(\.value, with: .greaterThan(0))
|
||||
.errorLabel("Value", inline: true)
|
||||
|
||||
Validator.validate(\.quantity, with: .greaterThanOrEquals(1))
|
||||
.errorLabel("Quantity", inline: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
22
Sources/DatabaseClient/Internal/Migrations.swift
Normal file
22
Sources/DatabaseClient/Internal/Migrations.swift
Normal file
@@ -0,0 +1,22 @@
|
||||
import Dependencies
|
||||
import ManualDCore
|
||||
|
||||
extension DatabaseClient.Migrations: DependencyKey {
|
||||
public static let testValue = Self()
|
||||
|
||||
public static let liveValue = Self(
|
||||
all: {
|
||||
[
|
||||
Project.Migrate(),
|
||||
User.Migrate(),
|
||||
User.Token.Migrate(),
|
||||
User.Profile.Migrate(),
|
||||
ComponentPressureLoss.Migrate(),
|
||||
EquipmentInfo.Migrate(),
|
||||
Room.Migrate(),
|
||||
EquivalentLength.Migrate(),
|
||||
TrunkSize.Migrate(),
|
||||
]
|
||||
}
|
||||
)
|
||||
}
|
||||
10
Sources/DatabaseClient/Internal/Model+validateAndSave.swift
Normal file
10
Sources/DatabaseClient/Internal/Model+validateAndSave.swift
Normal file
@@ -0,0 +1,10 @@
|
||||
import Fluent
|
||||
import Validations
|
||||
|
||||
extension Model where Self: Validations.Validatable {
|
||||
|
||||
func validateAndSave(on database: any Database) async throws {
|
||||
try self.validate()
|
||||
try await self.save(on: database)
|
||||
}
|
||||
}
|
||||
@@ -3,20 +3,7 @@ import DependenciesMacros
|
||||
import Fluent
|
||||
import Foundation
|
||||
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
|
||||
}
|
||||
}
|
||||
import Validations
|
||||
|
||||
extension DatabaseClient.Projects: TestDependencyKey {
|
||||
public static let testValue = Self()
|
||||
@@ -24,8 +11,8 @@ extension DatabaseClient.Projects: TestDependencyKey {
|
||||
public static func live(database: any Database) -> Self {
|
||||
.init(
|
||||
create: { userID, request in
|
||||
let model = try request.toModel(userID: userID)
|
||||
try await model.save(on: database)
|
||||
let model = request.toModel(userID: userID)
|
||||
try await model.validateAndSave(on: database)
|
||||
return try model.toDTO()
|
||||
},
|
||||
delete: { id in
|
||||
@@ -35,28 +22,7 @@ extension DatabaseClient.Projects: TestDependencyKey {
|
||||
try await model.delete(on: database)
|
||||
},
|
||||
detail: { id in
|
||||
guard
|
||||
let model = try await ProjectModel.query(on: database)
|
||||
.with(\.$componentLosses)
|
||||
.with(\.$equipment)
|
||||
.with(\.$equivalentLengths)
|
||||
.with(\.$rooms)
|
||||
.with(
|
||||
\.$trunks,
|
||||
{ trunk in
|
||||
trunk.with(
|
||||
\.$rooms,
|
||||
{
|
||||
$0.with(\.$room)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
.filter(\.$id == id)
|
||||
.first()
|
||||
else {
|
||||
throw NotFoundError()
|
||||
}
|
||||
let model = try await ProjectModel.fetchDetail(for: id, on: database)
|
||||
|
||||
// TODO: Different error ??
|
||||
guard let equipmentInfo = model.equipment else { return nil }
|
||||
@@ -76,44 +42,25 @@ extension DatabaseClient.Projects: TestDependencyKey {
|
||||
try await ProjectModel.find(id, on: database).map { try $0.toDTO() }
|
||||
},
|
||||
getCompletedSteps: { id in
|
||||
let roomsCount = try await RoomModel.query(on: database)
|
||||
.with(\.$project)
|
||||
.filter(\.$project.$id == id)
|
||||
.count()
|
||||
|
||||
let equivalentLengths = try await EffectiveLengthModel.query(on: database)
|
||||
.with(\.$project)
|
||||
.filter(\.$project.$id == id)
|
||||
.all()
|
||||
|
||||
let model = try await ProjectModel.fetchDetail(for: id, on: database)
|
||||
var equivalentLengthsCompleted = false
|
||||
|
||||
if equivalentLengths.filter({ $0.type == "supply" }).first != nil,
|
||||
equivalentLengths.filter({ $0.type == "return" }).first != nil
|
||||
if model.equivalentLengths.filter({ $0.type == "supply" }).first != nil,
|
||||
model.equivalentLengths.filter({ $0.type == "return" }).first != nil
|
||||
{
|
||||
equivalentLengthsCompleted = true
|
||||
}
|
||||
|
||||
let componentLosses = try await ComponentLossModel.query(on: database)
|
||||
.with(\.$project)
|
||||
.filter(\.$project.$id == id)
|
||||
.count()
|
||||
|
||||
let equipmentInfo = try await EquipmentModel.query(on: database)
|
||||
.with(\.$project)
|
||||
.filter(\.$project.$id == id)
|
||||
.first()
|
||||
|
||||
return .init(
|
||||
equipmentInfo: equipmentInfo != nil,
|
||||
rooms: roomsCount > 0,
|
||||
equipmentInfo: model.equipment != nil,
|
||||
rooms: model.rooms.count > 0,
|
||||
equivalentLength: equivalentLengthsCompleted,
|
||||
frictionRate: componentLosses > 0
|
||||
frictionRate: model.componentLosses.count > 0
|
||||
)
|
||||
},
|
||||
getSensibleHeatRatio: { id in
|
||||
guard
|
||||
let shr = try await ProjectModel.query(on: database)
|
||||
let model = try await ProjectModel.query(on: database)
|
||||
.field(\.$id)
|
||||
.field(\.$sensibleHeatRatio)
|
||||
.filter(\.$id == id)
|
||||
@@ -121,7 +68,7 @@ extension DatabaseClient.Projects: TestDependencyKey {
|
||||
else {
|
||||
throw NotFoundError()
|
||||
}
|
||||
return shr.sensibleHeatRatio
|
||||
return model.sensibleHeatRatio
|
||||
},
|
||||
fetch: { userID, request in
|
||||
try await ProjectModel.query(on: database)
|
||||
@@ -135,10 +82,9 @@ extension DatabaseClient.Projects: TestDependencyKey {
|
||||
guard let model = try await ProjectModel.find(id, on: database) else {
|
||||
throw NotFoundError()
|
||||
}
|
||||
try updates.validate()
|
||||
model.applyUpdates(updates)
|
||||
if model.hasChanges {
|
||||
try await model.save(on: database)
|
||||
try await model.validateAndSave(on: database)
|
||||
}
|
||||
return try model.toDTO()
|
||||
}
|
||||
@@ -148,8 +94,7 @@ extension DatabaseClient.Projects: TestDependencyKey {
|
||||
|
||||
extension Project.Create {
|
||||
|
||||
func toModel(userID: User.ID) throws -> ProjectModel {
|
||||
try validate()
|
||||
func toModel(userID: User.ID) -> ProjectModel {
|
||||
return .init(
|
||||
name: name,
|
||||
streetAddress: streetAddress,
|
||||
@@ -160,70 +105,6 @@ extension Project.Create {
|
||||
)
|
||||
}
|
||||
|
||||
func validate() throws(ValidationError) {
|
||||
guard !name.isEmpty else {
|
||||
throw ValidationError("Project name should not be empty.")
|
||||
}
|
||||
guard !streetAddress.isEmpty else {
|
||||
throw ValidationError("Project street address should not be empty.")
|
||||
}
|
||||
guard !city.isEmpty else {
|
||||
throw ValidationError("Project city should not be empty.")
|
||||
}
|
||||
guard !state.isEmpty else {
|
||||
throw ValidationError("Project state should not be empty.")
|
||||
}
|
||||
guard !zipCode.isEmpty else {
|
||||
throw ValidationError("Project zipCode should not be empty.")
|
||||
}
|
||||
if let sensibleHeatRatio {
|
||||
guard sensibleHeatRatio >= 0 else {
|
||||
throw ValidationError("Project sensible heat ratio should be greater than 0.")
|
||||
}
|
||||
guard sensibleHeatRatio <= 1 else {
|
||||
throw ValidationError("Project sensible heat ratio should be less than 1.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Project.Update {
|
||||
|
||||
func validate() throws(ValidationError) {
|
||||
if let name {
|
||||
guard !name.isEmpty else {
|
||||
throw ValidationError("Project name should not be empty.")
|
||||
}
|
||||
}
|
||||
if let streetAddress {
|
||||
guard !streetAddress.isEmpty else {
|
||||
throw ValidationError("Project street address should not be empty.")
|
||||
}
|
||||
}
|
||||
if let city {
|
||||
guard !city.isEmpty else {
|
||||
throw ValidationError("Project city should not be empty.")
|
||||
}
|
||||
}
|
||||
if let state {
|
||||
guard !state.isEmpty else {
|
||||
throw ValidationError("Project state should not be empty.")
|
||||
}
|
||||
}
|
||||
if let zipCode {
|
||||
guard !zipCode.isEmpty else {
|
||||
throw ValidationError("Project zipCode should not be empty.")
|
||||
}
|
||||
}
|
||||
if let sensibleHeatRatio {
|
||||
guard sensibleHeatRatio >= 0 else {
|
||||
throw ValidationError("Project sensible heat ratio should be greater than 0.")
|
||||
}
|
||||
guard sensibleHeatRatio <= 1 else {
|
||||
throw ValidationError("Project sensible heat ratio should be less than 1.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Project {
|
||||
@@ -241,7 +122,7 @@ extension Project {
|
||||
.field("sensibleHeatRatio", .double)
|
||||
.field("createdAt", .datetime)
|
||||
.field("updatedAt", .datetime)
|
||||
.field("userID", .uuid, .required, .references(UserModel.schema, "id"))
|
||||
.field("userID", .uuid, .required, .references(UserModel.schema, "id", onDelete: .cascade))
|
||||
.unique(on: "userID", "name")
|
||||
.create()
|
||||
}
|
||||
@@ -364,4 +245,67 @@ final class ProjectModel: Model, @unchecked Sendable {
|
||||
self.sensibleHeatRatio = sensibleHeatRatio
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a ``ProjectModel`` with all the relations eagerly loaded.
|
||||
static func fetchDetail(
|
||||
for projectID: Project.ID,
|
||||
on database: any Database
|
||||
) async throws -> ProjectModel {
|
||||
guard
|
||||
let model =
|
||||
try await ProjectModel.query(on: database)
|
||||
.with(\.$componentLosses)
|
||||
.with(\.$equipment)
|
||||
.with(\.$equivalentLengths)
|
||||
.with(\.$rooms)
|
||||
.with(
|
||||
\.$trunks,
|
||||
{ trunk in
|
||||
trunk.with(
|
||||
\.$rooms,
|
||||
{
|
||||
$0.with(\.$room)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
.filter(\.$id == projectID)
|
||||
.first()
|
||||
else {
|
||||
throw NotFoundError()
|
||||
}
|
||||
return model
|
||||
}
|
||||
}
|
||||
|
||||
extension ProjectModel: Validatable {
|
||||
|
||||
var body: some Validation<ProjectModel> {
|
||||
Validator.accumulating {
|
||||
Validator.validate(\.name, with: .notEmpty())
|
||||
.errorLabel("Name", inline: true)
|
||||
|
||||
Validator.validate(\.streetAddress, with: .notEmpty())
|
||||
.errorLabel("Address", inline: true)
|
||||
|
||||
Validator.validate(\.city, with: .notEmpty())
|
||||
.errorLabel("City", inline: true)
|
||||
|
||||
Validator.validate(\.state, with: .notEmpty())
|
||||
.errorLabel("State", inline: true)
|
||||
|
||||
Validator.validate(\.zipCode, with: .notEmpty())
|
||||
.errorLabel("Zip", inline: true)
|
||||
|
||||
Validator.validate(\.sensibleHeatRatio) {
|
||||
Validator {
|
||||
Double.greaterThan(0)
|
||||
Double.lessThanOrEquals(1.0)
|
||||
}
|
||||
.optional()
|
||||
}
|
||||
.errorLabel("Sensible Heat Ratio", inline: true)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,31 +3,25 @@ import DependenciesMacros
|
||||
import Fluent
|
||||
import Foundation
|
||||
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
|
||||
}
|
||||
}
|
||||
import Validations
|
||||
|
||||
extension DatabaseClient.Rooms: TestDependencyKey {
|
||||
public static let testValue = Self()
|
||||
|
||||
public static func live(database: any Database) -> Self {
|
||||
.init(
|
||||
create: { request in
|
||||
let model = try request.toModel()
|
||||
try await model.save(on: database)
|
||||
create: { projectID, request in
|
||||
let model = try request.toModel(projectID: projectID)
|
||||
try await model.validateAndSave(on: database)
|
||||
return try model.toDTO()
|
||||
},
|
||||
createMany: { projectID, rooms in
|
||||
try await rooms.asyncMap { request in
|
||||
let model = try request.toModel(projectID: projectID)
|
||||
try await model.validateAndSave(on: database)
|
||||
return try model.toDTO()
|
||||
}
|
||||
},
|
||||
delete: { id in
|
||||
guard let model = try await RoomModel.find(id, on: database) else {
|
||||
throw NotFoundError()
|
||||
@@ -41,8 +35,11 @@ extension DatabaseClient.Rooms: TestDependencyKey {
|
||||
model.rectangularSizes?.removeAll {
|
||||
$0.id == rectangularDuctID
|
||||
}
|
||||
if model.rectangularSizes?.count == 0 {
|
||||
model.rectangularSizes = nil
|
||||
}
|
||||
if model.hasChanges {
|
||||
try await model.save(on: database)
|
||||
try await model.validateAndSave(on: database)
|
||||
}
|
||||
return try model.toDTO()
|
||||
},
|
||||
@@ -61,11 +58,9 @@ extension DatabaseClient.Rooms: TestDependencyKey {
|
||||
guard let model = try await RoomModel.find(id, on: database) else {
|
||||
throw NotFoundError()
|
||||
}
|
||||
|
||||
try updates.validate()
|
||||
model.applyUpdates(updates)
|
||||
if model.hasChanges {
|
||||
try await model.save(on: database)
|
||||
try await model.validateAndSave(on: database)
|
||||
}
|
||||
return try model.toDTO()
|
||||
},
|
||||
@@ -88,68 +83,15 @@ extension DatabaseClient.Rooms: TestDependencyKey {
|
||||
|
||||
extension Room.Create {
|
||||
|
||||
func toModel() throws(ValidationError) -> RoomModel {
|
||||
try validate()
|
||||
func toModel(projectID: Project.ID) throws -> RoomModel {
|
||||
return .init(
|
||||
name: name,
|
||||
heatingLoad: heatingLoad,
|
||||
coolingTotal: coolingTotal,
|
||||
coolingSensible: coolingSensible,
|
||||
coolingLoad: coolingLoad,
|
||||
registerCount: registerCount,
|
||||
projectID: projectID
|
||||
)
|
||||
}
|
||||
|
||||
func validate() throws(ValidationError) {
|
||||
guard !name.isEmpty else {
|
||||
throw ValidationError("Room name should not be empty.")
|
||||
}
|
||||
guard heatingLoad >= 0 else {
|
||||
throw ValidationError("Room heating load should not be less than 0.")
|
||||
}
|
||||
guard coolingTotal >= 0 else {
|
||||
throw ValidationError("Room cooling total should not be less than 0.")
|
||||
}
|
||||
if let coolingSensible {
|
||||
guard coolingSensible >= 0 else {
|
||||
throw ValidationError("Room cooling sensible should not be less than 0.")
|
||||
}
|
||||
}
|
||||
guard registerCount >= 1 else {
|
||||
throw ValidationError("Room cooling sensible should not be less than 1.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Room.Update {
|
||||
|
||||
func validate() throws(ValidationError) {
|
||||
if let name {
|
||||
guard !name.isEmpty else {
|
||||
throw ValidationError("Room name should not be empty.")
|
||||
}
|
||||
}
|
||||
if let heatingLoad {
|
||||
guard heatingLoad >= 0 else {
|
||||
throw ValidationError("Room heating load should not be less than 0.")
|
||||
}
|
||||
}
|
||||
if let coolingTotal {
|
||||
guard coolingTotal >= 0 else {
|
||||
throw ValidationError("Room cooling total should not be less than 0.")
|
||||
}
|
||||
}
|
||||
if let coolingSensible {
|
||||
guard coolingSensible >= 0 else {
|
||||
throw ValidationError("Room cooling sensible should not be less than 0.")
|
||||
}
|
||||
}
|
||||
if let registerCount {
|
||||
guard registerCount >= 1 else {
|
||||
throw ValidationError("Room cooling sensible should not be less than 1.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Room {
|
||||
@@ -161,8 +103,7 @@ extension Room {
|
||||
.id()
|
||||
.field("name", .string, .required)
|
||||
.field("heatingLoad", .double, .required)
|
||||
.field("coolingTotal", .double, .required)
|
||||
.field("coolingSensible", .double)
|
||||
.field("coolingLoad", .dictionary, .required)
|
||||
.field("registerCount", .int8, .required)
|
||||
.field("rectangularSizes", .array)
|
||||
.field("createdAt", .datetime)
|
||||
@@ -180,7 +121,7 @@ extension Room {
|
||||
}
|
||||
}
|
||||
|
||||
final class RoomModel: Model, @unchecked Sendable {
|
||||
final class RoomModel: Model, @unchecked Sendable, Validatable {
|
||||
|
||||
static let schema = "room"
|
||||
|
||||
@@ -193,11 +134,8 @@ final class RoomModel: Model, @unchecked Sendable {
|
||||
@Field(key: "heatingLoad")
|
||||
var heatingLoad: Double
|
||||
|
||||
@Field(key: "coolingTotal")
|
||||
var coolingTotal: Double
|
||||
|
||||
@Field(key: "coolingSensible")
|
||||
var coolingSensible: Double?
|
||||
@Field(key: "coolingLoad")
|
||||
var coolingLoad: Room.CoolingLoad
|
||||
|
||||
@Field(key: "registerCount")
|
||||
var registerCount: Int
|
||||
@@ -220,8 +158,7 @@ final class RoomModel: Model, @unchecked Sendable {
|
||||
id: UUID? = nil,
|
||||
name: String,
|
||||
heatingLoad: Double,
|
||||
coolingTotal: Double,
|
||||
coolingSensible: Double? = nil,
|
||||
coolingLoad: Room.CoolingLoad,
|
||||
registerCount: Int,
|
||||
rectangularSizes: [Room.RectangularSize]? = nil,
|
||||
createdAt: Date? = nil,
|
||||
@@ -231,8 +168,7 @@ final class RoomModel: Model, @unchecked Sendable {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.heatingLoad = heatingLoad
|
||||
self.coolingTotal = coolingTotal
|
||||
self.coolingSensible = coolingSensible
|
||||
self.coolingLoad = coolingLoad
|
||||
self.registerCount = registerCount
|
||||
self.rectangularSizes = rectangularSizes
|
||||
self.createdAt = createdAt
|
||||
@@ -246,8 +182,7 @@ final class RoomModel: Model, @unchecked Sendable {
|
||||
projectID: $project.id,
|
||||
name: name,
|
||||
heatingLoad: heatingLoad,
|
||||
coolingTotal: coolingTotal,
|
||||
coolingSensible: coolingSensible,
|
||||
coolingLoad: coolingLoad,
|
||||
registerCount: registerCount,
|
||||
rectangularSizes: rectangularSizes,
|
||||
createdAt: createdAt!,
|
||||
@@ -263,11 +198,8 @@ final class RoomModel: Model, @unchecked Sendable {
|
||||
if let heatingLoad = updates.heatingLoad, heatingLoad != self.heatingLoad {
|
||||
self.heatingLoad = heatingLoad
|
||||
}
|
||||
if let coolingTotal = updates.coolingTotal, coolingTotal != self.coolingTotal {
|
||||
self.coolingTotal = coolingTotal
|
||||
}
|
||||
if let coolingSensible = updates.coolingSensible, coolingSensible != self.coolingSensible {
|
||||
self.coolingSensible = coolingSensible
|
||||
if let coolingLoad = updates.coolingLoad, coolingLoad != self.coolingLoad {
|
||||
self.coolingLoad = coolingLoad
|
||||
}
|
||||
if let registerCount = updates.registerCount, registerCount != self.registerCount {
|
||||
self.registerCount = registerCount
|
||||
@@ -278,4 +210,54 @@ final class RoomModel: Model, @unchecked Sendable {
|
||||
|
||||
}
|
||||
|
||||
var body: some Validation<RoomModel> {
|
||||
Validator.accumulating {
|
||||
Validator.validate(\.name, with: .notEmpty())
|
||||
.errorLabel("Name", inline: true)
|
||||
|
||||
Validator.validate(\.heatingLoad, with: .greaterThanOrEquals(0))
|
||||
.errorLabel("Heating Load", inline: true)
|
||||
|
||||
Validator.validate(\.coolingLoad)
|
||||
.errorLabel("Cooling Load", inline: true)
|
||||
|
||||
Validator.validate(\.registerCount, with: .greaterThanOrEquals(1))
|
||||
.errorLabel("Register Count", inline: true)
|
||||
|
||||
Validator.validate(\.rectangularSizes)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Room.CoolingLoad: Validatable {
|
||||
|
||||
public var body: some Validation<Self> {
|
||||
Validator.accumulating {
|
||||
// Ensure that at least one of the values is not nil.
|
||||
Validator.oneOf {
|
||||
Validator.validate(\.total, with: .notNil())
|
||||
.errorLabel("Total or Sensible", inline: true)
|
||||
Validator.validate(\.sensible, with: .notNil())
|
||||
.errorLabel("Total or Sensible", inline: true)
|
||||
}
|
||||
|
||||
Validator.validate(\.total, with: Double.greaterThan(0).optional())
|
||||
Validator.validate(\.sensible, with: Double.greaterThan(0).optional())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Room.RectangularSize: Validatable {
|
||||
|
||||
public var body: some Validation<Self> {
|
||||
Validator.accumulating {
|
||||
Validator.validate(\.register, with: Int.greaterThanOrEquals(1).optional())
|
||||
.errorLabel("Register", inline: true)
|
||||
|
||||
Validator.validate(\.height, with: Int.greaterThanOrEquals(1))
|
||||
.errorLabel("Height", inline: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
41
Sources/DatabaseClient/Internal/Sequence+asyncMap.swift
Normal file
41
Sources/DatabaseClient/Internal/Sequence+asyncMap.swift
Normal file
@@ -0,0 +1,41 @@
|
||||
import Foundation
|
||||
|
||||
extension Sequence {
|
||||
|
||||
// Taken from: https://forums.swift.org/t/are-there-any-kind-of-asyncmap-method-for-a-normal-sequence/77354/7
|
||||
func asyncMap<Result: Sendable>(
|
||||
_ transform: @escaping @Sendable (Element) async throws -> Result
|
||||
) async rethrows -> [Result] where Element: Sendable {
|
||||
try await withThrowingTaskGroup(of: (Int, Result).self) { group in
|
||||
|
||||
var i = 0
|
||||
var iterator = self.makeIterator()
|
||||
var results = [Result?]()
|
||||
results.reserveCapacity(underestimatedCount)
|
||||
|
||||
func submitTask() throws {
|
||||
try Task.checkCancellation()
|
||||
if let element = iterator.next() {
|
||||
results.append(nil)
|
||||
group.addTask { [i] in try await (i, transform(element)) }
|
||||
i += 1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Add initial tasks
|
||||
for _ in 0..<ProcessInfo.processInfo.processorCount {
|
||||
try submitTask()
|
||||
}
|
||||
|
||||
// Submit more tasks as results complete
|
||||
while let (index, result) = try await group.next() {
|
||||
results[index] = result
|
||||
try submitTask()
|
||||
}
|
||||
|
||||
return results.compactMap { $0 }
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,19 +3,7 @@ import DependenciesMacros
|
||||
import Fluent
|
||||
import Foundation
|
||||
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
|
||||
}
|
||||
}
|
||||
import Validations
|
||||
|
||||
extension DatabaseClient.TrunkSizes: TestDependencyKey {
|
||||
public static let testValue = Self()
|
||||
@@ -23,12 +11,12 @@ extension DatabaseClient.TrunkSizes: TestDependencyKey {
|
||||
public static func live(database: any Database) -> Self {
|
||||
.init(
|
||||
create: { request in
|
||||
try request.validate()
|
||||
// try request.validate()
|
||||
|
||||
let trunk = request.toModel()
|
||||
var roomProxies = [TrunkSize.RoomProxy]()
|
||||
|
||||
try await trunk.save(on: database)
|
||||
try await trunk.validateAndSave(on: database)
|
||||
|
||||
for (roomID, registers) in request.rooms {
|
||||
guard let room = try await RoomModel.find(roomID, on: database) else {
|
||||
@@ -40,7 +28,7 @@ extension DatabaseClient.TrunkSizes: TestDependencyKey {
|
||||
registers: registers,
|
||||
type: request.type
|
||||
)
|
||||
try await model.save(on: database)
|
||||
try await model.validateAndSave(on: database)
|
||||
roomProxies.append(
|
||||
.init(room: try room.toDTO(), registers: registers)
|
||||
)
|
||||
@@ -50,7 +38,9 @@ extension DatabaseClient.TrunkSizes: TestDependencyKey {
|
||||
id: trunk.requireID(),
|
||||
projectID: trunk.$project.id,
|
||||
type: .init(rawValue: trunk.type)!,
|
||||
rooms: roomProxies
|
||||
rooms: roomProxies,
|
||||
height: trunk.height,
|
||||
name: trunk.name
|
||||
)
|
||||
},
|
||||
delete: { id in
|
||||
@@ -91,7 +81,7 @@ extension DatabaseClient.TrunkSizes: TestDependencyKey {
|
||||
else {
|
||||
throw NotFoundError()
|
||||
}
|
||||
try updates.validate()
|
||||
// try updates.validate()
|
||||
try await model.applyUpdates(updates, on: database)
|
||||
return try model.toDTO()
|
||||
}
|
||||
@@ -101,17 +91,6 @@ extension DatabaseClient.TrunkSizes: TestDependencyKey {
|
||||
|
||||
extension TrunkSize.Create {
|
||||
|
||||
func validate() throws(ValidationError) {
|
||||
guard rooms.count > 0 else {
|
||||
throw ValidationError("Trunk size should have associated rooms / registers.")
|
||||
}
|
||||
if let height {
|
||||
guard height > 0 else {
|
||||
throw ValidationError("Trunk size height should be greater than 0.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toModel() -> TrunkModel {
|
||||
.init(
|
||||
projectID: projectID,
|
||||
@@ -122,21 +101,6 @@ extension TrunkSize.Create {
|
||||
}
|
||||
}
|
||||
|
||||
extension TrunkSize.Update {
|
||||
func validate() throws(ValidationError) {
|
||||
if let rooms {
|
||||
guard rooms.count > 0 else {
|
||||
throw ValidationError("Trunk size should have associated rooms / registers.")
|
||||
}
|
||||
}
|
||||
if let height {
|
||||
guard height > 0 else {
|
||||
throw ValidationError("Trunk size height should be greater than 0.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TrunkSize {
|
||||
|
||||
struct Migrate: AsyncMigration {
|
||||
@@ -211,15 +175,22 @@ final class TrunkRoomModel: Model, @unchecked Sendable {
|
||||
}
|
||||
|
||||
func toDTO() throws -> TrunkSize.RoomProxy {
|
||||
// guard let room = try await RoomModel.find($room.id, on: database) else {
|
||||
// throw NotFoundError()
|
||||
// }
|
||||
return .init(
|
||||
room: try room.toDTO(),
|
||||
registers: registers
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension TrunkRoomModel: Validatable {
|
||||
var body: some Validation<TrunkRoomModel> {
|
||||
Validator.validate(\.registers) {
|
||||
[Int].notEmpty()
|
||||
ForEachValidator {
|
||||
Int.greaterThanOrEquals(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class TrunkModel: Model, @unchecked Sendable {
|
||||
@@ -261,19 +232,6 @@ final class TrunkModel: Model, @unchecked Sendable {
|
||||
}
|
||||
|
||||
func toDTO() throws -> TrunkSize {
|
||||
// let rooms = try await withThrowingTaskGroup(of: TrunkSize.RoomProxy.self) { group in
|
||||
// for room in self.rooms {
|
||||
// group.addTask {
|
||||
// try await room.toDTO(on: database)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return try await group.reduce(into: [TrunkSize.RoomProxy]()) {
|
||||
// $0.append($1)
|
||||
// }
|
||||
//
|
||||
// }
|
||||
|
||||
let rooms = try rooms.reduce(into: [TrunkSize.RoomProxy]()) {
|
||||
$0.append(try $1.toDTO())
|
||||
}
|
||||
@@ -303,7 +261,7 @@ final class TrunkModel: Model, @unchecked Sendable {
|
||||
self.name = name
|
||||
}
|
||||
if hasChanges {
|
||||
try await self.save(on: database)
|
||||
try await self.validateAndSave(on: database)
|
||||
}
|
||||
|
||||
guard let updateRooms = updates.rooms else {
|
||||
@@ -324,7 +282,7 @@ final class TrunkModel: Model, @unchecked Sendable {
|
||||
currRoom.registers = registers
|
||||
}
|
||||
if currRoom.hasChanges {
|
||||
try await currRoom.save(on: database)
|
||||
try await currRoom.validateAndSave(on: database)
|
||||
}
|
||||
} else {
|
||||
database.logger.debug("CREATING NEW TrunkRoomModel")
|
||||
@@ -351,19 +309,27 @@ final class TrunkModel: Model, @unchecked Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
extension TrunkModel: Validatable {
|
||||
|
||||
var body: some Validation<TrunkModel> {
|
||||
Validator.accumulating {
|
||||
|
||||
Validator.validate(\.height, with: Int.greaterThan(0).optional())
|
||||
.errorLabel("Height", inline: true)
|
||||
|
||||
Validator.validate(\.name, with: String.notEmpty().optional())
|
||||
.errorLabel("Name", inline: true)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == TrunkModel {
|
||||
|
||||
func toDTO() throws -> [TrunkSize] {
|
||||
// try await withThrowingTaskGroup(of: TrunkSize.self) { group in
|
||||
// for model in self {
|
||||
// group.addTask {
|
||||
// try await model.toDTO(on: database)
|
||||
// }
|
||||
// }
|
||||
|
||||
return try reduce(into: [TrunkSize]()) {
|
||||
$0.append(try $1.toDTO())
|
||||
}
|
||||
}
|
||||
// }
|
||||
}
|
||||
20
Sources/DatabaseClient/Internal/User+validation.swift
Normal file
20
Sources/DatabaseClient/Internal/User+validation.swift
Normal file
@@ -0,0 +1,20 @@
|
||||
import ManualDCore
|
||||
import Validations
|
||||
|
||||
// Declaring this in seperate file because some Vapor imports
|
||||
// have same name's and this was easiest solution.
|
||||
extension User.Create: Validatable {
|
||||
public var body: some Validation<Self> {
|
||||
Validator.accumulating {
|
||||
Validator.validate(\.email, with: .email())
|
||||
.errorLabel("Email", inline: true)
|
||||
|
||||
Validator.validate(\.password.count, with: .greaterThanOrEquals(8))
|
||||
.errorLabel("Password Count", inline: true)
|
||||
|
||||
Validator.validate(\.confirmPassword, with: .equals(password))
|
||||
.mapError(ValidationError("Confirm password does not match."))
|
||||
.errorLabel("Confirm Password", inline: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,19 @@
|
||||
import Dependencies
|
||||
import DependenciesMacros
|
||||
import Fluent
|
||||
import Foundation
|
||||
import ManualDCore
|
||||
import Vapor
|
||||
import Validations
|
||||
|
||||
extension DatabaseClient {
|
||||
@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 {
|
||||
extension DatabaseClient.UserProfiles: TestDependencyKey {
|
||||
|
||||
public static let testValue = Self()
|
||||
|
||||
public static func live(database: any Database) -> Self {
|
||||
.init(
|
||||
create: { profile in
|
||||
try profile.validate()
|
||||
let model = profile.toModel()
|
||||
try await model.save(on: database)
|
||||
try await model.validateAndSave(on: database)
|
||||
return try model.toDTO()
|
||||
},
|
||||
delete: { id in
|
||||
@@ -48,10 +37,9 @@ extension DatabaseClient.UserProfile: TestDependencyKey {
|
||||
guard let model = try await UserProfileModel.find(id, on: database) else {
|
||||
throw NotFoundError()
|
||||
}
|
||||
try updates.validate()
|
||||
model.applyUpdates(updates)
|
||||
if model.hasChanges {
|
||||
try await model.save(on: database)
|
||||
try await model.validateAndSave(on: database)
|
||||
}
|
||||
return try model.toDTO()
|
||||
}
|
||||
@@ -61,30 +49,6 @@ extension DatabaseClient.UserProfile: TestDependencyKey {
|
||||
|
||||
extension User.Profile.Create {
|
||||
|
||||
func validate() throws(ValidationError) {
|
||||
guard !firstName.isEmpty else {
|
||||
throw ValidationError("User first name should not be empty.")
|
||||
}
|
||||
guard !lastName.isEmpty else {
|
||||
throw ValidationError("User last name should not be empty.")
|
||||
}
|
||||
guard !companyName.isEmpty else {
|
||||
throw ValidationError("User company name should not be empty.")
|
||||
}
|
||||
guard !streetAddress.isEmpty else {
|
||||
throw ValidationError("User street address should not be empty.")
|
||||
}
|
||||
guard !city.isEmpty else {
|
||||
throw ValidationError("User city should not be empty.")
|
||||
}
|
||||
guard !state.isEmpty else {
|
||||
throw ValidationError("User state should not be empty.")
|
||||
}
|
||||
guard !zipCode.isEmpty else {
|
||||
throw ValidationError("User zip code should not be empty.")
|
||||
}
|
||||
}
|
||||
|
||||
func toModel() -> UserProfileModel {
|
||||
.init(
|
||||
userID: userID,
|
||||
@@ -100,47 +64,6 @@ extension User.Profile.Create {
|
||||
}
|
||||
}
|
||||
|
||||
extension User.Profile.Update {
|
||||
|
||||
func validate() throws(ValidationError) {
|
||||
if let firstName {
|
||||
guard !firstName.isEmpty else {
|
||||
throw ValidationError("User first name should not be empty.")
|
||||
}
|
||||
}
|
||||
if let lastName {
|
||||
guard !lastName.isEmpty else {
|
||||
throw ValidationError("User last name should not be empty.")
|
||||
}
|
||||
}
|
||||
if let companyName {
|
||||
guard !companyName.isEmpty else {
|
||||
throw ValidationError("User company name should not be empty.")
|
||||
}
|
||||
}
|
||||
if let streetAddress {
|
||||
guard !streetAddress.isEmpty else {
|
||||
throw ValidationError("User street address should not be empty.")
|
||||
}
|
||||
}
|
||||
if let city {
|
||||
guard !city.isEmpty else {
|
||||
throw ValidationError("User city should not be empty.")
|
||||
}
|
||||
}
|
||||
if let state {
|
||||
guard !state.isEmpty else {
|
||||
throw ValidationError("User state should not be empty.")
|
||||
}
|
||||
}
|
||||
if let zipCode {
|
||||
guard !zipCode.isEmpty else {
|
||||
throw ValidationError("User zip code should not be empty.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension User.Profile {
|
||||
|
||||
struct Migrate: AsyncMigration {
|
||||
@@ -281,3 +204,31 @@ final class UserProfileModel: Model, @unchecked Sendable {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension UserProfileModel: Validatable {
|
||||
|
||||
var body: some Validation<UserProfileModel> {
|
||||
Validator.accumulating {
|
||||
Validator.validate(\.firstName, with: .notEmpty())
|
||||
.errorLabel("First Name", inline: true)
|
||||
|
||||
Validator.validate(\.lastName, with: .notEmpty())
|
||||
.errorLabel("Last Name", inline: true)
|
||||
|
||||
Validator.validate(\.companyName, with: .notEmpty())
|
||||
.errorLabel("Company", inline: true)
|
||||
|
||||
Validator.validate(\.streetAddress, with: .notEmpty())
|
||||
.errorLabel("Address", inline: true)
|
||||
|
||||
Validator.validate(\.city, with: .notEmpty())
|
||||
.errorLabel("City", inline: true)
|
||||
|
||||
Validator.validate(\.state, with: .notEmpty())
|
||||
.errorLabel("State", inline: true)
|
||||
|
||||
Validator.validate(\.zipCode, with: .notEmpty())
|
||||
.errorLabel("Zip", inline: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,17 @@
|
||||
import Dependencies
|
||||
import DependenciesMacros
|
||||
import Fluent
|
||||
import Foundation
|
||||
import ManualDCore
|
||||
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 {
|
||||
public static let testValue = Self()
|
||||
|
||||
public static func live(database: any Database) -> Self {
|
||||
.init(
|
||||
create: { request in
|
||||
try request.validate()
|
||||
let model = try request.toModel()
|
||||
try await model.save(on: database)
|
||||
return try model.toDTO()
|
||||
@@ -46,6 +35,11 @@ extension DatabaseClient.Users: TestDependencyKey {
|
||||
throw NotFoundError()
|
||||
}
|
||||
|
||||
// Verify the password matches the user's hashed password.
|
||||
guard try user.verifyPassword(request.password) else {
|
||||
throw Abort(.unauthorized)
|
||||
}
|
||||
|
||||
let token: User.Token
|
||||
|
||||
// Check if there's a user token
|
||||
@@ -126,21 +120,8 @@ extension User {
|
||||
extension User.Create {
|
||||
|
||||
func toModel() throws -> UserModel {
|
||||
try validate()
|
||||
return try .init(email: email, passwordHash: User.hashPassword(password))
|
||||
}
|
||||
|
||||
func validate() throws {
|
||||
guard !email.isEmpty else {
|
||||
throw ValidationError("Email should not be empty")
|
||||
}
|
||||
guard password.count > 8 else {
|
||||
throw ValidationError("Password should be more than 8 characters long.")
|
||||
}
|
||||
guard password == confirmPassword else {
|
||||
throw ValidationError("Passwords do not match.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class UserModel: Model, @unchecked Sendable {
|
||||
@@ -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
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@@ -1,74 +0,0 @@
|
||||
import Dependencies
|
||||
import DependenciesMacros
|
||||
import Foundation
|
||||
|
||||
extension DependencyValues {
|
||||
|
||||
/// Holds values defined in the process environment that are needed.
|
||||
///
|
||||
/// These are generally loaded from a `.env` file, but also have default values,
|
||||
/// if not found.
|
||||
public var env: @Sendable () throws -> EnvVars {
|
||||
get { self[EnvClient.self].env }
|
||||
set { self[EnvClient.self].env = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
@DependencyClient
|
||||
struct EnvClient: Sendable {
|
||||
public var env: @Sendable () throws -> EnvVars
|
||||
}
|
||||
|
||||
/// Holds values defined in the process environment that are needed.
|
||||
///
|
||||
/// These are generally loaded from a `.env` file, but also have default values,
|
||||
/// if not found.
|
||||
public struct EnvVars: Codable, Equatable, Sendable {
|
||||
|
||||
/// The path to the pandoc executable on the system, used to generate pdf's.
|
||||
public let pandocPath: String
|
||||
|
||||
/// The pdf engine to use with pandoc when creating pdf's.
|
||||
public let pdfEngine: String
|
||||
|
||||
public init(
|
||||
pandocPath: String = "/usr/bin/pandoc",
|
||||
pdfEngine: String = "weasyprint"
|
||||
) {
|
||||
self.pandocPath = pandocPath
|
||||
self.pdfEngine = pdfEngine
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case pandocPath = "PANDOC_PATH"
|
||||
case pdfEngine = "PDF_ENGINE"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension EnvClient: DependencyKey {
|
||||
static let testValue = Self()
|
||||
|
||||
static let liveValue = Self(env: {
|
||||
// Convert default values into a dictionary.
|
||||
let defaults =
|
||||
(try? encoder.encode(EnvVars()))
|
||||
.flatMap { try? decoder.decode([String: String].self, from: $0) }
|
||||
?? [:]
|
||||
|
||||
// Merge the default values with values found in process environment.
|
||||
let assigned = defaults.merging(ProcessInfo.processInfo.environment, uniquingKeysWith: { $1 })
|
||||
|
||||
return (try? JSONSerialization.data(withJSONObject: assigned))
|
||||
.flatMap { try? decoder.decode(EnvVars.self, from: $0) }
|
||||
?? .init()
|
||||
})
|
||||
}
|
||||
|
||||
private let encoder: JSONEncoder = {
|
||||
JSONEncoder()
|
||||
}()
|
||||
|
||||
private let decoder: JSONDecoder = {
|
||||
JSONDecoder()
|
||||
}()
|
||||
100
Sources/EnvVars/Interface.swift
Normal file
100
Sources/EnvVars/Interface.swift
Normal file
@@ -0,0 +1,100 @@
|
||||
import Dependencies
|
||||
import DependenciesMacros
|
||||
import Foundation
|
||||
|
||||
extension DependencyValues {
|
||||
|
||||
/// Holds values defined in the process environment that are needed.
|
||||
///
|
||||
/// These are generally loaded from a `.env` file, but also have default values,
|
||||
/// if not found.
|
||||
public var environment: EnvVars {
|
||||
get { self[EnvVars.self] }
|
||||
set { self[EnvVars.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds values defined in the process environment that are needed.
|
||||
///
|
||||
/// These are generally loaded from a `.env` file, but also have default values,
|
||||
/// if not found.
|
||||
public struct EnvVars: Codable, Equatable, Sendable {
|
||||
|
||||
/// The path to the pandoc executable on the system, used to generate pdf's.
|
||||
public let pandocPath: String
|
||||
|
||||
/// The pdf engine to use with pandoc when creating pdf's.
|
||||
public let pdfEngine: String
|
||||
|
||||
/// The postgres hostname, used for production database connection.
|
||||
public let postgresHostname: String?
|
||||
|
||||
/// The postgres username, used for production database connection.
|
||||
public let postgresUsername: String?
|
||||
|
||||
/// The postgres password, used for production database connection.
|
||||
public let postgresPassword: String?
|
||||
|
||||
/// The postgres database, used for production database connection.
|
||||
public let postgresDatabase: String?
|
||||
|
||||
/// The path to the sqlite database, used for development database connection.
|
||||
public let sqlitePath: String?
|
||||
|
||||
public init(
|
||||
pandocPath: String = "/usr/bin/pandoc",
|
||||
pdfEngine: String = "weasyprint",
|
||||
postgresHostname: String? = "localhost",
|
||||
postgresUsername: String? = "vapor",
|
||||
postgresPassword: String? = "super-secret",
|
||||
postgresDatabase: String? = "vapor",
|
||||
sqlitePath: String? = "db.sqlite"
|
||||
) {
|
||||
self.pandocPath = pandocPath
|
||||
self.pdfEngine = pdfEngine
|
||||
self.postgresHostname = postgresHostname
|
||||
self.postgresUsername = postgresUsername
|
||||
self.postgresPassword = postgresPassword
|
||||
self.postgresDatabase = postgresDatabase
|
||||
self.sqlitePath = sqlitePath
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case pandocPath = "PANDOC_PATH"
|
||||
case pdfEngine = "PDF_ENGINE"
|
||||
case postgresHostname = "POSTGRES_HOSTNAME"
|
||||
case postgresUsername = "POSTGRES_USERNAME"
|
||||
case postgresPassword = "POSTGRES_PASSWORD"
|
||||
case postgresDatabase = "POSTGRES_DATABASE"
|
||||
case sqlitePath = "SQLITE_PATH"
|
||||
}
|
||||
|
||||
public static func live(_ env: [String: String] = ProcessInfo.processInfo.environment) -> Self {
|
||||
|
||||
// Convert default values into a dictionary.
|
||||
let defaults =
|
||||
(try? encoder.encode(EnvVars()))
|
||||
.flatMap { try? decoder.decode([String: String].self, from: $0) }
|
||||
?? [:]
|
||||
|
||||
// Merge the default values with values found in process environment.
|
||||
let assigned = defaults.merging(env, uniquingKeysWith: { $1 })
|
||||
|
||||
return (try? JSONSerialization.data(withJSONObject: assigned))
|
||||
.flatMap { try? decoder.decode(EnvVars.self, from: $0) }
|
||||
?? .init()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension EnvVars: TestDependencyKey {
|
||||
public static let testValue = Self()
|
||||
}
|
||||
|
||||
private let encoder: JSONEncoder = {
|
||||
JSONEncoder()
|
||||
}()
|
||||
|
||||
private let decoder: JSONDecoder = {
|
||||
JSONDecoder()
|
||||
}()
|
||||
@@ -4,6 +4,7 @@ import Foundation
|
||||
import Vapor
|
||||
|
||||
extension DependencyValues {
|
||||
/// Dependency used for file operations.
|
||||
public var fileClient: FileClient {
|
||||
get { self[FileClient.self] }
|
||||
set { self[FileClient.self] = newValue }
|
||||
@@ -14,9 +15,27 @@ extension DependencyValues {
|
||||
public struct FileClient: Sendable {
|
||||
public typealias OnCompleteHandler = @Sendable () async throws -> Void
|
||||
|
||||
public var writeFile: @Sendable (String, String) async throws -> Void
|
||||
public var removeFile: @Sendable (String) async throws -> Void
|
||||
public var streamFile: @Sendable (String, @escaping OnCompleteHandler) async throws -> Response
|
||||
/// Write contents to a file.
|
||||
///
|
||||
/// > Warning: This will overwrite a file if it exists.
|
||||
public var writeFile: @Sendable (_ contents: String, _ path: String) async throws -> Void
|
||||
/// Remove a file.
|
||||
public var removeFile: @Sendable (_ path: String) async throws -> Void
|
||||
/// Stream a file.
|
||||
public var streamFile:
|
||||
@Sendable (_ path: String, @escaping OnCompleteHandler) async throws -> Response
|
||||
|
||||
/// Stream a file at the given path.
|
||||
///
|
||||
/// - Paramters:
|
||||
/// - path: The path to the file to stream.
|
||||
/// - onComplete: Completion handler to run when done streaming the file.
|
||||
public func streamFile(
|
||||
at path: String,
|
||||
onComplete: @escaping OnCompleteHandler = {}
|
||||
) async throws -> Response {
|
||||
try await streamFile(path, onComplete)
|
||||
}
|
||||
}
|
||||
|
||||
extension FileClient: TestDependencyKey {
|
||||
|
||||
@@ -11,12 +11,12 @@ extension Snapshotting where Value == (any HTML), Format == String {
|
||||
}
|
||||
}
|
||||
|
||||
extension Snapshotting where Value == String, Format == String {
|
||||
public static var html: Snapshotting {
|
||||
var snapshotting = SimplySnapshotting.lines
|
||||
.pullback { $0 }
|
||||
|
||||
snapshotting.pathExtension = "html"
|
||||
return snapshotting
|
||||
}
|
||||
}
|
||||
// extension Snapshotting where Value == String, Format == String {
|
||||
// public static var html: Snapshotting {
|
||||
// var snapshotting = SimplySnapshotting.lines
|
||||
// .pullback { $0 }
|
||||
//
|
||||
// snapshotting.pathExtension = "html"
|
||||
// return snapshotting
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -3,13 +3,12 @@ import ManualDCore
|
||||
|
||||
extension Room {
|
||||
|
||||
var heatingLoadPerRegister: Double {
|
||||
|
||||
public var heatingLoadPerRegister: Double {
|
||||
heatingLoad / Double(registerCount)
|
||||
}
|
||||
|
||||
func coolingSensiblePerRegister(projectSHR: Double) -> Double {
|
||||
let sensible = coolingSensible ?? (coolingTotal * projectSHR)
|
||||
public func coolingSensiblePerRegister(projectSHR: Double) throws -> Double {
|
||||
let sensible = try coolingLoad.ensured(shr: projectSHR).sensible
|
||||
return sensible / Double(registerCount)
|
||||
}
|
||||
}
|
||||
@@ -30,8 +29,8 @@ extension TrunkSize.RoomProxy {
|
||||
room.heatingLoadPerRegister * Double(actualRegisterCount)
|
||||
}
|
||||
|
||||
func totalCoolingSensible(projectSHR: Double) -> Double {
|
||||
room.coolingSensiblePerRegister(projectSHR: projectSHR) * Double(actualRegisterCount)
|
||||
func totalCoolingSensible(projectSHR: Double) throws -> Double {
|
||||
try room.coolingSensiblePerRegister(projectSHR: projectSHR) * Double(actualRegisterCount)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,15 +40,11 @@ extension TrunkSize {
|
||||
rooms.reduce(into: 0) { $0 += $1.totalHeatingLoad }
|
||||
}
|
||||
|
||||
func totalCoolingSensible(projectSHR: Double) -> Double {
|
||||
rooms.reduce(into: 0) { $0 += $1.totalCoolingSensible(projectSHR: projectSHR) }
|
||||
func totalCoolingSensible(projectSHR: Double) throws -> Double {
|
||||
try rooms.reduce(into: 0) { $0 += try $1.totalCoolingSensible(projectSHR: projectSHR) }
|
||||
}
|
||||
}
|
||||
|
||||
extension ComponentPressureLosses {
|
||||
var totalLosses: Double { values.reduce(0) { $0 + $1 } }
|
||||
}
|
||||
|
||||
extension Array where Element == EffectiveLengthGroup {
|
||||
var totalEffectiveLength: Int {
|
||||
reduce(0) { $0 + $1.effectiveLength }
|
||||
@@ -101,16 +96,16 @@ func roundSize(_ size: Double) throws -> Int {
|
||||
}
|
||||
}
|
||||
|
||||
func velocity(cfm: Int, roundSize: Int) -> Int {
|
||||
let cfm = Double(cfm)
|
||||
func velocity(cfm: ManualDClient.CFM, roundSize: Int) -> Int {
|
||||
let cfm = Double(cfm.rawValue)
|
||||
let roundSize = Double(roundSize)
|
||||
let velocity = cfm / (pow(roundSize / 24, 2) * 3.14)
|
||||
return Int(round(velocity))
|
||||
}
|
||||
|
||||
func flexSize(_ request: ManualDClient.DuctSizeRequest) throws -> Int {
|
||||
let cfm = pow(Double(request.designCFM), 0.4)
|
||||
let fr = pow(request.frictionRate / 1.76, 0.2)
|
||||
func flexSize(_ cfm: ManualDClient.CFM, _ frictionRate: Double) throws -> Int {
|
||||
let cfm = pow(Double(cfm.rawValue), 0.4)
|
||||
let fr = pow(frictionRate / 1.76, 0.2)
|
||||
let size = 0.55 * (cfm / fr)
|
||||
return try roundSize(size)
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@ import Dependencies
|
||||
import DependenciesMacros
|
||||
import Logging
|
||||
import ManualDCore
|
||||
import Tagged
|
||||
|
||||
extension DependencyValues {
|
||||
/// Dependency that performs manual-d duct sizing calculations.
|
||||
public var manualD: ManualDClient {
|
||||
get { self[ManualDClient.self] }
|
||||
set { self[ManualDClient.self] = newValue }
|
||||
@@ -15,12 +17,61 @@ extension DependencyValues {
|
||||
///
|
||||
@DependencyClient
|
||||
public struct ManualDClient: Sendable {
|
||||
public var ductSize: @Sendable (DuctSizeRequest) async throws -> DuctSizeResponse
|
||||
public var frictionRate: @Sendable (FrictionRateRequest) async throws -> FrictionRate
|
||||
public var totalEquivalentLength: @Sendable (TotalEquivalentLengthRequest) async throws -> Int
|
||||
public var rectangularSize:
|
||||
@Sendable (RectangularSizeRequest) async throws -> RectangularSizeResponse
|
||||
|
||||
/// Calculates the duct size for the given cfm and friction rate.
|
||||
public var ductSize: @Sendable (CFM, DesignFrictionRate) async throws -> DuctSize
|
||||
/// Calculates the design friction rate for the given request.
|
||||
public var frictionRate: @Sendable (FrictionRateRequest) async throws -> FrictionRate
|
||||
/// Calculates the equivalent rectangular size for the given round duct and rectangular height.
|
||||
public var rectangularSize: @Sendable (RoundSize, Height) async throws -> RectangularSize
|
||||
|
||||
/// Calculates the duct size for the given cfm and friction rate.
|
||||
///
|
||||
/// - Paramaters:
|
||||
/// - designCFM: The design cfm for the duct.
|
||||
/// - designFrictionRate: The design friction rate for the system.
|
||||
public func ductSize(
|
||||
cfm designCFM: Int,
|
||||
frictionRate designFrictionRate: Double
|
||||
) async throws -> DuctSize {
|
||||
try await ductSize(.init(rawValue: designCFM), .init(rawValue: designFrictionRate))
|
||||
}
|
||||
|
||||
/// Calculates the duct size for the given cfm and friction rate.
|
||||
///
|
||||
/// - Paramaters:
|
||||
/// - designCFM: The design cfm for the duct.
|
||||
/// - designFrictionRate: The design friction rate for the system.
|
||||
public func ductSize(
|
||||
cfm designCFM: Double,
|
||||
frictionRate designFrictionRate: Double
|
||||
) async throws -> DuctSize {
|
||||
try await ductSize(.init(rawValue: Int(designCFM)), .init(rawValue: designFrictionRate))
|
||||
}
|
||||
|
||||
/// Calculates the equivalent rectangular size for the given round duct and rectangular height.
|
||||
///
|
||||
/// - Paramaters:
|
||||
/// - roundSize: The round duct size.
|
||||
/// - height: The rectangular height of the duct.
|
||||
public func rectangularSize(
|
||||
round roundSize: RoundSize,
|
||||
height: Height
|
||||
) async throws -> RectangularSize {
|
||||
try await rectangularSize(roundSize, height)
|
||||
}
|
||||
|
||||
/// Calculates the equivalent rectangular size for the given round duct and rectangular height.
|
||||
///
|
||||
/// - Paramaters:
|
||||
/// - roundSize: The round duct size.
|
||||
/// - height: The rectangular height of the duct.
|
||||
public func rectangularSize(
|
||||
round roundSize: Int,
|
||||
height: Int
|
||||
) async throws -> RectangularSize {
|
||||
try await rectangularSize(.init(rawValue: roundSize), .init(rawValue: height))
|
||||
}
|
||||
}
|
||||
|
||||
extension ManualDClient: TestDependencyKey {
|
||||
@@ -28,21 +79,20 @@ extension ManualDClient: TestDependencyKey {
|
||||
}
|
||||
|
||||
extension ManualDClient {
|
||||
|
||||
public struct DuctSizeRequest: Codable, Equatable, Sendable {
|
||||
public let designCFM: Int
|
||||
public let frictionRate: Double
|
||||
|
||||
public init(
|
||||
designCFM: Int,
|
||||
frictionRate: Double
|
||||
) {
|
||||
self.designCFM = designCFM
|
||||
self.frictionRate = frictionRate
|
||||
}
|
||||
/// A name space for tags used by the ManualDClient.
|
||||
public enum Tag {
|
||||
public enum CFM {}
|
||||
public enum DesignFrictionRate {}
|
||||
public enum Height {}
|
||||
public enum Round {}
|
||||
}
|
||||
|
||||
public struct DuctSizeResponse: Codable, Equatable, Sendable {
|
||||
public typealias CFM = Tagged<Tag.CFM, Int>
|
||||
public typealias DesignFrictionRate = Tagged<Tag.DesignFrictionRate, Double>
|
||||
public typealias Height = Tagged<Tag.Height, Int>
|
||||
public typealias RoundSize = Tagged<Tag.Round, Int>
|
||||
|
||||
public struct DuctSize: Codable, Equatable, Sendable {
|
||||
|
||||
public let calculatedSize: Double
|
||||
public let finalSize: Int
|
||||
@@ -66,58 +116,20 @@ extension ManualDClient {
|
||||
|
||||
public let externalStaticPressure: Double
|
||||
public let componentPressureLosses: [ComponentPressureLoss]
|
||||
public let totalEffectiveLength: Int
|
||||
public let totalEquivalentLength: Int
|
||||
|
||||
public init(
|
||||
externalStaticPressure: Double,
|
||||
componentPressureLosses: [ComponentPressureLoss],
|
||||
totalEffectiveLength: Int
|
||||
totalEquivalentLength: Int
|
||||
) {
|
||||
self.externalStaticPressure = externalStaticPressure
|
||||
self.componentPressureLosses = componentPressureLosses
|
||||
self.totalEffectiveLength = totalEffectiveLength
|
||||
self.totalEquivalentLength = totalEquivalentLength
|
||||
}
|
||||
}
|
||||
|
||||
public struct FrictionRateResponse: Codable, Equatable, Sendable {
|
||||
|
||||
public let availableStaticPressure: Double
|
||||
public let frictionRate: Double
|
||||
|
||||
public init(availableStaticPressure: Double, frictionRate: Double) {
|
||||
self.availableStaticPressure = availableStaticPressure
|
||||
self.frictionRate = frictionRate
|
||||
}
|
||||
}
|
||||
|
||||
public struct TotalEquivalentLengthRequest: Codable, Equatable, Sendable {
|
||||
|
||||
public let trunkLengths: [Int]
|
||||
public let runoutLengths: [Int]
|
||||
public let effectiveLengthGroups: [EffectiveLengthGroup]
|
||||
|
||||
public init(
|
||||
trunkLengths: [Int],
|
||||
runoutLengths: [Int],
|
||||
effectiveLengthGroups: [EffectiveLengthGroup]
|
||||
) {
|
||||
self.trunkLengths = trunkLengths
|
||||
self.runoutLengths = runoutLengths
|
||||
self.effectiveLengthGroups = effectiveLengthGroups
|
||||
}
|
||||
}
|
||||
|
||||
public struct RectangularSizeRequest: Codable, Equatable, Sendable {
|
||||
public let roundSize: Int
|
||||
public let height: Int
|
||||
|
||||
public init(round roundSize: Int, height: Int) {
|
||||
self.roundSize = roundSize
|
||||
self.height = height
|
||||
}
|
||||
}
|
||||
|
||||
public struct RectangularSizeResponse: Codable, Equatable, Sendable {
|
||||
public struct RectangularSize: Codable, Equatable, Sendable {
|
||||
public let height: Int
|
||||
public let width: Int
|
||||
|
||||
|
||||
@@ -4,41 +4,47 @@ import ManualDCore
|
||||
|
||||
extension ManualDClient: DependencyKey {
|
||||
public static let liveValue: Self = .init(
|
||||
ductSize: { request in
|
||||
guard request.designCFM > 0 else {
|
||||
ductSize: { cfm, frictionRate in
|
||||
guard cfm > 0 else {
|
||||
throw ManualDError(message: "Design CFM should be greater than 0.")
|
||||
}
|
||||
let fr = pow(request.frictionRate, 0.5)
|
||||
let ductulatorSize = pow(Double(request.designCFM) / (3.12 * fr), 0.38)
|
||||
let fr = pow(frictionRate.rawValue, 0.5)
|
||||
let ductulatorSize = pow(Double(cfm.rawValue) / (3.12 * fr), 0.38)
|
||||
let finalSize = try roundSize(ductulatorSize)
|
||||
let flexSize = try flexSize(request)
|
||||
let flexSize = try flexSize(cfm, frictionRate.rawValue)
|
||||
return .init(
|
||||
calculatedSize: ductulatorSize,
|
||||
finalSize: finalSize,
|
||||
flexSize: flexSize,
|
||||
velocity: velocity(cfm: request.designCFM, roundSize: finalSize)
|
||||
velocity: velocity(cfm: cfm, roundSize: finalSize)
|
||||
)
|
||||
},
|
||||
frictionRate: { request in
|
||||
// Ensure the total effective length is greater than 0.
|
||||
guard request.totalEffectiveLength > 0 else {
|
||||
guard request.totalEquivalentLength > 0 else {
|
||||
throw ManualDError(message: "Total Effective Length should be greater than 0.")
|
||||
}
|
||||
|
||||
let totalComponentLosses = request.componentPressureLosses.total
|
||||
let availableStaticPressure = request.externalStaticPressure - totalComponentLosses
|
||||
let frictionRate = availableStaticPressure * 100.0 / Double(request.totalEffectiveLength)
|
||||
return .init(availableStaticPressure: availableStaticPressure, value: frictionRate)
|
||||
let frictionRate = availableStaticPressure * 100.0 / Double(request.totalEquivalentLength)
|
||||
return .init(
|
||||
availableStaticPressure: availableStaticPressure,
|
||||
value: frictionRate
|
||||
)
|
||||
},
|
||||
totalEquivalentLength: { request in
|
||||
let trunkLengths = request.trunkLengths.reduce(0) { $0 + $1 }
|
||||
let runoutLengths = request.runoutLengths.reduce(0) { $0 + $1 }
|
||||
let groupLengths = request.effectiveLengthGroups.totalEffectiveLength
|
||||
return trunkLengths + runoutLengths + groupLengths
|
||||
},
|
||||
rectangularSize: { request in
|
||||
let width = (Double.pi * (pow(Double(request.roundSize) / 2.0, 2.0))) / Double(request.height)
|
||||
return .init(height: request.height, width: Int(width.rounded(.toNearestOrEven)))
|
||||
// totalEquivalentLength: { request in
|
||||
// let trunkLengths = request.trunkLengths.reduce(0) { $0 + $1 }
|
||||
// let runoutLengths = request.runoutLengths.reduce(0) { $0 + $1 }
|
||||
// let groupLengths = request.effectiveLengthGroups.totalEffectiveLength
|
||||
// return trunkLengths + runoutLengths + groupLengths
|
||||
// },
|
||||
rectangularSize: { round, height in
|
||||
let width = (Double.pi * (pow(Double(round.rawValue) / 2.0, 2.0))) / Double(height.rawValue)
|
||||
return .init(
|
||||
height: height.rawValue,
|
||||
width: Int(width.rounded(.toNearestOrEven))
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import Dependencies
|
||||
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 let id: UUID
|
||||
@@ -44,7 +48,7 @@ extension ComponentPressureLoss {
|
||||
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] {
|
||||
[
|
||||
.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
|
||||
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 {
|
||||
public static func mock(projectID: Project.ID) -> Self {
|
||||
@@ -1,13 +1,14 @@
|
||||
import Foundation
|
||||
// import Foundation
|
||||
|
||||
public struct CoolingLoad: Codable, Equatable, Sendable {
|
||||
public let total: Double
|
||||
public let sensible: Double
|
||||
public var latent: Double { total - sensible }
|
||||
public var shr: Double { sensible / total }
|
||||
|
||||
public init(total: Double, sensible: Double) {
|
||||
self.total = total
|
||||
self.sensible = sensible
|
||||
}
|
||||
}
|
||||
//
|
||||
// public struct CoolingLoad: Codable, Equatable, Sendable {
|
||||
// public let total: Double
|
||||
// public let sensible: Double
|
||||
// public var latent: Double { total - sensible }
|
||||
// public var shr: Double { sensible / total }
|
||||
//
|
||||
// public init(total: Double, sensible: Double) {
|
||||
// self.total = total
|
||||
// self.sensible = sensible
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -152,11 +152,12 @@ extension DuctSizes {
|
||||
public static func mock(
|
||||
equipmentInfo: EquipmentInfo,
|
||||
rooms: [Room],
|
||||
trunks: [TrunkSize]
|
||||
trunks: [TrunkSize],
|
||||
shr: Double
|
||||
) -> Self {
|
||||
|
||||
let totalHeatingLoad = rooms.totalHeatingLoad
|
||||
let totalCoolingLoad = rooms.totalCoolingLoad
|
||||
let totalCoolingLoad = try! rooms.totalCoolingLoad(shr: shr)
|
||||
|
||||
let roomContainers = rooms.reduce(into: [RoomContainer]()) { array, room in
|
||||
array += RoomContainer.mock(
|
||||
@@ -164,7 +165,8 @@ extension DuctSizes {
|
||||
totalHeatingLoad: totalHeatingLoad,
|
||||
totalCoolingLoad: totalCoolingLoad,
|
||||
totalHeatingCFM: Double(equipmentInfo.heatingCFM),
|
||||
totalCoolingCFM: Double(equipmentInfo.coolingCFM)
|
||||
totalCoolingCFM: Double(equipmentInfo.coolingCFM),
|
||||
shr: shr
|
||||
)
|
||||
}
|
||||
|
||||
@@ -187,14 +189,15 @@ extension DuctSizes {
|
||||
totalHeatingLoad: Double,
|
||||
totalCoolingLoad: Double,
|
||||
totalHeatingCFM: Double,
|
||||
totalCoolingCFM: Double
|
||||
totalCoolingCFM: Double,
|
||||
shr: Double
|
||||
) -> [Self] {
|
||||
var retval = [DuctSizes.RoomContainer]()
|
||||
let heatingLoad = room.heatingLoad / Double(room.registerCount)
|
||||
let heatingFraction = heatingLoad / totalHeatingLoad
|
||||
let heatingCFM = totalHeatingCFM * heatingFraction
|
||||
// Not really accurate, but works for mocks.
|
||||
let coolingLoad = room.coolingTotal / Double(room.registerCount)
|
||||
let coolingLoad = (try! room.coolingLoad.ensured(shr: shr).total) / Double(room.registerCount)
|
||||
let coolingFraction = coolingLoad / totalCoolingLoad
|
||||
let coolingCFM = totalCoolingCFM * coolingFraction
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -1,5 +1,7 @@
|
||||
import Foundation
|
||||
|
||||
// TODO: These are not used, should they be removed??
|
||||
|
||||
// TODO: Add other description / label for items that have same group & letter, but
|
||||
// different effective length.
|
||||
public struct EffectiveLengthGroup: Codable, Equatable, Sendable {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import Dependencies
|
||||
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 let id: UUID
|
||||
public let projectID: Project.ID
|
||||
|
||||
239
Sources/ManualDCore/EquivalentLength.swift
Normal file
239
Sources/ManualDCore/EquivalentLength.swift
Normal 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 init(supply: EquivalentLength? = nil, return: EquivalentLength? = nil) {
|
||||
self.supply = supply
|
||||
self.return = `return`
|
||||
}
|
||||
|
||||
public var totalEquivalentLength: Double? {
|
||||
guard let supply else { return nil }
|
||||
guard let `return` else { return nil }
|
||||
return supply.totalEquivalentLength + `return`.totalEquivalentLength
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -1,4 +1,17 @@
|
||||
import Foundation
|
||||
import Tagged
|
||||
|
||||
extension Tagged where RawValue == Double {
|
||||
public func string(digits: Int = 2) -> String {
|
||||
rawValue.string(digits: digits)
|
||||
}
|
||||
}
|
||||
|
||||
extension Tagged where RawValue == Int {
|
||||
public func string() -> String {
|
||||
rawValue.string()
|
||||
}
|
||||
}
|
||||
|
||||
extension Double {
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
/// Holds onto values returned when calculating the design
|
||||
/// 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 {
|
||||
/// The available static pressure is the equipment's design static pressure
|
||||
/// minus the ``ComponentPressureLoss``es for the project.
|
||||
public let availableStaticPressure: Double
|
||||
/// The calculated design friction rate value.
|
||||
public let value: Double
|
||||
/// Whether the design friction rate is within a valid range.
|
||||
public var hasErrors: Bool { error != nil }
|
||||
|
||||
public init(
|
||||
@@ -13,6 +19,7 @@ public struct FrictionRate: Codable, Equatable, Sendable {
|
||||
self.value = value
|
||||
}
|
||||
|
||||
/// The error if the design friction rate is out of a valid range.
|
||||
public var error: FrictionRateError? {
|
||||
if value >= 0.18 {
|
||||
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 {
|
||||
/// The reason for the error.
|
||||
public let reason: String
|
||||
/// The possible resolutions to the error.
|
||||
public let resolutions: [String]
|
||||
|
||||
public init(
|
||||
|
||||
@@ -1,16 +1,29 @@
|
||||
import Dependencies
|
||||
import Foundation
|
||||
|
||||
/// Represents a single duct design project / system.
|
||||
///
|
||||
/// Holds items such as project name and address.
|
||||
public struct Project: Codable, Equatable, Identifiable, Sendable {
|
||||
|
||||
/// The unique ID of the project.
|
||||
public let id: UUID
|
||||
/// The name of the project.
|
||||
public let name: String
|
||||
/// The street address of the project.
|
||||
public let streetAddress: String
|
||||
/// The city of the project.
|
||||
public let city: String
|
||||
/// The state of the project.
|
||||
public let state: String
|
||||
/// The zip code of the project.
|
||||
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?
|
||||
/// When the project was created in the database.
|
||||
public let createdAt: Date
|
||||
/// When the project was updated in the database.
|
||||
public let updatedAt: Date
|
||||
|
||||
public init(
|
||||
@@ -37,14 +50,20 @@ public struct Project: Codable, Equatable, Identifiable, Sendable {
|
||||
}
|
||||
|
||||
extension Project {
|
||||
|
||||
/// Represents the data needed to create a new project.
|
||||
public struct Create: Codable, Equatable, Sendable {
|
||||
|
||||
/// The name of the project.
|
||||
public let name: String
|
||||
/// The street address of the project.
|
||||
public let streetAddress: String
|
||||
/// The city of the project.
|
||||
public let city: String
|
||||
/// The state of the project.
|
||||
public let state: String
|
||||
/// The zip code of the project.
|
||||
public let zipCode: String
|
||||
/// The global sensible heat ratio for the project.
|
||||
public let sensibleHeatRatio: Double?
|
||||
|
||||
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 {
|
||||
|
||||
/// Whether there is ``EquipmentInfo`` for a project.
|
||||
public let equipmentInfo: Bool
|
||||
/// Whether there are ``Room``'s for a project.
|
||||
public let rooms: Bool
|
||||
/// Whether there are ``EquivalentLength``'s for a project.
|
||||
public let equivalentLength: Bool
|
||||
/// Whether there is a ``FrictionRate`` for a project.
|
||||
public let frictionRate: Bool
|
||||
|
||||
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 {
|
||||
|
||||
/// The project.
|
||||
public let project: Project
|
||||
/// The component pressure losses for the project.
|
||||
public let componentLosses: [ComponentPressureLoss]
|
||||
/// The equipment info for the project.
|
||||
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]
|
||||
/// The trunk sizes in the project.
|
||||
public let trunks: [TrunkSize]
|
||||
|
||||
public init(
|
||||
project: Project,
|
||||
componentLosses: [ComponentPressureLoss],
|
||||
equipmentInfo: EquipmentInfo,
|
||||
equivalentLengths: [EffectiveLength],
|
||||
equivalentLengths: [EquivalentLength],
|
||||
rooms: [Room],
|
||||
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 {
|
||||
|
||||
/// The name of the project.
|
||||
public let name: String?
|
||||
/// The street address of the project.
|
||||
public let streetAddress: String?
|
||||
/// The city of the project.
|
||||
public let city: String?
|
||||
/// The state of the project.
|
||||
public let state: String?
|
||||
/// The zip code of the project.
|
||||
public let zipCode: String?
|
||||
/// The global sensible heat ratio for the project.
|
||||
public let sensibleHeatRatio: Double?
|
||||
|
||||
public init(
|
||||
|
||||
@@ -1,16 +1,39 @@
|
||||
import Dependencies
|
||||
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 {
|
||||
/// The unique id of the room.
|
||||
public let id: UUID
|
||||
|
||||
/// The project this room is associated with.
|
||||
public let projectID: Project.ID
|
||||
|
||||
/// A unique name for the room in the project.
|
||||
public let name: String
|
||||
|
||||
/// The heating load required for the room (from Manual-J).
|
||||
public let heatingLoad: Double
|
||||
public let coolingTotal: Double
|
||||
public let coolingSensible: Double?
|
||||
|
||||
/// The cooling load required for the room (from Manual-J).
|
||||
public let coolingLoad: CoolingLoad
|
||||
|
||||
/// The number of registers for the room.
|
||||
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]?
|
||||
|
||||
/// When the room was created in the database.
|
||||
public let createdAt: Date
|
||||
|
||||
/// When the room was updated in the database.
|
||||
public let updatedAt: Date
|
||||
|
||||
public init(
|
||||
@@ -18,8 +41,7 @@ public struct Room: Codable, Equatable, Identifiable, Sendable {
|
||||
projectID: Project.ID,
|
||||
name: String,
|
||||
heatingLoad: Double,
|
||||
coolingTotal: Double,
|
||||
coolingSensible: Double? = nil,
|
||||
coolingLoad: CoolingLoad,
|
||||
registerCount: Int = 1,
|
||||
rectangularSizes: [RectangularSize]? = nil,
|
||||
createdAt: Date,
|
||||
@@ -29,34 +51,73 @@ public struct Room: Codable, Equatable, Identifiable, Sendable {
|
||||
self.projectID = projectID
|
||||
self.name = name
|
||||
self.heatingLoad = heatingLoad
|
||||
self.coolingTotal = coolingTotal
|
||||
self.coolingSensible = coolingSensible
|
||||
self.coolingLoad = coolingLoad
|
||||
self.registerCount = registerCount
|
||||
self.rectangularSizes = rectangularSizes
|
||||
self.createdAt = createdAt
|
||||
self.updatedAt = updatedAt
|
||||
}
|
||||
|
||||
/// Represents the cooling load of a room.
|
||||
///
|
||||
/// Generally only one of the values is provided by a Manual-J room x room
|
||||
/// calculation.
|
||||
///
|
||||
public struct CoolingLoad: Codable, Equatable, Sendable {
|
||||
|
||||
public let total: Double?
|
||||
public let sensible: Double?
|
||||
|
||||
public init(total: Double? = nil, sensible: Double? = nil) {
|
||||
self.total = total
|
||||
self.sensible = sensible
|
||||
}
|
||||
|
||||
/// Calculates the cooling load based on the shr.
|
||||
///
|
||||
/// Generally Manual-J room x room loads provide either the total load or the
|
||||
/// sensible load, so this allows us to calculate whichever is not provided.
|
||||
public func ensured(shr: Double) throws -> (total: Double, sensible: Double) {
|
||||
switch (total, sensible) {
|
||||
case (.none, .none):
|
||||
throw CoolingLoadError("Both the total and sensible loads are nil.")
|
||||
case (.some(let total), .some(let sensible)):
|
||||
return (total, sensible)
|
||||
case (.some(let total), .none):
|
||||
return (total, total * shr)
|
||||
case (.none, .some(let sensible)):
|
||||
return (sensible / shr, sensible)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Room {
|
||||
|
||||
/// Represents the data required to create a new room for a project.
|
||||
public struct Create: Codable, Equatable, Sendable {
|
||||
public let projectID: Project.ID
|
||||
/// A unique name for the room in the project.
|
||||
public let name: String
|
||||
/// The heating load required for the room (from Manual-J).
|
||||
public let heatingLoad: Double
|
||||
public let coolingTotal: Double
|
||||
/// The total cooling load required for the room (from Manual-J).
|
||||
public let coolingTotal: Double?
|
||||
/// An optional sensible cooling load for the room.
|
||||
public let coolingSensible: Double?
|
||||
/// The number of registers for the room.
|
||||
public let registerCount: Int
|
||||
|
||||
public var coolingLoad: Room.CoolingLoad {
|
||||
.init(total: coolingTotal, sensible: coolingSensible)
|
||||
}
|
||||
|
||||
public init(
|
||||
projectID: Project.ID,
|
||||
name: String,
|
||||
heatingLoad: Double,
|
||||
coolingTotal: Double,
|
||||
coolingTotal: Double? = nil,
|
||||
coolingSensible: Double? = nil,
|
||||
registerCount: Int = 1
|
||||
) {
|
||||
self.projectID = projectID
|
||||
self.name = name
|
||||
self.heatingLoad = heatingLoad
|
||||
self.coolingTotal = coolingTotal
|
||||
@@ -65,10 +126,25 @@ extension Room {
|
||||
}
|
||||
}
|
||||
|
||||
public struct RectangularSize: Codable, Equatable, Identifiable, Sendable {
|
||||
public struct CSV: Equatable, Sendable {
|
||||
public let file: Data
|
||||
|
||||
public init(file: Data) {
|
||||
self.file = file
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
/// The unique id of the rectangular size.
|
||||
public let id: UUID
|
||||
/// The register the rectangular size is associated with.
|
||||
public let register: Int?
|
||||
/// The height of the rectangular size, the width gets calculated.
|
||||
public let height: Int
|
||||
|
||||
public init(
|
||||
@@ -82,14 +158,30 @@ extension Room {
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents field that can be updated on a room after it's been created in the database.
|
||||
///
|
||||
/// Onlly fields that are supplied get updated.
|
||||
public struct Update: Codable, Equatable, Sendable {
|
||||
/// A unique name for the room in the project.
|
||||
public let name: String?
|
||||
/// The heating load required for the room (from Manual-J).
|
||||
public let heatingLoad: Double?
|
||||
/// The total cooling load required for the room (from Manual-J).
|
||||
public let coolingTotal: Double?
|
||||
/// An optional sensible cooling load for the room.
|
||||
public let coolingSensible: Double?
|
||||
/// The number of registers for the room.
|
||||
public let registerCount: Int?
|
||||
/// The rectangular duct size calculations for a room.
|
||||
public let rectangularSizes: [RectangularSize]?
|
||||
|
||||
public var coolingLoad: CoolingLoad? {
|
||||
guard coolingTotal != nil || coolingSensible != nil else {
|
||||
return nil
|
||||
}
|
||||
return .init(total: coolingTotal, sensible: coolingSensible)
|
||||
}
|
||||
|
||||
public init(
|
||||
name: String? = nil,
|
||||
heatingLoad: Double? = nil,
|
||||
@@ -120,57 +212,39 @@ extension Room {
|
||||
|
||||
extension Array where Element == Room {
|
||||
|
||||
/// The sum of heating loads for an array of rooms.
|
||||
public var totalHeatingLoad: Double {
|
||||
reduce(into: 0) { $0 += $1.heatingLoad }
|
||||
}
|
||||
|
||||
public var totalCoolingLoad: Double {
|
||||
reduce(into: 0) { $0 += $1.coolingTotal }
|
||||
/// The sum of total cooling loads for an array of rooms.
|
||||
public func totalCoolingLoad(shr: Double) throws -> Double {
|
||||
try reduce(into: 0) { $0 += try $1.coolingLoad.ensured(shr: shr).total }
|
||||
}
|
||||
|
||||
public func totalCoolingSensible(shr: Double) -> Double {
|
||||
reduce(into: 0) {
|
||||
let sensible = $1.coolingSensible ?? ($1.coolingTotal * shr)
|
||||
$0 += sensible
|
||||
/// The sum of sensible cooling loads for an array of rooms.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - shr: The project wide sensible heat ratio.
|
||||
public func totalCoolingSensible(shr: Double) throws -> Double {
|
||||
try reduce(into: 0) {
|
||||
// let sensible = $1.coolingSensible ?? ($1.coolingTotal * shr)
|
||||
$0 += try $1.coolingLoad.ensured(shr: shr).sensible
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct CoolingLoadError: Error, Equatable, Sendable {
|
||||
public let reason: String
|
||||
|
||||
public init(_ reason: String) {
|
||||
self.reason = reason
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
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] {
|
||||
@Dependency(\.uuid) var uuid
|
||||
@@ -182,8 +256,8 @@ extension Array where Element == Room {
|
||||
projectID: projectID,
|
||||
name: "Bed-1",
|
||||
heatingLoad: 3913,
|
||||
coolingTotal: 2472,
|
||||
coolingSensible: nil,
|
||||
coolingLoad: .init(total: 2472),
|
||||
// coolingSensible: nil,
|
||||
registerCount: 1,
|
||||
rectangularSizes: nil,
|
||||
createdAt: now,
|
||||
@@ -194,8 +268,8 @@ extension Array where Element == Room {
|
||||
projectID: projectID,
|
||||
name: "Entry",
|
||||
heatingLoad: 8284,
|
||||
coolingTotal: 2916,
|
||||
coolingSensible: nil,
|
||||
coolingLoad: .init(total: 2916),
|
||||
// coolingSensible: nil,
|
||||
registerCount: 2,
|
||||
rectangularSizes: nil,
|
||||
createdAt: now,
|
||||
@@ -206,8 +280,8 @@ extension Array where Element == Room {
|
||||
projectID: projectID,
|
||||
name: "Family Room",
|
||||
heatingLoad: 9785,
|
||||
coolingTotal: 7446,
|
||||
coolingSensible: nil,
|
||||
coolingLoad: .init(total: 7446),
|
||||
// coolingSensible: nil,
|
||||
registerCount: 3,
|
||||
rectangularSizes: nil,
|
||||
createdAt: now,
|
||||
@@ -218,8 +292,8 @@ extension Array where Element == Room {
|
||||
projectID: projectID,
|
||||
name: "Kitchen",
|
||||
heatingLoad: 4518,
|
||||
coolingTotal: 5096,
|
||||
coolingSensible: nil,
|
||||
coolingLoad: .init(total: 5096),
|
||||
// coolingSensible: nil,
|
||||
registerCount: 2,
|
||||
rectangularSizes: nil,
|
||||
createdAt: now,
|
||||
@@ -230,8 +304,8 @@ extension Array where Element == Room {
|
||||
projectID: projectID,
|
||||
name: "Living Room",
|
||||
heatingLoad: 7553,
|
||||
coolingTotal: 6829,
|
||||
coolingSensible: nil,
|
||||
coolingLoad: .init(total: 6829),
|
||||
// coolingSensible: nil,
|
||||
registerCount: 2,
|
||||
rectangularSizes: nil,
|
||||
createdAt: now,
|
||||
@@ -242,8 +316,8 @@ extension Array where Element == Room {
|
||||
projectID: projectID,
|
||||
name: "Master",
|
||||
heatingLoad: 8202,
|
||||
coolingTotal: 2076,
|
||||
coolingSensible: nil,
|
||||
coolingLoad: .init(total: 2076),
|
||||
// coolingSensible: nil,
|
||||
registerCount: 2,
|
||||
rectangularSizes: nil,
|
||||
createdAt: now,
|
||||
|
||||
@@ -1,270 +0,0 @@
|
||||
import CasePathsCore
|
||||
import Foundation
|
||||
@preconcurrency import URLRouting
|
||||
|
||||
extension SiteRoute {
|
||||
/// Represents api routes.
|
||||
///
|
||||
/// The routes return json as opposed to view routes that return html.
|
||||
public enum Api: Sendable, Equatable {
|
||||
|
||||
case project(Self.ProjectRoute)
|
||||
case room(Self.RoomRoute)
|
||||
case equipment(Self.EquipmentRoute)
|
||||
case componentLoss(Self.ComponentLossRoute)
|
||||
|
||||
public static let rootPath = Path {
|
||||
"api"
|
||||
"v1"
|
||||
}
|
||||
|
||||
public static let router = OneOf {
|
||||
Route(.case(Self.project)) {
|
||||
rootPath
|
||||
ProjectRoute.router
|
||||
}
|
||||
Route(.case(Self.room)) {
|
||||
rootPath
|
||||
RoomRoute.router
|
||||
}
|
||||
Route(.case(Self.equipment)) {
|
||||
rootPath
|
||||
EquipmentRoute.router
|
||||
}
|
||||
Route(.case(Self.componentLoss)) {
|
||||
rootPath
|
||||
ComponentLossRoute.router
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SiteRoute.Api {
|
||||
public enum ProjectRoute: Sendable, Equatable {
|
||||
case create(Project.Create)
|
||||
case delete(id: Project.ID)
|
||||
case detail(id: Project.ID, route: DetailRoute)
|
||||
case get(id: Project.ID)
|
||||
case index
|
||||
|
||||
static let rootPath = "projects"
|
||||
|
||||
public static let router = OneOf {
|
||||
Route(.case(Self.create)) {
|
||||
Path { rootPath }
|
||||
Method.post
|
||||
Body(.json(Project.Create.self))
|
||||
}
|
||||
Route(.case(Self.delete(id:))) {
|
||||
Path {
|
||||
rootPath
|
||||
Project.ID.parser()
|
||||
}
|
||||
Method.delete
|
||||
}
|
||||
Route(.case(Self.get(id:))) {
|
||||
Path {
|
||||
rootPath
|
||||
Project.ID.parser()
|
||||
}
|
||||
Method.get
|
||||
}
|
||||
Route(.case(Self.index)) {
|
||||
Path { rootPath }
|
||||
Method.get
|
||||
}
|
||||
Route(.case(Self.detail(id:route:))) {
|
||||
Path {
|
||||
rootPath
|
||||
Project.ID.parser()
|
||||
}
|
||||
DetailRoute.router
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SiteRoute.Api.ProjectRoute {
|
||||
public enum DetailRoute: Equatable, Sendable {
|
||||
case index
|
||||
case completedSteps
|
||||
|
||||
static let rootPath = "details"
|
||||
|
||||
static let router = OneOf {
|
||||
Route(.case(Self.index)) {
|
||||
Path { rootPath }
|
||||
Method.get
|
||||
}
|
||||
Route(.case(Self.completedSteps)) {
|
||||
Path {
|
||||
rootPath
|
||||
"completed"
|
||||
}
|
||||
Method.get
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SiteRoute.Api {
|
||||
|
||||
public enum RoomRoute: Sendable, Equatable {
|
||||
case create(Room.Create)
|
||||
case delete(id: Room.ID)
|
||||
case get(id: Room.ID)
|
||||
|
||||
static let rootPath = "rooms"
|
||||
|
||||
public static let router = OneOf {
|
||||
Route(.case(Self.create)) {
|
||||
Path { rootPath }
|
||||
Method.post
|
||||
Body(.json(Room.Create.self))
|
||||
}
|
||||
Route(.case(Self.delete(id:))) {
|
||||
Path {
|
||||
rootPath
|
||||
Room.ID.parser()
|
||||
}
|
||||
Method.delete
|
||||
}
|
||||
Route(.case(Self.get(id:))) {
|
||||
Path {
|
||||
rootPath
|
||||
Room.ID.parser()
|
||||
}
|
||||
Method.get
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SiteRoute.Api {
|
||||
|
||||
public enum EquipmentRoute: Sendable, Equatable {
|
||||
case create(EquipmentInfo.Create)
|
||||
case delete(id: EquipmentInfo.ID)
|
||||
case fetch(projectID: Project.ID)
|
||||
case get(id: EquipmentInfo.ID)
|
||||
|
||||
static let rootPath = "equipment"
|
||||
|
||||
public static let router = OneOf {
|
||||
Route(.case(Self.create)) {
|
||||
Path { rootPath }
|
||||
Method.post
|
||||
Body(.json(EquipmentInfo.Create.self))
|
||||
}
|
||||
Route(.case(Self.delete(id:))) {
|
||||
Path {
|
||||
rootPath
|
||||
EquipmentInfo.ID.parser()
|
||||
}
|
||||
Method.delete
|
||||
}
|
||||
Route(.case(Self.fetch(projectID:))) {
|
||||
Path { rootPath }
|
||||
Method.get
|
||||
Query {
|
||||
Field("projectID") { Project.ID.parser() }
|
||||
}
|
||||
}
|
||||
Route(.case(Self.get(id:))) {
|
||||
Path {
|
||||
rootPath
|
||||
EquipmentInfo.ID.parser()
|
||||
}
|
||||
Method.get
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SiteRoute.Api {
|
||||
|
||||
public enum ComponentLossRoute: Sendable, Equatable {
|
||||
case create(ComponentPressureLoss.Create)
|
||||
case delete(id: ComponentPressureLoss.ID)
|
||||
case fetch(projectID: Project.ID)
|
||||
case get(id: ComponentPressureLoss.ID)
|
||||
|
||||
static let rootPath = "componentLoss"
|
||||
|
||||
public static let router = OneOf {
|
||||
Route(.case(Self.create)) {
|
||||
Path { rootPath }
|
||||
Method.post
|
||||
Body(.json(ComponentPressureLoss.Create.self))
|
||||
}
|
||||
Route(.case(Self.delete(id:))) {
|
||||
Path {
|
||||
rootPath
|
||||
ComponentPressureLoss.ID.parser()
|
||||
}
|
||||
Method.delete
|
||||
}
|
||||
Route(.case(Self.fetch(projectID:))) {
|
||||
Path { rootPath }
|
||||
Method.get
|
||||
Query {
|
||||
Field("projectID") { Project.ID.parser() }
|
||||
}
|
||||
}
|
||||
Route(.case(Self.get(id:))) {
|
||||
Path {
|
||||
rootPath
|
||||
ComponentPressureLoss.ID.parser()
|
||||
}
|
||||
Method.get
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SiteRoute.Api {
|
||||
public enum EffectiveLengthRoute: Equatable, Sendable {
|
||||
case create(EffectiveLength.Create)
|
||||
case delete(id: EffectiveLength.ID)
|
||||
case fetch(projectID: Project.ID)
|
||||
case get(id: EffectiveLength.ID)
|
||||
|
||||
static let rootPath = "effectiveLength"
|
||||
|
||||
public static let router = OneOf {
|
||||
Route(.case(Self.create)) {
|
||||
Path {
|
||||
rootPath
|
||||
"create"
|
||||
}
|
||||
Method.post
|
||||
Body(.json(EffectiveLength.Create.self))
|
||||
}
|
||||
Route(.case(Self.delete(id:))) {
|
||||
Path {
|
||||
rootPath
|
||||
EffectiveLength.ID.parser()
|
||||
}
|
||||
Method.delete
|
||||
}
|
||||
Route(.case(Self.fetch(projectID:))) {
|
||||
Path {
|
||||
rootPath
|
||||
}
|
||||
Method.get
|
||||
Query {
|
||||
Field("projectID") { Project.ID.parser() }
|
||||
}
|
||||
}
|
||||
Route(.case(Self.get(id:))) {
|
||||
Path {
|
||||
rootPath
|
||||
EffectiveLength.ID.parser()
|
||||
}
|
||||
Method.get
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,10 @@ import Foundation
|
||||
|
||||
public enum SiteRoute: Equatable, Sendable {
|
||||
|
||||
case api(Self.Api)
|
||||
case health
|
||||
case view(Self.View)
|
||||
|
||||
public static let router = OneOf {
|
||||
Route(.case(Self.api)) {
|
||||
SiteRoute.Api.router
|
||||
}
|
||||
Route(.case(Self.health)) {
|
||||
Path { "health" }
|
||||
Method.get
|
||||
|
||||
@@ -188,6 +188,7 @@ extension SiteRoute.View.ProjectRoute {
|
||||
}
|
||||
|
||||
public enum RoomRoute: Equatable, Sendable {
|
||||
case csv(Room.CSV)
|
||||
case delete(id: Room.ID)
|
||||
case index
|
||||
case submit(Room.Create)
|
||||
@@ -197,6 +198,23 @@ extension SiteRoute.View.ProjectRoute {
|
||||
static let rootPath = "rooms"
|
||||
|
||||
public static let router = OneOf {
|
||||
Route(.case(Self.csv)) {
|
||||
Path {
|
||||
rootPath
|
||||
"csv"
|
||||
}
|
||||
Headers {
|
||||
Field("Content-Type") { "multipart/form-data" }
|
||||
}
|
||||
Method.post
|
||||
Body().map(.memberwise(Room.CSV.init))
|
||||
// Body {
|
||||
// FormData {
|
||||
//
|
||||
// }
|
||||
// .map(.memberwise(Room.CSV.init))
|
||||
// }
|
||||
}
|
||||
Route(.case(Self.delete)) {
|
||||
Path {
|
||||
rootPath
|
||||
@@ -215,12 +233,14 @@ extension SiteRoute.View.ProjectRoute {
|
||||
Method.post
|
||||
Body {
|
||||
FormData {
|
||||
Field("projectID") { Project.ID.parser() }
|
||||
// Field("projectID") { Project.ID.parser() }
|
||||
Field("name", .string)
|
||||
Field("heatingLoad") { Double.parser() }
|
||||
Field("coolingTotal") { Double.parser() }
|
||||
Optionally {
|
||||
Field("coolingSensible", default: nil) { Double.parser() }
|
||||
Field("coolingTotal") { Double.parser() }
|
||||
}
|
||||
Optionally {
|
||||
Field("coolingSensible") { Double.parser() }
|
||||
}
|
||||
Field("registerCount") { Digits() }
|
||||
}
|
||||
@@ -394,11 +414,11 @@ extension SiteRoute.View.ProjectRoute {
|
||||
}
|
||||
|
||||
public enum EquivalentLengthRoute: Equatable, Sendable {
|
||||
case delete(id: EffectiveLength.ID)
|
||||
case field(FieldType, style: EffectiveLength.EffectiveLengthType? = nil)
|
||||
case delete(id: EquivalentLength.ID)
|
||||
case field(FieldType, style: EquivalentLength.EffectiveLengthType? = nil)
|
||||
case index
|
||||
case submit(FormStep)
|
||||
case update(EffectiveLength.ID, StepThree)
|
||||
case update(EquivalentLength.ID, StepThree)
|
||||
|
||||
static let rootPath = "effective-lengths"
|
||||
|
||||
@@ -406,7 +426,7 @@ extension SiteRoute.View.ProjectRoute {
|
||||
Route(.case(Self.delete(id:))) {
|
||||
Path {
|
||||
rootPath
|
||||
EffectiveLength.ID.parser()
|
||||
EquivalentLength.ID.parser()
|
||||
}
|
||||
Method.delete
|
||||
}
|
||||
@@ -424,7 +444,7 @@ extension SiteRoute.View.ProjectRoute {
|
||||
Field("type") { FieldType.parser() }
|
||||
Optionally {
|
||||
Field("style", default: nil) {
|
||||
EffectiveLength.EffectiveLengthType.parser()
|
||||
EquivalentLength.EffectiveLengthType.parser()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -437,16 +457,16 @@ extension SiteRoute.View.ProjectRoute {
|
||||
Route(.case(Self.update)) {
|
||||
Path {
|
||||
rootPath
|
||||
EffectiveLength.ID.parser()
|
||||
EquivalentLength.ID.parser()
|
||||
}
|
||||
Method.patch
|
||||
Body {
|
||||
FormData {
|
||||
Optionally {
|
||||
Field("id", default: nil) { EffectiveLength.ID.parser() }
|
||||
Field("id", default: nil) { EquivalentLength.ID.parser() }
|
||||
}
|
||||
Field("name", .string)
|
||||
Field("type") { EffectiveLength.EffectiveLengthType.parser() }
|
||||
Field("type") { EquivalentLength.EffectiveLengthType.parser() }
|
||||
Many {
|
||||
Field("straightLengths") {
|
||||
Int.parser()
|
||||
@@ -490,10 +510,10 @@ extension SiteRoute.View.ProjectRoute {
|
||||
Body {
|
||||
FormData {
|
||||
Optionally {
|
||||
Field("id", default: nil) { EffectiveLength.ID.parser() }
|
||||
Field("id", default: nil) { EquivalentLength.ID.parser() }
|
||||
}
|
||||
Field("name", .string)
|
||||
Field("type") { EffectiveLength.EffectiveLengthType.parser() }
|
||||
Field("type") { EquivalentLength.EffectiveLengthType.parser() }
|
||||
}
|
||||
.map(.memberwise(StepOne.init))
|
||||
}
|
||||
@@ -505,10 +525,10 @@ extension SiteRoute.View.ProjectRoute {
|
||||
Body {
|
||||
FormData {
|
||||
Optionally {
|
||||
Field("id", default: nil) { EffectiveLength.ID.parser() }
|
||||
Field("id", default: nil) { EquivalentLength.ID.parser() }
|
||||
}
|
||||
Field("name", .string)
|
||||
Field("type") { EffectiveLength.EffectiveLengthType.parser() }
|
||||
Field("type") { EquivalentLength.EffectiveLengthType.parser() }
|
||||
Many {
|
||||
Field("straightLengths") {
|
||||
Int.parser()
|
||||
@@ -525,10 +545,10 @@ extension SiteRoute.View.ProjectRoute {
|
||||
Body {
|
||||
FormData {
|
||||
Optionally {
|
||||
Field("id", default: nil) { EffectiveLength.ID.parser() }
|
||||
Field("id", default: nil) { EquivalentLength.ID.parser() }
|
||||
}
|
||||
Field("name", .string)
|
||||
Field("type") { EffectiveLength.EffectiveLengthType.parser() }
|
||||
Field("type") { EquivalentLength.EffectiveLengthType.parser() }
|
||||
Many {
|
||||
Field("straightLengths") {
|
||||
Int.parser()
|
||||
@@ -567,22 +587,22 @@ extension SiteRoute.View.ProjectRoute {
|
||||
}
|
||||
|
||||
public struct StepOne: Codable, Equatable, Sendable {
|
||||
public let id: EffectiveLength.ID?
|
||||
public let id: EquivalentLength.ID?
|
||||
public let name: String
|
||||
public let type: EffectiveLength.EffectiveLengthType
|
||||
public let type: EquivalentLength.EffectiveLengthType
|
||||
}
|
||||
|
||||
public struct StepTwo: Codable, Equatable, Sendable {
|
||||
|
||||
public let id: EffectiveLength.ID?
|
||||
public let id: EquivalentLength.ID?
|
||||
public let name: String
|
||||
public let type: EffectiveLength.EffectiveLengthType
|
||||
public let type: EquivalentLength.EffectiveLengthType
|
||||
public let straightLengths: [Int]
|
||||
|
||||
public init(
|
||||
id: EffectiveLength.ID? = nil,
|
||||
id: EquivalentLength.ID? = nil,
|
||||
name: String,
|
||||
type: EffectiveLength.EffectiveLengthType,
|
||||
type: EquivalentLength.EffectiveLengthType,
|
||||
straightLengths: [Int]
|
||||
) {
|
||||
self.id = id
|
||||
@@ -593,9 +613,9 @@ extension SiteRoute.View.ProjectRoute {
|
||||
}
|
||||
|
||||
public struct StepThree: Codable, Equatable, Sendable {
|
||||
public let id: EffectiveLength.ID?
|
||||
public let id: EquivalentLength.ID?
|
||||
public let name: String
|
||||
public let type: EffectiveLength.EffectiveLengthType
|
||||
public let type: EquivalentLength.EffectiveLengthType
|
||||
public let straightLengths: [Int]
|
||||
public let groupGroups: [Int]
|
||||
public let groupLetters: [String]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Foundation
|
||||
|
||||
/// Represents supported color themes for the website.
|
||||
public enum Theme: String, CaseIterable, Codable, Equatable, Sendable {
|
||||
case aqua
|
||||
case cupcake
|
||||
@@ -13,6 +14,7 @@ public enum Theme: String, CaseIterable, Codable, Equatable, Sendable {
|
||||
case retro
|
||||
case synthwave
|
||||
|
||||
/// Represents dark color themes.
|
||||
public static let darkThemes = [
|
||||
Self.aqua,
|
||||
Self.cyberpunk,
|
||||
@@ -22,6 +24,7 @@ public enum Theme: String, CaseIterable, Codable, Equatable, Sendable {
|
||||
Self.synthwave,
|
||||
]
|
||||
|
||||
/// Represents light color themes.
|
||||
public static let lightThemes = [
|
||||
Self.cupcake,
|
||||
Self.light,
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
import Dependencies
|
||||
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 {
|
||||
|
||||
/// The unique identifier of the trunk size.
|
||||
public let id: UUID
|
||||
/// The project the trunk size is for.
|
||||
public let projectID: Project.ID
|
||||
/// The type of the trunk size (supply or return).
|
||||
public let type: TrunkType
|
||||
/// The rooms / registers associated with the trunk size.
|
||||
public let rooms: [RoomProxy]
|
||||
/// An optional rectangular height used to calculate the equivalent
|
||||
/// rectangular size of the trunk.
|
||||
public let height: Int?
|
||||
/// An optional name / label used for identifying the trunk.
|
||||
public let name: String?
|
||||
|
||||
public init(
|
||||
@@ -29,12 +38,19 @@ public struct TrunkSize: Codable, Equatable, Identifiable, Sendable {
|
||||
}
|
||||
|
||||
extension TrunkSize {
|
||||
/// Represents the data needed to create a new ``TrunkSize`` in the database.
|
||||
public struct Create: Codable, Equatable, Sendable {
|
||||
|
||||
/// The project the trunk size is for.
|
||||
public let projectID: Project.ID
|
||||
/// The type of the trunk size (supply or return).
|
||||
public let type: TrunkType
|
||||
/// The rooms / registers associated with the trunk size.
|
||||
public let rooms: [Room.ID: [Int]]
|
||||
/// An optional rectangular height used to calculate the equivalent
|
||||
/// rectangular size of the trunk.
|
||||
public let height: Int?
|
||||
/// An optional name / label used for identifying the trunk.
|
||||
public let name: String?
|
||||
|
||||
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 {
|
||||
|
||||
/// The type of the trunk size (supply or return).
|
||||
public let type: TrunkType?
|
||||
/// The rooms / registers associated with the trunk size.
|
||||
public let rooms: [Room.ID: [Int]]?
|
||||
/// An optional rectangular height used to calculate the equivalent
|
||||
/// rectangular size of the trunk.
|
||||
public let height: Int?
|
||||
/// An optional name / label used for identifying the trunk.
|
||||
public let name: String?
|
||||
|
||||
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
|
||||
/// The specific room registers associated with the ``TrunkSize``.
|
||||
public let registers: [Int]
|
||||
|
||||
public init(room: Room, registers: [Int]) {
|
||||
self.room = room
|
||||
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 {
|
||||
case `return`
|
||||
case supply
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import Dependencies
|
||||
import Foundation
|
||||
|
||||
// FIX: Remove username.
|
||||
/// Represents a user of the site.
|
||||
///
|
||||
public struct User: Codable, Equatable, Identifiable, Sendable {
|
||||
|
||||
/// The unique id of the user.
|
||||
public let id: UUID
|
||||
/// The user's email address.
|
||||
public let email: String
|
||||
/// When the user was created in the database.
|
||||
public let createdAt: Date
|
||||
/// When the user was updated in the database.
|
||||
public let updatedAt: Date
|
||||
|
||||
public init(
|
||||
@@ -23,10 +28,14 @@ public struct User: Codable, Equatable, Identifiable, Sendable {
|
||||
}
|
||||
|
||||
extension User {
|
||||
/// Represents the data required to create a new user.
|
||||
public struct Create: Codable, Equatable, Sendable {
|
||||
|
||||
/// The user's email address.
|
||||
public let email: String
|
||||
/// The password for the user.
|
||||
public let password: String
|
||||
/// The password confirmation, must match the password.
|
||||
public let confirmPassword: String
|
||||
|
||||
public init(
|
||||
@@ -40,9 +49,13 @@ extension User {
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents data required to login a user.
|
||||
public struct Login: Codable, Equatable, Sendable {
|
||||
/// The user's email address.
|
||||
public let email: String
|
||||
/// The password for the user.
|
||||
public let password: String
|
||||
/// An optional page / route to navigate to after logging in the user.
|
||||
public let next: String?
|
||||
|
||||
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 {
|
||||
|
||||
/// The unique id of the token.
|
||||
public let id: UUID
|
||||
/// The user id the token is for.
|
||||
public let userID: User.ID
|
||||
/// The token value.
|
||||
public let value: String
|
||||
|
||||
public init(id: UUID, userID: User.ID, value: String) {
|
||||
|
||||
@@ -2,19 +2,32 @@ import Dependencies
|
||||
import Foundation
|
||||
|
||||
extension User {
|
||||
/// Represents a user's profile. Which contains extra information about a user of the site.
|
||||
public struct Profile: Codable, Equatable, Identifiable, Sendable {
|
||||
|
||||
/// The unique id of the profile
|
||||
public let id: UUID
|
||||
/// The user id the profile is for.
|
||||
public let userID: User.ID
|
||||
/// The user's first name.
|
||||
public let firstName: String
|
||||
/// The user's last name.
|
||||
public let lastName: String
|
||||
/// The user's company name.
|
||||
public let companyName: String
|
||||
/// The user's street address.
|
||||
public let streetAddress: String
|
||||
/// The user's city.
|
||||
public let city: String
|
||||
/// The user's state.
|
||||
public let state: String
|
||||
/// The user's zip code.
|
||||
public let zipCode: String
|
||||
/// An optional theme that the user prefers.
|
||||
public let theme: Theme?
|
||||
/// When the profile was created in the database.
|
||||
public let createdAt: Date
|
||||
/// When the profile was updated in the database.
|
||||
public let updatedAt: Date
|
||||
|
||||
public init(
|
||||
@@ -49,15 +62,25 @@ extension User {
|
||||
|
||||
extension User.Profile {
|
||||
|
||||
/// Represents the data required to create a user profile.
|
||||
public struct Create: Codable, Equatable, Sendable {
|
||||
/// The user id the profile is for.
|
||||
public let userID: User.ID
|
||||
/// The user's first name.
|
||||
public let firstName: String
|
||||
/// The user's last name.
|
||||
public let lastName: String
|
||||
/// The user's company name.
|
||||
public let companyName: String
|
||||
/// The user's street address.
|
||||
public let streetAddress: String
|
||||
/// The user's city.
|
||||
public let city: String
|
||||
/// The user's state.
|
||||
public let state: String
|
||||
/// The user's zip code.
|
||||
public let zipCode: String
|
||||
/// An optional theme that the user prefers.
|
||||
public let theme: Theme?
|
||||
|
||||
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 {
|
||||
/// The user's first name.
|
||||
public let firstName: String?
|
||||
/// The user's last name.
|
||||
public let lastName: String?
|
||||
/// The user's company name.
|
||||
public let companyName: String?
|
||||
/// The user's street address.
|
||||
public let streetAddress: String?
|
||||
/// The user's city.
|
||||
public let city: String?
|
||||
/// The user's state.
|
||||
public let state: String?
|
||||
/// The user's zip code.
|
||||
public let zipCode: String?
|
||||
/// An optional theme that the user prefers.
|
||||
public let theme: Theme?
|
||||
|
||||
public init(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Dependencies
|
||||
import DependenciesMacros
|
||||
import Elementary
|
||||
import EnvClient
|
||||
import EnvVars
|
||||
import FileClient
|
||||
import Foundation
|
||||
import ManualDCore
|
||||
@@ -32,8 +32,7 @@ public struct PdfClient: Sendable {
|
||||
/// - Parameters:
|
||||
/// - request: The project data used to generate the pdf.
|
||||
public func generatePdf(request: Request) async throws -> Response {
|
||||
let html = try await self.html(request)
|
||||
return try await self.generatePdf(request.project.id, html)
|
||||
try await self.generatePdf(request.project.id, html(request))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -47,9 +46,8 @@ extension PdfClient: DependencyKey {
|
||||
},
|
||||
generatePdf: { projectID, html in
|
||||
@Dependency(\.fileClient) var fileClient
|
||||
@Dependency(\.env) var env
|
||||
@Dependency(\.environment) var environment
|
||||
|
||||
let envVars = try env()
|
||||
let baseUrl = "/tmp/\(projectID)"
|
||||
try await fileClient.writeFile(html.render(), "\(baseUrl).html")
|
||||
|
||||
@@ -58,10 +56,10 @@ extension PdfClient: DependencyKey {
|
||||
let standardOutput = Pipe()
|
||||
process.standardInput = standardInput
|
||||
process.standardOutput = standardOutput
|
||||
process.executableURL = URL(fileURLWithPath: envVars.pandocPath)
|
||||
process.executableURL = URL(fileURLWithPath: environment.pandocPath)
|
||||
process.arguments = [
|
||||
"\(baseUrl).html",
|
||||
"--pdf-engine=\(envVars.pdfEngine)",
|
||||
"--pdf-engine=\(environment.pdfEngine)",
|
||||
"--from=html",
|
||||
"--css=Public/css/pdf.css",
|
||||
"--output=\(baseUrl).pdf",
|
||||
@@ -76,17 +74,26 @@ extension PdfClient: DependencyKey {
|
||||
}
|
||||
|
||||
extension PdfClient {
|
||||
/// Container for the data required to generate a pdf for a given project.
|
||||
/// Represents the data required to generate a pdf for a given project.
|
||||
public struct Request: Codable, Equatable, Sendable {
|
||||
|
||||
/// The project we're generating a pdf for.
|
||||
public let project: Project
|
||||
/// The rooms in the project.
|
||||
public let rooms: [Room]
|
||||
/// The component pressure losses for the project.
|
||||
public let componentLosses: [ComponentPressureLoss]
|
||||
/// The calculated duct sizes for the project.
|
||||
public let ductSizes: DuctSizes
|
||||
/// The equipment information for the project.
|
||||
public let equipmentInfo: EquipmentInfo
|
||||
public let maxSupplyTEL: EffectiveLength
|
||||
public let maxReturnTEL: EffectiveLength
|
||||
/// The max supply equivalent length for the project.
|
||||
public let maxSupplyTEL: EquivalentLength
|
||||
/// The max return equivalent length for the project.
|
||||
public let maxReturnTEL: EquivalentLength
|
||||
/// The calculated design friction rate for the project.
|
||||
public let frictionRate: FrictionRate
|
||||
/// The project wide sensible heat ratio.
|
||||
public let projectSHR: Double
|
||||
|
||||
var totalEquivalentLength: Double {
|
||||
@@ -99,8 +106,8 @@ extension PdfClient {
|
||||
componentLosses: [ComponentPressureLoss],
|
||||
ductSizes: DuctSizes,
|
||||
equipmentInfo: EquipmentInfo,
|
||||
maxSupplyTEL: EffectiveLength,
|
||||
maxReturnTEL: EffectiveLength,
|
||||
maxSupplyTEL: EquivalentLength,
|
||||
maxReturnTEL: EquivalentLength,
|
||||
frictionRate: FrictionRate,
|
||||
projectSHR: Double
|
||||
) {
|
||||
@@ -116,9 +123,12 @@ extension PdfClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the response after generating a pdf.
|
||||
public struct Response: Equatable, Sendable {
|
||||
|
||||
/// The path to the html file used to generate the pdf from.
|
||||
public let htmlPath: String
|
||||
/// The path to the pdf file.
|
||||
public let pdfPath: String
|
||||
|
||||
public init(htmlPath: String, pdfPath: String) {
|
||||
@@ -134,13 +144,18 @@ extension PdfClient {
|
||||
let rooms = Room.mock(projectID: project.id)
|
||||
let trunks = TrunkSize.mock(projectID: project.id, rooms: rooms)
|
||||
let equipmentInfo = EquipmentInfo.mock(projectID: project.id)
|
||||
let equivalentLengths = EffectiveLength.mock(projectID: project.id)
|
||||
let equivalentLengths = EquivalentLength.mock(projectID: project.id)
|
||||
|
||||
return .init(
|
||||
project: project,
|
||||
rooms: rooms,
|
||||
componentLosses: ComponentPressureLoss.mock(projectID: project.id),
|
||||
ductSizes: .mock(equipmentInfo: equipmentInfo, rooms: rooms, trunks: trunks),
|
||||
ductSizes: .mock(
|
||||
equipmentInfo: equipmentInfo,
|
||||
rooms: rooms,
|
||||
trunks: trunks,
|
||||
shr: project.sensibleHeatRatio ?? 0.83
|
||||
),
|
||||
equipmentInfo: equipmentInfo,
|
||||
maxSupplyTEL: equivalentLengths.first { $0.type == .supply }!,
|
||||
maxReturnTEL: equivalentLengths.first { $0.type == .return }!,
|
||||
|
||||
@@ -2,7 +2,7 @@ import Elementary
|
||||
import ManualDCore
|
||||
|
||||
struct EffectiveLengthsTable: HTML, Sendable {
|
||||
let effectiveLengths: [EffectiveLength]
|
||||
let effectiveLengths: [EquivalentLength]
|
||||
|
||||
var body: some HTML<HTMLTag.table> {
|
||||
table {
|
||||
@@ -41,7 +41,7 @@ struct EffectiveLengthsTable: HTML, Sendable {
|
||||
}
|
||||
|
||||
struct EffectiveLengthGroupTable: HTML, Sendable {
|
||||
let groups: [EffectiveLength.Group]
|
||||
let groups: [EquivalentLength.FittingGroup]
|
||||
|
||||
var body: some HTML<HTMLTag.table> {
|
||||
table {
|
||||
|
||||
@@ -21,10 +21,9 @@ struct RoomsTable: HTML, Sendable {
|
||||
tr {
|
||||
td { room.name }
|
||||
td { room.heatingLoad.string(digits: 0) }
|
||||
td { room.coolingTotal.string(digits: 0) }
|
||||
td { try! room.coolingLoad.ensured(shr: projectSHR).total.string(digits: 0) }
|
||||
td {
|
||||
(room.coolingSensible
|
||||
?? (room.coolingTotal * projectSHR)).string(digits: 0)
|
||||
try! room.coolingLoad.ensured(shr: projectSHR).sensible.string(digits: 0)
|
||||
}
|
||||
td { room.registerCount.string() }
|
||||
}
|
||||
@@ -37,10 +36,10 @@ struct RoomsTable: HTML, Sendable {
|
||||
rooms.totalHeatingLoad.string(digits: 0)
|
||||
}
|
||||
td(.class("coolingTotal label")) {
|
||||
rooms.totalCoolingLoad.string(digits: 0)
|
||||
try! rooms.totalCoolingLoad(shr: projectSHR).string(digits: 0)
|
||||
}
|
||||
td(.class("coolingSensible label")) {
|
||||
rooms.totalCoolingSensible(shr: projectSHR).string(digits: 0)
|
||||
try! rooms.totalCoolingSensible(shr: projectSHR).string(digits: 0)
|
||||
}
|
||||
td {}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
public struct ProjectClientError: Error {
|
||||
public let reason: String
|
||||
|
||||
public init(_ reason: String) {
|
||||
self.reason = reason
|
||||
}
|
||||
}
|
||||
@@ -15,12 +15,15 @@ extension DependencyValues {
|
||||
/// Useful helper utilities for project's.
|
||||
///
|
||||
/// This is primarily used for implementing logic required to get the needed data
|
||||
/// for the view controller client to render views.
|
||||
/// for the view controller to render views.
|
||||
@DependencyClient
|
||||
public struct ProjectClient: Sendable {
|
||||
public var calculateDuctSizes: @Sendable (Project.ID) async throws -> DuctSizes
|
||||
|
||||
/// Calculates the room duct sizes for the given project.
|
||||
public var calculateRoomDuctSizes:
|
||||
@Sendable (Project.ID) async throws -> [DuctSizes.RoomContainer]
|
||||
|
||||
/// Calculates the trunk duct sizes for the given project.
|
||||
public var calculateTrunkDuctSizes:
|
||||
@Sendable (Project.ID) async throws -> [DuctSizes.TrunkContainer]
|
||||
|
||||
@@ -29,6 +32,13 @@ public struct ProjectClient: Sendable {
|
||||
|
||||
public var frictionRate: @Sendable (Project.ID) async throws -> FrictionRateResponse
|
||||
public var generatePdf: @Sendable (Project.ID) async throws -> Response
|
||||
|
||||
public func calculateDuctSizes(_ projectID: Project.ID) async throws -> DuctSizes {
|
||||
.init(
|
||||
rooms: try await calculateRoomDuctSizes(projectID),
|
||||
trunks: try await calculateTrunkDuctSizes(projectID)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension ProjectClient: TestDependencyKey {
|
||||
@@ -60,12 +70,12 @@ extension ProjectClient {
|
||||
public struct FrictionRateResponse: Codable, Equatable, Sendable {
|
||||
|
||||
public let componentLosses: [ComponentPressureLoss]
|
||||
public let equivalentLengths: EffectiveLength.MaxContainer
|
||||
public let equivalentLengths: EquivalentLength.MaxContainer
|
||||
public let frictionRate: FrictionRate?
|
||||
|
||||
public init(
|
||||
componentLosses: [ComponentPressureLoss],
|
||||
equivalentLengths: EffectiveLength.MaxContainer,
|
||||
equivalentLengths: EquivalentLength.MaxContainer,
|
||||
frictionRate: FrictionRate? = nil
|
||||
) {
|
||||
self.componentLosses = componentLosses
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import DatabaseClient
|
||||
import ManualDCore
|
||||
|
||||
extension DatabaseClient.ComponentLoss {
|
||||
extension DatabaseClient.ComponentLosses {
|
||||
|
||||
func createDefaults(projectID: Project.ID) async throws {
|
||||
let defaults = ComponentPressureLoss.Create.default(projectID: projectID)
|
||||
|
||||
@@ -9,32 +9,18 @@ extension DatabaseClient {
|
||||
details: Project.Detail
|
||||
) async throws -> (DuctSizes, DuctSizeSharedRequest) {
|
||||
let (rooms, shared) = try await calculateRoomDuctSizes(details: details)
|
||||
let (trunks, _) = try await calculateTrunkDuctSizes(details: details)
|
||||
return (.init(rooms: rooms, trunks: trunks), shared)
|
||||
}
|
||||
|
||||
func calculateDuctSizes(
|
||||
projectID: Project.ID
|
||||
) async throws -> (DuctSizes, DuctSizeSharedRequest, [Room]) {
|
||||
@Dependency(\.manualD) var manualD
|
||||
|
||||
let shared = try await sharedDuctRequest(projectID)
|
||||
let rooms = try await rooms.fetch(projectID)
|
||||
|
||||
return try await (
|
||||
manualD.calculateDuctSizes(
|
||||
.init(
|
||||
rooms: rooms,
|
||||
trunks: trunkSizes.fetch(projectID),
|
||||
sharedRequest: shared
|
||||
trunks: calculateTrunkDuctSizes(details: details, shared: shared)
|
||||
),
|
||||
shared,
|
||||
rooms
|
||||
shared
|
||||
)
|
||||
}
|
||||
|
||||
func calculateRoomDuctSizes(
|
||||
details: Project.Detail
|
||||
) async throws -> ([DuctSizes.RoomContainer], DuctSizeSharedRequest) {
|
||||
) async throws -> (rooms: [DuctSizes.RoomContainer], shared: DuctSizeSharedRequest) {
|
||||
@Dependency(\.manualD) var manualD
|
||||
|
||||
let shared = try sharedDuctRequest(details: details)
|
||||
@@ -42,54 +28,29 @@ extension DatabaseClient {
|
||||
return (rooms, shared)
|
||||
}
|
||||
|
||||
func calculateRoomDuctSizes(
|
||||
projectID: Project.ID
|
||||
) async throws -> ([DuctSizes.RoomContainer], DuctSizeSharedRequest) {
|
||||
@Dependency(\.manualD) var manualD
|
||||
|
||||
let shared = try await sharedDuctRequest(projectID)
|
||||
|
||||
return try await (
|
||||
manualD.calculateRoomSizes(
|
||||
rooms: rooms.fetch(projectID),
|
||||
sharedRequest: shared
|
||||
),
|
||||
shared
|
||||
)
|
||||
}
|
||||
|
||||
func calculateTrunkDuctSizes(
|
||||
details: Project.Detail
|
||||
) async throws -> ([DuctSizes.TrunkContainer], DuctSizeSharedRequest) {
|
||||
details: Project.Detail,
|
||||
shared: DuctSizeSharedRequest? = nil
|
||||
) async throws -> [DuctSizes.TrunkContainer] {
|
||||
@Dependency(\.manualD) var manualD
|
||||
|
||||
let shared = try sharedDuctRequest(details: details)
|
||||
let trunks = try await manualD.calculateTrunkSizes(
|
||||
let sharedRequest: DuctSizeSharedRequest
|
||||
if let shared {
|
||||
sharedRequest = shared
|
||||
} else {
|
||||
sharedRequest = try sharedDuctRequest(details: details)
|
||||
}
|
||||
|
||||
return try await manualD.calculateTrunkSizes(
|
||||
rooms: details.rooms,
|
||||
trunks: details.trunks,
|
||||
sharedRequest: shared
|
||||
)
|
||||
return (trunks, shared)
|
||||
}
|
||||
|
||||
func calculateTrunkDuctSizes(
|
||||
projectID: Project.ID
|
||||
) async throws -> ([DuctSizes.TrunkContainer], DuctSizeSharedRequest) {
|
||||
@Dependency(\.manualD) var manualD
|
||||
|
||||
let shared = try await sharedDuctRequest(projectID)
|
||||
|
||||
return try await (
|
||||
manualD.calculateTrunkSizes(
|
||||
rooms: rooms.fetch(projectID),
|
||||
trunks: trunkSizes.fetch(projectID),
|
||||
sharedRequest: shared
|
||||
),
|
||||
shared
|
||||
sharedRequest: sharedRequest
|
||||
)
|
||||
}
|
||||
|
||||
func sharedDuctRequest(details: Project.Detail) throws -> DuctSizeSharedRequest {
|
||||
let projectSHR = try details.project.ensuredSHR()
|
||||
|
||||
guard
|
||||
let dfrResponse = designFrictionRate(
|
||||
componentLosses: details.componentLosses,
|
||||
@@ -100,10 +61,6 @@ extension DatabaseClient {
|
||||
throw ProjectClientError("Project not complete.")
|
||||
}
|
||||
|
||||
guard let projectSHR = details.project.sensibleHeatRatio else {
|
||||
throw ProjectClientError("Project sensible heat ratio not set.")
|
||||
}
|
||||
|
||||
let ensuredTEL = try dfrResponse.ensureMaxContainer()
|
||||
|
||||
return .init(
|
||||
@@ -115,40 +72,14 @@ extension DatabaseClient {
|
||||
)
|
||||
}
|
||||
|
||||
func sharedDuctRequest(_ projectID: Project.ID) async throws -> DuctSizeSharedRequest {
|
||||
|
||||
guard let dfrResponse = try await designFrictionRate(projectID: projectID) else {
|
||||
throw ProjectClientError("Project not complete.")
|
||||
}
|
||||
|
||||
let ensuredTEL = try dfrResponse.ensureMaxContainer()
|
||||
|
||||
return try await .init(
|
||||
equipmentInfo: dfrResponse.equipmentInfo,
|
||||
maxSupplyLength: ensuredTEL.supply,
|
||||
maxReturnLenght: ensuredTEL.return,
|
||||
designFrictionRate: dfrResponse.designFrictionRate,
|
||||
projectSHR: ensuredSHR(projectID)
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
// Fetches the project sensible heat ratio or throws an error if it's nil.
|
||||
func ensuredSHR(_ projectID: Project.ID) async throws -> Double {
|
||||
guard let projectSHR = try await projects.getSensibleHeatRatio(projectID) else {
|
||||
throw ProjectClientError("Project sensible heat ratio not set.")
|
||||
}
|
||||
return projectSHR
|
||||
}
|
||||
|
||||
// Internal container.
|
||||
struct DesignFrictionRateResponse: Equatable, Sendable {
|
||||
|
||||
typealias EnsuredTEL = (supply: EffectiveLength, return: EffectiveLength)
|
||||
typealias EnsuredTEL = (supply: EquivalentLength, return: EquivalentLength)
|
||||
|
||||
let designFrictionRate: Double
|
||||
let equipmentInfo: EquipmentInfo
|
||||
let telMaxContainer: EffectiveLength.MaxContainer
|
||||
let telMaxContainer: EquivalentLength.MaxContainer
|
||||
|
||||
func ensureMaxContainer() throws -> EnsuredTEL {
|
||||
|
||||
@@ -167,9 +98,9 @@ extension DatabaseClient {
|
||||
func designFrictionRate(
|
||||
componentLosses: [ComponentPressureLoss],
|
||||
equipmentInfo: EquipmentInfo,
|
||||
equivalentLengths: EffectiveLength.MaxContainer
|
||||
equivalentLengths: EquivalentLength.MaxContainer
|
||||
) -> DesignFrictionRateResponse? {
|
||||
guard let tel = equivalentLengths.total,
|
||||
guard let tel = equivalentLengths.totalEquivalentLength,
|
||||
componentLosses.count > 0
|
||||
else { return nil }
|
||||
|
||||
@@ -183,18 +114,13 @@ extension DatabaseClient {
|
||||
|
||||
}
|
||||
|
||||
func designFrictionRate(
|
||||
projectID: Project.ID
|
||||
) async throws -> DesignFrictionRateResponse? {
|
||||
}
|
||||
|
||||
guard let equipmentInfo = try await equipment.fetch(projectID) else {
|
||||
return nil
|
||||
extension Project {
|
||||
func ensuredSHR() throws -> Double {
|
||||
guard let shr = sensibleHeatRatio else {
|
||||
throw ProjectClientError("Sensible heat ratio not set on project id: \(id)")
|
||||
}
|
||||
|
||||
return try await designFrictionRate(
|
||||
componentLosses: componentLoss.fetch(projectID),
|
||||
equipmentInfo: equipmentInfo,
|
||||
equivalentLengths: effectiveLength.fetchMax(projectID)
|
||||
)
|
||||
return shr
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import DatabaseClient
|
||||
import Dependencies
|
||||
import ManualDClient
|
||||
import ManualDCore
|
||||
import PdfClient
|
||||
|
||||
extension DatabaseClient {
|
||||
|
||||
/// Generate a pdf request for the given project.
|
||||
func makePdfRequest(_ projectID: Project.ID) async throws -> PdfClient.Request {
|
||||
@Dependency(\.manualD) var manualD
|
||||
|
||||
guard let projectDetails = try await projects.detail(projectID) else {
|
||||
throw ProjectClientError.notFound(.project(projectID))
|
||||
}
|
||||
|
||||
let (ductSizes, shared) = try await calculateDuctSizes(details: projectDetails)
|
||||
|
||||
let frictionRateResponse = try await manualD.frictionRate(details: projectDetails)
|
||||
guard let frictionRate = frictionRateResponse.frictionRate else {
|
||||
throw ProjectClientError.notFound(.frictionRate(projectID))
|
||||
}
|
||||
|
||||
return .init(
|
||||
details: projectDetails,
|
||||
ductSizes: ductSizes,
|
||||
shared: shared,
|
||||
frictionRate: frictionRate
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension PdfClient.Request {
|
||||
fileprivate init(
|
||||
details: Project.Detail,
|
||||
ductSizes: DuctSizes,
|
||||
shared: DuctSizeSharedRequest,
|
||||
frictionRate: FrictionRate
|
||||
) {
|
||||
self.init(
|
||||
project: details.project,
|
||||
rooms: details.rooms,
|
||||
componentLosses: details.componentLosses,
|
||||
ductSizes: ductSizes,
|
||||
equipmentInfo: details.equipmentInfo,
|
||||
maxSupplyTEL: shared.maxSupplyLength,
|
||||
maxReturnTEL: shared.maxReturnLenght,
|
||||
frictionRate: frictionRate,
|
||||
projectSHR: shared.projectSHR
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,8 @@ import ManualDCore
|
||||
|
||||
struct DuctSizeSharedRequest {
|
||||
let equipmentInfo: EquipmentInfo
|
||||
let maxSupplyLength: EffectiveLength
|
||||
let maxReturnLenght: EffectiveLength
|
||||
let maxSupplyLength: EquivalentLength
|
||||
let maxReturnLenght: EquivalentLength
|
||||
let designFrictionRate: Double
|
||||
let projectSHR: Double
|
||||
}
|
||||
@@ -41,18 +41,19 @@ extension ManualDClient {
|
||||
|
||||
var retval: [DuctSizes.RoomContainer] = []
|
||||
let totalHeatingLoad = rooms.totalHeatingLoad
|
||||
let totalCoolingSensible = rooms.totalCoolingSensible(shr: sharedRequest.projectSHR)
|
||||
let totalCoolingSensible = try rooms.totalCoolingSensible(shr: sharedRequest.projectSHR)
|
||||
|
||||
for room in rooms {
|
||||
let heatingLoad = room.heatingLoadPerRegister
|
||||
let coolingLoad = room.coolingSensiblePerRegister(projectSHR: sharedRequest.projectSHR)
|
||||
let coolingLoad = try room.coolingSensiblePerRegister(projectSHR: sharedRequest.projectSHR)
|
||||
let heatingPercent = heatingLoad / totalHeatingLoad
|
||||
let coolingPercent = coolingLoad / totalCoolingSensible
|
||||
let heatingCFM = heatingPercent * Double(sharedRequest.equipmentInfo.heatingCFM)
|
||||
let coolingCFM = coolingPercent * Double(sharedRequest.equipmentInfo.coolingCFM)
|
||||
let designCFM = DuctSizes.DesignCFM(heating: heatingCFM, cooling: coolingCFM)
|
||||
let sizes = try await self.ductSize(
|
||||
.init(designCFM: Int(designCFM.value), frictionRate: sharedRequest.designFrictionRate)
|
||||
cfm: designCFM.value,
|
||||
frictionRate: sharedRequest.designFrictionRate
|
||||
)
|
||||
|
||||
for n in 1...room.registerCount {
|
||||
@@ -63,7 +64,8 @@ extension ManualDClient {
|
||||
|
||||
if let rectangularSize {
|
||||
let response = try await self.rectangularSize(
|
||||
.init(round: sizes.finalSize, height: rectangularSize.height)
|
||||
round: sizes.finalSize,
|
||||
height: rectangularSize.height
|
||||
)
|
||||
rectangularWidth = response.width
|
||||
}
|
||||
@@ -100,23 +102,25 @@ extension ManualDClient {
|
||||
|
||||
var retval = [DuctSizes.TrunkContainer]()
|
||||
let totalHeatingLoad = rooms.totalHeatingLoad
|
||||
let totalCoolingSensible = rooms.totalCoolingSensible(shr: sharedRequest.projectSHR)
|
||||
let totalCoolingSensible = try rooms.totalCoolingSensible(shr: sharedRequest.projectSHR)
|
||||
|
||||
for trunk in trunks {
|
||||
let heatingLoad = trunk.totalHeatingLoad
|
||||
let coolingLoad = trunk.totalCoolingSensible(projectSHR: sharedRequest.projectSHR)
|
||||
let coolingLoad = try trunk.totalCoolingSensible(projectSHR: sharedRequest.projectSHR)
|
||||
let heatingPercent = heatingLoad / totalHeatingLoad
|
||||
let coolingPercent = coolingLoad / totalCoolingSensible
|
||||
let heatingCFM = heatingPercent * Double(sharedRequest.equipmentInfo.heatingCFM)
|
||||
let coolingCFM = coolingPercent * Double(sharedRequest.equipmentInfo.coolingCFM)
|
||||
let designCFM = DuctSizes.DesignCFM(heating: heatingCFM, cooling: coolingCFM)
|
||||
let sizes = try await self.ductSize(
|
||||
.init(designCFM: Int(designCFM.value), frictionRate: sharedRequest.designFrictionRate)
|
||||
cfm: designCFM.value,
|
||||
frictionRate: sharedRequest.designFrictionRate
|
||||
)
|
||||
var width: Int? = nil
|
||||
if let height = trunk.height {
|
||||
let rectangularSize = try await self.rectangularSize(
|
||||
.init(round: sizes.finalSize, height: height)
|
||||
round: sizes.finalSize,
|
||||
height: height
|
||||
)
|
||||
width = rectangularSize.width
|
||||
}
|
||||
@@ -142,7 +146,7 @@ extension ManualDClient {
|
||||
extension DuctSizes.SizeContainer {
|
||||
init(
|
||||
designCFM: DuctSizes.DesignCFM,
|
||||
sizes: ManualDClient.DuctSizeResponse,
|
||||
sizes: ManualDClient.DuctSize,
|
||||
height: Int?,
|
||||
width: Int?
|
||||
) {
|
||||
@@ -160,7 +164,7 @@ extension DuctSizes.SizeContainer {
|
||||
|
||||
init(
|
||||
designCFM: DuctSizes.DesignCFM,
|
||||
sizes: ManualDClient.DuctSizeResponse,
|
||||
sizes: ManualDClient.DuctSize,
|
||||
rectangularSize: Room.RectangularSize?,
|
||||
width: Int?
|
||||
) {
|
||||
@@ -177,18 +181,18 @@ extension DuctSizes.SizeContainer {
|
||||
}
|
||||
}
|
||||
|
||||
extension Room {
|
||||
|
||||
var heatingLoadPerRegister: Double {
|
||||
|
||||
heatingLoad / Double(registerCount)
|
||||
}
|
||||
|
||||
func coolingSensiblePerRegister(projectSHR: Double) -> Double {
|
||||
let sensible = coolingSensible ?? (coolingTotal * projectSHR)
|
||||
return sensible / Double(registerCount)
|
||||
}
|
||||
}
|
||||
// extension Room {
|
||||
//
|
||||
// var heatingLoadPerRegister: Double {
|
||||
//
|
||||
// heatingLoad / Double(registerCount)
|
||||
// }
|
||||
//
|
||||
// func coolingSensiblePerRegister(projectSHR: Double) -> Double {
|
||||
// let sensible = coolingSensible ?? (coolingTotal * projectSHR)
|
||||
// return sensible / Double(registerCount)
|
||||
// }
|
||||
// }
|
||||
|
||||
extension TrunkSize.RoomProxy {
|
||||
|
||||
@@ -206,8 +210,8 @@ extension TrunkSize.RoomProxy {
|
||||
room.heatingLoadPerRegister * Double(actualRegisterCount)
|
||||
}
|
||||
|
||||
func totalCoolingSensible(projectSHR: Double) -> Double {
|
||||
room.coolingSensiblePerRegister(projectSHR: projectSHR) * Double(actualRegisterCount)
|
||||
func totalCoolingSensible(projectSHR: Double) throws -> Double {
|
||||
try room.coolingSensiblePerRegister(projectSHR: projectSHR) * Double(actualRegisterCount)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,7 +221,7 @@ extension TrunkSize {
|
||||
rooms.reduce(into: 0) { $0 += $1.totalHeatingLoad }
|
||||
}
|
||||
|
||||
func totalCoolingSensible(projectSHR: Double) -> Double {
|
||||
rooms.reduce(into: 0) { $0 += $1.totalCoolingSensible(projectSHR: projectSHR) }
|
||||
func totalCoolingSensible(projectSHR: Double) throws -> Double {
|
||||
try rooms.reduce(into: 0) { $0 += try $1.totalCoolingSensible(projectSHR: projectSHR) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ extension ManualDClient {
|
||||
func frictionRate(details: Project.Detail) async throws -> ProjectClient.FrictionRateResponse {
|
||||
|
||||
let maxContainer = details.maxContainer
|
||||
guard let totalEquivalentLength = maxContainer.total else {
|
||||
guard let totalEquivalentLength = maxContainer.totalEquivalentLength else {
|
||||
return .init(componentLosses: details.componentLosses, equivalentLengths: maxContainer)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ extension ManualDClient {
|
||||
.init(
|
||||
externalStaticPressure: details.equipmentInfo.staticPressure,
|
||||
componentPressureLosses: details.componentLosses,
|
||||
totalEffectiveLength: Int(totalEquivalentLength)
|
||||
totalEquivalentLength: Int(totalEquivalentLength)
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -28,15 +28,15 @@ extension ManualDClient {
|
||||
func frictionRate(projectID: Project.ID) async throws -> ProjectClient.FrictionRateResponse {
|
||||
@Dependency(\.database) var database
|
||||
|
||||
let componentLosses = try await database.componentLoss.fetch(projectID)
|
||||
let lengths = try await database.effectiveLength.fetchMax(projectID)
|
||||
let componentLosses = try await database.componentLosses.fetch(projectID)
|
||||
let lengths = try await database.equivalentLengths.fetchMax(projectID)
|
||||
|
||||
let equipmentInfo = try await database.equipment.fetch(projectID)
|
||||
guard let staticPressure = equipmentInfo?.staticPressure else {
|
||||
return .init(componentLosses: componentLosses, equivalentLengths: lengths)
|
||||
}
|
||||
|
||||
guard let totalEquivalentLength = lengths.total else {
|
||||
guard let totalEquivalentLength = lengths.totalEquivalentLength else {
|
||||
return .init(componentLosses: componentLosses, equivalentLengths: lengths)
|
||||
}
|
||||
|
||||
@@ -46,8 +46,8 @@ extension ManualDClient {
|
||||
frictionRate: frictionRate(
|
||||
.init(
|
||||
externalStaticPressure: staticPressure,
|
||||
componentPressureLosses: database.componentLoss.fetch(projectID),
|
||||
totalEffectiveLength: Int(totalEquivalentLength)
|
||||
componentPressureLosses: componentLosses,
|
||||
totalEquivalentLength: Int(totalEquivalentLength)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import ManualDCore
|
||||
|
||||
extension Project.Detail {
|
||||
var maxContainer: EffectiveLength.MaxContainer {
|
||||
var maxContainer: EquivalentLength.MaxContainer {
|
||||
.init(
|
||||
supply: equivalentLengths.filter({ $0.type == .supply })
|
||||
.sorted(by: { $0.totalEquivalentLength > $1.totalEquivalentLength })
|
||||
|
||||
@@ -15,18 +15,21 @@ extension ProjectClient: DependencyKey {
|
||||
@Dependency(\.fileClient) var fileClient
|
||||
|
||||
return .init(
|
||||
calculateDuctSizes: { projectID in
|
||||
try await database.calculateDuctSizes(projectID: projectID).0
|
||||
},
|
||||
calculateRoomDuctSizes: { projectID in
|
||||
try await database.calculateRoomDuctSizes(projectID: projectID).0
|
||||
guard let details = try await database.projects.detail(projectID) else {
|
||||
throw ProjectClientError.notFound(.project(projectID))
|
||||
}
|
||||
return try await database.calculateRoomDuctSizes(details: details).rooms
|
||||
},
|
||||
calculateTrunkDuctSizes: { projectID in
|
||||
try await database.calculateTrunkDuctSizes(projectID: projectID).0
|
||||
guard let details = try await database.projects.detail(projectID) else {
|
||||
throw ProjectClientError.notFound(.project(projectID))
|
||||
}
|
||||
return try await database.calculateTrunkDuctSizes(details: details)
|
||||
},
|
||||
createProject: { userID, request in
|
||||
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(
|
||||
projectID: project.id,
|
||||
rooms: database.rooms.fetch(project.id),
|
||||
@@ -42,13 +45,10 @@ extension ProjectClient: DependencyKey {
|
||||
request: database.makePdfRequest(projectID)
|
||||
)
|
||||
|
||||
let response = try await fileClient.streamFile(
|
||||
pdfResponse.pdfPath,
|
||||
{
|
||||
try await fileClient.removeFile(pdfResponse.htmlPath)
|
||||
try await fileClient.removeFile(pdfResponse.pdfPath)
|
||||
}
|
||||
)
|
||||
let response = try await fileClient.streamFile(at: pdfResponse.pdfPath) {
|
||||
try await fileClient.removeFile(pdfResponse.htmlPath)
|
||||
try await fileClient.removeFile(pdfResponse.pdfPath)
|
||||
}
|
||||
|
||||
response.headers.replaceOrAdd(name: .contentType, value: "application/octet-stream")
|
||||
response.headers.replaceOrAdd(
|
||||
@@ -61,50 +61,3 @@ extension ProjectClient: DependencyKey {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension DatabaseClient {
|
||||
|
||||
fileprivate func makePdfRequest(_ projectID: Project.ID) async throws -> PdfClient.Request {
|
||||
@Dependency(\.manualD) var manualD
|
||||
|
||||
guard let projectDetails = try await projects.detail(projectID) else {
|
||||
throw ProjectClientError("Project not found. id: \(projectID)")
|
||||
}
|
||||
|
||||
let (ductSizes, shared) = try await calculateDuctSizes(details: projectDetails)
|
||||
|
||||
let frictionRateResponse = try await manualD.frictionRate(details: projectDetails)
|
||||
guard let frictionRate = frictionRateResponse.frictionRate else {
|
||||
throw ProjectClientError("Friction rate not found. id: \(projectID)")
|
||||
}
|
||||
|
||||
return .init(
|
||||
details: projectDetails,
|
||||
ductSizes: ductSizes,
|
||||
shared: shared,
|
||||
frictionRate: frictionRate
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension PdfClient.Request {
|
||||
init(
|
||||
details: Project.Detail,
|
||||
ductSizes: DuctSizes,
|
||||
shared: DuctSizeSharedRequest,
|
||||
frictionRate: FrictionRate
|
||||
) {
|
||||
self.init(
|
||||
project: details.project,
|
||||
rooms: details.rooms,
|
||||
componentLosses: details.componentLosses,
|
||||
ductSizes: ductSizes,
|
||||
equipmentInfo: details.equipmentInfo,
|
||||
maxSupplyTEL: shared.maxSupplyLength,
|
||||
maxReturnTEL: shared.maxReturnLenght,
|
||||
frictionRate: frictionRate,
|
||||
projectSHR: shared.projectSHR
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
35
Sources/ProjectClient/ProjectClientError.swift
Normal file
35
Sources/ProjectClient/ProjectClientError.swift
Normal file
@@ -0,0 +1,35 @@
|
||||
import Foundation
|
||||
import ManualDCore
|
||||
|
||||
public struct ProjectClientError: Error {
|
||||
public let reason: String
|
||||
|
||||
public init(_ reason: String) {
|
||||
self.reason = reason
|
||||
}
|
||||
|
||||
static func notFound(_ notFound: NotFound) -> Self {
|
||||
.init(notFound.reason)
|
||||
}
|
||||
|
||||
enum NotFound {
|
||||
case project(Project.ID)
|
||||
case frictionRate(Project.ID)
|
||||
|
||||
var reason: String {
|
||||
switch self {
|
||||
case .project(let id):
|
||||
return "Project not found. id: \(id)"
|
||||
case .frictionRate(let id):
|
||||
return """
|
||||
Friction unable to be calculated. id: \(id)
|
||||
|
||||
This usually means that not all the required steps have been completed.
|
||||
|
||||
Calculating the friction rate requires the component pressure losses to be set and
|
||||
have a max equivalent length for both the supply and return.
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import Elementary
|
||||
import Tagged
|
||||
|
||||
public struct Badge<Inner: HTML>: HTML, Sendable where Inner: Sendable {
|
||||
|
||||
@@ -25,4 +26,5 @@ extension Badge where Inner == Number {
|
||||
public init(number: Double, digits: Int = 2) {
|
||||
self.inner = Number(number, digits: digits)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -26,32 +26,6 @@ public struct SubmitButton: HTML, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct CancelButton: HTML, Sendable {
|
||||
let title: String
|
||||
let type: HTMLAttribute<HTMLTag.button>.ButtonType
|
||||
|
||||
public init(
|
||||
title: String = "Cancel",
|
||||
type: HTMLAttribute<HTMLTag.button>.ButtonType = .button
|
||||
) {
|
||||
self.title = title
|
||||
self.type = type
|
||||
}
|
||||
|
||||
public var body: some HTML<HTMLTag.button> {
|
||||
button(
|
||||
.class(
|
||||
"""
|
||||
text-white font-bold text-xl bg-red-500 hover:bg-red-600 px-4 py-2 rounded-lg shadow-lg
|
||||
"""
|
||||
),
|
||||
.type(type)
|
||||
) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct EditButton: HTML, Sendable {
|
||||
let title: String?
|
||||
let type: HTMLAttribute<HTMLTag.button>.ButtonType
|
||||
|
||||
@@ -9,13 +9,6 @@ extension HTMLAttribute where Tag: HTMLTrait.Attributes.href {
|
||||
}
|
||||
}
|
||||
|
||||
extension HTMLAttribute where Tag == HTMLTag.form {
|
||||
|
||||
public static func action(route: SiteRoute.View) -> Self {
|
||||
action(SiteRoute.View.router.path(for: route))
|
||||
}
|
||||
}
|
||||
|
||||
extension HTMLAttribute where Tag == HTMLTag.input {
|
||||
|
||||
public static func value(_ string: String?) -> Self {
|
||||
@@ -42,9 +35,6 @@ extension HTMLAttribute where Tag == HTMLTag.button {
|
||||
}
|
||||
|
||||
extension HTML where Tag: HTMLTrait.Attributes.Global {
|
||||
public func badge() -> _AttributedElement<Self> {
|
||||
attributes(.class("badge badge-lg badge-outline"))
|
||||
}
|
||||
|
||||
public func hidden(when shouldHide: Bool) -> _AttributedElement<Self> {
|
||||
attributes(.class("hidden"), when: shouldHide)
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import Elementary
|
||||
|
||||
// TODO: Remove, using svg's.
|
||||
public struct Icon: HTML, Sendable {
|
||||
|
||||
let icon: String
|
||||
|
||||
public init(icon: String) {
|
||||
self.icon = icon
|
||||
}
|
||||
|
||||
public var body: some HTML {
|
||||
i(.data("lucide", value: icon)) {}
|
||||
}
|
||||
}
|
||||
|
||||
extension Icon {
|
||||
|
||||
public init(_ icon: Key) {
|
||||
self.init(icon: icon.icon)
|
||||
}
|
||||
|
||||
public enum Key: String {
|
||||
|
||||
case circlePlus
|
||||
case close
|
||||
case doorClosed
|
||||
case mapPin
|
||||
case rulerDimensionLine
|
||||
case squareFunction
|
||||
case wind
|
||||
|
||||
var icon: String {
|
||||
switch self {
|
||||
case .circlePlus: return "circle-plus"
|
||||
case .close: return "x"
|
||||
case .doorClosed: return "door-closed"
|
||||
case .mapPin: return "map-pin"
|
||||
case .rulerDimensionLine: return "ruler-dimension-line"
|
||||
case .squareFunction: return "square-function"
|
||||
case .wind: return rawValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,60 +21,6 @@ public struct LabeledInput: HTML, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct Input: HTML, Sendable {
|
||||
|
||||
let id: String?
|
||||
let name: String?
|
||||
let placeholder: String
|
||||
|
||||
private var _name: String {
|
||||
guard let name else {
|
||||
return id ?? ""
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
init(
|
||||
id: String? = nil,
|
||||
name: String? = nil,
|
||||
placeholder: String
|
||||
) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.placeholder = placeholder
|
||||
}
|
||||
|
||||
public init(
|
||||
id: String,
|
||||
name: String? = nil,
|
||||
placeholder: String
|
||||
) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.placeholder = placeholder
|
||||
}
|
||||
|
||||
public init(
|
||||
name: String,
|
||||
placeholder: String
|
||||
) {
|
||||
self.init(id: nil, name: name, placeholder: placeholder)
|
||||
}
|
||||
|
||||
public var body: some HTML<HTMLTag.input> {
|
||||
input(
|
||||
.id(id ?? ""), .name(_name), .placeholder(placeholder),
|
||||
.class(
|
||||
"""
|
||||
input w-full rounded-md bg-white px-3 py-1.5 text-slate-900 outline-1
|
||||
-outline-offset-1 outline-slate-300 focus:outline focus:-outline-offset-2
|
||||
focus:outline-indigo-600 invalid:border-red-500 out-of-range:border-red-500
|
||||
"""
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension HTMLAttribute where Tag == HTMLTag.input {
|
||||
|
||||
public static func max(_ value: String) -> Self {
|
||||
|
||||
@@ -6,15 +6,6 @@ public struct Number: HTML, Sendable {
|
||||
let fractionDigits: Int
|
||||
let value: Double
|
||||
|
||||
// private var formatter: NumberFormatter {
|
||||
// let formatter = NumberFormatter()
|
||||
// formatter.maximumFractionDigits = fractionDigits
|
||||
// formatter.numberStyle = .decimal
|
||||
// formatter.groupingSize = 3
|
||||
// formatter.groupingSeparator = ","
|
||||
// return formatter
|
||||
// }
|
||||
|
||||
public init(
|
||||
_ value: Double,
|
||||
digits fractionDigits: Int = 2
|
||||
|
||||
@@ -12,8 +12,8 @@ extension SiteRoute.View.ProjectRoute.EquivalentLengthRoute.StepThree {
|
||||
}
|
||||
}
|
||||
|
||||
var groups: [EffectiveLength.Group] {
|
||||
var groups = [EffectiveLength.Group]()
|
||||
var groups: [EquivalentLength.FittingGroup] {
|
||||
var groups = [EquivalentLength.FittingGroup]()
|
||||
for (n, group) in groupGroups.enumerated() {
|
||||
groups.append(
|
||||
.init(
|
||||
@@ -29,7 +29,7 @@ extension SiteRoute.View.ProjectRoute.EquivalentLengthRoute.StepThree {
|
||||
}
|
||||
}
|
||||
|
||||
extension EffectiveLength.Create {
|
||||
extension EquivalentLength.Create {
|
||||
|
||||
init(
|
||||
form: SiteRoute.View.ProjectRoute.EquivalentLengthRoute.StepThree,
|
||||
@@ -45,7 +45,7 @@ extension EffectiveLength.Create {
|
||||
}
|
||||
}
|
||||
|
||||
extension EffectiveLength.Update {
|
||||
extension EquivalentLength.Update {
|
||||
init(
|
||||
form: SiteRoute.View.ProjectRoute.EquivalentLengthRoute.StepThree,
|
||||
projectID: Project.ID
|
||||
|
||||
@@ -54,14 +54,14 @@ extension ViewController: DependencyKey {
|
||||
extension ViewController.Request {
|
||||
|
||||
func currentUser() throws -> User {
|
||||
@Dependency(\.authClient.currentUser) var currentUser
|
||||
@Dependency(\.auth.currentUser) var currentUser
|
||||
return try currentUser()
|
||||
}
|
||||
|
||||
func authenticate(
|
||||
_ login: User.Login
|
||||
) async throws -> User {
|
||||
@Dependency(\.authClient) var auth
|
||||
@Dependency(\.auth) var auth
|
||||
return try await auth.login(login)
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ extension ViewController.Request {
|
||||
func createAndAuthenticate(
|
||||
_ signup: User.Create
|
||||
) async throws -> User {
|
||||
@Dependency(\.authClient) var auth
|
||||
@Dependency(\.auth) var auth
|
||||
return try await auth.createAndLogin(signup)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import CSVParser
|
||||
import DatabaseClient
|
||||
import Dependencies
|
||||
import Elementary
|
||||
@@ -70,7 +71,7 @@ extension ViewController.Request {
|
||||
case .submitProfile(let profile):
|
||||
return await view {
|
||||
await ResultView {
|
||||
_ = try await database.userProfile.create(profile)
|
||||
_ = try await database.userProfiles.create(profile)
|
||||
let userID = profile.userID
|
||||
// let user = try currentUser()
|
||||
return (
|
||||
@@ -108,7 +109,7 @@ extension ViewController.Request {
|
||||
get async {
|
||||
@Dependency(\.database) var database
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,10 +285,18 @@ extension SiteRoute.View.ProjectRoute.RoomRoute {
|
||||
on request: ViewController.Request,
|
||||
projectID: Project.ID
|
||||
) async -> AnySendableHTML {
|
||||
@Dependency(\.csvParser) var csvParser
|
||||
@Dependency(\.database) var database
|
||||
|
||||
switch self {
|
||||
|
||||
case .csv(let csv):
|
||||
return await roomsView(on: request, projectID: projectID) {
|
||||
let rooms = try await csvParser.parseRooms(csv)
|
||||
_ = try await database.rooms.createMany(projectID, rooms)
|
||||
}
|
||||
// return EmptyHTML()
|
||||
|
||||
case .delete(let id):
|
||||
return await ResultView {
|
||||
try await database.rooms.delete(id)
|
||||
@@ -298,7 +307,7 @@ extension SiteRoute.View.ProjectRoute.RoomRoute {
|
||||
|
||||
case .submit(let form):
|
||||
return await roomsView(on: request, projectID: projectID) {
|
||||
_ = try await database.rooms.create(form)
|
||||
_ = try await database.rooms.create(projectID, form)
|
||||
}
|
||||
|
||||
case .update(let id, let form):
|
||||
@@ -366,8 +375,8 @@ extension SiteRoute.View.ProjectRoute.FrictionRateRoute {
|
||||
return await request.view {
|
||||
await ResultView {
|
||||
let equipment = try await database.equipment.fetch(projectID)
|
||||
let componentLosses = try await database.componentLoss.fetch(projectID)
|
||||
let lengths = try await database.effectiveLength.fetchMax(projectID)
|
||||
let componentLosses = try await database.componentLosses.fetch(projectID)
|
||||
let lengths = try await database.equivalentLengths.fetchMax(projectID)
|
||||
|
||||
return (
|
||||
try await database.projects.getCompletedSteps(projectID),
|
||||
@@ -407,16 +416,16 @@ extension SiteRoute.View.ProjectRoute.ComponentLossRoute {
|
||||
return EmptyHTML()
|
||||
case .delete(let id):
|
||||
return await view(on: request, projectID: projectID) {
|
||||
_ = try await database.componentLoss.delete(id)
|
||||
_ = try await database.componentLosses.delete(id)
|
||||
}
|
||||
case .submit(let form):
|
||||
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):
|
||||
return await view(on: request, projectID: projectID) {
|
||||
_ = try await database.componentLoss.update(id, form)
|
||||
_ = try await database.componentLosses.update(id, form)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -464,7 +473,7 @@ extension SiteRoute.View.ProjectRoute.EquivalentLengthRoute {
|
||||
|
||||
case .delete(let id):
|
||||
return await ResultView {
|
||||
try await database.effectiveLength.delete(id)
|
||||
try await database.equivalentLengths.delete(id)
|
||||
}
|
||||
|
||||
case .index:
|
||||
@@ -481,16 +490,16 @@ extension SiteRoute.View.ProjectRoute.EquivalentLengthRoute {
|
||||
|
||||
case .update(let id, let form):
|
||||
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):
|
||||
switch step {
|
||||
case .one(let stepOne):
|
||||
return await ResultView {
|
||||
var effectiveLength: EffectiveLength? = nil
|
||||
var effectiveLength: EquivalentLength? = nil
|
||||
if let id = stepOne.id {
|
||||
effectiveLength = try await database.effectiveLength.get(id)
|
||||
effectiveLength = try await database.equivalentLengths.get(id)
|
||||
}
|
||||
return effectiveLength
|
||||
} onSuccess: { effectiveLength in
|
||||
@@ -503,9 +512,9 @@ extension SiteRoute.View.ProjectRoute.EquivalentLengthRoute {
|
||||
case .two(let stepTwo):
|
||||
return await ResultView {
|
||||
request.logger.debug("ViewController: Got step two...")
|
||||
var effectiveLength: EffectiveLength? = nil
|
||||
var effectiveLength: EquivalentLength? = nil
|
||||
if let id = stepTwo.id {
|
||||
effectiveLength = try await database.effectiveLength.get(id)
|
||||
effectiveLength = try await database.equivalentLengths.get(id)
|
||||
}
|
||||
return effectiveLength
|
||||
} onSuccess: { effectiveLength in
|
||||
@@ -515,7 +524,7 @@ extension SiteRoute.View.ProjectRoute.EquivalentLengthRoute {
|
||||
}
|
||||
case .three(let stepThree):
|
||||
return await view(on: request, projectID: projectID) {
|
||||
_ = try await database.effectiveLength.create(
|
||||
_ = try await database.equivalentLengths.create(
|
||||
.init(form: stepThree, projectID: projectID)
|
||||
)
|
||||
}
|
||||
@@ -536,7 +545,7 @@ extension SiteRoute.View.ProjectRoute.EquivalentLengthRoute {
|
||||
try await catching()
|
||||
return (
|
||||
try await database.projects.getCompletedSteps(projectID),
|
||||
try await database.effectiveLength.fetch(projectID)
|
||||
try await database.equivalentLengths.fetch(projectID)
|
||||
)
|
||||
} onSuccess: { (steps, equivalentLengths) in
|
||||
ProjectView(projectID: projectID, activeTab: .equivalentLength, completedSteps: steps) {
|
||||
@@ -652,11 +661,11 @@ extension SiteRoute.View.UserRoute.Profile {
|
||||
return await view(on: request)
|
||||
case .submit(let form):
|
||||
return await view(on: request) {
|
||||
_ = try await database.userProfile.create(form)
|
||||
_ = try await database.userProfiles.create(form)
|
||||
}
|
||||
case .update(let id, let updates):
|
||||
return await view(on: request) {
|
||||
_ = try await database.userProfile.update(id, updates)
|
||||
_ = try await database.userProfiles.update(id, updates)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -673,7 +682,7 @@ extension SiteRoute.View.UserRoute.Profile {
|
||||
let user = try request.currentUser()
|
||||
return (
|
||||
user,
|
||||
try await database.userProfile.fetch(user.id)
|
||||
try await database.userProfiles.fetch(user.id)
|
||||
)
|
||||
} onSuccess: { (user, profile) in
|
||||
UserView(user: user, profile: profile)
|
||||
|
||||
@@ -7,7 +7,7 @@ import Styleguide
|
||||
|
||||
struct EffectiveLengthForm: HTML, Sendable {
|
||||
|
||||
static func id(_ equivalentLength: EffectiveLength?) -> String {
|
||||
static func id(_ equivalentLength: EquivalentLength?) -> String {
|
||||
let base = "equivalentLengthForm"
|
||||
guard let equivalentLength else { return base }
|
||||
return "\(base)_\(equivalentLength.id.uuidString.replacing("-", with: ""))"
|
||||
@@ -15,15 +15,15 @@ struct EffectiveLengthForm: HTML, Sendable {
|
||||
|
||||
let projectID: Project.ID
|
||||
let dismiss: Bool
|
||||
let type: EffectiveLength.EffectiveLengthType
|
||||
let effectiveLength: EffectiveLength?
|
||||
let type: EquivalentLength.EffectiveLengthType
|
||||
let effectiveLength: EquivalentLength?
|
||||
|
||||
var id: String { Self.id(effectiveLength) }
|
||||
|
||||
init(
|
||||
projectID: Project.ID,
|
||||
dismiss: Bool,
|
||||
type: EffectiveLength.EffectiveLengthType = .supply
|
||||
type: EquivalentLength.EffectiveLengthType = .supply
|
||||
) {
|
||||
self.projectID = projectID
|
||||
self.dismiss = dismiss
|
||||
@@ -32,7 +32,7 @@ struct EffectiveLengthForm: HTML, Sendable {
|
||||
}
|
||||
|
||||
init(
|
||||
effectiveLength: EffectiveLength
|
||||
effectiveLength: EquivalentLength
|
||||
) {
|
||||
self.dismiss = true
|
||||
self.type = effectiveLength.type
|
||||
@@ -55,7 +55,7 @@ struct EffectiveLengthForm: HTML, Sendable {
|
||||
|
||||
struct StepOne: HTML, Sendable {
|
||||
let projectID: Project.ID
|
||||
let effectiveLength: EffectiveLength?
|
||||
let effectiveLength: EquivalentLength?
|
||||
|
||||
var route: String {
|
||||
let baseRoute = SiteRoute.View.router.path(
|
||||
@@ -97,7 +97,7 @@ struct EffectiveLengthForm: HTML, Sendable {
|
||||
struct StepTwo: HTML, Sendable {
|
||||
let projectID: Project.ID
|
||||
let stepOne: SiteRoute.View.ProjectRoute.EquivalentLengthRoute.StepOne
|
||||
let effectiveLength: EffectiveLength?
|
||||
let effectiveLength: EquivalentLength?
|
||||
|
||||
var route: String {
|
||||
let baseRoute = SiteRoute.View.router.path(
|
||||
@@ -152,7 +152,7 @@ struct EffectiveLengthForm: HTML, Sendable {
|
||||
|
||||
struct StepThree: HTML, Sendable {
|
||||
let projectID: Project.ID
|
||||
let effectiveLength: EffectiveLength?
|
||||
let effectiveLength: EquivalentLength?
|
||||
let stepTwo: SiteRoute.View.ProjectRoute.EquivalentLengthRoute.StepTwo
|
||||
|
||||
var route: String {
|
||||
@@ -254,10 +254,10 @@ struct StraightLengthField: HTML, Sendable {
|
||||
|
||||
struct GroupField: HTML, Sendable {
|
||||
|
||||
let style: EffectiveLength.EffectiveLengthType
|
||||
let group: EffectiveLength.Group?
|
||||
let style: EquivalentLength.EffectiveLengthType
|
||||
let group: EquivalentLength.FittingGroup?
|
||||
|
||||
init(style: EffectiveLength.EffectiveLengthType, group: EffectiveLength.Group? = nil) {
|
||||
init(style: EquivalentLength.EffectiveLengthType, group: EquivalentLength.FittingGroup? = nil) {
|
||||
self.style = style
|
||||
self.group = group
|
||||
}
|
||||
@@ -307,7 +307,7 @@ struct GroupField: HTML, Sendable {
|
||||
|
||||
struct GroupSelect: HTML, Sendable {
|
||||
|
||||
let style: EffectiveLength.EffectiveLengthType
|
||||
let style: EquivalentLength.EffectiveLengthType
|
||||
|
||||
var body: some HTML {
|
||||
label(.class("select")) {
|
||||
@@ -328,13 +328,13 @@ struct GroupSelect: HTML, Sendable {
|
||||
struct GroupTypeSelect: HTML, Sendable {
|
||||
|
||||
let projectID: Project.ID
|
||||
let selected: EffectiveLength.EffectiveLengthType
|
||||
let selected: EquivalentLength.EffectiveLengthType
|
||||
|
||||
var body: some HTML<HTMLTag.label> {
|
||||
label(.class("select w-full")) {
|
||||
span(.class("label")) { "Type" }
|
||||
select(.name("type"), .id("type")) {
|
||||
for value in EffectiveLength.EffectiveLengthType.allCases {
|
||||
for value in EquivalentLength.EffectiveLengthType.allCases {
|
||||
option(
|
||||
.value("\(value.rawValue)"),
|
||||
) { value.title }
|
||||
@@ -345,7 +345,7 @@ struct GroupTypeSelect: HTML, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
extension EffectiveLength.EffectiveLengthType {
|
||||
extension EquivalentLength.EffectiveLengthType {
|
||||
|
||||
var title: String { rawValue.capitalized }
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ import Styleguide
|
||||
|
||||
struct EffectiveLengthsTable: HTML, Sendable {
|
||||
|
||||
let effectiveLengths: [EffectiveLength]
|
||||
let effectiveLengths: [EquivalentLength]
|
||||
|
||||
private var sortedLengths: [EffectiveLength] {
|
||||
private var sortedLengths: [EquivalentLength] {
|
||||
effectiveLengths.sorted {
|
||||
$0.totalEquivalentLength > $1.totalEquivalentLength
|
||||
}
|
||||
@@ -55,7 +55,7 @@ struct EffectiveLengthsTable: HTML, Sendable {
|
||||
|
||||
struct EffectiveLenghtRow: HTML, Sendable {
|
||||
|
||||
let effectiveLength: EffectiveLength
|
||||
let effectiveLength: EquivalentLength
|
||||
|
||||
private var deleteRoute: SiteRoute.View {
|
||||
.project(
|
||||
|
||||
@@ -7,14 +7,14 @@ struct EffectiveLengthsView: HTML, Sendable {
|
||||
|
||||
@Environment(ProjectViewValue.$projectID) var projectID
|
||||
|
||||
let effectiveLengths: [EffectiveLength]
|
||||
let effectiveLengths: [EquivalentLength]
|
||||
|
||||
var supplies: [EffectiveLength] {
|
||||
var supplies: [EquivalentLength] {
|
||||
effectiveLengths.filter({ $0.type == .supply })
|
||||
.sorted { $0.totalEquivalentLength > $1.totalEquivalentLength }
|
||||
}
|
||||
|
||||
var returns: [EffectiveLength] {
|
||||
var returns: [EquivalentLength] {
|
||||
effectiveLengths.filter({ $0.type == .return })
|
||||
.sorted { $0.totalEquivalentLength > $1.totalEquivalentLength }
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ struct FrictionRateView: HTML, Sendable {
|
||||
@Environment(ProjectViewValue.$projectID) var projectID
|
||||
|
||||
let componentLosses: [ComponentPressureLoss]
|
||||
let equivalentLengths: EffectiveLength.MaxContainer
|
||||
let equivalentLengths: EquivalentLength.MaxContainer
|
||||
let frictionRate: FrictionRate?
|
||||
|
||||
private var availableStaticPressure: Double? {
|
||||
|
||||
@@ -233,19 +233,19 @@ extension ManualDClient {
|
||||
func frictionRate(
|
||||
equipmentInfo: EquipmentInfo?,
|
||||
componentLosses: [ComponentPressureLoss],
|
||||
effectiveLength: EffectiveLength.MaxContainer
|
||||
effectiveLength: EquivalentLength.MaxContainer
|
||||
) async throws -> FrictionRate? {
|
||||
guard let staticPressure = equipmentInfo?.staticPressure else {
|
||||
return nil
|
||||
}
|
||||
guard let totalEquivalentLength = effectiveLength.total else {
|
||||
guard let totalEquivalentLength = effectiveLength.totalEquivalentLength else {
|
||||
return nil
|
||||
}
|
||||
return try await self.frictionRate(
|
||||
.init(
|
||||
externalStaticPressure: staticPressure,
|
||||
componentPressureLosses: componentLosses,
|
||||
totalEffectiveLength: Int(totalEquivalentLength)
|
||||
totalEquivalentLength: Int(totalEquivalentLength)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -47,8 +47,6 @@ struct RoomForm: HTML, Sendable {
|
||||
.hx.swap(.outerHTML)
|
||||
) {
|
||||
|
||||
input(.class("hidden"), .name("projectID"), .value("\(projectID)"))
|
||||
|
||||
if let id = room?.id {
|
||||
input(.class("hidden"), .name("id"), .value("\(id)"))
|
||||
}
|
||||
@@ -73,14 +71,15 @@ struct RoomForm: HTML, Sendable {
|
||||
.value(room?.heatingLoad)
|
||||
)
|
||||
|
||||
// TODO: Add description that only one is required (cooling total or sensible)
|
||||
|
||||
LabeledInput(
|
||||
"Cooling Total",
|
||||
.name("coolingTotal"),
|
||||
.type(.number),
|
||||
.placeholder("1234"),
|
||||
.required,
|
||||
.placeholder("1234 (Optional)"),
|
||||
.min("0"),
|
||||
.value(room?.coolingTotal)
|
||||
.value(room?.coolingLoad.total)
|
||||
)
|
||||
|
||||
LabeledInput(
|
||||
@@ -89,7 +88,7 @@ struct RoomForm: HTML, Sendable {
|
||||
.type(.number),
|
||||
.placeholder("1234 (Optional)"),
|
||||
.min("0"),
|
||||
.value(room?.coolingSensible)
|
||||
.value(room?.coolingLoad.sensible)
|
||||
)
|
||||
|
||||
LabeledInput(
|
||||
|
||||
@@ -11,6 +11,11 @@ struct RoomsView: HTML, Sendable {
|
||||
let rooms: [Room]
|
||||
let sensibleHeatRatio: Double?
|
||||
|
||||
private var csvRoute: String {
|
||||
SiteRoute.router.path(for: .view(.project(.detail(projectID, .rooms(.index)))))
|
||||
.appendingPath("csv")
|
||||
}
|
||||
|
||||
var body: some HTML {
|
||||
div(.class("flex w-full flex-col")) {
|
||||
PageTitleRow {
|
||||
@@ -44,6 +49,18 @@ struct RoomsView: HTML, Sendable {
|
||||
.attributes(.class("border border-error"), when: sensibleHeatRatio == nil)
|
||||
}
|
||||
.attributes(.class("tooltip-open"), when: sensibleHeatRatio == nil)
|
||||
|
||||
Tooltip("Upload csv file", position: .left) {
|
||||
form(
|
||||
.hx.post(csvRoute),
|
||||
.hx.target("body"),
|
||||
.hx.swap(.outerHTML),
|
||||
.custom(name: "enctype", value: "multipart/form-data")
|
||||
) {
|
||||
input(.type(.file), .name("file"), .accept(".csv"))
|
||||
SubmitButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div(.class("flex items-end space-x-4 font-bold")) {
|
||||
@@ -54,13 +71,15 @@ struct RoomsView: HTML, Sendable {
|
||||
|
||||
div(.class("flex justify-center items-end space-x-4 my-auto font-bold")) {
|
||||
span(.class("text-lg")) { "Cooling Total" }
|
||||
Badge(number: rooms.totalCoolingLoad, digits: 0)
|
||||
// TODO: ResultView ??
|
||||
Badge(number: try! rooms.totalCoolingLoad(shr: sensibleHeatRatio ?? 1.0), digits: 0)
|
||||
.attributes(.class("badge-success"))
|
||||
}
|
||||
|
||||
div(.class("flex grow justify-end items-end space-x-4 me-4 my-auto font-bold")) {
|
||||
span(.class("text-lg")) { "Cooling Sensible" }
|
||||
Badge(number: rooms.totalCoolingSensible(shr: sensibleHeatRatio ?? 1.0), digits: 0)
|
||||
// TODO: ResultView ??
|
||||
Badge(number: try! rooms.totalCoolingSensible(shr: sensibleHeatRatio ?? 1.0), digits: 0)
|
||||
.attributes(.class("badge-info"))
|
||||
}
|
||||
}
|
||||
@@ -125,10 +144,11 @@ struct RoomsView: HTML, Sendable {
|
||||
let shr: Double
|
||||
|
||||
var coolingSensible: Double {
|
||||
guard let value = room.coolingSensible else {
|
||||
return room.coolingTotal * shr
|
||||
}
|
||||
return value
|
||||
try! room.coolingLoad.ensured(shr: shr).sensible
|
||||
// guard let value = room.coolingSensible else {
|
||||
// return room.coolingTotal * shr
|
||||
// }
|
||||
// return value
|
||||
}
|
||||
|
||||
init(room: Room, shr: Double?) {
|
||||
@@ -147,7 +167,7 @@ struct RoomsView: HTML, Sendable {
|
||||
}
|
||||
td {
|
||||
div(.class("flex justify-center")) {
|
||||
Number(room.coolingTotal, digits: 0)
|
||||
Number(try! room.coolingLoad.ensured(shr: shr).total, digits: 0)
|
||||
// .attributes(.class("text-success"))
|
||||
}
|
||||
}
|
||||
|
||||
17
TODO.md
17
TODO.md
@@ -1,9 +1,18 @@
|
||||
# TODO's
|
||||
|
||||
- [x] Fix theme not working when selected upon signup.
|
||||
- [ ] Pdf generation
|
||||
- [ ] Add postgres / mysql support
|
||||
- [x] Pdf generation
|
||||
- [x] Add postgres / mysql support
|
||||
- [ ] Opensource / license ??
|
||||
- [ ] Figure out domain to host (currently thinking ductcalc.pro)
|
||||
- [ ] Logo / navbar name may have to change if it's not duct-calc.
|
||||
- [ ] MainPage meta items will have to change also
|
||||
- [ ] Logo / navbar name may have to change if it's not duct-calc.
|
||||
- [ ] MainPage meta items will have to change also
|
||||
- [ ] Add ability for either sensible or total load while specifying a room load.
|
||||
- CoolCalc current version specifies the sensible cooling for a room break down,
|
||||
and currently we require the total load and calculate sensible based on project
|
||||
shr.
|
||||
- [ ] Add ability to associate room load / airflow with another room.
|
||||
- [ ] Trunk size form, room / register selection is wonky when labels are long.
|
||||
- They will overlap each other making it difficult to read / decipher which checkbox belongs
|
||||
to which label.
|
||||
- [ ] Add select all rooms for trunks, useful for sizing main supply or return trunks.
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
import Dependencies
|
||||
import Foundation
|
||||
import ManualDCore
|
||||
import Testing
|
||||
import URLRouting
|
||||
|
||||
@Suite("ProjectRouteTests")
|
||||
struct ProjectRouteTests {
|
||||
let router = SiteRoute.Api.router
|
||||
|
||||
@Test
|
||||
func create() throws {
|
||||
let json = """
|
||||
{
|
||||
\"name\": \"Test\",
|
||||
\"streetAddress\": \"1234 Seasme Street\",
|
||||
\"city\": \"Nowhere\",
|
||||
\"state\": \"OH\",
|
||||
\"zipCode\": \"55555\"
|
||||
}
|
||||
"""
|
||||
var request = URLRequestData(
|
||||
method: "POST",
|
||||
path: "/api/v1/projects",
|
||||
body: .init(json.utf8)
|
||||
)
|
||||
let route = try router.parse(&request)
|
||||
#expect(
|
||||
route
|
||||
== .project(
|
||||
.create(
|
||||
.init(
|
||||
name: "Test",
|
||||
streetAddress: "1234 Seasme Street",
|
||||
city: "Nowhere",
|
||||
state: "OH",
|
||||
zipCode: "55555"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
func delete() throws {
|
||||
let id = UUID(0)
|
||||
var request = URLRequestData(
|
||||
method: "DELETE",
|
||||
path: "/api/v1/projects/\(id)"
|
||||
)
|
||||
let route = try router.parse(&request)
|
||||
#expect(route == .project(.delete(id: id)))
|
||||
}
|
||||
|
||||
@Test
|
||||
func get() throws {
|
||||
let id = UUID(0)
|
||||
var request = URLRequestData(
|
||||
method: "GET",
|
||||
path: "/api/v1/projects/\(id)"
|
||||
)
|
||||
let route = try router.parse(&request)
|
||||
#expect(route == .project(.get(id: id)))
|
||||
}
|
||||
|
||||
@Test
|
||||
func index() throws {
|
||||
var request = URLRequestData(
|
||||
method: "GET",
|
||||
path: "/api/v1/projects"
|
||||
)
|
||||
let route = try router.parse(&request)
|
||||
#expect(route == .project(.index))
|
||||
}
|
||||
|
||||
@Test
|
||||
func formData() throws {
|
||||
let p = Body {
|
||||
FormData {
|
||||
Optionally {
|
||||
Field("id", default: nil) { EffectiveLength.ID.parser() }
|
||||
}
|
||||
Field("name", .string)
|
||||
Field("type") { EffectiveLength.EffectiveLengthType.parser() }
|
||||
Many {
|
||||
Field("straightLengths") {
|
||||
Int.parser()
|
||||
}
|
||||
}
|
||||
}
|
||||
.map(.memberwise(SiteRoute.View.ProjectRoute.EquivalentLengthRoute.StepTwo.init))
|
||||
}
|
||||
|
||||
var request = URLRequestData(
|
||||
body: .init(
|
||||
"name=Test&type=supply&straightLengths=20&straightLengths=10"
|
||||
.utf8
|
||||
)
|
||||
)
|
||||
let value = try p.parse(&request)
|
||||
print(value)
|
||||
#expect(value.straightLengths == [20, 10])
|
||||
}
|
||||
}
|
||||
22
Tests/CSVParsingTests/CSVParsingTests.swift
Normal file
22
Tests/CSVParsingTests/CSVParsingTests.swift
Normal file
@@ -0,0 +1,22 @@
|
||||
import CSVParser
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@Suite
|
||||
struct CSVParsingTests {
|
||||
|
||||
@Test
|
||||
func roomParsing() async throws {
|
||||
|
||||
let parser = CSVParser.liveValue
|
||||
|
||||
let input = """
|
||||
Name,Heating Load,Cooling Total,Cooling Sensible,Register Count
|
||||
Bed-1,12345,12345,,2
|
||||
Bed-2,1223,,1123,1
|
||||
"""
|
||||
let rooms = try await parser.parseRooms(.init(file: Data(input.utf8)))
|
||||
|
||||
#expect(rooms.count == 2)
|
||||
}
|
||||
}
|
||||
68
Tests/DatabaseClientTests/ComponentLossTests.swift
Normal file
68
Tests/DatabaseClientTests/ComponentLossTests.swift
Normal file
@@ -0,0 +1,68 @@
|
||||
import Dependencies
|
||||
import Foundation
|
||||
import ManualDCore
|
||||
import Testing
|
||||
|
||||
@testable import DatabaseClient
|
||||
|
||||
@Suite
|
||||
struct ComponentLossTests {
|
||||
|
||||
@Test
|
||||
func happyPaths() async throws {
|
||||
try await withTestUserAndProject { user, project in
|
||||
@Dependency(\.database) var database
|
||||
|
||||
// let project = try await database.projects.create(user.id, .mock)
|
||||
|
||||
let componentLoss = try await database.componentLosses.create(
|
||||
.init(projectID: project.id, name: "Test", value: 0.2)
|
||||
)
|
||||
|
||||
let fetched = try await database.componentLosses.fetch(project.id)
|
||||
#expect(fetched == [componentLoss])
|
||||
|
||||
let got = try await database.componentLosses.get(componentLoss.id)
|
||||
#expect(got == componentLoss)
|
||||
|
||||
let updated = try await database.componentLosses.update(
|
||||
componentLoss.id, .init(name: "Updated", value: nil)
|
||||
)
|
||||
#expect(updated.id == componentLoss.id)
|
||||
#expect(updated.value == componentLoss.value)
|
||||
#expect(updated.name == "Updated")
|
||||
|
||||
try await database.componentLosses.delete(componentLoss.id)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func notFound() async throws {
|
||||
try await withDatabase {
|
||||
@Dependency(\.database.componentLosses) var componentLosses
|
||||
|
||||
await #expect(throws: NotFoundError.self) {
|
||||
try await componentLosses.delete(UUID(0))
|
||||
}
|
||||
|
||||
await #expect(throws: NotFoundError.self) {
|
||||
try await componentLosses.update(UUID(0), .init(name: "Updated"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test(
|
||||
arguments: [
|
||||
ComponentLossModel(name: "", value: 0.2, projectID: UUID(0)),
|
||||
ComponentLossModel(name: "Foo", value: -0.2, projectID: UUID(0)),
|
||||
ComponentLossModel(name: "Foo", value: 1.2, projectID: UUID(0)),
|
||||
ComponentLossModel(name: "", value: -0.2, projectID: UUID(0)),
|
||||
]
|
||||
)
|
||||
func validations(model: ComponentLossModel) {
|
||||
#expect(throws: (any Error).self) {
|
||||
try model.validate()
|
||||
}
|
||||
}
|
||||
}
|
||||
69
Tests/DatabaseClientTests/EquipmentTests.swift
Normal file
69
Tests/DatabaseClientTests/EquipmentTests.swift
Normal file
@@ -0,0 +1,69 @@
|
||||
import Dependencies
|
||||
import Foundation
|
||||
import ManualDCore
|
||||
import Testing
|
||||
|
||||
@testable import DatabaseClient
|
||||
|
||||
@Suite
|
||||
struct EquipmentTests {
|
||||
|
||||
@Test
|
||||
func happyPath() async throws {
|
||||
try await withTestUserAndProject { user, project in
|
||||
@Dependency(\.database) var database
|
||||
|
||||
let equipment = try await database.equipment.create(
|
||||
.init(projectID: project.id, heatingCFM: 1000, coolingCFM: 1000)
|
||||
)
|
||||
|
||||
let fetched = try await database.equipment.fetch(project.id)
|
||||
#expect(fetched == equipment)
|
||||
|
||||
let got = try await database.equipment.get(equipment.id)
|
||||
#expect(got == equipment)
|
||||
|
||||
let updated = try await database.equipment.update(
|
||||
equipment.id, .init(heatingCFM: 900)
|
||||
)
|
||||
#expect(updated.heatingCFM == 900)
|
||||
#expect(updated.id == equipment.id)
|
||||
|
||||
try await database.equipment.delete(equipment.id)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func notFound() async throws {
|
||||
try await withTestUserAndProject { _, project in
|
||||
@Dependency(\.database.equipment) var equipment
|
||||
|
||||
let fetched = try await equipment.fetch(project.id)
|
||||
#expect(fetched == nil)
|
||||
|
||||
await #expect(throws: NotFoundError.self) {
|
||||
try await equipment.delete(UUID(0))
|
||||
}
|
||||
|
||||
await #expect(throws: NotFoundError.self) {
|
||||
try await equipment.update(UUID(0), .init(staticPressure: 0.3))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test(
|
||||
arguments: [
|
||||
EquipmentModel(staticPressure: -1, heatingCFM: 1000, coolingCFM: 1000, projectID: UUID(0)),
|
||||
EquipmentModel(staticPressure: 0.5, heatingCFM: -1, coolingCFM: 1000, projectID: UUID(0)),
|
||||
EquipmentModel(staticPressure: 0.5, heatingCFM: 1000, coolingCFM: -1000, projectID: UUID(0)),
|
||||
EquipmentModel(staticPressure: 1.1, heatingCFM: 1000, coolingCFM: -1000, projectID: UUID(0)),
|
||||
]
|
||||
)
|
||||
func validations(model: EquipmentModel) {
|
||||
#expect(throws: (any Error).self) {
|
||||
try model.validate()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
123
Tests/DatabaseClientTests/EquivalentLengthTests.swift
Normal file
123
Tests/DatabaseClientTests/EquivalentLengthTests.swift
Normal file
@@ -0,0 +1,123 @@
|
||||
import Dependencies
|
||||
import Foundation
|
||||
import ManualDCore
|
||||
import Testing
|
||||
|
||||
@testable import DatabaseClient
|
||||
|
||||
@Suite
|
||||
struct EquivalentLengthTests {
|
||||
|
||||
@Test
|
||||
func happyPath() async throws {
|
||||
try await withTestUserAndProject { user, project in
|
||||
@Dependency(\.database.equivalentLengths) var equivalentLengths
|
||||
|
||||
let equivalentLength = try await equivalentLengths.create(
|
||||
.init(
|
||||
projectID: project.id,
|
||||
name: "Test",
|
||||
type: .supply,
|
||||
straightLengths: [10],
|
||||
groups: [
|
||||
.init(group: 1, letter: "a", value: 20),
|
||||
.init(group: 2, letter: "a", value: 30, quantity: 2),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
let fetched = try await equivalentLengths.fetch(project.id)
|
||||
#expect(fetched == [equivalentLength])
|
||||
|
||||
let got = try await equivalentLengths.get(equivalentLength.id)
|
||||
#expect(got == equivalentLength)
|
||||
|
||||
var max = try await equivalentLengths.fetchMax(project.id)
|
||||
#expect(max.supply == equivalentLength)
|
||||
#expect(max.return == nil)
|
||||
|
||||
let returnLength = try await equivalentLengths.create(
|
||||
.init(
|
||||
projectID: project.id,
|
||||
name: "Test",
|
||||
type: .return,
|
||||
straightLengths: [10],
|
||||
groups: [
|
||||
.init(group: 1, letter: "a", value: 20),
|
||||
.init(group: 2, letter: "a", value: 30, quantity: 2),
|
||||
]
|
||||
)
|
||||
)
|
||||
max = try await equivalentLengths.fetchMax(project.id)
|
||||
#expect(max.supply == equivalentLength)
|
||||
#expect(max.return == returnLength)
|
||||
|
||||
let updated = try await equivalentLengths.update(
|
||||
equivalentLength.id, .init(name: "Supply Test")
|
||||
)
|
||||
#expect(updated.name == "Supply Test")
|
||||
#expect(updated.id == equivalentLength.id)
|
||||
|
||||
try await equivalentLengths.delete(equivalentLength.id)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func notFound() async throws {
|
||||
try await withDatabase {
|
||||
@Dependency(\.database.equivalentLengths) var equivalentLengths
|
||||
|
||||
await #expect(throws: NotFoundError.self) {
|
||||
try await equivalentLengths.delete(UUID(0))
|
||||
}
|
||||
|
||||
await #expect(throws: NotFoundError.self) {
|
||||
try await equivalentLengths.update(UUID(0), .init())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test(
|
||||
arguments: [
|
||||
EquivalentLength.Create(
|
||||
projectID: UUID(0), name: "", type: .return, straightLengths: [], groups: []
|
||||
),
|
||||
EquivalentLength.Create(
|
||||
projectID: UUID(0), name: "Testy", type: .return, straightLengths: [-1, 1], groups: []
|
||||
),
|
||||
EquivalentLength.Create(
|
||||
projectID: UUID(0), name: "Testy", type: .return, straightLengths: [1, -1], groups: []
|
||||
),
|
||||
EquivalentLength.Create(
|
||||
projectID: UUID(0), name: "Testy", type: .return, straightLengths: [1, 1],
|
||||
groups: [
|
||||
.init(group: -1, letter: "a", value: 1.0, quantity: 1)
|
||||
]
|
||||
),
|
||||
EquivalentLength.Create(
|
||||
projectID: UUID(0), name: "Testy", type: .return, straightLengths: [1, 1],
|
||||
groups: [
|
||||
.init(group: 1, letter: "1", value: 1.0, quantity: 1)
|
||||
]
|
||||
),
|
||||
EquivalentLength.Create(
|
||||
projectID: UUID(0), name: "Testy", type: .return, straightLengths: [1, 1],
|
||||
groups: [
|
||||
.init(group: 1, letter: "a", value: -1.0, quantity: 1)
|
||||
]
|
||||
),
|
||||
EquivalentLength.Create(
|
||||
projectID: UUID(0), name: "Testy", type: .return, straightLengths: [1, 1],
|
||||
groups: [
|
||||
.init(group: 1, letter: "a", value: 1.0, quantity: -1)
|
||||
]
|
||||
),
|
||||
]
|
||||
)
|
||||
func validations(model: EquivalentLength.Create) {
|
||||
#expect(throws: (any Error).self) {
|
||||
try model.toModel().validate()
|
||||
}
|
||||
}
|
||||
}
|
||||
83
Tests/DatabaseClientTests/Helpers.swift
Normal file
83
Tests/DatabaseClientTests/Helpers.swift
Normal file
@@ -0,0 +1,83 @@
|
||||
import App
|
||||
import DatabaseClient
|
||||
import Dependencies
|
||||
import Fluent
|
||||
import FluentSQLiteDriver
|
||||
import Foundation
|
||||
import ManualDCore
|
||||
import NIO
|
||||
import Vapor
|
||||
|
||||
// Helper to create an in-memory database used for testing.
|
||||
func withDatabase(
|
||||
setupDependencies: (inout DependencyValues) -> Void = { _ in },
|
||||
operation: () async throws -> Void
|
||||
) async throws {
|
||||
let app = try await Application.make(.testing)
|
||||
do {
|
||||
try await configure(app, in: .live())
|
||||
let database = app.db
|
||||
try await app.autoMigrate()
|
||||
|
||||
try await withDependencies {
|
||||
$0.uuid = .incrementing
|
||||
$0.date = .init { Date() }
|
||||
$0.database = .live(database: database)
|
||||
setupDependencies(&$0)
|
||||
} operation: {
|
||||
try await operation()
|
||||
}
|
||||
|
||||
try await app.autoRevert()
|
||||
try await app.asyncShutdown()
|
||||
|
||||
} catch {
|
||||
try? await app.autoRevert()
|
||||
try await app.asyncShutdown()
|
||||
throw error
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Set's up the database and a test user for running tests that require a
|
||||
/// a user.
|
||||
func withTestUser(
|
||||
setupDependencies: (inout DependencyValues) -> Void = { _ in },
|
||||
operation: (User) async throws -> Void
|
||||
) async throws {
|
||||
try await withDatabase(setupDependencies: setupDependencies) {
|
||||
@Dependency(\.database.users) var users
|
||||
let user = try await users.create(
|
||||
.init(email: "testy@example.com", password: "super-secret", confirmPassword: "super-secret")
|
||||
)
|
||||
try await operation(user)
|
||||
}
|
||||
}
|
||||
|
||||
/// Set's up the database and a test user for running tests that require a
|
||||
/// a user.
|
||||
func withTestUserAndProject(
|
||||
setupDependencies: (inout DependencyValues) -> Void = { _ in },
|
||||
operation: (User, Project) async throws -> Void
|
||||
) async throws {
|
||||
try await withDatabase(setupDependencies: setupDependencies) {
|
||||
@Dependency(\.database) var database
|
||||
let user = try await database.users.create(
|
||||
.init(email: "testy@example.com", password: "super-secret", confirmPassword: "super-secret")
|
||||
)
|
||||
let project = try await database.projects.create(user.id, .mock)
|
||||
try await operation(user, project)
|
||||
}
|
||||
}
|
||||
|
||||
extension Project.Create {
|
||||
|
||||
static let mock = Self(
|
||||
name: "Testy McTestface",
|
||||
streetAddress: "1234 Sesame St",
|
||||
city: "Nowhere",
|
||||
state: "MN",
|
||||
zipCode: "55555",
|
||||
sensibleHeatRatio: 0.83
|
||||
)
|
||||
}
|
||||
206
Tests/DatabaseClientTests/ProjectTests.swift
Normal file
206
Tests/DatabaseClientTests/ProjectTests.swift
Normal file
@@ -0,0 +1,206 @@
|
||||
import Dependencies
|
||||
import Fluent
|
||||
import FluentSQLiteDriver
|
||||
import ManualDCore
|
||||
import Testing
|
||||
import Vapor
|
||||
|
||||
@testable import DatabaseClient
|
||||
|
||||
@Suite
|
||||
struct ProjectTests {
|
||||
|
||||
@Test
|
||||
func projectHappyPaths() async throws {
|
||||
try await withTestUser { user in
|
||||
@Dependency(\.database.projects) var projects
|
||||
|
||||
let project = try await projects.create(user.id, .mock)
|
||||
|
||||
let got = try await projects.get(project.id)
|
||||
#expect(got == project)
|
||||
|
||||
let page = try await projects.fetch(user.id, .init(page: 1, per: 25))
|
||||
#expect(page.items.first! == project)
|
||||
|
||||
let updated = try await projects.update(project.id, .init(sensibleHeatRatio: 0.83))
|
||||
#expect(updated.sensibleHeatRatio == 0.83)
|
||||
#expect(updated.id == project.id)
|
||||
|
||||
let shr = try await projects.getSensibleHeatRatio(project.id)
|
||||
#expect(shr == 0.83)
|
||||
|
||||
try await projects.delete(project.id)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
func notFound() async throws {
|
||||
try await withDatabase {
|
||||
@Dependency(\.database.projects) var projects
|
||||
|
||||
await #expect(throws: NotFoundError.self) {
|
||||
try await projects.delete(UUID(0))
|
||||
}
|
||||
|
||||
await #expect(throws: NotFoundError.self) {
|
||||
try await projects.update(UUID(0), .init(name: "Foo"))
|
||||
}
|
||||
|
||||
await #expect(throws: NotFoundError.self) {
|
||||
try await projects.getSensibleHeatRatio(UUID(0))
|
||||
}
|
||||
|
||||
await #expect(throws: NotFoundError.self) {
|
||||
try await projects.getCompletedSteps(UUID(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func completedSteps() async throws {
|
||||
try await withTestUser { user in
|
||||
@Dependency(\.database) var database
|
||||
|
||||
let project = try await database.projects.create(user.id, .mock)
|
||||
|
||||
var completed = try await database.projects.getCompletedSteps(project.id)
|
||||
#expect(completed.equipmentInfo == false)
|
||||
#expect(completed.equivalentLength == false)
|
||||
#expect(completed.frictionRate == false)
|
||||
#expect(completed.rooms == false)
|
||||
|
||||
_ = try await database.equipment.create(
|
||||
.init(projectID: project.id, heatingCFM: 1000, coolingCFM: 1000)
|
||||
)
|
||||
completed = try await database.projects.getCompletedSteps(project.id)
|
||||
#expect(completed.equipmentInfo == true)
|
||||
|
||||
_ = try await database.componentLosses.create(
|
||||
.init(projectID: project.id, name: "Test", value: 0.2)
|
||||
)
|
||||
completed = try await database.projects.getCompletedSteps(project.id)
|
||||
#expect(completed.frictionRate == true)
|
||||
|
||||
_ = try await database.rooms.create(
|
||||
project.id,
|
||||
.init(name: "Test", heatingLoad: 12345, coolingTotal: 12345)
|
||||
)
|
||||
completed = try await database.projects.getCompletedSteps(project.id)
|
||||
#expect(completed.rooms == true)
|
||||
|
||||
_ = try await database.equivalentLengths.create(
|
||||
.init(
|
||||
projectID: project.id, name: "Supply", type: .supply, straightLengths: [1], groups: [])
|
||||
)
|
||||
completed = try await database.projects.getCompletedSteps(project.id)
|
||||
// Should not be complete until we have both return and supply for a project.
|
||||
#expect(completed.equivalentLength == false)
|
||||
|
||||
_ = try await database.equivalentLengths.create(
|
||||
.init(
|
||||
projectID: project.id, name: "Return", type: .return, straightLengths: [1], groups: [])
|
||||
)
|
||||
completed = try await database.projects.getCompletedSteps(project.id)
|
||||
#expect(completed.equipmentInfo == true)
|
||||
#expect(completed.equivalentLength == true)
|
||||
#expect(completed.frictionRate == true)
|
||||
#expect(completed.rooms == true)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func detail() async throws {
|
||||
try await withTestUser { user in
|
||||
@Dependency(\.database) var database
|
||||
let project = try await database.projects.create(user.id, .mock)
|
||||
|
||||
var detail = try await database.projects.detail(project.id)
|
||||
#expect(detail == nil)
|
||||
|
||||
let equipment = try await database.equipment.create(
|
||||
.init(projectID: project.id, heatingCFM: 1000, coolingCFM: 1000)
|
||||
)
|
||||
detail = try await database.projects.detail(project.id)
|
||||
#expect(detail != nil)
|
||||
|
||||
let componentLoss = try await database.componentLosses.create(
|
||||
.init(projectID: project.id, name: "Test", value: 0.2)
|
||||
)
|
||||
let room = try await database.rooms.create(
|
||||
project.id,
|
||||
.init(name: "Test", heatingLoad: 12345, coolingTotal: 12345)
|
||||
)
|
||||
let supplyLength = try await database.equivalentLengths.create(
|
||||
.init(
|
||||
projectID: project.id, name: "Supply", type: .supply, straightLengths: [1], groups: [])
|
||||
)
|
||||
let returnLength = try await database.equivalentLengths.create(
|
||||
.init(
|
||||
projectID: project.id, name: "Return", type: .return, straightLengths: [1], groups: [])
|
||||
)
|
||||
detail = try await database.projects.detail(project.id)
|
||||
#expect(detail?.componentLosses == [componentLoss])
|
||||
#expect(detail?.equipmentInfo == equipment)
|
||||
#expect(detail?.rooms == [room])
|
||||
#expect(detail?.equivalentLengths.contains(supplyLength) == true)
|
||||
#expect(detail?.equivalentLengths.contains(returnLength) == true)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Test(
|
||||
arguments: [
|
||||
ProjectModel(
|
||||
name: "", streetAddress: "1234 Sesame St", city: "Nowhere", state: "OH", zipCode: "55555",
|
||||
sensibleHeatRatio: nil, userID: UUID(0)
|
||||
),
|
||||
ProjectModel(
|
||||
name: "Testy", streetAddress: "", city: "Nowhere", state: "OH", zipCode: "55555",
|
||||
sensibleHeatRatio: nil, userID: UUID(0)
|
||||
),
|
||||
ProjectModel(
|
||||
name: "Testy", streetAddress: "1234 Sesame St", city: "", state: "OH", zipCode: "55555",
|
||||
sensibleHeatRatio: nil, userID: UUID(0)
|
||||
),
|
||||
ProjectModel(
|
||||
name: "Testy", streetAddress: "1234 Sesame St", city: "Nowhere", state: "",
|
||||
zipCode: "55555",
|
||||
sensibleHeatRatio: nil, userID: UUID(0)
|
||||
),
|
||||
ProjectModel(
|
||||
name: "Testy", streetAddress: "1234 Sesame St", city: "Nowhere", state: "OH",
|
||||
zipCode: "",
|
||||
sensibleHeatRatio: nil, userID: UUID(0)
|
||||
),
|
||||
ProjectModel(
|
||||
name: "Testy", streetAddress: "1234 Sesame St", city: "Nowhere", state: "OH",
|
||||
zipCode: "55555",
|
||||
sensibleHeatRatio: -1, userID: UUID(0)
|
||||
),
|
||||
ProjectModel(
|
||||
name: "Testy", streetAddress: "1234 Sesame St", city: "Nowhere", state: "OH",
|
||||
zipCode: "55555",
|
||||
sensibleHeatRatio: 1.1, userID: UUID(0)
|
||||
),
|
||||
]
|
||||
)
|
||||
func validations(model: ProjectModel) {
|
||||
var errors = [String]()
|
||||
|
||||
#expect(throws: (any Error).self) {
|
||||
do {
|
||||
try model.validate()
|
||||
} catch {
|
||||
// Just checking to make sure I'm not testing the same error over and over /
|
||||
// making sure I've reset to good values / only testing one property at a time.
|
||||
#expect(!errors.contains("\(error)"))
|
||||
errors.append("\(error)")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
160
Tests/DatabaseClientTests/RoomTests.swift
Normal file
160
Tests/DatabaseClientTests/RoomTests.swift
Normal file
@@ -0,0 +1,160 @@
|
||||
import Dependencies
|
||||
import Foundation
|
||||
import ManualDCore
|
||||
import Parsing
|
||||
import Testing
|
||||
|
||||
@testable import DatabaseClient
|
||||
|
||||
@Suite
|
||||
struct RoomTests {
|
||||
|
||||
@Test
|
||||
func happyPath() async throws {
|
||||
try await withTestUserAndProject { _, project in
|
||||
@Dependency(\.database.rooms) var rooms
|
||||
|
||||
let room = try await rooms.create(
|
||||
project.id,
|
||||
.init(name: "Test", heatingLoad: 1234, coolingTotal: 1234)
|
||||
)
|
||||
|
||||
let fetched = try await rooms.fetch(project.id)
|
||||
#expect(fetched == [room])
|
||||
|
||||
let got = try await rooms.get(room.id)
|
||||
#expect(got == room)
|
||||
|
||||
let updated = try await rooms.update(
|
||||
room.id,
|
||||
.init(rectangularSizes: [.init(id: UUID(0), register: 1, height: 8)])
|
||||
)
|
||||
#expect(updated.id == room.id)
|
||||
|
||||
let updatedSize = try await rooms.updateRectangularSize(
|
||||
room.id, .init(id: UUID(0), register: 1, height: 10)
|
||||
)
|
||||
#expect(updatedSize.id == room.id)
|
||||
|
||||
let deletedSize = try await rooms.deleteRectangularSize(room.id, UUID(0))
|
||||
#expect(deletedSize.rectangularSizes == nil)
|
||||
|
||||
try await rooms.delete(room.id)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func createMany() async throws {
|
||||
try await withTestUserAndProject { _, project in
|
||||
@Dependency(\.database.rooms) var rooms
|
||||
|
||||
let created = try await rooms.createMany(
|
||||
project.id,
|
||||
[
|
||||
.init(name: "Test 1", heatingLoad: 1234, coolingTotal: 1234),
|
||||
.init(name: "Test 2", heatingLoad: 1234, coolingTotal: 1234),
|
||||
]
|
||||
)
|
||||
|
||||
#expect(created.count == 2)
|
||||
#expect(created[0].name == "Test 1")
|
||||
#expect(created[1].name == "Test 2")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func notFound() async throws {
|
||||
try await withDatabase {
|
||||
@Dependency(\.database.rooms) var rooms
|
||||
|
||||
await #expect(throws: NotFoundError.self) {
|
||||
try await rooms.delete(UUID(0))
|
||||
}
|
||||
|
||||
await #expect(throws: NotFoundError.self) {
|
||||
try await rooms.deleteRectangularSize(UUID(0), UUID(1))
|
||||
}
|
||||
|
||||
await #expect(throws: NotFoundError.self) {
|
||||
try await rooms.update(UUID(0), .init())
|
||||
}
|
||||
|
||||
await #expect(throws: NotFoundError.self) {
|
||||
try await rooms.updateRectangularSize(UUID(0), .init(height: 8))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test(
|
||||
arguments: [
|
||||
Room.Create(
|
||||
// projectID: UUID(0),
|
||||
name: "",
|
||||
heatingLoad: 12345,
|
||||
coolingTotal: 12344,
|
||||
coolingSensible: nil,
|
||||
registerCount: 1
|
||||
),
|
||||
Room.Create(
|
||||
// projectID: UUID(0),
|
||||
name: "Test",
|
||||
heatingLoad: -12345,
|
||||
coolingTotal: 12344,
|
||||
coolingSensible: nil,
|
||||
registerCount: 1
|
||||
),
|
||||
Room.Create(
|
||||
// projectID: UUID(0),
|
||||
name: "Test",
|
||||
heatingLoad: 12345,
|
||||
coolingTotal: -12344,
|
||||
coolingSensible: nil,
|
||||
registerCount: 1
|
||||
),
|
||||
Room.Create(
|
||||
// projectID: UUID(0),
|
||||
name: "Test",
|
||||
heatingLoad: 12345,
|
||||
coolingTotal: 12344,
|
||||
coolingSensible: -123,
|
||||
registerCount: 1
|
||||
),
|
||||
Room.Create(
|
||||
// projectID: UUID(0),
|
||||
name: "Test",
|
||||
heatingLoad: 12345,
|
||||
coolingTotal: 12344,
|
||||
coolingSensible: nil,
|
||||
registerCount: -1
|
||||
),
|
||||
Room.Create(
|
||||
// projectID: UUID(0),
|
||||
name: "",
|
||||
heatingLoad: -12345,
|
||||
coolingTotal: -12344,
|
||||
coolingSensible: -1,
|
||||
registerCount: -1
|
||||
),
|
||||
Room.Create(
|
||||
// projectID: UUID(0),
|
||||
name: "Test",
|
||||
heatingLoad: 12345,
|
||||
coolingTotal: nil,
|
||||
coolingSensible: nil,
|
||||
registerCount: 1
|
||||
),
|
||||
]
|
||||
)
|
||||
func validations(room: Room.Create) throws {
|
||||
#expect(throws: (any Error).self) {
|
||||
// do {
|
||||
try room.toModel(projectID: UUID(0)).validate()
|
||||
// } catch {
|
||||
// print("\(error)")
|
||||
// throw error
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
95
Tests/DatabaseClientTests/TrunkSizeTests.swift
Normal file
95
Tests/DatabaseClientTests/TrunkSizeTests.swift
Normal file
@@ -0,0 +1,95 @@
|
||||
import Dependencies
|
||||
import Foundation
|
||||
import ManualDCore
|
||||
import Testing
|
||||
|
||||
@testable import DatabaseClient
|
||||
|
||||
@Suite
|
||||
struct TrunkSizeTests {
|
||||
|
||||
@Test
|
||||
func happyPath() async throws {
|
||||
try await withTestUserAndProject { _, project in
|
||||
@Dependency(\.database) var database
|
||||
|
||||
let room = try await database.rooms.create(
|
||||
project.id,
|
||||
.init(
|
||||
name: "Test", heatingLoad: 12345, coolingTotal: 12345,
|
||||
coolingSensible: nil, registerCount: 5
|
||||
)
|
||||
)
|
||||
|
||||
let trunk = try await database.trunkSizes.create(
|
||||
.init(
|
||||
projectID: project.id,
|
||||
type: .supply,
|
||||
rooms: [room.id: [1, 2, 3]],
|
||||
height: 8,
|
||||
name: "Test Trunk"
|
||||
)
|
||||
)
|
||||
|
||||
let fetched = try await database.trunkSizes.fetch(project.id)
|
||||
#expect(fetched == [trunk])
|
||||
|
||||
let got = try await database.trunkSizes.get(trunk.id)
|
||||
#expect(got == trunk)
|
||||
|
||||
let updated = try await database.trunkSizes.update(
|
||||
trunk.id, .init(type: .return)
|
||||
)
|
||||
#expect(updated.type == .return)
|
||||
#expect(updated.id == trunk.id)
|
||||
|
||||
try await database.trunkSizes.delete(trunk.id)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func notFound() async throws {
|
||||
try await withTestUserAndProject { _, project in
|
||||
@Dependency(\.database.trunkSizes) var trunks
|
||||
|
||||
await #expect(throws: NotFoundError.self) {
|
||||
try await trunks.create(
|
||||
.init(projectID: project.id, type: .supply, rooms: [UUID(0): [1]])
|
||||
)
|
||||
}
|
||||
|
||||
await #expect(throws: NotFoundError.self) {
|
||||
try await trunks.delete(UUID(0))
|
||||
}
|
||||
|
||||
await #expect(throws: NotFoundError.self) {
|
||||
try await trunks.update(UUID(0), .init(type: .return))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test(
|
||||
arguments: [
|
||||
TrunkModel(projectID: UUID(0), type: .return, height: 8, name: ""),
|
||||
TrunkModel(projectID: UUID(0), type: .return, height: -8, name: "Test"),
|
||||
]
|
||||
)
|
||||
func validations(model: TrunkModel) {
|
||||
#expect(throws: (any Error).self) {
|
||||
try model.validate()
|
||||
}
|
||||
}
|
||||
|
||||
@Test(
|
||||
arguments: [
|
||||
TrunkRoomModel(trunkID: UUID(0), roomID: UUID(0), registers: [-1, 1], type: .return),
|
||||
TrunkRoomModel(trunkID: UUID(0), roomID: UUID(0), registers: [1, -1], type: .return),
|
||||
TrunkRoomModel(trunkID: UUID(0), roomID: UUID(0), registers: [], type: .return),
|
||||
]
|
||||
)
|
||||
func trunkRoomModelValidations(model: TrunkRoomModel) {
|
||||
#expect(throws: (any Error).self) {
|
||||
try model.validate()
|
||||
}
|
||||
}
|
||||
}
|
||||
190
Tests/DatabaseClientTests/UserTests.swift
Normal file
190
Tests/DatabaseClientTests/UserTests.swift
Normal file
@@ -0,0 +1,190 @@
|
||||
import Dependencies
|
||||
import Foundation
|
||||
import ManualDCore
|
||||
import Testing
|
||||
import Vapor
|
||||
|
||||
@testable import DatabaseClient
|
||||
|
||||
@Suite
|
||||
struct UserDatabaseTests {
|
||||
|
||||
@Test
|
||||
func happyPaths() async throws {
|
||||
try await withDatabase {
|
||||
@Dependency(\.database.users) var users
|
||||
|
||||
let user = try await users.create(
|
||||
.init(email: "testy@example.com", password: "super-secret", confirmPassword: "super-secret")
|
||||
)
|
||||
|
||||
#expect(user.email == "testy@example.com")
|
||||
|
||||
// Test login the user in
|
||||
let token = try await users.login(
|
||||
.init(email: user.email, password: "super-secret")
|
||||
)
|
||||
#expect(token.userID == user.id)
|
||||
// Test the same token is returned.
|
||||
let token2 = try await users.login(
|
||||
.init(email: user.email, password: "super-secret")
|
||||
)
|
||||
#expect(token.id == token2.id)
|
||||
// Test logging out
|
||||
try await users.logout(token.id)
|
||||
|
||||
try await users.delete(user.id)
|
||||
|
||||
let shouldBeNilUser = try await users.get(user.id)
|
||||
#expect(shouldBeNilUser == nil)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func deleteFailsWithInvalidUserID() async throws {
|
||||
try await withDatabase {
|
||||
@Dependency(\.database.users) var users
|
||||
await #expect(throws: NotFoundError.self) {
|
||||
try await users.delete(UUID(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func logoutIgnoresUnfoundTokenID() async throws {
|
||||
try await withDatabase {
|
||||
@Dependency(\.database.users) var users
|
||||
try await users.logout(UUID(0))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func loginFails() async throws {
|
||||
try await withDatabase {
|
||||
@Dependency(\.database.users) var users
|
||||
|
||||
await #expect(throws: NotFoundError.self) {
|
||||
try await users.login(
|
||||
.init(email: "foo@example.com", password: "super-secret")
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
let user = try await users.create(
|
||||
.init(email: "testy@example.com", password: "super-secret", confirmPassword: "super-secret")
|
||||
)
|
||||
|
||||
// Ensure can not login with invalid password
|
||||
await #expect(throws: Abort.self) {
|
||||
try await users.login(
|
||||
.init(email: user.email, password: "wrong-password")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func userProfileHappyPath() async throws {
|
||||
try await withTestUser { user in
|
||||
@Dependency(\.database.userProfiles) var profiles
|
||||
let profile = try await profiles.create(
|
||||
.init(
|
||||
userID: user.id,
|
||||
firstName: "Testy",
|
||||
lastName: "McTestface",
|
||||
companyName: "Acme Co.",
|
||||
streetAddress: "12345 Sesame St",
|
||||
city: "Nowhere",
|
||||
state: "FL",
|
||||
zipCode: "55555"
|
||||
)
|
||||
)
|
||||
|
||||
let fetched = try await profiles.fetch(user.id)
|
||||
#expect(fetched == profile)
|
||||
|
||||
let got = try await profiles.get(profile.id)
|
||||
#expect(got == profile)
|
||||
|
||||
let updated = try await profiles.update(profile.id, .init(firstName: "Updated"))
|
||||
#expect(updated.firstName == "Updated")
|
||||
#expect(updated.id == profile.id)
|
||||
|
||||
try await profiles.delete(profile.id)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func testUserProfileFails() async throws {
|
||||
try await withDatabase {
|
||||
@Dependency(\.database.userProfiles) var profiles
|
||||
await #expect(throws: NotFoundError.self) {
|
||||
try await profiles.delete(UUID(0))
|
||||
}
|
||||
await #expect(throws: NotFoundError.self) {
|
||||
try await profiles.update(UUID(0), .init(firstName: "Foo"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test(
|
||||
arguments: [
|
||||
UserProfileModel(
|
||||
userID: UUID(0), firstName: "", lastName: "McTestface", companyName: "Acme Co.",
|
||||
streetAddress: "1234 Sesame St", city: "Nowhere", state: "CA", zipCode: "55555"
|
||||
),
|
||||
UserProfileModel(
|
||||
userID: UUID(0), firstName: "Testy", lastName: "", companyName: "Acme Co.",
|
||||
streetAddress: "1234 Sesame St", city: "Nowhere", state: "CA", zipCode: "55555"
|
||||
),
|
||||
UserProfileModel(
|
||||
userID: UUID(0), firstName: "Testy", lastName: "McTestface", companyName: "",
|
||||
streetAddress: "1234 Sesame St", city: "Nowhere", state: "CA", zipCode: "55555"
|
||||
),
|
||||
UserProfileModel(
|
||||
userID: UUID(0), firstName: "Testy", lastName: "McTestface", companyName: "Acme Co.",
|
||||
streetAddress: "", city: "Nowhere", state: "CA", zipCode: "55555"
|
||||
),
|
||||
UserProfileModel(
|
||||
userID: UUID(0), firstName: "Testy", lastName: "McTestface", companyName: "Acme Co.",
|
||||
streetAddress: "1234 Sesame St", city: "", state: "CA", zipCode: "55555"
|
||||
),
|
||||
UserProfileModel(
|
||||
userID: UUID(0), firstName: "Testy", lastName: "McTestface", companyName: "Acme Co.",
|
||||
streetAddress: "1234 Sesame St", city: "Nowhere", state: "", zipCode: "55555"
|
||||
),
|
||||
UserProfileModel(
|
||||
userID: UUID(0), firstName: "Testy", lastName: "McTestface", companyName: "Acme Co.",
|
||||
streetAddress: "1234 Sesame St", city: "Nowhere", state: "CA", zipCode: ""
|
||||
),
|
||||
]
|
||||
)
|
||||
func profileValidations(model: UserProfileModel) {
|
||||
var errors = [String]()
|
||||
#expect(throws: (any Error).self) {
|
||||
do {
|
||||
try model.validate()
|
||||
} catch {
|
||||
// Just checking to make sure I'm not testing the same error over and over /
|
||||
// making sure I've reset to good values / only testing one property at a time.
|
||||
#expect(!errors.contains("\(error)"))
|
||||
errors.append("\(error)")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test(
|
||||
arguments: [
|
||||
User.Create(email: "", password: "super-secret", confirmPassword: "super-secret"),
|
||||
User.Create(email: "testy@example.com", password: "", confirmPassword: "super-secret"),
|
||||
User.Create(email: "testy@example.com", password: "super-secret", confirmPassword: ""),
|
||||
User.Create(email: "testy@example.com", password: "super", confirmPassword: "super"),
|
||||
]
|
||||
)
|
||||
func userValidations(model: User.Create) {
|
||||
#expect(throws: (any Error).self) {
|
||||
try model.validate()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,62 +24,16 @@ struct ManualDClientTests {
|
||||
|
||||
@Test
|
||||
func ductSize() async throws {
|
||||
let response = try await manualD.ductSize(
|
||||
.init(designCFM: 88, frictionRate: 0.06)
|
||||
)
|
||||
let response = try await manualD.ductSize(88, 0.06)
|
||||
#expect(numberFormatter.string(for: response.calculatedSize) == "6.07")
|
||||
#expect(response.finalSize == 7)
|
||||
#expect(response.flexSize == 7)
|
||||
#expect(response.velocity == 329)
|
||||
}
|
||||
|
||||
// @Test
|
||||
// func frictionRate() async throws {
|
||||
// let response = try await manualD.frictionRate(
|
||||
// .init(
|
||||
// externalStaticPressure: 0.5,
|
||||
// componentPressureLosses: .mock,
|
||||
// totalEffectiveLength: 185
|
||||
// )
|
||||
// )
|
||||
// #expect(numberFormatter.string(for: response.availableStaticPressure) == "0.11")
|
||||
// #expect(numberFormatter.string(for: response.frictionRate) == "0.06")
|
||||
// }
|
||||
|
||||
// @Test
|
||||
// func frictionRateFails() async throws {
|
||||
// await #expect(throws: ManualDError.self) {
|
||||
// _ = try await manualD.frictionRate(
|
||||
// .init(
|
||||
// externalStaticPressure: 0.5,
|
||||
// componentPressureLosses: .mock,
|
||||
// totalEffectiveLength: 0
|
||||
// )
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
|
||||
@Test
|
||||
func totalEffectiveLength() async throws {
|
||||
let response = try await manualD.totalEquivalentLength(
|
||||
.init(
|
||||
trunkLengths: [25],
|
||||
runoutLengths: [10],
|
||||
effectiveLengthGroups: [
|
||||
// NOTE: These are made up and may not correspond to actual manual-d group tel's.
|
||||
EffectiveLengthGroup(group: 1, letter: "a", effectiveLength: 20, category: .supply),
|
||||
EffectiveLengthGroup(group: 2, letter: "a", effectiveLength: 30, category: .supply),
|
||||
EffectiveLengthGroup(group: 3, letter: "a", effectiveLength: 10, category: .supply),
|
||||
EffectiveLengthGroup(group: 12, letter: "a", effectiveLength: 10, category: .supply),
|
||||
]
|
||||
)
|
||||
)
|
||||
#expect(response == 105)
|
||||
}
|
||||
|
||||
@Test
|
||||
func equivalentRectangularDuct() async throws {
|
||||
let response = try await manualD.rectangularSize(.init(round: 7, height: 8))
|
||||
let response = try await manualD.rectangularSize(round: 7, height: 8)
|
||||
#expect(response.height == 8)
|
||||
#expect(response.width == 5)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ struct ViewControllerTests {
|
||||
func login() async throws {
|
||||
try await withDependencies {
|
||||
$0.viewController = .liveValue
|
||||
$0.authClient = .failing
|
||||
$0.auth = .failing
|
||||
} operation: {
|
||||
@Dependency(\.viewController) var viewController
|
||||
|
||||
@@ -31,7 +31,7 @@ struct ViewControllerTests {
|
||||
func signup() async throws {
|
||||
try await withDependencies {
|
||||
$0.viewController = .liveValue
|
||||
$0.authClient = .failing
|
||||
$0.auth = .failing
|
||||
} operation: {
|
||||
@Dependency(\.viewController) var viewController
|
||||
|
||||
@@ -86,7 +86,7 @@ struct ViewControllerTests {
|
||||
let project = Project.mock
|
||||
let rooms = Room.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 trunks = TrunkSize.mock(projectID: project.id, rooms: rooms)
|
||||
|
||||
@@ -100,6 +100,13 @@ struct ViewControllerTests {
|
||||
)
|
||||
}
|
||||
|
||||
let mockDuctSizes = DuctSizes.mock(
|
||||
equipmentInfo: equipment,
|
||||
rooms: rooms,
|
||||
trunks: trunks,
|
||||
shr: project.sensibleHeatRatio ?? 0.83
|
||||
)
|
||||
|
||||
try await withDefaultDependencies {
|
||||
$0.database.projects.get = { _ in project }
|
||||
$0.database.projects.getCompletedSteps = { _ in
|
||||
@@ -108,13 +115,16 @@ struct ViewControllerTests {
|
||||
$0.database.projects.getSensibleHeatRatio = { _ in 0.83 }
|
||||
$0.database.rooms.fetch = { _ in rooms }
|
||||
$0.database.equipment.fetch = { _ in equipment }
|
||||
$0.database.effectiveLength.fetch = { _ in tels }
|
||||
$0.database.effectiveLength.fetchMax = { _ in
|
||||
$0.database.equivalentLengths.fetch = { _ in tels }
|
||||
$0.database.equivalentLengths.fetchMax = { _ in
|
||||
.init(supply: tels.first, return: tels.last)
|
||||
}
|
||||
$0.database.componentLoss.fetch = { _ in componentLosses }
|
||||
$0.projectClient.calculateDuctSizes = { _ in
|
||||
.mock(equipmentInfo: equipment, rooms: rooms, trunks: trunks)
|
||||
$0.database.componentLosses.fetch = { _ in componentLosses }
|
||||
$0.projectClient.calculateRoomDuctSizes = { _ in
|
||||
mockDuctSizes.rooms
|
||||
}
|
||||
$0.projectClient.calculateTrunkDuctSizes = { _ in
|
||||
mockDuctSizes.trunks
|
||||
}
|
||||
} operation: {
|
||||
@Dependency(\.viewController) var viewController
|
||||
@@ -163,8 +173,8 @@ struct ViewControllerTests {
|
||||
|
||||
return try await withDependencies {
|
||||
$0.viewController = .liveValue
|
||||
$0.authClient.currentUser = { user }
|
||||
$0.database.userProfile.fetch = { _ in profile }
|
||||
$0.auth.currentUser = { user }
|
||||
$0.database.userProfiles.fetch = { _ in profile }
|
||||
$0.manualD = .liveValue
|
||||
try await updateDependencies(&$0)
|
||||
} operation: {
|
||||
|
||||
@@ -63,6 +63,12 @@ p-6 w-full">
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="tooltip tooltip-left" data-tip="Upload csv file">
|
||||
<form hx-post="/projects/00000000-0000-0000-0000-000000000000/rooms/csv" hx-target="body" hx-swap="outerHTML" enctype="multipart/form-data">
|
||||
<input type="file" name="file" accept=".csv">
|
||||
<button class="btn btn-secondary" type="submit">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-end space-x-4 font-bold">
|
||||
<span class="text-lg">Heating Total</span>
|
||||
@@ -151,14 +157,13 @@ p-6 w-full">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm_00000000000000000000000000000001.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
|
||||
<h1 class="text-3xl font-bold pb-6">Room</h1>
|
||||
<form class="grid grid-cols-1 gap-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000001" hx-target="body" hx-swap="outerHTML">
|
||||
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
|
||||
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000001">
|
||||
Name<label class="input w-full"><span class="label"></span>
|
||||
<input name="name" type="text" placeholder="Name" required autofocus value="Bed-1">
|
||||
Heating Load</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="heatingLoad" type="number" placeholder="1234" required min="0" value="3913.0">
|
||||
Cooling Total</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="coolingTotal" type="number" placeholder="1234" required min="0" value="2472.0">
|
||||
<input name="coolingTotal" type="number" placeholder="1234 (Optional)" min="0" value="2472.0">
|
||||
Cooling Sensible</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value="">
|
||||
Registers</label><label class="input w-full"><span class="label"></span>
|
||||
@@ -204,14 +209,13 @@ p-6 w-full">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm_00000000000000000000000000000002.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
|
||||
<h1 class="text-3xl font-bold pb-6">Room</h1>
|
||||
<form class="grid grid-cols-1 gap-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000002" hx-target="body" hx-swap="outerHTML">
|
||||
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
|
||||
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000002">
|
||||
Name<label class="input w-full"><span class="label"></span>
|
||||
<input name="name" type="text" placeholder="Name" required autofocus value="Entry">
|
||||
Heating Load</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="heatingLoad" type="number" placeholder="1234" required min="0" value="8284.0">
|
||||
Cooling Total</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="coolingTotal" type="number" placeholder="1234" required min="0" value="2916.0">
|
||||
<input name="coolingTotal" type="number" placeholder="1234 (Optional)" min="0" value="2916.0">
|
||||
Cooling Sensible</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value="">
|
||||
Registers</label><label class="input w-full"><span class="label"></span>
|
||||
@@ -257,14 +261,13 @@ p-6 w-full">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm_00000000000000000000000000000003.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
|
||||
<h1 class="text-3xl font-bold pb-6">Room</h1>
|
||||
<form class="grid grid-cols-1 gap-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000003" hx-target="body" hx-swap="outerHTML">
|
||||
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
|
||||
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000003">
|
||||
Name<label class="input w-full"><span class="label"></span>
|
||||
<input name="name" type="text" placeholder="Name" required autofocus value="Family Room">
|
||||
Heating Load</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="heatingLoad" type="number" placeholder="1234" required min="0" value="9785.0">
|
||||
Cooling Total</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="coolingTotal" type="number" placeholder="1234" required min="0" value="7446.0">
|
||||
<input name="coolingTotal" type="number" placeholder="1234 (Optional)" min="0" value="7446.0">
|
||||
Cooling Sensible</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value="">
|
||||
Registers</label><label class="input w-full"><span class="label"></span>
|
||||
@@ -310,14 +313,13 @@ p-6 w-full">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm_00000000000000000000000000000004.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
|
||||
<h1 class="text-3xl font-bold pb-6">Room</h1>
|
||||
<form class="grid grid-cols-1 gap-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000004" hx-target="body" hx-swap="outerHTML">
|
||||
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
|
||||
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000004">
|
||||
Name<label class="input w-full"><span class="label"></span>
|
||||
<input name="name" type="text" placeholder="Name" required autofocus value="Kitchen">
|
||||
Heating Load</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="heatingLoad" type="number" placeholder="1234" required min="0" value="4518.0">
|
||||
Cooling Total</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="coolingTotal" type="number" placeholder="1234" required min="0" value="5096.0">
|
||||
<input name="coolingTotal" type="number" placeholder="1234 (Optional)" min="0" value="5096.0">
|
||||
Cooling Sensible</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value="">
|
||||
Registers</label><label class="input w-full"><span class="label"></span>
|
||||
@@ -363,14 +365,13 @@ p-6 w-full">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm_00000000000000000000000000000005.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
|
||||
<h1 class="text-3xl font-bold pb-6">Room</h1>
|
||||
<form class="grid grid-cols-1 gap-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000005" hx-target="body" hx-swap="outerHTML">
|
||||
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
|
||||
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000005">
|
||||
Name<label class="input w-full"><span class="label"></span>
|
||||
<input name="name" type="text" placeholder="Name" required autofocus value="Living Room">
|
||||
Heating Load</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="heatingLoad" type="number" placeholder="1234" required min="0" value="7553.0">
|
||||
Cooling Total</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="coolingTotal" type="number" placeholder="1234" required min="0" value="6829.0">
|
||||
<input name="coolingTotal" type="number" placeholder="1234 (Optional)" min="0" value="6829.0">
|
||||
Cooling Sensible</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value="">
|
||||
Registers</label><label class="input w-full"><span class="label"></span>
|
||||
@@ -416,14 +417,13 @@ p-6 w-full">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm_00000000000000000000000000000006.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
|
||||
<h1 class="text-3xl font-bold pb-6">Room</h1>
|
||||
<form class="grid grid-cols-1 gap-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000006" hx-target="body" hx-swap="outerHTML">
|
||||
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
|
||||
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000006">
|
||||
Name<label class="input w-full"><span class="label"></span>
|
||||
<input name="name" type="text" placeholder="Name" required autofocus value="Master">
|
||||
Heating Load</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="heatingLoad" type="number" placeholder="1234" required min="0" value="8202.0">
|
||||
Cooling Total</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="coolingTotal" type="number" placeholder="1234" required min="0" value="2076.0">
|
||||
<input name="coolingTotal" type="number" placeholder="1234 (Optional)" min="0" value="2076.0">
|
||||
Cooling Sensible</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value="">
|
||||
Registers</label><label class="input w-full"><span class="label"></span>
|
||||
@@ -441,13 +441,12 @@ p-6 w-full">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
|
||||
<h1 class="text-3xl font-bold pb-6">Room</h1>
|
||||
<form class="grid grid-cols-1 gap-4" hx-post="/projects/00000000-0000-0000-0000-000000000000/rooms" hx-target="body" hx-swap="outerHTML">
|
||||
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
|
||||
Name<label class="input w-full"><span class="label"></span>
|
||||
<label class="input w-full"><span class="label">Name</span>
|
||||
<input name="name" type="text" placeholder="Name" required autofocus value="">
|
||||
Heating Load</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="heatingLoad" type="number" placeholder="1234" required min="0" value="">
|
||||
Cooling Total</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="coolingTotal" type="number" placeholder="1234" required min="0" value="">
|
||||
<input name="coolingTotal" type="number" placeholder="1234 (Optional)" min="0" value="">
|
||||
Cooling Sensible</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value="">
|
||||
Registers</label><label class="input w-full"><span class="label"></span>
|
||||
|
||||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user