Env: Vapor/Fluent 4.0.0
I have a tree structured data with model like this:
final class Ingredient: Model {
static let schema = "ingredients"
#ID(key: "id")
var id: UUID?
#Field(key: "name")
var name: String
#OptionalParent(key: "parent_id")
var parent: Ingredient?
#Children(for: \.$parent)
var children: [Ingredient]
}
And I want to return the whole tree as a JSON in one of API methods.
func index(req: Request) throws -> EventLoopFuture<[APIIngredient]> {
// Gathering top-level nodes without parents
let ingredients = try Ingredient.query(on: req.db)
.filter(\.$parent.$id == nil)
.sort(\.$name)
.all()
.wait()
enter code here
// Creating API models
let apiIngredients = try ingredients.map {
try APIIngredient(
ingredient: $0,
childrenGetter: {
try $0.$children.query(on: req.db).all().wait()
}
)
}
return req.eventLoop.future(apiIngredients)
}
But I've found that .wait() is disallowed to use in request handlers. What's the right way to approach this?
wait() is not allowed because it blocks the EventLoop. So you have a few options:
Use eager loading as suggested by Nick. This will be much more efficient as it's only 2 DB queries instead of N+1.
Use async/await to write it how you want
Handle the futures properly. Instead of using wait() switch to handling the futures:
func index(req: Request) throws -> EventLoopFuture<[APIIngredient]> {
// Gathering top-level nodes without parents
return Ingredient.query(on: req.db)
.filter(\.$parent.$id == nil)
.sort(\.$name)
.all()
.flatMap { ingredients in
ingredients.map { ingredient -> EventLoopFuture<APIIngredient>
let future = ingredient.$children.query(on: req.db).all().map { children in
APIIngredient(ingredient: ingredient, children: children)
}
}.flatten(on: req.eventLoop)
}
}
Related
I have the following code, and I need to use Query so that I can programmatically build the query up before making the call to Firestore, but the document I get back apparently doesn't support Decodable. If I don't use Query, I cannot build up the where clauses programmatically however the documents I get back do support Decodable. How can I get the first case to allow Decodable to work?
public static func query<T: Codable>(queryFields: [String: Any]) async -> [T] {
let db = Firestore.firestore()
var ref: Query = db.collection("myDocuments")
for (key, value) in queryFields {
ref = ref.whereField(key, isEqualTo: value)
}
let snapshot = try? await ref.getDocuments()
if let snapshot = snapshot {
let results = snapshot.documents.compactMap { document in
try? document.data(as: T.self) // this does not compile
}
return results
} else {
return [T]()
}
}
Currently I'm having a Observable created using scan to update underlying model using a PublishSubject like this:
class ViewModel {
private enum Action {
case updateName(String)
}
private let product: Observable<Product>
private let actions = PublishSubject<Action>()
init(initialProduct: Product) {
product = actions
.scan(initialProduct, accumulator: { (oldProduct, action) -> Product in
var newProduct = oldProduct
switch action {
case .updateName(let name):
newProduct.name = name
}
return newProduct
})
.startWith(initialProduct)
.share()
}
func updateProductName(_ name: String) {
actions.onNext(.updateName(name))
}
private func getProductDetail() {
/// This will call a network request
}
}
Every "local" actions like update product's name, prices... is done by using method like updateProductName(_ name: String) above. But what if I want to have a network request that also update the product, and can be called every time I want, for example after a button tap, or after calling updateProductName?
// UPDATE: After read iWheelBuy's comment and Daniel's answer, I ended up using 2 more actions
class ViewModel {
private enum Action {
case getDetail
case updateProduct(Product)
}
///....
init(initialProduct: Product) {
product = actions
.scan(initialProduct, accumulator: { (oldProduct, action) -> Product in
var newProduct = oldProduct
switch action {
case .updateName(let name):
newProduct.name = name
case .getDetail:
self.getProductDetail()
case .updateProduct(let p):
return p
}
return newProduct
})
.startWith(initialProduct)
.share()
}
func getProductDetail() {
actions.onNext(.getDetail)
}
private func getProductDetail(id: Int) {
ProductService.getProductDetail(id) { product in
self.actions.onNext(.updateProduct(product))
}
}
}
But I feel that, I trigger side effect (call network request) inside scan, without updating the model, is that something wrong?
Also how can I use a "rx" network request?
// What if I want to use this method instead of the one above,
// without subscribe inside viewmodel?
private func rxGetProductDetail(id: Int) -> Observable<Product> {
return ProductService.rxGetProductDetail(id: Int)
}
I'm not sure why #iWheelBuy didn't make a real answer because their comment is the correct answer. Given the hybrid approach to Rx in your question, I expect something like the below will accommodate your style:
class ViewModel {
private enum Action {
case updateName(String)
case updateProduct(Product)
}
private let product: Observable<Product>
private let actions = PublishSubject<Action>()
private var disposable: Disposable?
init(initialProduct: Product) {
product = actions
.scan(initialProduct, accumulator: { (oldProduct, action) -> Product in
var newProduct = oldProduct
switch action {
case .updateName(let name):
newProduct.name = name
case .updateProduct(let product):
newProduct = product
}
return newProduct
})
.startWith(initialProduct)
.share()
// without a subscribe, none of this matters. I assume you just didn't show all your code.
}
deinit {
disposable?.dispose()
}
func updateProductName(_ name: String) {
actions.onNext(.updateName(name))
}
private func getProductDetail() {
let request = URLRequest(url: URL(string: "https://foo.com")!)
disposable?.dispose()
disposable = URLSession.shared.rx.data(request: request)
.map { try JSONDecoder().decode(Product.self, from: $0) }
.map { Action.updateProduct($0) }
.subscribe(
onNext: { [actions] in actions.onNext($0) },
onError: { error in /* handle error */ }
)
}
}
The style above is still pretty imperative but if you don't want your use of Rx to leak out of the view model it's okay.
If you want to see a "full Rx" setup, you might find my sample repo interesting: https://github.com/danielt1263/RxEarthquake
UPDATE
But I feel that, I trigger side effect (call network request) inside scan, without updating the model, is that something wrong?
The scan function should be pure with no side effects. Calling a network request inside it's closure is inappropriate.
I'm getting a JSON response of the format:
{
"current_page":1,
"data":[
{
"id":1,
"title":"Title 1"
},
{
"id":2,
"title":"Title 2"
},
{
"id":3,
"title":"Title 3"
}
]
}
As you can see, data contains a list of objects, in this case, a list of Posts. Here is my Realm/Objectmapper Post class:
import RealmSwift
import ObjectMapper
class Post: Object, Mappable {
let id = RealmOptional<Int>()
#objc dynamic var title: String? = nil
required convenience init?(map: Map) {
self.init()
}
func mapping(map: Map) {
}
}
I created a generic class (I'm not sure it's written right) to handle Pagination responses. I want it to be generic because I have other pagination responses that return Users instead of Posts, among other objects.
Here is my current Pagination class:
import ObjectMapper
class Pagination<T: Mappable>: Mappable {
var data: [T]?
required convenience init?(map: Map) {
self.init()
}
func mapping(map: Map) {
data <- map["data"]
}
}
However, I'm not sure if I've written this class right.
And here is the class where I call the endpoint that sends back the pagination data (I've removed irrelevant code):
var posts = [Post]()
provider.request(.getPosts(page: 1)) { result in
switch result {
case let .success(response):
do {
let json = try JSONSerialization.jsonObject(with: response.data, options: .allowFragments)
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// Not sure what to do here to handle and retrieve the list of Posts
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// Eventually, I need to append the posts to the variable
// self.posts.append(pagination.data)
// Reload the table view's data
self.tableView.reloadData()
} catch {
print(error)
}
case let .failure(error):
print(error)
break
}
}
How do I handle the JSON response correctly in order to get the list of Posts and then append them to the var posts = [Post]() variable? Do I need to make any changes to my Pagination class?
Once you have your json, it is easy to parse it using object mapper:
let pagination = Mapper<Pagination<Post>>().map(JSONObject: json)
It could be further generalized, I have used a direct reference as an example. Your Pagination class can also hold the current page index value.
I think you are also missing the implementation of the mapping(map:) function in your Post class, it should be something like this:
func mapping(map: Map) {
title <- map["title"]
}
I have an object and its properties as following:
class Section {
var cards: [MemberCard]
init(card: [MemberCard]) {
}
}
class MemberCard {
var name: String
var address: String?
init(name: String) {
self.name = name
}
}
I'm subscribing to a RxStream of type Observable<[Section]>. Before I subscribe I would to want flat map this function.
where the flat map would perform the following actions:
let sectionsStream : Observable<[Section]> = Observable.just([sections])
sectionsStream
.flatMap { [weak self] (sections) -> Observable<[Section]> in
for section in sections {
for card in section.cards {
}
}
}.subscribe(onNext: { [weak self] (sections) in
self?.updateUI(memberSections: sections)
}).disposed(by: disposeBag)
func getAddressFromCache(card: MemberCard) -> Observable<MemberCard> {
return Cache(id: card.name).flatMap ({ (card) -> Observable<MemberCard> in
asyncCall{
return Observable.just(card)
}
}
}
How would the flatmap look like when it comes to converting Observable<[Section]> to array of [Observable<MemberCard>] and back to Observable<[Section]>?
Technically, like that -
let o1: Observable<MemberCard> = ...
let o2: Observable<Section> = omc.toList().map { Section($0) }
let o2: Observable<[Section]> = Observable.concat(o2 /* and all others */).toList()
But I do not think it is an optimal solution, at least because there is no error handling for the case when one or more cards cannot be retrieved. I would rather build something around aggregation with .scan() operator as in https://github.com/maxvol/RaspSwift
Here you go:
extension ObservableType where E == [Section] {
func addressedCards() -> Observable<[Section]> {
return flatMap {
Observable.combineLatest($0.map { getAddresses($0.cards) })
}
.map {
$0.map { Section(cards: $0) }
}
}
}
func getAddresses(_ cards: [MemberCard]) -> Observable<[MemberCard]> {
return Observable.combineLatest(cards
.map {
getAddressFromCache(card: $0)
.catchErrorJustReturn($0)
})
}
If one of the caches emits an error, the above will return the MemberCard unchanged.
I have a couple of other tips as well.
In keeping with the functional nature of Rx, your Section and MemberCard types should either be structs or (classes with lets instead of vars).
Don't use String? unless you have a compelling reason why an empty string ("") is different than a missing string (nil). There's no reason why you should have to check existence and isEmpty every time you want to see if the address has been filled in. (The same goes for arrays and Dictionaries.)
For this code, proper use of combineLatest is the key. It can turn an [Observable<T>] into an Observable<[T]>. Learn other interesting ways of combining Observables here: https://medium.com/#danielt1263/recipes-for-combining-observables-in-rxswift-ec4f8157265f
I am currently learning Realm and am converting my experimental app/game which uses arrays to Realm;
It loads pre-seeding data via a local JSON file and ObjectMapper; then creates objects in realm; this part seems to work.
// Parse response
let json = try! JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.mutableContainers) as! Array<Dictionary<String, AnyObject>>
let factories = Mapper<Factory>().mapArray(JSONArray: json)!
do {
let realm = try Realm()
try realm.write {
for factory in factories
{
realm.add(factory, update: true)
}
}
} catch let error as NSError {
print(error.localizedDescription as Any)
}
The issue I'm having is that when it maps; I'd like it to create its child objects at the same time and link them to parent.
Each parent (Factory) has about between 4 children (Engine) linked to it.
// Factory is parent object
class Factory: Object, Mappable {
dynamic var name: String = ""
let engines = List<Engine>()
//Impl. of Mappable protocol
required convenience init?(map: Map) {
self.init()
}
// Mappable
func mapping(map: Map) {
name <- map["name"]
}
}
// Engine is a child to Factory
class Engine: Object {
dynamic var production: Int = 0
// create children and add to the parent factory
static func createEngines(parent:Factory) -> [Engines]
{
var engines:[Engine] = [Engine]()
for _ in stride(from:0, to: 3, by: 1) {
//let engine : Engine = Engine.init(parent: element)
//engines.append(engine)
}
return engines
}
}
If I attempt to put this in my mappable
engines = Engine.createEngines(parent: self)
and make a change in my Factory model;
`var engines = List<Engine>()`
I get this error:
Cannot assign value of type '[Engine]?' to type 'List<Engine>'
The problem here is that simply creating an array of engines (children), appending it to an array doesn't seem to work with Realm and I'm not sure how to do this.
Hence, my question is how do I bulk create children, assign it to a given parent and add it to the current realm write/save?
Many thanks.
I changed my code to do this;
Read all the factories from JSON
Loop through the factories, creating engines
Link the parent object up.
I'm not sure if I did it right but it seems to be working.
I just don't like how I'm having to hardwire the parent; as I thought Realm/ObjectMapper could do that for me. But its not a major issue as there is only about 3 or 4 relationships.
let factories = Mapper<Factory>().mapArray(JSONArray: json)!
do {
let realm = try Realm()
try realm.write {
for f in factories
{
realm.add(f, update: true)
}
let factories = realm.objects(Factory.self)
print (factories.count) // for debug purposes
for f in factories {
for _ in stride(from: 0, to: f.qty, by: 1) {
let engine : Engine = Engine.init()
engine.parent = f
f.engines.append(engine)
}
}
}
} catch let error as NSError {
print(error.localizedDescription as Any)
}
This above code seems to do the work for me; although I do wish I didn't have to manually set the parent (engine.parent = f)
Anyhow, I've accepted #BogdanFarca's answer.
There is a very nice solution by Jerrot here on Githib Gist
The mapping should be defined in your main model object like this:
func mapping(map: Map) {
title <- map["title"]
products <- (map["products"], ArrayTransform<ProductModel>())
}
The real magic is happening in the ArrayTransform class:
func transformFromJSON(value: AnyObject?) -> List<T>? {
var result = List<T>()
if let tempArr = value as! Array<AnyObject>? {
for entry in tempArr {
let mapper = Mapper<T>()
let model : T = mapper.map(entry)!
result.append(model)
}
}
return result
}