This repository has been archived on 2026-02-12. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
swift-duct-calc/Sources/DatabaseClient/TrunkSizes.swift

344 lines
8.7 KiB
Swift

import Dependencies
import DependenciesMacros
import Fluent
import Foundation
import ManualDCore
extension DatabaseClient {
@DependencyClient
public struct TrunkSizes: Sendable {
public var create: @Sendable (DuctSizing.TrunkSize.Create) async throws -> DuctSizing.TrunkSize
public var delete: @Sendable (DuctSizing.TrunkSize.ID) async throws -> Void
public var fetch: @Sendable (Project.ID) async throws -> [DuctSizing.TrunkSize]
public var get: @Sendable (DuctSizing.TrunkSize.ID) async throws -> DuctSizing.TrunkSize?
public var update:
@Sendable (DuctSizing.TrunkSize.ID, DuctSizing.TrunkSize.Update) async throws ->
DuctSizing.TrunkSize
}
}
extension DatabaseClient.TrunkSizes: TestDependencyKey {
public static let testValue = Self()
public static func live(database: any Database) -> Self {
.init(
create: { request in
try request.validate()
let trunk = request.toModel()
var roomProxies = [DuctSizing.TrunkSize.RoomProxy]()
try await trunk.save(on: database)
for (roomID, registers) in request.rooms {
guard let room = try await RoomModel.find(roomID, on: database) else {
throw NotFoundError()
}
let model = try TrunkRoomModel(
trunkID: trunk.requireID(),
roomID: room.requireID(),
registers: registers,
type: request.type
)
try await model.save(on: database)
try await roomProxies.append(model.toDTO(on: database))
}
return try .init(
id: trunk.requireID(),
projectID: trunk.$project.id,
type: .init(rawValue: trunk.type)!,
rooms: roomProxies
)
},
delete: { id in
guard let model = try await TrunkModel.find(id, on: database) else {
throw NotFoundError()
}
try await model.delete(on: database)
},
fetch: { projectID in
let models = try await TrunkModel.query(on: database)
.with(\.$project)
.with(\.$rooms)
.filter(\.$project.$id == projectID)
.all()
return try await withThrowingTaskGroup(of: DuctSizing.TrunkSize.self) { group in
for model in models {
group.addTask {
try await model.toDTO(on: database)
}
}
return try await group.reduce(into: [DuctSizing.TrunkSize]()) {
$0.append($1)
}
}
// return try await models.map {
// try await $0.toDTO(on: database)
// }
},
get: { id in
guard let model = try await TrunkModel.find(id, on: database) else {
return nil
}
return try await model.toDTO(on: database)
},
update: { id, updates in
guard
let model =
try await TrunkModel
.query(on: database)
.with(\.$rooms)
.filter(\.$id == id)
.first()
else {
throw NotFoundError()
}
try updates.validate()
try await model.applyUpdates(updates, on: database)
return try await model.toDTO(on: database)
}
)
}
}
extension DuctSizing.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,
type: type,
height: height
)
}
}
extension DuctSizing.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 DuctSizing.TrunkSize {
struct Migrate: AsyncMigration {
let name = "CreateTrunkSize"
func prepare(on database: any Database) async throws {
try await database.schema(TrunkModel.schema)
.id()
.field("height", .int8)
.field("type", .string, .required)
.field(
"projectID", .uuid, .required, .references(ProjectModel.schema, "id", onDelete: .cascade)
)
.create()
try await database.schema(TrunkRoomModel.schema)
.id()
.field("registers", .array(of: .int), .required)
.field("type", .string, .required)
.field(
"trunkID", .uuid, .required, .references(TrunkModel.schema, "id", onDelete: .cascade)
)
.field(
"roomID", .uuid, .required, .references(RoomModel.schema, "id", onDelete: .cascade)
)
.unique(on: "trunkID", "roomID", "type")
.create()
}
func revert(on database: any Database) async throws {
try await database.schema(TrunkRoomModel.schema).delete()
try await database.schema(TrunkModel.schema).delete()
}
}
}
// Pivot table for associating rooms and trunks.
final class TrunkRoomModel: Model, @unchecked Sendable {
static let schema = "room+trunk"
@ID(key: .id)
var id: UUID?
@Parent(key: "trunkID")
var trunk: TrunkModel
@Parent(key: "roomID")
var room: RoomModel
@Field(key: "registers")
var registers: [Int]
@Field(key: "type")
var type: String
init() {}
init(
id: UUID? = nil,
trunkID: TrunkModel.IDValue,
roomID: RoomModel.IDValue,
registers: [Int],
type: DuctSizing.TrunkSize.TrunkType
) {
self.id = id
$trunk.id = trunkID
$room.id = roomID
self.registers = registers
self.type = type.rawValue
}
func toDTO(on database: any Database) async throws -> DuctSizing.TrunkSize.RoomProxy {
guard let room = try await RoomModel.find($room.id, on: database) else {
throw NotFoundError()
}
return .init(
room: try room.toDTO(),
registers: registers
)
}
}
final class TrunkModel: Model, @unchecked Sendable {
static let schema = "trunk"
@ID(key: .id)
var id: UUID?
@Parent(key: "projectID")
var project: ProjectModel
@Field(key: "height")
var height: Int?
@Field(key: "type")
var type: String
@Children(for: \.$trunk)
var rooms: [TrunkRoomModel]
init() {}
init(
id: UUID? = nil,
projectID: Project.ID,
type: DuctSizing.TrunkSize.TrunkType,
height: Int? = nil,
) {
self.id = id
$project.id = projectID
self.height = height
self.type = type.rawValue
}
func toDTO(on database: any Database) async throws -> DuctSizing.TrunkSize {
let rooms = try await withThrowingTaskGroup(of: DuctSizing.TrunkSize.RoomProxy.self) { group in
for room in self.rooms {
group.addTask {
try await room.toDTO(on: database)
}
}
return try await group.reduce(into: [DuctSizing.TrunkSize.RoomProxy]()) {
$0.append($1)
}
}
return try .init(
id: requireID(),
projectID: $project.id,
type: .init(rawValue: type)!,
rooms: rooms,
height: height
)
}
func applyUpdates(
_ updates: DuctSizing.TrunkSize.Update,
on database: any Database
) async throws {
if let type = updates.type, type.rawValue != self.type {
self.type = type.rawValue
}
if let height = updates.height, height != self.height {
self.height = height
}
if hasChanges {
try await self.save(on: database)
}
guard let updateRooms = updates.rooms else {
return
}
// Update rooms.
let rooms = try await TrunkRoomModel.query(on: database)
.with(\.$room)
.filter(\.$trunk.$id == requireID())
.all()
for (roomID, registers) in updateRooms {
if let currRoom = rooms.first(where: { $0.$room.id == roomID }) {
database.logger.debug("CURRENT ROOM: \(currRoom.room.name)")
if registers != currRoom.registers {
database.logger.debug("Updating registers for: \(currRoom.room.name)")
currRoom.registers = registers
}
if currRoom.hasChanges {
try await currRoom.save(on: database)
}
} else {
database.logger.debug("CREATING NEW TrunkRoomModel")
let newModel = try TrunkRoomModel(
trunkID: requireID(),
roomID: roomID,
registers: registers,
type: .init(rawValue: type)!
)
try await newModel.save(on: database)
}
}
let roomsToDelete = rooms.filter {
!updateRooms.keys.contains($0.$room.id)
}
for room in roomsToDelete {
try await room.delete(on: database)
}
database.logger.debug("DONE WITH UPDATES")
}
}