I am starting using Vapor to run an API, and I couldn't figure out an elegant way of doing the following: (1) load a Fluent relation and (2) wait for the response of an HTTP callback before (3) responding to the original request.
Here is what I ended up coding, after I transformed my real-life entities into planets and stars to maintain the examples from Vapor's documentation. It works™️, but I couldn't achieve a nice chaining of operations ☹️.
func flagPlanetAsHumanFriendly(req: Request) throws -> EventLoopFuture<Planet> {
Planet(req.parameters.get("planetID"), on: req.db)
.unwrap(or: Abort(.notFound))
.flatMap { planet in
// I load the star because I need its ID for the HTTP callback
_ = planet.$star.load(on: req.db).map {
let uri = URI(scheme: "http", host: "localhost", port: 4200, path: "/webhooks/star")
// HTTP Callback
req.client.post(uri) { req in
try req.content.encode(
WebhookDTO(
starId: planet.star.id!,
status: .humanFriendly,
planetId: planet.id!
),
using: JSONEncoder()
)
}
.map { res in
debugPrint("\(res)")
return
}
}
// Couldn't find a way to wait for the response, so the HTTP callback is a side-effect
// and its response is not used in the original HTTP response...
// If the callback fails, my API can't report it.
planet.state = .humanFriendly
return planet.save(on: req.db).map { planet }
}
}
EventLoopFuture
My first issue was that I couldn't find a way to load a parent relationship while keeping the entity in the scope.
Planet(req.parameters.get("planetID"), on: req.db)
.unwrap(or: Abort(.notFound))
.flatMap { planet in // planet is in the scope 🎉
return planet.$star.load(on: req.db) // returns a EventLoopFuture<Void>
}
.map {
// I know that star has been loaded but I lost my `planet` reference ☹️
???
}
I assume there is an operator that should be able to return a mix of the 2 EventLoopFuture
instances but I couldn't figure it out.
EventLoopFuture
with an auxiliary HTTP request's responseHere as well, I assume I miss an operator that allows me to wait for the request's response, while keeping a reference to the planet, before I respond to the original request.
Help would be welcome on how to achieve this in a nice chaining of operations — if it is possible, of course — and I will more than happy to update Vapor's documentation accordingly. 🙏
In short, if you need the result of one future inside the result of another future then you have to nest the callbacks - you can't use chaining. So for instance:
Planet(req.parameters.get("planetID"), on: req.db)
.unwrap(or: Abort(.notFound))
.flatMap { planet in // planet is in the scope 🎉
return planet.$star.load(on: req.db).map {
// Star is now eager loaded and you have access to planet
}
}
If you have multiple futures that aren't reliant on each other but you need the results of both you can use and
to chain the two together and wait for them. E.g.:
func flagPlanetAsHumanFriendly(req: Request) throws -> EventLoopFuture<Planet> {
Planet(req.parameters.get("planetID"), on: req.db)
.unwrap(or: Abort(.notFound))
.flatMap { planet in
// I load the star because I need its ID for the HTTP callback
planet.$star.load(on: req.db).map {
let uri = URI(scheme: "http", host: "localhost", port: 4200, path: "/webhooks/star")
// HTTP Callback
let postFuture = req.client.post(uri) { req in
try req.content.encode(
WebhookDTO(
starId: planet.star.id!,
status: .humanFriendly,
planetId: planet.id!
),
using: JSONEncoder()
)
}.map { res in
debugPrint("\(res)")
return
}
planet.state = .humanFriendly
let planetUpdateFuture = planet.save(on: req.db)
return postFuture.and(planetUpdateFuture).flatMap { _, _ in
return planet
}
}
}
}