diff --git a/Package.swift b/Package.swift index 507bb4a..86469dd 100644 --- a/Package.swift +++ b/Package.swift @@ -6,6 +6,7 @@ let package = Package( name: "swift-manual-d", products: [ .executable(name: "App", targets: ["App"]), + .library(name: "ApiController", targets: ["ApiController"]), .library(name: "DatabaseClient", targets: ["DatabaseClient"]), .library(name: "ManualDCore", targets: ["ManualDCore"]), .library(name: "ManualDClient", targets: ["ManualDClient"]), @@ -28,6 +29,7 @@ let package = Package( .executableTarget( name: "App", dependencies: [ + .target(name: "ApiController"), .target(name: "DatabaseClient"), .target(name: "ViewController"), .product(name: "Dependencies", package: "swift-dependencies"), @@ -40,6 +42,16 @@ 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: "DatabaseClient", dependencies: [ diff --git a/Sources/ApiController/Interface.swift b/Sources/ApiController/Interface.swift new file mode 100644 index 0000000..58efd83 --- /dev/null +++ b/Sources/ApiController/Interface.swift @@ -0,0 +1,38 @@ +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) + } + ) +} diff --git a/Sources/ApiController/Live.swift b/Sources/ApiController/Live.swift new file mode 100644 index 0000000..4ec93e4 --- /dev/null +++ b/Sources/ApiController/Live.swift @@ -0,0 +1,119 @@ +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) + case .delete(let id): + try await database.projects.delete(id) + return nil + 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 + } +} diff --git a/Sources/App/Extensions/ApiController+respond.swift b/Sources/App/Extensions/ApiController+respond.swift new file mode 100644 index 0000000..e0786aa --- /dev/null +++ b/Sources/App/Extensions/ApiController+respond.swift @@ -0,0 +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)) + ) + } +} diff --git a/Sources/App/Middleware/DependenciesMiddleware.swift b/Sources/App/Middleware/DependenciesMiddleware.swift index b72ab93..466b5b1 100644 --- a/Sources/App/Middleware/DependenciesMiddleware.swift +++ b/Sources/App/Middleware/DependenciesMiddleware.swift @@ -1,4 +1,4 @@ -// import ApiControllerLive +import ApiController import DatabaseClient import Dependencies import Vapor @@ -10,17 +10,17 @@ import ViewController struct DependenciesMiddleware: AsyncMiddleware { private let values: DependencyValues.Continuation - // private let apiController: ApiController + private let apiController: ApiController private let database: DatabaseClient private let viewController: ViewController init( database: DatabaseClient, - // apiController: ApiController = .liveValue, + apiController: ApiController = .liveValue, viewController: ViewController = .testValue ) { self.values = withEscapedDependencies { $0 } - // self.apiController = apiController + self.apiController = apiController self.database = database self.viewController = viewController } @@ -28,7 +28,7 @@ struct DependenciesMiddleware: AsyncMiddleware { func respond(to request: Request, chainingTo next: any AsyncResponder) async throws -> Response { try await values.yield { try await withDependencies { - // $0.apiController = apiController + $0.apiController = apiController $0.database = database // $0.dateFormatter = .liveValue $0.viewController = viewController diff --git a/Sources/App/configure.swift b/Sources/App/configure.swift index 54adf71..9a93f42 100644 --- a/Sources/App/configure.swift +++ b/Sources/App/configure.swift @@ -117,13 +117,12 @@ private func siteHandler( request: Request, route: SiteRoute ) async throws -> any AsyncResponseEncodable { - // @Dependency(\.apiController) var apiController + @Dependency(\.apiController) var apiController @Dependency(\.viewController) var viewController switch route { case .api(let route): - return HTTPStatus.ok - // return try await apiController.respond(route, request: request) + return try await apiController.respond(route, request: request) case .health: return HTTPStatus.ok case .view(let route): diff --git a/Sources/DatabaseClient/ComponentPressureLoss.swift b/Sources/DatabaseClient/ComponentPressureLoss.swift index adb0100..21a089b 100644 --- a/Sources/DatabaseClient/ComponentPressureLoss.swift +++ b/Sources/DatabaseClient/ComponentPressureLoss.swift @@ -82,7 +82,8 @@ extension ComponentPressureLoss { .field("value", .double, .required) .field("createdAt", .datetime) .field("updatedAt", .datetime) - .foreignKey("projectID", references: ProjectModel.schema, "id", onDelete: .cascade) + .field("projectID", .uuid, .required, .references(ProjectModel.schema, "id")) + // .foreignKey("projectID", references: ProjectModel.schema, "id", onDelete: .cascade) .unique(on: "projectID", "name") .create() } diff --git a/Sources/DatabaseClient/Equipment.swift b/Sources/DatabaseClient/Equipment.swift index 1367836..e57434d 100644 --- a/Sources/DatabaseClient/Equipment.swift +++ b/Sources/DatabaseClient/Equipment.swift @@ -91,7 +91,7 @@ extension EquipmentInfo { .field("coolingCFM", .int16, .required) .field("createdAt", .datetime) .field("updatedAt", .datetime) - .foreignKey("projectID", references: ProjectModel.schema, "id", onDelete: .cascade) + .field("projectID", .uuid, .required, .references(ProjectModel.schema, "id")) .unique(on: "projectID") .create() } diff --git a/Sources/DatabaseClient/Interface.swift b/Sources/DatabaseClient/Interface.swift index 57040d9..ea6a11e 100644 --- a/Sources/DatabaseClient/Interface.swift +++ b/Sources/DatabaseClient/Interface.swift @@ -54,9 +54,9 @@ extension DatabaseClient.Migrations: DependencyKey { public static let liveValue = Self( run: { [ + Project.Migrate(), ComponentPressureLoss.Migrate(), EquipmentInfo.Migrate(), - Project.Migrate(), Room.Migrate(), ] } diff --git a/Sources/DatabaseClient/Projects.swift b/Sources/DatabaseClient/Projects.swift index 079cafa..796b965 100644 --- a/Sources/DatabaseClient/Projects.swift +++ b/Sources/DatabaseClient/Projects.swift @@ -123,6 +123,9 @@ final class ProjectModel: Model, @unchecked Sendable { @Timestamp(key: "updatedAt", on: .update, format: .iso8601) var updatedAt: Date? + @Children(for: \.$project) + var componentLosses: [ComponentLossModel] + init() {} init( diff --git a/Sources/DatabaseClient/Rooms.swift b/Sources/DatabaseClient/Rooms.swift index 590abde..3b3b5b1 100644 --- a/Sources/DatabaseClient/Rooms.swift +++ b/Sources/DatabaseClient/Rooms.swift @@ -83,7 +83,7 @@ extension Room { .field("coolingTotal", .double, .required) .field("coolingSensible", .double, .required) .field("registerCount", .int8, .required) - .foreignKey("projectID", references: ProjectModel.schema, "id", onDelete: .cascade) + .field("projectID", .uuid, .required, .references(ProjectModel.schema, "id")) .unique(on: "projectID", "name") .create() }