feat: Adds createFromCSV to create rooms in the database, properly handling delegating airflow to another room.
Some checks failed
CI / Linux Tests (push) Failing after 5m29s
Some checks failed
CI / Linux Tests (push) Failing after 5m29s
This commit is contained in:
@@ -95,6 +95,9 @@ let package = Package(
|
||||
.target(name: "DatabaseClient"),
|
||||
.product(name: "DependenciesTestSupport", package: "swift-dependencies"),
|
||||
.product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"),
|
||||
],
|
||||
resources: [
|
||||
.copy("Resources")
|
||||
]
|
||||
),
|
||||
.target(
|
||||
|
||||
@@ -6841,6 +6841,13 @@
|
||||
margin-block-end: calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)));
|
||||
}
|
||||
}
|
||||
.space-y-3 {
|
||||
:where(& > :not(:last-child)) {
|
||||
--tw-space-y-reverse: 0;
|
||||
margin-block-start: calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));
|
||||
margin-block-end: calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)));
|
||||
}
|
||||
}
|
||||
.space-y-4 {
|
||||
:where(& > :not(:last-child)) {
|
||||
--tw-space-y-reverse: 0;
|
||||
|
||||
@@ -12,7 +12,7 @@ extension DependencyValues {
|
||||
|
||||
@DependencyClient
|
||||
public struct CSVParser: Sendable {
|
||||
public var parseRooms: @Sendable (Room.CSV) async throws -> [Room.Create]
|
||||
public var parseRooms: @Sendable (Room.CSV) async throws -> [Room.CSV.Row]
|
||||
}
|
||||
|
||||
extension CSVParser: DependencyKey {
|
||||
@@ -24,12 +24,11 @@ extension CSVParser: DependencyKey {
|
||||
throw CSVParsingError("Unreadable file data")
|
||||
}
|
||||
let rows = try RoomCSVParser().parse(string[...].utf8)
|
||||
let rooms = rows.reduce(into: [Room.Create]()) {
|
||||
return rows.reduce(into: [Room.CSV.Row]()) {
|
||||
if case .room(let room) = $1 {
|
||||
$0.append(room)
|
||||
}
|
||||
}
|
||||
return rooms
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ struct RoomRowParser: Parser {
|
||||
|
||||
enum RoomRowType {
|
||||
case header(String)
|
||||
case room(Room.Create)
|
||||
case room(Room.CSV.Row)
|
||||
}
|
||||
|
||||
struct RoomCreateParser: ParserPrinter {
|
||||
@@ -34,7 +34,7 @@ struct RoomCreateParser: ParserPrinter {
|
||||
// the room yet, so we will need an intermediate representation for the csv data
|
||||
// that uses a room's name or disregard and require user to delegate airflow in
|
||||
// the ui.
|
||||
var body: some ParserPrinter<Substring.UTF8View, Room.Create> {
|
||||
var body: some ParserPrinter<Substring.UTF8View, Room.CSV.Row> {
|
||||
ParsePrint {
|
||||
Prefix { $0 != UInt8(ascii: ",") }.map(.string)
|
||||
",".utf8
|
||||
@@ -51,9 +51,9 @@ struct RoomCreateParser: ParserPrinter {
|
||||
Int.parser()
|
||||
",".utf8
|
||||
Optionally {
|
||||
Room.ID.parser()
|
||||
Prefix { $0 != UInt8(ascii: "\n") }.map(.string)
|
||||
}
|
||||
}
|
||||
.map(.memberwise(Room.Create.init))
|
||||
.map(.memberwise(Room.CSV.Row.init))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ public struct DatabaseClient: Sendable {
|
||||
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 createFromCSV: @Sendable (Project.ID, [Room.CSV.Row]) async throws -> [Room]
|
||||
public var delete: @Sendable (Room.ID) async throws -> Void
|
||||
public var deleteRectangularSize:
|
||||
@Sendable (Room.ID, Room.RectangularSize.ID) async throws -> Room
|
||||
@@ -98,6 +99,7 @@ public struct DatabaseClient: Sendable {
|
||||
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
|
||||
|
||||
@@ -16,11 +16,59 @@ extension DatabaseClient.Rooms: TestDependencyKey {
|
||||
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()
|
||||
try await RoomModel.createMany(projectID: projectID, rooms: rooms, on: database)
|
||||
},
|
||||
createFromCSV: { projectID, rows in
|
||||
|
||||
database.logger.debug("\nCreate From CSV rows: \(rows)\n")
|
||||
|
||||
// Filter out rows that delegate their airflow / load to another room,
|
||||
// these need to be created last.
|
||||
let rowsThatDelegate = rows.filter({
|
||||
$0.delegatedToName != nil && $0.delegatedToName != ""
|
||||
})
|
||||
|
||||
let initialRooms = rows.filter({
|
||||
$0.delegatedToName == nil || $0.delegatedToName == ""
|
||||
})
|
||||
.map(\.createModel)
|
||||
|
||||
database.logger.debug("\nInitial rows: \(initialRooms)\n")
|
||||
|
||||
let initialCreated = try await RoomModel.createMany(
|
||||
projectID: projectID,
|
||||
rooms: initialRooms,
|
||||
on: database
|
||||
)
|
||||
database.logger.debug("\nInitially created rows: \(initialCreated)\n")
|
||||
|
||||
let roomsThatDelegateModels = try rowsThatDelegate.reduce(into: [Room.Create]()) {
|
||||
array, row in
|
||||
database.logger.debug("\n\(row.name), delegating to: \(row.delegatedToName!)\n")
|
||||
guard let created = initialCreated.first(where: { $0.name == row.delegatedToName }) else {
|
||||
database.logger.debug(
|
||||
"\nUnable to find created room with name: \(row.delegatedToName!)\n"
|
||||
)
|
||||
throw NotFoundError()
|
||||
}
|
||||
array.append(
|
||||
Room.Create.init(
|
||||
name: row.name,
|
||||
heatingLoad: row.heatingLoad,
|
||||
coolingTotal: row.coolingTotal,
|
||||
coolingSensible: row.coolingSensible,
|
||||
registerCount: 0,
|
||||
delegatedTo: created.id
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return try await RoomModel.createMany(
|
||||
projectID: projectID,
|
||||
rooms: roomsThatDelegateModels,
|
||||
on: database
|
||||
) + initialCreated
|
||||
|
||||
},
|
||||
delete: { id in
|
||||
guard let model = try await RoomModel.find(id, on: database) else {
|
||||
@@ -81,6 +129,34 @@ extension DatabaseClient.Rooms: TestDependencyKey {
|
||||
}
|
||||
}
|
||||
|
||||
extension Room.CSV.Row {
|
||||
fileprivate var createModel: Room.Create {
|
||||
assert(delegatedToName == nil || delegatedToName == "")
|
||||
return .init(
|
||||
name: name,
|
||||
heatingLoad: heatingLoad,
|
||||
coolingTotal: coolingTotal,
|
||||
coolingSensible: coolingSensible,
|
||||
registerCount: registerCount,
|
||||
delegatedTo: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension RoomModel {
|
||||
fileprivate static func createMany(
|
||||
projectID: Project.ID,
|
||||
rooms: [Room.Create],
|
||||
on database: any Database
|
||||
) async throws -> [Room] {
|
||||
try await rooms.asyncMap { request in
|
||||
let model = try request.toModel(projectID: projectID)
|
||||
try await model.validateAndSave(on: database)
|
||||
return try model.toDTO()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Room.Create {
|
||||
|
||||
func toModel(projectID: Project.ID) throws -> RoomModel {
|
||||
|
||||
@@ -148,6 +148,50 @@ extension Room {
|
||||
public init(file: Data) {
|
||||
self.file = file
|
||||
}
|
||||
|
||||
/// Represents a row in a CSV file.
|
||||
///
|
||||
/// This is similar to ``Room.Create``, but since the rooms are not yet
|
||||
/// created, delegating to another room is done via the room's name
|
||||
/// instead of id.
|
||||
///
|
||||
public struct Row: 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
|
||||
|
||||
/// An optional room that this room delegates it's airflow to.
|
||||
public let delegatedToName: String?
|
||||
|
||||
public init(
|
||||
name: String,
|
||||
heatingLoad: Double,
|
||||
coolingTotal: Double? = nil,
|
||||
coolingSensible: Double? = nil,
|
||||
registerCount: Int,
|
||||
delegatedToName: String? = nil
|
||||
) {
|
||||
self.name = name
|
||||
self.heatingLoad = heatingLoad
|
||||
self.coolingTotal = coolingTotal
|
||||
self.coolingSensible = coolingSensible
|
||||
self.registerCount = registerCount
|
||||
self.delegatedToName = delegatedToName
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Represents a rectangular size calculation that is stored in the
|
||||
|
||||
@@ -293,7 +293,7 @@ extension SiteRoute.View.ProjectRoute.RoomRoute {
|
||||
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)
|
||||
_ = try await database.rooms.createFromCSV(projectID, rooms)
|
||||
}
|
||||
// return EmptyHTML()
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import App
|
||||
import CSVParser
|
||||
import DatabaseClient
|
||||
import Dependencies
|
||||
import Fluent
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import CSVParser
|
||||
import Dependencies
|
||||
import FileClient
|
||||
import Foundation
|
||||
import ManualDCore
|
||||
import Parsing
|
||||
@@ -63,6 +65,31 @@ struct RoomTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func createFromCSV() async throws {
|
||||
try await withTestUserAndProject {
|
||||
$0.csvParser = .liveValue
|
||||
} operation: { _, project in
|
||||
@Dependency(\.csvParser) var csvParser
|
||||
@Dependency(\.database) var database
|
||||
@Dependency(\.fileClient) var fileClient
|
||||
|
||||
let csvPath = Bundle.module.path(forResource: "rooms", ofType: "csv")
|
||||
let csvFile = Room.CSV(file: try Data(contentsOf: URL(filePath: csvPath!)))
|
||||
let rows = try await csvParser.parseRooms(csvFile)
|
||||
print()
|
||||
print("ROWS: \(rows)")
|
||||
print()
|
||||
|
||||
let created = try await database.rooms.createFromCSV(project.id, rows)
|
||||
|
||||
print()
|
||||
print("CREATED: \(created)")
|
||||
print()
|
||||
#expect(created.count == rows.count)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func notFound() async throws {
|
||||
try await withDatabase {
|
||||
@@ -157,4 +184,3 @@ struct RoomTests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@source not "./tailwindcss";
|
||||
@source not "./daisyui{,*}.mjs";
|
||||
|
||||
@plugin "./daisyui.mjs";
|
||||
2825
output.css
2825
output.css
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user