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"),
|
.target(name: "DatabaseClient"),
|
||||||
.product(name: "DependenciesTestSupport", package: "swift-dependencies"),
|
.product(name: "DependenciesTestSupport", package: "swift-dependencies"),
|
||||||
.product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"),
|
.product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"),
|
||||||
|
],
|
||||||
|
resources: [
|
||||||
|
.copy("Resources")
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
.target(
|
.target(
|
||||||
|
|||||||
@@ -6841,6 +6841,13 @@
|
|||||||
margin-block-end: calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)));
|
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 {
|
.space-y-4 {
|
||||||
:where(& > :not(:last-child)) {
|
:where(& > :not(:last-child)) {
|
||||||
--tw-space-y-reverse: 0;
|
--tw-space-y-reverse: 0;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ extension DependencyValues {
|
|||||||
|
|
||||||
@DependencyClient
|
@DependencyClient
|
||||||
public struct CSVParser: Sendable {
|
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 {
|
extension CSVParser: DependencyKey {
|
||||||
@@ -24,12 +24,11 @@ extension CSVParser: DependencyKey {
|
|||||||
throw CSVParsingError("Unreadable file data")
|
throw CSVParsingError("Unreadable file data")
|
||||||
}
|
}
|
||||||
let rows = try RoomCSVParser().parse(string[...].utf8)
|
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 {
|
if case .room(let room) = $1 {
|
||||||
$0.append(room)
|
$0.append(room)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return rooms
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ struct RoomRowParser: Parser {
|
|||||||
|
|
||||||
enum RoomRowType {
|
enum RoomRowType {
|
||||||
case header(String)
|
case header(String)
|
||||||
case room(Room.Create)
|
case room(Room.CSV.Row)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RoomCreateParser: ParserPrinter {
|
struct RoomCreateParser: ParserPrinter {
|
||||||
@@ -34,7 +34,7 @@ struct RoomCreateParser: ParserPrinter {
|
|||||||
// the room yet, so we will need an intermediate representation for the csv data
|
// 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
|
// that uses a room's name or disregard and require user to delegate airflow in
|
||||||
// the ui.
|
// the ui.
|
||||||
var body: some ParserPrinter<Substring.UTF8View, Room.Create> {
|
var body: some ParserPrinter<Substring.UTF8View, Room.CSV.Row> {
|
||||||
ParsePrint {
|
ParsePrint {
|
||||||
Prefix { $0 != UInt8(ascii: ",") }.map(.string)
|
Prefix { $0 != UInt8(ascii: ",") }.map(.string)
|
||||||
",".utf8
|
",".utf8
|
||||||
@@ -51,9 +51,9 @@ struct RoomCreateParser: ParserPrinter {
|
|||||||
Int.parser()
|
Int.parser()
|
||||||
",".utf8
|
",".utf8
|
||||||
Optionally {
|
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 struct Rooms: Sendable {
|
||||||
public var create: @Sendable (Project.ID, Room.Create) async throws -> Room
|
public var create: @Sendable (Project.ID, Room.Create) async throws -> Room
|
||||||
public var createMany: @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 delete: @Sendable (Room.ID) async throws -> Void
|
||||||
public var deleteRectangularSize:
|
public var deleteRectangularSize:
|
||||||
@Sendable (Room.ID, Room.RectangularSize.ID) async throws -> Room
|
@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 fetch: @Sendable (Project.ID) async throws -> [Room]
|
||||||
public var update: @Sendable (Room.ID, Room.Update) 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
|
public var updateRectangularSize: @Sendable (Room.ID, Room.RectangularSize) async throws -> Room
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@DependencyClient
|
@DependencyClient
|
||||||
|
|||||||
@@ -16,11 +16,59 @@ extension DatabaseClient.Rooms: TestDependencyKey {
|
|||||||
return try model.toDTO()
|
return try model.toDTO()
|
||||||
},
|
},
|
||||||
createMany: { projectID, rooms in
|
createMany: { projectID, rooms in
|
||||||
try await rooms.asyncMap { request in
|
try await RoomModel.createMany(projectID: projectID, rooms: rooms, on: database)
|
||||||
let model = try request.toModel(projectID: projectID)
|
},
|
||||||
try await model.validateAndSave(on: database)
|
createFromCSV: { projectID, rows in
|
||||||
return try model.toDTO()
|
|
||||||
|
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
|
delete: { id in
|
||||||
guard let model = try await RoomModel.find(id, on: database) else {
|
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 {
|
extension Room.Create {
|
||||||
|
|
||||||
func toModel(projectID: Project.ID) throws -> RoomModel {
|
func toModel(projectID: Project.ID) throws -> RoomModel {
|
||||||
|
|||||||
@@ -148,6 +148,50 @@ extension Room {
|
|||||||
public init(file: Data) {
|
public init(file: Data) {
|
||||||
self.file = file
|
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
|
/// Represents a rectangular size calculation that is stored in the
|
||||||
|
|||||||
@@ -293,7 +293,7 @@ extension SiteRoute.View.ProjectRoute.RoomRoute {
|
|||||||
case .csv(let csv):
|
case .csv(let csv):
|
||||||
return await roomsView(on: request, projectID: projectID) {
|
return await roomsView(on: request, projectID: projectID) {
|
||||||
let rooms = try await csvParser.parseRooms(csv)
|
let rooms = try await csvParser.parseRooms(csv)
|
||||||
_ = try await database.rooms.createMany(projectID, rooms)
|
_ = try await database.rooms.createFromCSV(projectID, rooms)
|
||||||
}
|
}
|
||||||
// return EmptyHTML()
|
// return EmptyHTML()
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import App
|
import App
|
||||||
|
import CSVParser
|
||||||
import DatabaseClient
|
import DatabaseClient
|
||||||
import Dependencies
|
import Dependencies
|
||||||
import Fluent
|
import Fluent
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import CSVParser
|
||||||
import Dependencies
|
import Dependencies
|
||||||
|
import FileClient
|
||||||
import Foundation
|
import Foundation
|
||||||
import ManualDCore
|
import ManualDCore
|
||||||
import Parsing
|
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
|
@Test
|
||||||
func notFound() async throws {
|
func notFound() async throws {
|
||||||
try await withDatabase {
|
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